]> git.mxchange.org Git - friendica.git/blob - include/conversation.php
db14558871cff7665605ab38f4727c50eb82237f
[friendica.git] / include / conversation.php
1 <?php
2 /**
3  * @file include/conversation.php
4  */
5
6 use Friendica\App;
7 use Friendica\Content\ContactSelector;
8 use Friendica\Content\Feature;
9 use Friendica\Content\Text\BBCode;
10 use Friendica\Core\Addon;
11 use Friendica\Core\Config;
12 use Friendica\Core\L10n;
13 use Friendica\Core\PConfig;
14 use Friendica\Core\Protocol;
15 use Friendica\Core\System;
16 use Friendica\Database\DBA;
17 use Friendica\Model\Contact;
18 use Friendica\Model\Item;
19 use Friendica\Model\Profile;
20 use Friendica\Model\Term;
21 use Friendica\Object\Post;
22 use Friendica\Object\Thread;
23 use Friendica\Util\DateTimeFormat;
24 use Friendica\Util\Proxy as ProxyUtils;
25 use Friendica\Util\Temporal;
26 use Friendica\Util\XML;
27
28 function item_extract_images($body) {
29
30         $saved_image = [];
31         $orig_body = $body;
32         $new_body = '';
33
34         $cnt = 0;
35         $img_start = strpos($orig_body, '[img');
36         $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
37         $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
38         while (($img_st_close !== false) && ($img_end !== false)) {
39
40                 $img_st_close++; // make it point to AFTER the closing bracket
41                 $img_end += $img_start;
42
43                 if (!strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
44                         // This is an embedded image
45
46                         $saved_image[$cnt] = substr($orig_body, $img_start + $img_st_close, $img_end - ($img_start + $img_st_close));
47                         $new_body = $new_body . substr($orig_body, 0, $img_start) . '[!#saved_image' . $cnt . '#!]';
48
49                         $cnt++;
50                 } else {
51                         $new_body = $new_body . substr($orig_body, 0, $img_end + strlen('[/img]'));
52                 }
53
54                 $orig_body = substr($orig_body, $img_end + strlen('[/img]'));
55
56                 if ($orig_body === false) {
57                         // in case the body ends on a closing image tag
58                         $orig_body = '';
59                 }
60
61                 $img_start = strpos($orig_body, '[img');
62                 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
63                 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
64         }
65
66         $new_body = $new_body . $orig_body;
67
68         return ['body' => $new_body, 'images' => $saved_image];
69 }
70
71 function item_redir_and_replace_images($body, $images, $cid) {
72
73         $origbody = $body;
74         $newbody = '';
75
76         $cnt = 1;
77         $pos = BBCode::getTagPosition($origbody, 'url', 0);
78         while ($pos !== false && $cnt < 1000) {
79
80                 $search = '/\[url\=(.*?)\]\[!#saved_image([0-9]*)#!\]\[\/url\]' . '/is';
81                 $replace = '[url=' . System::baseUrl() . '/redir/' . $cid
82                                    . '?f=1&url=' . '$1' . '][!#saved_image' . '$2' .'#!][/url]';
83
84                 $newbody .= substr($origbody, 0, $pos['start']['open']);
85                 $subject = substr($origbody, $pos['start']['open'], $pos['end']['close'] - $pos['start']['open']);
86                 $origbody = substr($origbody, $pos['end']['close']);
87                 if ($origbody === false) {
88                         $origbody = '';
89                 }
90
91                 $subject = preg_replace($search, $replace, $subject);
92                 $newbody .= $subject;
93
94                 $cnt++;
95                 // Isn't this supposed to use $cnt value for $occurrences? - @MrPetovan
96                 $pos = BBCode::getTagPosition($origbody, 'url', 0);
97         }
98         $newbody .= $origbody;
99
100         $cnt = 0;
101         foreach ($images as $image) {
102                 /*
103                  * We're depending on the property of 'foreach' (specified on the PHP website) that
104                  * it loops over the array starting from the first element and going sequentially
105                  * to the last element.
106                  */
107                 $newbody = str_replace('[!#saved_image' . $cnt . '#!]', '[img]' . $image . '[/img]', $newbody);
108                 $cnt++;
109         }
110         return $newbody;
111 }
112
113 /**
114  * Render actions localized
115  */
116 function localize_item(&$item)
117 {
118         $extracted = item_extract_images($item['body']);
119         if ($extracted['images']) {
120                 $item['body'] = item_redir_and_replace_images($extracted['body'], $extracted['images'], $item['contact-id']);
121         }
122
123         /*
124         heluecht 2018-06-19: from my point of view this whole code part is useless.
125         It just renders the body message of technical posts (Like, dislike, ...).
126         But: The body isn't visible at all. So we do this stuff just because we can.
127         Even if these messages were visible, this would only mean that something went wrong.
128         During the further steps of the database restructuring I would like to address this issue.
129         */
130
131         $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
132         if (activity_match($item['verb'], ACTIVITY_LIKE)
133                 || activity_match($item['verb'], ACTIVITY_DISLIKE)
134                 || activity_match($item['verb'], ACTIVITY_ATTEND)
135                 || activity_match($item['verb'], ACTIVITY_ATTENDNO)
136                 || activity_match($item['verb'], ACTIVITY_ATTENDMAYBE)) {
137
138                 $fields = ['author-link', 'author-name', 'verb', 'object-type', 'resource-id', 'body', 'plink'];
139                 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
140                 if (!DBA::isResult($obj)) {
141                         return;
142                 }
143
144                 $author  = '[url=' . $item['author-link'] . ']' . $item['author-name'] . '[/url]';
145                 $objauthor =  '[url=' . $obj['author-link'] . ']' . $obj['author-name'] . '[/url]';
146
147                 switch ($obj['verb']) {
148                         case ACTIVITY_POST:
149                                 switch ($obj['object-type']) {
150                                         case ACTIVITY_OBJ_EVENT:
151                                                 $post_type = L10n::t('event');
152                                                 break;
153                                         default:
154                                                 $post_type = L10n::t('status');
155                                 }
156                                 break;
157                         default:
158                                 if ($obj['resource-id']) {
159                                         $post_type = L10n::t('photo');
160                                         $m = [];
161                                         preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
162                                         $rr['plink'] = $m[1];
163                                 } else {
164                                         $post_type = L10n::t('status');
165                                 }
166                 }
167
168                 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
169
170                 if (activity_match($item['verb'], ACTIVITY_LIKE)) {
171                         $bodyverb = L10n::t('%1$s likes %2$s\'s %3$s');
172                 } elseif (activity_match($item['verb'], ACTIVITY_DISLIKE)) {
173                         $bodyverb = L10n::t('%1$s doesn\'t like %2$s\'s %3$s');
174                 } elseif (activity_match($item['verb'], ACTIVITY_ATTEND)) {
175                         $bodyverb = L10n::t('%1$s attends %2$s\'s %3$s');
176                 } elseif (activity_match($item['verb'], ACTIVITY_ATTENDNO)) {
177                         $bodyverb = L10n::t('%1$s doesn\'t attend %2$s\'s %3$s');
178                 } elseif (activity_match($item['verb'], ACTIVITY_ATTENDMAYBE)) {
179                         $bodyverb = L10n::t('%1$s attends maybe %2$s\'s %3$s');
180                 }
181
182                 $item['body'] = sprintf($bodyverb, $author, $objauthor, $plink);
183         }
184
185         if (activity_match($item['verb'], ACTIVITY_FRIEND)) {
186
187                 if ($item['object-type']=="" || $item['object-type']!== ACTIVITY_OBJ_PERSON) return;
188
189                 $Aname = $item['author-name'];
190                 $Alink = $item['author-link'];
191
192                 $xmlhead="<"."?xml version='1.0' encoding='UTF-8' ?".">";
193
194                 $obj = XML::parseString($xmlhead.$item['object']);
195                 $links = XML::parseString($xmlhead."<links>".unxmlify($obj->link)."</links>");
196
197                 $Bname = $obj->title;
198                 $Blink = "";
199                 $Bphoto = "";
200                 foreach ($links->link as $l) {
201                         $atts = $l->attributes();
202                         switch ($atts['rel']) {
203                                 case "alternate": $Blink = $atts['href'];
204                                 case "photo": $Bphoto = $atts['href'];
205                         }
206                 }
207
208                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
209                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
210                 if ($Bphoto != "") {
211                         $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img]' . $Bphoto . '[/img][/url]';
212                 }
213
214                 $item['body'] = L10n::t('%1$s is now friends with %2$s', $A, $B)."\n\n\n".$Bphoto;
215
216         }
217         if (stristr($item['verb'], ACTIVITY_POKE)) {
218                 $verb = urldecode(substr($item['verb'],strpos($item['verb'],'#')+1));
219                 if (!$verb) {
220                         return;
221                 }
222                 if ($item['object-type']=="" || $item['object-type']!== ACTIVITY_OBJ_PERSON) {
223                         return;
224                 }
225
226                 $Aname = $item['author-name'];
227                 $Alink = $item['author-link'];
228
229                 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
230
231                 $obj = XML::parseString($xmlhead.$item['object']);
232                 $links = XML::parseString($xmlhead."<links>".unxmlify($obj->link)."</links>");
233
234                 $Bname = $obj->title;
235                 $Blink = "";
236                 $Bphoto = "";
237                 foreach ($links->link as $l) {
238                         $atts = $l->attributes();
239                         switch ($atts['rel']) {
240                                 case "alternate": $Blink = $atts['href'];
241                                 case "photo": $Bphoto = $atts['href'];
242                         }
243                 }
244
245                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
246                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
247                 if ($Bphoto != "") {
248                         $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img=80x80]' . $Bphoto . '[/img][/url]';
249                 }
250
251                 /*
252                  * we can't have a translation string with three positions but no distinguishable text
253                  * So here is the translate string.
254                  */
255                 $txt = L10n::t('%1$s poked %2$s');
256
257                 // now translate the verb
258                 $poked_t = trim(sprintf($txt, "", ""));
259                 $txt = str_replace($poked_t, L10n::t($verb), $txt);
260
261                 // then do the sprintf on the translation string
262
263                 $item['body'] = sprintf($txt, $A, $B). "\n\n\n" . $Bphoto;
264
265         }
266
267         if (activity_match($item['verb'], ACTIVITY_TAG)) {
268                 $fields = ['author-id', 'author-link', 'author-name', 'author-network',
269                         'verb', 'object-type', 'resource-id', 'body', 'plink'];
270                 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
271                 if (!DBA::isResult($obj)) {
272                         return;
273                 }
274
275                 $author_arr = ['uid' => 0, 'id' => $item['author-id'],
276                         'network' => $item['author-network'], 'url' => $item['author-link']];
277                 $author  = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $item['author-name'] . '[/url]';
278
279                 $author_arr = ['uid' => 0, 'id' => $obj['author-id'],
280                         'network' => $obj['author-network'], 'url' => $obj['author-link']];
281                 $objauthor  = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $obj['author-name'] . '[/url]';
282
283                 switch ($obj['verb']) {
284                         case ACTIVITY_POST:
285                                 switch ($obj['object-type']) {
286                                         case ACTIVITY_OBJ_EVENT:
287                                                 $post_type = L10n::t('event');
288                                                 break;
289                                         default:
290                                                 $post_type = L10n::t('status');
291                                 }
292                                 break;
293                         default:
294                                 if ($obj['resource-id']) {
295                                         $post_type = L10n::t('photo');
296                                         $m=[]; preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
297                                         $rr['plink'] = $m[1];
298                                 } else {
299                                         $post_type = L10n::t('status');
300                                 }
301                                 // Let's break everthing ... ;-)
302                                 break;
303                 }
304                 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
305
306                 $parsedobj = XML::parseString($xmlhead.$item['object']);
307
308                 $tag = sprintf('#[url=%s]%s[/url]', $parsedobj->id, $parsedobj->content);
309                 $item['body'] = L10n::t('%1$s tagged %2$s\'s %3$s with %4$s', $author, $objauthor, $plink, $tag);
310         }
311
312         if (activity_match($item['verb'], ACTIVITY_FAVORITE)) {
313                 if ($item['object-type'] == "") {
314                         return;
315                 }
316
317                 $Aname = $item['author-name'];
318                 $Alink = $item['author-link'];
319
320                 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
321
322                 $obj = XML::parseString($xmlhead.$item['object']);
323                 if (strlen($obj->id)) {
324                         $fields = ['author-link', 'author-name', 'plink'];
325                         $target = Item::selectFirst($fields, ['uri' => $obj->id, 'uid' => $item['uid']]);
326                         if (DBA::isResult($target) && $target['plink']) {
327                                 $Bname = $target['author-name'];
328                                 $Blink = $target['author-link'];
329                                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
330                                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
331                                 $P = '[url=' . $target['plink'] . ']' . L10n::t('post/item') . '[/url]';
332                                 $item['body'] = L10n::t('%1$s marked %2$s\'s %3$s as favorite', $A, $B, $P)."\n";
333                         }
334                 }
335         }
336         $matches = null;
337         if (preg_match_all('/@\[url=(.*?)\]/is', $item['body'], $matches, PREG_SET_ORDER)) {
338                 foreach ($matches as $mtch) {
339                         if (!strpos($mtch[1], 'zrl=')) {
340                                 $item['body'] = str_replace($mtch[0], '@[url=' . Contact::magicLink($mtch[1]) . ']', $item['body']);
341                         }
342                 }
343         }
344
345         // add zrl's to public images
346         $photo_pattern = "/\[url=(.*?)\/photos\/(.*?)\/image\/(.*?)\]\[img(.*?)\]h(.*?)\[\/img\]\[\/url\]/is";
347         if (preg_match($photo_pattern, $item['body'])) {
348                 $photo_replace = '[url=' . Profile::zrl('$1' . '/photos/' . '$2' . '/image/' . '$3' ,true) . '][img' . '$4' . ']h' . '$5'  . '[/img][/url]';
349                 $item['body'] = BBCode::pregReplaceInTag($photo_pattern, $photo_replace, 'url', $item['body']);
350         }
351
352         // add sparkle links to appropriate permalinks
353         $author = ['uid' => 0, 'id' => $item['author-id'],
354                 'network' => $item['author-network'], 'url' => $item['author-link']];
355
356         if (!empty($item['plink'])) {
357                 $item['plink'] = Contact::magicLinkbyContact($author, $item['plink']);
358         }
359 }
360
361 /**
362  * Count the total of comments on this item and its desendants
363  * @TODO proper type-hint + doc-tag
364  */
365 function count_descendants($item) {
366         $total = count($item['children']);
367
368         if ($total > 0) {
369                 foreach ($item['children'] as $child) {
370                         if (!visible_activity($child)) {
371                                 $total --;
372                         }
373                         $total += count_descendants($child);
374                 }
375         }
376
377         return $total;
378 }
379
380 function visible_activity($item) {
381
382         /*
383          * likes (etc.) can apply to other things besides posts. Check if they are post children,
384          * in which case we handle them specially
385          */
386         $hidden_activities = [ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE];
387         foreach ($hidden_activities as $act) {
388                 if (activity_match($item['verb'], $act)) {
389                         return false;
390                 }
391         }
392
393         // @TODO below if() block can be rewritten to a single line: $isVisible = allConditionsHere;
394         if (activity_match($item['verb'], ACTIVITY_FOLLOW) && $item['object-type'] === ACTIVITY_OBJ_NOTE && empty($item['self']) && $item['uid'] == local_user()) {
395                 return false;
396         }
397
398         return true;
399 }
400
401 function conv_get_blocklist()
402 {
403         if (!local_user()) {
404                 return [];
405         }
406
407         $str_blocked = PConfig::get(local_user(), 'system', 'blocked');
408         if (empty($str_blocked)) {
409                 return [];
410         }
411
412         $blocklist = [];
413
414         foreach (explode(',', $str_blocked) as $entry) {
415                 // The 4th parameter guarantees that there always will be a public contact entry
416                 $cid = Contact::getIdForURL(trim($entry), 0, true, ['url' => trim($entry)]);
417                 if (!empty($cid)) {
418                         $blocklist[] = $cid;
419                 }
420         }
421
422         return $blocklist;
423 }
424
425 /**
426  * "Render" a conversation or list of items for HTML display.
427  * There are two major forms of display:
428  *      - Sequential or unthreaded ("New Item View" or search results)
429  *      - conversation view
430  * The $mode parameter decides between the various renderings and also
431  * figures out how to determine page owner and other contextual items
432  * that are based on unique features of the calling module.
433  *
434  */
435 function conversation(App $a, array $items, $mode, $update, $preview = false, $order = 'commented', $uid = 0) {
436
437         $ssl_state = (local_user() ? true : false);
438
439         $profile_owner = 0;
440         $live_update_div = '';
441
442         $blocklist = conv_get_blocklist();
443
444         $previewing = (($preview) ? ' preview ' : '');
445
446         if ($mode === 'network') {
447                 $items = conversation_add_children($items, false, $order, $uid);
448                 $profile_owner = local_user();
449                 if (!$update) {
450                         /*
451                          * The special div is needed for liveUpdate to kick in for this page.
452                          * We only launch liveUpdate if you aren't filtering in some incompatible
453                          * way and also you aren't writing a comment (discovered in javascript).
454                          */
455                         $live_update_div = '<div id="live-network"></div>' . "\r\n"
456                                 . "<script> var profile_uid = " . $_SESSION['uid']
457                                 . "; var netargs = '" . substr($a->cmd, 8)
458                                 . '?f='
459                                 . ((x($_GET, 'cid'))    ? '&cid='    . $_GET['cid']    : '')
460                                 . ((x($_GET, 'search')) ? '&search=' . $_GET['search'] : '')
461                                 . ((x($_GET, 'star'))   ? '&star='   . $_GET['star']   : '')
462                                 . ((x($_GET, 'order'))  ? '&order='  . $_GET['order']  : '')
463                                 . ((x($_GET, 'bmark'))  ? '&bmark='  . $_GET['bmark']  : '')
464                                 . ((x($_GET, 'liked'))  ? '&liked='  . $_GET['liked']  : '')
465                                 . ((x($_GET, 'conv'))   ? '&conv='   . $_GET['conv']   : '')
466                                 . ((x($_GET, 'nets'))   ? '&nets='   . $_GET['nets']   : '')
467                                 . ((x($_GET, 'cmin'))   ? '&cmin='   . $_GET['cmin']   : '')
468                                 . ((x($_GET, 'cmax'))   ? '&cmax='   . $_GET['cmax']   : '')
469                                 . ((x($_GET, 'file'))   ? '&file='   . $_GET['file']   : '')
470
471                                 . "'; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
472                 }
473         } elseif ($mode === 'profile') {
474                 $items = conversation_add_children($items, false, $order, $uid);
475                 $profile_owner = $a->profile['profile_uid'];
476
477                 if (!$update) {
478                         $tab = 'posts';
479                         if (x($_GET, 'tab')) {
480                                 $tab = notags(trim($_GET['tab']));
481                         }
482                         if ($tab === 'posts') {
483                                 /*
484                                  * This is ugly, but we can't pass the profile_uid through the session to the ajax updater,
485                                  * because browser prefetching might change it on us. We have to deliver it with the page.
486                                  */
487
488                                 $live_update_div = '<div id="live-profile"></div>' . "\r\n"
489                                         . "<script> var profile_uid = " . $a->profile['profile_uid']
490                                         . "; var netargs = '?f='; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
491                         }
492                 }
493         } elseif ($mode === 'notes') {
494                 $profile_owner = local_user();
495
496                 if (!$update) {
497                         $live_update_div = '<div id="live-notes"></div>' . "\r\n"
498                                 . "<script> var profile_uid = " . local_user()
499                                 . "; var netargs = '/?f='; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
500                 }
501         } elseif ($mode === 'display') {
502                 $profile_owner = $a->profile['uid'];
503
504                 if (!$update) {
505                         $live_update_div = '<div id="live-display"></div>' . "\r\n"
506                                 . "<script> var profile_uid = " . defaults($_SESSION, 'uid', 0) . ";"
507                                 . " var profile_page = 1; </script>";
508                 }
509         } elseif ($mode === 'community') {
510                 $items = conversation_add_children($items, true, $order, $uid);
511                 $profile_owner = 0;
512
513                 if (!$update) {
514                         $live_update_div = '<div id="live-community"></div>' . "\r\n"
515                                 . "<script> var profile_uid = -1; var netargs = '" . substr($a->cmd, 10)
516                                 ."/?f='; var profile_page = " . $a->pager['page'] . "; </script>\r\n";
517                 }
518         } elseif ($mode === 'search') {
519                 $live_update_div = '<div id="live-search"></div>' . "\r\n";
520         }
521
522         $page_dropping = ((local_user() && local_user() == $profile_owner) ? true : false);
523
524         if (!$update) {
525                 $_SESSION['return_url'] = $a->query_string;
526         }
527
528         $cb = ['items' => $items, 'mode' => $mode, 'update' => $update, 'preview' => $preview];
529         Addon::callHooks('conversation_start',$cb);
530
531         $items = $cb['items'];
532
533         $conv_responses = [
534                 'like' => ['title' => L10n::t('Likes','title')], 'dislike' => ['title' => L10n::t('Dislikes','title')],
535                 'attendyes' => ['title' => L10n::t('Attending','title')], 'attendno' => ['title' => L10n::t('Not attending','title')], 'attendmaybe' => ['title' => L10n::t('Might attend','title')]
536         ];
537
538         // array with html for each thread (parent+comments)
539         $threads = [];
540         $threadsid = -1;
541
542         $page_template = get_markup_template("conversation.tpl");
543
544         if (!empty($items)) {
545                 if ($mode === 'community') {
546                         $writable = true;
547                 } else {
548                         $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], [Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
549                 }
550
551                 if (!local_user()) {
552                         $writable = false;
553                 }
554
555                 if (in_array($mode, ['network-new', 'search', 'contact-posts'])) {
556
557                         /*
558                          * "New Item View" on network page or search page results
559                          * - just loop through the items and format them minimally for display
560                          */
561
562                         $tpl = 'search_item.tpl';
563
564                         foreach ($items as $item) {
565
566                                 if (!visible_activity($item)) {
567                                         continue;
568                                 }
569
570                                 if (in_array($item['author-id'], $blocklist)) {
571                                         continue;
572                                 }
573
574                                 $threadsid++;
575
576                                 $owner_url   = '';
577                                 $owner_name  = '';
578                                 $sparkle     = '';
579
580                                 // prevent private email from leaking.
581                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
582                                         continue;
583                                 }
584
585                                 $profile_name = $item['author-name'];
586                                 if (!empty($item['author-link']) && empty($item['author-name'])) {
587                                         $profile_name = $item['author-link'];
588                                 }
589
590                                 $tags = Term::populateTagsFromItem($item);
591
592                                 $author = ['uid' => 0, 'id' => $item['author-id'],
593                                         'network' => $item['author-network'], 'url' => $item['author-link']];
594                                 $profile_link = Contact::magicLinkbyContact($author);
595
596                                 if (strpos($profile_link, 'redir/') === 0) {
597                                         $sparkle = ' sparkle';
598                                 }
599
600                                 $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
601                                 Addon::callHooks('render_location',$locate);
602
603                                 $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
604
605                                 localize_item($item);
606                                 if ($mode === 'network-new') {
607                                         $dropping = true;
608                                 } else {
609                                         $dropping = false;
610                                 }
611
612                                 $drop = [
613                                         'dropping' => $dropping,
614                                         'pagedrop' => $page_dropping,
615                                         'select' => L10n::t('Select'),
616                                         'delete' => L10n::t('Delete'),
617                                 ];
618
619                                 $star = false;
620                                 $isstarred = "unstarred";
621
622                                 $lock = false;
623                                 $likebuttons = false;
624
625                                 $body = prepare_body($item, true, $preview);
626
627                                 list($categories, $folders) = get_cats_and_terms($item);
628
629                                 $profile_name_e = $profile_name;
630
631                                 if (!empty($item['content-warning']) && PConfig::get(local_user(), 'system', 'disable_cw', false)) {
632                                         $title_e = ucfirst($item['content-warning']);
633                                 } else {
634                                         $title_e = $item['title'];
635                                 }
636
637                                 $body_e = $body;
638                                 $tags_e = $tags['tags'];
639                                 $hashtags_e = $tags['hashtags'];
640                                 $mentions_e = $tags['mentions'];
641                                 $location_e = $location;
642                                 $owner_name_e = $owner_name;
643
644                                 $tmp_item = [
645                                         'template' => $tpl,
646                                         'id' => ($preview ? 'P0' : $item['id']),
647                                         'guid' => ($preview ? 'Q0' : $item['guid']),
648                                         'network' => $item['network'],
649                                         'network_name' => ContactSelector::networkToName($item['network'], $profile_link),
650                                         'linktitle' => L10n::t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
651                                         'profile_url' => $profile_link,
652                                         'item_photo_menu' => item_photo_menu($item),
653                                         'name' => $profile_name_e,
654                                         'sparkle' => $sparkle,
655                                         'lock' => $lock,
656                                         'thumb' => System::removedBaseUrl(ProxyUtils::proxifyUrl($item['author-avatar'], false, ProxyUtils::SIZE_THUMB)),
657                                         'title' => $title_e,
658                                         'body' => $body_e,
659                                         'tags' => $tags_e,
660                                         'hashtags' => $hashtags_e,
661                                         'mentions' => $mentions_e,
662                                         'txt_cats' => L10n::t('Categories:'),
663                                         'txt_folders' => L10n::t('Filed under:'),
664                                         'has_cats' => ((count($categories)) ? 'true' : ''),
665                                         'has_folders' => ((count($folders)) ? 'true' : ''),
666                                         'categories' => $categories,
667                                         'folders' => $folders,
668                                         'text' => strip_tags($body_e),
669                                         'localtime' => DateTimeFormat::local($item['created'], 'r'),
670                                         'ago' => (($item['app']) ? L10n::t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
671                                         'location' => $location_e,
672                                         'indent' => '',
673                                         'owner_name' => $owner_name_e,
674                                         'owner_url' => $owner_url,
675                                         'owner_photo' => System::removedBaseUrl(ProxyUtils::proxifyUrl($item['owner-avatar'], false, ProxyUtils::SIZE_THUMB)),
676                                         'plink' => get_plink($item),
677                                         'edpost' => false,
678                                         'isstarred' => $isstarred,
679                                         'star' => $star,
680                                         'drop' => $drop,
681                                         'vote' => $likebuttons,
682                                         'like' => '',
683                                         'dislike' => '',
684                                         'comment' => '',
685                                         'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> L10n::t('View in context')]),
686                                         'previewing' => $previewing,
687                                         'wait' => L10n::t('Please wait'),
688                                         'thread_level' => 1,
689                                 ];
690
691                                 $arr = ['item' => $item, 'output' => $tmp_item];
692                                 Addon::callHooks('display_item', $arr);
693
694                                 $threads[$threadsid]['id'] = $item['id'];
695                                 $threads[$threadsid]['network'] = $item['network'];
696                                 $threads[$threadsid]['items'] = [$arr['output']];
697
698                         }
699                 } else {
700                         // Normal View
701                         $page_template = get_markup_template("threaded_conversation.tpl");
702
703                         $conv = new Thread($mode, $preview, $writable);
704
705                         /*
706                          * get all the topmost parents
707                          * this shouldn't be needed, as we should have only them in our array
708                          * But for now, this array respects the old style, just in case
709                          */
710                         foreach ($items as $item) {
711                                 if (in_array($item['author-id'], $blocklist)) {
712                                         continue;
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'] === Protocol::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'], [Protocol::OSTATUS, Protocol::DIASPORA, Protocol::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 (DBA::isResult($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, [Protocol::DFRN, Protocol::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 == Protocol::DFRN) {
865                         $menu[L10n::t("Poke")] = $poke_link;
866                 }
867
868                 if ((($cid == 0) || ($rel == Contact::FOLLOWER)) &&
869                         in_array($item['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::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                 '$wall'         => $notes_cid ? 0 : 1,
1153                 '$posttype'     => $notes_cid ? Item::PT_PERSONAL_NOTE : Item::PT_ARTICLE,
1154                 '$content'      => defaults($x, 'content', ''),
1155                 '$post_id'      => defaults($x, 'post_id', ''),
1156                 '$baseurl'      => System::baseUrl(true),
1157                 '$defloc'       => $x['default_location'],
1158                 '$visitor'      => $x['visitor'],
1159                 '$pvisit'       => $notes_cid ? 'none' : $x['visitor'],
1160                 '$public'       => L10n::t('Public post'),
1161                 '$lockstate'    => $x['lockstate'],
1162                 '$bang'         => $x['bang'],
1163                 '$profile_uid'  => $x['profile_uid'],
1164                 '$preview'      => Feature::isEnabled($x['profile_uid'], 'preview') ? L10n::t('Preview') : '',
1165                 '$jotplugins'   => $jotplugins,
1166                 '$notes_cid'    => $notes_cid,
1167                 '$sourceapp'    => L10n::t($a->sourcename),
1168                 '$cancel'       => L10n::t('Cancel'),
1169                 '$rand_num'     => random_digits(12),
1170
1171                 // ACL permissions box
1172                 '$acl'           => $x['acl'],
1173                 '$group_perms'   => L10n::t('Post to Groups'),
1174                 '$contact_perms' => L10n::t('Post to Contacts'),
1175                 '$private'       => L10n::t('Private post'),
1176                 '$is_private'    => $private_post,
1177                 '$public_link'   => $public_post_link,
1178
1179                 //jot nav tab (used in some themes)
1180                 '$message' => L10n::t('Message'),
1181                 '$browser' => L10n::t('Browser'),
1182         ]);
1183
1184
1185         if ($popup == true) {
1186                 $o = '<div id="jot-popup" style="display: none;">' . $o . '</div>';
1187         }
1188
1189         return $o;
1190 }
1191
1192 /**
1193  * Plucks the children of the given parent from a given item list.
1194  *
1195  * @brief Plucks all the children in the given item list of the given parent
1196  *
1197  * @param array $item_list
1198  * @param array $parent
1199  * @param bool $recursive
1200  * @return type
1201  */
1202 function get_item_children(array &$item_list, array $parent, $recursive = true)
1203 {
1204         $children = [];
1205         foreach ($item_list as $i => $item) {
1206                 if ($item['id'] != $item['parent']) {
1207                         if ($recursive) {
1208                                 // Fallback to parent-uri if thr-parent is not set
1209                                 $thr_parent = $item['thr-parent'];
1210                                 if ($thr_parent == '') {
1211                                         $thr_parent = $item['parent-uri'];
1212                                 }
1213
1214                                 if ($thr_parent == $parent['uri']) {
1215                                         $item['children'] = get_item_children($item_list, $item);
1216                                         $children[] = $item;
1217                                         unset($item_list[$i]);
1218                                 }
1219                         } elseif ($item['parent'] == $parent['id']) {
1220                                 $children[] = $item;
1221                                 unset($item_list[$i]);
1222                         }
1223                 }
1224         }
1225         return $children;
1226 }
1227
1228 /**
1229  * @brief Recursively sorts a tree-like item array
1230  *
1231  * @param array $items
1232  * @return array
1233  */
1234 function sort_item_children(array $items)
1235 {
1236         $result = $items;
1237         usort($result, 'sort_thr_created_rev');
1238         foreach ($result as $k => $i) {
1239                 if (isset($result[$k]['children'])) {
1240                         $result[$k]['children'] = sort_item_children($result[$k]['children']);
1241                 }
1242         }
1243         return $result;
1244 }
1245
1246 /**
1247  * @brief Recursively add all children items at the top level of a list
1248  *
1249  * @param array $children List of items to append
1250  * @param array $item_list
1251  */
1252 function add_children_to_list(array $children, array &$item_list)
1253 {
1254         foreach ($children as $child) {
1255                 $item_list[] = $child;
1256                 if (isset($child['children'])) {
1257                         add_children_to_list($child['children'], $item_list);
1258                 }
1259         }
1260 }
1261
1262 /**
1263  * This recursive function takes the item tree structure created by conv_sort() and
1264  * flatten the extraneous depth levels when people reply sequentially, removing the
1265  * stairs effect in threaded conversations limiting the available content width.
1266  *
1267  * The basic principle is the following: if a post item has only one reply and is
1268  * the last reply of its parent, then the reply is moved to the parent.
1269  *
1270  * This process is rendered somewhat more complicated because items can be either
1271  * replies or likes, and these don't factor at all in the reply count/last reply.
1272  *
1273  * @brief Selectively flattens a tree-like item structure to prevent threading stairs
1274  *
1275  * @param array $parent A tree-like array of items
1276  * @return array
1277  */
1278 function smart_flatten_conversation(array $parent)
1279 {
1280         if (!isset($parent['children']) || count($parent['children']) == 0) {
1281                 return $parent;
1282         }
1283
1284         // We use a for loop to ensure we process the newly-moved items
1285         for ($i = 0; $i < count($parent['children']); $i++) {
1286                 $child = $parent['children'][$i];
1287
1288                 if (isset($child['children']) && count($child['children'])) {
1289                         // This helps counting only the regular posts
1290                         $count_post_closure = function($var) {
1291                                 return $var['verb'] === ACTIVITY_POST;
1292                         };
1293
1294                         $child_post_count = count(array_filter($child['children'], $count_post_closure));
1295
1296                         $remaining_post_count = count(array_filter(array_slice($parent['children'], $i), $count_post_closure));
1297
1298                         // If there's only one child's children post and this is the last child post
1299                         if ($child_post_count == 1 && $remaining_post_count == 1) {
1300
1301                                 // Searches the post item in the children
1302                                 $j = 0;
1303                                 while($child['children'][$j]['verb'] !== ACTIVITY_POST && $j < count($child['children'])) {
1304                                         $j ++;
1305                                 }
1306
1307                                 $moved_item = $child['children'][$j];
1308                                 unset($parent['children'][$i]['children'][$j]);
1309                                 $parent['children'][] = $moved_item;
1310                         } else {
1311                                 $parent['children'][$i] = smart_flatten_conversation($child);
1312                         }
1313                 }
1314         }
1315
1316         return $parent;
1317 }
1318
1319
1320 /**
1321  * Expands a flat list of items into corresponding tree-like conversation structures,
1322  * sort the top-level posts either on "created" or "commented", and finally
1323  * append all the items at the top level (???)
1324  *
1325  * @brief Expands a flat item list into a conversation array for display
1326  *
1327  * @param array  $item_list A list of items belonging to one or more conversations
1328  * @param string $order     Either on "created" or "commented"
1329  * @return array
1330  */
1331 function conv_sort(array $item_list, $order)
1332 {
1333         $parents = [];
1334
1335         if (!(is_array($item_list) && count($item_list))) {
1336                 return $parents;
1337         }
1338
1339         $blocklist = conv_get_blocklist();
1340
1341         $item_array = [];
1342
1343         // Dedupes the item list on the uri to prevent infinite loops
1344         foreach ($item_list as $item) {
1345                 if (in_array($item['author-id'], $blocklist)) {
1346                         continue;
1347                 }
1348
1349                 $item_array[$item['uri']] = $item;
1350         }
1351
1352         // Extract the top level items
1353         foreach ($item_array as $item) {
1354                 if ($item['id'] == $item['parent']) {
1355                         $parents[] = $item;
1356                 }
1357         }
1358
1359         if (stristr($order, 'created')) {
1360                 usort($parents, 'sort_thr_created');
1361         } elseif (stristr($order, 'commented')) {
1362                 usort($parents, 'sort_thr_commented');
1363         }
1364
1365         /*
1366          * Plucks children from the item_array, second pass collects eventual orphan
1367          * items and add them as children of their top-level post.
1368          */
1369         foreach ($parents as $i => $parent) {
1370                 $parents[$i]['children'] =
1371                         array_merge(get_item_children($item_array, $parent, true),
1372                                 get_item_children($item_array, $parent, false));
1373         }
1374
1375         foreach ($parents as $i => $parent) {
1376                 $parents[$i]['children'] = sort_item_children($parents[$i]['children']);
1377         }
1378
1379         if (PConfig::get(local_user(), 'system', 'smart_threading', 0)) {
1380                 foreach ($parents as $i => $parent) {
1381                         $parents[$i] = smart_flatten_conversation($parent);
1382                 }
1383         }
1384
1385         /// @TODO: Stop recusrsively adding all children back to the top level (!!!)
1386         /// However, this apparently ensures responses (likes, attendance) display (?!)
1387         foreach ($parents as $parent) {
1388                 if (count($parent['children'])) {
1389                         add_children_to_list($parent['children'], $parents);
1390                 }
1391         }
1392
1393         return $parents;
1394 }
1395
1396 /**
1397  * @brief usort() callback to sort item arrays by the created key
1398  *
1399  * @param array $a
1400  * @param array $b
1401  * @return int
1402  */
1403 function sort_thr_created(array $a, array $b)
1404 {
1405         return strcmp($b['created'], $a['created']);
1406 }
1407
1408 /**
1409  * @brief usort() callback to reverse sort item arrays by the created key
1410  *
1411  * @param array $a
1412  * @param array $b
1413  * @return int
1414  */
1415 function sort_thr_created_rev(array $a, array $b)
1416 {
1417         return strcmp($a['created'], $b['created']);
1418 }
1419
1420 /**
1421  * @brief usort() callback to sort item arrays by the commented key
1422  *
1423  * @param array $a
1424  * @param array $b
1425  * @return type
1426  */
1427 function sort_thr_commented(array $a, array $b)
1428 {
1429         return strcmp($b['commented'], $a['commented']);
1430 }
1431
1432 function render_location_dummy(array $item) {
1433         if (x($item, 'location') && !empty($item['location'])) {
1434                 return $item['location'];
1435         }
1436
1437         if (x($item, 'coord') && !empty($item['coord'])) {
1438                 return $item['coord'];
1439         }
1440 }
1441
1442 function get_responses(array $conv_responses, array $response_verbs, $ob, array $item) {
1443         $ret = [];
1444         foreach ($response_verbs as $v) {
1445                 $ret[$v] = [];
1446                 $ret[$v]['count'] = defaults($conv_responses[$v], $item['uri'], '');
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 }