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