]> git.mxchange.org Git - friendica.git/blob - include/conversation.php
Remove pager parameter from conversation()
[friendica.git] / include / conversation.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2020, Friendica
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 use Friendica\App;
23 use Friendica\Content\ContactSelector;
24 use Friendica\Content\Feature;
25 use Friendica\Content\Pager;
26 use Friendica\Content\Text\BBCode;
27 use Friendica\Core\Hook;
28 use Friendica\Core\Logger;
29 use Friendica\Core\Protocol;
30 use Friendica\Core\Renderer;
31 use Friendica\Core\Session;
32 use Friendica\Database\DBA;
33 use Friendica\DI;
34 use Friendica\Model\Contact;
35 use Friendica\Model\Item;
36 use Friendica\Model\Profile;
37 use Friendica\Model\Term;
38 use Friendica\Object\Post;
39 use Friendica\Object\Thread;
40 use Friendica\Protocol\Activity;
41 use Friendica\Util\Crypto;
42 use Friendica\Util\DateTimeFormat;
43 use Friendica\Util\Proxy as ProxyUtils;
44 use Friendica\Util\Strings;
45 use Friendica\Util\Temporal;
46 use Friendica\Util\XML;
47
48 function item_extract_images($body) {
49
50         $saved_image = [];
51         $orig_body = $body;
52         $new_body = '';
53
54         $cnt = 0;
55         $img_start = strpos($orig_body, '[img');
56         $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
57         $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
58         while (($img_st_close !== false) && ($img_end !== false)) {
59
60                 $img_st_close++; // make it point to AFTER the closing bracket
61                 $img_end += $img_start;
62
63                 if (!strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
64                         // This is an embedded image
65
66                         $saved_image[$cnt] = substr($orig_body, $img_start + $img_st_close, $img_end - ($img_start + $img_st_close));
67                         $new_body = $new_body . substr($orig_body, 0, $img_start) . '[!#saved_image' . $cnt . '#!]';
68
69                         $cnt++;
70                 } else {
71                         $new_body = $new_body . substr($orig_body, 0, $img_end + strlen('[/img]'));
72                 }
73
74                 $orig_body = substr($orig_body, $img_end + strlen('[/img]'));
75
76                 if ($orig_body === false) {
77                         // in case the body ends on a closing image tag
78                         $orig_body = '';
79                 }
80
81                 $img_start = strpos($orig_body, '[img');
82                 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
83                 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
84         }
85
86         $new_body = $new_body . $orig_body;
87
88         return ['body' => $new_body, 'images' => $saved_image];
89 }
90
91 function item_redir_and_replace_images($body, $images, $cid) {
92
93         $origbody = $body;
94         $newbody = '';
95
96         $cnt = 1;
97         $pos = BBCode::getTagPosition($origbody, 'url', 0);
98         while ($pos !== false && $cnt < 1000) {
99
100                 $search = '/\[url\=(.*?)\]\[!#saved_image([0-9]*)#!\]\[\/url\]' . '/is';
101                 $replace = '[url=' . DI::baseUrl() . '/redir/' . $cid
102                                    . '?url=' . '$1' . '][!#saved_image' . '$2' .'#!][/url]';
103
104                 $newbody .= substr($origbody, 0, $pos['start']['open']);
105                 $subject = substr($origbody, $pos['start']['open'], $pos['end']['close'] - $pos['start']['open']);
106                 $origbody = substr($origbody, $pos['end']['close']);
107                 if ($origbody === false) {
108                         $origbody = '';
109                 }
110
111                 $subject = preg_replace($search, $replace, $subject);
112                 $newbody .= $subject;
113
114                 $cnt++;
115                 // Isn't this supposed to use $cnt value for $occurrences? - @MrPetovan
116                 $pos = BBCode::getTagPosition($origbody, 'url', 0);
117         }
118         $newbody .= $origbody;
119
120         $cnt = 0;
121         foreach ($images as $image) {
122                 /*
123                  * We're depending on the property of 'foreach' (specified on the PHP website) that
124                  * it loops over the array starting from the first element and going sequentially
125                  * to the last element.
126                  */
127                 $newbody = str_replace('[!#saved_image' . $cnt . '#!]', '[img]' . $image . '[/img]', $newbody);
128                 $cnt++;
129         }
130         return $newbody;
131 }
132
133 /**
134  * Render actions localized
135  *
136  * @param $item
137  * @throws ImagickException
138  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
139  */
140 function localize_item(&$item)
141 {
142         $extracted = item_extract_images($item['body']);
143         if ($extracted['images']) {
144                 $item['body'] = item_redir_and_replace_images($extracted['body'], $extracted['images'], $item['contact-id']);
145         }
146
147         /*
148         heluecht 2018-06-19: from my point of view this whole code part is useless.
149         It just renders the body message of technical posts (Like, dislike, ...).
150         But: The body isn't visible at all. So we do this stuff just because we can.
151         Even if these messages were visible, this would only mean that something went wrong.
152         During the further steps of the database restructuring I would like to address this issue.
153         */
154
155         $activity = DI::activity();
156
157         $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
158         if ($activity->match($item['verb'], Activity::LIKE)
159                 || $activity->match($item['verb'], Activity::DISLIKE)
160                 || $activity->match($item['verb'], Activity::ATTEND)
161                 || $activity->match($item['verb'], Activity::ATTENDNO)
162                 || $activity->match($item['verb'], Activity::ATTENDMAYBE)) {
163
164                 $fields = ['author-link', 'author-name', 'verb', 'object-type', 'resource-id', 'body', 'plink'];
165                 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
166                 if (!DBA::isResult($obj)) {
167                         return;
168                 }
169
170                 $author  = '[url=' . $item['author-link'] . ']' . $item['author-name'] . '[/url]';
171                 $objauthor =  '[url=' . $obj['author-link'] . ']' . $obj['author-name'] . '[/url]';
172
173                 switch ($obj['verb']) {
174                         case Activity::POST:
175                                 switch ($obj['object-type']) {
176                                         case Activity\ObjectType::EVENT:
177                                                 $post_type = DI::l10n()->t('event');
178                                                 break;
179                                         default:
180                                                 $post_type = DI::l10n()->t('status');
181                                 }
182                                 break;
183                         default:
184                                 if ($obj['resource-id']) {
185                                         $post_type = DI::l10n()->t('photo');
186                                         $m = [];
187                                         preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
188                                         $rr['plink'] = $m[1];
189                                 } else {
190                                         $post_type = DI::l10n()->t('status');
191                                 }
192                 }
193
194                 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
195
196                 $bodyverb = '';
197                 if ($activity->match($item['verb'], Activity::LIKE)) {
198                         $bodyverb = DI::l10n()->t('%1$s likes %2$s\'s %3$s');
199                 } elseif ($activity->match($item['verb'], Activity::DISLIKE)) {
200                         $bodyverb = DI::l10n()->t('%1$s doesn\'t like %2$s\'s %3$s');
201                 } elseif ($activity->match($item['verb'], Activity::ATTEND)) {
202                         $bodyverb = DI::l10n()->t('%1$s attends %2$s\'s %3$s');
203                 } elseif ($activity->match($item['verb'], Activity::ATTENDNO)) {
204                         $bodyverb = DI::l10n()->t('%1$s doesn\'t attend %2$s\'s %3$s');
205                 } elseif ($activity->match($item['verb'], Activity::ATTENDMAYBE)) {
206                         $bodyverb = DI::l10n()->t('%1$s attends maybe %2$s\'s %3$s');
207                 }
208
209                 $item['body'] = sprintf($bodyverb, $author, $objauthor, $plink);
210         }
211
212         if ($activity->match($item['verb'], Activity::FRIEND)) {
213
214                 if ($item['object-type']=="" || $item['object-type']!== Activity\ObjectType::PERSON) return;
215
216                 $Aname = $item['author-name'];
217                 $Alink = $item['author-link'];
218
219                 $xmlhead="<"."?xml version='1.0' encoding='UTF-8' ?".">";
220
221                 $obj = XML::parseString($xmlhead.$item['object']);
222                 $links = XML::parseString($xmlhead."<links>".XML::unescape($obj->link)."</links>");
223
224                 $Bname = $obj->title;
225                 $Blink = "";
226                 $Bphoto = "";
227                 foreach ($links->link as $l) {
228                         $atts = $l->attributes();
229                         switch ($atts['rel']) {
230                                 case "alternate": $Blink = $atts['href']; break;
231                                 case "photo": $Bphoto = $atts['href']; break;
232                         }
233                 }
234
235                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
236                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
237                 if ($Bphoto != "") {
238                         $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img]' . $Bphoto . '[/img][/url]';
239                 }
240
241                 $item['body'] = DI::l10n()->t('%1$s is now friends with %2$s', $A, $B)."\n\n\n".$Bphoto;
242
243         }
244         if (stristr($item['verb'], Activity::POKE)) {
245                 $verb = urldecode(substr($item['verb'],strpos($item['verb'],'#')+1));
246                 if (!$verb) {
247                         return;
248                 }
249                 if ($item['object-type']=="" || $item['object-type']!== Activity\ObjectType::PERSON) {
250                         return;
251                 }
252
253                 $Aname = $item['author-name'];
254                 $Alink = $item['author-link'];
255
256                 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
257
258                 $obj = XML::parseString($xmlhead.$item['object']);
259
260                 $Bname = $obj->title;
261                 $Blink = $obj->id;
262                 $Bphoto = "";
263
264                 foreach ($obj->link as $l) {
265                         $atts = $l->attributes();
266                         switch ($atts['rel']) {
267                                 case "alternate": $Blink = $atts['href'];
268                                 case "photo": $Bphoto = $atts['href'];
269                         }
270                 }
271
272                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
273                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
274                 if ($Bphoto != "") {
275                         $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img=80x80]' . $Bphoto . '[/img][/url]';
276                 }
277
278                 /*
279                  * we can't have a translation string with three positions but no distinguishable text
280                  * So here is the translate string.
281                  */
282                 $txt = DI::l10n()->t('%1$s poked %2$s');
283
284                 // now translate the verb
285                 $poked_t = trim(sprintf($txt, "", ""));
286                 $txt = str_replace($poked_t, DI::l10n()->t($verb), $txt);
287
288                 // then do the sprintf on the translation string
289
290                 $item['body'] = sprintf($txt, $A, $B). "\n\n\n" . $Bphoto;
291
292         }
293
294         if ($activity->match($item['verb'],  Activity::TAG)) {
295                 $fields = ['author-id', 'author-link', 'author-name', 'author-network',
296                         'verb', 'object-type', 'resource-id', 'body', 'plink'];
297                 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
298                 if (!DBA::isResult($obj)) {
299                         return;
300                 }
301
302                 $author_arr = ['uid' => 0, 'id' => $item['author-id'],
303                         'network' => $item['author-network'], 'url' => $item['author-link']];
304                 $author  = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $item['author-name'] . '[/url]';
305
306                 $author_arr = ['uid' => 0, 'id' => $obj['author-id'],
307                         'network' => $obj['author-network'], 'url' => $obj['author-link']];
308                 $objauthor  = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $obj['author-name'] . '[/url]';
309
310                 switch ($obj['verb']) {
311                         case Activity::POST:
312                                 switch ($obj['object-type']) {
313                                         case Activity\ObjectType::EVENT:
314                                                 $post_type = DI::l10n()->t('event');
315                                                 break;
316                                         default:
317                                                 $post_type = DI::l10n()->t('status');
318                                 }
319                                 break;
320                         default:
321                                 if ($obj['resource-id']) {
322                                         $post_type = DI::l10n()->t('photo');
323                                         $m=[]; preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
324                                         $rr['plink'] = $m[1];
325                                 } else {
326                                         $post_type = DI::l10n()->t('status');
327                                 }
328                                 // Let's break everthing ... ;-)
329                                 break;
330                 }
331                 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
332
333                 $parsedobj = XML::parseString($xmlhead.$item['object']);
334
335                 $tag = sprintf('#[url=%s]%s[/url]', $parsedobj->id, $parsedobj->content);
336                 $item['body'] = DI::l10n()->t('%1$s tagged %2$s\'s %3$s with %4$s', $author, $objauthor, $plink, $tag);
337         }
338
339         if ($activity->match($item['verb'], Activity::FAVORITE)) {
340                 if ($item['object-type'] == "") {
341                         return;
342                 }
343
344                 $Aname = $item['author-name'];
345                 $Alink = $item['author-link'];
346
347                 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
348
349                 $obj = XML::parseString($xmlhead.$item['object']);
350                 if (strlen($obj->id)) {
351                         $fields = ['author-link', 'author-name', 'plink'];
352                         $target = Item::selectFirst($fields, ['uri' => $obj->id, 'uid' => $item['uid']]);
353                         if (DBA::isResult($target) && $target['plink']) {
354                                 $Bname = $target['author-name'];
355                                 $Blink = $target['author-link'];
356                                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
357                                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
358                                 $P = '[url=' . $target['plink'] . ']' . DI::l10n()->t('post/item') . '[/url]';
359                                 $item['body'] = DI::l10n()->t('%1$s marked %2$s\'s %3$s as favorite', $A, $B, $P)."\n";
360                         }
361                 }
362         }
363         $matches = null;
364         if (preg_match_all('/@\[url=(.*?)\]/is', $item['body'], $matches, PREG_SET_ORDER)) {
365                 foreach ($matches as $mtch) {
366                         if (!strpos($mtch[1], 'zrl=')) {
367                                 $item['body'] = str_replace($mtch[0], '@[url=' . Contact::magicLink($mtch[1]) . ']', $item['body']);
368                         }
369                 }
370         }
371
372         // add zrl's to public images
373         $photo_pattern = "/\[url=(.*?)\/photos\/(.*?)\/image\/(.*?)\]\[img(.*?)\]h(.*?)\[\/img\]\[\/url\]/is";
374         if (preg_match($photo_pattern, $item['body'])) {
375                 $photo_replace = '[url=' . Profile::zrl('$1' . '/photos/' . '$2' . '/image/' . '$3' ,true) . '][img' . '$4' . ']h' . '$5'  . '[/img][/url]';
376                 $item['body'] = BBCode::pregReplaceInTag($photo_pattern, $photo_replace, 'url', $item['body']);
377         }
378
379         // add sparkle links to appropriate permalinks
380         $author = ['uid' => 0, 'id' => $item['author-id'],
381                 'network' => $item['author-network'], 'url' => $item['author-link']];
382
383         // Only create a redirection to a magic link when logged in
384         if (!empty($item['plink']) && Session::isAuthenticated()) {
385                 $item['plink'] = Contact::magicLinkByContact($author, $item['plink']);
386         }
387 }
388
389 /**
390  * Count the total of comments on this item and its desendants
391  * @TODO proper type-hint + doc-tag
392  * @param $item
393  * @return int
394  */
395 function count_descendants($item) {
396         $total = count($item['children']);
397
398         if ($total > 0) {
399                 foreach ($item['children'] as $child) {
400                         if (!visible_activity($child)) {
401                                 $total --;
402                         }
403                         $total += count_descendants($child);
404                 }
405         }
406
407         return $total;
408 }
409
410 function visible_activity($item) {
411
412         $activity = DI::activity();
413
414         if (empty($item['verb']) || $activity->isHidden($item['verb'])) {
415                 return false;
416         }
417
418         // @TODO below if() block can be rewritten to a single line: $isVisible = allConditionsHere;
419         if ($activity->match($item['verb'], Activity::FOLLOW) &&
420             $item['object-type'] === Activity\ObjectType::NOTE &&
421             empty($item['self']) &&
422             $item['uid'] == local_user()) {
423                 return false;
424         }
425
426         return true;
427 }
428
429 function conv_get_blocklist()
430 {
431         if (!local_user()) {
432                 return [];
433         }
434
435         $str_blocked = DI::pConfig()->get(local_user(), 'system', 'blocked');
436         if (empty($str_blocked)) {
437                 return [];
438         }
439
440         $blocklist = [];
441
442         foreach (explode(',', $str_blocked) as $entry) {
443                 // The 4th parameter guarantees that there always will be a public contact entry
444                 $cid = Contact::getIdForURL(trim($entry), 0, true, ['url' => trim($entry)]);
445                 if (!empty($cid)) {
446                         $blocklist[] = $cid;
447                 }
448         }
449
450         return $blocklist;
451 }
452
453 /**
454  * "Render" a conversation or list of items for HTML display.
455  * There are two major forms of display:
456  *      - Sequential or unthreaded ("New Item View" or search results)
457  *      - conversation view
458  * The $mode parameter decides between the various renderings and also
459  * figures out how to determine page owner and other contextual items
460  * that are based on unique features of the calling module.
461  * @param App    $a
462  * @param array  $items
463  * @param        $mode
464  * @param        $update
465  * @param bool   $preview
466  * @param string $order
467  * @param int    $uid
468  * @return string
469  * @throws ImagickException
470  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
471  */
472 function conversation(App $a, array $items, $mode, $update, $preview = false, $order = 'commented', $uid = 0)
473 {
474         $ssl_state = (local_user() ? true : false);
475
476         $profile_owner = 0;
477         $live_update_div = '';
478
479         $blocklist = conv_get_blocklist();
480
481         $previewing = (($preview) ? ' preview ' : '');
482
483         if ($mode === 'network') {
484                 $items = conversation_add_children($items, false, $order, $uid);
485                 $profile_owner = local_user();
486                 if (!$update) {
487                         /*
488                          * The special div is needed for liveUpdate to kick in for this page.
489                          * We only launch liveUpdate if you aren't filtering in some incompatible
490                          * way and also you aren't writing a comment (discovered in javascript).
491                          */
492                         $live_update_div = '<div id="live-network"></div>' . "\r\n"
493                                 . "<script> var profile_uid = " . $_SESSION['uid']
494                                 . "; var netargs = '" . substr(DI::args()->getCommand(), 8)
495                                 . '?f='
496                                 . (!empty($_GET['cid'])    ? '&cid='    . rawurlencode($_GET['cid'])    : '')
497                                 . (!empty($_GET['search']) ? '&search=' . rawurlencode($_GET['search']) : '')
498                                 . (!empty($_GET['star'])   ? '&star='   . rawurlencode($_GET['star'])   : '')
499                                 . (!empty($_GET['order'])  ? '&order='  . rawurlencode($_GET['order'])  : '')
500                                 . (!empty($_GET['bmark'])  ? '&bmark='  . rawurlencode($_GET['bmark'])  : '')
501                                 . (!empty($_GET['liked'])  ? '&liked='  . rawurlencode($_GET['liked'])  : '')
502                                 . (!empty($_GET['conv'])   ? '&conv='   . rawurlencode($_GET['conv'])   : '')
503                                 . (!empty($_GET['nets'])   ? '&nets='   . rawurlencode($_GET['nets'])   : '')
504                                 . (!empty($_GET['cmin'])   ? '&cmin='   . rawurlencode($_GET['cmin'])   : '')
505                                 . (!empty($_GET['cmax'])   ? '&cmax='   . rawurlencode($_GET['cmax'])   : '')
506                                 . (!empty($_GET['file'])   ? '&file='   . rawurlencode($_GET['file'])   : '')
507
508                                 . "'; </script>\r\n";
509                 }
510         } elseif ($mode === 'profile') {
511                 $items = conversation_add_children($items, false, $order, $uid);
512                 $profile_owner = $a->profile['uid'];
513
514                 if (!$update) {
515                         $tab = 'posts';
516                         if (!empty($_GET['tab'])) {
517                                 $tab = Strings::escapeTags(trim($_GET['tab']));
518                         }
519                         if ($tab === 'posts') {
520                                 /*
521                                  * This is ugly, but we can't pass the profile_uid through the session to the ajax updater,
522                                  * because browser prefetching might change it on us. We have to deliver it with the page.
523                                  */
524
525                                 $live_update_div = '<div id="live-profile"></div>' . "\r\n"
526                                         . "<script> var profile_uid = " . $a->profile['uid']
527                                         . "; var netargs = '?f='; </script>\r\n";
528                         }
529                 }
530         } elseif ($mode === 'notes') {
531                 $items = conversation_add_children($items, false, $order, local_user());
532                 $profile_owner = local_user();
533
534                 if (!$update) {
535                         $live_update_div = '<div id="live-notes"></div>' . "\r\n"
536                                 . "<script> var profile_uid = " . local_user()
537                                 . "; var netargs = '/?f='; </script>\r\n";
538                 }
539         } elseif ($mode === 'display') {
540                 $items = conversation_add_children($items, false, $order, $uid);
541                 $profile_owner = $a->profile['uid'];
542
543                 if (!$update) {
544                         $live_update_div = '<div id="live-display"></div>' . "\r\n"
545                                 . "<script> var profile_uid = " . Session::get('uid', 0) . ";"
546                                 . "</script>";
547                 }
548         } elseif ($mode === 'community') {
549                 $items = conversation_add_children($items, true, $order, $uid);
550                 $profile_owner = 0;
551
552                 if (!$update) {
553                         $live_update_div = '<div id="live-community"></div>' . "\r\n"
554                                 . "<script> var profile_uid = -1; var netargs = '" . substr(DI::args()->getCommand(), 10)
555                                 ."/?f='; </script>\r\n";
556                 }
557         } elseif ($mode === 'contacts') {
558                 $items = conversation_add_children($items, false, $order, $uid);
559                 $profile_owner = 0;
560
561                 if (!$update) {
562                         $live_update_div = '<div id="live-contacts"></div>' . "\r\n"
563                                 . "<script> var profile_uid = -1; var netargs = '" . substr(DI::args()->getCommand(), 9)
564                                 ."/?f='; </script>\r\n";
565                 }
566         } elseif ($mode === 'search') {
567                 $live_update_div = '<div id="live-search"></div>' . "\r\n";
568         }
569
570         $page_dropping = ((local_user() && local_user() == $profile_owner) ? true : false);
571
572         if (!$update) {
573                 $_SESSION['return_path'] = DI::args()->getQueryString();
574         }
575
576         $cb = ['items' => $items, 'mode' => $mode, 'update' => $update, 'preview' => $preview];
577         Hook::callAll('conversation_start',$cb);
578
579         $items = $cb['items'];
580
581         $conv_responses = [
582                 'like' => ['title' => DI::l10n()->t('Likes','title')],
583                 'dislike' => ['title' => DI::l10n()->t('Dislikes','title')],
584                 'attendyes' => ['title' => DI::l10n()->t('Attending','title')],
585                 'attendno' => ['title' => DI::l10n()->t('Not attending','title')],
586                 'attendmaybe' => ['title' => DI::l10n()->t('Might attend','title')],
587                 'announce' => ['title' => DI::l10n()->t('Reshares','title')]
588         ];
589
590         // array with html for each thread (parent+comments)
591         $threads = [];
592         $threadsid = -1;
593
594         $page_template = Renderer::getMarkupTemplate("conversation.tpl");
595
596         if (!empty($items)) {
597                 if (in_array($mode, ['community', 'contacts'])) {
598                         $writable = true;
599                 } else {
600                         $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED);
601                 }
602
603                 if (!local_user()) {
604                         $writable = false;
605                 }
606
607                 if (in_array($mode, ['network-new', 'search', 'contact-posts'])) {
608
609                         /*
610                          * "New Item View" on network page or search page results
611                          * - just loop through the items and format them minimally for display
612                          */
613
614                         $tpl = 'search_item.tpl';
615
616                         foreach ($items as $item) {
617
618                                 if (!visible_activity($item)) {
619                                         continue;
620                                 }
621
622                                 if (in_array($item['author-id'], $blocklist)) {
623                                         continue;
624                                 }
625
626                                 $threadsid++;
627
628                                 $owner_url   = '';
629                                 $owner_name  = '';
630                                 $sparkle     = '';
631
632                                 // prevent private email from leaking.
633                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
634                                         continue;
635                                 }
636
637                                 $profile_name = $item['author-name'];
638                                 if (!empty($item['author-link']) && empty($item['author-name'])) {
639                                         $profile_name = $item['author-link'];
640                                 }
641
642                                 $tags = Term::populateTagsFromItem($item);
643
644                                 $author = ['uid' => 0, 'id' => $item['author-id'],
645                                         'network' => $item['author-network'], 'url' => $item['author-link']];
646                                 $profile_link = Contact::magicLinkByContact($author);
647
648                                 if (strpos($profile_link, 'redir/') === 0) {
649                                         $sparkle = ' sparkle';
650                                 }
651
652                                 $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
653                                 Hook::callAll('render_location',$locate);
654
655                                 $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
656
657                                 localize_item($item);
658                                 if ($mode === 'network-new') {
659                                         $dropping = true;
660                                 } else {
661                                         $dropping = false;
662                                 }
663
664                                 $drop = [
665                                         'dropping' => $dropping,
666                                         'pagedrop' => $page_dropping,
667                                         'select' => DI::l10n()->t('Select'),
668                                         'delete' => DI::l10n()->t('Delete'),
669                                 ];
670
671                                 $star = false;
672                                 $isstarred = "unstarred";
673
674                                 $lock = false;
675                                 $likebuttons = false;
676
677                                 $body = Item::prepareBody($item, true, $preview);
678
679                                 list($categories, $folders) = DI::contentItem()->determineCategoriesTerms($item);
680
681                                 if (!empty($item['content-warning']) && DI::pConfig()->get(local_user(), 'system', 'disable_cw', false)) {
682                                         $title = ucfirst($item['content-warning']);
683                                 } else {
684                                         $title = $item['title'];
685                                 }
686
687                                 $tmp_item = [
688                                         'template' => $tpl,
689                                         'id' => ($preview ? 'P0' : $item['id']),
690                                         'guid' => ($preview ? 'Q0' : $item['guid']),
691                                         'network' => $item['network'],
692                                         'network_name' => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']),
693                                         'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link']),
694                                         'linktitle' => DI::l10n()->t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
695                                         'profile_url' => $profile_link,
696                                         'item_photo_menu' => item_photo_menu($item),
697                                         'name' => $profile_name,
698                                         'sparkle' => $sparkle,
699                                         'lock' => $lock,
700                                         'thumb' => DI::baseUrl()->remove(ProxyUtils::proxifyUrl($item['author-avatar'], false, ProxyUtils::SIZE_THUMB)),
701                                         'title' => $title,
702                                         'body' => $body,
703                                         'tags' => $tags['tags'],
704                                         'hashtags' => $tags['hashtags'],
705                                         'mentions' => $tags['mentions'],
706                                         'implicit_mentions' => $tags['implicit_mentions'],
707                                         'txt_cats' => DI::l10n()->t('Categories:'),
708                                         'txt_folders' => DI::l10n()->t('Filed under:'),
709                                         'has_cats' => ((count($categories)) ? 'true' : ''),
710                                         'has_folders' => ((count($folders)) ? 'true' : ''),
711                                         'categories' => $categories,
712                                         'folders' => $folders,
713                                         'text' => strip_tags($body),
714                                         'localtime' => DateTimeFormat::local($item['created'], 'r'),
715                                         'ago' => (($item['app']) ? DI::l10n()->t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
716                                         'location' => $location,
717                                         'indent' => '',
718                                         'owner_name' => $owner_name,
719                                         'owner_url' => $owner_url,
720                                         'owner_photo' => DI::baseUrl()->remove(ProxyUtils::proxifyUrl($item['owner-avatar'], false, ProxyUtils::SIZE_THUMB)),
721                                         'plink' => Item::getPlink($item),
722                                         'edpost' => false,
723                                         'isstarred' => $isstarred,
724                                         'star' => $star,
725                                         'drop' => $drop,
726                                         'vote' => $likebuttons,
727                                         'like' => '',
728                                         'dislike' => '',
729                                         'comment' => '',
730                                         'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> DI::l10n()->t('View in context')]),
731                                         'previewing' => $previewing,
732                                         'wait' => DI::l10n()->t('Please wait'),
733                                         'thread_level' => 1,
734                                 ];
735
736                                 $arr = ['item' => $item, 'output' => $tmp_item];
737                                 Hook::callAll('display_item', $arr);
738
739                                 $threads[$threadsid]['id'] = $item['id'];
740                                 $threads[$threadsid]['network'] = $item['network'];
741                                 $threads[$threadsid]['items'] = [$arr['output']];
742
743                         }
744                 } else {
745                         // Normal View
746                         $page_template = Renderer::getMarkupTemplate("threaded_conversation.tpl");
747
748                         $conv = new Thread($mode, $preview, $writable);
749
750                         /*
751                          * get all the topmost parents
752                          * this shouldn't be needed, as we should have only them in our array
753                          * But for now, this array respects the old style, just in case
754                          */
755                         foreach ($items as $item) {
756                                 if (in_array($item['author-id'], $blocklist)) {
757                                         continue;
758                                 }
759
760                                 // Can we put this after the visibility check?
761                                 builtin_activity_puller($item, $conv_responses);
762
763                                 // Only add what is visible
764                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
765                                         continue;
766                                 }
767
768                                 if (!visible_activity($item)) {
769                                         continue;
770                                 }
771
772                                 /// @todo Check if this call is needed or not
773                                 $arr = ['item' => $item];
774                                 Hook::callAll('display_item', $arr);
775
776                                 $item['pagedrop'] = $page_dropping;
777
778                                 if ($item['id'] == $item['parent']) {
779                                         $item_object = new Post($item);
780                                         $conv->addParent($item_object);
781                                 }
782                         }
783
784                         $threads = $conv->getTemplateData($conv_responses);
785                         if (!$threads) {
786                                 Logger::log('[ERROR] conversation : Failed to get template data.', Logger::DEBUG);
787                                 $threads = [];
788                         }
789                 }
790         }
791
792         $o = Renderer::replaceMacros($page_template, [
793                 '$baseurl' => DI::baseUrl()->get($ssl_state),
794                 '$return_path' => DI::args()->getQueryString(),
795                 '$live_update' => $live_update_div,
796                 '$remove' => DI::l10n()->t('remove'),
797                 '$mode' => $mode,
798                 '$user' => $a->user,
799                 '$threads' => $threads,
800                 '$dropping' => ($page_dropping ? DI::l10n()->t('Delete Selected Items') : False),
801         ]);
802
803         return $o;
804 }
805
806 /**
807  * Fetch all comments from a query. Additionally set the newest resharer as thread owner.
808  *
809  * @param mixed   $thread_items Database statement with thread posts
810  * @param boolean $pinned       Is the item pinned?
811  *
812  * @return array items with parents and comments
813  */
814 function conversation_fetch_comments($thread_items, $pinned) {
815         $comments = [];
816         $parentlines = [];
817         $lineno = 0;
818         $actor = [];
819         $received = '';
820
821         while ($row = Item::fetch($thread_items)) {
822                 if (($row['verb'] == Activity::ANNOUNCE) && !empty($row['contact-uid']) && ($row['received'] > $received) && ($row['thr-parent'] == $row['parent-uri'])) {
823                         $actor = ['link' => $row['author-link'], 'avatar' => $row['author-avatar'], 'name' => $row['author-name']];
824                         $received = $row['received'];
825                 }
826
827                 if ((($row['gravity'] == GRAVITY_PARENT) && !$row['origin'] && !in_array($row['network'], [Protocol::DIASPORA])) &&
828                         (empty($row['contact-uid']) || !in_array($row['network'], Protocol::NATIVE_SUPPORT))) {
829                         $parentlines[] = $lineno;
830                 }
831
832                 if ($row['gravity'] == GRAVITY_PARENT) {
833                         $row['pinned'] = $pinned;
834                 }
835
836                 $comments[] = $row;
837                 $lineno++;
838         }
839
840         DBA::close($thread_items);
841
842         if (!empty($actor)) {
843                 foreach ($parentlines as $line) {
844                         $comments[$line]['owner-link'] = $actor['link'];
845                         $comments[$line]['owner-avatar'] = $actor['avatar'];
846                         $comments[$line]['owner-name'] = $actor['name'];
847                 }
848         }
849         return $comments;
850 }
851
852 /**
853  * Add comments to top level entries that had been fetched before
854  *
855  * The system will fetch the comments for the local user whenever possible.
856  * This behaviour is currently needed to allow commenting on Friendica posts.
857  *
858  * @param array $parents Parent items
859  *
860  * @param       $block_authors
861  * @param       $order
862  * @param       $uid
863  * @return array items with parents and comments
864  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
865  */
866 function conversation_add_children(array $parents, $block_authors, $order, $uid) {
867         $max_comments = DI::config()->get('system', 'max_comments', 100);
868
869         $params = ['order' => ['uid', 'commented' => true]];
870
871         if ($max_comments > 0) {
872                 $params['limit'] = $max_comments;
873         }
874
875         $items = [];
876
877         foreach ($parents AS $parent) {
878                 $condition = ["`item`.`parent-uri` = ? AND `item`.`uid` IN (0, ?) ",
879                         $parent['uri'], $uid];
880                 if ($block_authors) {
881                         $condition[0] .= "AND NOT `author`.`hidden`";
882                 }
883
884                 $thread_items = Item::selectForUser(local_user(), array_merge(Item::DISPLAY_FIELDLIST, ['contact-uid', 'gravity']), $condition, $params);
885
886                 $comments = conversation_fetch_comments($thread_items, $parent['pinned'] ?? false);
887
888                 if (count($comments) != 0) {
889                         $items = array_merge($items, $comments);
890                 }
891         }
892
893         foreach ($items as $index => $item) {
894                 if ($item['uid'] == 0) {
895                         $items[$index]['writable'] = in_array($item['network'], Protocol::FEDERATED);
896                 }
897         }
898
899         $items = conv_sort($items, $order);
900
901         return $items;
902 }
903
904 function item_photo_menu($item) {
905         $sub_link = '';
906         $poke_link = '';
907         $contact_url = '';
908         $pm_url = '';
909         $status_link = '';
910         $photos_link = '';
911         $posts_link = '';
912         $block_link = '';
913         $ignore_link = '';
914
915         if (local_user() && local_user() == $item['uid'] && $item['parent'] == $item['id'] && !$item['self']) {
916                 $sub_link = 'javascript:dosubthread(' . $item['id'] . '); return false;';
917         }
918
919         $author = ['uid' => 0, 'id' => $item['author-id'],
920                 'network' => $item['author-network'], 'url' => $item['author-link']];
921         $profile_link = Contact::magicLinkByContact($author, $item['author-link']);
922         $sparkle = (strpos($profile_link, 'redir/') === 0);
923
924         $cid = 0;
925         $pcid = Contact::getIdForURL($item['author-link'], 0, true);
926         $network = '';
927         $rel = 0;
928         $condition = ['uid' => local_user(), 'nurl' => Strings::normaliseLink($item['author-link'])];
929         $contact = DBA::selectFirst('contact', ['id', 'network', 'rel'], $condition);
930         if (DBA::isResult($contact)) {
931                 $cid = $contact['id'];
932                 $network = $contact['network'];
933                 $rel = $contact['rel'];
934         }
935
936         if ($sparkle) {
937                 $status_link = $profile_link . '/status';
938                 $photos_link = str_replace('/profile/', '/photos/', $profile_link);
939                 $profile_link = $profile_link . '/profile';
940         }
941
942         if (!empty($pcid)) {
943                 $contact_url = 'contact/' . $pcid;
944                 $posts_link = 'contact/' . $pcid . '/posts';
945                 $block_link = 'contact/' . $pcid . '/block';
946                 $ignore_link = 'contact/' . $pcid . '/ignore';
947         }
948
949         if ($cid && !$item['self']) {
950                 $poke_link = 'poke?c=' . $cid;
951                 $contact_url = 'contact/' . $cid;
952                 $posts_link = 'contact/' . $cid . '/posts';
953
954                 if (in_array($network, [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA])) {
955                         $pm_url = 'message/new/' . $cid;
956                 }
957         }
958
959         if (local_user()) {
960                 $menu = [
961                         DI::l10n()->t('Follow Thread') => $sub_link,
962                         DI::l10n()->t('View Status') => $status_link,
963                         DI::l10n()->t('View Profile') => $profile_link,
964                         DI::l10n()->t('View Photos') => $photos_link,
965                         DI::l10n()->t('Network Posts') => $posts_link,
966                         DI::l10n()->t('View Contact') => $contact_url,
967                         DI::l10n()->t('Send PM') => $pm_url,
968                         DI::l10n()->t('Block') => $block_link,
969                         DI::l10n()->t('Ignore') => $ignore_link
970                 ];
971
972                 if ($network == Protocol::DFRN) {
973                         $menu[DI::l10n()->t("Poke")] = $poke_link;
974                 }
975
976                 if ((($cid == 0) || ($rel == Contact::FOLLOWER)) &&
977                         in_array($item['network'], Protocol::FEDERATED)) {
978                         $menu[DI::l10n()->t('Connect/Follow')] = 'follow?url=' . urlencode($item['author-link']);
979                 }
980         } else {
981                 $menu = [DI::l10n()->t('View Profile') => $item['author-link']];
982         }
983
984         $args = ['item' => $item, 'menu' => $menu];
985
986         Hook::callAll('item_photo_menu', $args);
987
988         $menu = $args['menu'];
989
990         $o = '';
991         foreach ($menu as $k => $v) {
992                 if (strpos($v, 'javascript:') === 0) {
993                         $v = substr($v, 11);
994                         $o .= '<li role="menuitem"><a onclick="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
995                 } elseif ($v!='') {
996                         $o .= '<li role="menuitem"><a href="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
997                 }
998         }
999         return $o;
1000 }
1001
1002 /**
1003  * Checks item to see if it is one of the builtin activities (like/dislike, event attendance, consensus items, etc.)
1004  *
1005  * Increments the count of each matching activity and adds a link to the author as needed.
1006  *
1007  * @param array  $item
1008  * @param array &$conv_responses (already created with builtin activity structure)
1009  * @return void
1010  * @throws ImagickException
1011  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1012  */
1013 function builtin_activity_puller($item, &$conv_responses) {
1014         foreach ($conv_responses as $mode => $v) {
1015                 $sparkle = '';
1016
1017                 switch ($mode) {
1018                         case 'like':
1019                                 $verb = Activity::LIKE;
1020                                 break;
1021                         case 'dislike':
1022                                 $verb = Activity::DISLIKE;
1023                                 break;
1024                         case 'attendyes':
1025                                 $verb = Activity::ATTEND;
1026                                 break;
1027                         case 'attendno':
1028                                 $verb = Activity::ATTENDNO;
1029                                 break;
1030                         case 'attendmaybe':
1031                                 $verb = Activity::ATTENDMAYBE;
1032                                 break;
1033                         case 'announce':
1034                                 $verb = Activity::ANNOUNCE;
1035                                 break;
1036                         default:
1037                                 return;
1038                 }
1039
1040                 if (!empty($item['verb']) && DI::activity()->match($item['verb'], $verb) && ($item['id'] != $item['parent'])) {
1041                         $author = ['uid' => 0, 'id' => $item['author-id'],
1042                                 'network' => $item['author-network'], 'url' => $item['author-link']];
1043                         $url = Contact::magicLinkByContact($author);
1044                         if (strpos($url, 'redir/') === 0) {
1045                                 $sparkle = ' class="sparkle" ';
1046                         }
1047
1048                         $url = '<a href="'. $url . '"'. $sparkle .'>' . htmlentities($item['author-name']) . '</a>';
1049
1050                         if (empty($item['thr-parent'])) {
1051                                 $item['thr-parent'] = $item['parent-uri'];
1052                         }
1053
1054                         if (!(isset($conv_responses[$mode][$item['thr-parent'] . '-l'])
1055                                 && is_array($conv_responses[$mode][$item['thr-parent'] . '-l']))) {
1056                                 $conv_responses[$mode][$item['thr-parent'] . '-l'] = [];
1057                         }
1058
1059                         // only list each unique author once
1060                         if (in_array($url,$conv_responses[$mode][$item['thr-parent'] . '-l'])) {
1061                                 continue;
1062                         }
1063
1064                         if (!isset($conv_responses[$mode][$item['thr-parent']])) {
1065                                 $conv_responses[$mode][$item['thr-parent']] = 1;
1066                         } else {
1067                                 $conv_responses[$mode][$item['thr-parent']] ++;
1068                         }
1069
1070                         if (public_contact() == $item['author-id']) {
1071                                 $conv_responses[$mode][$item['thr-parent'] . '-self'] = 1;
1072                         }
1073
1074                         $conv_responses[$mode][$item['thr-parent'] . '-l'][] = $url;
1075
1076                         // there can only be one activity verb per item so if we found anything, we can stop looking
1077                         return;
1078                 }
1079         }
1080 }
1081
1082 /**
1083  * Format the vote text for a profile item
1084  *
1085  * @param int    $cnt  = number of people who vote the item
1086  * @param array  $arr  = array of pre-linked names of likers/dislikers
1087  * @param string $type = one of 'like, 'dislike', 'attendyes', 'attendno', 'attendmaybe'
1088  * @param int    $id   = item id
1089  * @return string formatted text
1090  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1091  */
1092 function format_like($cnt, array $arr, $type, $id) {
1093         $o = '';
1094         $expanded = '';
1095         $phrase = '';
1096
1097         if ($cnt == 1) {
1098                 $likers = $arr[0];
1099
1100                 // Phrase if there is only one liker. In other cases it will be uses for the expanded
1101                 // list which show all likers
1102                 switch ($type) {
1103                         case 'like' :
1104                                 $phrase = DI::l10n()->t('%s likes this.', $likers);
1105                                 break;
1106                         case 'dislike' :
1107                                 $phrase = DI::l10n()->t('%s doesn\'t like this.', $likers);
1108                                 break;
1109                         case 'attendyes' :
1110                                 $phrase = DI::l10n()->t('%s attends.', $likers);
1111                                 break;
1112                         case 'attendno' :
1113                                 $phrase = DI::l10n()->t('%s doesn\'t attend.', $likers);
1114                                 break;
1115                         case 'attendmaybe' :
1116                                 $phrase = DI::l10n()->t('%s attends maybe.', $likers);
1117                                 break;
1118                         case 'announce' :
1119                                 $phrase = DI::l10n()->t('%s reshared this.', $likers);
1120                                 break;
1121                 }
1122         }
1123
1124         if ($cnt > 1) {
1125                 $total = count($arr);
1126                 if ($total < MAX_LIKERS) {
1127                         $last = DI::l10n()->t('and') . ' ' . $arr[count($arr)-1];
1128                         $arr2 = array_slice($arr, 0, -1);
1129                         $likers = implode(', ', $arr2) . ' ' . $last;
1130                 } else  {
1131                         $arr = array_slice($arr, 0, MAX_LIKERS - 1);
1132                         $likers = implode(', ', $arr);
1133                         $likers .= DI::l10n()->t('and %d other people', $total - MAX_LIKERS);
1134                 }
1135
1136                 $spanatts = "class=\"fakelink\" onclick=\"openClose('{$type}list-$id');\"";
1137
1138                 $explikers = '';
1139                 switch ($type) {
1140                         case 'like':
1141                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> like this', $spanatts, $cnt);
1142                                 $explikers = DI::l10n()->t('%s like this.', $likers);
1143                                 break;
1144                         case 'dislike':
1145                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> don\'t like this', $spanatts, $cnt);
1146                                 $explikers = DI::l10n()->t('%s don\'t like this.', $likers);
1147                                 break;
1148                         case 'attendyes':
1149                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> attend', $spanatts, $cnt);
1150                                 $explikers = DI::l10n()->t('%s attend.', $likers);
1151                                 break;
1152                         case 'attendno':
1153                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> don\'t attend', $spanatts, $cnt);
1154                                 $explikers = DI::l10n()->t('%s don\'t attend.', $likers);
1155                                 break;
1156                         case 'attendmaybe':
1157                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> attend maybe', $spanatts, $cnt);
1158                                 $explikers = DI::l10n()->t('%s attend maybe.', $likers);
1159                                 break;
1160                         case 'announce':
1161                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> reshared this', $spanatts, $cnt);
1162                                 $explikers = DI::l10n()->t('%s reshared this.', $likers);
1163                                 break;
1164                 }
1165
1166                 $expanded .= "\t" . '<p class="wall-item-' . $type . '-expanded" id="' . $type . 'list-' . $id . '" style="display: none;" >' . $explikers . EOL . '</p>';
1167         }
1168
1169         $o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('voting_fakelink.tpl'), [
1170                 '$phrase' => $phrase,
1171                 '$type' => $type,
1172                 '$id' => $id
1173         ]);
1174         $o .= $expanded;
1175
1176         return $o;
1177 }
1178
1179 function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
1180 {
1181         $o = '';
1182
1183         $geotag = !empty($x['allow_location']) ? Renderer::replaceMacros(Renderer::getMarkupTemplate('jot_geotag.tpl'), []) : '';
1184
1185         $tpl = Renderer::getMarkupTemplate('jot-header.tpl');
1186         DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
1187                 '$newpost'   => 'true',
1188                 '$baseurl'   => DI::baseUrl()->get(true),
1189                 '$geotag'    => $geotag,
1190                 '$nickname'  => $x['nickname'],
1191                 '$ispublic'  => DI::l10n()->t('Visible to <strong>everybody</strong>'),
1192                 '$linkurl'   => DI::l10n()->t('Please enter a image/video/audio/webpage URL:'),
1193                 '$term'      => DI::l10n()->t('Tag term:'),
1194                 '$fileas'    => DI::l10n()->t('Save to Folder:'),
1195                 '$whereareu' => DI::l10n()->t('Where are you right now?'),
1196                 '$delitems'  => DI::l10n()->t("Delete item\x28s\x29?")
1197         ]);
1198
1199         $jotplugins = '';
1200         Hook::callAll('jot_tool', $jotplugins);
1201
1202         // Private/public post links for the non-JS ACL form
1203         $private_post = 1;
1204         if (!empty($_REQUEST['public'])) {
1205                 $private_post = 0;
1206         }
1207
1208         $query_str = DI::args()->getQueryString();
1209         if (strpos($query_str, 'public=1') !== false) {
1210                 $query_str = str_replace(['?public=1', '&public=1'], ['', ''], $query_str);
1211         }
1212
1213         /*
1214          * I think $a->query_string may never have ? in it, but I could be wrong
1215          * It looks like it's from the index.php?q=[etc] rewrite that the web
1216          * server does, which converts any ? to &, e.g. suggest&ignore=61 for suggest?ignore=61
1217          */
1218         if (strpos($query_str, '?') === false) {
1219                 $public_post_link = '?public=1';
1220         } else {
1221                 $public_post_link = '&public=1';
1222         }
1223
1224         // $tpl = Renderer::replaceMacros($tpl,array('$jotplugins' => $jotplugins));
1225         $tpl = Renderer::getMarkupTemplate("jot.tpl");
1226
1227         $o .= Renderer::replaceMacros($tpl,[
1228                 '$new_post' => DI::l10n()->t('New Post'),
1229                 '$return_path'  => $query_str,
1230                 '$action'       => 'item',
1231                 '$share'        => ($x['button'] ?? '') ?: DI::l10n()->t('Share'),
1232                 '$loading'      => DI::l10n()->t('Loading...'),
1233                 '$upload'       => DI::l10n()->t('Upload photo'),
1234                 '$shortupload'  => DI::l10n()->t('upload photo'),
1235                 '$attach'       => DI::l10n()->t('Attach file'),
1236                 '$shortattach'  => DI::l10n()->t('attach file'),
1237                 '$edbold'       => DI::l10n()->t('Bold'),
1238                 '$editalic'     => DI::l10n()->t('Italic'),
1239                 '$eduline'      => DI::l10n()->t('Underline'),
1240                 '$edquote'      => DI::l10n()->t('Quote'),
1241                 '$edcode'       => DI::l10n()->t('Code'),
1242                 '$edimg'        => DI::l10n()->t('Image'),
1243                 '$edurl'        => DI::l10n()->t('Link'),
1244                 '$edattach'     => DI::l10n()->t('Link or Media'),
1245                 '$setloc'       => DI::l10n()->t('Set your location'),
1246                 '$shortsetloc'  => DI::l10n()->t('set location'),
1247                 '$noloc'        => DI::l10n()->t('Clear browser location'),
1248                 '$shortnoloc'   => DI::l10n()->t('clear location'),
1249                 '$title'        => $x['title'] ?? '',
1250                 '$placeholdertitle' => DI::l10n()->t('Set title'),
1251                 '$category'     => $x['category'] ?? '',
1252                 '$placeholdercategory' => Feature::isEnabled(local_user(), 'categories') ? DI::l10n()->t("Categories \x28comma-separated list\x29") : '',
1253                 '$wait'         => DI::l10n()->t('Please wait'),
1254                 '$permset'      => DI::l10n()->t('Permission settings'),
1255                 '$shortpermset' => DI::l10n()->t('permissions'),
1256                 '$wall'         => $notes_cid ? 0 : 1,
1257                 '$posttype'     => $notes_cid ? Item::PT_PERSONAL_NOTE : Item::PT_ARTICLE,
1258                 '$content'      => $x['content'] ?? '',
1259                 '$post_id'      => $x['post_id'] ?? '',
1260                 '$baseurl'      => DI::baseUrl()->get(true),
1261                 '$defloc'       => $x['default_location'],
1262                 '$visitor'      => $x['visitor'],
1263                 '$pvisit'       => $notes_cid ? 'none' : $x['visitor'],
1264                 '$public'       => DI::l10n()->t('Public post'),
1265                 '$lockstate'    => $x['lockstate'],
1266                 '$bang'         => $x['bang'],
1267                 '$profile_uid'  => $x['profile_uid'],
1268                 '$preview'      => DI::l10n()->t('Preview'),
1269                 '$jotplugins'   => $jotplugins,
1270                 '$notes_cid'    => $notes_cid,
1271                 '$sourceapp'    => DI::l10n()->t($a->sourcename),
1272                 '$cancel'       => DI::l10n()->t('Cancel'),
1273                 '$rand_num'     => Crypto::randomDigits(12),
1274
1275                 // ACL permissions box
1276                 '$acl'           => $x['acl'],
1277                 '$group_perms'   => DI::l10n()->t('Post to Groups'),
1278                 '$contact_perms' => DI::l10n()->t('Post to Contacts'),
1279                 '$private'       => DI::l10n()->t('Private post'),
1280                 '$is_private'    => $private_post,
1281                 '$public_link'   => $public_post_link,
1282
1283                 //jot nav tab (used in some themes)
1284                 '$message' => DI::l10n()->t('Message'),
1285                 '$browser' => DI::l10n()->t('Browser'),
1286         ]);
1287
1288
1289         if ($popup == true) {
1290                 $o = '<div id="jot-popup" style="display: none;">' . $o . '</div>';
1291         }
1292
1293         return $o;
1294 }
1295
1296 /**
1297  * Plucks the children of the given parent from a given item list.
1298  *
1299  * @param array $item_list
1300  * @param array $parent
1301  * @param bool  $recursive
1302  * @return array
1303  */
1304 function get_item_children(array &$item_list, array $parent, $recursive = true)
1305 {
1306         $children = [];
1307         foreach ($item_list as $i => $item) {
1308                 if ($item['id'] != $item['parent']) {
1309                         if ($recursive) {
1310                                 // Fallback to parent-uri if thr-parent is not set
1311                                 $thr_parent = $item['thr-parent'];
1312                                 if ($thr_parent == '') {
1313                                         $thr_parent = $item['parent-uri'];
1314                                 }
1315
1316                                 if ($thr_parent == $parent['uri']) {
1317                                         $item['children'] = get_item_children($item_list, $item);
1318                                         $children[] = $item;
1319                                         unset($item_list[$i]);
1320                                 }
1321                         } elseif ($item['parent'] == $parent['id']) {
1322                                 $children[] = $item;
1323                                 unset($item_list[$i]);
1324                         }
1325                 }
1326         }
1327         return $children;
1328 }
1329
1330 /**
1331  * Recursively sorts a tree-like item array
1332  *
1333  * @param array $items
1334  * @return array
1335  */
1336 function sort_item_children(array $items)
1337 {
1338         $result = $items;
1339         usort($result, 'sort_thr_received_rev');
1340         foreach ($result as $k => $i) {
1341                 if (isset($result[$k]['children'])) {
1342                         $result[$k]['children'] = sort_item_children($result[$k]['children']);
1343                 }
1344         }
1345         return $result;
1346 }
1347
1348 /**
1349  * Recursively add all children items at the top level of a list
1350  *
1351  * @param array $children List of items to append
1352  * @param array $item_list
1353  */
1354 function add_children_to_list(array $children, array &$item_list)
1355 {
1356         foreach ($children as $child) {
1357                 $item_list[] = $child;
1358                 if (isset($child['children'])) {
1359                         add_children_to_list($child['children'], $item_list);
1360                 }
1361         }
1362 }
1363
1364 /**
1365  * Selectively flattens a tree-like item structure to prevent threading stairs
1366  *
1367  * This recursive function takes the item tree structure created by conv_sort() and
1368  * flatten the extraneous depth levels when people reply sequentially, removing the
1369  * stairs effect in threaded conversations limiting the available content width.
1370  *
1371  * The basic principle is the following: if a post item has only one reply and is
1372  * the last reply of its parent, then the reply is moved to the parent.
1373  *
1374  * This process is rendered somewhat more complicated because items can be either
1375  * replies or likes, and these don't factor at all in the reply count/last reply.
1376  *
1377  * @param array $parent A tree-like array of items
1378  * @return array
1379  */
1380 function smart_flatten_conversation(array $parent)
1381 {
1382         if (!isset($parent['children']) || count($parent['children']) == 0) {
1383                 return $parent;
1384         }
1385
1386         // We use a for loop to ensure we process the newly-moved items
1387         for ($i = 0; $i < count($parent['children']); $i++) {
1388                 $child = $parent['children'][$i];
1389
1390                 if (isset($child['children']) && count($child['children'])) {
1391                         // This helps counting only the regular posts
1392                         $count_post_closure = function($var) {
1393                                 return $var['verb'] === Activity::POST;
1394                         };
1395
1396                         $child_post_count = count(array_filter($child['children'], $count_post_closure));
1397
1398                         $remaining_post_count = count(array_filter(array_slice($parent['children'], $i), $count_post_closure));
1399
1400                         // If there's only one child's children post and this is the last child post
1401                         if ($child_post_count == 1 && $remaining_post_count == 1) {
1402
1403                                 // Searches the post item in the children
1404                                 $j = 0;
1405                                 while($child['children'][$j]['verb'] !== Activity::POST && $j < count($child['children'])) {
1406                                         $j ++;
1407                                 }
1408
1409                                 $moved_item = $child['children'][$j];
1410                                 unset($parent['children'][$i]['children'][$j]);
1411                                 $parent['children'][] = $moved_item;
1412                         } else {
1413                                 $parent['children'][$i] = smart_flatten_conversation($child);
1414                         }
1415                 }
1416         }
1417
1418         return $parent;
1419 }
1420
1421
1422 /**
1423  * Expands a flat list of items into corresponding tree-like conversation structures.
1424  *
1425  * sort the top-level posts either on "received" or "commented", and finally
1426  * append all the items at the top level (???)
1427  *
1428  * @param array  $item_list A list of items belonging to one or more conversations
1429  * @param string $order     Either on "received" or "commented"
1430  * @return array
1431  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1432  */
1433 function conv_sort(array $item_list, $order)
1434 {
1435         $parents = [];
1436
1437         if (!(is_array($item_list) && count($item_list))) {
1438                 return $parents;
1439         }
1440
1441         $blocklist = conv_get_blocklist();
1442
1443         $item_array = [];
1444
1445         // Dedupes the item list on the uri to prevent infinite loops
1446         foreach ($item_list as $item) {
1447                 if (in_array($item['author-id'], $blocklist)) {
1448                         continue;
1449                 }
1450
1451                 $item_array[$item['uri']] = $item;
1452         }
1453
1454         // Extract the top level items
1455         foreach ($item_array as $item) {
1456                 if ($item['id'] == $item['parent']) {
1457                         $parents[] = $item;
1458                 }
1459         }
1460
1461         if (stristr($order, 'pinned_received')) {
1462                 usort($parents, 'sort_thr_pinned_received');
1463         } elseif (stristr($order, 'received')) {
1464                 usort($parents, 'sort_thr_received');
1465         } elseif (stristr($order, 'commented')) {
1466                 usort($parents, 'sort_thr_commented');
1467         }
1468
1469         /*
1470          * Plucks children from the item_array, second pass collects eventual orphan
1471          * items and add them as children of their top-level post.
1472          */
1473         foreach ($parents as $i => $parent) {
1474                 $parents[$i]['children'] =
1475                         array_merge(get_item_children($item_array, $parent, true),
1476                                 get_item_children($item_array, $parent, false));
1477         }
1478
1479         foreach ($parents as $i => $parent) {
1480                 $parents[$i]['children'] = sort_item_children($parents[$i]['children']);
1481         }
1482
1483         if (!DI::pConfig()->get(local_user(), 'system', 'no_smart_threading', 0)) {
1484                 foreach ($parents as $i => $parent) {
1485                         $parents[$i] = smart_flatten_conversation($parent);
1486                 }
1487         }
1488
1489         /// @TODO: Stop recusrsively adding all children back to the top level (!!!)
1490         /// However, this apparently ensures responses (likes, attendance) display (?!)
1491         foreach ($parents as $parent) {
1492                 if (count($parent['children'])) {
1493                         add_children_to_list($parent['children'], $parents);
1494                 }
1495         }
1496
1497         return $parents;
1498 }
1499
1500 /**
1501  * usort() callback to sort item arrays by pinned and the received key
1502  *
1503  * @param array $a
1504  * @param array $b
1505  * @return int
1506  */
1507 function sort_thr_pinned_received(array $a, array $b)
1508 {
1509         if ($b['pinned'] && !$a['pinned']) {
1510                 return 1;
1511         } elseif (!$b['pinned'] && $a['pinned']) {
1512                 return -1;
1513         }
1514
1515         return strcmp($b['received'], $a['received']);
1516 }
1517
1518 /**
1519  * usort() callback to sort item arrays by the received key
1520  *
1521  * @param array $a
1522  * @param array $b
1523  * @return int
1524  */
1525 function sort_thr_received(array $a, array $b)
1526 {
1527         return strcmp($b['received'], $a['received']);
1528 }
1529
1530 /**
1531  * usort() callback to reverse sort item arrays by the received key
1532  *
1533  * @param array $a
1534  * @param array $b
1535  * @return int
1536  */
1537 function sort_thr_received_rev(array $a, array $b)
1538 {
1539         return strcmp($a['received'], $b['received']);
1540 }
1541
1542 /**
1543  * usort() callback to sort item arrays by the commented key
1544  *
1545  * @param array $a
1546  * @param array $b
1547  * @return int
1548  */
1549 function sort_thr_commented(array $a, array $b)
1550 {
1551         return strcmp($b['commented'], $a['commented']);
1552 }
1553
1554 function render_location_dummy(array $item) {
1555         if (!empty($item['location']) && !empty($item['location'])) {
1556                 return $item['location'];
1557         }
1558
1559         if (!empty($item['coord']) && !empty($item['coord'])) {
1560                 return $item['coord'];
1561         }
1562 }
1563
1564 function get_responses(array $conv_responses, array $response_verbs, array $item, Post $ob = null) {
1565         $ret = [];
1566         foreach ($response_verbs as $v) {
1567                 $ret[$v] = [];
1568                 $ret[$v]['count'] = $conv_responses[$v][$item['uri']] ?? 0;
1569                 $ret[$v]['list']  = $conv_responses[$v][$item['uri'] . '-l'] ?? [];
1570                 $ret[$v]['self']  = $conv_responses[$v][$item['uri'] . '-self'] ?? '0';
1571                 if (count($ret[$v]['list']) > MAX_LIKERS) {
1572                         $ret[$v]['list_part'] = array_slice($ret[$v]['list'], 0, MAX_LIKERS);
1573                         array_push($ret[$v]['list_part'], '<a href="#" data-toggle="modal" data-target="#' . $v . 'Modal-'
1574                                 . (($ob) ? $ob->getId() : $item['id']) . '"><b>' . DI::l10n()->t('View all') . '</b></a>');
1575                 } else {
1576                         $ret[$v]['list_part'] = '';
1577                 }
1578                 $ret[$v]['button'] = get_response_button_text($v, $ret[$v]['count']);
1579                 $ret[$v]['title'] = $conv_responses[$v]['title'];
1580         }
1581
1582         $count = 0;
1583         foreach ($ret as $key) {
1584                 if ($key['count'] == true) {
1585                         $count++;
1586                 }
1587         }
1588         $ret['count'] = $count;
1589
1590         return $ret;
1591 }
1592
1593 function get_response_button_text($v, $count)
1594 {
1595         $return = '';
1596         switch ($v) {
1597                 case 'like':
1598                         $return = DI::l10n()->tt('Like', 'Likes', $count);
1599                         break;
1600                 case 'dislike':
1601                         $return = DI::l10n()->tt('Dislike', 'Dislikes', $count);
1602                         break;
1603                 case 'attendyes':
1604                         $return = DI::l10n()->tt('Attending', 'Attending', $count);
1605                         break;
1606                 case 'attendno':
1607                         $return = DI::l10n()->tt('Not Attending', 'Not Attending', $count);
1608                         break;
1609                 case 'attendmaybe':
1610                         $return = DI::l10n()->tt('Undecided', 'Undecided', $count);
1611                         break;
1612         }
1613
1614         return $return;
1615 }