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