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