Change contact post link to contact network page
[friendica.git/.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\Text\BBCode;
10 use Friendica\Core\Addon;
11 use Friendica\Core\Config;
12 use Friendica\Core\L10n;
13 use Friendica\Core\PConfig;
14 use Friendica\Core\Protocol;
15 use Friendica\Core\System;
16 use Friendica\Database\DBA;
17 use Friendica\Model\Contact;
18 use Friendica\Model\Item;
19 use Friendica\Model\Profile;
20 use Friendica\Model\Term;
21 use Friendica\Object\Post;
22 use Friendica\Object\Thread;
23 use Friendica\Util\DateTimeFormat;
24 use Friendica\Util\Proxy as ProxyUtils;
25 use Friendica\Util\Temporal;
26 use Friendica\Util\XML;
27
28 function item_extract_images($body) {
29
30         $saved_image = [];
31         $orig_body = $body;
32         $new_body = '';
33
34         $cnt = 0;
35         $img_start = strpos($orig_body, '[img');
36         $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
37         $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
38         while (($img_st_close !== false) && ($img_end !== false)) {
39
40                 $img_st_close++; // make it point to AFTER the closing bracket
41                 $img_end += $img_start;
42
43                 if (!strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
44                         // This is an embedded image
45
46                         $saved_image[$cnt] = substr($orig_body, $img_start + $img_st_close, $img_end - ($img_start + $img_st_close));
47                         $new_body = $new_body . substr($orig_body, 0, $img_start) . '[!#saved_image' . $cnt . '#!]';
48
49                         $cnt++;
50                 } else {
51                         $new_body = $new_body . substr($orig_body, 0, $img_end + strlen('[/img]'));
52                 }
53
54                 $orig_body = substr($orig_body, $img_end + strlen('[/img]'));
55
56                 if ($orig_body === false) {
57                         // in case the body ends on a closing image tag
58                         $orig_body = '';
59                 }
60
61                 $img_start = strpos($orig_body, '[img');
62                 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
63                 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
64         }
65
66         $new_body = $new_body . $orig_body;
67
68         return ['body' => $new_body, 'images' => $saved_image];
69 }
70
71 function item_redir_and_replace_images($body, $images, $cid) {
72
73         $origbody = $body;
74         $newbody = '';
75
76         $cnt = 1;
77         $pos = BBCode::getTagPosition($origbody, 'url', 0);
78         while ($pos !== false && $cnt < 1000) {
79
80                 $search = '/\[url\=(.*?)\]\[!#saved_image([0-9]*)#!\]\[\/url\]' . '/is';
81                 $replace = '[url=' . System::baseUrl() . '/redir/' . $cid
82                                    . '?f=1&url=' . '$1' . '][!#saved_image' . '$2' .'#!][/url]';
83
84                 $newbody .= substr($origbody, 0, $pos['start']['open']);
85                 $subject = substr($origbody, $pos['start']['open'], $pos['end']['close'] - $pos['start']['open']);
86                 $origbody = substr($origbody, $pos['end']['close']);
87                 if ($origbody === false) {
88                         $origbody = '';
89                 }
90
91                 $subject = preg_replace($search, $replace, $subject);
92                 $newbody .= $subject;
93
94                 $cnt++;
95                 // Isn't this supposed to use $cnt value for $occurrences? - @MrPetovan
96                 $pos = BBCode::getTagPosition($origbody, 'url', 0);
97         }
98         $newbody .= $origbody;
99
100         $cnt = 0;
101         foreach ($images as $image) {
102                 /*
103                  * We're depending on the property of 'foreach' (specified on the PHP website) that
104                  * it loops over the array starting from the first element and going sequentially
105                  * to the last element.
106                  */
107                 $newbody = str_replace('[!#saved_image' . $cnt . '#!]', '[img]' . $image . '[/img]', $newbody);
108                 $cnt++;
109         }
110         return $newbody;
111 }
112
113 /**
114  * Render actions localized
115  */
116 function localize_item(&$item)
117 {
118         $extracted = item_extract_images($item['body']);
119         if ($extracted['images']) {
120                 $item['body'] = item_redir_and_replace_images($extracted['body'], $extracted['images'], $item['contact-id']);
121         }
122
123         /*
124         heluecht 2018-06-19: from my point of view this whole code part is useless.
125         It just renders the body message of technical posts (Like, dislike, ...).
126         But: The body isn't visible at all. So we do this stuff just because we can.
127         Even if these messages were visible, this would only mean that something went wrong.
128         During the further steps of the database restructuring I would like to address this issue.
129         */
130
131         $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
132         if (activity_match($item['verb'], ACTIVITY_LIKE)
133                 || activity_match($item['verb'], ACTIVITY_DISLIKE)
134                 || activity_match($item['verb'], ACTIVITY_ATTEND)
135                 || activity_match($item['verb'], ACTIVITY_ATTENDNO)
136                 || activity_match($item['verb'], ACTIVITY_ATTENDMAYBE)) {
137
138                 $fields = ['author-link', 'author-name', 'verb', 'object-type', 'resource-id', 'body', 'plink'];
139                 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
140                 if (!DBA::isResult($obj)) {
141                         return;
142                 }
143
144                 $author  = '[url=' . $item['author-link'] . ']' . $item['author-name'] . '[/url]';
145                 $objauthor =  '[url=' . $obj['author-link'] . ']' . $obj['author-name'] . '[/url]';
146
147                 switch ($obj['verb']) {
148                         case ACTIVITY_POST:
149                                 switch ($obj['object-type']) {
150                                         case ACTIVITY_OBJ_EVENT:
151                                                 $post_type = L10n::t('event');
152                                                 break;
153                                         default:
154                                                 $post_type = L10n::t('status');
155                                 }
156                                 break;
157                         default:
158                                 if ($obj['resource-id']) {
159                                         $post_type = L10n::t('photo');
160                                         $m = [];
161                                         preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
162                                         $rr['plink'] = $m[1];
163                                 } else {
164                                         $post_type = L10n::t('status');
165                                 }
166                 }
167
168                 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
169
170                 if (activity_match($item['verb'], ACTIVITY_LIKE)) {
171                         $bodyverb = L10n::t('%1$s likes %2$s\'s %3$s');
172                 } elseif (activity_match($item['verb'], ACTIVITY_DISLIKE)) {
173                         $bodyverb = L10n::t('%1$s doesn\'t like %2$s\'s %3$s');
174                 } elseif (activity_match($item['verb'], ACTIVITY_ATTEND)) {
175                         $bodyverb = L10n::t('%1$s attends %2$s\'s %3$s');
176                 } elseif (activity_match($item['verb'], ACTIVITY_ATTENDNO)) {
177                         $bodyverb = L10n::t('%1$s doesn\'t attend %2$s\'s %3$s');
178                 } elseif (activity_match($item['verb'], ACTIVITY_ATTENDMAYBE)) {
179                         $bodyverb = L10n::t('%1$s attends maybe %2$s\'s %3$s');
180                 }
181
182                 $item['body'] = sprintf($bodyverb, $author, $objauthor, $plink);
183         }
184
185         if (activity_match($item['verb'], ACTIVITY_FRIEND)) {
186
187                 if ($item['object-type']=="" || $item['object-type']!== ACTIVITY_OBJ_PERSON) return;
188
189                 $Aname = $item['author-name'];
190                 $Alink = $item['author-link'];
191
192                 $xmlhead="<"."?xml version='1.0' encoding='UTF-8' ?".">";
193
194                 $obj = XML::parseString($xmlhead.$item['object']);
195                 $links = XML::parseString($xmlhead."<links>".unxmlify($obj->link)."</links>");
196
197                 $Bname = $obj->title;
198                 $Blink = "";
199                 $Bphoto = "";
200                 foreach ($links->link as $l) {
201                         $atts = $l->attributes();
202                         switch ($atts['rel']) {
203                                 case "alternate": $Blink = $atts['href'];
204                                 case "photo": $Bphoto = $atts['href'];
205                         }
206                 }
207
208                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
209                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
210                 if ($Bphoto != "") {
211                         $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img]' . $Bphoto . '[/img][/url]';
212                 }
213
214                 $item['body'] = L10n::t('%1$s is now friends with %2$s', $A, $B)."\n\n\n".$Bphoto;
215
216         }
217         if (stristr($item['verb'], ACTIVITY_POKE)) {
218                 $verb = urldecode(substr($item['verb'],strpos($item['verb'],'#')+1));
219                 if (!$verb) {
220                         return;
221                 }
222                 if ($item['object-type']=="" || $item['object-type']!== ACTIVITY_OBJ_PERSON) {
223                         return;
224                 }
225
226                 $Aname = $item['author-name'];
227                 $Alink = $item['author-link'];
228
229                 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
230
231                 $obj = XML::parseString($xmlhead.$item['object']);
232                 $links = XML::parseString($xmlhead."<links>".unxmlify($obj->link)."</links>");
233
234                 $Bname = $obj->title;
235                 $Blink = "";
236                 $Bphoto = "";
237                 foreach ($links->link as $l) {
238                         $atts = $l->attributes();
239                         switch ($atts['rel']) {
240                                 case "alternate": $Blink = $atts['href'];
241                                 case "photo": $Bphoto = $atts['href'];
242                         }
243                 }
244
245                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
246                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
247                 if ($Bphoto != "") {
248                         $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img=80x80]' . $Bphoto . '[/img][/url]';
249                 }
250
251                 /*
252                  * we can't have a translation string with three positions but no distinguishable text
253                  * So here is the translate string.
254                  */
255                 $txt = L10n::t('%1$s poked %2$s');
256
257                 // now translate the verb
258                 $poked_t = trim(sprintf($txt, "", ""));
259                 $txt = str_replace($poked_t, L10n::t($verb), $txt);
260
261                 // then do the sprintf on the translation string
262
263                 $item['body'] = sprintf($txt, $A, $B). "\n\n\n" . $Bphoto;
264
265         }
266
267         if (activity_match($item['verb'], ACTIVITY_TAG)) {
268                 $fields = ['author-id', 'author-link', 'author-name', 'author-network',
269                         'verb', 'object-type', 'resource-id', 'body', 'plink'];
270                 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
271                 if (!DBA::isResult($obj)) {
272                         return;
273                 }
274
275                 $author_arr = ['uid' => 0, 'id' => $item['author-id'],
276                         'network' => $item['author-network'], 'url' => $item['author-link']];
277                 $author  = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $item['author-name'] . '[/url]';
278
279                 $author_arr = ['uid' => 0, 'id' => $obj['author-id'],
280                         'network' => $obj['author-network'], 'url' => $obj['author-link']];
281                 $objauthor  = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $obj['author-name'] . '[/url]';
282
283                 switch ($obj['verb']) {
284                         case ACTIVITY_POST:
285                                 switch ($obj['object-type']) {
286                                         case ACTIVITY_OBJ_EVENT:
287                                                 $post_type = L10n::t('event');
288                                                 break;
289                                         default:
290                                                 $post_type = L10n::t('status');
291                                 }
292                                 break;
293                         default:
294                                 if ($obj['resource-id']) {
295                                         $post_type = L10n::t('photo');
296                                         $m=[]; preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
297                                         $rr['plink'] = $m[1];
298                                 } else {
299                                         $post_type = L10n::t('status');
300                                 }
301                                 // Let's break everthing ... ;-)
302                                 break;
303                 }
304                 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
305
306                 $parsedobj = XML::parseString($xmlhead.$item['object']);
307
308                 $tag = sprintf('#[url=%s]%s[/url]', $parsedobj->id, $parsedobj->content);
309                 $item['body'] = L10n::t('%1$s tagged %2$s\'s %3$s with %4$s', $author, $objauthor, $plink, $tag);
310         }
311
312         if (activity_match($item['verb'], ACTIVITY_FAVORITE)) {
313                 if ($item['object-type'] == "") {
314                         return;
315                 }
316
317                 $Aname = $item['author-name'];
318                 $Alink = $item['author-link'];
319
320                 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
321
322                 $obj = XML::parseString($xmlhead.$item['object']);
323                 if (strlen($obj->id)) {
324                         $fields = ['author-link', 'author-name', 'plink'];
325                         $target = Item::selectFirst($fields, ['uri' => $obj->id, 'uid' => $item['uid']]);
326                         if (DBA::isResult($target) && $target['plink']) {
327                                 $Bname = $target['author-name'];
328                                 $Blink = $target['author-link'];
329                                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
330                                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
331                                 $P = '[url=' . $target['plink'] . ']' . L10n::t('post/item') . '[/url]';
332                                 $item['body'] = L10n::t('%1$s marked %2$s\'s %3$s as favorite', $A, $B, $P)."\n";
333                         }
334                 }
335         }
336         $matches = null;
337         if (preg_match_all('/@\[url=(.*?)\]/is', $item['body'], $matches, PREG_SET_ORDER)) {
338                 foreach ($matches as $mtch) {
339                         if (!strpos($mtch[1], 'zrl=')) {
340                                 $item['body'] = str_replace($mtch[0], '@[url=' . Contact::magicLink($mtch[1]) . ']', $item['body']);
341                         }
342                 }
343         }
344
345         // add zrl's to public images
346         $photo_pattern = "/\[url=(.*?)\/photos\/(.*?)\/image\/(.*?)\]\[img(.*?)\]h(.*?)\[\/img\]\[\/url\]/is";
347         if (preg_match($photo_pattern, $item['body'])) {
348                 $photo_replace = '[url=' . Profile::zrl('$1' . '/photos/' . '$2' . '/image/' . '$3' ,true) . '][img' . '$4' . ']h' . '$5'  . '[/img][/url]';
349                 $item['body'] = BBCode::pregReplaceInTag($photo_pattern, $photo_replace, 'url', $item['body']);
350         }
351
352         // add sparkle links to appropriate permalinks
353         $author = ['uid' => 0, 'id' => $item['author-id'],
354                 'network' => $item['author-network'], 'url' => $item['author-link']];
355
356         if (!empty($item['plink'])) {
357                 $item['plink'] = Contact::magicLinkbyContact($author, $item['plink']);
358         }
359 }
360
361 /**
362  * Count the total of comments on this item and its desendants
363  * @TODO proper type-hint + doc-tag
364  */
365 function count_descendants($item) {
366         $total = count($item['children']);
367
368         if ($total > 0) {
369                 foreach ($item['children'] as $child) {
370                         if (!visible_activity($child)) {
371                                 $total --;
372                         }
373                         $total += count_descendants($child);
374                 }
375         }
376
377         return $total;
378 }
379
380 function visible_activity($item) {
381
382         /*
383          * likes (etc.) can apply to other things besides posts. Check if they are post children,
384          * in which case we handle them specially
385          */
386         $hidden_activities = [ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE];
387         foreach ($hidden_activities as $act) {
388                 if (activity_match($item['verb'], $act)) {
389                         return false;
390                 }
391         }
392
393         // @TODO below if() block can be rewritten to a single line: $isVisible = allConditionsHere;
394         if (activity_match($item['verb'], ACTIVITY_FOLLOW) && $item['object-type'] === ACTIVITY_OBJ_NOTE && empty($item['self']) && $item['uid'] == local_user()) {
395                 return false;
396         }
397
398         return true;
399 }
400
401 function conv_get_blocklist()
402 {
403         if (!local_user()) {
404                 return [];
405         }
406
407         $str_blocked = PConfig::get(local_user(), 'system', 'blocked');
408         if (empty($str_blocked)) {
409                 return [];
410         }
411
412         $blocklist = [];
413
414         foreach (explode(',', $str_blocked) as $entry) {
415                 $cid = Contact::getIdForURL(trim($entry), 0, true);
416                 if (!empty($cid)) {
417                         $blocklist[] = $cid;
418                 }
419         }
420
421         return $blocklist;
422 }
423
424 /**
425  * "Render" a conversation or list of items for HTML display.
426  * There are two major forms of display:
427  *      - Sequential or unthreaded ("New Item View" or search results)
428  *      - conversation view
429  * The $mode parameter decides between the various renderings and also
430  * figures out how to determine page owner and other contextual items
431  * that are based on unique features of the calling module.
432  *
433  */
434 function conversation(App $a, array $items, $mode, $update, $preview = false, $order = 'commented', $uid = 0) {
435
436         $ssl_state = (local_user() ? true : false);
437
438         $profile_owner = 0;
439         $live_update_div = '';
440
441         $blocklist = conv_get_blocklist();
442
443         $previewing = (($preview) ? ' preview ' : '');
444
445         if ($mode === 'network') {
446                 $items = conversation_add_children($items, false, $order, $uid);
447                 $profile_owner = local_user();
448                 if (!$update) {
449                         /*
450                          * The special div is needed for liveUpdate to kick in for this page.
451                          * We only launch liveUpdate if you aren't filtering in some incompatible
452                          * way and also you aren't writing a comment (discovered in javascript).
453                          */
454                         $live_update_div = '<div id="live-network"></div>' . "\r\n"
455                                 . "<script> var profile_uid = " . $_SESSION['uid']
456                                 . "; var netargs = '" . substr($a->cmd, 8)
457                                 . '?f='
458                                 . ((x($_GET, 'cid'))    ? '&cid='    . $_GET['cid']    : '')
459                                 . ((x($_GET, 'search')) ? '&search=' . $_GET['search'] : '')
460                                 . ((x($_GET, 'star'))   ? '&star='   . $_GET['star']   : '')
461                                 . ((x($_GET, 'order'))  ? '&order='  . $_GET['order']  : '')
462                                 . ((x($_GET, 'bmark'))  ? '&bmark='  . $_GET['bmark']  : '')
463                                 . ((x($_GET, 'liked'))  ? '&liked='  . $_GET['liked']  : '')
464                                 . ((x($_GET, 'conv'))   ? '&conv='   . $_GET['conv']   : '')
465                                 . ((x($_GET, 'nets'))   ? '&nets='   . $_GET['nets']   : '')
466                                 . ((x($_GET, 'cmin'))   ? '&cmin='   . $_GET['cmin']   : '')
467                                 . ((x($_GET, 'cmax'))   ? '&cmax='   . $_GET['cmax']   : '')
468                                 . ((x($_GET, 'file'))   ? '&file='   . $_GET['file']   : '')
469
470                                 . "'; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
471                 }
472         } elseif ($mode === 'profile') {
473                 $profile_owner = $a->profile['profile_uid'];
474
475                 if (!$update) {
476                         $tab = 'posts';
477                         if (x($_GET, 'tab')) {
478                                 $tab = notags(trim($_GET['tab']));
479                         }
480                         if ($tab === 'posts') {
481                                 /*
482                                  * This is ugly, but we can't pass the profile_uid through the session to the ajax updater,
483                                  * because browser prefetching might change it on us. We have to deliver it with the page.
484                                  */
485
486                                 $live_update_div = '<div id="live-profile"></div>' . "\r\n"
487                                         . "<script> var profile_uid = " . $a->profile['profile_uid']
488                                         . "; var netargs = '?f='; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
489                         }
490                 }
491         } elseif ($mode === 'notes') {
492                 $profile_owner = local_user();
493
494                 if (!$update) {
495                         $live_update_div = '<div id="live-notes"></div>' . "\r\n"
496                                 . "<script> var profile_uid = " . local_user()
497                                 . "; var netargs = '/?f='; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
498                 }
499         } elseif ($mode === 'display') {
500                 $profile_owner = $a->profile['uid'];
501
502                 if (!$update) {
503                         $live_update_div = '<div id="live-display"></div>' . "\r\n"
504                                 . "<script> var profile_uid = " . defaults($_SESSION, 'uid', 0) . ";"
505                                 . " var profile_page = 1; </script>";
506                 }
507         } elseif ($mode === 'community') {
508                 $items = conversation_add_children($items, true, $order, $uid);
509                 $profile_owner = 0;
510
511                 if (!$update) {
512                         $live_update_div = '<div id="live-community"></div>' . "\r\n"
513                                 . "<script> var profile_uid = -1; var netargs = '" . substr($a->cmd, 10)
514                                 ."/?f='; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
515                 }
516         } elseif ($mode === 'search') {
517                 $live_update_div = '<div id="live-search"></div>' . "\r\n";
518         }
519
520         $page_dropping = ((local_user() && local_user() == $profile_owner) ? true : false);
521
522         if (!$update) {
523                 $_SESSION['return_url'] = $a->query_string;
524         }
525
526         $cb = ['items' => $items, 'mode' => $mode, 'update' => $update, 'preview' => $preview];
527         Addon::callHooks('conversation_start',$cb);
528
529         $items = $cb['items'];
530
531         $conv_responses = [
532                 'like' => ['title' => L10n::t('Likes','title')], 'dislike' => ['title' => L10n::t('Dislikes','title')],
533                 'attendyes' => ['title' => L10n::t('Attending','title')], 'attendno' => ['title' => L10n::t('Not attending','title')], 'attendmaybe' => ['title' => L10n::t('Might attend','title')]
534         ];
535
536         // array with html for each thread (parent+comments)
537         $threads = [];
538         $threadsid = -1;
539
540         $page_template = get_markup_template("conversation.tpl");
541
542         if (!empty($items)) {
543                 if ($mode === 'community') {
544                         $writable = true;
545                 } else {
546                         $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], [Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
547                 }
548
549                 if (!local_user()) {
550                         $writable = false;
551                 }
552
553                 if (in_array($mode, ['network-new', 'search', 'contact-posts'])) {
554
555                         /*
556                          * "New Item View" on network page or search page results
557                          * - just loop through the items and format them minimally for display
558                          */
559
560                         $tpl = 'search_item.tpl';
561
562                         foreach ($items as $item) {
563
564                                 if (!visible_activity($item)) {
565                                         continue;
566                                 }
567
568                                 if (in_array($item['author-id'], $blocklist)) {
569                                         continue;
570                                 }
571
572                                 $threadsid++;
573
574                                 $owner_url   = '';
575                                 $owner_name  = '';
576                                 $sparkle     = '';
577
578                                 // prevent private email from leaking.
579                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
580                                         continue;
581                                 }
582
583                                 $profile_name = $item['author-name'];
584                                 if (!empty($item['author-link']) && empty($item['author-name'])) {
585                                         $profile_name = $item['author-link'];
586                                 }
587
588                                 $tags = Term::populateTagsFromItem($item);
589
590                                 $author = ['uid' => 0, 'id' => $item['author-id'],
591                                         'network' => $item['author-network'], 'url' => $item['author-link']];
592                                 $profile_link = Contact::magicLinkbyContact($author);
593
594                                 if (strpos($profile_link, 'redir/') === 0) {
595                                         $sparkle = ' sparkle';
596                                 }
597
598                                 $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
599                                 Addon::callHooks('render_location',$locate);
600
601                                 $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
602
603                                 localize_item($item);
604                                 if ($mode === 'network-new') {
605                                         $dropping = true;
606                                 } else {
607                                         $dropping = false;
608                                 }
609
610                                 $drop = [
611                                         'dropping' => $dropping,
612                                         'pagedrop' => $page_dropping,
613                                         'select' => L10n::t('Select'),
614                                         'delete' => L10n::t('Delete'),
615                                 ];
616
617                                 $star = false;
618                                 $isstarred = "unstarred";
619
620                                 $lock = false;
621                                 $likebuttons = false;
622
623                                 $body = prepare_body($item, true, $preview);
624
625                                 list($categories, $folders) = get_cats_and_terms($item);
626
627                                 $profile_name_e = $profile_name;
628
629                                 if (!empty($item['content-warning']) && PConfig::get(local_user(), 'system', 'disable_cw', false)) {
630                                         $title_e = ucfirst($item['content-warning']);
631                                 } else {
632                                         $title_e = $item['title'];
633                                 }
634
635                                 $body_e = $body;
636                                 $tags_e = $tags['tags'];
637                                 $hashtags_e = $tags['hashtags'];
638                                 $mentions_e = $tags['mentions'];
639                                 $location_e = $location;
640                                 $owner_name_e = $owner_name;
641
642                                 $tmp_item = [
643                                         'template' => $tpl,
644                                         'id' => ($preview ? 'P0' : $item['id']),
645                                         'guid' => ($preview ? 'Q0' : $item['guid']),
646                                         'network' => $item['network'],
647                                         'network_name' => ContactSelector::networkToName($item['network'], $profile_link),
648                                         'linktitle' => L10n::t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
649                                         'profile_url' => $profile_link,
650                                         'item_photo_menu' => item_photo_menu($item),
651                                         'name' => $profile_name_e,
652                                         'sparkle' => $sparkle,
653                                         'lock' => $lock,
654                                         'thumb' => System::removedBaseUrl(ProxyUtils::proxifyUrl($item['author-avatar'], false, ProxyUtils::SIZE_THUMB)),
655                                         'title' => $title_e,
656                                         'body' => $body_e,
657                                         'tags' => $tags_e,
658                                         'hashtags' => $hashtags_e,
659                                         'mentions' => $mentions_e,
660                                         'txt_cats' => L10n::t('Categories:'),
661                                         'txt_folders' => L10n::t('Filed under:'),
662                                         'has_cats' => ((count($categories)) ? 'true' : ''),
663                                         'has_folders' => ((count($folders)) ? 'true' : ''),
664                                         'categories' => $categories,
665                                         'folders' => $folders,
666                                         'text' => strip_tags($body_e),
667                                         'localtime' => DateTimeFormat::local($item['created'], 'r'),
668                                         'ago' => (($item['app']) ? L10n::t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
669                                         'location' => $location_e,
670                                         'indent' => '',
671                                         'owner_name' => $owner_name_e,
672                                         'owner_url' => $owner_url,
673                                         'owner_photo' => System::removedBaseUrl(ProxyUtils::proxifyUrl($item['owner-avatar'], false, ProxyUtils::SIZE_THUMB)),
674                                         'plink' => get_plink($item),
675                                         'edpost' => false,
676                                         'isstarred' => $isstarred,
677                                         'star' => $star,
678                                         'drop' => $drop,
679                                         'vote' => $likebuttons,
680                                         'like' => '',
681                                         'dislike' => '',
682                                         'comment' => '',
683                                         'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> L10n::t('View in context')]),
684                                         'previewing' => $previewing,
685                                         'wait' => L10n::t('Please wait'),
686                                         'thread_level' => 1,
687                                 ];
688
689                                 $arr = ['item' => $item, 'output' => $tmp_item];
690                                 Addon::callHooks('display_item', $arr);
691
692                                 $threads[$threadsid]['id'] = $item['id'];
693                                 $threads[$threadsid]['network'] = $item['network'];
694                                 $threads[$threadsid]['items'] = [$arr['output']];
695
696                         }
697                 } else {
698                         // Normal View
699                         $page_template = get_markup_template("threaded_conversation.tpl");
700
701                         $conv = new Thread($mode, $preview, $writable);
702
703                         /*
704                          * get all the topmost parents
705                          * this shouldn't be needed, as we should have only them in our array
706                          * But for now, this array respects the old style, just in case
707                          */
708                         foreach ($items as $item) {
709                                 if (in_array($item['author-id'], $blocklist)) {
710                                         continue;
711                                 }
712
713                                 // Can we put this after the visibility check?
714                                 builtin_activity_puller($item, $conv_responses);
715
716                                 // Only add what is visible
717                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
718                                         continue;
719                                 }
720
721                                 if (!visible_activity($item)) {
722                                         continue;
723                                 }
724
725                                 /// @todo Check if this call is needed or not
726                                 $arr = ['item' => $item];
727                                 Addon::callHooks('display_item', $arr);
728
729                                 $item['pagedrop'] = $page_dropping;
730
731                                 if ($item['id'] == $item['parent']) {
732                                         $item_object = new Post($item);
733                                         $conv->addParent($item_object);
734                                 }
735                         }
736
737                         $threads = $conv->getTemplateData($conv_responses);
738                         if (!$threads) {
739                                 logger('[ERROR] conversation : Failed to get template data.', LOGGER_DEBUG);
740                                 $threads = [];
741                         }
742                 }
743         }
744
745         $o = replace_macros($page_template, [
746                 '$baseurl' => System::baseUrl($ssl_state),
747                 '$return_path' => $a->query_string,
748                 '$live_update' => $live_update_div,
749                 '$remove' => L10n::t('remove'),
750                 '$mode' => $mode,
751                 '$user' => $a->user,
752                 '$threads' => $threads,
753                 '$dropping' => ($page_dropping && Feature::isEnabled(local_user(), 'multi_delete') ? L10n::t('Delete Selected Items') : False),
754         ]);
755
756         return $o;
757 }
758
759 /**
760  * @brief Add comments to top level entries that had been fetched before
761  *
762  * The system will fetch the comments for the local user whenever possible.
763  * This behaviour is currently needed to allow commenting on Friendica posts.
764  *
765  * @param array $parents Parent items
766  *
767  * @return array items with parents and comments
768  */
769 function conversation_add_children(array $parents, $block_authors, $order, $uid) {
770         $max_comments = Config::get('system', 'max_comments', 100);
771
772         $params = ['order' => ['uid', 'commented' => true]];
773
774         if ($max_comments > 0) {
775                 $params['limit'] = $max_comments;
776         }
777
778         $items = [];
779
780         foreach ($parents AS $parent) {
781                 $condition = ["`item`.`parent-uri` = ? AND `item`.`uid` IN (0, ?) ",
782                         $parent['uri'], local_user()];
783                 if ($block_authors) {
784                         $condition[0] .= "AND NOT `author`.`hidden`";
785                 }
786                 $thread_items = Item::selectForUser(local_user(), [], $condition, $params);
787
788                 $comments = Item::inArray($thread_items);
789
790                 if (count($comments) != 0) {
791                         $items = array_merge($items, $comments);
792                 }
793         }
794
795         foreach ($items as $index => $item) {
796                 if ($item['uid'] == 0) {
797                         $items[$index]['writable'] = in_array($item['network'], [Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
798                 }
799         }
800
801         $items = conv_sort($items, $order);
802
803         return $items;
804 }
805
806 function item_photo_menu($item) {
807         $sub_link = '';
808         $poke_link = '';
809         $contact_url = '';
810         $pm_url = '';
811         $status_link = '';
812         $photos_link = '';
813         $posts_link = '';
814
815         if (local_user() && local_user() == $item['uid'] && $item['parent'] == $item['id'] && !$item['self']) {
816                 $sub_link = 'javascript:dosubthread(' . $item['id'] . '); return false;';
817         }
818
819         $author = ['uid' => 0, 'id' => $item['author-id'],
820                 'network' => $item['author-network'], 'url' => $item['author-link']];
821         $profile_link = Contact::magicLinkbyContact($author);
822         $sparkle = (strpos($profile_link, 'redir/') === 0);
823
824         $cid = 0;
825         $network = '';
826         $rel = 0;
827         $condition = ['uid' => local_user(), 'nurl' => normalise_link($item['author-link'])];
828         $contact = DBA::selectFirst('contact', ['id', 'network', 'rel'], $condition);
829         if (DBA::isResult($contact)) {
830                 $cid = $contact['id'];
831                 $network = $contact['network'];
832                 $rel = $contact['rel'];
833         }
834
835         if ($sparkle) {
836                 $status_link = $profile_link . '?url=status';
837                 $photos_link = $profile_link . '?url=photos';
838                 $profile_link = $profile_link . '?url=profile';
839         }
840
841         if ($cid && !$item['self']) {
842                 $poke_link = 'poke/?f=&c=' . $cid;
843                 $contact_url = 'contacts/' . $cid;
844                 $posts_link = 'network?cid=' . $cid;
845
846                 if (in_array($network, [Protocol::DFRN, Protocol::DIASPORA])) {
847                         $pm_url = 'message/new/' . $cid;
848                 }
849         }
850
851         if (local_user()) {
852                 $menu = [
853                         L10n::t('Follow Thread') => $sub_link,
854                         L10n::t('View Status') => $status_link,
855                         L10n::t('View Profile') => $profile_link,
856                         L10n::t('View Photos') => $photos_link,
857                         L10n::t('Network Posts') => $posts_link,
858                         L10n::t('View Contact') => $contact_url,
859                         L10n::t('Send PM') => $pm_url
860                 ];
861
862                 if ($network == Protocol::DFRN) {
863                         $menu[L10n::t("Poke")] = $poke_link;
864                 }
865
866                 if ((($cid == 0) || ($rel == Contact::FOLLOWER)) &&
867                         in_array($item['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) {
868                         $menu[L10n::t('Connect/Follow')] = 'follow?url=' . urlencode($item['author-link']);
869                 }
870         } else {
871                 $menu = [L10n::t('View Profile') => $item['author-link']];
872         }
873
874         $args = ['item' => $item, 'menu' => $menu];
875
876         Addon::callHooks('item_photo_menu', $args);
877
878         $menu = $args['menu'];
879
880         $o = '';
881         foreach ($menu as $k => $v) {
882                 if (strpos($v, 'javascript:') === 0) {
883                         $v = substr($v, 11);
884                         $o .= '<li role="menuitem"><a onclick="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
885                 } elseif ($v!='') {
886                         $o .= '<li role="menuitem"><a href="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
887                 }
888         }
889         return $o;
890 }
891
892 /**
893  * @brief Checks item to see if it is one of the builtin activities (like/dislike, event attendance, consensus items, etc.)
894  * Increments the count of each matching activity and adds a link to the author as needed.
895  *
896  * @param array $item
897  * @param array &$conv_responses (already created with builtin activity structure)
898  * @return void
899  */
900 function builtin_activity_puller($item, &$conv_responses) {
901         foreach ($conv_responses as $mode => $v) {
902                 $url = '';
903                 $sparkle = '';
904
905                 switch ($mode) {
906                         case 'like':
907                                 $verb = ACTIVITY_LIKE;
908                                 break;
909                         case 'dislike':
910                                 $verb = ACTIVITY_DISLIKE;
911                                 break;
912                         case 'attendyes':
913                                 $verb = ACTIVITY_ATTEND;
914                                 break;
915                         case 'attendno':
916                                 $verb = ACTIVITY_ATTENDNO;
917                                 break;
918                         case 'attendmaybe':
919                                 $verb = ACTIVITY_ATTENDMAYBE;
920                                 break;
921                         default:
922                                 return;
923                 }
924
925                 if (activity_match($item['verb'], $verb) && ($item['id'] != $item['parent'])) {
926                         $author = ['uid' => 0, 'id' => $item['author-id'],
927                                 'network' => $item['author-network'], 'url' => $item['author-link']];
928                         $url = Contact::magicLinkbyContact($author);
929                         if (strpos($url, 'redir/') === 0) {
930                                 $sparkle = ' class="sparkle" ';
931                         }
932
933                         $url = '<a href="'. $url . '"'. $sparkle .'>' . htmlentities($item['author-name']) . '</a>';
934
935                         if (!x($item, 'thr-parent')) {
936                                 $item['thr-parent'] = $item['parent-uri'];
937                         }
938
939                         if (!(isset($conv_responses[$mode][$item['thr-parent'] . '-l'])
940                                 && is_array($conv_responses[$mode][$item['thr-parent'] . '-l']))) {
941                                 $conv_responses[$mode][$item['thr-parent'] . '-l'] = [];
942                         }
943
944                         // only list each unique author once
945                         if (in_array($url,$conv_responses[$mode][$item['thr-parent'] . '-l'])) {
946                                 continue;
947                         }
948
949                         if (!isset($conv_responses[$mode][$item['thr-parent']])) {
950                                 $conv_responses[$mode][$item['thr-parent']] = 1;
951                         } else {
952                                 $conv_responses[$mode][$item['thr-parent']] ++;
953                         }
954
955                         if (public_contact() == $item['author-id']) {
956                                 $conv_responses[$mode][$item['thr-parent'] . '-self'] = 1;
957                         }
958
959                         $conv_responses[$mode][$item['thr-parent'] . '-l'][] = $url;
960
961                         // there can only be one activity verb per item so if we found anything, we can stop looking
962                         return;
963                 }
964         }
965 }
966
967 /**
968  * Format the vote text for a profile item
969  * @param int $cnt = number of people who vote the item
970  * @param array $arr = array of pre-linked names of likers/dislikers
971  * @param string $type = one of 'like, 'dislike', 'attendyes', 'attendno', 'attendmaybe'
972  * @param int $id  = item id
973  * @return string formatted text
974  */
975 function format_like($cnt, array $arr, $type, $id) {
976         $o = '';
977         $expanded = '';
978
979         if ($cnt == 1) {
980                 $likers = $arr[0];
981
982                 // Phrase if there is only one liker. In other cases it will be uses for the expanded
983                 // list which show all likers
984                 switch ($type) {
985                         case 'like' :
986                                 $phrase = L10n::t('%s likes this.', $likers);
987                                 break;
988                         case 'dislike' :
989                                 $phrase = L10n::t('%s doesn\'t like this.', $likers);
990                                 break;
991                         case 'attendyes' :
992                                 $phrase = L10n::t('%s attends.', $likers);
993                                 break;
994                         case 'attendno' :
995                                 $phrase = L10n::t('%s doesn\'t attend.', $likers);
996                                 break;
997                         case 'attendmaybe' :
998                                 $phrase = L10n::t('%s attends maybe.', $likers);
999                                 break;
1000                 }
1001         }
1002
1003         if ($cnt > 1) {
1004                 $total = count($arr);
1005                 if ($total >= MAX_LIKERS) {
1006                         $arr = array_slice($arr, 0, MAX_LIKERS - 1);
1007                 }
1008                 if ($total < MAX_LIKERS) {
1009                         $last = L10n::t('and') . ' ' . $arr[count($arr)-1];
1010                         $arr2 = array_slice($arr, 0, -1);
1011                         $str = implode(', ', $arr2) . ' ' . $last;
1012                 }
1013                 if ($total >= MAX_LIKERS) {
1014                         $str = implode(', ', $arr);
1015                         $str .= L10n::t('and %d other people', $total - MAX_LIKERS);
1016                 }
1017
1018                 $likers = $str;
1019
1020                 $spanatts = "class=\"fakelink\" onclick=\"openClose('{$type}list-$id');\"";
1021
1022                 switch ($type) {
1023                         case 'like':
1024                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> like this', $spanatts, $cnt);
1025                                 $explikers = L10n::t('%s like this.', $likers);
1026                                 break;
1027                         case 'dislike':
1028                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> don\'t like this', $spanatts, $cnt);
1029                                 $explikers = L10n::t('%s don\'t like this.', $likers);
1030                                 break;
1031                         case 'attendyes':
1032                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> attend', $spanatts, $cnt);
1033                                 $explikers = L10n::t('%s attend.', $likers);
1034                                 break;
1035                         case 'attendno':
1036                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> don\'t attend', $spanatts, $cnt);
1037                                 $explikers = L10n::t('%s don\'t attend.', $likers);
1038                                 break;
1039                         case 'attendmaybe':
1040                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> attend maybe', $spanatts, $cnt);
1041                                 $explikers = L10n::t('%s attend maybe.', $likers);
1042                                 break;
1043                 }
1044
1045                 $expanded .= "\t" . '<div class="wall-item-' . $type . '-expanded" id="' . $type . 'list-' . $id . '" style="display: none;" >' . $explikers . EOL . '</div>';
1046         }
1047
1048         $phrase .= EOL ;
1049         $o .= replace_macros(get_markup_template('voting_fakelink.tpl'), [
1050                 '$phrase' => $phrase,
1051                 '$type' => $type,
1052                 '$id' => $id
1053         ]);
1054         $o .= $expanded;
1055
1056         return $o;
1057 }
1058
1059 function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
1060 {
1061         $o = '';
1062
1063         $geotag = x($x, 'allow_location') ? replace_macros(get_markup_template('jot_geotag.tpl'), []) : '';
1064
1065         $tpl = get_markup_template('jot-header.tpl');
1066         $a->page['htmlhead'] .= replace_macros($tpl, [
1067                 '$newpost'   => 'true',
1068                 '$baseurl'   => System::baseUrl(true),
1069                 '$geotag'    => $geotag,
1070                 '$nickname'  => $x['nickname'],
1071                 '$ispublic'  => L10n::t('Visible to <strong>everybody</strong>'),
1072                 '$linkurl'   => L10n::t('Please enter a link URL:'),
1073                 '$vidurl'    => L10n::t("Please enter a video link/URL:"),
1074                 '$audurl'    => L10n::t("Please enter an audio link/URL:"),
1075                 '$term'      => L10n::t('Tag term:'),
1076                 '$fileas'    => L10n::t('Save to Folder:'),
1077                 '$whereareu' => L10n::t('Where are you right now?'),
1078                 '$delitems'  => L10n::t("Delete item\x28s\x29?")
1079         ]);
1080
1081         $tpl = get_markup_template('jot-end.tpl');
1082         $a->page['end'] .= replace_macros($tpl, [
1083                 '$newpost'   => 'true',
1084                 '$baseurl'   => System::baseUrl(true),
1085                 '$geotag'    => $geotag,
1086                 '$nickname'  => $x['nickname'],
1087                 '$ispublic'  => L10n::t('Visible to <strong>everybody</strong>'),
1088                 '$linkurl'   => L10n::t('Please enter a link URL:'),
1089                 '$vidurl'    => L10n::t("Please enter a video link/URL:"),
1090                 '$audurl'    => L10n::t("Please enter an audio link/URL:"),
1091                 '$term'      => L10n::t('Tag term:'),
1092                 '$fileas'    => L10n::t('Save to Folder:'),
1093                 '$whereareu' => L10n::t('Where are you right now?')
1094         ]);
1095
1096         $jotplugins = '';
1097         Addon::callHooks('jot_tool', $jotplugins);
1098
1099         // Private/public post links for the non-JS ACL form
1100         $private_post = 1;
1101         if (x($_REQUEST, 'public')) {
1102                 $private_post = 0;
1103         }
1104
1105         $query_str = $a->query_string;
1106         if (strpos($query_str, 'public=1') !== false) {
1107                 $query_str = str_replace(['?public=1', '&public=1'], ['', ''], $query_str);
1108         }
1109
1110         /*
1111          * I think $a->query_string may never have ? in it, but I could be wrong
1112          * It looks like it's from the index.php?q=[etc] rewrite that the web
1113          * server does, which converts any ? to &, e.g. suggest&ignore=61 for suggest?ignore=61
1114          */
1115         if (strpos($query_str, '?') === false) {
1116                 $public_post_link = '?public=1';
1117         } else {
1118                 $public_post_link = '&public=1';
1119         }
1120
1121         // $tpl = replace_macros($tpl,array('$jotplugins' => $jotplugins));
1122         $tpl = get_markup_template("jot.tpl");
1123
1124         $o .= replace_macros($tpl,[
1125                 '$new_post' => L10n::t('New Post'),
1126                 '$return_path'  => $query_str,
1127                 '$action'       => 'item',
1128                 '$share'        => defaults($x, 'button', L10n::t('Share')),
1129                 '$upload'       => L10n::t('Upload photo'),
1130                 '$shortupload'  => L10n::t('upload photo'),
1131                 '$attach'       => L10n::t('Attach file'),
1132                 '$shortattach'  => L10n::t('attach file'),
1133                 '$weblink'      => L10n::t('Insert web link'),
1134                 '$shortweblink' => L10n::t('web link'),
1135                 '$video'        => L10n::t('Insert video link'),
1136                 '$shortvideo'   => L10n::t('video link'),
1137                 '$audio'        => L10n::t('Insert audio link'),
1138                 '$shortaudio'   => L10n::t('audio link'),
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'], '');
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 }