]> git.mxchange.org Git - friendica.git/blob - src/Module/Conversation/Network.php
(Hopefully) SQL improvements
[friendica.git] / src / Module / Conversation / Network.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Module\Conversation;
23
24 use Friendica\BaseModule;
25 use Friendica\Content\BoundariesPager;
26 use Friendica\Content\ForumManager;
27 use Friendica\Content\Nav;
28 use Friendica\Content\Widget;
29 use Friendica\Content\Text\HTML;
30 use Friendica\Core\ACL;
31 use Friendica\Core\Hook;
32 use Friendica\Core\Renderer;
33 use Friendica\Core\Session;
34 use Friendica\Database\DBA;
35 use Friendica\DI;
36 use Friendica\Model\Contact;
37 use Friendica\Model\Group;
38 use Friendica\Model\Item;
39 use Friendica\Model\Post;
40 use Friendica\Model\Profile;
41 use Friendica\Model\User;
42 use Friendica\Model\Verb;
43 use Friendica\Module\Contact as ModuleContact;
44 use Friendica\Module\Security\Login;
45 use Friendica\Protocol\Activity;
46 use Friendica\Util\DateTimeFormat;
47
48 class Network extends BaseModule
49 {
50         /** @var int */
51         private static $groupId;
52         /** @var int */
53         private static $forumContactId;
54         /** @var string */
55         private static $selectedTab;
56         /** @var mixed */
57         private static $min_id;
58         /** @var mixed */
59         private static $max_id;
60         /** @var string */
61         private static $accountTypeString;
62         /** @var int */
63         private static $accountType;
64         /** @var string */
65         private static $network;
66         /** @var int */
67         private static $itemsPerPage;
68         /** @var string */
69         private static $dateFrom;
70         /** @var string */
71         private static $dateTo;
72         /** @var int */
73         private static $star;
74         /** @var int */
75         private static $mention;
76         /** @var string */
77         protected static $order;
78
79         protected function content(array $request = []): string
80         {
81                 if (!local_user()) {
82                         return Login::form();
83                 }
84
85                 $this->parseRequest($_GET);
86
87                 $module = 'network';
88
89                 DI::page()['aside'] .= Widget::accountTypes($module, self::$accountTypeString);
90                 DI::page()['aside'] .= Group::sidebarWidget($module, $module . '/group', 'standard', self::$groupId);
91                 DI::page()['aside'] .= ForumManager::widget($module . '/forum', local_user(), self::$forumContactId);
92                 DI::page()['aside'] .= Widget::postedByYear($module . '/archive', local_user(), false);
93                 DI::page()['aside'] .= Widget::networks($module, !self::$forumContactId ? self::$network : '');
94                 DI::page()['aside'] .= Widget\SavedSearches::getHTML(DI::args()->getQueryString());
95                 DI::page()['aside'] .= Widget::fileAs('filed', '');
96
97                 $arr = ['query' => DI::args()->getQueryString()];
98                 Hook::callAll('network_content_init', $arr);
99
100                 $o = '';
101
102                 // Fetch a page full of parent items for this page
103                 $params = ['limit' => self::$itemsPerPage];
104                 $table = 'network-thread-view';
105
106                 $items = self::getItems($table, $params);
107
108                 if (DI::pConfig()->get(local_user(), 'system', 'infinite_scroll') && ($_GET['mode'] ?? '') != 'minimal') {
109                         $tpl = Renderer::getMarkupTemplate('infinite_scroll_head.tpl');
110                         $o .= Renderer::replaceMacros($tpl, ['$reload_uri' => DI::args()->getQueryString()]);
111                 }
112
113                 if (!(isset($_GET['mode']) AND ($_GET['mode'] == 'raw'))) {
114                         $o .= self::getTabsHTML(self::$selectedTab);
115
116                         Nav::setSelected(DI::args()->get(0));
117
118                         $content = '';
119
120                         if (self::$forumContactId) {
121                                 // If self::$forumContactId belongs to a communitity forum or a privat goup,.add a mention to the status editor
122                                 $condition = ["`id` = ? AND `contact-type` = ?", self::$forumContactId, Contact::TYPE_COMMUNITY];
123                                 $contact = DBA::selectFirst('contact', ['addr'], $condition);
124                                 if (!empty($contact['addr'])) {
125                                         $content = '!' . $contact['addr'];
126                                 }
127                         }
128
129                         $a = DI::app();
130
131                         $default_permissions = [];
132                         if (self::$groupId) {
133                                 $default_permissions['allow_gid'] = [self::$groupId];
134                         }
135
136                         $allowedCids = [];
137                         if (self::$forumContactId) {
138                                 $allowedCids[] = (int) self::$forumContactId;
139                         } elseif (self::$network) {
140                                 $condition = [
141                                         'uid'     => local_user(),
142                                         'network' => self::$network,
143                                         'self'    => false,
144                                         'blocked' => false,
145                                         'pending' => false,
146                                         'archive' => false,
147                                         'rel'     => [Contact::SHARING, Contact::FRIEND],
148                                 ];
149                                 $contactStmt = DBA::select('contact', ['id'], $condition);
150                                 while ($contact = DBA::fetch($contactStmt)) {
151                                         $allowedCids[] = (int) $contact['id'];
152                                 }
153                                 DBA::close($contactStmt);
154                         }
155
156                         if (count($allowedCids)) {
157                                 $default_permissions['allow_cid'] = $allowedCids;
158                         }
159
160                         $x = [
161                                 'lockstate' => self::$groupId || self::$forumContactId || self::$network || ACL::getLockstateForUserId($a->getLoggedInUserId()) ? 'lock' : 'unlock',
162                                 'acl' => ACL::getFullSelectorHTML(DI::page(), $a->getLoggedInUserId(), true, $default_permissions),
163                                 'bang' => ((self::$groupId || self::$forumContactId || self::$network) ? '!' : ''),
164                                 'content' => $content,
165                         ];
166
167                         $o .= DI::conversation()->statusEditor($x);
168                 }
169
170                 if (self::$groupId) {
171                         $group = DBA::selectFirst('group', ['name'], ['id' => self::$groupId, 'uid' => local_user()]);
172                         if (!DBA::isResult($group)) {
173                                 notice(DI::l10n()->t('No such group'));
174                         }
175
176                         $o = Renderer::replaceMacros(Renderer::getMarkupTemplate('section_title.tpl'), [
177                                 '$title' => DI::l10n()->t('Group: %s', $group['name'])
178                         ]) . $o;
179                 } elseif (self::$forumContactId) {
180                         $contact = Contact::getById(self::$forumContactId);
181                         if (DBA::isResult($contact)) {
182                                 $o = Renderer::replaceMacros(Renderer::getMarkupTemplate('viewcontact_template.tpl'), [
183                                         'contacts' => [ModuleContact::getContactTemplateVars($contact)],
184                                         'id' => DI::args()->get(0),
185                                 ]) . $o;
186                         } else {
187                                 notice(DI::l10n()->t('Invalid contact.'));
188                         }
189                 } elseif (!DI::config()->get('theme', 'hide_eventlist')) {
190                         $o .= Profile::getBirthdays();
191                         $o .= Profile::getEventsReminderHTML();
192                 }
193
194                 if (self::$order === 'received') {
195                         $ordering = '`received`';
196                 } elseif (self::$order === 'created') {
197                         $ordering = '`created`';
198                 } else {
199                         $ordering = '`commented`';
200                 }
201
202                 $o .= DI::conversation()->create($items, 'network', false, false, $ordering, local_user());
203
204                 if (DI::pConfig()->get(local_user(), 'system', 'infinite_scroll')) {
205                         $o .= HTML::scrollLoader();
206                 } else {
207                         $pager = new BoundariesPager(
208                                 DI::l10n(),
209                                 DI::args()->getQueryString(),
210                                 $items[0][self::$order] ?? null,
211                                 $items[count($items) - 1][self::$order] ?? null,
212                                 self::$itemsPerPage
213                         );
214
215                         $o .= $pager->renderMinimal(count($items));
216                 }
217
218                 return $o;
219         }
220
221         /**
222          * Sets items as seen
223          *
224          * @param array $condition The array with the SQL condition
225          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
226          */
227         private static function setItemsSeenByCondition(array $condition)
228         {
229                 if (empty($condition)) {
230                         return;
231                 }
232
233                 $unseen = Post::exists($condition);
234
235                 if ($unseen) {
236                         /// @todo handle huge "unseen" updates in the background to avoid timeout errors
237                         Item::update(['unseen' => false], $condition);
238                 }
239         }
240
241         /**
242          * Get the network tabs menu
243          *
244          * @param string $selectedTab
245          * @return string Html of the network tabs
246          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
247          */
248         private static function getTabsHTML(string $selectedTab)
249         {
250                 $cmd = DI::args()->getCommand();
251
252                 // tabs
253                 $tabs = [
254                         [
255                                 'label' => DI::l10n()->t('Latest Activity'),
256                                 'url'   => $cmd . '?' . http_build_query(['order' => 'commented']),
257                                 'sel'   => !$selectedTab || $selectedTab == 'commented' ? 'active' : '',
258                                 'title' => DI::l10n()->t('Sort by latest activity'),
259                                 'id'    => 'activity-order-tab',
260                                 'accesskey' => 'e',
261                         ],
262                         [
263                                 'label' => DI::l10n()->t('Latest Posts'),
264                                 'url'   => $cmd . '?' . http_build_query(['order' => 'received']),
265                                 'sel'   => $selectedTab == 'received' ? 'active' : '',
266                                 'title' => DI::l10n()->t('Sort by post received date'),
267                                 'id'    => 'post-order-tab',
268                                 'accesskey' => 't',
269                         ],
270                         [
271                                 'label' => DI::l10n()->t('Latest Creation'),
272                                 'url'   => $cmd . '?' . http_build_query(['order' => 'created']),
273                                 'sel'   => $selectedTab == 'created' ? 'active' : '',
274                                 'title' => DI::l10n()->t('Sort by post creation date'),
275                                 'id'    => 'creation-order-tab',
276                                 'accesskey' => 'q',
277                         ],
278                         [
279                                 'label' => DI::l10n()->t('Personal'),
280                                 'url'   => $cmd . '?' . http_build_query(['mention' => true]),
281                                 'sel'   => $selectedTab == 'mention' ? 'active' : '',
282                                 'title' => DI::l10n()->t('Posts that mention or involve you'),
283                                 'id'    => 'personal-tab',
284                                 'accesskey' => 'r',
285                         ],
286                         [
287                                 'label' => DI::l10n()->t('Starred'),
288                                 'url'   => $cmd . '?' . http_build_query(['star' => true]),
289                                 'sel'   => $selectedTab == 'star' ? 'active' : '',
290                                 'title' => DI::l10n()->t('Favourite Posts'),
291                                 'id'    => 'starred-posts-tab',
292                                 'accesskey' => 'm',
293                         ],
294                 ];
295
296                 $arr = ['tabs' => $tabs];
297                 Hook::callAll('network_tabs', $arr);
298
299                 $tpl = Renderer::getMarkupTemplate('common_tabs.tpl');
300
301                 return Renderer::replaceMacros($tpl, ['$tabs' => $arr['tabs']]);
302         }
303
304         protected function parseRequest(array $get)
305         {
306                 self::$groupId = $this->parameters['group_id'] ?? 0;
307
308                 self::$forumContactId = $this->parameters['contact_id'] ?? 0;
309
310                 self::$selectedTab = Session::get('network-tab', DI::pConfig()->get(local_user(), 'network.view', 'selected_tab', ''));
311
312                 if (!empty($get['star'])) {
313                         self::$selectedTab = 'star';
314                         self::$star = true;
315                 } else {
316                         self::$star = self::$selectedTab == 'star';
317                 }
318
319                 if (!empty($get['mention'])) {
320                         self::$selectedTab = 'mention';
321                         self::$mention = true;
322                 } else {
323                         self::$mention = self::$selectedTab == 'mention';
324                 }
325
326                 if (!empty($get['order'])) {
327                         self::$selectedTab = $get['order'];
328                         self::$order = $get['order'];
329                         self::$star = false;
330                         self::$mention = false;
331                 } elseif (in_array(self::$selectedTab, ['received', 'star'])) {
332                         self::$order = 'received';
333                 } elseif (self::$selectedTab == 'created') {
334                         self::$order = 'created';
335                 } else {
336                         self::$order = 'commented';
337                 }
338
339                 self::$selectedTab = self::$selectedTab ?? self::$order;
340
341                 // Prohibit combined usage of "star" and "mention"
342                 if (self::$selectedTab == 'star') {
343                         self::$mention = false;
344                 } elseif (self::$selectedTab == 'mention') {
345                         self::$star = false;
346                 }
347
348                 Session::set('network-tab', self::$selectedTab);
349                 DI::pConfig()->set(local_user(), 'network.view', 'selected_tab', self::$selectedTab);
350
351                 self::$accountTypeString = $get['accounttype'] ?? $this->parameters['accounttype'] ?? '';
352                 self::$accountType = User::getAccountTypeByString(self::$accountTypeString);
353
354                 self::$network = $get['nets'] ?? '';
355
356                 self::$dateFrom = $this->parameters['from'] ?? '';
357                 self::$dateTo = $this->parameters['to'] ?? '';
358
359                 if (DI::mode()->isMobile()) {
360                         self::$itemsPerPage = DI::pConfig()->get(local_user(), 'system', 'itemspage_mobile_network',
361                                 DI::config()->get('system', 'itemspage_network_mobile'));
362                 } else {
363                         self::$itemsPerPage = DI::pConfig()->get(local_user(), 'system', 'itemspage_network',
364                                 DI::config()->get('system', 'itemspage_network'));
365                 }
366
367                 self::$min_id = $get['min_id'] ?? null;
368                 self::$max_id = $get['max_id'] ?? null;
369
370                 switch (self::$order) {
371                         case 'received':
372                                 self::$max_id = $get['last_received'] ?? self::$max_id;
373                                 break;
374                         case 'created':
375                                 self::$max_id = $get['last_created'] ?? self::$max_id;
376                                 break;
377                         case 'uriid':
378                                 self::$max_id = $get['last_uriid'] ?? self::$max_id;
379                                 break;
380                         default:
381                                 self::$order = 'commented';
382                                 self::$max_id = $get['last_commented'] ?? self::$max_id;
383                 }
384         }
385
386         protected static function getItems(string $table, array $params, array $conditionFields = [])
387         {
388                 $conditionFields['uid'] = local_user();
389                 $conditionStrings = [];
390
391                 if (!is_null(self::$accountType)) {
392                         $conditionFields['contact-type'] = self::$accountType;
393                 }
394
395                 if (self::$star) {
396                         $conditionFields['starred'] = true;
397                 }
398                 if (self::$mention) {
399                         $conditionFields['mention'] = true;
400                 }
401                 if (self::$network) {
402                         $conditionFields['network'] = self::$network;
403                 }
404
405                 if (self::$dateFrom) {
406                         $conditionStrings = DBA::mergeConditions($conditionStrings, ["`received` <= ? ", DateTimeFormat::convert(self::$dateFrom, 'UTC', DI::app()->getTimeZone())]);
407                 }
408                 if (self::$dateTo) {
409                         $conditionStrings = DBA::mergeConditions($conditionStrings, ["`received` >= ? ", DateTimeFormat::convert(self::$dateTo, 'UTC', DI::app()->getTimeZone())]);
410                 }
411
412                 if (self::$groupId) {
413                         $conditionStrings = DBA::mergeConditions($conditionStrings, ["`contact-id` IN (SELECT `contact-id` FROM `group_member` WHERE `gid` = ?)", self::$groupId]);
414                 } elseif (self::$forumContactId) {
415                         $conditionStrings = DBA::mergeConditions($conditionStrings, 
416                                 ["((`contact-id` = ?) OR `uri-id` IN (SELECT `parent-uri-id` FROM `post-user-view` WHERE (`contact-id` = ? AND `gravity` = ? AND `vid` = ? AND `uid` = ?)))",
417                                 self::$forumContactId, self::$forumContactId, GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE), local_user()]);
418                 }
419
420                 // Currently only the order modes "received" and "commented" are in use
421                 if (isset(self::$max_id)) {
422                         switch (self::$order) {
423                                 case 'received':
424                                         $conditionStrings = DBA::mergeConditions($conditionStrings, ["`received` < ?", self::$max_id]);
425                                         break;
426                                 case 'commented':
427                                         $conditionStrings = DBA::mergeConditions($conditionStrings, ["`commented` < ?", self::$max_id]);
428                                         break;
429                                 case 'created':
430                                         $conditionStrings = DBA::mergeConditions($conditionStrings, ["`created` < ?", self::$max_id]);
431                                         break;
432                                 case 'uriid':
433                                         $conditionStrings = DBA::mergeConditions($conditionStrings, ["`uri-id` < ?", self::$max_id]);
434                                         break;
435                         }
436                 }
437
438                 if (isset(self::$min_id)) {
439                         switch (self::$order) {
440                                 case 'received':
441                                         $conditionStrings = DBA::mergeConditions($conditionStrings, ["`received` > ?", self::$min_id]);
442                                         break;
443                                 case 'commented':
444                                         $conditionStrings = DBA::mergeConditions($conditionStrings, ["`commented` > ?", self::$min_id]);
445                                         break;
446                                 case 'created':
447                                         $conditionStrings = DBA::mergeConditions($conditionStrings, ["`created` > ?", self::$min_id]);
448                                         break;
449                                 case 'uriid':
450                                         $conditionStrings = DBA::mergeConditions($conditionStrings, ["`uri-id` > ?", self::$min_id]);
451                                         break;
452                         }
453                 }
454
455                 if (isset(self::$min_id) && !isset(self::$max_id)) {
456                         // min_id quirk: querying in reverse order with min_id gets the most recent rows, regardless of how close
457                         // they are to min_id. We change the query ordering to get the expected data, and we need to reverse the
458                         // order of the results.
459                         $params['order'] = [self::$order => false];
460                 } else {
461                         $params['order'] = [self::$order => true];
462                 }
463
464                 $items = DBA::selectToArray($table, [], DBA::mergeConditions($conditionFields, $conditionStrings), $params);
465
466                 // min_id quirk, continued
467                 if (isset(self::$min_id) && !isset(self::$max_id)) {
468                         $items = array_reverse($items);
469                 }
470
471                 if (DBA::isResult($items)) {
472                         $parents = array_column($items, 'parent-uri-id');
473                 } else {
474                         $parents = [];
475                 }
476
477                 // We aren't going to try and figure out at the item, group, and page
478                 // level which items you've seen and which you haven't. If you're looking
479                 // at the top level network page just mark everything seen.
480                 if (!self::$groupId && !self::$forumContactId && !self::$star && !self::$mention) {
481                         $condition = ['unseen' => true, 'uid' => local_user()];
482                         self::setItemsSeenByCondition($condition);
483                 } elseif (!empty($parents)) {
484                         $condition = ['unseen' => true, 'uid' => local_user(), 'parent-uri-id' => $parents];
485                         self::setItemsSeenByCondition($condition);
486                 }
487
488                 return $items;
489         }
490 }