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