]> git.mxchange.org Git - friendica.git/blob - include/conversation.php
Merge pull request #7751 from tobiasd/20191017-lighttpdsample
[friendica.git] / include / conversation.php
1 <?php
2 /**
3  * @file include/conversation.php
4  */
5
6 use Friendica\App;
7 use Friendica\Content\ContactSelector;
8 use Friendica\Content\Feature;
9 use Friendica\Content\Pager;
10 use Friendica\Content\Text\BBCode;
11 use Friendica\Core\Config;
12 use Friendica\Core\Hook;
13 use Friendica\Core\L10n;
14 use Friendica\Core\Logger;
15 use Friendica\Core\PConfig;
16 use Friendica\Core\Protocol;
17 use Friendica\Core\Renderer;
18 use Friendica\Core\Session;
19 use Friendica\Core\System;
20 use Friendica\Database\DBA;
21 use Friendica\Model\Contact;
22 use Friendica\Model\Item;
23 use Friendica\Model\Profile;
24 use Friendica\Model\Term;
25 use Friendica\Object\Post;
26 use Friendica\Object\Thread;
27 use Friendica\Util\DateTimeFormat;
28 use Friendica\Util\Proxy as ProxyUtils;
29 use Friendica\Util\Temporal;
30 use Friendica\Util\Strings;
31 use Friendica\Util\XML;
32 use Friendica\Util\Crypto;
33
34 function item_extract_images($body) {
35
36         $saved_image = [];
37         $orig_body = $body;
38         $new_body = '';
39
40         $cnt = 0;
41         $img_start = strpos($orig_body, '[img');
42         $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
43         $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
44         while (($img_st_close !== false) && ($img_end !== false)) {
45
46                 $img_st_close++; // make it point to AFTER the closing bracket
47                 $img_end += $img_start;
48
49                 if (!strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
50                         // This is an embedded image
51
52                         $saved_image[$cnt] = substr($orig_body, $img_start + $img_st_close, $img_end - ($img_start + $img_st_close));
53                         $new_body = $new_body . substr($orig_body, 0, $img_start) . '[!#saved_image' . $cnt . '#!]';
54
55                         $cnt++;
56                 } else {
57                         $new_body = $new_body . substr($orig_body, 0, $img_end + strlen('[/img]'));
58                 }
59
60                 $orig_body = substr($orig_body, $img_end + strlen('[/img]'));
61
62                 if ($orig_body === false) {
63                         // in case the body ends on a closing image tag
64                         $orig_body = '';
65                 }
66
67                 $img_start = strpos($orig_body, '[img');
68                 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
69                 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
70         }
71
72         $new_body = $new_body . $orig_body;
73
74         return ['body' => $new_body, 'images' => $saved_image];
75 }
76
77 function item_redir_and_replace_images($body, $images, $cid) {
78
79         $origbody = $body;
80         $newbody = '';
81
82         $cnt = 1;
83         $pos = BBCode::getTagPosition($origbody, 'url', 0);
84         while ($pos !== false && $cnt < 1000) {
85
86                 $search = '/\[url\=(.*?)\]\[!#saved_image([0-9]*)#!\]\[\/url\]' . '/is';
87                 $replace = '[url=' . System::baseUrl() . '/redir/' . $cid
88                                    . '?f=1&url=' . '$1' . '][!#saved_image' . '$2' .'#!][/url]';
89
90                 $newbody .= substr($origbody, 0, $pos['start']['open']);
91                 $subject = substr($origbody, $pos['start']['open'], $pos['end']['close'] - $pos['start']['open']);
92                 $origbody = substr($origbody, $pos['end']['close']);
93                 if ($origbody === false) {
94                         $origbody = '';
95                 }
96
97                 $subject = preg_replace($search, $replace, $subject);
98                 $newbody .= $subject;
99
100                 $cnt++;
101                 // Isn't this supposed to use $cnt value for $occurrences? - @MrPetovan
102                 $pos = BBCode::getTagPosition($origbody, 'url', 0);
103         }
104         $newbody .= $origbody;
105
106         $cnt = 0;
107         foreach ($images as $image) {
108                 /*
109                  * We're depending on the property of 'foreach' (specified on the PHP website) that
110                  * it loops over the array starting from the first element and going sequentially
111                  * to the last element.
112                  */
113                 $newbody = str_replace('[!#saved_image' . $cnt . '#!]', '[img]' . $image . '[/img]', $newbody);
114                 $cnt++;
115         }
116         return $newbody;
117 }
118
119 /**
120  * Render actions localized
121  *
122  * @param $item
123  * @throws ImagickException
124  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
125  */
126 function localize_item(&$item)
127 {
128         $extracted = item_extract_images($item['body']);
129         if ($extracted['images']) {
130                 $item['body'] = item_redir_and_replace_images($extracted['body'], $extracted['images'], $item['contact-id']);
131         }
132
133         /*
134         heluecht 2018-06-19: from my point of view this whole code part is useless.
135         It just renders the body message of technical posts (Like, dislike, ...).
136         But: The body isn't visible at all. So we do this stuff just because we can.
137         Even if these messages were visible, this would only mean that something went wrong.
138         During the further steps of the database restructuring I would like to address this issue.
139         */
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_OBJ_EVENT:
161                                                 $post_type = L10n::t('event');
162                                                 break;
163                                         default:
164                                                 $post_type = L10n::t('status');
165                                 }
166                                 break;
167                         default:
168                                 if ($obj['resource-id']) {
169                                         $post_type = L10n::t('photo');
170                                         $m = [];
171                                         preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
172                                         $rr['plink'] = $m[1];
173                                 } else {
174                                         $post_type = 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 = L10n::t('%1$s likes %2$s\'s %3$s');
183                 } elseif (activity_match($item['verb'], ACTIVITY_DISLIKE)) {
184                         $bodyverb = L10n::t('%1$s doesn\'t like %2$s\'s %3$s');
185                 } elseif (activity_match($item['verb'], ACTIVITY_ATTEND)) {
186                         $bodyverb = L10n::t('%1$s attends %2$s\'s %3$s');
187                 } elseif (activity_match($item['verb'], ACTIVITY_ATTENDNO)) {
188                         $bodyverb = L10n::t('%1$s doesn\'t attend %2$s\'s %3$s');
189                 } elseif (activity_match($item['verb'], ACTIVITY_ATTENDMAYBE)) {
190                         $bodyverb = 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_OBJ_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'] = 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_OBJ_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 = 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, 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_OBJ_EVENT:
298                                                 $post_type = L10n::t('event');
299                                                 break;
300                                         default:
301                                                 $post_type = L10n::t('status');
302                                 }
303                                 break;
304                         default:
305                                 if ($obj['resource-id']) {
306                                         $post_type = L10n::t('photo');
307                                         $m=[]; preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
308                                         $rr['plink'] = $m[1];
309                                 } else {
310                                         $post_type = 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'] = 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'] . ']' . L10n::t('post/item') . '[/url]';
343                                 $item['body'] = 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         /*
397          * likes (etc.) can apply to other things besides posts. Check if they are post children,
398          * in which case we handle them specially
399          */
400         $hidden_activities = [ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE, ACTIVITY_FOLLOW, ACTIVITY2_ANNOUNCE];
401         foreach ($hidden_activities as $act) {
402                 if (activity_match($item['verb'], $act)) {
403                         return false;
404                 }
405         }
406
407         // @TODO below if() block can be rewritten to a single line: $isVisible = allConditionsHere;
408         if (activity_match($item['verb'], ACTIVITY_FOLLOW) && $item['object-type'] === ACTIVITY_OBJ_NOTE && empty($item['self']) && $item['uid'] == local_user()) {
409                 return false;
410         }
411
412         return true;
413 }
414
415 function conv_get_blocklist()
416 {
417         if (!local_user()) {
418                 return [];
419         }
420
421         $str_blocked = PConfig::get(local_user(), 'system', 'blocked');
422         if (empty($str_blocked)) {
423                 return [];
424         }
425
426         $blocklist = [];
427
428         foreach (explode(',', $str_blocked) as $entry) {
429                 // The 4th parameter guarantees that there always will be a public contact entry
430                 $cid = Contact::getIdForURL(trim($entry), 0, true, ['url' => trim($entry)]);
431                 if (!empty($cid)) {
432                         $blocklist[] = $cid;
433                 }
434         }
435
436         return $blocklist;
437 }
438
439 /**
440  * "Render" a conversation or list of items for HTML display.
441  * There are two major forms of display:
442  *      - Sequential or unthreaded ("New Item View" or search results)
443  *      - conversation view
444  * The $mode parameter decides between the various renderings and also
445  * figures out how to determine page owner and other contextual items
446  * that are based on unique features of the calling module.
447  * @param App    $a
448  * @param array  $items
449  * @param Pager  $pager
450  * @param        $mode
451  * @param        $update
452  * @param bool   $preview
453  * @param string $order
454  * @param int    $uid
455  * @return string
456  * @throws ImagickException
457  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
458  */
459 function conversation(App $a, array $items, Pager $pager, $mode, $update, $preview = false, $order = 'commented', $uid = 0)
460 {
461         $ssl_state = (local_user() ? true : false);
462
463         $profile_owner = 0;
464         $live_update_div = '';
465
466         $blocklist = conv_get_blocklist();
467
468         $previewing = (($preview) ? ' preview ' : '');
469
470         if ($mode === 'network') {
471                 $items = conversation_add_children($items, false, $order, $uid);
472                 $profile_owner = local_user();
473                 if (!$update) {
474                         /*
475                          * The special div is needed for liveUpdate to kick in for this page.
476                          * We only launch liveUpdate if you aren't filtering in some incompatible
477                          * way and also you aren't writing a comment (discovered in javascript).
478                          */
479                         $live_update_div = '<div id="live-network"></div>' . "\r\n"
480                                 . "<script> var profile_uid = " . $_SESSION['uid']
481                                 . "; var netargs = '" . substr($a->cmd, 8)
482                                 . '?f='
483                                 . (!empty($_GET['cid'])    ? '&cid='    . rawurlencode($_GET['cid'])    : '')
484                                 . (!empty($_GET['search']) ? '&search=' . rawurlencode($_GET['search']) : '')
485                                 . (!empty($_GET['star'])   ? '&star='   . rawurlencode($_GET['star'])   : '')
486                                 . (!empty($_GET['order'])  ? '&order='  . rawurlencode($_GET['order'])  : '')
487                                 . (!empty($_GET['bmark'])  ? '&bmark='  . rawurlencode($_GET['bmark'])  : '')
488                                 . (!empty($_GET['liked'])  ? '&liked='  . rawurlencode($_GET['liked'])  : '')
489                                 . (!empty($_GET['conv'])   ? '&conv='   . rawurlencode($_GET['conv'])   : '')
490                                 . (!empty($_GET['nets'])   ? '&nets='   . rawurlencode($_GET['nets'])   : '')
491                                 . (!empty($_GET['cmin'])   ? '&cmin='   . rawurlencode($_GET['cmin'])   : '')
492                                 . (!empty($_GET['cmax'])   ? '&cmax='   . rawurlencode($_GET['cmax'])   : '')
493                                 . (!empty($_GET['file'])   ? '&file='   . rawurlencode($_GET['file'])   : '')
494
495                                 . "'; var profile_page = " . $pager->getPage() . "; </script>\r\n";
496                 }
497         } elseif ($mode === 'profile') {
498                 $items = conversation_add_children($items, false, $order, $uid);
499                 $profile_owner = $a->profile['profile_uid'];
500
501                 if (!$update) {
502                         $tab = 'posts';
503                         if (!empty($_GET['tab'])) {
504                                 $tab = Strings::escapeTags(trim($_GET['tab']));
505                         }
506                         if ($tab === 'posts') {
507                                 /*
508                                  * This is ugly, but we can't pass the profile_uid through the session to the ajax updater,
509                                  * because browser prefetching might change it on us. We have to deliver it with the page.
510                                  */
511
512                                 $live_update_div = '<div id="live-profile"></div>' . "\r\n"
513                                         . "<script> var profile_uid = " . $a->profile['profile_uid']
514                                         . "; var netargs = '?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
515                         }
516                 }
517         } elseif ($mode === 'notes') {
518                 $items = conversation_add_children($items, false, $order, local_user());
519                 $profile_owner = local_user();
520
521                 if (!$update) {
522                         $live_update_div = '<div id="live-notes"></div>' . "\r\n"
523                                 . "<script> var profile_uid = " . local_user()
524                                 . "; var netargs = '/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
525                 }
526         } elseif ($mode === 'display') {
527                 $items = conversation_add_children($items, false, $order, $uid);
528                 $profile_owner = $a->profile['uid'];
529
530                 if (!$update) {
531                         $live_update_div = '<div id="live-display"></div>' . "\r\n"
532                                 . "<script> var profile_uid = " . Session::get('uid', 0) . ";"
533                                 . " var profile_page = 1; </script>";
534                 }
535         } elseif ($mode === 'community') {
536                 $items = conversation_add_children($items, true, $order, $uid);
537                 $profile_owner = 0;
538
539                 if (!$update) {
540                         $live_update_div = '<div id="live-community"></div>' . "\r\n"
541                                 . "<script> var profile_uid = -1; var netargs = '" . substr($a->cmd, 10)
542                                 ."/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
543                 }
544         } elseif ($mode === 'contacts') {
545                 $items = conversation_add_children($items, false, $order, $uid);
546                 $profile_owner = 0;
547
548                 if (!$update) {
549                         $live_update_div = '<div id="live-contacts"></div>' . "\r\n"
550                                 . "<script> var profile_uid = -1; var netargs = '" . substr($a->cmd, 9)
551                                 ."/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
552                 }
553         } elseif ($mode === 'search') {
554                 $live_update_div = '<div id="live-search"></div>' . "\r\n";
555         }
556
557         $page_dropping = ((local_user() && local_user() == $profile_owner) ? true : false);
558
559         if (!$update) {
560                 $_SESSION['return_path'] = $a->query_string;
561         }
562
563         $cb = ['items' => $items, 'mode' => $mode, 'update' => $update, 'preview' => $preview];
564         Hook::callAll('conversation_start',$cb);
565
566         $items = $cb['items'];
567
568         $conv_responses = [
569                 'like' => ['title' => L10n::t('Likes','title')],
570                 'dislike' => ['title' => L10n::t('Dislikes','title')],
571                 'attendyes' => ['title' => L10n::t('Attending','title')],
572                 'attendno' => ['title' => L10n::t('Not attending','title')],
573                 'attendmaybe' => ['title' => L10n::t('Might attend','title')],
574                 'announce' => ['title' => L10n::t('Reshares','title')]
575         ];
576
577         // array with html for each thread (parent+comments)
578         $threads = [];
579         $threadsid = -1;
580
581         $page_template = Renderer::getMarkupTemplate("conversation.tpl");
582
583         if (!empty($items)) {
584                 if (in_array($mode, ['community', 'contacts'])) {
585                         $writable = true;
586                 } else {
587                         $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED);
588                 }
589
590                 if (!local_user()) {
591                         $writable = false;
592                 }
593
594                 if (in_array($mode, ['network-new', 'search', 'contact-posts'])) {
595
596                         /*
597                          * "New Item View" on network page or search page results
598                          * - just loop through the items and format them minimally for display
599                          */
600
601                         $tpl = 'search_item.tpl';
602
603                         foreach ($items as $item) {
604
605                                 if (!visible_activity($item)) {
606                                         continue;
607                                 }
608
609                                 if (in_array($item['author-id'], $blocklist)) {
610                                         continue;
611                                 }
612
613                                 $threadsid++;
614
615                                 $owner_url   = '';
616                                 $owner_name  = '';
617                                 $sparkle     = '';
618
619                                 // prevent private email from leaking.
620                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
621                                         continue;
622                                 }
623
624                                 $profile_name = $item['author-name'];
625                                 if (!empty($item['author-link']) && empty($item['author-name'])) {
626                                         $profile_name = $item['author-link'];
627                                 }
628
629                                 $tags = Term::populateTagsFromItem($item);
630
631                                 $author = ['uid' => 0, 'id' => $item['author-id'],
632                                         'network' => $item['author-network'], 'url' => $item['author-link']];
633                                 $profile_link = Contact::magicLinkByContact($author);
634
635                                 if (strpos($profile_link, 'redir/') === 0) {
636                                         $sparkle = ' sparkle';
637                                 }
638
639                                 $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
640                                 Hook::callAll('render_location',$locate);
641
642                                 $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
643
644                                 localize_item($item);
645                                 if ($mode === 'network-new') {
646                                         $dropping = true;
647                                 } else {
648                                         $dropping = false;
649                                 }
650
651                                 $drop = [
652                                         'dropping' => $dropping,
653                                         'pagedrop' => $page_dropping,
654                                         'select' => L10n::t('Select'),
655                                         'delete' => L10n::t('Delete'),
656                                 ];
657
658                                 $star = false;
659                                 $isstarred = "unstarred";
660
661                                 $lock = false;
662                                 $likebuttons = false;
663
664                                 $body = Item::prepareBody($item, true, $preview);
665
666                                 list($categories, $folders) = get_cats_and_terms($item);
667
668                                 if (!empty($item['content-warning']) && PConfig::get(local_user(), 'system', 'disable_cw', false)) {
669                                         $title = ucfirst($item['content-warning']);
670                                 } else {
671                                         $title = $item['title'];
672                                 }
673
674                                 $tmp_item = [
675                                         'template' => $tpl,
676                                         'id' => ($preview ? 'P0' : $item['id']),
677                                         'guid' => ($preview ? 'Q0' : $item['guid']),
678                                         'network' => $item['network'],
679                                         'network_name' => ContactSelector::networkToName($item['network'], $item['author-link']),
680                                         'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link']),
681                                         'linktitle' => L10n::t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
682                                         'profile_url' => $profile_link,
683                                         'item_photo_menu' => item_photo_menu($item),
684                                         'name' => $profile_name,
685                                         'sparkle' => $sparkle,
686                                         'lock' => $lock,
687                                         'thumb' => System::removedBaseUrl(ProxyUtils::proxifyUrl($item['author-avatar'], false, ProxyUtils::SIZE_THUMB)),
688                                         'title' => $title,
689                                         'body' => $body,
690                                         'tags' => $tags['tags'],
691                                         'hashtags' => $tags['hashtags'],
692                                         'mentions' => $tags['mentions'],
693                                         'implicit_mentions' => $tags['implicit_mentions'],
694                                         'txt_cats' => L10n::t('Categories:'),
695                                         'txt_folders' => L10n::t('Filed under:'),
696                                         'has_cats' => ((count($categories)) ? 'true' : ''),
697                                         'has_folders' => ((count($folders)) ? 'true' : ''),
698                                         'categories' => $categories,
699                                         'folders' => $folders,
700                                         'text' => strip_tags($body),
701                                         'localtime' => DateTimeFormat::local($item['created'], 'r'),
702                                         'ago' => (($item['app']) ? L10n::t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
703                                         'location' => $location,
704                                         'indent' => '',
705                                         'owner_name' => $owner_name,
706                                         'owner_url' => $owner_url,
707                                         'owner_photo' => System::removedBaseUrl(ProxyUtils::proxifyUrl($item['owner-avatar'], false, ProxyUtils::SIZE_THUMB)),
708                                         'plink' => Item::getPlink($item),
709                                         'edpost' => false,
710                                         'isstarred' => $isstarred,
711                                         'star' => $star,
712                                         'drop' => $drop,
713                                         'vote' => $likebuttons,
714                                         'like' => '',
715                                         'dislike' => '',
716                                         'comment' => '',
717                                         'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> L10n::t('View in context')]),
718                                         'previewing' => $previewing,
719                                         'wait' => L10n::t('Please wait'),
720                                         'thread_level' => 1,
721                                 ];
722
723                                 $arr = ['item' => $item, 'output' => $tmp_item];
724                                 Hook::callAll('display_item', $arr);
725
726                                 $threads[$threadsid]['id'] = $item['id'];
727                                 $threads[$threadsid]['network'] = $item['network'];
728                                 $threads[$threadsid]['items'] = [$arr['output']];
729
730                         }
731                 } else {
732                         // Normal View
733                         $page_template = Renderer::getMarkupTemplate("threaded_conversation.tpl");
734
735                         $conv = new Thread($mode, $preview, $writable);
736
737                         /*
738                          * get all the topmost parents
739                          * this shouldn't be needed, as we should have only them in our array
740                          * But for now, this array respects the old style, just in case
741                          */
742                         foreach ($items as $item) {
743                                 if (in_array($item['author-id'], $blocklist)) {
744                                         continue;
745                                 }
746
747                                 // Can we put this after the visibility check?
748                                 builtin_activity_puller($item, $conv_responses);
749
750                                 // Only add what is visible
751                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
752                                         continue;
753                                 }
754
755                                 if (!visible_activity($item)) {
756                                         continue;
757                                 }
758
759                                 /// @todo Check if this call is needed or not
760                                 $arr = ['item' => $item];
761                                 Hook::callAll('display_item', $arr);
762
763                                 $item['pagedrop'] = $page_dropping;
764
765                                 if ($item['id'] == $item['parent']) {
766                                         $item_object = new Post($item);
767                                         $conv->addParent($item_object);
768                                 }
769                         }
770
771                         $threads = $conv->getTemplateData($conv_responses);
772                         if (!$threads) {
773                                 Logger::log('[ERROR] conversation : Failed to get template data.', Logger::DEBUG);
774                                 $threads = [];
775                         }
776                 }
777         }
778
779         $o = Renderer::replaceMacros($page_template, [
780                 '$baseurl' => System::baseUrl($ssl_state),
781                 '$return_path' => $a->query_string,
782                 '$live_update' => $live_update_div,
783                 '$remove' => L10n::t('remove'),
784                 '$mode' => $mode,
785                 '$user' => $a->user,
786                 '$threads' => $threads,
787                 '$dropping' => ($page_dropping ? L10n::t('Delete Selected Items') : False),
788         ]);
789
790         return $o;
791 }
792
793 /**
794  * Fetch all comments from a query. Additionally set the newest resharer as thread owner.
795  *
796  * @param $thread_items Database statement with thread posts
797  * @return array items with parents and comments
798  */
799 function conversation_fetch_comments($thread_items) {
800         $comments = [];
801         $parentlines = [];
802         $lineno = 0;
803         $actor = [];
804         $received = '';
805
806         while ($row = Item::fetch($thread_items)) {
807                 if (($row['verb'] == ACTIVITY2_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                 $comments[] = $row;
818                 $lineno++;
819         }
820
821         DBA::close($thread_items);
822
823         if (!empty($actor)) {
824                 foreach ($parentlines as $line) {
825                         $comments[$line]['owner-link'] = $actor['link'];
826                         $comments[$line]['owner-avatar'] = $actor['avatar'];
827                         $comments[$line]['owner-name'] = $actor['name'];
828                 }
829         }
830         return $comments;
831 }
832
833 /**
834  * @brief Add comments to top level entries that had been fetched before
835  *
836  * The system will fetch the comments for the local user whenever possible.
837  * This behaviour is currently needed to allow commenting on Friendica posts.
838  *
839  * @param array $parents Parent items
840  *
841  * @param       $block_authors
842  * @param       $order
843  * @param       $uid
844  * @return array items with parents and comments
845  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
846  */
847 function conversation_add_children(array $parents, $block_authors, $order, $uid) {
848         $max_comments = Config::get('system', 'max_comments', 100);
849
850         $params = ['order' => ['uid', 'commented' => true]];
851
852         if ($max_comments > 0) {
853                 $params['limit'] = $max_comments;
854         }
855
856         $items = [];
857
858         foreach ($parents AS $parent) {
859                 $condition = ["`item`.`parent-uri` = ? AND `item`.`uid` IN (0, ?) ",
860                         $parent['uri'], $uid];
861                 if ($block_authors) {
862                         $condition[0] .= "AND NOT `author`.`hidden`";
863                 }
864
865                 $thread_items = Item::selectForUser(local_user(), array_merge(Item::DISPLAY_FIELDLIST, ['contact-uid', 'gravity']), $condition, $params);
866
867                 $comments = conversation_fetch_comments($thread_items);
868
869                 if (count($comments) != 0) {
870                         $items = array_merge($items, $comments);
871                 }
872         }
873
874         foreach ($items as $index => $item) {
875                 if ($item['uid'] == 0) {
876                         $items[$index]['writable'] = in_array($item['network'], Protocol::FEDERATED);
877                 }
878         }
879
880         $items = conv_sort($items, $order);
881
882         return $items;
883 }
884
885 function item_photo_menu($item) {
886         $sub_link = '';
887         $poke_link = '';
888         $contact_url = '';
889         $pm_url = '';
890         $status_link = '';
891         $photos_link = '';
892         $posts_link = '';
893         $block_link = '';
894         $ignore_link = '';
895
896         if (local_user() && local_user() == $item['uid'] && $item['parent'] == $item['id'] && !$item['self']) {
897                 $sub_link = 'javascript:dosubthread(' . $item['id'] . '); return false;';
898         }
899
900         $author = ['uid' => 0, 'id' => $item['author-id'],
901                 'network' => $item['author-network'], 'url' => $item['author-link']];
902         $profile_link = Contact::magicLinkByContact($author, $item['author-link']);
903         $sparkle = (strpos($profile_link, 'redir/') === 0);
904
905         $cid = 0;
906         $pcid = Contact::getIdForURL($item['author-link'], 0, true);
907         $network = '';
908         $rel = 0;
909         $condition = ['uid' => local_user(), 'nurl' => Strings::normaliseLink($item['author-link'])];
910         $contact = DBA::selectFirst('contact', ['id', 'network', 'rel'], $condition);
911         if (DBA::isResult($contact)) {
912                 $cid = $contact['id'];
913                 $network = $contact['network'];
914                 $rel = $contact['rel'];
915         }
916
917         if ($sparkle) {
918                 $status_link = $profile_link . '?tab=status';
919                 $photos_link = str_replace('/profile/', '/photos/', $profile_link);
920                 $profile_link = $profile_link . '?=profile';
921         }
922
923         if (!empty($pcid)) {
924                 $contact_url = 'contact/' . $pcid;
925                 $posts_link = 'contact/' . $pcid . '/posts';
926                 $block_link = 'contact/' . $pcid . '/block';
927                 $ignore_link = 'contact/' . $pcid . '/ignore';
928         }
929
930         if ($cid && !$item['self']) {
931                 $poke_link = 'poke/?f=&c=' . $cid;
932                 $contact_url = 'contact/' . $cid;
933                 $posts_link = 'contact/' . $cid . '/posts';
934
935                 if (in_array($network, [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA])) {
936                         $pm_url = 'message/new/' . $cid;
937                 }
938         }
939
940         if (local_user()) {
941                 $menu = [
942                         L10n::t('Follow Thread') => $sub_link,
943                         L10n::t('View Status') => $status_link,
944                         L10n::t('View Profile') => $profile_link,
945                         L10n::t('View Photos') => $photos_link,
946                         L10n::t('Network Posts') => $posts_link,
947                         L10n::t('View Contact') => $contact_url,
948                         L10n::t('Send PM') => $pm_url,
949                         L10n::t('Block') => $block_link,
950                         L10n::t('Ignore') => $ignore_link
951                 ];
952
953                 if ($network == Protocol::DFRN) {
954                         $menu[L10n::t("Poke")] = $poke_link;
955                 }
956
957                 if ((($cid == 0) || ($rel == Contact::FOLLOWER)) &&
958                         in_array($item['network'], Protocol::FEDERATED)) {
959                         $menu[L10n::t('Connect/Follow')] = 'follow?url=' . urlencode($item['author-link']);
960                 }
961         } else {
962                 $menu = [L10n::t('View Profile') => $item['author-link']];
963         }
964
965         $args = ['item' => $item, 'menu' => $menu];
966
967         Hook::callAll('item_photo_menu', $args);
968
969         $menu = $args['menu'];
970
971         $o = '';
972         foreach ($menu as $k => $v) {
973                 if (strpos($v, 'javascript:') === 0) {
974                         $v = substr($v, 11);
975                         $o .= '<li role="menuitem"><a onclick="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
976                 } elseif ($v!='') {
977                         $o .= '<li role="menuitem"><a href="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
978                 }
979         }
980         return $o;
981 }
982
983 /**
984  * @brief Checks item to see if it is one of the builtin activities (like/dislike, event attendance, consensus items, etc.)
985  * Increments the count of each matching activity and adds a link to the author as needed.
986  *
987  * @param array  $item
988  * @param array &$conv_responses (already created with builtin activity structure)
989  * @return void
990  * @throws ImagickException
991  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
992  */
993 function builtin_activity_puller($item, &$conv_responses) {
994         foreach ($conv_responses as $mode => $v) {
995                 $sparkle = '';
996
997                 switch ($mode) {
998                         case 'like':
999                                 $verb = ACTIVITY_LIKE;
1000                                 break;
1001                         case 'dislike':
1002                                 $verb = ACTIVITY_DISLIKE;
1003                                 break;
1004                         case 'attendyes':
1005                                 $verb = ACTIVITY_ATTEND;
1006                                 break;
1007                         case 'attendno':
1008                                 $verb = ACTIVITY_ATTENDNO;
1009                                 break;
1010                         case 'attendmaybe':
1011                                 $verb = ACTIVITY_ATTENDMAYBE;
1012                                 break;
1013                         case 'announce':
1014                                 $verb = ACTIVITY2_ANNOUNCE;
1015                                 break;
1016                         default:
1017                                 return;
1018                 }
1019
1020                 if (activity_match($item['verb'], $verb) && ($item['id'] != $item['parent'])) {
1021                         $author = ['uid' => 0, 'id' => $item['author-id'],
1022                                 'network' => $item['author-network'], 'url' => $item['author-link']];
1023                         $url = Contact::magicLinkByContact($author);
1024                         if (strpos($url, 'redir/') === 0) {
1025                                 $sparkle = ' class="sparkle" ';
1026                         }
1027
1028                         $url = '<a href="'. $url . '"'. $sparkle .'>' . htmlentities($item['author-name']) . '</a>';
1029
1030                         if (empty($item['thr-parent'])) {
1031                                 $item['thr-parent'] = $item['parent-uri'];
1032                         }
1033
1034                         if (!(isset($conv_responses[$mode][$item['thr-parent'] . '-l'])
1035                                 && is_array($conv_responses[$mode][$item['thr-parent'] . '-l']))) {
1036                                 $conv_responses[$mode][$item['thr-parent'] . '-l'] = [];
1037                         }
1038
1039                         // only list each unique author once
1040                         if (in_array($url,$conv_responses[$mode][$item['thr-parent'] . '-l'])) {
1041                                 continue;
1042                         }
1043
1044                         if (!isset($conv_responses[$mode][$item['thr-parent']])) {
1045                                 $conv_responses[$mode][$item['thr-parent']] = 1;
1046                         } else {
1047                                 $conv_responses[$mode][$item['thr-parent']] ++;
1048                         }
1049
1050                         if (public_contact() == $item['author-id']) {
1051                                 $conv_responses[$mode][$item['thr-parent'] . '-self'] = 1;
1052                         }
1053
1054                         $conv_responses[$mode][$item['thr-parent'] . '-l'][] = $url;
1055
1056                         // there can only be one activity verb per item so if we found anything, we can stop looking
1057                         return;
1058                 }
1059         }
1060 }
1061
1062 /**
1063  * Format the vote text for a profile item
1064  *
1065  * @param int    $cnt  = number of people who vote the item
1066  * @param array  $arr  = array of pre-linked names of likers/dislikers
1067  * @param string $type = one of 'like, 'dislike', 'attendyes', 'attendno', 'attendmaybe'
1068  * @param int    $id   = item id
1069  * @return string formatted text
1070  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1071  */
1072 function format_like($cnt, array $arr, $type, $id) {
1073         $o = '';
1074         $expanded = '';
1075         $phrase = '';
1076
1077         if ($cnt == 1) {
1078                 $likers = $arr[0];
1079
1080                 // Phrase if there is only one liker. In other cases it will be uses for the expanded
1081                 // list which show all likers
1082                 switch ($type) {
1083                         case 'like' :
1084                                 $phrase = L10n::t('%s likes this.', $likers);
1085                                 break;
1086                         case 'dislike' :
1087                                 $phrase = L10n::t('%s doesn\'t like this.', $likers);
1088                                 break;
1089                         case 'attendyes' :
1090                                 $phrase = L10n::t('%s attends.', $likers);
1091                                 break;
1092                         case 'attendno' :
1093                                 $phrase = L10n::t('%s doesn\'t attend.', $likers);
1094                                 break;
1095                         case 'attendmaybe' :
1096                                 $phrase = L10n::t('%s attends maybe.', $likers);
1097                                 break;
1098                         case 'announce' :
1099                                 $phrase = L10n::t('%s reshared this.', $likers);
1100                                 break;
1101                 }
1102         }
1103
1104         if ($cnt > 1) {
1105                 $total = count($arr);
1106                 if ($total < MAX_LIKERS) {
1107                         $last = L10n::t('and') . ' ' . $arr[count($arr)-1];
1108                         $arr2 = array_slice($arr, 0, -1);
1109                         $likers = implode(', ', $arr2) . ' ' . $last;
1110                 } else  {
1111                         $arr = array_slice($arr, 0, MAX_LIKERS - 1);
1112                         $likers = implode(', ', $arr);
1113                         $likers .= L10n::t('and %d other people', $total - MAX_LIKERS);
1114                 }
1115
1116                 $spanatts = "class=\"fakelink\" onclick=\"openClose('{$type}list-$id');\"";
1117
1118                 $explikers = '';
1119                 switch ($type) {
1120                         case 'like':
1121                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> like this', $spanatts, $cnt);
1122                                 $explikers = L10n::t('%s like this.', $likers);
1123                                 break;
1124                         case 'dislike':
1125                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> don\'t like this', $spanatts, $cnt);
1126                                 $explikers = L10n::t('%s don\'t like this.', $likers);
1127                                 break;
1128                         case 'attendyes':
1129                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> attend', $spanatts, $cnt);
1130                                 $explikers = L10n::t('%s attend.', $likers);
1131                                 break;
1132                         case 'attendno':
1133                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> don\'t attend', $spanatts, $cnt);
1134                                 $explikers = L10n::t('%s don\'t attend.', $likers);
1135                                 break;
1136                         case 'attendmaybe':
1137                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> attend maybe', $spanatts, $cnt);
1138                                 $explikers = L10n::t('%s attend maybe.', $likers);
1139                                 break;
1140                         case 'announce':
1141                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> reshared this', $spanatts, $cnt);
1142                                 $explikers = L10n::t('%s reshared this.', $likers);
1143                                 break;
1144                 }
1145
1146                 $expanded .= "\t" . '<p class="wall-item-' . $type . '-expanded" id="' . $type . 'list-' . $id . '" style="display: none;" >' . $explikers . EOL . '</p>';
1147         }
1148
1149         $o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('voting_fakelink.tpl'), [
1150                 '$phrase' => $phrase,
1151                 '$type' => $type,
1152                 '$id' => $id
1153         ]);
1154         $o .= $expanded;
1155
1156         return $o;
1157 }
1158
1159 function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
1160 {
1161         $o = '';
1162
1163         $geotag = !empty($x['allow_location']) ? Renderer::replaceMacros(Renderer::getMarkupTemplate('jot_geotag.tpl'), []) : '';
1164
1165         $tpl = Renderer::getMarkupTemplate('jot-header.tpl');
1166         $a->page['htmlhead'] .= Renderer::replaceMacros($tpl, [
1167                 '$newpost'   => 'true',
1168                 '$baseurl'   => System::baseUrl(true),
1169                 '$geotag'    => $geotag,
1170                 '$nickname'  => $x['nickname'],
1171                 '$ispublic'  => L10n::t('Visible to <strong>everybody</strong>'),
1172                 '$linkurl'   => L10n::t('Please enter a image/video/audio/webpage URL:'),
1173                 '$term'      => L10n::t('Tag term:'),
1174                 '$fileas'    => L10n::t('Save to Folder:'),
1175                 '$whereareu' => L10n::t('Where are you right now?'),
1176                 '$delitems'  => L10n::t("Delete item\x28s\x29?")
1177         ]);
1178
1179         $jotplugins = '';
1180         Hook::callAll('jot_tool', $jotplugins);
1181
1182         // Private/public post links for the non-JS ACL form
1183         $private_post = 1;
1184         if (!empty($_REQUEST['public'])) {
1185                 $private_post = 0;
1186         }
1187
1188         $query_str = $a->query_string;
1189         if (strpos($query_str, 'public=1') !== false) {
1190                 $query_str = str_replace(['?public=1', '&public=1'], ['', ''], $query_str);
1191         }
1192
1193         /*
1194          * I think $a->query_string may never have ? in it, but I could be wrong
1195          * It looks like it's from the index.php?q=[etc] rewrite that the web
1196          * server does, which converts any ? to &, e.g. suggest&ignore=61 for suggest?ignore=61
1197          */
1198         if (strpos($query_str, '?') === false) {
1199                 $public_post_link = '?public=1';
1200         } else {
1201                 $public_post_link = '&public=1';
1202         }
1203
1204         // $tpl = Renderer::replaceMacros($tpl,array('$jotplugins' => $jotplugins));
1205         $tpl = Renderer::getMarkupTemplate("jot.tpl");
1206
1207         $o .= Renderer::replaceMacros($tpl,[
1208                 '$new_post' => L10n::t('New Post'),
1209                 '$return_path'  => $query_str,
1210                 '$action'       => 'item',
1211                 '$share'        => ($x['button'] ?? '') ?: L10n::t('Share'),
1212                 '$upload'       => L10n::t('Upload photo'),
1213                 '$shortupload'  => L10n::t('upload photo'),
1214                 '$attach'       => L10n::t('Attach file'),
1215                 '$shortattach'  => L10n::t('attach file'),
1216                 '$edbold'       => L10n::t('Bold'),
1217                 '$editalic'     => L10n::t('Italic'),
1218                 '$eduline'      => L10n::t('Underline'),
1219                 '$edquote'      => L10n::t('Quote'),
1220                 '$edcode'       => L10n::t('Code'),
1221                 '$edimg'        => L10n::t('Image'),
1222                 '$edurl'        => L10n::t('Link'),
1223                 '$edattach'     => L10n::t('Link or Media'),
1224                 '$setloc'       => L10n::t('Set your location'),
1225                 '$shortsetloc'  => L10n::t('set location'),
1226                 '$noloc'        => L10n::t('Clear browser location'),
1227                 '$shortnoloc'   => L10n::t('clear location'),
1228                 '$title'        => $x['title'] ?? '',
1229                 '$placeholdertitle' => L10n::t('Set title'),
1230                 '$category'     => $x['category'] ?? '',
1231                 '$placeholdercategory' => Feature::isEnabled(local_user(), 'categories') ? L10n::t("Categories \x28comma-separated list\x29") : '',
1232                 '$wait'         => L10n::t('Please wait'),
1233                 '$permset'      => L10n::t('Permission settings'),
1234                 '$shortpermset' => L10n::t('permissions'),
1235                 '$wall'         => $notes_cid ? 0 : 1,
1236                 '$posttype'     => $notes_cid ? Item::PT_PERSONAL_NOTE : Item::PT_ARTICLE,
1237                 '$content'      => $x['content'] ?? '',
1238                 '$post_id'      => $x['post_id'] ?? '',
1239                 '$baseurl'      => System::baseUrl(true),
1240                 '$defloc'       => $x['default_location'],
1241                 '$visitor'      => $x['visitor'],
1242                 '$pvisit'       => $notes_cid ? 'none' : $x['visitor'],
1243                 '$public'       => L10n::t('Public post'),
1244                 '$lockstate'    => $x['lockstate'],
1245                 '$bang'         => $x['bang'],
1246                 '$profile_uid'  => $x['profile_uid'],
1247                 '$preview'      => L10n::t('Preview'),
1248                 '$jotplugins'   => $jotplugins,
1249                 '$notes_cid'    => $notes_cid,
1250                 '$sourceapp'    => L10n::t($a->sourcename),
1251                 '$cancel'       => L10n::t('Cancel'),
1252                 '$rand_num'     => Crypto::randomDigits(12),
1253
1254                 // ACL permissions box
1255                 '$acl'           => $x['acl'],
1256                 '$group_perms'   => L10n::t('Post to Groups'),
1257                 '$contact_perms' => L10n::t('Post to Contacts'),
1258                 '$private'       => L10n::t('Private post'),
1259                 '$is_private'    => $private_post,
1260                 '$public_link'   => $public_post_link,
1261
1262                 //jot nav tab (used in some themes)
1263                 '$message' => L10n::t('Message'),
1264                 '$browser' => L10n::t('Browser'),
1265         ]);
1266
1267
1268         if ($popup == true) {
1269                 $o = '<div id="jot-popup" style="display: none;">' . $o . '</div>';
1270         }
1271
1272         return $o;
1273 }
1274
1275 /**
1276  * Plucks the children of the given parent from a given item list.
1277  *
1278  * @brief Plucks all the children in the given item list of the given parent
1279  *
1280  * @param array $item_list
1281  * @param array $parent
1282  * @param bool  $recursive
1283  * @return array
1284  */
1285 function get_item_children(array &$item_list, array $parent, $recursive = true)
1286 {
1287         $children = [];
1288         foreach ($item_list as $i => $item) {
1289                 if ($item['id'] != $item['parent']) {
1290                         if ($recursive) {
1291                                 // Fallback to parent-uri if thr-parent is not set
1292                                 $thr_parent = $item['thr-parent'];
1293                                 if ($thr_parent == '') {
1294                                         $thr_parent = $item['parent-uri'];
1295                                 }
1296
1297                                 if ($thr_parent == $parent['uri']) {
1298                                         $item['children'] = get_item_children($item_list, $item);
1299                                         $children[] = $item;
1300                                         unset($item_list[$i]);
1301                                 }
1302                         } elseif ($item['parent'] == $parent['id']) {
1303                                 $children[] = $item;
1304                                 unset($item_list[$i]);
1305                         }
1306                 }
1307         }
1308         return $children;
1309 }
1310
1311 /**
1312  * @brief Recursively sorts a tree-like item array
1313  *
1314  * @param array $items
1315  * @return array
1316  */
1317 function sort_item_children(array $items)
1318 {
1319         $result = $items;
1320         usort($result, 'sort_thr_received_rev');
1321         foreach ($result as $k => $i) {
1322                 if (isset($result[$k]['children'])) {
1323                         $result[$k]['children'] = sort_item_children($result[$k]['children']);
1324                 }
1325         }
1326         return $result;
1327 }
1328
1329 /**
1330  * @brief Recursively add all children items at the top level of a list
1331  *
1332  * @param array $children List of items to append
1333  * @param array $item_list
1334  */
1335 function add_children_to_list(array $children, array &$item_list)
1336 {
1337         foreach ($children as $child) {
1338                 $item_list[] = $child;
1339                 if (isset($child['children'])) {
1340                         add_children_to_list($child['children'], $item_list);
1341                 }
1342         }
1343 }
1344
1345 /**
1346  * This recursive function takes the item tree structure created by conv_sort() and
1347  * flatten the extraneous depth levels when people reply sequentially, removing the
1348  * stairs effect in threaded conversations limiting the available content width.
1349  *
1350  * The basic principle is the following: if a post item has only one reply and is
1351  * the last reply of its parent, then the reply is moved to the parent.
1352  *
1353  * This process is rendered somewhat more complicated because items can be either
1354  * replies or likes, and these don't factor at all in the reply count/last reply.
1355  *
1356  * @brief Selectively flattens a tree-like item structure to prevent threading stairs
1357  *
1358  * @param array $parent A tree-like array of items
1359  * @return array
1360  */
1361 function smart_flatten_conversation(array $parent)
1362 {
1363         if (!isset($parent['children']) || count($parent['children']) == 0) {
1364                 return $parent;
1365         }
1366
1367         // We use a for loop to ensure we process the newly-moved items
1368         for ($i = 0; $i < count($parent['children']); $i++) {
1369                 $child = $parent['children'][$i];
1370
1371                 if (isset($child['children']) && count($child['children'])) {
1372                         // This helps counting only the regular posts
1373                         $count_post_closure = function($var) {
1374                                 return $var['verb'] === ACTIVITY_POST;
1375                         };
1376
1377                         $child_post_count = count(array_filter($child['children'], $count_post_closure));
1378
1379                         $remaining_post_count = count(array_filter(array_slice($parent['children'], $i), $count_post_closure));
1380
1381                         // If there's only one child's children post and this is the last child post
1382                         if ($child_post_count == 1 && $remaining_post_count == 1) {
1383
1384                                 // Searches the post item in the children
1385                                 $j = 0;
1386                                 while($child['children'][$j]['verb'] !== ACTIVITY_POST && $j < count($child['children'])) {
1387                                         $j ++;
1388                                 }
1389
1390                                 $moved_item = $child['children'][$j];
1391                                 unset($parent['children'][$i]['children'][$j]);
1392                                 $parent['children'][] = $moved_item;
1393                         } else {
1394                                 $parent['children'][$i] = smart_flatten_conversation($child);
1395                         }
1396                 }
1397         }
1398
1399         return $parent;
1400 }
1401
1402
1403 /**
1404  * Expands a flat list of items into corresponding tree-like conversation structures,
1405  * sort the top-level posts either on "received" or "commented", and finally
1406  * append all the items at the top level (???)
1407  *
1408  * @brief Expands a flat item list into a conversation array for display
1409  *
1410  * @param array  $item_list A list of items belonging to one or more conversations
1411  * @param string $order     Either on "received" or "commented"
1412  * @return array
1413  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1414  */
1415 function conv_sort(array $item_list, $order)
1416 {
1417         $parents = [];
1418
1419         if (!(is_array($item_list) && count($item_list))) {
1420                 return $parents;
1421         }
1422
1423         $blocklist = conv_get_blocklist();
1424
1425         $item_array = [];
1426
1427         // Dedupes the item list on the uri to prevent infinite loops
1428         foreach ($item_list as $item) {
1429                 if (in_array($item['author-id'], $blocklist)) {
1430                         continue;
1431                 }
1432
1433                 $item_array[$item['uri']] = $item;
1434         }
1435
1436         // Extract the top level items
1437         foreach ($item_array as $item) {
1438                 if ($item['id'] == $item['parent']) {
1439                         $parents[] = $item;
1440                 }
1441         }
1442
1443         if (stristr($order, 'received')) {
1444                 usort($parents, 'sort_thr_received');
1445         } elseif (stristr($order, 'commented')) {
1446                 usort($parents, 'sort_thr_commented');
1447         }
1448
1449         /*
1450          * Plucks children from the item_array, second pass collects eventual orphan
1451          * items and add them as children of their top-level post.
1452          */
1453         foreach ($parents as $i => $parent) {
1454                 $parents[$i]['children'] =
1455                         array_merge(get_item_children($item_array, $parent, true),
1456                                 get_item_children($item_array, $parent, false));
1457         }
1458
1459         foreach ($parents as $i => $parent) {
1460                 $parents[$i]['children'] = sort_item_children($parents[$i]['children']);
1461         }
1462
1463         if (PConfig::get(local_user(), 'system', 'smart_threading', 0)) {
1464                 foreach ($parents as $i => $parent) {
1465                         $parents[$i] = smart_flatten_conversation($parent);
1466                 }
1467         }
1468
1469         /// @TODO: Stop recusrsively adding all children back to the top level (!!!)
1470         /// However, this apparently ensures responses (likes, attendance) display (?!)
1471         foreach ($parents as $parent) {
1472                 if (count($parent['children'])) {
1473                         add_children_to_list($parent['children'], $parents);
1474                 }
1475         }
1476
1477         return $parents;
1478 }
1479
1480 /**
1481  * @brief usort() callback to sort item arrays by the received key
1482  *
1483  * @param array $a
1484  * @param array $b
1485  * @return int
1486  */
1487 function sort_thr_received(array $a, array $b)
1488 {
1489         return strcmp($b['received'], $a['received']);
1490 }
1491
1492 /**
1493  * @brief usort() callback to reverse sort item arrays by the received key
1494  *
1495  * @param array $a
1496  * @param array $b
1497  * @return int
1498  */
1499 function sort_thr_received_rev(array $a, array $b)
1500 {
1501         return strcmp($a['received'], $b['received']);
1502 }
1503
1504 /**
1505  * @brief usort() callback to sort item arrays by the commented key
1506  *
1507  * @param array $a
1508  * @param array $b
1509  * @return int
1510  */
1511 function sort_thr_commented(array $a, array $b)
1512 {
1513         return strcmp($b['commented'], $a['commented']);
1514 }
1515
1516 function render_location_dummy(array $item) {
1517         if (!empty($item['location']) && !empty($item['location'])) {
1518                 return $item['location'];
1519         }
1520
1521         if (!empty($item['coord']) && !empty($item['coord'])) {
1522                 return $item['coord'];
1523         }
1524 }
1525
1526 function get_responses(array $conv_responses, array $response_verbs, array $item, Post $ob = null) {
1527         $ret = [];
1528         foreach ($response_verbs as $v) {
1529                 $ret[$v] = [];
1530                 $ret[$v]['count'] = $conv_responses[$v][$item['uri']] ?? 0;
1531                 $ret[$v]['list']  = $conv_responses[$v][$item['uri'] . '-l'] ?? [];
1532                 $ret[$v]['self']  = $conv_responses[$v][$item['uri'] . '-self'] ?? '0';
1533                 if (count($ret[$v]['list']) > MAX_LIKERS) {
1534                         $ret[$v]['list_part'] = array_slice($ret[$v]['list'], 0, MAX_LIKERS);
1535                         array_push($ret[$v]['list_part'], '<a href="#" data-toggle="modal" data-target="#' . $v . 'Modal-'
1536                                 . (($ob) ? $ob->getId() : $item['id']) . '"><b>' . L10n::t('View all') . '</b></a>');
1537                 } else {
1538                         $ret[$v]['list_part'] = '';
1539                 }
1540                 $ret[$v]['button'] = get_response_button_text($v, $ret[$v]['count']);
1541                 $ret[$v]['title'] = $conv_responses[$v]['title'];
1542         }
1543
1544         $count = 0;
1545         foreach ($ret as $key) {
1546                 if ($key['count'] == true) {
1547                         $count++;
1548                 }
1549         }
1550         $ret['count'] = $count;
1551
1552         return $ret;
1553 }
1554
1555 function get_response_button_text($v, $count)
1556 {
1557         $return = '';
1558         switch ($v) {
1559                 case 'like':
1560                         $return = L10n::tt('Like', 'Likes', $count);
1561                         break;
1562                 case 'dislike':
1563                         $return = L10n::tt('Dislike', 'Dislikes', $count);
1564                         break;
1565                 case 'attendyes':
1566                         $return = L10n::tt('Attending', 'Attending', $count);
1567                         break;
1568                 case 'attendno':
1569                         $return = L10n::tt('Not Attending', 'Not Attending', $count);
1570                         break;
1571                 case 'attendmaybe':
1572                         $return = L10n::tt('Undecided', 'Undecided', $count);
1573                         break;
1574         }
1575
1576         return $return;
1577 }