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