]> git.mxchange.org Git - friendica.git/blob - include/conversation.php
f7e08571c09ab6f23ed4b847b43980841e572b7e
[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')], 'dislike' => ['title' => L10n::t('Dislikes','title')],
569                 'attendyes' => ['title' => L10n::t('Attending','title')], 'attendno' => ['title' => L10n::t('Not attending','title')], 'attendmaybe' => ['title' => L10n::t('Might attend','title')]
570         ];
571
572         // array with html for each thread (parent+comments)
573         $threads = [];
574         $threadsid = -1;
575
576         $page_template = Renderer::getMarkupTemplate("conversation.tpl");
577
578         if (!empty($items)) {
579                 if (in_array($mode, ['community', 'contacts'])) {
580                         $writable = true;
581                 } else {
582                         $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
583                 }
584
585                 if (!local_user()) {
586                         $writable = false;
587                 }
588
589                 if (in_array($mode, ['network-new', 'search', 'contact-posts'])) {
590
591                         /*
592                          * "New Item View" on network page or search page results
593                          * - just loop through the items and format them minimally for display
594                          */
595
596                         $tpl = 'search_item.tpl';
597
598                         foreach ($items as $item) {
599
600                                 if (!visible_activity($item)) {
601                                         continue;
602                                 }
603
604                                 if (in_array($item['author-id'], $blocklist)) {
605                                         continue;
606                                 }
607
608                                 $threadsid++;
609
610                                 $owner_url   = '';
611                                 $owner_name  = '';
612                                 $sparkle     = '';
613
614                                 // prevent private email from leaking.
615                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
616                                         continue;
617                                 }
618
619                                 $profile_name = $item['author-name'];
620                                 if (!empty($item['author-link']) && empty($item['author-name'])) {
621                                         $profile_name = $item['author-link'];
622                                 }
623
624                                 $tags = Term::populateTagsFromItem($item);
625
626                                 $author = ['uid' => 0, 'id' => $item['author-id'],
627                                         'network' => $item['author-network'], 'url' => $item['author-link']];
628                                 $profile_link = Contact::magicLinkByContact($author);
629
630                                 if (strpos($profile_link, 'redir/') === 0) {
631                                         $sparkle = ' sparkle';
632                                 }
633
634                                 $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
635                                 Hook::callAll('render_location',$locate);
636
637                                 $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
638
639                                 localize_item($item);
640                                 if ($mode === 'network-new') {
641                                         $dropping = true;
642                                 } else {
643                                         $dropping = false;
644                                 }
645
646                                 $drop = [
647                                         'dropping' => $dropping,
648                                         'pagedrop' => $page_dropping,
649                                         'select' => L10n::t('Select'),
650                                         'delete' => L10n::t('Delete'),
651                                 ];
652
653                                 $star = false;
654                                 $isstarred = "unstarred";
655
656                                 $lock = false;
657                                 $likebuttons = false;
658
659                                 $body = Item::prepareBody($item, true, $preview);
660
661                                 list($categories, $folders) = get_cats_and_terms($item);
662
663                                 if (!empty($item['content-warning']) && PConfig::get(local_user(), 'system', 'disable_cw', false)) {
664                                         $title = ucfirst($item['content-warning']);
665                                 } else {
666                                         $title = $item['title'];
667                                 }
668
669                                 $tmp_item = [
670                                         'template' => $tpl,
671                                         'id' => ($preview ? 'P0' : $item['id']),
672                                         'guid' => ($preview ? 'Q0' : $item['guid']),
673                                         'network' => $item['network'],
674                                         'network_name' => ContactSelector::networkToName($item['network'], $item['author-link']),
675                                         'linktitle' => L10n::t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
676                                         'profile_url' => $profile_link,
677                                         'item_photo_menu' => item_photo_menu($item),
678                                         'name' => $profile_name,
679                                         'sparkle' => $sparkle,
680                                         'lock' => $lock,
681                                         'thumb' => System::removedBaseUrl(ProxyUtils::proxifyUrl($item['author-avatar'], false, ProxyUtils::SIZE_THUMB)),
682                                         'title' => $title,
683                                         'body' => $body,
684                                         'tags' => $tags['tags'],
685                                         'hashtags' => $tags['hashtags'],
686                                         'mentions' => $tags['mentions'],
687                                         'implicit_mentions' => $tags['implicit_mentions'],
688                                         'txt_cats' => L10n::t('Categories:'),
689                                         'txt_folders' => L10n::t('Filed under:'),
690                                         'has_cats' => ((count($categories)) ? 'true' : ''),
691                                         'has_folders' => ((count($folders)) ? 'true' : ''),
692                                         'categories' => $categories,
693                                         'folders' => $folders,
694                                         'text' => strip_tags($body),
695                                         'localtime' => DateTimeFormat::local($item['created'], 'r'),
696                                         'ago' => (($item['app']) ? L10n::t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
697                                         'location' => $location,
698                                         'indent' => '',
699                                         'owner_name' => $owner_name,
700                                         'owner_url' => $owner_url,
701                                         'owner_photo' => System::removedBaseUrl(ProxyUtils::proxifyUrl($item['owner-avatar'], false, ProxyUtils::SIZE_THUMB)),
702                                         'plink' => Item::getPlink($item),
703                                         'edpost' => false,
704                                         'isstarred' => $isstarred,
705                                         'star' => $star,
706                                         'drop' => $drop,
707                                         'vote' => $likebuttons,
708                                         'like' => '',
709                                         'dislike' => '',
710                                         'comment' => '',
711                                         'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> L10n::t('View in context')]),
712                                         'previewing' => $previewing,
713                                         'wait' => L10n::t('Please wait'),
714                                         'thread_level' => 1,
715                                 ];
716
717                                 $arr = ['item' => $item, 'output' => $tmp_item];
718                                 Hook::callAll('display_item', $arr);
719
720                                 $threads[$threadsid]['id'] = $item['id'];
721                                 $threads[$threadsid]['network'] = $item['network'];
722                                 $threads[$threadsid]['items'] = [$arr['output']];
723
724                         }
725                 } else {
726                         // Normal View
727                         $page_template = Renderer::getMarkupTemplate("threaded_conversation.tpl");
728
729                         $conv = new Thread($mode, $preview, $writable);
730
731                         /*
732                          * get all the topmost parents
733                          * this shouldn't be needed, as we should have only them in our array
734                          * But for now, this array respects the old style, just in case
735                          */
736                         foreach ($items as $item) {
737                                 if (in_array($item['author-id'], $blocklist)) {
738                                         continue;
739                                 }
740
741                                 // Can we put this after the visibility check?
742                                 builtin_activity_puller($item, $conv_responses);
743
744                                 // Only add what is visible
745                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
746                                         continue;
747                                 }
748
749                                 if (!visible_activity($item)) {
750                                         continue;
751                                 }
752
753                                 /// @todo Check if this call is needed or not
754                                 $arr = ['item' => $item];
755                                 Hook::callAll('display_item', $arr);
756
757                                 $item['pagedrop'] = $page_dropping;
758
759                                 if ($item['id'] == $item['parent']) {
760                                         $item_object = new Post($item);
761                                         $conv->addParent($item_object);
762                                 }
763                         }
764
765                         $threads = $conv->getTemplateData($conv_responses);
766                         if (!$threads) {
767                                 Logger::log('[ERROR] conversation : Failed to get template data.', Logger::DEBUG);
768                                 $threads = [];
769                         }
770                 }
771         }
772
773         $o = Renderer::replaceMacros($page_template, [
774                 '$baseurl' => System::baseUrl($ssl_state),
775                 '$return_path' => $a->query_string,
776                 '$live_update' => $live_update_div,
777                 '$remove' => L10n::t('remove'),
778                 '$mode' => $mode,
779                 '$user' => $a->user,
780                 '$threads' => $threads,
781                 '$dropping' => ($page_dropping ? L10n::t('Delete Selected Items') : False),
782         ]);
783
784         return $o;
785 }
786
787 /**
788  * @brief Add comments to top level entries that had been fetched before
789  *
790  * The system will fetch the comments for the local user whenever possible.
791  * This behaviour is currently needed to allow commenting on Friendica posts.
792  *
793  * @param array $parents Parent items
794  *
795  * @param       $block_authors
796  * @param       $order
797  * @param       $uid
798  * @return array items with parents and comments
799  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
800  */
801 function conversation_add_children(array $parents, $block_authors, $order, $uid) {
802         $max_comments = Config::get('system', 'max_comments', 100);
803
804         $params = ['order' => ['uid', 'commented' => true]];
805
806         if ($max_comments > 0) {
807                 $params['limit'] = $max_comments;
808         }
809
810         $items = [];
811
812         foreach ($parents AS $parent) {
813                 $condition = ["`item`.`parent-uri` = ? AND `item`.`uid` IN (0, ?) ",
814                         $parent['uri'], $uid];
815                 if ($block_authors) {
816                         $condition[0] .= "AND NOT `author`.`hidden`";
817                 }
818                 $thread_items = Item::selectForUser(local_user(), [], $condition, $params);
819
820                 $comments = Item::inArray($thread_items);
821
822                 if (count($comments) != 0) {
823                         $items = array_merge($items, $comments);
824                 }
825         }
826
827         foreach ($items as $index => $item) {
828                 if ($item['uid'] == 0) {
829                         $items[$index]['writable'] = in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
830                 }
831         }
832
833         $items = conv_sort($items, $order);
834
835         return $items;
836 }
837
838 function item_photo_menu($item) {
839         $sub_link = '';
840         $poke_link = '';
841         $contact_url = '';
842         $pm_url = '';
843         $status_link = '';
844         $photos_link = '';
845         $posts_link = '';
846         $block_link = '';
847         $ignore_link = '';
848
849         if (local_user() && local_user() == $item['uid'] && $item['parent'] == $item['id'] && !$item['self']) {
850                 $sub_link = 'javascript:dosubthread(' . $item['id'] . '); return false;';
851         }
852
853         $author = ['uid' => 0, 'id' => $item['author-id'],
854                 'network' => $item['author-network'], 'url' => $item['author-link']];
855         $profile_link = Contact::magicLinkByContact($author, $item['author-link']);
856         $sparkle = (strpos($profile_link, 'redir/') === 0);
857
858         $cid = 0;
859         $pcid = Contact::getIdForURL($item['author-link'], 0, true);
860         $network = '';
861         $rel = 0;
862         $condition = ['uid' => local_user(), 'nurl' => Strings::normaliseLink($item['author-link'])];
863         $contact = DBA::selectFirst('contact', ['id', 'network', 'rel'], $condition);
864         if (DBA::isResult($contact)) {
865                 $cid = $contact['id'];
866                 $network = $contact['network'];
867                 $rel = $contact['rel'];
868         }
869
870         if ($sparkle) {
871                 $status_link = $profile_link . '?tab=status';
872                 $photos_link = str_replace('/profile/', '/photos/', $profile_link);
873                 $profile_link = $profile_link . '?=profile';
874         }
875
876         if (!empty($pcid)) {
877                 $contact_url = 'contact/' . $pcid;
878                 $posts_link = 'contact/' . $pcid . '/posts';
879                 $block_link = 'contact/' . $pcid . '/block';
880                 $ignore_link = 'contact/' . $pcid . '/ignore';
881         }
882
883         if ($cid && !$item['self']) {
884                 $poke_link = 'poke/?f=&c=' . $cid;
885                 $contact_url = 'contact/' . $cid;
886                 $posts_link = 'contact/' . $cid . '/posts';
887
888                 if (in_array($network, [Protocol::DFRN, Protocol::DIASPORA])) {
889                         $pm_url = 'message/new/' . $cid;
890                 }
891         }
892
893         if (local_user()) {
894                 $menu = [
895                         L10n::t('Follow Thread') => $sub_link,
896                         L10n::t('View Status') => $status_link,
897                         L10n::t('View Profile') => $profile_link,
898                         L10n::t('View Photos') => $photos_link,
899                         L10n::t('Network Posts') => $posts_link,
900                         L10n::t('View Contact') => $contact_url,
901                         L10n::t('Send PM') => $pm_url,
902                         L10n::t('Block') => $block_link,
903                         L10n::t('Ignore') => $ignore_link
904                 ];
905
906                 if ($network == Protocol::DFRN) {
907                         $menu[L10n::t("Poke")] = $poke_link;
908                 }
909
910                 if ((($cid == 0) || ($rel == Contact::FOLLOWER)) &&
911                         in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) {
912                         $menu[L10n::t('Connect/Follow')] = 'follow?url=' . urlencode($item['author-link']);
913                 }
914         } else {
915                 $menu = [L10n::t('View Profile') => $item['author-link']];
916         }
917
918         $args = ['item' => $item, 'menu' => $menu];
919
920         Hook::callAll('item_photo_menu', $args);
921
922         $menu = $args['menu'];
923
924         $o = '';
925         foreach ($menu as $k => $v) {
926                 if (strpos($v, 'javascript:') === 0) {
927                         $v = substr($v, 11);
928                         $o .= '<li role="menuitem"><a onclick="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
929                 } elseif ($v!='') {
930                         $o .= '<li role="menuitem"><a href="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
931                 }
932         }
933         return $o;
934 }
935
936 /**
937  * @brief Checks item to see if it is one of the builtin activities (like/dislike, event attendance, consensus items, etc.)
938  * Increments the count of each matching activity and adds a link to the author as needed.
939  *
940  * @param array  $item
941  * @param array &$conv_responses (already created with builtin activity structure)
942  * @return void
943  * @throws ImagickException
944  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
945  */
946 function builtin_activity_puller($item, &$conv_responses) {
947         foreach ($conv_responses as $mode => $v) {
948                 $sparkle = '';
949
950                 switch ($mode) {
951                         case 'like':
952                                 $verb = ACTIVITY_LIKE;
953                                 break;
954                         case 'dislike':
955                                 $verb = ACTIVITY_DISLIKE;
956                                 break;
957                         case 'attendyes':
958                                 $verb = ACTIVITY_ATTEND;
959                                 break;
960                         case 'attendno':
961                                 $verb = ACTIVITY_ATTENDNO;
962                                 break;
963                         case 'attendmaybe':
964                                 $verb = ACTIVITY_ATTENDMAYBE;
965                                 break;
966                         default:
967                                 return;
968                 }
969
970                 if (activity_match($item['verb'], $verb) && ($item['id'] != $item['parent'])) {
971                         $author = ['uid' => 0, 'id' => $item['author-id'],
972                                 'network' => $item['author-network'], 'url' => $item['author-link']];
973                         $url = Contact::magicLinkByContact($author);
974                         if (strpos($url, 'redir/') === 0) {
975                                 $sparkle = ' class="sparkle" ';
976                         }
977
978                         $url = '<a href="'. $url . '"'. $sparkle .'>' . htmlentities($item['author-name']) . '</a>';
979
980                         if (empty($item['thr-parent'])) {
981                                 $item['thr-parent'] = $item['parent-uri'];
982                         }
983
984                         if (!(isset($conv_responses[$mode][$item['thr-parent'] . '-l'])
985                                 && is_array($conv_responses[$mode][$item['thr-parent'] . '-l']))) {
986                                 $conv_responses[$mode][$item['thr-parent'] . '-l'] = [];
987                         }
988
989                         // only list each unique author once
990                         if (in_array($url,$conv_responses[$mode][$item['thr-parent'] . '-l'])) {
991                                 continue;
992                         }
993
994                         if (!isset($conv_responses[$mode][$item['thr-parent']])) {
995                                 $conv_responses[$mode][$item['thr-parent']] = 1;
996                         } else {
997                                 $conv_responses[$mode][$item['thr-parent']] ++;
998                         }
999
1000                         if (public_contact() == $item['author-id']) {
1001                                 $conv_responses[$mode][$item['thr-parent'] . '-self'] = 1;
1002                         }
1003
1004                         $conv_responses[$mode][$item['thr-parent'] . '-l'][] = $url;
1005
1006                         // there can only be one activity verb per item so if we found anything, we can stop looking
1007                         return;
1008                 }
1009         }
1010 }
1011
1012 /**
1013  * Format the vote text for a profile item
1014  *
1015  * @param int    $cnt  = number of people who vote the item
1016  * @param array  $arr  = array of pre-linked names of likers/dislikers
1017  * @param string $type = one of 'like, 'dislike', 'attendyes', 'attendno', 'attendmaybe'
1018  * @param int    $id   = item id
1019  * @return string formatted text
1020  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1021  */
1022 function format_like($cnt, array $arr, $type, $id) {
1023         $o = '';
1024         $expanded = '';
1025         $phrase = '';
1026
1027         if ($cnt == 1) {
1028                 $likers = $arr[0];
1029
1030                 // Phrase if there is only one liker. In other cases it will be uses for the expanded
1031                 // list which show all likers
1032                 switch ($type) {
1033                         case 'like' :
1034                                 $phrase = L10n::t('%s likes this.', $likers);
1035                                 break;
1036                         case 'dislike' :
1037                                 $phrase = L10n::t('%s doesn\'t like this.', $likers);
1038                                 break;
1039                         case 'attendyes' :
1040                                 $phrase = L10n::t('%s attends.', $likers);
1041                                 break;
1042                         case 'attendno' :
1043                                 $phrase = L10n::t('%s doesn\'t attend.', $likers);
1044                                 break;
1045                         case 'attendmaybe' :
1046                                 $phrase = L10n::t('%s attends maybe.', $likers);
1047                                 break;
1048                 }
1049         }
1050
1051         if ($cnt > 1) {
1052                 $total = count($arr);
1053                 if ($total < MAX_LIKERS) {
1054                         $last = L10n::t('and') . ' ' . $arr[count($arr)-1];
1055                         $arr2 = array_slice($arr, 0, -1);
1056                         $likers = implode(', ', $arr2) . ' ' . $last;
1057                 } else  {
1058                         $arr = array_slice($arr, 0, MAX_LIKERS - 1);
1059                         $likers = implode(', ', $arr);
1060                         $likers .= L10n::t('and %d other people', $total - MAX_LIKERS);
1061                 }
1062
1063                 $spanatts = "class=\"fakelink\" onclick=\"openClose('{$type}list-$id');\"";
1064
1065                 $explikers = '';
1066                 switch ($type) {
1067                         case 'like':
1068                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> like this', $spanatts, $cnt);
1069                                 $explikers = L10n::t('%s like this.', $likers);
1070                                 break;
1071                         case 'dislike':
1072                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> don\'t like this', $spanatts, $cnt);
1073                                 $explikers = L10n::t('%s don\'t like this.', $likers);
1074                                 break;
1075                         case 'attendyes':
1076                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> attend', $spanatts, $cnt);
1077                                 $explikers = L10n::t('%s attend.', $likers);
1078                                 break;
1079                         case 'attendno':
1080                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> don\'t attend', $spanatts, $cnt);
1081                                 $explikers = L10n::t('%s don\'t attend.', $likers);
1082                                 break;
1083                         case 'attendmaybe':
1084                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> attend maybe', $spanatts, $cnt);
1085                                 $explikers = L10n::t('%s attend maybe.', $likers);
1086                                 break;
1087                 }
1088
1089                 $expanded .= "\t" . '<p class="wall-item-' . $type . '-expanded" id="' . $type . 'list-' . $id . '" style="display: none;" >' . $explikers . EOL . '</p>';
1090         }
1091
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
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, array $item, Post $ob = null) {
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         $return = '';
1501         switch ($v) {
1502                 case 'like':
1503                         $return = L10n::tt('Like', 'Likes', $count);
1504                         break;
1505                 case 'dislike':
1506                         $return = L10n::tt('Dislike', 'Dislikes', $count);
1507                         break;
1508                 case 'attendyes':
1509                         $return = L10n::tt('Attending', 'Attending', $count);
1510                         break;
1511                 case 'attendno':
1512                         $return = L10n::tt('Not Attending', 'Not Attending', $count);
1513                         break;
1514                 case 'attendmaybe':
1515                         $return = L10n::tt('Undecided', 'Undecided', $count);
1516                         break;
1517         }
1518
1519         return $return;
1520 }