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