]> git.mxchange.org Git - friendica.git/blob - include/conversation.php
Issue 4507: Superblock does now work on comments as well
[friendica.git] / include / conversation.php
1 <?php
2 /**
3  * @file include/conversation.php
4  */
5
6 use Friendica\App;
7 use Friendica\Content\ContactSelector;
8 use Friendica\Content\Feature;
9 use Friendica\Content\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\DBA;
16 use Friendica\Model\Contact;
17 use Friendica\Model\Item;
18 use Friendica\Model\Profile;
19 use Friendica\Model\Term;
20 use Friendica\Object\Post;
21 use Friendica\Object\Thread;
22 use Friendica\Util\DateTimeFormat;
23 use Friendica\Util\Proxy as ProxyUtils;
24 use Friendica\Util\Temporal;
25 use Friendica\Util\XML;
26
27 function item_extract_images($body) {
28
29         $saved_image = [];
30         $orig_body = $body;
31         $new_body = '';
32
33         $cnt = 0;
34         $img_start = strpos($orig_body, '[img');
35         $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
36         $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
37         while (($img_st_close !== false) && ($img_end !== false)) {
38
39                 $img_st_close++; // make it point to AFTER the closing bracket
40                 $img_end += $img_start;
41
42                 if (!strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
43                         // This is an embedded image
44
45                         $saved_image[$cnt] = substr($orig_body, $img_start + $img_st_close, $img_end - ($img_start + $img_st_close));
46                         $new_body = $new_body . substr($orig_body, 0, $img_start) . '[!#saved_image' . $cnt . '#!]';
47
48                         $cnt++;
49                 } else {
50                         $new_body = $new_body . substr($orig_body, 0, $img_end + strlen('[/img]'));
51                 }
52
53                 $orig_body = substr($orig_body, $img_end + strlen('[/img]'));
54
55                 if ($orig_body === false) {
56                         // in case the body ends on a closing image tag
57                         $orig_body = '';
58                 }
59
60                 $img_start = strpos($orig_body, '[img');
61                 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
62                 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
63         }
64
65         $new_body = $new_body . $orig_body;
66
67         return ['body' => $new_body, 'images' => $saved_image];
68 }
69
70 function item_redir_and_replace_images($body, $images, $cid) {
71
72         $origbody = $body;
73         $newbody = '';
74
75         $cnt = 1;
76         $pos = BBCode::getTagPosition($origbody, 'url', 0);
77         while ($pos !== false && $cnt < 1000) {
78
79                 $search = '/\[url\=(.*?)\]\[!#saved_image([0-9]*)#!\]\[\/url\]' . '/is';
80                 $replace = '[url=' . System::baseUrl() . '/redir/' . $cid
81                                    . '?f=1&url=' . '$1' . '][!#saved_image' . '$2' .'#!][/url]';
82
83                 $newbody .= substr($origbody, 0, $pos['start']['open']);
84                 $subject = substr($origbody, $pos['start']['open'], $pos['end']['close'] - $pos['start']['open']);
85                 $origbody = substr($origbody, $pos['end']['close']);
86                 if ($origbody === false) {
87                         $origbody = '';
88                 }
89
90                 $subject = preg_replace($search, $replace, $subject);
91                 $newbody .= $subject;
92
93                 $cnt++;
94                 // Isn't this supposed to use $cnt value for $occurrences? - @MrPetovan
95                 $pos = BBCode::getTagPosition($origbody, 'url', 0);
96         }
97         $newbody .= $origbody;
98
99         $cnt = 0;
100         foreach ($images as $image) {
101                 /*
102                  * We're depending on the property of 'foreach' (specified on the PHP website) that
103                  * it loops over the array starting from the first element and going sequentially
104                  * to the last element.
105                  */
106                 $newbody = str_replace('[!#saved_image' . $cnt . '#!]', '[img]' . $image . '[/img]', $newbody);
107                 $cnt++;
108         }
109         return $newbody;
110 }
111
112 /**
113  * Render actions localized
114  */
115 function localize_item(&$item)
116 {
117         $extracted = item_extract_images($item['body']);
118         if ($extracted['images']) {
119                 $item['body'] = item_redir_and_replace_images($extracted['body'], $extracted['images'], $item['contact-id']);
120         }
121
122         /*
123         heluecht 2018-06-19: from my point of view this whole code part is useless.
124         It just renders the body message of technical posts (Like, dislike, ...).
125         But: The body isn't visible at all. So we do this stuff just because we can.
126         Even if these messages were visible, this would only mean that something went wrong.
127         During the further steps of the database restructuring I would like to address this issue.
128         */
129
130         $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
131         if (activity_match($item['verb'], ACTIVITY_LIKE)
132                 || activity_match($item['verb'], ACTIVITY_DISLIKE)
133                 || activity_match($item['verb'], ACTIVITY_ATTEND)
134                 || activity_match($item['verb'], ACTIVITY_ATTENDNO)
135                 || activity_match($item['verb'], ACTIVITY_ATTENDMAYBE)) {
136
137                 $fields = ['author-link', 'author-name', 'verb', 'object-type', 'resource-id', 'body', 'plink'];
138                 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
139                 if (!DBA::isResult($obj)) {
140                         return;
141                 }
142
143                 $author  = '[url=' . $item['author-link'] . ']' . $item['author-name'] . '[/url]';
144                 $objauthor =  '[url=' . $obj['author-link'] . ']' . $obj['author-name'] . '[/url]';
145
146                 switch ($obj['verb']) {
147                         case ACTIVITY_POST:
148                                 switch ($obj['object-type']) {
149                                         case ACTIVITY_OBJ_EVENT:
150                                                 $post_type = L10n::t('event');
151                                                 break;
152                                         default:
153                                                 $post_type = L10n::t('status');
154                                 }
155                                 break;
156                         default:
157                                 if ($obj['resource-id']) {
158                                         $post_type = L10n::t('photo');
159                                         $m = [];
160                                         preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
161                                         $rr['plink'] = $m[1];
162                                 } else {
163                                         $post_type = L10n::t('status');
164                                 }
165                 }
166
167                 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
168
169                 if (activity_match($item['verb'], ACTIVITY_LIKE)) {
170                         $bodyverb = L10n::t('%1$s likes %2$s\'s %3$s');
171                 } elseif (activity_match($item['verb'], ACTIVITY_DISLIKE)) {
172                         $bodyverb = L10n::t('%1$s doesn\'t like %2$s\'s %3$s');
173                 } elseif (activity_match($item['verb'], ACTIVITY_ATTEND)) {
174                         $bodyverb = L10n::t('%1$s attends %2$s\'s %3$s');
175                 } elseif (activity_match($item['verb'], ACTIVITY_ATTENDNO)) {
176                         $bodyverb = L10n::t('%1$s doesn\'t attend %2$s\'s %3$s');
177                 } elseif (activity_match($item['verb'], ACTIVITY_ATTENDMAYBE)) {
178                         $bodyverb = L10n::t('%1$s attends maybe %2$s\'s %3$s');
179                 }
180
181                 $item['body'] = sprintf($bodyverb, $author, $objauthor, $plink);
182         }
183
184         if (activity_match($item['verb'], ACTIVITY_FRIEND)) {
185
186                 if ($item['object-type']=="" || $item['object-type']!== ACTIVITY_OBJ_PERSON) return;
187
188                 $Aname = $item['author-name'];
189                 $Alink = $item['author-link'];
190
191                 $xmlhead="<"."?xml version='1.0' encoding='UTF-8' ?".">";
192
193                 $obj = XML::parseString($xmlhead.$item['object']);
194                 $links = XML::parseString($xmlhead."<links>".unxmlify($obj->link)."</links>");
195
196                 $Bname = $obj->title;
197                 $Blink = "";
198                 $Bphoto = "";
199                 foreach ($links->link as $l) {
200                         $atts = $l->attributes();
201                         switch ($atts['rel']) {
202                                 case "alternate": $Blink = $atts['href'];
203                                 case "photo": $Bphoto = $atts['href'];
204                         }
205                 }
206
207                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
208                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
209                 if ($Bphoto != "") {
210                         $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img]' . $Bphoto . '[/img][/url]';
211                 }
212
213                 $item['body'] = L10n::t('%1$s is now friends with %2$s', $A, $B)."\n\n\n".$Bphoto;
214
215         }
216         if (stristr($item['verb'], ACTIVITY_POKE)) {
217                 $verb = urldecode(substr($item['verb'],strpos($item['verb'],'#')+1));
218                 if (!$verb) {
219                         return;
220                 }
221                 if ($item['object-type']=="" || $item['object-type']!== ACTIVITY_OBJ_PERSON) {
222                         return;
223                 }
224
225                 $Aname = $item['author-name'];
226                 $Alink = $item['author-link'];
227
228                 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
229
230                 $obj = XML::parseString($xmlhead.$item['object']);
231                 $links = XML::parseString($xmlhead."<links>".unxmlify($obj->link)."</links>");
232
233                 $Bname = $obj->title;
234                 $Blink = "";
235                 $Bphoto = "";
236                 foreach ($links->link as $l) {
237                         $atts = $l->attributes();
238                         switch ($atts['rel']) {
239                                 case "alternate": $Blink = $atts['href'];
240                                 case "photo": $Bphoto = $atts['href'];
241                         }
242                 }
243
244                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
245                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
246                 if ($Bphoto != "") {
247                         $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img=80x80]' . $Bphoto . '[/img][/url]';
248                 }
249
250                 /*
251                  * we can't have a translation string with three positions but no distinguishable text
252                  * So here is the translate string.
253                  */
254                 $txt = L10n::t('%1$s poked %2$s');
255
256                 // now translate the verb
257                 $poked_t = trim(sprintf($txt, "", ""));
258                 $txt = str_replace($poked_t, L10n::t($verb), $txt);
259
260                 // then do the sprintf on the translation string
261
262                 $item['body'] = sprintf($txt, $A, $B). "\n\n\n" . $Bphoto;
263
264         }
265
266         if (activity_match($item['verb'], ACTIVITY_TAG)) {
267                 $fields = ['author-id', 'author-link', 'author-name', 'author-network',
268                         'verb', 'object-type', 'resource-id', 'body', 'plink'];
269                 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
270                 if (!DBA::isResult($obj)) {
271                         return;
272                 }
273
274                 $author_arr = ['uid' => 0, 'id' => $item['author-id'],
275                         'network' => $item['author-network'], 'url' => $item['author-link']];
276                 $author  = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $item['author-name'] . '[/url]';
277
278                 $author_arr = ['uid' => 0, 'id' => $obj['author-id'],
279                         'network' => $obj['author-network'], 'url' => $obj['author-link']];
280                 $objauthor  = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $obj['author-name'] . '[/url]';
281
282                 switch ($obj['verb']) {
283                         case ACTIVITY_POST:
284                                 switch ($obj['object-type']) {
285                                         case ACTIVITY_OBJ_EVENT:
286                                                 $post_type = L10n::t('event');
287                                                 break;
288                                         default:
289                                                 $post_type = L10n::t('status');
290                                 }
291                                 break;
292                         default:
293                                 if ($obj['resource-id']) {
294                                         $post_type = L10n::t('photo');
295                                         $m=[]; preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
296                                         $rr['plink'] = $m[1];
297                                 } else {
298                                         $post_type = L10n::t('status');
299                                 }
300                                 // Let's break everthing ... ;-)
301                                 break;
302                 }
303                 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
304
305                 $parsedobj = XML::parseString($xmlhead.$item['object']);
306
307                 $tag = sprintf('#[url=%s]%s[/url]', $parsedobj->id, $parsedobj->content);
308                 $item['body'] = L10n::t('%1$s tagged %2$s\'s %3$s with %4$s', $author, $objauthor, $plink, $tag);
309         }
310
311         if (activity_match($item['verb'], ACTIVITY_FAVORITE)) {
312                 if ($item['object-type'] == "") {
313                         return;
314                 }
315
316                 $Aname = $item['author-name'];
317                 $Alink = $item['author-link'];
318
319                 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
320
321                 $obj = XML::parseString($xmlhead.$item['object']);
322                 if (strlen($obj->id)) {
323                         $fields = ['author-link', 'author-name', 'plink'];
324                         $target = Item::selectFirst($fields, ['uri' => $obj->id, 'uid' => $item['uid']]);
325                         if (DBA::isResult($target) && $target['plink']) {
326                                 $Bname = $target['author-name'];
327                                 $Blink = $target['author-link'];
328                                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
329                                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
330                                 $P = '[url=' . $target['plink'] . ']' . L10n::t('post/item') . '[/url]';
331                                 $item['body'] = L10n::t('%1$s marked %2$s\'s %3$s as favorite', $A, $B, $P)."\n";
332                         }
333                 }
334         }
335         $matches = null;
336         if (preg_match_all('/@\[url=(.*?)\]/is', $item['body'], $matches, PREG_SET_ORDER)) {
337                 foreach ($matches as $mtch) {
338                         if (!strpos($mtch[1], 'zrl=')) {
339                                 $item['body'] = str_replace($mtch[0], '@[url=' . Contact::magicLink($mtch[1]) . ']', $item['body']);
340                         }
341                 }
342         }
343
344         // add zrl's to public images
345         $photo_pattern = "/\[url=(.*?)\/photos\/(.*?)\/image\/(.*?)\]\[img(.*?)\]h(.*?)\[\/img\]\[\/url\]/is";
346         if (preg_match($photo_pattern, $item['body'])) {
347                 $photo_replace = '[url=' . Profile::zrl('$1' . '/photos/' . '$2' . '/image/' . '$3' ,true) . '][img' . '$4' . ']h' . '$5'  . '[/img][/url]';
348                 $item['body'] = BBCode::pregReplaceInTag($photo_pattern, $photo_replace, 'url', $item['body']);
349         }
350
351         // add sparkle links to appropriate permalinks
352         $author = ['uid' => 0, 'id' => $item['author-id'],
353                 'network' => $item['author-network'], 'url' => $item['author-link']];
354
355         if (!empty($item['plink'])) {
356                 $item['plink'] = Contact::magicLinkbyContact($author, $item['plink']);
357         }
358 }
359
360 /**
361  * Count the total of comments on this item and its desendants
362  * @TODO proper type-hint + doc-tag
363  */
364 function count_descendants($item) {
365         $total = count($item['children']);
366
367         if ($total > 0) {
368                 foreach ($item['children'] as $child) {
369                         if (!visible_activity($child)) {
370                                 $total --;
371                         }
372                         $total += count_descendants($child);
373                 }
374         }
375
376         return $total;
377 }
378
379 function visible_activity($item) {
380
381         /*
382          * likes (etc.) can apply to other things besides posts. Check if they are post children,
383          * in which case we handle them specially
384          */
385         $hidden_activities = [ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE];
386         foreach ($hidden_activities as $act) {
387                 if (activity_match($item['verb'], $act)) {
388                         return false;
389                 }
390         }
391
392         // @TODO below if() block can be rewritten to a single line: $isVisible = allConditionsHere;
393         if (activity_match($item['verb'], ACTIVITY_FOLLOW) && $item['object-type'] === ACTIVITY_OBJ_NOTE && empty($item['self']) && $item['uid'] == local_user()) {
394                 return false;
395         }
396
397         return true;
398 }
399
400 function conv_get_blocklist()
401 {
402         if (!local_user()) {
403                 return [];
404         }
405
406         $str_blocked = PConfig::get(local_user(), 'system', 'blocked');
407         if (empty($str_blocked)) {
408                 return [];
409         }
410
411         $blocklist = [];
412
413         foreach (explode(',', $str_blocked) as $entry) {
414                 $cid = Contact::getIdForURL(trim($entry), 0, true);
415                 if (!empty($cid)) {
416                         $blocklist[] = $cid;
417                 }
418         }
419
420         return $blocklist;
421 }
422
423 /**
424  * "Render" a conversation or list of items for HTML display.
425  * There are two major forms of display:
426  *      - Sequential or unthreaded ("New Item View" or search results)
427  *      - conversation view
428  * The $mode parameter decides between the various renderings and also
429  * figures out how to determine page owner and other contextual items
430  * that are based on unique features of the calling module.
431  *
432  */
433 function conversation(App $a, array $items, $mode, $update, $preview = false, $order = 'commented', $uid = 0) {
434
435         $ssl_state = (local_user() ? true : false);
436
437         $profile_owner = 0;
438         $live_update_div = '';
439
440         $blocklist = conv_get_blocklist();
441
442         $previewing = (($preview) ? ' preview ' : '');
443
444         if ($mode === 'network') {
445                 $items = conversation_add_children($items, false, $order, $uid);
446                 $profile_owner = local_user();
447                 if (!$update) {
448                         /*
449                          * The special div is needed for liveUpdate to kick in for this page.
450                          * We only launch liveUpdate if you aren't filtering in some incompatible
451                          * way and also you aren't writing a comment (discovered in javascript).
452                          */
453                         $live_update_div = '<div id="live-network"></div>' . "\r\n"
454                                 . "<script> var profile_uid = " . $_SESSION['uid']
455                                 . "; var netargs = '" . substr($a->cmd, 8)
456                                 . '?f='
457                                 . ((x($_GET, 'cid'))    ? '&cid='    . $_GET['cid']    : '')
458                                 . ((x($_GET, 'search')) ? '&search=' . $_GET['search'] : '')
459                                 . ((x($_GET, 'star'))   ? '&star='   . $_GET['star']   : '')
460                                 . ((x($_GET, 'order'))  ? '&order='  . $_GET['order']  : '')
461                                 . ((x($_GET, 'bmark'))  ? '&bmark='  . $_GET['bmark']  : '')
462                                 . ((x($_GET, 'liked'))  ? '&liked='  . $_GET['liked']  : '')
463                                 . ((x($_GET, 'conv'))   ? '&conv='   . $_GET['conv']   : '')
464                                 . ((x($_GET, 'nets'))   ? '&nets='   . $_GET['nets']   : '')
465                                 . ((x($_GET, 'cmin'))   ? '&cmin='   . $_GET['cmin']   : '')
466                                 . ((x($_GET, 'cmax'))   ? '&cmax='   . $_GET['cmax']   : '')
467                                 . ((x($_GET, 'file'))   ? '&file='   . $_GET['file']   : '')
468
469                                 . "'; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
470                 }
471         } elseif ($mode === 'profile') {
472                 $profile_owner = $a->profile['profile_uid'];
473
474                 if (!$update) {
475                         $tab = 'posts';
476                         if (x($_GET, 'tab')) {
477                                 $tab = notags(trim($_GET['tab']));
478                         }
479                         if ($tab === 'posts') {
480                                 /*
481                                  * This is ugly, but we can't pass the profile_uid through the session to the ajax updater,
482                                  * because browser prefetching might change it on us. We have to deliver it with the page.
483                                  */
484
485                                 $live_update_div = '<div id="live-profile"></div>' . "\r\n"
486                                         . "<script> var profile_uid = " . $a->profile['profile_uid']
487                                         . "; var netargs = '?f='; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
488                         }
489                 }
490         } elseif ($mode === 'notes') {
491                 $profile_owner = local_user();
492
493                 if (!$update) {
494                         $live_update_div = '<div id="live-notes"></div>' . "\r\n"
495                                 . "<script> var profile_uid = " . local_user()
496                                 . "; var netargs = '/?f='; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
497                 }
498         } elseif ($mode === 'display') {
499                 $profile_owner = $a->profile['uid'];
500
501                 if (!$update) {
502                         $live_update_div = '<div id="live-display"></div>' . "\r\n"
503                                 . "<script> var profile_uid = " . defaults($_SESSION, 'uid', 0) . ";"
504                                 . " var profile_page = 1; </script>";
505                 }
506         } elseif ($mode === 'community') {
507                 $items = conversation_add_children($items, true, $order, $uid);
508                 $profile_owner = 0;
509
510                 if (!$update) {
511                         $live_update_div = '<div id="live-community"></div>' . "\r\n"
512                                 . "<script> var profile_uid = -1; var netargs = '" . substr($a->cmd, 10)
513                                 ."/?f='; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
514                 }
515         } elseif ($mode === 'search') {
516                 $live_update_div = '<div id="live-search"></div>' . "\r\n";
517         }
518
519         $page_dropping = ((local_user() && local_user() == $profile_owner) ? true : false);
520
521         if (!$update) {
522                 $_SESSION['return_url'] = $a->query_string;
523         }
524
525         $cb = ['items' => $items, 'mode' => $mode, 'update' => $update, 'preview' => $preview];
526         Addon::callHooks('conversation_start',$cb);
527
528         $items = $cb['items'];
529
530         $conv_responses = [
531                 'like' => ['title' => L10n::t('Likes','title')], 'dislike' => ['title' => L10n::t('Dislikes','title')],
532                 'attendyes' => ['title' => L10n::t('Attending','title')], 'attendno' => ['title' => L10n::t('Not attending','title')], 'attendmaybe' => ['title' => L10n::t('Might attend','title')]
533         ];
534
535         // array with html for each thread (parent+comments)
536         $threads = [];
537         $threadsid = -1;
538
539         $page_template = get_markup_template("conversation.tpl");
540
541         if (!empty($items)) {
542                 if ($mode === 'community') {
543                         $writable = true;
544                 } else {
545                         $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], [NETWORK_OSTATUS, NETWORK_DIASPORA, NETWORK_DFRN]);
546                 }
547
548                 if (!local_user()) {
549                         $writable = false;
550                 }
551
552                 if (in_array($mode, ['network-new', 'search', 'contact-posts'])) {
553
554                         /*
555                          * "New Item View" on network page or search page results
556                          * - just loop through the items and format them minimally for display
557                          */
558
559                         $tpl = 'search_item.tpl';
560
561                         foreach ($items as $item) {
562
563                                 if (!visible_activity($item)) {
564                                         continue;
565                                 }
566
567                                 if (in_array($item['author-id'], $blocklist)) {
568                                         continue;
569                                 }
570
571                                 $threadsid++;
572
573                                 $owner_url   = '';
574                                 $owner_name  = '';
575                                 $sparkle     = '';
576
577                                 // prevent private email from leaking.
578                                 if ($item['network'] === NETWORK_MAIL && local_user() != $item['uid']) {
579                                         continue;
580                                 }
581
582                                 $profile_name = $item['author-name'];
583                                 if (!empty($item['author-link']) && empty($item['author-name'])) {
584                                         $profile_name = $item['author-link'];
585                                 }
586
587                                 $tags = Term::populateTagsFromItem($item);
588
589                                 $author = ['uid' => 0, 'id' => $item['author-id'],
590                                         'network' => $item['author-network'], 'url' => $item['author-link']];
591                                 $profile_link = Contact::magicLinkbyContact($author);
592
593                                 if (strpos($profile_link, 'redir/') === 0) {
594                                         $sparkle = ' sparkle';
595                                 }
596
597                                 $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
598                                 Addon::callHooks('render_location',$locate);
599
600                                 $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
601
602                                 localize_item($item);
603                                 if ($mode === 'network-new') {
604                                         $dropping = true;
605                                 } else {
606                                         $dropping = false;
607                                 }
608
609                                 $drop = [
610                                         'dropping' => $dropping,
611                                         'pagedrop' => $page_dropping,
612                                         'select' => L10n::t('Select'),
613                                         'delete' => L10n::t('Delete'),
614                                 ];
615
616                                 $star = false;
617                                 $isstarred = "unstarred";
618
619                                 $lock = false;
620                                 $likebuttons = false;
621
622                                 $body = prepare_body($item, true, $preview);
623
624                                 list($categories, $folders) = get_cats_and_terms($item);
625
626                                 $profile_name_e = $profile_name;
627
628                                 if (!empty($item['content-warning']) && PConfig::get(local_user(), 'system', 'disable_cw', false)) {
629                                         $title_e = ucfirst($item['content-warning']);
630                                 } else {
631                                         $title_e = $item['title'];
632                                 }
633
634                                 $body_e = $body;
635                                 $tags_e = $tags['tags'];
636                                 $hashtags_e = $tags['hashtags'];
637                                 $mentions_e = $tags['mentions'];
638                                 $location_e = $location;
639                                 $owner_name_e = $owner_name;
640
641                                 $tmp_item = [
642                                         'template' => $tpl,
643                                         'id' => ($preview ? 'P0' : $item['id']),
644                                         'guid' => ($preview ? 'Q0' : $item['guid']),
645                                         'network' => $item['network'],
646                                         'network_name' => ContactSelector::networkToName($item['network'], $profile_link),
647                                         'linktitle' => L10n::t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
648                                         'profile_url' => $profile_link,
649                                         'item_photo_menu' => item_photo_menu($item),
650                                         'name' => $profile_name_e,
651                                         'sparkle' => $sparkle,
652                                         'lock' => $lock,
653                                         'thumb' => System::removedBaseUrl(ProxyUtils::proxifyUrl($item['author-avatar'], false, ProxyUtils::SIZE_THUMB)),
654                                         'title' => $title_e,
655                                         'body' => $body_e,
656                                         'tags' => $tags_e,
657                                         'hashtags' => $hashtags_e,
658                                         'mentions' => $mentions_e,
659                                         'txt_cats' => L10n::t('Categories:'),
660                                         'txt_folders' => L10n::t('Filed under:'),
661                                         'has_cats' => ((count($categories)) ? 'true' : ''),
662                                         'has_folders' => ((count($folders)) ? 'true' : ''),
663                                         'categories' => $categories,
664                                         'folders' => $folders,
665                                         'text' => strip_tags($body_e),
666                                         'localtime' => DateTimeFormat::local($item['created'], 'r'),
667                                         'ago' => (($item['app']) ? L10n::t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
668                                         'location' => $location_e,
669                                         'indent' => '',
670                                         'owner_name' => $owner_name_e,
671                                         'owner_url' => $owner_url,
672                                         'owner_photo' => System::removedBaseUrl(ProxyUtils::proxifyUrl($item['owner-avatar'], false, ProxyUtils::SIZE_THUMB)),
673                                         'plink' => get_plink($item),
674                                         'edpost' => false,
675                                         'isstarred' => $isstarred,
676                                         'star' => $star,
677                                         'drop' => $drop,
678                                         'vote' => $likebuttons,
679                                         'like' => '',
680                                         'dislike' => '',
681                                         'comment' => '',
682                                         'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> L10n::t('View in context')]),
683                                         'previewing' => $previewing,
684                                         'wait' => L10n::t('Please wait'),
685                                         'thread_level' => 1,
686                                 ];
687
688                                 $arr = ['item' => $item, 'output' => $tmp_item];
689                                 Addon::callHooks('display_item', $arr);
690
691                                 $threads[$threadsid]['id'] = $item['id'];
692                                 $threads[$threadsid]['network'] = $item['network'];
693                                 $threads[$threadsid]['items'] = [$arr['output']];
694
695                         }
696                 } else {
697                         // Normal View
698                         $page_template = get_markup_template("threaded_conversation.tpl");
699
700                         $conv = new Thread($mode, $preview, $writable);
701
702                         /*
703                          * get all the topmost parents
704                          * this shouldn't be needed, as we should have only them in our array
705                          * But for now, this array respects the old style, just in case
706                          */
707                         foreach ($items as $item) {
708                                 if (in_array($item['author-id'], $blocklist)) {
709                                         continue;
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(array $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 (DBA::isResult($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::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                 '$wall'         => $notes_cid ? 0 : 1,
1150                 '$posttype'     => $notes_cid ? Item::PT_PERSONAL_NOTE : Item::PT_ARTICLE,
1151                 '$content'      => defaults($x, 'content', ''),
1152                 '$post_id'      => defaults($x, 'post_id', ''),
1153                 '$baseurl'      => System::baseUrl(true),
1154                 '$defloc'       => $x['default_location'],
1155                 '$visitor'      => $x['visitor'],
1156                 '$pvisit'       => $notes_cid ? 'none' : $x['visitor'],
1157                 '$public'       => L10n::t('Public post'),
1158                 '$lockstate'    => $x['lockstate'],
1159                 '$bang'         => $x['bang'],
1160                 '$profile_uid'  => $x['profile_uid'],
1161                 '$preview'      => Feature::isEnabled($x['profile_uid'], 'preview') ? L10n::t('Preview') : '',
1162                 '$jotplugins'   => $jotplugins,
1163                 '$notes_cid'    => $notes_cid,
1164                 '$sourceapp'    => L10n::t($a->sourcename),
1165                 '$cancel'       => L10n::t('Cancel'),
1166                 '$rand_num'     => random_digits(12),
1167
1168                 // ACL permissions box
1169                 '$acl'           => $x['acl'],
1170                 '$group_perms'   => L10n::t('Post to Groups'),
1171                 '$contact_perms' => L10n::t('Post to Contacts'),
1172                 '$private'       => L10n::t('Private post'),
1173                 '$is_private'    => $private_post,
1174                 '$public_link'   => $public_post_link,
1175
1176                 //jot nav tab (used in some themes)
1177                 '$message' => L10n::t('Message'),
1178                 '$browser' => L10n::t('Browser'),
1179         ]);
1180
1181
1182         if ($popup == true) {
1183                 $o = '<div id="jot-popup" style="display: none;">' . $o . '</div>';
1184         }
1185
1186         return $o;
1187 }
1188
1189 /**
1190  * Plucks the children of the given parent from a given item list.
1191  *
1192  * @brief Plucks all the children in the given item list of the given parent
1193  *
1194  * @param array $item_list
1195  * @param array $parent
1196  * @param bool $recursive
1197  * @return type
1198  */
1199 function get_item_children(array &$item_list, array $parent, $recursive = true)
1200 {
1201         $children = [];
1202         foreach ($item_list as $i => $item) {
1203                 if ($item['id'] != $item['parent']) {
1204                         if ($recursive) {
1205                                 // Fallback to parent-uri if thr-parent is not set
1206                                 $thr_parent = $item['thr-parent'];
1207                                 if ($thr_parent == '') {
1208                                         $thr_parent = $item['parent-uri'];
1209                                 }
1210
1211                                 if ($thr_parent == $parent['uri']) {
1212                                         $item['children'] = get_item_children($item_list, $item);
1213                                         $children[] = $item;
1214                                         unset($item_list[$i]);
1215                                 }
1216                         } elseif ($item['parent'] == $parent['id']) {
1217                                 $children[] = $item;
1218                                 unset($item_list[$i]);
1219                         }
1220                 }
1221         }
1222         return $children;
1223 }
1224
1225 /**
1226  * @brief Recursively sorts a tree-like item array
1227  *
1228  * @param array $items
1229  * @return array
1230  */
1231 function sort_item_children(array $items)
1232 {
1233         $result = $items;
1234         usort($result, 'sort_thr_created_rev');
1235         foreach ($result as $k => $i) {
1236                 if (isset($result[$k]['children'])) {
1237                         $result[$k]['children'] = sort_item_children($result[$k]['children']);
1238                 }
1239         }
1240         return $result;
1241 }
1242
1243 /**
1244  * @brief Recursively add all children items at the top level of a list
1245  *
1246  * @param array $children List of items to append
1247  * @param array $item_list
1248  */
1249 function add_children_to_list(array $children, array &$item_list)
1250 {
1251         foreach ($children as $child) {
1252                 $item_list[] = $child;
1253                 if (isset($child['children'])) {
1254                         add_children_to_list($child['children'], $item_list);
1255                 }
1256         }
1257 }
1258
1259 /**
1260  * This recursive function takes the item tree structure created by conv_sort() and
1261  * flatten the extraneous depth levels when people reply sequentially, removing the
1262  * stairs effect in threaded conversations limiting the available content width.
1263  *
1264  * The basic principle is the following: if a post item has only one reply and is
1265  * the last reply of its parent, then the reply is moved to the parent.
1266  *
1267  * This process is rendered somewhat more complicated because items can be either
1268  * replies or likes, and these don't factor at all in the reply count/last reply.
1269  *
1270  * @brief Selectively flattens a tree-like item structure to prevent threading stairs
1271  *
1272  * @param array $parent A tree-like array of items
1273  * @return array
1274  */
1275 function smart_flatten_conversation(array $parent)
1276 {
1277         if (!isset($parent['children']) || count($parent['children']) == 0) {
1278                 return $parent;
1279         }
1280
1281         // We use a for loop to ensure we process the newly-moved items
1282         for ($i = 0; $i < count($parent['children']); $i++) {
1283                 $child = $parent['children'][$i];
1284
1285                 if (isset($child['children']) && count($child['children'])) {
1286                         // This helps counting only the regular posts
1287                         $count_post_closure = function($var) {
1288                                 return $var['verb'] === ACTIVITY_POST;
1289                         };
1290
1291                         $child_post_count = count(array_filter($child['children'], $count_post_closure));
1292
1293                         $remaining_post_count = count(array_filter(array_slice($parent['children'], $i), $count_post_closure));
1294
1295                         // If there's only one child's children post and this is the last child post
1296                         if ($child_post_count == 1 && $remaining_post_count == 1) {
1297
1298                                 // Searches the post item in the children
1299                                 $j = 0;
1300                                 while($child['children'][$j]['verb'] !== ACTIVITY_POST && $j < count($child['children'])) {
1301                                         $j ++;
1302                                 }
1303
1304                                 $moved_item = $child['children'][$j];
1305                                 unset($parent['children'][$i]['children'][$j]);
1306                                 $parent['children'][] = $moved_item;
1307                         } else {
1308                                 $parent['children'][$i] = smart_flatten_conversation($child);
1309                         }
1310                 }
1311         }
1312
1313         return $parent;
1314 }
1315
1316
1317 /**
1318  * Expands a flat list of items into corresponding tree-like conversation structures,
1319  * sort the top-level posts either on "created" or "commented", and finally
1320  * append all the items at the top level (???)
1321  *
1322  * @brief Expands a flat item list into a conversation array for display
1323  *
1324  * @param array  $item_list A list of items belonging to one or more conversations
1325  * @param string $order     Either on "created" or "commented"
1326  * @return array
1327  */
1328 function conv_sort(array $item_list, $order)
1329 {
1330         $parents = [];
1331
1332         if (!(is_array($item_list) && count($item_list))) {
1333                 return $parents;
1334         }
1335
1336         $blocklist = conv_get_blocklist();
1337
1338         $item_array = [];
1339
1340         // Dedupes the item list on the uri to prevent infinite loops
1341         foreach ($item_list as $item) {
1342                 if (in_array($item['author-id'], $blocklist)) {
1343                         continue;
1344                 }
1345
1346                 $item_array[$item['uri']] = $item;
1347         }
1348
1349         // Extract the top level items
1350         foreach ($item_array as $item) {
1351                 if ($item['id'] == $item['parent']) {
1352                         $parents[] = $item;
1353                 }
1354         }
1355
1356         if (stristr($order, 'created')) {
1357                 usort($parents, 'sort_thr_created');
1358         } elseif (stristr($order, 'commented')) {
1359                 usort($parents, 'sort_thr_commented');
1360         }
1361
1362         /*
1363          * Plucks children from the item_array, second pass collects eventual orphan
1364          * items and add them as children of their top-level post.
1365          */
1366         foreach ($parents as $i => $parent) {
1367                 $parents[$i]['children'] =
1368                         array_merge(get_item_children($item_array, $parent, true),
1369                                 get_item_children($item_array, $parent, false));
1370         }
1371
1372         foreach ($parents as $i => $parent) {
1373                 $parents[$i]['children'] = sort_item_children($parents[$i]['children']);
1374         }
1375
1376         if (PConfig::get(local_user(), 'system', 'smart_threading', 0)) {
1377                 foreach ($parents as $i => $parent) {
1378                         $parents[$i] = smart_flatten_conversation($parent);
1379                 }
1380         }
1381
1382         /// @TODO: Stop recusrsively adding all children back to the top level (!!!)
1383         /// However, this apparently ensures responses (likes, attendance) display (?!)
1384         foreach ($parents as $parent) {
1385                 if (count($parent['children'])) {
1386                         add_children_to_list($parent['children'], $parents);
1387                 }
1388         }
1389
1390         return $parents;
1391 }
1392
1393 /**
1394  * @brief usort() callback to sort item arrays by the created key
1395  *
1396  * @param array $a
1397  * @param array $b
1398  * @return int
1399  */
1400 function sort_thr_created(array $a, array $b)
1401 {
1402         return strcmp($b['created'], $a['created']);
1403 }
1404
1405 /**
1406  * @brief usort() callback to reverse sort item arrays by the created key
1407  *
1408  * @param array $a
1409  * @param array $b
1410  * @return int
1411  */
1412 function sort_thr_created_rev(array $a, array $b)
1413 {
1414         return strcmp($a['created'], $b['created']);
1415 }
1416
1417 /**
1418  * @brief usort() callback to sort item arrays by the commented key
1419  *
1420  * @param array $a
1421  * @param array $b
1422  * @return type
1423  */
1424 function sort_thr_commented(array $a, array $b)
1425 {
1426         return strcmp($b['commented'], $a['commented']);
1427 }
1428
1429 function render_location_dummy(array $item) {
1430         if (x($item, 'location') && !empty($item['location'])) {
1431                 return $item['location'];
1432         }
1433
1434         if (x($item, 'coord') && !empty($item['coord'])) {
1435                 return $item['coord'];
1436         }
1437 }
1438
1439 function get_responses(array $conv_responses, array $response_verbs, $ob, array $item) {
1440         $ret = [];
1441         foreach ($response_verbs as $v) {
1442                 $ret[$v] = [];
1443                 $ret[$v]['count'] = defaults($conv_responses[$v], $item['uri'], '');
1444                 $ret[$v]['list']  = defaults($conv_responses[$v], $item['uri'] . '-l', []);
1445                 $ret[$v]['self']  = defaults($conv_responses[$v], $item['uri'] . '-self', '0');
1446                 if (count($ret[$v]['list']) > MAX_LIKERS) {
1447                         $ret[$v]['list_part'] = array_slice($ret[$v]['list'], 0, MAX_LIKERS);
1448                         array_push($ret[$v]['list_part'], '<a href="#" data-toggle="modal" data-target="#' . $v . 'Modal-'
1449                                 . (($ob) ? $ob->getId() : $item['id']) . '"><b>' . L10n::t('View all') . '</b></a>');
1450                 } else {
1451                         $ret[$v]['list_part'] = '';
1452                 }
1453                 $ret[$v]['button'] = get_response_button_text($v, $ret[$v]['count']);
1454                 $ret[$v]['title'] = $conv_responses[$v]['title'];
1455         }
1456
1457         $count = 0;
1458         foreach ($ret as $key) {
1459                 if ($key['count'] == true) {
1460                         $count++;
1461                 }
1462         }
1463         $ret['count'] = $count;
1464
1465         return $ret;
1466 }
1467
1468 function get_response_button_text($v, $count)
1469 {
1470         switch ($v) {
1471                 case 'like':
1472                         $return = L10n::tt('Like', 'Likes', $count);
1473                         break;
1474                 case 'dislike':
1475                         $return = L10n::tt('Dislike', 'Dislikes', $count);
1476                         break;
1477                 case 'attendyes':
1478                         $return = L10n::tt('Attending', 'Attending', $count);
1479                         break;
1480                 case 'attendno':
1481                         $return = L10n::tt('Not Attending', 'Not Attending', $count);
1482                         break;
1483                 case 'attendmaybe':
1484                         $return = L10n::tt('Undecided', 'Undecided', $count);
1485                         break;
1486         }
1487
1488         return $return;
1489 }