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