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