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