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