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