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