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