]> git.mxchange.org Git - friendica.git/blob - include/conversation.php
Move Config::get() to DI::config()->get()
[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\Logger;
14 use Friendica\Core\Protocol;
15 use Friendica\Core\Renderer;
16 use Friendica\Core\Session;
17 use Friendica\Database\DBA;
18 use Friendica\DI;
19 use Friendica\Model\Contact;
20 use Friendica\Model\Item;
21 use Friendica\Model\Profile;
22 use Friendica\Model\Term;
23 use Friendica\Object\Post;
24 use Friendica\Object\Thread;
25 use Friendica\Protocol\Activity;
26 use Friendica\Util\Crypto;
27 use Friendica\Util\DateTimeFormat;
28 use Friendica\Util\Proxy as ProxyUtils;
29 use Friendica\Util\Strings;
30 use Friendica\Util\Temporal;
31 use Friendica\Util\XML;
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=' . DI::baseUrl() . '/redir/' . $cid
87                                    . '?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         $activity = DI::activity();
141
142         $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
143         if ($activity->match($item['verb'], Activity::LIKE)
144                 || $activity->match($item['verb'], Activity::DISLIKE)
145                 || $activity->match($item['verb'], Activity::ATTEND)
146                 || $activity->match($item['verb'], Activity::ATTENDNO)
147                 || $activity->match($item['verb'], Activity::ATTENDMAYBE)) {
148
149                 $fields = ['author-link', 'author-name', 'verb', 'object-type', 'resource-id', 'body', 'plink'];
150                 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
151                 if (!DBA::isResult($obj)) {
152                         return;
153                 }
154
155                 $author  = '[url=' . $item['author-link'] . ']' . $item['author-name'] . '[/url]';
156                 $objauthor =  '[url=' . $obj['author-link'] . ']' . $obj['author-name'] . '[/url]';
157
158                 switch ($obj['verb']) {
159                         case Activity::POST:
160                                 switch ($obj['object-type']) {
161                                         case Activity\ObjectType::EVENT:
162                                                 $post_type = DI::l10n()->t('event');
163                                                 break;
164                                         default:
165                                                 $post_type = DI::l10n()->t('status');
166                                 }
167                                 break;
168                         default:
169                                 if ($obj['resource-id']) {
170                                         $post_type = DI::l10n()->t('photo');
171                                         $m = [];
172                                         preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
173                                         $rr['plink'] = $m[1];
174                                 } else {
175                                         $post_type = DI::l10n()->t('status');
176                                 }
177                 }
178
179                 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
180
181                 $bodyverb = '';
182                 if ($activity->match($item['verb'], Activity::LIKE)) {
183                         $bodyverb = DI::l10n()->t('%1$s likes %2$s\'s %3$s');
184                 } elseif ($activity->match($item['verb'], Activity::DISLIKE)) {
185                         $bodyverb = DI::l10n()->t('%1$s doesn\'t like %2$s\'s %3$s');
186                 } elseif ($activity->match($item['verb'], Activity::ATTEND)) {
187                         $bodyverb = DI::l10n()->t('%1$s attends %2$s\'s %3$s');
188                 } elseif ($activity->match($item['verb'], Activity::ATTENDNO)) {
189                         $bodyverb = DI::l10n()->t('%1$s doesn\'t attend %2$s\'s %3$s');
190                 } elseif ($activity->match($item['verb'], Activity::ATTENDMAYBE)) {
191                         $bodyverb = DI::l10n()->t('%1$s attends maybe %2$s\'s %3$s');
192                 }
193
194                 $item['body'] = sprintf($bodyverb, $author, $objauthor, $plink);
195         }
196
197         if ($activity->match($item['verb'], Activity::FRIEND)) {
198
199                 if ($item['object-type']=="" || $item['object-type']!== Activity\ObjectType::PERSON) return;
200
201                 $Aname = $item['author-name'];
202                 $Alink = $item['author-link'];
203
204                 $xmlhead="<"."?xml version='1.0' encoding='UTF-8' ?".">";
205
206                 $obj = XML::parseString($xmlhead.$item['object']);
207                 $links = XML::parseString($xmlhead."<links>".XML::unescape($obj->link)."</links>");
208
209                 $Bname = $obj->title;
210                 $Blink = "";
211                 $Bphoto = "";
212                 foreach ($links->link as $l) {
213                         $atts = $l->attributes();
214                         switch ($atts['rel']) {
215                                 case "alternate": $Blink = $atts['href']; break;
216                                 case "photo": $Bphoto = $atts['href']; break;
217                         }
218                 }
219
220                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
221                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
222                 if ($Bphoto != "") {
223                         $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img]' . $Bphoto . '[/img][/url]';
224                 }
225
226                 $item['body'] = DI::l10n()->t('%1$s is now friends with %2$s', $A, $B)."\n\n\n".$Bphoto;
227
228         }
229         if (stristr($item['verb'], Activity::POKE)) {
230                 $verb = urldecode(substr($item['verb'],strpos($item['verb'],'#')+1));
231                 if (!$verb) {
232                         return;
233                 }
234                 if ($item['object-type']=="" || $item['object-type']!== Activity\ObjectType::PERSON) {
235                         return;
236                 }
237
238                 $Aname = $item['author-name'];
239                 $Alink = $item['author-link'];
240
241                 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
242
243                 $obj = XML::parseString($xmlhead.$item['object']);
244
245                 $Bname = $obj->title;
246                 $Blink = $obj->id;
247                 $Bphoto = "";
248
249                 foreach ($obj->link as $l) {
250                         $atts = $l->attributes();
251                         switch ($atts['rel']) {
252                                 case "alternate": $Blink = $atts['href'];
253                                 case "photo": $Bphoto = $atts['href'];
254                         }
255                 }
256
257                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
258                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
259                 if ($Bphoto != "") {
260                         $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img=80x80]' . $Bphoto . '[/img][/url]';
261                 }
262
263                 /*
264                  * we can't have a translation string with three positions but no distinguishable text
265                  * So here is the translate string.
266                  */
267                 $txt = DI::l10n()->t('%1$s poked %2$s');
268
269                 // now translate the verb
270                 $poked_t = trim(sprintf($txt, "", ""));
271                 $txt = str_replace($poked_t, DI::l10n()->t($verb), $txt);
272
273                 // then do the sprintf on the translation string
274
275                 $item['body'] = sprintf($txt, $A, $B). "\n\n\n" . $Bphoto;
276
277         }
278
279         if ($activity->match($item['verb'],  Activity::TAG)) {
280                 $fields = ['author-id', 'author-link', 'author-name', 'author-network',
281                         'verb', 'object-type', 'resource-id', 'body', 'plink'];
282                 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
283                 if (!DBA::isResult($obj)) {
284                         return;
285                 }
286
287                 $author_arr = ['uid' => 0, 'id' => $item['author-id'],
288                         'network' => $item['author-network'], 'url' => $item['author-link']];
289                 $author  = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $item['author-name'] . '[/url]';
290
291                 $author_arr = ['uid' => 0, 'id' => $obj['author-id'],
292                         'network' => $obj['author-network'], 'url' => $obj['author-link']];
293                 $objauthor  = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $obj['author-name'] . '[/url]';
294
295                 switch ($obj['verb']) {
296                         case Activity::POST:
297                                 switch ($obj['object-type']) {
298                                         case Activity\ObjectType::EVENT:
299                                                 $post_type = DI::l10n()->t('event');
300                                                 break;
301                                         default:
302                                                 $post_type = DI::l10n()->t('status');
303                                 }
304                                 break;
305                         default:
306                                 if ($obj['resource-id']) {
307                                         $post_type = DI::l10n()->t('photo');
308                                         $m=[]; preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
309                                         $rr['plink'] = $m[1];
310                                 } else {
311                                         $post_type = DI::l10n()->t('status');
312                                 }
313                                 // Let's break everthing ... ;-)
314                                 break;
315                 }
316                 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
317
318                 $parsedobj = XML::parseString($xmlhead.$item['object']);
319
320                 $tag = sprintf('#[url=%s]%s[/url]', $parsedobj->id, $parsedobj->content);
321                 $item['body'] = DI::l10n()->t('%1$s tagged %2$s\'s %3$s with %4$s', $author, $objauthor, $plink, $tag);
322         }
323
324         if ($activity->match($item['verb'], Activity::FAVORITE)) {
325                 if ($item['object-type'] == "") {
326                         return;
327                 }
328
329                 $Aname = $item['author-name'];
330                 $Alink = $item['author-link'];
331
332                 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
333
334                 $obj = XML::parseString($xmlhead.$item['object']);
335                 if (strlen($obj->id)) {
336                         $fields = ['author-link', 'author-name', 'plink'];
337                         $target = Item::selectFirst($fields, ['uri' => $obj->id, 'uid' => $item['uid']]);
338                         if (DBA::isResult($target) && $target['plink']) {
339                                 $Bname = $target['author-name'];
340                                 $Blink = $target['author-link'];
341                                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
342                                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
343                                 $P = '[url=' . $target['plink'] . ']' . DI::l10n()->t('post/item') . '[/url]';
344                                 $item['body'] = DI::l10n()->t('%1$s marked %2$s\'s %3$s as favorite', $A, $B, $P)."\n";
345                         }
346                 }
347         }
348         $matches = null;
349         if (preg_match_all('/@\[url=(.*?)\]/is', $item['body'], $matches, PREG_SET_ORDER)) {
350                 foreach ($matches as $mtch) {
351                         if (!strpos($mtch[1], 'zrl=')) {
352                                 $item['body'] = str_replace($mtch[0], '@[url=' . Contact::magicLink($mtch[1]) . ']', $item['body']);
353                         }
354                 }
355         }
356
357         // add zrl's to public images
358         $photo_pattern = "/\[url=(.*?)\/photos\/(.*?)\/image\/(.*?)\]\[img(.*?)\]h(.*?)\[\/img\]\[\/url\]/is";
359         if (preg_match($photo_pattern, $item['body'])) {
360                 $photo_replace = '[url=' . Profile::zrl('$1' . '/photos/' . '$2' . '/image/' . '$3' ,true) . '][img' . '$4' . ']h' . '$5'  . '[/img][/url]';
361                 $item['body'] = BBCode::pregReplaceInTag($photo_pattern, $photo_replace, 'url', $item['body']);
362         }
363
364         // add sparkle links to appropriate permalinks
365         $author = ['uid' => 0, 'id' => $item['author-id'],
366                 'network' => $item['author-network'], 'url' => $item['author-link']];
367
368         // Only create a redirection to a magic link when logged in
369         if (!empty($item['plink']) && Session::isAuthenticated()) {
370                 $item['plink'] = Contact::magicLinkByContact($author, $item['plink']);
371         }
372 }
373
374 /**
375  * Count the total of comments on this item and its desendants
376  * @TODO proper type-hint + doc-tag
377  * @param $item
378  * @return int
379  */
380 function count_descendants($item) {
381         $total = count($item['children']);
382
383         if ($total > 0) {
384                 foreach ($item['children'] as $child) {
385                         if (!visible_activity($child)) {
386                                 $total --;
387                         }
388                         $total += count_descendants($child);
389                 }
390         }
391
392         return $total;
393 }
394
395 function visible_activity($item) {
396
397         $activity = DI::activity();
398
399         if (empty($item['verb']) || $activity->isHidden($item['verb'])) {
400                 return false;
401         }
402
403         // @TODO below if() block can be rewritten to a single line: $isVisible = allConditionsHere;
404         if ($activity->match($item['verb'], Activity::FOLLOW) &&
405             $item['object-type'] === Activity\ObjectType::NOTE &&
406             empty($item['self']) &&
407             $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 = DI::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(DI::args()->getCommand(), 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 = " . Session::get('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(DI::args()->getCommand(), 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(DI::args()->getCommand(), 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'] = DI::args()->getQueryString();
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' => DI::l10n()->t('Likes','title')],
569                 'dislike' => ['title' => DI::l10n()->t('Dislikes','title')],
570                 'attendyes' => ['title' => DI::l10n()->t('Attending','title')],
571                 'attendno' => ['title' => DI::l10n()->t('Not attending','title')],
572                 'attendmaybe' => ['title' => DI::l10n()->t('Might attend','title')],
573                 'announce' => ['title' => DI::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::FEDERATED);
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' => DI::l10n()->t('Select'),
654                                         'delete' => DI::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) = DI::contentItem()->determineCategoriesTerms($item);
666
667                                 if (!empty($item['content-warning']) && DI::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['author-network'], $item['author-link'], $item['network']),
679                                         'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link']),
680                                         'linktitle' => DI::l10n()->t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
681                                         'profile_url' => $profile_link,
682                                         'item_photo_menu' => item_photo_menu($item),
683                                         'name' => $profile_name,
684                                         'sparkle' => $sparkle,
685                                         'lock' => $lock,
686                                         'thumb' => DI::baseUrl()->remove(ProxyUtils::proxifyUrl($item['author-avatar'], false, ProxyUtils::SIZE_THUMB)),
687                                         'title' => $title,
688                                         'body' => $body,
689                                         'tags' => $tags['tags'],
690                                         'hashtags' => $tags['hashtags'],
691                                         'mentions' => $tags['mentions'],
692                                         'implicit_mentions' => $tags['implicit_mentions'],
693                                         'txt_cats' => DI::l10n()->t('Categories:'),
694                                         'txt_folders' => DI::l10n()->t('Filed under:'),
695                                         'has_cats' => ((count($categories)) ? 'true' : ''),
696                                         'has_folders' => ((count($folders)) ? 'true' : ''),
697                                         'categories' => $categories,
698                                         'folders' => $folders,
699                                         'text' => strip_tags($body),
700                                         'localtime' => DateTimeFormat::local($item['created'], 'r'),
701                                         'ago' => (($item['app']) ? DI::l10n()->t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
702                                         'location' => $location,
703                                         'indent' => '',
704                                         'owner_name' => $owner_name,
705                                         'owner_url' => $owner_url,
706                                         'owner_photo' => DI::baseUrl()->remove(ProxyUtils::proxifyUrl($item['owner-avatar'], false, ProxyUtils::SIZE_THUMB)),
707                                         'plink' => Item::getPlink($item),
708                                         'edpost' => false,
709                                         'isstarred' => $isstarred,
710                                         'star' => $star,
711                                         'drop' => $drop,
712                                         'vote' => $likebuttons,
713                                         'like' => '',
714                                         'dislike' => '',
715                                         'comment' => '',
716                                         'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> DI::l10n()->t('View in context')]),
717                                         'previewing' => $previewing,
718                                         'wait' => DI::l10n()->t('Please wait'),
719                                         'thread_level' => 1,
720                                 ];
721
722                                 $arr = ['item' => $item, 'output' => $tmp_item];
723                                 Hook::callAll('display_item', $arr);
724
725                                 $threads[$threadsid]['id'] = $item['id'];
726                                 $threads[$threadsid]['network'] = $item['network'];
727                                 $threads[$threadsid]['items'] = [$arr['output']];
728
729                         }
730                 } else {
731                         // Normal View
732                         $page_template = Renderer::getMarkupTemplate("threaded_conversation.tpl");
733
734                         $conv = new Thread($mode, $preview, $writable);
735
736                         /*
737                          * get all the topmost parents
738                          * this shouldn't be needed, as we should have only them in our array
739                          * But for now, this array respects the old style, just in case
740                          */
741                         foreach ($items as $item) {
742                                 if (in_array($item['author-id'], $blocklist)) {
743                                         continue;
744                                 }
745
746                                 // Can we put this after the visibility check?
747                                 builtin_activity_puller($item, $conv_responses);
748
749                                 // Only add what is visible
750                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
751                                         continue;
752                                 }
753
754                                 if (!visible_activity($item)) {
755                                         continue;
756                                 }
757
758                                 /// @todo Check if this call is needed or not
759                                 $arr = ['item' => $item];
760                                 Hook::callAll('display_item', $arr);
761
762                                 $item['pagedrop'] = $page_dropping;
763
764                                 if ($item['id'] == $item['parent']) {
765                                         $item_object = new Post($item);
766                                         $conv->addParent($item_object);
767                                 }
768                         }
769
770                         $threads = $conv->getTemplateData($conv_responses);
771                         if (!$threads) {
772                                 Logger::log('[ERROR] conversation : Failed to get template data.', Logger::DEBUG);
773                                 $threads = [];
774                         }
775                 }
776         }
777
778         $o = Renderer::replaceMacros($page_template, [
779                 '$baseurl' => DI::baseUrl()->get($ssl_state),
780                 '$return_path' => DI::args()->getQueryString(),
781                 '$live_update' => $live_update_div,
782                 '$remove' => DI::l10n()->t('remove'),
783                 '$mode' => $mode,
784                 '$user' => $a->user,
785                 '$threads' => $threads,
786                 '$dropping' => ($page_dropping ? DI::l10n()->t('Delete Selected Items') : False),
787         ]);
788
789         return $o;
790 }
791
792 /**
793  * Fetch all comments from a query. Additionally set the newest resharer as thread owner.
794  *
795  * @param array   $thread_items Database statement with thread posts
796  * @param boolean $pinned       Is the item pinned?
797  *
798  * @return array items with parents and comments
799  */
800 function conversation_fetch_comments($thread_items, $pinned) {
801         $comments = [];
802         $parentlines = [];
803         $lineno = 0;
804         $actor = [];
805         $received = '';
806
807         while ($row = Item::fetch($thread_items)) {
808                 if (($row['verb'] == Activity::ANNOUNCE) && !empty($row['contact-uid']) && ($row['received'] > $received) && ($row['thr-parent'] == $row['parent-uri'])) {
809                         $actor = ['link' => $row['author-link'], 'avatar' => $row['author-avatar'], 'name' => $row['author-name']];
810                         $received = $row['received'];
811                 }
812
813                 if ((($row['gravity'] == GRAVITY_PARENT) && !$row['origin'] && !in_array($row['network'], [Protocol::DIASPORA])) &&
814                         (empty($row['contact-uid']) || !in_array($row['network'], Protocol::NATIVE_SUPPORT))) {
815                         $parentlines[] = $lineno;
816                 }
817
818                 if ($row['gravity'] == GRAVITY_PARENT) {
819                         $row['pinned'] = $pinned;
820                 }
821
822                 $comments[] = $row;
823                 $lineno++;
824         }
825
826         DBA::close($thread_items);
827
828         if (!empty($actor)) {
829                 foreach ($parentlines as $line) {
830                         $comments[$line]['owner-link'] = $actor['link'];
831                         $comments[$line]['owner-avatar'] = $actor['avatar'];
832                         $comments[$line]['owner-name'] = $actor['name'];
833                 }
834         }
835         return $comments;
836 }
837
838 /**
839  * Add comments to top level entries that had been fetched before
840  *
841  * The system will fetch the comments for the local user whenever possible.
842  * This behaviour is currently needed to allow commenting on Friendica posts.
843  *
844  * @param array $parents Parent items
845  *
846  * @param       $block_authors
847  * @param       $order
848  * @param       $uid
849  * @return array items with parents and comments
850  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
851  */
852 function conversation_add_children(array $parents, $block_authors, $order, $uid) {
853         $max_comments = DI::config()->get('system', 'max_comments', 100);
854
855         $params = ['order' => ['uid', 'commented' => true]];
856
857         if ($max_comments > 0) {
858                 $params['limit'] = $max_comments;
859         }
860
861         $items = [];
862
863         foreach ($parents AS $parent) {
864                 $condition = ["`item`.`parent-uri` = ? AND `item`.`uid` IN (0, ?) ",
865                         $parent['uri'], $uid];
866                 if ($block_authors) {
867                         $condition[0] .= "AND NOT `author`.`hidden`";
868                 }
869
870                 $thread_items = Item::selectForUser(local_user(), array_merge(Item::DISPLAY_FIELDLIST, ['contact-uid', 'gravity']), $condition, $params);
871
872                 $comments = conversation_fetch_comments($thread_items, $parent['pinned'] ?? false);
873
874                 if (count($comments) != 0) {
875                         $items = array_merge($items, $comments);
876                 }
877         }
878
879         foreach ($items as $index => $item) {
880                 if ($item['uid'] == 0) {
881                         $items[$index]['writable'] = in_array($item['network'], Protocol::FEDERATED);
882                 }
883         }
884
885         $items = conv_sort($items, $order);
886
887         return $items;
888 }
889
890 function item_photo_menu($item) {
891         $sub_link = '';
892         $poke_link = '';
893         $contact_url = '';
894         $pm_url = '';
895         $status_link = '';
896         $photos_link = '';
897         $posts_link = '';
898         $block_link = '';
899         $ignore_link = '';
900
901         if (local_user() && local_user() == $item['uid'] && $item['parent'] == $item['id'] && !$item['self']) {
902                 $sub_link = 'javascript:dosubthread(' . $item['id'] . '); return false;';
903         }
904
905         $author = ['uid' => 0, 'id' => $item['author-id'],
906                 'network' => $item['author-network'], 'url' => $item['author-link']];
907         $profile_link = Contact::magicLinkByContact($author, $item['author-link']);
908         $sparkle = (strpos($profile_link, 'redir/') === 0);
909
910         $cid = 0;
911         $pcid = Contact::getIdForURL($item['author-link'], 0, true);
912         $network = '';
913         $rel = 0;
914         $condition = ['uid' => local_user(), 'nurl' => Strings::normaliseLink($item['author-link'])];
915         $contact = DBA::selectFirst('contact', ['id', 'network', 'rel'], $condition);
916         if (DBA::isResult($contact)) {
917                 $cid = $contact['id'];
918                 $network = $contact['network'];
919                 $rel = $contact['rel'];
920         }
921
922         if ($sparkle) {
923                 $status_link = $profile_link . '?tab=status';
924                 $photos_link = str_replace('/profile/', '/photos/', $profile_link);
925                 $profile_link = $profile_link . '?=profile';
926         }
927
928         if (!empty($pcid)) {
929                 $contact_url = 'contact/' . $pcid;
930                 $posts_link = 'contact/' . $pcid . '/posts';
931                 $block_link = 'contact/' . $pcid . '/block';
932                 $ignore_link = 'contact/' . $pcid . '/ignore';
933         }
934
935         if ($cid && !$item['self']) {
936                 $poke_link = 'poke?c=' . $cid;
937                 $contact_url = 'contact/' . $cid;
938                 $posts_link = 'contact/' . $cid . '/posts';
939
940                 if (in_array($network, [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA])) {
941                         $pm_url = 'message/new/' . $cid;
942                 }
943         }
944
945         if (local_user()) {
946                 $menu = [
947                         DI::l10n()->t('Follow Thread') => $sub_link,
948                         DI::l10n()->t('View Status') => $status_link,
949                         DI::l10n()->t('View Profile') => $profile_link,
950                         DI::l10n()->t('View Photos') => $photos_link,
951                         DI::l10n()->t('Network Posts') => $posts_link,
952                         DI::l10n()->t('View Contact') => $contact_url,
953                         DI::l10n()->t('Send PM') => $pm_url,
954                         DI::l10n()->t('Block') => $block_link,
955                         DI::l10n()->t('Ignore') => $ignore_link
956                 ];
957
958                 if ($network == Protocol::DFRN) {
959                         $menu[DI::l10n()->t("Poke")] = $poke_link;
960                 }
961
962                 if ((($cid == 0) || ($rel == Contact::FOLLOWER)) &&
963                         in_array($item['network'], Protocol::FEDERATED)) {
964                         $menu[DI::l10n()->t('Connect/Follow')] = 'follow?url=' . urlencode($item['author-link']);
965                 }
966         } else {
967                 $menu = [DI::l10n()->t('View Profile') => $item['author-link']];
968         }
969
970         $args = ['item' => $item, 'menu' => $menu];
971
972         Hook::callAll('item_photo_menu', $args);
973
974         $menu = $args['menu'];
975
976         $o = '';
977         foreach ($menu as $k => $v) {
978                 if (strpos($v, 'javascript:') === 0) {
979                         $v = substr($v, 11);
980                         $o .= '<li role="menuitem"><a onclick="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
981                 } elseif ($v!='') {
982                         $o .= '<li role="menuitem"><a href="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
983                 }
984         }
985         return $o;
986 }
987
988 /**
989  * Checks item to see if it is one of the builtin activities (like/dislike, event attendance, consensus items, etc.)
990  *
991  * Increments the count of each matching activity and adds a link to the author as needed.
992  *
993  * @param array  $item
994  * @param array &$conv_responses (already created with builtin activity structure)
995  * @return void
996  * @throws ImagickException
997  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
998  */
999 function builtin_activity_puller($item, &$conv_responses) {
1000         foreach ($conv_responses as $mode => $v) {
1001                 $sparkle = '';
1002
1003                 switch ($mode) {
1004                         case 'like':
1005                                 $verb = Activity::LIKE;
1006                                 break;
1007                         case 'dislike':
1008                                 $verb = Activity::DISLIKE;
1009                                 break;
1010                         case 'attendyes':
1011                                 $verb = Activity::ATTEND;
1012                                 break;
1013                         case 'attendno':
1014                                 $verb = Activity::ATTENDNO;
1015                                 break;
1016                         case 'attendmaybe':
1017                                 $verb = Activity::ATTENDMAYBE;
1018                                 break;
1019                         case 'announce':
1020                                 $verb = Activity::ANNOUNCE;
1021                                 break;
1022                         default:
1023                                 return;
1024                 }
1025
1026                 if (!empty($item['verb']) && DI::activity()->match($item['verb'], $verb) && ($item['id'] != $item['parent'])) {
1027                         $author = ['uid' => 0, 'id' => $item['author-id'],
1028                                 'network' => $item['author-network'], 'url' => $item['author-link']];
1029                         $url = Contact::magicLinkByContact($author);
1030                         if (strpos($url, 'redir/') === 0) {
1031                                 $sparkle = ' class="sparkle" ';
1032                         }
1033
1034                         $url = '<a href="'. $url . '"'. $sparkle .'>' . htmlentities($item['author-name']) . '</a>';
1035
1036                         if (empty($item['thr-parent'])) {
1037                                 $item['thr-parent'] = $item['parent-uri'];
1038                         }
1039
1040                         if (!(isset($conv_responses[$mode][$item['thr-parent'] . '-l'])
1041                                 && is_array($conv_responses[$mode][$item['thr-parent'] . '-l']))) {
1042                                 $conv_responses[$mode][$item['thr-parent'] . '-l'] = [];
1043                         }
1044
1045                         // only list each unique author once
1046                         if (in_array($url,$conv_responses[$mode][$item['thr-parent'] . '-l'])) {
1047                                 continue;
1048                         }
1049
1050                         if (!isset($conv_responses[$mode][$item['thr-parent']])) {
1051                                 $conv_responses[$mode][$item['thr-parent']] = 1;
1052                         } else {
1053                                 $conv_responses[$mode][$item['thr-parent']] ++;
1054                         }
1055
1056                         if (public_contact() == $item['author-id']) {
1057                                 $conv_responses[$mode][$item['thr-parent'] . '-self'] = 1;
1058                         }
1059
1060                         $conv_responses[$mode][$item['thr-parent'] . '-l'][] = $url;
1061
1062                         // there can only be one activity verb per item so if we found anything, we can stop looking
1063                         return;
1064                 }
1065         }
1066 }
1067
1068 /**
1069  * Format the vote text for a profile item
1070  *
1071  * @param int    $cnt  = number of people who vote the item
1072  * @param array  $arr  = array of pre-linked names of likers/dislikers
1073  * @param string $type = one of 'like, 'dislike', 'attendyes', 'attendno', 'attendmaybe'
1074  * @param int    $id   = item id
1075  * @return string formatted text
1076  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1077  */
1078 function format_like($cnt, array $arr, $type, $id) {
1079         $o = '';
1080         $expanded = '';
1081         $phrase = '';
1082
1083         if ($cnt == 1) {
1084                 $likers = $arr[0];
1085
1086                 // Phrase if there is only one liker. In other cases it will be uses for the expanded
1087                 // list which show all likers
1088                 switch ($type) {
1089                         case 'like' :
1090                                 $phrase = DI::l10n()->t('%s likes this.', $likers);
1091                                 break;
1092                         case 'dislike' :
1093                                 $phrase = DI::l10n()->t('%s doesn\'t like this.', $likers);
1094                                 break;
1095                         case 'attendyes' :
1096                                 $phrase = DI::l10n()->t('%s attends.', $likers);
1097                                 break;
1098                         case 'attendno' :
1099                                 $phrase = DI::l10n()->t('%s doesn\'t attend.', $likers);
1100                                 break;
1101                         case 'attendmaybe' :
1102                                 $phrase = DI::l10n()->t('%s attends maybe.', $likers);
1103                                 break;
1104                         case 'announce' :
1105                                 $phrase = DI::l10n()->t('%s reshared this.', $likers);
1106                                 break;
1107                 }
1108         }
1109
1110         if ($cnt > 1) {
1111                 $total = count($arr);
1112                 if ($total < MAX_LIKERS) {
1113                         $last = DI::l10n()->t('and') . ' ' . $arr[count($arr)-1];
1114                         $arr2 = array_slice($arr, 0, -1);
1115                         $likers = implode(', ', $arr2) . ' ' . $last;
1116                 } else  {
1117                         $arr = array_slice($arr, 0, MAX_LIKERS - 1);
1118                         $likers = implode(', ', $arr);
1119                         $likers .= DI::l10n()->t('and %d other people', $total - MAX_LIKERS);
1120                 }
1121
1122                 $spanatts = "class=\"fakelink\" onclick=\"openClose('{$type}list-$id');\"";
1123
1124                 $explikers = '';
1125                 switch ($type) {
1126                         case 'like':
1127                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> like this', $spanatts, $cnt);
1128                                 $explikers = DI::l10n()->t('%s like this.', $likers);
1129                                 break;
1130                         case 'dislike':
1131                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> don\'t like this', $spanatts, $cnt);
1132                                 $explikers = DI::l10n()->t('%s don\'t like this.', $likers);
1133                                 break;
1134                         case 'attendyes':
1135                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> attend', $spanatts, $cnt);
1136                                 $explikers = DI::l10n()->t('%s attend.', $likers);
1137                                 break;
1138                         case 'attendno':
1139                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> don\'t attend', $spanatts, $cnt);
1140                                 $explikers = DI::l10n()->t('%s don\'t attend.', $likers);
1141                                 break;
1142                         case 'attendmaybe':
1143                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> attend maybe', $spanatts, $cnt);
1144                                 $explikers = DI::l10n()->t('%s attend maybe.', $likers);
1145                                 break;
1146                         case 'announce':
1147                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> reshared this', $spanatts, $cnt);
1148                                 $explikers = DI::l10n()->t('%s reshared this.', $likers);
1149                                 break;
1150                 }
1151
1152                 $expanded .= "\t" . '<p class="wall-item-' . $type . '-expanded" id="' . $type . 'list-' . $id . '" style="display: none;" >' . $explikers . EOL . '</p>';
1153         }
1154
1155         $o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('voting_fakelink.tpl'), [
1156                 '$phrase' => $phrase,
1157                 '$type' => $type,
1158                 '$id' => $id
1159         ]);
1160         $o .= $expanded;
1161
1162         return $o;
1163 }
1164
1165 function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
1166 {
1167         $o = '';
1168
1169         $geotag = !empty($x['allow_location']) ? Renderer::replaceMacros(Renderer::getMarkupTemplate('jot_geotag.tpl'), []) : '';
1170
1171         $tpl = Renderer::getMarkupTemplate('jot-header.tpl');
1172         DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
1173                 '$newpost'   => 'true',
1174                 '$baseurl'   => DI::baseUrl()->get(true),
1175                 '$geotag'    => $geotag,
1176                 '$nickname'  => $x['nickname'],
1177                 '$ispublic'  => DI::l10n()->t('Visible to <strong>everybody</strong>'),
1178                 '$linkurl'   => DI::l10n()->t('Please enter a image/video/audio/webpage URL:'),
1179                 '$term'      => DI::l10n()->t('Tag term:'),
1180                 '$fileas'    => DI::l10n()->t('Save to Folder:'),
1181                 '$whereareu' => DI::l10n()->t('Where are you right now?'),
1182                 '$delitems'  => DI::l10n()->t("Delete item\x28s\x29?")
1183         ]);
1184
1185         $jotplugins = '';
1186         Hook::callAll('jot_tool', $jotplugins);
1187
1188         // Private/public post links for the non-JS ACL form
1189         $private_post = 1;
1190         if (!empty($_REQUEST['public'])) {
1191                 $private_post = 0;
1192         }
1193
1194         $query_str = DI::args()->getQueryString();
1195         if (strpos($query_str, 'public=1') !== false) {
1196                 $query_str = str_replace(['?public=1', '&public=1'], ['', ''], $query_str);
1197         }
1198
1199         /*
1200          * I think $a->query_string may never have ? in it, but I could be wrong
1201          * It looks like it's from the index.php?q=[etc] rewrite that the web
1202          * server does, which converts any ? to &, e.g. suggest&ignore=61 for suggest?ignore=61
1203          */
1204         if (strpos($query_str, '?') === false) {
1205                 $public_post_link = '?public=1';
1206         } else {
1207                 $public_post_link = '&public=1';
1208         }
1209
1210         // $tpl = Renderer::replaceMacros($tpl,array('$jotplugins' => $jotplugins));
1211         $tpl = Renderer::getMarkupTemplate("jot.tpl");
1212
1213         $o .= Renderer::replaceMacros($tpl,[
1214                 '$new_post' => DI::l10n()->t('New Post'),
1215                 '$return_path'  => $query_str,
1216                 '$action'       => 'item',
1217                 '$share'        => ($x['button'] ?? '') ?: DI::l10n()->t('Share'),
1218                 '$upload'       => DI::l10n()->t('Upload photo'),
1219                 '$shortupload'  => DI::l10n()->t('upload photo'),
1220                 '$attach'       => DI::l10n()->t('Attach file'),
1221                 '$shortattach'  => DI::l10n()->t('attach file'),
1222                 '$edbold'       => DI::l10n()->t('Bold'),
1223                 '$editalic'     => DI::l10n()->t('Italic'),
1224                 '$eduline'      => DI::l10n()->t('Underline'),
1225                 '$edquote'      => DI::l10n()->t('Quote'),
1226                 '$edcode'       => DI::l10n()->t('Code'),
1227                 '$edimg'        => DI::l10n()->t('Image'),
1228                 '$edurl'        => DI::l10n()->t('Link'),
1229                 '$edattach'     => DI::l10n()->t('Link or Media'),
1230                 '$setloc'       => DI::l10n()->t('Set your location'),
1231                 '$shortsetloc'  => DI::l10n()->t('set location'),
1232                 '$noloc'        => DI::l10n()->t('Clear browser location'),
1233                 '$shortnoloc'   => DI::l10n()->t('clear location'),
1234                 '$title'        => $x['title'] ?? '',
1235                 '$placeholdertitle' => DI::l10n()->t('Set title'),
1236                 '$category'     => $x['category'] ?? '',
1237                 '$placeholdercategory' => Feature::isEnabled(local_user(), 'categories') ? DI::l10n()->t("Categories \x28comma-separated list\x29") : '',
1238                 '$wait'         => DI::l10n()->t('Please wait'),
1239                 '$permset'      => DI::l10n()->t('Permission settings'),
1240                 '$shortpermset' => DI::l10n()->t('permissions'),
1241                 '$wall'         => $notes_cid ? 0 : 1,
1242                 '$posttype'     => $notes_cid ? Item::PT_PERSONAL_NOTE : Item::PT_ARTICLE,
1243                 '$content'      => $x['content'] ?? '',
1244                 '$post_id'      => $x['post_id'] ?? '',
1245                 '$baseurl'      => DI::baseUrl()->get(true),
1246                 '$defloc'       => $x['default_location'],
1247                 '$visitor'      => $x['visitor'],
1248                 '$pvisit'       => $notes_cid ? 'none' : $x['visitor'],
1249                 '$public'       => DI::l10n()->t('Public post'),
1250                 '$lockstate'    => $x['lockstate'],
1251                 '$bang'         => $x['bang'],
1252                 '$profile_uid'  => $x['profile_uid'],
1253                 '$preview'      => DI::l10n()->t('Preview'),
1254                 '$jotplugins'   => $jotplugins,
1255                 '$notes_cid'    => $notes_cid,
1256                 '$sourceapp'    => DI::l10n()->t($a->sourcename),
1257                 '$cancel'       => DI::l10n()->t('Cancel'),
1258                 '$rand_num'     => Crypto::randomDigits(12),
1259
1260                 // ACL permissions box
1261                 '$acl'           => $x['acl'],
1262                 '$group_perms'   => DI::l10n()->t('Post to Groups'),
1263                 '$contact_perms' => DI::l10n()->t('Post to Contacts'),
1264                 '$private'       => DI::l10n()->t('Private post'),
1265                 '$is_private'    => $private_post,
1266                 '$public_link'   => $public_post_link,
1267
1268                 //jot nav tab (used in some themes)
1269                 '$message' => DI::l10n()->t('Message'),
1270                 '$browser' => DI::l10n()->t('Browser'),
1271         ]);
1272
1273
1274         if ($popup == true) {
1275                 $o = '<div id="jot-popup" style="display: none;">' . $o . '</div>';
1276         }
1277
1278         return $o;
1279 }
1280
1281 /**
1282  * Plucks the children of the given parent from a given item list.
1283  *
1284  * @param array $item_list
1285  * @param array $parent
1286  * @param bool  $recursive
1287  * @return array
1288  */
1289 function get_item_children(array &$item_list, array $parent, $recursive = true)
1290 {
1291         $children = [];
1292         foreach ($item_list as $i => $item) {
1293                 if ($item['id'] != $item['parent']) {
1294                         if ($recursive) {
1295                                 // Fallback to parent-uri if thr-parent is not set
1296                                 $thr_parent = $item['thr-parent'];
1297                                 if ($thr_parent == '') {
1298                                         $thr_parent = $item['parent-uri'];
1299                                 }
1300
1301                                 if ($thr_parent == $parent['uri']) {
1302                                         $item['children'] = get_item_children($item_list, $item);
1303                                         $children[] = $item;
1304                                         unset($item_list[$i]);
1305                                 }
1306                         } elseif ($item['parent'] == $parent['id']) {
1307                                 $children[] = $item;
1308                                 unset($item_list[$i]);
1309                         }
1310                 }
1311         }
1312         return $children;
1313 }
1314
1315 /**
1316  * Recursively sorts a tree-like item array
1317  *
1318  * @param array $items
1319  * @return array
1320  */
1321 function sort_item_children(array $items)
1322 {
1323         $result = $items;
1324         usort($result, 'sort_thr_received_rev');
1325         foreach ($result as $k => $i) {
1326                 if (isset($result[$k]['children'])) {
1327                         $result[$k]['children'] = sort_item_children($result[$k]['children']);
1328                 }
1329         }
1330         return $result;
1331 }
1332
1333 /**
1334  * Recursively add all children items at the top level of a list
1335  *
1336  * @param array $children List of items to append
1337  * @param array $item_list
1338  */
1339 function add_children_to_list(array $children, array &$item_list)
1340 {
1341         foreach ($children as $child) {
1342                 $item_list[] = $child;
1343                 if (isset($child['children'])) {
1344                         add_children_to_list($child['children'], $item_list);
1345                 }
1346         }
1347 }
1348
1349 /**
1350  * Selectively flattens a tree-like item structure to prevent threading stairs
1351  *
1352  * This recursive function takes the item tree structure created by conv_sort() and
1353  * flatten the extraneous depth levels when people reply sequentially, removing the
1354  * stairs effect in threaded conversations limiting the available content width.
1355  *
1356  * The basic principle is the following: if a post item has only one reply and is
1357  * the last reply of its parent, then the reply is moved to the parent.
1358  *
1359  * This process is rendered somewhat more complicated because items can be either
1360  * replies or likes, and these don't factor at all in the reply count/last reply.
1361  *
1362  * @param array $parent A tree-like array of items
1363  * @return array
1364  */
1365 function smart_flatten_conversation(array $parent)
1366 {
1367         if (!isset($parent['children']) || count($parent['children']) == 0) {
1368                 return $parent;
1369         }
1370
1371         // We use a for loop to ensure we process the newly-moved items
1372         for ($i = 0; $i < count($parent['children']); $i++) {
1373                 $child = $parent['children'][$i];
1374
1375                 if (isset($child['children']) && count($child['children'])) {
1376                         // This helps counting only the regular posts
1377                         $count_post_closure = function($var) {
1378                                 return $var['verb'] === Activity::POST;
1379                         };
1380
1381                         $child_post_count = count(array_filter($child['children'], $count_post_closure));
1382
1383                         $remaining_post_count = count(array_filter(array_slice($parent['children'], $i), $count_post_closure));
1384
1385                         // If there's only one child's children post and this is the last child post
1386                         if ($child_post_count == 1 && $remaining_post_count == 1) {
1387
1388                                 // Searches the post item in the children
1389                                 $j = 0;
1390                                 while($child['children'][$j]['verb'] !== Activity::POST && $j < count($child['children'])) {
1391                                         $j ++;
1392                                 }
1393
1394                                 $moved_item = $child['children'][$j];
1395                                 unset($parent['children'][$i]['children'][$j]);
1396                                 $parent['children'][] = $moved_item;
1397                         } else {
1398                                 $parent['children'][$i] = smart_flatten_conversation($child);
1399                         }
1400                 }
1401         }
1402
1403         return $parent;
1404 }
1405
1406
1407 /**
1408  * Expands a flat list of items into corresponding tree-like conversation structures.
1409  *
1410  * sort the top-level posts either on "received" or "commented", and finally
1411  * append all the items at the top level (???)
1412  *
1413  * @param array  $item_list A list of items belonging to one or more conversations
1414  * @param string $order     Either on "received" or "commented"
1415  * @return array
1416  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1417  */
1418 function conv_sort(array $item_list, $order)
1419 {
1420         $parents = [];
1421
1422         if (!(is_array($item_list) && count($item_list))) {
1423                 return $parents;
1424         }
1425
1426         $blocklist = conv_get_blocklist();
1427
1428         $item_array = [];
1429
1430         // Dedupes the item list on the uri to prevent infinite loops
1431         foreach ($item_list as $item) {
1432                 if (in_array($item['author-id'], $blocklist)) {
1433                         continue;
1434                 }
1435
1436                 $item_array[$item['uri']] = $item;
1437         }
1438
1439         // Extract the top level items
1440         foreach ($item_array as $item) {
1441                 if ($item['id'] == $item['parent']) {
1442                         $parents[] = $item;
1443                 }
1444         }
1445
1446         if (stristr($order, 'pinned_received')) {
1447                 usort($parents, 'sort_thr_pinned_received');
1448         } elseif (stristr($order, 'received')) {
1449                 usort($parents, 'sort_thr_received');
1450         } elseif (stristr($order, 'commented')) {
1451                 usort($parents, 'sort_thr_commented');
1452         }
1453
1454         /*
1455          * Plucks children from the item_array, second pass collects eventual orphan
1456          * items and add them as children of their top-level post.
1457          */
1458         foreach ($parents as $i => $parent) {
1459                 $parents[$i]['children'] =
1460                         array_merge(get_item_children($item_array, $parent, true),
1461                                 get_item_children($item_array, $parent, false));
1462         }
1463
1464         foreach ($parents as $i => $parent) {
1465                 $parents[$i]['children'] = sort_item_children($parents[$i]['children']);
1466         }
1467
1468         if (!DI::pConfig()->get(local_user(), 'system', 'no_smart_threading', 0)) {
1469                 foreach ($parents as $i => $parent) {
1470                         $parents[$i] = smart_flatten_conversation($parent);
1471                 }
1472         }
1473
1474         /// @TODO: Stop recusrsively adding all children back to the top level (!!!)
1475         /// However, this apparently ensures responses (likes, attendance) display (?!)
1476         foreach ($parents as $parent) {
1477                 if (count($parent['children'])) {
1478                         add_children_to_list($parent['children'], $parents);
1479                 }
1480         }
1481
1482         return $parents;
1483 }
1484
1485 /**
1486  * usort() callback to sort item arrays by pinned and the received key
1487  *
1488  * @param array $a
1489  * @param array $b
1490  * @return int
1491  */
1492 function sort_thr_pinned_received(array $a, array $b)
1493 {
1494         if ($b['pinned'] && !$a['pinned']) {
1495                 return 1;
1496         } elseif (!$b['pinned'] && $a['pinned']) {
1497                 return -1;
1498         }
1499
1500         return strcmp($b['received'], $a['received']);
1501 }
1502
1503 /**
1504  * usort() callback to sort item arrays by the received key
1505  *
1506  * @param array $a
1507  * @param array $b
1508  * @return int
1509  */
1510 function sort_thr_received(array $a, array $b)
1511 {
1512         return strcmp($b['received'], $a['received']);
1513 }
1514
1515 /**
1516  * usort() callback to reverse sort item arrays by the received key
1517  *
1518  * @param array $a
1519  * @param array $b
1520  * @return int
1521  */
1522 function sort_thr_received_rev(array $a, array $b)
1523 {
1524         return strcmp($a['received'], $b['received']);
1525 }
1526
1527 /**
1528  * usort() callback to sort item arrays by the commented key
1529  *
1530  * @param array $a
1531  * @param array $b
1532  * @return int
1533  */
1534 function sort_thr_commented(array $a, array $b)
1535 {
1536         return strcmp($b['commented'], $a['commented']);
1537 }
1538
1539 function render_location_dummy(array $item) {
1540         if (!empty($item['location']) && !empty($item['location'])) {
1541                 return $item['location'];
1542         }
1543
1544         if (!empty($item['coord']) && !empty($item['coord'])) {
1545                 return $item['coord'];
1546         }
1547 }
1548
1549 function get_responses(array $conv_responses, array $response_verbs, array $item, Post $ob = null) {
1550         $ret = [];
1551         foreach ($response_verbs as $v) {
1552                 $ret[$v] = [];
1553                 $ret[$v]['count'] = $conv_responses[$v][$item['uri']] ?? 0;
1554                 $ret[$v]['list']  = $conv_responses[$v][$item['uri'] . '-l'] ?? [];
1555                 $ret[$v]['self']  = $conv_responses[$v][$item['uri'] . '-self'] ?? '0';
1556                 if (count($ret[$v]['list']) > MAX_LIKERS) {
1557                         $ret[$v]['list_part'] = array_slice($ret[$v]['list'], 0, MAX_LIKERS);
1558                         array_push($ret[$v]['list_part'], '<a href="#" data-toggle="modal" data-target="#' . $v . 'Modal-'
1559                                 . (($ob) ? $ob->getId() : $item['id']) . '"><b>' . DI::l10n()->t('View all') . '</b></a>');
1560                 } else {
1561                         $ret[$v]['list_part'] = '';
1562                 }
1563                 $ret[$v]['button'] = get_response_button_text($v, $ret[$v]['count']);
1564                 $ret[$v]['title'] = $conv_responses[$v]['title'];
1565         }
1566
1567         $count = 0;
1568         foreach ($ret as $key) {
1569                 if ($key['count'] == true) {
1570                         $count++;
1571                 }
1572         }
1573         $ret['count'] = $count;
1574
1575         return $ret;
1576 }
1577
1578 function get_response_button_text($v, $count)
1579 {
1580         $return = '';
1581         switch ($v) {
1582                 case 'like':
1583                         $return = DI::l10n()->tt('Like', 'Likes', $count);
1584                         break;
1585                 case 'dislike':
1586                         $return = DI::l10n()->tt('Dislike', 'Dislikes', $count);
1587                         break;
1588                 case 'attendyes':
1589                         $return = DI::l10n()->tt('Attending', 'Attending', $count);
1590                         break;
1591                 case 'attendno':
1592                         $return = DI::l10n()->tt('Not Attending', 'Not Attending', $count);
1593                         break;
1594                 case 'attendmaybe':
1595                         $return = DI::l10n()->tt('Undecided', 'Undecided', $count);
1596                         break;
1597         }
1598
1599         return $return;
1600 }