]> git.mxchange.org Git - friendica.git/blob - include/conversation.php
Merge pull request #6013 from JonnyTischbein/issue_comment_media_link_prompt
[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\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         // Only create a redirection to a magic link when logged in
357         if (!empty($item['plink']) && (local_user() || remote_user())) {
358                 $item['plink'] = Contact::magicLinkbyContact($author, $item['plink']);
359         }
360 }
361
362 /**
363  * Count the total of comments on this item and its desendants
364  * @TODO proper type-hint + doc-tag
365  */
366 function count_descendants($item) {
367         $total = count($item['children']);
368
369         if ($total > 0) {
370                 foreach ($item['children'] as $child) {
371                         if (!visible_activity($child)) {
372                                 $total --;
373                         }
374                         $total += count_descendants($child);
375                 }
376         }
377
378         return $total;
379 }
380
381 function visible_activity($item) {
382
383         /*
384          * likes (etc.) can apply to other things besides posts. Check if they are post children,
385          * in which case we handle them specially
386          */
387         $hidden_activities = [ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE];
388         foreach ($hidden_activities as $act) {
389                 if (activity_match($item['verb'], $act)) {
390                         return false;
391                 }
392         }
393
394         // @TODO below if() block can be rewritten to a single line: $isVisible = allConditionsHere;
395         if (activity_match($item['verb'], ACTIVITY_FOLLOW) && $item['object-type'] === ACTIVITY_OBJ_NOTE && empty($item['self']) && $item['uid'] == local_user()) {
396                 return false;
397         }
398
399         return true;
400 }
401
402 function conv_get_blocklist()
403 {
404         if (!local_user()) {
405                 return [];
406         }
407
408         $str_blocked = PConfig::get(local_user(), 'system', 'blocked');
409         if (empty($str_blocked)) {
410                 return [];
411         }
412
413         $blocklist = [];
414
415         foreach (explode(',', $str_blocked) as $entry) {
416                 // The 4th parameter guarantees that there always will be a public contact entry
417                 $cid = Contact::getIdForURL(trim($entry), 0, true, ['url' => trim($entry)]);
418                 if (!empty($cid)) {
419                         $blocklist[] = $cid;
420                 }
421         }
422
423         return $blocklist;
424 }
425
426 /**
427  * "Render" a conversation or list of items for HTML display.
428  * There are two major forms of display:
429  *      - Sequential or unthreaded ("New Item View" or search results)
430  *      - conversation view
431  * The $mode parameter decides between the various renderings and also
432  * figures out how to determine page owner and other contextual items
433  * that are based on unique features of the calling module.
434  *
435  */
436 function conversation(App $a, array $items, $mode, $update, $preview = false, $order = 'commented', $uid = 0) {
437
438         $ssl_state = (local_user() ? true : false);
439
440         $profile_owner = 0;
441         $live_update_div = '';
442
443         $blocklist = conv_get_blocklist();
444
445         $previewing = (($preview) ? ' preview ' : '');
446
447         if ($mode === 'network') {
448                 $items = conversation_add_children($items, false, $order, $uid);
449                 $profile_owner = local_user();
450                 if (!$update) {
451                         /*
452                          * The special div is needed for liveUpdate to kick in for this page.
453                          * We only launch liveUpdate if you aren't filtering in some incompatible
454                          * way and also you aren't writing a comment (discovered in javascript).
455                          */
456                         $live_update_div = '<div id="live-network"></div>' . "\r\n"
457                                 . "<script> var profile_uid = " . $_SESSION['uid']
458                                 . "; var netargs = '" . substr($a->cmd, 8)
459                                 . '?f='
460                                 . ((x($_GET, 'cid'))    ? '&cid='    . $_GET['cid']    : '')
461                                 . ((x($_GET, 'search')) ? '&search=' . $_GET['search'] : '')
462                                 . ((x($_GET, 'star'))   ? '&star='   . $_GET['star']   : '')
463                                 . ((x($_GET, 'order'))  ? '&order='  . $_GET['order']  : '')
464                                 . ((x($_GET, 'bmark'))  ? '&bmark='  . $_GET['bmark']  : '')
465                                 . ((x($_GET, 'liked'))  ? '&liked='  . $_GET['liked']  : '')
466                                 . ((x($_GET, 'conv'))   ? '&conv='   . $_GET['conv']   : '')
467                                 . ((x($_GET, 'nets'))   ? '&nets='   . $_GET['nets']   : '')
468                                 . ((x($_GET, 'cmin'))   ? '&cmin='   . $_GET['cmin']   : '')
469                                 . ((x($_GET, 'cmax'))   ? '&cmax='   . $_GET['cmax']   : '')
470                                 . ((x($_GET, 'file'))   ? '&file='   . $_GET['file']   : '')
471
472                                 . "'; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
473                 }
474         } elseif ($mode === 'profile') {
475                 $items = conversation_add_children($items, false, $order, $uid);
476                 $profile_owner = $a->profile['profile_uid'];
477
478                 if (!$update) {
479                         $tab = 'posts';
480                         if (x($_GET, 'tab')) {
481                                 $tab = notags(trim($_GET['tab']));
482                         }
483                         if ($tab === 'posts') {
484                                 /*
485                                  * This is ugly, but we can't pass the profile_uid through the session to the ajax updater,
486                                  * because browser prefetching might change it on us. We have to deliver it with the page.
487                                  */
488
489                                 $live_update_div = '<div id="live-profile"></div>' . "\r\n"
490                                         . "<script> var profile_uid = " . $a->profile['profile_uid']
491                                         . "; var netargs = '?f='; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
492                         }
493                 }
494         } elseif ($mode === 'notes') {
495                 $items = conversation_add_children($items, false, $order, $uid);
496                 $profile_owner = local_user();
497
498                 if (!$update) {
499                         $live_update_div = '<div id="live-notes"></div>' . "\r\n"
500                                 . "<script> var profile_uid = " . local_user()
501                                 . "; var netargs = '/?f='; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
502                 }
503         } elseif ($mode === 'display') {
504                 $items = conversation_add_children($items, false, $order, $uid);
505                 $profile_owner = $a->profile['uid'];
506
507                 if (!$update) {
508                         $live_update_div = '<div id="live-display"></div>' . "\r\n"
509                                 . "<script> var profile_uid = " . defaults($_SESSION, 'uid', 0) . ";"
510                                 . " var profile_page = 1; </script>";
511                 }
512         } elseif ($mode === 'community') {
513                 $items = conversation_add_children($items, true, $order, $uid);
514                 $profile_owner = 0;
515
516                 if (!$update) {
517                         $live_update_div = '<div id="live-community"></div>' . "\r\n"
518                                 . "<script> var profile_uid = -1; var netargs = '" . substr($a->cmd, 10)
519                                 ."/?f='; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
520                 }
521         } elseif ($mode === 'contacts') {
522                 $items = conversation_add_children($items, true, $order, $uid);
523                 $profile_owner = 0;
524
525                 if (!$update) {
526                         $live_update_div = '<div id="live-contacts"></div>' . "\r\n"
527                                 . "<script> var profile_uid = -1; var netargs = '" . substr($a->cmd, 9)
528                                 ."/?f='; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
529                 }
530         } elseif ($mode === 'search') {
531                 $live_update_div = '<div id="live-search"></div>' . "\r\n";
532         }
533
534         $page_dropping = ((local_user() && local_user() == $profile_owner) ? true : false);
535
536         if (!$update) {
537                 $_SESSION['return_path'] = $a->query_string;
538         }
539
540         $cb = ['items' => $items, 'mode' => $mode, 'update' => $update, 'preview' => $preview];
541         Addon::callHooks('conversation_start',$cb);
542
543         $items = $cb['items'];
544
545         $conv_responses = [
546                 'like' => ['title' => L10n::t('Likes','title')], 'dislike' => ['title' => L10n::t('Dislikes','title')],
547                 'attendyes' => ['title' => L10n::t('Attending','title')], 'attendno' => ['title' => L10n::t('Not attending','title')], 'attendmaybe' => ['title' => L10n::t('Might attend','title')]
548         ];
549
550         // array with html for each thread (parent+comments)
551         $threads = [];
552         $threadsid = -1;
553
554         $page_template = get_markup_template("conversation.tpl");
555
556         if (!empty($items)) {
557                 if (in_array($mode, ['community', 'contacts'])) {
558                         $writable = true;
559                 } else {
560                         $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
561                 }
562
563                 if (!local_user()) {
564                         $writable = false;
565                 }
566
567                 if (in_array($mode, ['network-new', 'search', 'contact-posts'])) {
568
569                         /*
570                          * "New Item View" on network page or search page results
571                          * - just loop through the items and format them minimally for display
572                          */
573
574                         $tpl = 'search_item.tpl';
575
576                         foreach ($items as $item) {
577
578                                 if (!visible_activity($item)) {
579                                         continue;
580                                 }
581
582                                 if (in_array($item['author-id'], $blocklist)) {
583                                         continue;
584                                 }
585
586                                 $threadsid++;
587
588                                 $owner_url   = '';
589                                 $owner_name  = '';
590                                 $sparkle     = '';
591
592                                 // prevent private email from leaking.
593                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
594                                         continue;
595                                 }
596
597                                 $profile_name = $item['author-name'];
598                                 if (!empty($item['author-link']) && empty($item['author-name'])) {
599                                         $profile_name = $item['author-link'];
600                                 }
601
602                                 $tags = Term::populateTagsFromItem($item);
603
604                                 $author = ['uid' => 0, 'id' => $item['author-id'],
605                                         'network' => $item['author-network'], 'url' => $item['author-link']];
606                                 $profile_link = Contact::magicLinkbyContact($author);
607
608                                 if (strpos($profile_link, 'redir/') === 0) {
609                                         $sparkle = ' sparkle';
610                                 }
611
612                                 $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
613                                 Addon::callHooks('render_location',$locate);
614
615                                 $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
616
617                                 localize_item($item);
618                                 if ($mode === 'network-new') {
619                                         $dropping = true;
620                                 } else {
621                                         $dropping = false;
622                                 }
623
624                                 $drop = [
625                                         'dropping' => $dropping,
626                                         'pagedrop' => $page_dropping,
627                                         'select' => L10n::t('Select'),
628                                         'delete' => L10n::t('Delete'),
629                                 ];
630
631                                 $star = false;
632                                 $isstarred = "unstarred";
633
634                                 $lock = false;
635                                 $likebuttons = false;
636
637                                 $body = prepare_body($item, true, $preview);
638
639                                 list($categories, $folders) = get_cats_and_terms($item);
640
641                                 $profile_name_e = $profile_name;
642
643                                 if (!empty($item['content-warning']) && PConfig::get(local_user(), 'system', 'disable_cw', false)) {
644                                         $title_e = ucfirst($item['content-warning']);
645                                 } else {
646                                         $title_e = $item['title'];
647                                 }
648
649                                 $body_e = $body;
650                                 $tags_e = $tags['tags'];
651                                 $hashtags_e = $tags['hashtags'];
652                                 $mentions_e = $tags['mentions'];
653                                 $location_e = $location;
654                                 $owner_name_e = $owner_name;
655
656                                 $tmp_item = [
657                                         'template' => $tpl,
658                                         'id' => ($preview ? 'P0' : $item['id']),
659                                         'guid' => ($preview ? 'Q0' : $item['guid']),
660                                         'network' => $item['network'],
661                                         'network_name' => ContactSelector::networkToName($item['network'], $item['author-link']),
662                                         'linktitle' => L10n::t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
663                                         'profile_url' => $profile_link,
664                                         'item_photo_menu' => item_photo_menu($item),
665                                         'name' => $profile_name_e,
666                                         'sparkle' => $sparkle,
667                                         'lock' => $lock,
668                                         'thumb' => System::removedBaseUrl(ProxyUtils::proxifyUrl($item['author-avatar'], false, ProxyUtils::SIZE_THUMB)),
669                                         'title' => $title_e,
670                                         'body' => $body_e,
671                                         'tags' => $tags_e,
672                                         'hashtags' => $hashtags_e,
673                                         'mentions' => $mentions_e,
674                                         'txt_cats' => L10n::t('Categories:'),
675                                         'txt_folders' => L10n::t('Filed under:'),
676                                         'has_cats' => ((count($categories)) ? 'true' : ''),
677                                         'has_folders' => ((count($folders)) ? 'true' : ''),
678                                         'categories' => $categories,
679                                         'folders' => $folders,
680                                         'text' => strip_tags($body_e),
681                                         'localtime' => DateTimeFormat::local($item['created'], 'r'),
682                                         'ago' => (($item['app']) ? L10n::t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
683                                         'location' => $location_e,
684                                         'indent' => '',
685                                         'owner_name' => $owner_name_e,
686                                         'owner_url' => $owner_url,
687                                         'owner_photo' => System::removedBaseUrl(ProxyUtils::proxifyUrl($item['owner-avatar'], false, ProxyUtils::SIZE_THUMB)),
688                                         'plink' => get_plink($item),
689                                         'edpost' => false,
690                                         'isstarred' => $isstarred,
691                                         'star' => $star,
692                                         'drop' => $drop,
693                                         'vote' => $likebuttons,
694                                         'like' => '',
695                                         'dislike' => '',
696                                         'comment' => '',
697                                         'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> L10n::t('View in context')]),
698                                         'previewing' => $previewing,
699                                         'wait' => L10n::t('Please wait'),
700                                         'thread_level' => 1,
701                                 ];
702
703                                 $arr = ['item' => $item, 'output' => $tmp_item];
704                                 Addon::callHooks('display_item', $arr);
705
706                                 $threads[$threadsid]['id'] = $item['id'];
707                                 $threads[$threadsid]['network'] = $item['network'];
708                                 $threads[$threadsid]['items'] = [$arr['output']];
709
710                         }
711                 } else {
712                         // Normal View
713                         $page_template = get_markup_template("threaded_conversation.tpl");
714
715                         $conv = new Thread($mode, $preview, $writable);
716
717                         /*
718                          * get all the topmost parents
719                          * this shouldn't be needed, as we should have only them in our array
720                          * But for now, this array respects the old style, just in case
721                          */
722                         foreach ($items as $item) {
723                                 if (in_array($item['author-id'], $blocklist)) {
724                                         continue;
725                                 }
726
727                                 // Can we put this after the visibility check?
728                                 builtin_activity_puller($item, $conv_responses);
729
730                                 // Only add what is visible
731                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
732                                         continue;
733                                 }
734
735                                 if (!visible_activity($item)) {
736                                         continue;
737                                 }
738
739                                 /// @todo Check if this call is needed or not
740                                 $arr = ['item' => $item];
741                                 Addon::callHooks('display_item', $arr);
742
743                                 $item['pagedrop'] = $page_dropping;
744
745                                 if ($item['id'] == $item['parent']) {
746                                         $item_object = new Post($item);
747                                         $conv->addParent($item_object);
748                                 }
749                         }
750
751                         $threads = $conv->getTemplateData($conv_responses);
752                         if (!$threads) {
753                                 logger('[ERROR] conversation : Failed to get template data.', LOGGER_DEBUG);
754                                 $threads = [];
755                         }
756                 }
757         }
758
759         $o = replace_macros($page_template, [
760                 '$baseurl' => System::baseUrl($ssl_state),
761                 '$return_path' => $a->query_string,
762                 '$live_update' => $live_update_div,
763                 '$remove' => L10n::t('remove'),
764                 '$mode' => $mode,
765                 '$user' => $a->user,
766                 '$threads' => $threads,
767                 '$dropping' => ($page_dropping && Feature::isEnabled(local_user(), 'multi_delete') ? L10n::t('Delete Selected Items') : False),
768         ]);
769
770         return $o;
771 }
772
773 /**
774  * @brief Add comments to top level entries that had been fetched before
775  *
776  * The system will fetch the comments for the local user whenever possible.
777  * This behaviour is currently needed to allow commenting on Friendica posts.
778  *
779  * @param array $parents Parent items
780  *
781  * @return array items with parents and comments
782  */
783 function conversation_add_children(array $parents, $block_authors, $order, $uid) {
784         $max_comments = Config::get('system', 'max_comments', 100);
785
786         $params = ['order' => ['uid', 'commented' => true]];
787
788         if ($max_comments > 0) {
789                 $params['limit'] = $max_comments;
790         }
791
792         $items = [];
793
794         foreach ($parents AS $parent) {
795                 $condition = ["`item`.`parent-uri` = ? AND `item`.`uid` IN (0, ?) ",
796                         $parent['uri'], local_user()];
797                 if ($block_authors) {
798                         $condition[0] .= "AND NOT `author`.`hidden`";
799                 }
800                 $thread_items = Item::selectForUser(local_user(), [], $condition, $params);
801
802                 $comments = Item::inArray($thread_items);
803
804                 if (count($comments) != 0) {
805                         $items = array_merge($items, $comments);
806                 }
807         }
808
809         foreach ($items as $index => $item) {
810                 if ($item['uid'] == 0) {
811                         $items[$index]['writable'] = in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
812                 }
813         }
814
815         $items = conv_sort($items, $order);
816
817         return $items;
818 }
819
820 function item_photo_menu($item) {
821         $sub_link = '';
822         $poke_link = '';
823         $contact_url = '';
824         $pm_url = '';
825         $status_link = '';
826         $photos_link = '';
827         $posts_link = '';
828
829         if (local_user() && local_user() == $item['uid'] && $item['parent'] == $item['id'] && !$item['self']) {
830                 $sub_link = 'javascript:dosubthread(' . $item['id'] . '); return false;';
831         }
832
833         $author = ['uid' => 0, 'id' => $item['author-id'],
834                 'network' => $item['author-network'], 'url' => $item['author-link']];
835         $profile_link = Contact::magicLinkbyContact($author);
836         $sparkle = (strpos($profile_link, 'redir/') === 0);
837
838         $cid = 0;
839         $network = '';
840         $rel = 0;
841         $condition = ['uid' => local_user(), 'nurl' => normalise_link($item['author-link'])];
842         $contact = DBA::selectFirst('contact', ['id', 'network', 'rel'], $condition);
843         if (DBA::isResult($contact)) {
844                 $cid = $contact['id'];
845                 $network = $contact['network'];
846                 $rel = $contact['rel'];
847         }
848
849         if ($sparkle) {
850                 $status_link = $profile_link . '?url=status';
851                 $photos_link = $profile_link . '?url=photos';
852                 $profile_link = $profile_link . '?url=profile';
853         }
854
855         if ($cid && !$item['self']) {
856                 $poke_link = 'poke/?f=&c=' . $cid;
857                 $contact_url = 'contact/' . $cid;
858                 $posts_link = 'contact/' . $cid . '/posts';
859
860                 if (in_array($network, [Protocol::DFRN, Protocol::DIASPORA])) {
861                         $pm_url = 'message/new/' . $cid;
862                 }
863         }
864
865         if (local_user()) {
866                 $menu = [
867                         L10n::t('Follow Thread') => $sub_link,
868                         L10n::t('View Status') => $status_link,
869                         L10n::t('View Profile') => $profile_link,
870                         L10n::t('View Photos') => $photos_link,
871                         L10n::t('Network Posts') => $posts_link,
872                         L10n::t('View Contact') => $contact_url,
873                         L10n::t('Send PM') => $pm_url
874                 ];
875
876                 if ($network == Protocol::DFRN) {
877                         $menu[L10n::t("Poke")] = $poke_link;
878                 }
879
880                 if ((($cid == 0) || ($rel == Contact::FOLLOWER)) &&
881                         in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) {
882                         $menu[L10n::t('Connect/Follow')] = 'follow?url=' . urlencode($item['author-link']);
883                 }
884         } else {
885                 $menu = [L10n::t('View Profile') => $item['author-link']];
886         }
887
888         $args = ['item' => $item, 'menu' => $menu];
889
890         Addon::callHooks('item_photo_menu', $args);
891
892         $menu = $args['menu'];
893
894         $o = '';
895         foreach ($menu as $k => $v) {
896                 if (strpos($v, 'javascript:') === 0) {
897                         $v = substr($v, 11);
898                         $o .= '<li role="menuitem"><a onclick="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
899                 } elseif ($v!='') {
900                         $o .= '<li role="menuitem"><a href="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
901                 }
902         }
903         return $o;
904 }
905
906 /**
907  * @brief Checks item to see if it is one of the builtin activities (like/dislike, event attendance, consensus items, etc.)
908  * Increments the count of each matching activity and adds a link to the author as needed.
909  *
910  * @param array $item
911  * @param array &$conv_responses (already created with builtin activity structure)
912  * @return void
913  */
914 function builtin_activity_puller($item, &$conv_responses) {
915         foreach ($conv_responses as $mode => $v) {
916                 $url = '';
917                 $sparkle = '';
918
919                 switch ($mode) {
920                         case 'like':
921                                 $verb = ACTIVITY_LIKE;
922                                 break;
923                         case 'dislike':
924                                 $verb = ACTIVITY_DISLIKE;
925                                 break;
926                         case 'attendyes':
927                                 $verb = ACTIVITY_ATTEND;
928                                 break;
929                         case 'attendno':
930                                 $verb = ACTIVITY_ATTENDNO;
931                                 break;
932                         case 'attendmaybe':
933                                 $verb = ACTIVITY_ATTENDMAYBE;
934                                 break;
935                         default:
936                                 return;
937                 }
938
939                 if (activity_match($item['verb'], $verb) && ($item['id'] != $item['parent'])) {
940                         $author = ['uid' => 0, 'id' => $item['author-id'],
941                                 'network' => $item['author-network'], 'url' => $item['author-link']];
942                         $url = Contact::magicLinkbyContact($author);
943                         if (strpos($url, 'redir/') === 0) {
944                                 $sparkle = ' class="sparkle" ';
945                         }
946
947                         $url = '<a href="'. $url . '"'. $sparkle .'>' . htmlentities($item['author-name']) . '</a>';
948
949                         if (!x($item, 'thr-parent')) {
950                                 $item['thr-parent'] = $item['parent-uri'];
951                         }
952
953                         if (!(isset($conv_responses[$mode][$item['thr-parent'] . '-l'])
954                                 && is_array($conv_responses[$mode][$item['thr-parent'] . '-l']))) {
955                                 $conv_responses[$mode][$item['thr-parent'] . '-l'] = [];
956                         }
957
958                         // only list each unique author once
959                         if (in_array($url,$conv_responses[$mode][$item['thr-parent'] . '-l'])) {
960                                 continue;
961                         }
962
963                         if (!isset($conv_responses[$mode][$item['thr-parent']])) {
964                                 $conv_responses[$mode][$item['thr-parent']] = 1;
965                         } else {
966                                 $conv_responses[$mode][$item['thr-parent']] ++;
967                         }
968
969                         if (public_contact() == $item['author-id']) {
970                                 $conv_responses[$mode][$item['thr-parent'] . '-self'] = 1;
971                         }
972
973                         $conv_responses[$mode][$item['thr-parent'] . '-l'][] = $url;
974
975                         // there can only be one activity verb per item so if we found anything, we can stop looking
976                         return;
977                 }
978         }
979 }
980
981 /**
982  * Format the vote text for a profile item
983  * @param int $cnt = number of people who vote the item
984  * @param array $arr = array of pre-linked names of likers/dislikers
985  * @param string $type = one of 'like, 'dislike', 'attendyes', 'attendno', 'attendmaybe'
986  * @param int $id  = item id
987  * @return string formatted text
988  */
989 function format_like($cnt, array $arr, $type, $id) {
990         $o = '';
991         $expanded = '';
992
993         if ($cnt == 1) {
994                 $likers = $arr[0];
995
996                 // Phrase if there is only one liker. In other cases it will be uses for the expanded
997                 // list which show all likers
998                 switch ($type) {
999                         case 'like' :
1000                                 $phrase = L10n::t('%s likes this.', $likers);
1001                                 break;
1002                         case 'dislike' :
1003                                 $phrase = L10n::t('%s doesn\'t like this.', $likers);
1004                                 break;
1005                         case 'attendyes' :
1006                                 $phrase = L10n::t('%s attends.', $likers);
1007                                 break;
1008                         case 'attendno' :
1009                                 $phrase = L10n::t('%s doesn\'t attend.', $likers);
1010                                 break;
1011                         case 'attendmaybe' :
1012                                 $phrase = L10n::t('%s attends maybe.', $likers);
1013                                 break;
1014                 }
1015         }
1016
1017         if ($cnt > 1) {
1018                 $total = count($arr);
1019                 if ($total >= MAX_LIKERS) {
1020                         $arr = array_slice($arr, 0, MAX_LIKERS - 1);
1021                 }
1022                 if ($total < MAX_LIKERS) {
1023                         $last = L10n::t('and') . ' ' . $arr[count($arr)-1];
1024                         $arr2 = array_slice($arr, 0, -1);
1025                         $str = implode(', ', $arr2) . ' ' . $last;
1026                 }
1027                 if ($total >= MAX_LIKERS) {
1028                         $str = implode(', ', $arr);
1029                         $str .= L10n::t('and %d other people', $total - MAX_LIKERS);
1030                 }
1031
1032                 $likers = $str;
1033
1034                 $spanatts = "class=\"fakelink\" onclick=\"openClose('{$type}list-$id');\"";
1035
1036                 switch ($type) {
1037                         case 'like':
1038                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> like this', $spanatts, $cnt);
1039                                 $explikers = L10n::t('%s like this.', $likers);
1040                                 break;
1041                         case 'dislike':
1042                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> don\'t like this', $spanatts, $cnt);
1043                                 $explikers = L10n::t('%s don\'t like this.', $likers);
1044                                 break;
1045                         case 'attendyes':
1046                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> attend', $spanatts, $cnt);
1047                                 $explikers = L10n::t('%s attend.', $likers);
1048                                 break;
1049                         case 'attendno':
1050                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> don\'t attend', $spanatts, $cnt);
1051                                 $explikers = L10n::t('%s don\'t attend.', $likers);
1052                                 break;
1053                         case 'attendmaybe':
1054                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> attend maybe', $spanatts, $cnt);
1055                                 $explikers = L10n::t('%s attend maybe.', $likers);
1056                                 break;
1057                 }
1058
1059                 $expanded .= "\t" . '<div class="wall-item-' . $type . '-expanded" id="' . $type . 'list-' . $id . '" style="display: none;" >' . $explikers . EOL . '</div>';
1060         }
1061
1062         $phrase .= EOL ;
1063         $o .= replace_macros(get_markup_template('voting_fakelink.tpl'), [
1064                 '$phrase' => $phrase,
1065                 '$type' => $type,
1066                 '$id' => $id
1067         ]);
1068         $o .= $expanded;
1069
1070         return $o;
1071 }
1072
1073 function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
1074 {
1075         $o = '';
1076
1077         $geotag = x($x, 'allow_location') ? replace_macros(get_markup_template('jot_geotag.tpl'), []) : '';
1078
1079         $tpl = get_markup_template('jot-header.tpl');
1080         $a->page['htmlhead'] .= replace_macros($tpl, [
1081                 '$newpost'   => 'true',
1082                 '$baseurl'   => System::baseUrl(true),
1083                 '$geotag'    => $geotag,
1084                 '$nickname'  => $x['nickname'],
1085                 '$ispublic'  => L10n::t('Visible to <strong>everybody</strong>'),
1086                 '$linkurl'   => L10n::t('Please enter a image/video/audio/webpage URL:'),
1087                 '$term'      => L10n::t('Tag term:'),
1088                 '$fileas'    => L10n::t('Save to Folder:'),
1089                 '$whereareu' => L10n::t('Where are you right now?'),
1090                 '$delitems'  => L10n::t("Delete item\x28s\x29?")
1091         ]);
1092
1093         $jotplugins = '';
1094         Addon::callHooks('jot_tool', $jotplugins);
1095
1096         // Private/public post links for the non-JS ACL form
1097         $private_post = 1;
1098         if (x($_REQUEST, 'public')) {
1099                 $private_post = 0;
1100         }
1101
1102         $query_str = $a->query_string;
1103         if (strpos($query_str, 'public=1') !== false) {
1104                 $query_str = str_replace(['?public=1', '&public=1'], ['', ''], $query_str);
1105         }
1106
1107         /*
1108          * I think $a->query_string may never have ? in it, but I could be wrong
1109          * It looks like it's from the index.php?q=[etc] rewrite that the web
1110          * server does, which converts any ? to &, e.g. suggest&ignore=61 for suggest?ignore=61
1111          */
1112         if (strpos($query_str, '?') === false) {
1113                 $public_post_link = '?public=1';
1114         } else {
1115                 $public_post_link = '&public=1';
1116         }
1117
1118         // $tpl = replace_macros($tpl,array('$jotplugins' => $jotplugins));
1119         $tpl = get_markup_template("jot.tpl");
1120
1121         $o .= replace_macros($tpl,[
1122                 '$new_post' => L10n::t('New Post'),
1123                 '$return_path'  => $query_str,
1124                 '$action'       => 'item',
1125                 '$share'        => defaults($x, 'button', L10n::t('Share')),
1126                 '$upload'       => L10n::t('Upload photo'),
1127                 '$shortupload'  => L10n::t('upload photo'),
1128                 '$attach'       => L10n::t('Attach file'),
1129                 '$shortattach'  => L10n::t('attach file'),
1130                 '$edbold'       => L10n::t('Bold'),
1131                 '$editalic'     => L10n::t('Italic'),
1132                 '$eduline'      => L10n::t('Underline'),
1133                 '$edquote'      => L10n::t('Quote'),
1134                 '$edcode'       => L10n::t('Code'),
1135                 '$edimg'        => L10n::t('Image'),
1136                 '$edurl'        => L10n::t('Link'),
1137                 '$edattach'     => L10n::t('Link or Media'),
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'], 0);
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 }