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