]> git.mxchange.org Git - friendica.git/blob - include/conversation.php
Merge pull request #8163 from MrPetovan/task/7817-custom-fields-part-3
[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\Hook;
12 use Friendica\Core\Logger;
13 use Friendica\Core\Protocol;
14 use Friendica\Core\Renderer;
15 use Friendica\Core\Session;
16 use Friendica\Database\DBA;
17 use Friendica\DI;
18 use Friendica\Model\Contact;
19 use Friendica\Model\Item;
20 use Friendica\Model\Profile;
21 use Friendica\Model\Term;
22 use Friendica\Object\Post;
23 use Friendica\Object\Thread;
24 use Friendica\Protocol\Activity;
25 use Friendica\Util\Crypto;
26 use Friendica\Util\DateTimeFormat;
27 use Friendica\Util\Proxy as ProxyUtils;
28 use Friendica\Util\Strings;
29 use Friendica\Util\Temporal;
30 use Friendica\Util\XML;
31
32 function item_extract_images($body) {
33
34         $saved_image = [];
35         $orig_body = $body;
36         $new_body = '';
37
38         $cnt = 0;
39         $img_start = strpos($orig_body, '[img');
40         $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
41         $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
42         while (($img_st_close !== false) && ($img_end !== false)) {
43
44                 $img_st_close++; // make it point to AFTER the closing bracket
45                 $img_end += $img_start;
46
47                 if (!strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
48                         // This is an embedded image
49
50                         $saved_image[$cnt] = substr($orig_body, $img_start + $img_st_close, $img_end - ($img_start + $img_st_close));
51                         $new_body = $new_body . substr($orig_body, 0, $img_start) . '[!#saved_image' . $cnt . '#!]';
52
53                         $cnt++;
54                 } else {
55                         $new_body = $new_body . substr($orig_body, 0, $img_end + strlen('[/img]'));
56                 }
57
58                 $orig_body = substr($orig_body, $img_end + strlen('[/img]'));
59
60                 if ($orig_body === false) {
61                         // in case the body ends on a closing image tag
62                         $orig_body = '';
63                 }
64
65                 $img_start = strpos($orig_body, '[img');
66                 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
67                 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
68         }
69
70         $new_body = $new_body . $orig_body;
71
72         return ['body' => $new_body, 'images' => $saved_image];
73 }
74
75 function item_redir_and_replace_images($body, $images, $cid) {
76
77         $origbody = $body;
78         $newbody = '';
79
80         $cnt = 1;
81         $pos = BBCode::getTagPosition($origbody, 'url', 0);
82         while ($pos !== false && $cnt < 1000) {
83
84                 $search = '/\[url\=(.*?)\]\[!#saved_image([0-9]*)#!\]\[\/url\]' . '/is';
85                 $replace = '[url=' . DI::baseUrl() . '/redir/' . $cid
86                                    . '?url=' . '$1' . '][!#saved_image' . '$2' .'#!][/url]';
87
88                 $newbody .= substr($origbody, 0, $pos['start']['open']);
89                 $subject = substr($origbody, $pos['start']['open'], $pos['end']['close'] - $pos['start']['open']);
90                 $origbody = substr($origbody, $pos['end']['close']);
91                 if ($origbody === false) {
92                         $origbody = '';
93                 }
94
95                 $subject = preg_replace($search, $replace, $subject);
96                 $newbody .= $subject;
97
98                 $cnt++;
99                 // Isn't this supposed to use $cnt value for $occurrences? - @MrPetovan
100                 $pos = BBCode::getTagPosition($origbody, 'url', 0);
101         }
102         $newbody .= $origbody;
103
104         $cnt = 0;
105         foreach ($images as $image) {
106                 /*
107                  * We're depending on the property of 'foreach' (specified on the PHP website) that
108                  * it loops over the array starting from the first element and going sequentially
109                  * to the last element.
110                  */
111                 $newbody = str_replace('[!#saved_image' . $cnt . '#!]', '[img]' . $image . '[/img]', $newbody);
112                 $cnt++;
113         }
114         return $newbody;
115 }
116
117 /**
118  * Render actions localized
119  *
120  * @param $item
121  * @throws ImagickException
122  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
123  */
124 function localize_item(&$item)
125 {
126         $extracted = item_extract_images($item['body']);
127         if ($extracted['images']) {
128                 $item['body'] = item_redir_and_replace_images($extracted['body'], $extracted['images'], $item['contact-id']);
129         }
130
131         /*
132         heluecht 2018-06-19: from my point of view this whole code part is useless.
133         It just renders the body message of technical posts (Like, dislike, ...).
134         But: The body isn't visible at all. So we do this stuff just because we can.
135         Even if these messages were visible, this would only mean that something went wrong.
136         During the further steps of the database restructuring I would like to address this issue.
137         */
138
139         $activity = DI::activity();
140
141         $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
142         if ($activity->match($item['verb'], Activity::LIKE)
143                 || $activity->match($item['verb'], Activity::DISLIKE)
144                 || $activity->match($item['verb'], Activity::ATTEND)
145                 || $activity->match($item['verb'], Activity::ATTENDNO)
146                 || $activity->match($item['verb'], Activity::ATTENDMAYBE)) {
147
148                 $fields = ['author-link', 'author-name', 'verb', 'object-type', 'resource-id', 'body', 'plink'];
149                 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
150                 if (!DBA::isResult($obj)) {
151                         return;
152                 }
153
154                 $author  = '[url=' . $item['author-link'] . ']' . $item['author-name'] . '[/url]';
155                 $objauthor =  '[url=' . $obj['author-link'] . ']' . $obj['author-name'] . '[/url]';
156
157                 switch ($obj['verb']) {
158                         case Activity::POST:
159                                 switch ($obj['object-type']) {
160                                         case Activity\ObjectType::EVENT:
161                                                 $post_type = DI::l10n()->t('event');
162                                                 break;
163                                         default:
164                                                 $post_type = DI::l10n()->t('status');
165                                 }
166                                 break;
167                         default:
168                                 if ($obj['resource-id']) {
169                                         $post_type = DI::l10n()->t('photo');
170                                         $m = [];
171                                         preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
172                                         $rr['plink'] = $m[1];
173                                 } else {
174                                         $post_type = DI::l10n()->t('status');
175                                 }
176                 }
177
178                 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
179
180                 $bodyverb = '';
181                 if ($activity->match($item['verb'], Activity::LIKE)) {
182                         $bodyverb = DI::l10n()->t('%1$s likes %2$s\'s %3$s');
183                 } elseif ($activity->match($item['verb'], Activity::DISLIKE)) {
184                         $bodyverb = DI::l10n()->t('%1$s doesn\'t like %2$s\'s %3$s');
185                 } elseif ($activity->match($item['verb'], Activity::ATTEND)) {
186                         $bodyverb = DI::l10n()->t('%1$s attends %2$s\'s %3$s');
187                 } elseif ($activity->match($item['verb'], Activity::ATTENDNO)) {
188                         $bodyverb = DI::l10n()->t('%1$s doesn\'t attend %2$s\'s %3$s');
189                 } elseif ($activity->match($item['verb'], Activity::ATTENDMAYBE)) {
190                         $bodyverb = DI::l10n()->t('%1$s attends maybe %2$s\'s %3$s');
191                 }
192
193                 $item['body'] = sprintf($bodyverb, $author, $objauthor, $plink);
194         }
195
196         if ($activity->match($item['verb'], Activity::FRIEND)) {
197
198                 if ($item['object-type']=="" || $item['object-type']!== Activity\ObjectType::PERSON) return;
199
200                 $Aname = $item['author-name'];
201                 $Alink = $item['author-link'];
202
203                 $xmlhead="<"."?xml version='1.0' encoding='UTF-8' ?".">";
204
205                 $obj = XML::parseString($xmlhead.$item['object']);
206                 $links = XML::parseString($xmlhead."<links>".XML::unescape($obj->link)."</links>");
207
208                 $Bname = $obj->title;
209                 $Blink = "";
210                 $Bphoto = "";
211                 foreach ($links->link as $l) {
212                         $atts = $l->attributes();
213                         switch ($atts['rel']) {
214                                 case "alternate": $Blink = $atts['href']; break;
215                                 case "photo": $Bphoto = $atts['href']; break;
216                         }
217                 }
218
219                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
220                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
221                 if ($Bphoto != "") {
222                         $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img]' . $Bphoto . '[/img][/url]';
223                 }
224
225                 $item['body'] = DI::l10n()->t('%1$s is now friends with %2$s', $A, $B)."\n\n\n".$Bphoto;
226
227         }
228         if (stristr($item['verb'], Activity::POKE)) {
229                 $verb = urldecode(substr($item['verb'],strpos($item['verb'],'#')+1));
230                 if (!$verb) {
231                         return;
232                 }
233                 if ($item['object-type']=="" || $item['object-type']!== Activity\ObjectType::PERSON) {
234                         return;
235                 }
236
237                 $Aname = $item['author-name'];
238                 $Alink = $item['author-link'];
239
240                 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
241
242                 $obj = XML::parseString($xmlhead.$item['object']);
243
244                 $Bname = $obj->title;
245                 $Blink = $obj->id;
246                 $Bphoto = "";
247
248                 foreach ($obj->link as $l) {
249                         $atts = $l->attributes();
250                         switch ($atts['rel']) {
251                                 case "alternate": $Blink = $atts['href'];
252                                 case "photo": $Bphoto = $atts['href'];
253                         }
254                 }
255
256                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
257                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
258                 if ($Bphoto != "") {
259                         $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img=80x80]' . $Bphoto . '[/img][/url]';
260                 }
261
262                 /*
263                  * we can't have a translation string with three positions but no distinguishable text
264                  * So here is the translate string.
265                  */
266                 $txt = DI::l10n()->t('%1$s poked %2$s');
267
268                 // now translate the verb
269                 $poked_t = trim(sprintf($txt, "", ""));
270                 $txt = str_replace($poked_t, DI::l10n()->t($verb), $txt);
271
272                 // then do the sprintf on the translation string
273
274                 $item['body'] = sprintf($txt, $A, $B). "\n\n\n" . $Bphoto;
275
276         }
277
278         if ($activity->match($item['verb'],  Activity::TAG)) {
279                 $fields = ['author-id', 'author-link', 'author-name', 'author-network',
280                         'verb', 'object-type', 'resource-id', 'body', 'plink'];
281                 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
282                 if (!DBA::isResult($obj)) {
283                         return;
284                 }
285
286                 $author_arr = ['uid' => 0, 'id' => $item['author-id'],
287                         'network' => $item['author-network'], 'url' => $item['author-link']];
288                 $author  = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $item['author-name'] . '[/url]';
289
290                 $author_arr = ['uid' => 0, 'id' => $obj['author-id'],
291                         'network' => $obj['author-network'], 'url' => $obj['author-link']];
292                 $objauthor  = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $obj['author-name'] . '[/url]';
293
294                 switch ($obj['verb']) {
295                         case Activity::POST:
296                                 switch ($obj['object-type']) {
297                                         case Activity\ObjectType::EVENT:
298                                                 $post_type = DI::l10n()->t('event');
299                                                 break;
300                                         default:
301                                                 $post_type = DI::l10n()->t('status');
302                                 }
303                                 break;
304                         default:
305                                 if ($obj['resource-id']) {
306                                         $post_type = DI::l10n()->t('photo');
307                                         $m=[]; preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
308                                         $rr['plink'] = $m[1];
309                                 } else {
310                                         $post_type = DI::l10n()->t('status');
311                                 }
312                                 // Let's break everthing ... ;-)
313                                 break;
314                 }
315                 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
316
317                 $parsedobj = XML::parseString($xmlhead.$item['object']);
318
319                 $tag = sprintf('#[url=%s]%s[/url]', $parsedobj->id, $parsedobj->content);
320                 $item['body'] = DI::l10n()->t('%1$s tagged %2$s\'s %3$s with %4$s', $author, $objauthor, $plink, $tag);
321         }
322
323         if ($activity->match($item['verb'], Activity::FAVORITE)) {
324                 if ($item['object-type'] == "") {
325                         return;
326                 }
327
328                 $Aname = $item['author-name'];
329                 $Alink = $item['author-link'];
330
331                 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
332
333                 $obj = XML::parseString($xmlhead.$item['object']);
334                 if (strlen($obj->id)) {
335                         $fields = ['author-link', 'author-name', 'plink'];
336                         $target = Item::selectFirst($fields, ['uri' => $obj->id, 'uid' => $item['uid']]);
337                         if (DBA::isResult($target) && $target['plink']) {
338                                 $Bname = $target['author-name'];
339                                 $Blink = $target['author-link'];
340                                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
341                                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
342                                 $P = '[url=' . $target['plink'] . ']' . DI::l10n()->t('post/item') . '[/url]';
343                                 $item['body'] = DI::l10n()->t('%1$s marked %2$s\'s %3$s as favorite', $A, $B, $P)."\n";
344                         }
345                 }
346         }
347         $matches = null;
348         if (preg_match_all('/@\[url=(.*?)\]/is', $item['body'], $matches, PREG_SET_ORDER)) {
349                 foreach ($matches as $mtch) {
350                         if (!strpos($mtch[1], 'zrl=')) {
351                                 $item['body'] = str_replace($mtch[0], '@[url=' . Contact::magicLink($mtch[1]) . ']', $item['body']);
352                         }
353                 }
354         }
355
356         // add zrl's to public images
357         $photo_pattern = "/\[url=(.*?)\/photos\/(.*?)\/image\/(.*?)\]\[img(.*?)\]h(.*?)\[\/img\]\[\/url\]/is";
358         if (preg_match($photo_pattern, $item['body'])) {
359                 $photo_replace = '[url=' . Profile::zrl('$1' . '/photos/' . '$2' . '/image/' . '$3' ,true) . '][img' . '$4' . ']h' . '$5'  . '[/img][/url]';
360                 $item['body'] = BBCode::pregReplaceInTag($photo_pattern, $photo_replace, 'url', $item['body']);
361         }
362
363         // add sparkle links to appropriate permalinks
364         $author = ['uid' => 0, 'id' => $item['author-id'],
365                 'network' => $item['author-network'], 'url' => $item['author-link']];
366
367         // Only create a redirection to a magic link when logged in
368         if (!empty($item['plink']) && Session::isAuthenticated()) {
369                 $item['plink'] = Contact::magicLinkByContact($author, $item['plink']);
370         }
371 }
372
373 /**
374  * Count the total of comments on this item and its desendants
375  * @TODO proper type-hint + doc-tag
376  * @param $item
377  * @return int
378  */
379 function count_descendants($item) {
380         $total = count($item['children']);
381
382         if ($total > 0) {
383                 foreach ($item['children'] as $child) {
384                         if (!visible_activity($child)) {
385                                 $total --;
386                         }
387                         $total += count_descendants($child);
388                 }
389         }
390
391         return $total;
392 }
393
394 function visible_activity($item) {
395
396         $activity = DI::activity();
397
398         if (empty($item['verb']) || $activity->isHidden($item['verb'])) {
399                 return false;
400         }
401
402         // @TODO below if() block can be rewritten to a single line: $isVisible = allConditionsHere;
403         if ($activity->match($item['verb'], Activity::FOLLOW) &&
404             $item['object-type'] === Activity\ObjectType::NOTE &&
405             empty($item['self']) &&
406             $item['uid'] == local_user()) {
407                 return false;
408         }
409
410         return true;
411 }
412
413 function conv_get_blocklist()
414 {
415         if (!local_user()) {
416                 return [];
417         }
418
419         $str_blocked = DI::pConfig()->get(local_user(), 'system', 'blocked');
420         if (empty($str_blocked)) {
421                 return [];
422         }
423
424         $blocklist = [];
425
426         foreach (explode(',', $str_blocked) as $entry) {
427                 // The 4th parameter guarantees that there always will be a public contact entry
428                 $cid = Contact::getIdForURL(trim($entry), 0, true, ['url' => trim($entry)]);
429                 if (!empty($cid)) {
430                         $blocklist[] = $cid;
431                 }
432         }
433
434         return $blocklist;
435 }
436
437 /**
438  * "Render" a conversation or list of items for HTML display.
439  * There are two major forms of display:
440  *      - Sequential or unthreaded ("New Item View" or search results)
441  *      - conversation view
442  * The $mode parameter decides between the various renderings and also
443  * figures out how to determine page owner and other contextual items
444  * that are based on unique features of the calling module.
445  * @param App    $a
446  * @param array  $items
447  * @param Pager  $pager
448  * @param        $mode
449  * @param        $update
450  * @param bool   $preview
451  * @param string $order
452  * @param int    $uid
453  * @return string
454  * @throws ImagickException
455  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
456  */
457 function conversation(App $a, array $items, Pager $pager, $mode, $update, $preview = false, $order = 'commented', $uid = 0)
458 {
459         $ssl_state = (local_user() ? true : false);
460
461         $profile_owner = 0;
462         $live_update_div = '';
463
464         $blocklist = conv_get_blocklist();
465
466         $previewing = (($preview) ? ' preview ' : '');
467
468         if ($mode === 'network') {
469                 $items = conversation_add_children($items, false, $order, $uid);
470                 $profile_owner = local_user();
471                 if (!$update) {
472                         /*
473                          * The special div is needed for liveUpdate to kick in for this page.
474                          * We only launch liveUpdate if you aren't filtering in some incompatible
475                          * way and also you aren't writing a comment (discovered in javascript).
476                          */
477                         $live_update_div = '<div id="live-network"></div>' . "\r\n"
478                                 . "<script> var profile_uid = " . $_SESSION['uid']
479                                 . "; var netargs = '" . substr(DI::args()->getCommand(), 8)
480                                 . '?f='
481                                 . (!empty($_GET['cid'])    ? '&cid='    . rawurlencode($_GET['cid'])    : '')
482                                 . (!empty($_GET['search']) ? '&search=' . rawurlencode($_GET['search']) : '')
483                                 . (!empty($_GET['star'])   ? '&star='   . rawurlencode($_GET['star'])   : '')
484                                 . (!empty($_GET['order'])  ? '&order='  . rawurlencode($_GET['order'])  : '')
485                                 . (!empty($_GET['bmark'])  ? '&bmark='  . rawurlencode($_GET['bmark'])  : '')
486                                 . (!empty($_GET['liked'])  ? '&liked='  . rawurlencode($_GET['liked'])  : '')
487                                 . (!empty($_GET['conv'])   ? '&conv='   . rawurlencode($_GET['conv'])   : '')
488                                 . (!empty($_GET['nets'])   ? '&nets='   . rawurlencode($_GET['nets'])   : '')
489                                 . (!empty($_GET['cmin'])   ? '&cmin='   . rawurlencode($_GET['cmin'])   : '')
490                                 . (!empty($_GET['cmax'])   ? '&cmax='   . rawurlencode($_GET['cmax'])   : '')
491                                 . (!empty($_GET['file'])   ? '&file='   . rawurlencode($_GET['file'])   : '')
492
493                                 . "'; var profile_page = " . $pager->getPage() . "; </script>\r\n";
494                 }
495         } elseif ($mode === 'profile') {
496                 $items = conversation_add_children($items, false, $order, $uid);
497                 $profile_owner = $a->profile['uid'];
498
499                 if (!$update) {
500                         $tab = 'posts';
501                         if (!empty($_GET['tab'])) {
502                                 $tab = Strings::escapeTags(trim($_GET['tab']));
503                         }
504                         if ($tab === 'posts') {
505                                 /*
506                                  * This is ugly, but we can't pass the profile_uid through the session to the ajax updater,
507                                  * because browser prefetching might change it on us. We have to deliver it with the page.
508                                  */
509
510                                 $live_update_div = '<div id="live-profile"></div>' . "\r\n"
511                                         . "<script> var profile_uid = " . $a->profile['uid']
512                                         . "; var netargs = '?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
513                         }
514                 }
515         } elseif ($mode === 'notes') {
516                 $items = conversation_add_children($items, false, $order, local_user());
517                 $profile_owner = local_user();
518
519                 if (!$update) {
520                         $live_update_div = '<div id="live-notes"></div>' . "\r\n"
521                                 . "<script> var profile_uid = " . local_user()
522                                 . "; var netargs = '/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
523                 }
524         } elseif ($mode === 'display') {
525                 $items = conversation_add_children($items, false, $order, $uid);
526                 $profile_owner = $a->profile['uid'];
527
528                 if (!$update) {
529                         $live_update_div = '<div id="live-display"></div>' . "\r\n"
530                                 . "<script> var profile_uid = " . Session::get('uid', 0) . ";"
531                                 . " var profile_page = 1; </script>";
532                 }
533         } elseif ($mode === 'community') {
534                 $items = conversation_add_children($items, true, $order, $uid);
535                 $profile_owner = 0;
536
537                 if (!$update) {
538                         $live_update_div = '<div id="live-community"></div>' . "\r\n"
539                                 . "<script> var profile_uid = -1; var netargs = '" . substr(DI::args()->getCommand(), 10)
540                                 ."/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
541                 }
542         } elseif ($mode === 'contacts') {
543                 $items = conversation_add_children($items, false, $order, $uid);
544                 $profile_owner = 0;
545
546                 if (!$update) {
547                         $live_update_div = '<div id="live-contacts"></div>' . "\r\n"
548                                 . "<script> var profile_uid = -1; var netargs = '" . substr(DI::args()->getCommand(), 9)
549                                 ."/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
550                 }
551         } elseif ($mode === 'search') {
552                 $live_update_div = '<div id="live-search"></div>' . "\r\n";
553         }
554
555         $page_dropping = ((local_user() && local_user() == $profile_owner) ? true : false);
556
557         if (!$update) {
558                 $_SESSION['return_path'] = DI::args()->getQueryString();
559         }
560
561         $cb = ['items' => $items, 'mode' => $mode, 'update' => $update, 'preview' => $preview];
562         Hook::callAll('conversation_start',$cb);
563
564         $items = $cb['items'];
565
566         $conv_responses = [
567                 'like' => ['title' => DI::l10n()->t('Likes','title')],
568                 'dislike' => ['title' => DI::l10n()->t('Dislikes','title')],
569                 'attendyes' => ['title' => DI::l10n()->t('Attending','title')],
570                 'attendno' => ['title' => DI::l10n()->t('Not attending','title')],
571                 'attendmaybe' => ['title' => DI::l10n()->t('Might attend','title')],
572                 'announce' => ['title' => DI::l10n()->t('Reshares','title')]
573         ];
574
575         // array with html for each thread (parent+comments)
576         $threads = [];
577         $threadsid = -1;
578
579         $page_template = Renderer::getMarkupTemplate("conversation.tpl");
580
581         if (!empty($items)) {
582                 if (in_array($mode, ['community', 'contacts'])) {
583                         $writable = true;
584                 } else {
585                         $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED);
586                 }
587
588                 if (!local_user()) {
589                         $writable = false;
590                 }
591
592                 if (in_array($mode, ['network-new', 'search', 'contact-posts'])) {
593
594                         /*
595                          * "New Item View" on network page or search page results
596                          * - just loop through the items and format them minimally for display
597                          */
598
599                         $tpl = 'search_item.tpl';
600
601                         foreach ($items as $item) {
602
603                                 if (!visible_activity($item)) {
604                                         continue;
605                                 }
606
607                                 if (in_array($item['author-id'], $blocklist)) {
608                                         continue;
609                                 }
610
611                                 $threadsid++;
612
613                                 $owner_url   = '';
614                                 $owner_name  = '';
615                                 $sparkle     = '';
616
617                                 // prevent private email from leaking.
618                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
619                                         continue;
620                                 }
621
622                                 $profile_name = $item['author-name'];
623                                 if (!empty($item['author-link']) && empty($item['author-name'])) {
624                                         $profile_name = $item['author-link'];
625                                 }
626
627                                 $tags = Term::populateTagsFromItem($item);
628
629                                 $author = ['uid' => 0, 'id' => $item['author-id'],
630                                         'network' => $item['author-network'], 'url' => $item['author-link']];
631                                 $profile_link = Contact::magicLinkByContact($author);
632
633                                 if (strpos($profile_link, 'redir/') === 0) {
634                                         $sparkle = ' sparkle';
635                                 }
636
637                                 $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
638                                 Hook::callAll('render_location',$locate);
639
640                                 $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
641
642                                 localize_item($item);
643                                 if ($mode === 'network-new') {
644                                         $dropping = true;
645                                 } else {
646                                         $dropping = false;
647                                 }
648
649                                 $drop = [
650                                         'dropping' => $dropping,
651                                         'pagedrop' => $page_dropping,
652                                         'select' => DI::l10n()->t('Select'),
653                                         'delete' => DI::l10n()->t('Delete'),
654                                 ];
655
656                                 $star = false;
657                                 $isstarred = "unstarred";
658
659                                 $lock = false;
660                                 $likebuttons = false;
661
662                                 $body = Item::prepareBody($item, true, $preview);
663
664                                 list($categories, $folders) = DI::contentItem()->determineCategoriesTerms($item);
665
666                                 if (!empty($item['content-warning']) && DI::pConfig()->get(local_user(), 'system', 'disable_cw', false)) {
667                                         $title = ucfirst($item['content-warning']);
668                                 } else {
669                                         $title = $item['title'];
670                                 }
671
672                                 $tmp_item = [
673                                         'template' => $tpl,
674                                         'id' => ($preview ? 'P0' : $item['id']),
675                                         'guid' => ($preview ? 'Q0' : $item['guid']),
676                                         'network' => $item['network'],
677                                         'network_name' => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']),
678                                         'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link']),
679                                         'linktitle' => DI::l10n()->t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
680                                         'profile_url' => $profile_link,
681                                         'item_photo_menu' => item_photo_menu($item),
682                                         'name' => $profile_name,
683                                         'sparkle' => $sparkle,
684                                         'lock' => $lock,
685                                         'thumb' => DI::baseUrl()->remove(ProxyUtils::proxifyUrl($item['author-avatar'], false, ProxyUtils::SIZE_THUMB)),
686                                         'title' => $title,
687                                         'body' => $body,
688                                         'tags' => $tags['tags'],
689                                         'hashtags' => $tags['hashtags'],
690                                         'mentions' => $tags['mentions'],
691                                         'implicit_mentions' => $tags['implicit_mentions'],
692                                         'txt_cats' => DI::l10n()->t('Categories:'),
693                                         'txt_folders' => DI::l10n()->t('Filed under:'),
694                                         'has_cats' => ((count($categories)) ? 'true' : ''),
695                                         'has_folders' => ((count($folders)) ? 'true' : ''),
696                                         'categories' => $categories,
697                                         'folders' => $folders,
698                                         'text' => strip_tags($body),
699                                         'localtime' => DateTimeFormat::local($item['created'], 'r'),
700                                         'ago' => (($item['app']) ? DI::l10n()->t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
701                                         'location' => $location,
702                                         'indent' => '',
703                                         'owner_name' => $owner_name,
704                                         'owner_url' => $owner_url,
705                                         'owner_photo' => DI::baseUrl()->remove(ProxyUtils::proxifyUrl($item['owner-avatar'], false, ProxyUtils::SIZE_THUMB)),
706                                         'plink' => Item::getPlink($item),
707                                         'edpost' => false,
708                                         'isstarred' => $isstarred,
709                                         'star' => $star,
710                                         'drop' => $drop,
711                                         'vote' => $likebuttons,
712                                         'like' => '',
713                                         'dislike' => '',
714                                         'comment' => '',
715                                         'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> DI::l10n()->t('View in context')]),
716                                         'previewing' => $previewing,
717                                         'wait' => DI::l10n()->t('Please wait'),
718                                         'thread_level' => 1,
719                                 ];
720
721                                 $arr = ['item' => $item, 'output' => $tmp_item];
722                                 Hook::callAll('display_item', $arr);
723
724                                 $threads[$threadsid]['id'] = $item['id'];
725                                 $threads[$threadsid]['network'] = $item['network'];
726                                 $threads[$threadsid]['items'] = [$arr['output']];
727
728                         }
729                 } else {
730                         // Normal View
731                         $page_template = Renderer::getMarkupTemplate("threaded_conversation.tpl");
732
733                         $conv = new Thread($mode, $preview, $writable);
734
735                         /*
736                          * get all the topmost parents
737                          * this shouldn't be needed, as we should have only them in our array
738                          * But for now, this array respects the old style, just in case
739                          */
740                         foreach ($items as $item) {
741                                 if (in_array($item['author-id'], $blocklist)) {
742                                         continue;
743                                 }
744
745                                 // Can we put this after the visibility check?
746                                 builtin_activity_puller($item, $conv_responses);
747
748                                 // Only add what is visible
749                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
750                                         continue;
751                                 }
752
753                                 if (!visible_activity($item)) {
754                                         continue;
755                                 }
756
757                                 /// @todo Check if this call is needed or not
758                                 $arr = ['item' => $item];
759                                 Hook::callAll('display_item', $arr);
760
761                                 $item['pagedrop'] = $page_dropping;
762
763                                 if ($item['id'] == $item['parent']) {
764                                         $item_object = new Post($item);
765                                         $conv->addParent($item_object);
766                                 }
767                         }
768
769                         $threads = $conv->getTemplateData($conv_responses);
770                         if (!$threads) {
771                                 Logger::log('[ERROR] conversation : Failed to get template data.', Logger::DEBUG);
772                                 $threads = [];
773                         }
774                 }
775         }
776
777         $o = Renderer::replaceMacros($page_template, [
778                 '$baseurl' => DI::baseUrl()->get($ssl_state),
779                 '$return_path' => DI::args()->getQueryString(),
780                 '$live_update' => $live_update_div,
781                 '$remove' => DI::l10n()->t('remove'),
782                 '$mode' => $mode,
783                 '$user' => $a->user,
784                 '$threads' => $threads,
785                 '$dropping' => ($page_dropping ? DI::l10n()->t('Delete Selected Items') : False),
786         ]);
787
788         return $o;
789 }
790
791 /**
792  * Fetch all comments from a query. Additionally set the newest resharer as thread owner.
793  *
794  * @param mixed   $thread_items Database statement with thread posts
795  * @param boolean $pinned       Is the item pinned?
796  *
797  * @return array items with parents and comments
798  */
799 function conversation_fetch_comments($thread_items, $pinned) {
800         $comments = [];
801         $parentlines = [];
802         $lineno = 0;
803         $actor = [];
804         $received = '';
805
806         while ($row = Item::fetch($thread_items)) {
807                 if (($row['verb'] == Activity::ANNOUNCE) && !empty($row['contact-uid']) && ($row['received'] > $received) && ($row['thr-parent'] == $row['parent-uri'])) {
808                         $actor = ['link' => $row['author-link'], 'avatar' => $row['author-avatar'], 'name' => $row['author-name']];
809                         $received = $row['received'];
810                 }
811
812                 if ((($row['gravity'] == GRAVITY_PARENT) && !$row['origin'] && !in_array($row['network'], [Protocol::DIASPORA])) &&
813                         (empty($row['contact-uid']) || !in_array($row['network'], Protocol::NATIVE_SUPPORT))) {
814                         $parentlines[] = $lineno;
815                 }
816
817                 if ($row['gravity'] == GRAVITY_PARENT) {
818                         $row['pinned'] = $pinned;
819                 }
820
821                 $comments[] = $row;
822                 $lineno++;
823         }
824
825         DBA::close($thread_items);
826
827         if (!empty($actor)) {
828                 foreach ($parentlines as $line) {
829                         $comments[$line]['owner-link'] = $actor['link'];
830                         $comments[$line]['owner-avatar'] = $actor['avatar'];
831                         $comments[$line]['owner-name'] = $actor['name'];
832                 }
833         }
834         return $comments;
835 }
836
837 /**
838  * Add comments to top level entries that had been fetched before
839  *
840  * The system will fetch the comments for the local user whenever possible.
841  * This behaviour is currently needed to allow commenting on Friendica posts.
842  *
843  * @param array $parents Parent items
844  *
845  * @param       $block_authors
846  * @param       $order
847  * @param       $uid
848  * @return array items with parents and comments
849  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
850  */
851 function conversation_add_children(array $parents, $block_authors, $order, $uid) {
852         $max_comments = DI::config()->get('system', 'max_comments', 100);
853
854         $params = ['order' => ['uid', 'commented' => true]];
855
856         if ($max_comments > 0) {
857                 $params['limit'] = $max_comments;
858         }
859
860         $items = [];
861
862         foreach ($parents AS $parent) {
863                 $condition = ["`item`.`parent-uri` = ? AND `item`.`uid` IN (0, ?) ",
864                         $parent['uri'], $uid];
865                 if ($block_authors) {
866                         $condition[0] .= "AND NOT `author`.`hidden`";
867                 }
868
869                 $thread_items = Item::selectForUser(local_user(), array_merge(Item::DISPLAY_FIELDLIST, ['contact-uid', 'gravity']), $condition, $params);
870
871                 $comments = conversation_fetch_comments($thread_items, $parent['pinned'] ?? false);
872
873                 if (count($comments) != 0) {
874                         $items = array_merge($items, $comments);
875                 }
876         }
877
878         foreach ($items as $index => $item) {
879                 if ($item['uid'] == 0) {
880                         $items[$index]['writable'] = in_array($item['network'], Protocol::FEDERATED);
881                 }
882         }
883
884         $items = conv_sort($items, $order);
885
886         return $items;
887 }
888
889 function item_photo_menu($item) {
890         $sub_link = '';
891         $poke_link = '';
892         $contact_url = '';
893         $pm_url = '';
894         $status_link = '';
895         $photos_link = '';
896         $posts_link = '';
897         $block_link = '';
898         $ignore_link = '';
899
900         if (local_user() && local_user() == $item['uid'] && $item['parent'] == $item['id'] && !$item['self']) {
901                 $sub_link = 'javascript:dosubthread(' . $item['id'] . '); return false;';
902         }
903
904         $author = ['uid' => 0, 'id' => $item['author-id'],
905                 'network' => $item['author-network'], 'url' => $item['author-link']];
906         $profile_link = Contact::magicLinkByContact($author, $item['author-link']);
907         $sparkle = (strpos($profile_link, 'redir/') === 0);
908
909         $cid = 0;
910         $pcid = Contact::getIdForURL($item['author-link'], 0, true);
911         $network = '';
912         $rel = 0;
913         $condition = ['uid' => local_user(), 'nurl' => Strings::normaliseLink($item['author-link'])];
914         $contact = DBA::selectFirst('contact', ['id', 'network', 'rel'], $condition);
915         if (DBA::isResult($contact)) {
916                 $cid = $contact['id'];
917                 $network = $contact['network'];
918                 $rel = $contact['rel'];
919         }
920
921         if ($sparkle) {
922                 $status_link = $profile_link . '/status';
923                 $photos_link = str_replace('/profile/', '/photos/', $profile_link);
924                 $profile_link = $profile_link . '/profile';
925         }
926
927         if (!empty($pcid)) {
928                 $contact_url = 'contact/' . $pcid;
929                 $posts_link = 'contact/' . $pcid . '/posts';
930                 $block_link = 'contact/' . $pcid . '/block';
931                 $ignore_link = 'contact/' . $pcid . '/ignore';
932         }
933
934         if ($cid && !$item['self']) {
935                 $poke_link = 'poke?c=' . $cid;
936                 $contact_url = 'contact/' . $cid;
937                 $posts_link = 'contact/' . $cid . '/posts';
938
939                 if (in_array($network, [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA])) {
940                         $pm_url = 'message/new/' . $cid;
941                 }
942         }
943
944         if (local_user()) {
945                 $menu = [
946                         DI::l10n()->t('Follow Thread') => $sub_link,
947                         DI::l10n()->t('View Status') => $status_link,
948                         DI::l10n()->t('View Profile') => $profile_link,
949                         DI::l10n()->t('View Photos') => $photos_link,
950                         DI::l10n()->t('Network Posts') => $posts_link,
951                         DI::l10n()->t('View Contact') => $contact_url,
952                         DI::l10n()->t('Send PM') => $pm_url,
953                         DI::l10n()->t('Block') => $block_link,
954                         DI::l10n()->t('Ignore') => $ignore_link
955                 ];
956
957                 if ($network == Protocol::DFRN) {
958                         $menu[DI::l10n()->t("Poke")] = $poke_link;
959                 }
960
961                 if ((($cid == 0) || ($rel == Contact::FOLLOWER)) &&
962                         in_array($item['network'], Protocol::FEDERATED)) {
963                         $menu[DI::l10n()->t('Connect/Follow')] = 'follow?url=' . urlencode($item['author-link']);
964                 }
965         } else {
966                 $menu = [DI::l10n()->t('View Profile') => $item['author-link']];
967         }
968
969         $args = ['item' => $item, 'menu' => $menu];
970
971         Hook::callAll('item_photo_menu', $args);
972
973         $menu = $args['menu'];
974
975         $o = '';
976         foreach ($menu as $k => $v) {
977                 if (strpos($v, 'javascript:') === 0) {
978                         $v = substr($v, 11);
979                         $o .= '<li role="menuitem"><a onclick="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
980                 } elseif ($v!='') {
981                         $o .= '<li role="menuitem"><a href="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
982                 }
983         }
984         return $o;
985 }
986
987 /**
988  * Checks item to see if it is one of the builtin activities (like/dislike, event attendance, consensus items, etc.)
989  *
990  * Increments the count of each matching activity and adds a link to the author as needed.
991  *
992  * @param array  $item
993  * @param array &$conv_responses (already created with builtin activity structure)
994  * @return void
995  * @throws ImagickException
996  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
997  */
998 function builtin_activity_puller($item, &$conv_responses) {
999         foreach ($conv_responses as $mode => $v) {
1000                 $sparkle = '';
1001
1002                 switch ($mode) {
1003                         case 'like':
1004                                 $verb = Activity::LIKE;
1005                                 break;
1006                         case 'dislike':
1007                                 $verb = Activity::DISLIKE;
1008                                 break;
1009                         case 'attendyes':
1010                                 $verb = Activity::ATTEND;
1011                                 break;
1012                         case 'attendno':
1013                                 $verb = Activity::ATTENDNO;
1014                                 break;
1015                         case 'attendmaybe':
1016                                 $verb = Activity::ATTENDMAYBE;
1017                                 break;
1018                         case 'announce':
1019                                 $verb = Activity::ANNOUNCE;
1020                                 break;
1021                         default:
1022                                 return;
1023                 }
1024
1025                 if (!empty($item['verb']) && DI::activity()->match($item['verb'], $verb) && ($item['id'] != $item['parent'])) {
1026                         $author = ['uid' => 0, 'id' => $item['author-id'],
1027                                 'network' => $item['author-network'], 'url' => $item['author-link']];
1028                         $url = Contact::magicLinkByContact($author);
1029                         if (strpos($url, 'redir/') === 0) {
1030                                 $sparkle = ' class="sparkle" ';
1031                         }
1032
1033                         $url = '<a href="'. $url . '"'. $sparkle .'>' . htmlentities($item['author-name']) . '</a>';
1034
1035                         if (empty($item['thr-parent'])) {
1036                                 $item['thr-parent'] = $item['parent-uri'];
1037                         }
1038
1039                         if (!(isset($conv_responses[$mode][$item['thr-parent'] . '-l'])
1040                                 && is_array($conv_responses[$mode][$item['thr-parent'] . '-l']))) {
1041                                 $conv_responses[$mode][$item['thr-parent'] . '-l'] = [];
1042                         }
1043
1044                         // only list each unique author once
1045                         if (in_array($url,$conv_responses[$mode][$item['thr-parent'] . '-l'])) {
1046                                 continue;
1047                         }
1048
1049                         if (!isset($conv_responses[$mode][$item['thr-parent']])) {
1050                                 $conv_responses[$mode][$item['thr-parent']] = 1;
1051                         } else {
1052                                 $conv_responses[$mode][$item['thr-parent']] ++;
1053                         }
1054
1055                         if (public_contact() == $item['author-id']) {
1056                                 $conv_responses[$mode][$item['thr-parent'] . '-self'] = 1;
1057                         }
1058
1059                         $conv_responses[$mode][$item['thr-parent'] . '-l'][] = $url;
1060
1061                         // there can only be one activity verb per item so if we found anything, we can stop looking
1062                         return;
1063                 }
1064         }
1065 }
1066
1067 /**
1068  * Format the vote text for a profile item
1069  *
1070  * @param int    $cnt  = number of people who vote the item
1071  * @param array  $arr  = array of pre-linked names of likers/dislikers
1072  * @param string $type = one of 'like, 'dislike', 'attendyes', 'attendno', 'attendmaybe'
1073  * @param int    $id   = item id
1074  * @return string formatted text
1075  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1076  */
1077 function format_like($cnt, array $arr, $type, $id) {
1078         $o = '';
1079         $expanded = '';
1080         $phrase = '';
1081
1082         if ($cnt == 1) {
1083                 $likers = $arr[0];
1084
1085                 // Phrase if there is only one liker. In other cases it will be uses for the expanded
1086                 // list which show all likers
1087                 switch ($type) {
1088                         case 'like' :
1089                                 $phrase = DI::l10n()->t('%s likes this.', $likers);
1090                                 break;
1091                         case 'dislike' :
1092                                 $phrase = DI::l10n()->t('%s doesn\'t like this.', $likers);
1093                                 break;
1094                         case 'attendyes' :
1095                                 $phrase = DI::l10n()->t('%s attends.', $likers);
1096                                 break;
1097                         case 'attendno' :
1098                                 $phrase = DI::l10n()->t('%s doesn\'t attend.', $likers);
1099                                 break;
1100                         case 'attendmaybe' :
1101                                 $phrase = DI::l10n()->t('%s attends maybe.', $likers);
1102                                 break;
1103                         case 'announce' :
1104                                 $phrase = DI::l10n()->t('%s reshared this.', $likers);
1105                                 break;
1106                 }
1107         }
1108
1109         if ($cnt > 1) {
1110                 $total = count($arr);
1111                 if ($total < MAX_LIKERS) {
1112                         $last = DI::l10n()->t('and') . ' ' . $arr[count($arr)-1];
1113                         $arr2 = array_slice($arr, 0, -1);
1114                         $likers = implode(', ', $arr2) . ' ' . $last;
1115                 } else  {
1116                         $arr = array_slice($arr, 0, MAX_LIKERS - 1);
1117                         $likers = implode(', ', $arr);
1118                         $likers .= DI::l10n()->t('and %d other people', $total - MAX_LIKERS);
1119                 }
1120
1121                 $spanatts = "class=\"fakelink\" onclick=\"openClose('{$type}list-$id');\"";
1122
1123                 $explikers = '';
1124                 switch ($type) {
1125                         case 'like':
1126                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> like this', $spanatts, $cnt);
1127                                 $explikers = DI::l10n()->t('%s like this.', $likers);
1128                                 break;
1129                         case 'dislike':
1130                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> don\'t like this', $spanatts, $cnt);
1131                                 $explikers = DI::l10n()->t('%s don\'t like this.', $likers);
1132                                 break;
1133                         case 'attendyes':
1134                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> attend', $spanatts, $cnt);
1135                                 $explikers = DI::l10n()->t('%s attend.', $likers);
1136                                 break;
1137                         case 'attendno':
1138                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> don\'t attend', $spanatts, $cnt);
1139                                 $explikers = DI::l10n()->t('%s don\'t attend.', $likers);
1140                                 break;
1141                         case 'attendmaybe':
1142                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> attend maybe', $spanatts, $cnt);
1143                                 $explikers = DI::l10n()->t('%s attend maybe.', $likers);
1144                                 break;
1145                         case 'announce':
1146                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> reshared this', $spanatts, $cnt);
1147                                 $explikers = DI::l10n()->t('%s reshared this.', $likers);
1148                                 break;
1149                 }
1150
1151                 $expanded .= "\t" . '<p class="wall-item-' . $type . '-expanded" id="' . $type . 'list-' . $id . '" style="display: none;" >' . $explikers . EOL . '</p>';
1152         }
1153
1154         $o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('voting_fakelink.tpl'), [
1155                 '$phrase' => $phrase,
1156                 '$type' => $type,
1157                 '$id' => $id
1158         ]);
1159         $o .= $expanded;
1160
1161         return $o;
1162 }
1163
1164 function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
1165 {
1166         $o = '';
1167
1168         $geotag = !empty($x['allow_location']) ? Renderer::replaceMacros(Renderer::getMarkupTemplate('jot_geotag.tpl'), []) : '';
1169
1170         $tpl = Renderer::getMarkupTemplate('jot-header.tpl');
1171         DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
1172                 '$newpost'   => 'true',
1173                 '$baseurl'   => DI::baseUrl()->get(true),
1174                 '$geotag'    => $geotag,
1175                 '$nickname'  => $x['nickname'],
1176                 '$ispublic'  => DI::l10n()->t('Visible to <strong>everybody</strong>'),
1177                 '$linkurl'   => DI::l10n()->t('Please enter a image/video/audio/webpage URL:'),
1178                 '$term'      => DI::l10n()->t('Tag term:'),
1179                 '$fileas'    => DI::l10n()->t('Save to Folder:'),
1180                 '$whereareu' => DI::l10n()->t('Where are you right now?'),
1181                 '$delitems'  => DI::l10n()->t("Delete item\x28s\x29?")
1182         ]);
1183
1184         $jotplugins = '';
1185         Hook::callAll('jot_tool', $jotplugins);
1186
1187         // Private/public post links for the non-JS ACL form
1188         $private_post = 1;
1189         if (!empty($_REQUEST['public'])) {
1190                 $private_post = 0;
1191         }
1192
1193         $query_str = DI::args()->getQueryString();
1194         if (strpos($query_str, 'public=1') !== false) {
1195                 $query_str = str_replace(['?public=1', '&public=1'], ['', ''], $query_str);
1196         }
1197
1198         /*
1199          * I think $a->query_string may never have ? in it, but I could be wrong
1200          * It looks like it's from the index.php?q=[etc] rewrite that the web
1201          * server does, which converts any ? to &, e.g. suggest&ignore=61 for suggest?ignore=61
1202          */
1203         if (strpos($query_str, '?') === false) {
1204                 $public_post_link = '?public=1';
1205         } else {
1206                 $public_post_link = '&public=1';
1207         }
1208
1209         // $tpl = Renderer::replaceMacros($tpl,array('$jotplugins' => $jotplugins));
1210         $tpl = Renderer::getMarkupTemplate("jot.tpl");
1211
1212         $o .= Renderer::replaceMacros($tpl,[
1213                 '$new_post' => DI::l10n()->t('New Post'),
1214                 '$return_path'  => $query_str,
1215                 '$action'       => 'item',
1216                 '$share'        => ($x['button'] ?? '') ?: DI::l10n()->t('Share'),
1217                 '$loading'      => DI::l10n()->t('Loading...'),
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 }