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