]> git.mxchange.org Git - friendica.git/blob - include/conversation.php
Remove unused conversation response code
[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'        => [],
583                 'dislike'     => [],
584                 'attendyes'   => [],
585                 'attendno'    => [],
586                 'attendmaybe' => []
587         ];
588
589         if (DI::pConfig()->get(local_user(), 'system', 'hide_dislike')) {
590                 unset($conv_responses['dislike']);
591         }
592
593         // array with html for each thread (parent+comments)
594         $threads = [];
595         $threadsid = -1;
596
597         $page_template = Renderer::getMarkupTemplate("conversation.tpl");
598
599         if (!empty($items)) {
600                 if (in_array($mode, ['community', 'contacts'])) {
601                         $writable = true;
602                 } else {
603                         $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED);
604                 }
605
606                 if (!local_user()) {
607                         $writable = false;
608                 }
609
610                 if (in_array($mode, ['network-new', 'search', 'contact-posts'])) {
611
612                         /*
613                          * "New Item View" on network page or search page results
614                          * - just loop through the items and format them minimally for display
615                          */
616
617                         $tpl = 'search_item.tpl';
618
619                         foreach ($items as $item) {
620
621                                 if (!visible_activity($item)) {
622                                         continue;
623                                 }
624
625                                 if (in_array($item['author-id'], $blocklist)) {
626                                         continue;
627                                 }
628
629                                 $threadsid++;
630
631                                 $owner_url   = '';
632                                 $owner_name  = '';
633                                 $sparkle     = '';
634
635                                 // prevent private email from leaking.
636                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
637                                         continue;
638                                 }
639
640                                 $profile_name = $item['author-name'];
641                                 if (!empty($item['author-link']) && empty($item['author-name'])) {
642                                         $profile_name = $item['author-link'];
643                                 }
644
645                                 $tags = Term::populateTagsFromItem($item);
646
647                                 $author = ['uid' => 0, 'id' => $item['author-id'],
648                                         'network' => $item['author-network'], 'url' => $item['author-link']];
649                                 $profile_link = Contact::magicLinkByContact($author);
650
651                                 if (strpos($profile_link, 'redir/') === 0) {
652                                         $sparkle = ' sparkle';
653                                 }
654
655                                 $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
656                                 Hook::callAll('render_location',$locate);
657
658                                 $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
659
660                                 localize_item($item);
661                                 if ($mode === 'network-new') {
662                                         $dropping = true;
663                                 } else {
664                                         $dropping = false;
665                                 }
666
667                                 $drop = [
668                                         'dropping' => $dropping,
669                                         'pagedrop' => $page_dropping,
670                                         'select' => DI::l10n()->t('Select'),
671                                         'delete' => DI::l10n()->t('Delete'),
672                                 ];
673
674                                 $star = false;
675                                 $isstarred = "unstarred";
676
677                                 $lock = false;
678                                 $likebuttons = [
679                                         'like'    => null,
680                                         'dislike' => null,
681                                         'share'   => null,
682                                 ];
683
684                                 if (DI::pConfig()->get(local_user(), 'system', 'hide_dislike')) {
685                                         unset($likebuttons['dislike']);
686                                 }
687
688                                 $body = Item::prepareBody($item, true, $preview);
689
690                                 list($categories, $folders) = DI::contentItem()->determineCategoriesTerms($item);
691
692                                 if (!empty($item['content-warning']) && DI::pConfig()->get(local_user(), 'system', 'disable_cw', false)) {
693                                         $title = ucfirst($item['content-warning']);
694                                 } else {
695                                         $title = $item['title'];
696                                 }
697
698                                 $tmp_item = [
699                                         'template' => $tpl,
700                                         'id' => ($preview ? 'P0' : $item['id']),
701                                         'guid' => ($preview ? 'Q0' : $item['guid']),
702                                         'network' => $item['network'],
703                                         'network_name' => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']),
704                                         'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link']),
705                                         'linktitle' => DI::l10n()->t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
706                                         'profile_url' => $profile_link,
707                                         'item_photo_menu' => item_photo_menu($item),
708                                         'name' => $profile_name,
709                                         'sparkle' => $sparkle,
710                                         'lock' => $lock,
711                                         'thumb' => DI::baseUrl()->remove(ProxyUtils::proxifyUrl($item['author-avatar'], false, ProxyUtils::SIZE_THUMB)),
712                                         'title' => $title,
713                                         'body' => $body,
714                                         'tags' => $tags['tags'],
715                                         'hashtags' => $tags['hashtags'],
716                                         'mentions' => $tags['mentions'],
717                                         'implicit_mentions' => $tags['implicit_mentions'],
718                                         'txt_cats' => DI::l10n()->t('Categories:'),
719                                         'txt_folders' => DI::l10n()->t('Filed under:'),
720                                         'has_cats' => ((count($categories)) ? 'true' : ''),
721                                         'has_folders' => ((count($folders)) ? 'true' : ''),
722                                         'categories' => $categories,
723                                         'folders' => $folders,
724                                         'text' => strip_tags($body),
725                                         'localtime' => DateTimeFormat::local($item['created'], 'r'),
726                                         'ago' => (($item['app']) ? DI::l10n()->t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
727                                         'location' => $location,
728                                         'indent' => '',
729                                         'owner_name' => $owner_name,
730                                         'owner_url' => $owner_url,
731                                         'owner_photo' => DI::baseUrl()->remove(ProxyUtils::proxifyUrl($item['owner-avatar'], false, ProxyUtils::SIZE_THUMB)),
732                                         'plink' => Item::getPlink($item),
733                                         'edpost' => false,
734                                         'isstarred' => $isstarred,
735                                         'star' => $star,
736                                         'drop' => $drop,
737                                         'vote' => $likebuttons,
738                                         'like' => '',
739                                         'dislike' => '',
740                                         'comment' => '',
741                                         'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> DI::l10n()->t('View in context')]),
742                                         'previewing' => $previewing,
743                                         'wait' => DI::l10n()->t('Please wait'),
744                                         'thread_level' => 1,
745                                 ];
746
747                                 $arr = ['item' => $item, 'output' => $tmp_item];
748                                 Hook::callAll('display_item', $arr);
749
750                                 $threads[$threadsid]['id'] = $item['id'];
751                                 $threads[$threadsid]['network'] = $item['network'];
752                                 $threads[$threadsid]['items'] = [$arr['output']];
753
754                         }
755                 } else {
756                         // Normal View
757                         $page_template = Renderer::getMarkupTemplate("threaded_conversation.tpl");
758
759                         $conv = new Thread($mode, $preview, $writable);
760
761                         /*
762                          * get all the topmost parents
763                          * this shouldn't be needed, as we should have only them in our array
764                          * But for now, this array respects the old style, just in case
765                          */
766                         foreach ($items as $item) {
767                                 if (in_array($item['author-id'], $blocklist)) {
768                                         continue;
769                                 }
770
771                                 // Can we put this after the visibility check?
772                                 builtin_activity_puller($item, $conv_responses);
773
774                                 // Only add what is visible
775                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
776                                         continue;
777                                 }
778
779                                 if (!visible_activity($item)) {
780                                         continue;
781                                 }
782
783                                 /// @todo Check if this call is needed or not
784                                 $arr = ['item' => $item];
785                                 Hook::callAll('display_item', $arr);
786
787                                 $item['pagedrop'] = $page_dropping;
788
789                                 if ($item['id'] == $item['parent']) {
790                                         $item_object = new Post($item);
791                                         $conv->addParent($item_object);
792                                 }
793                         }
794
795                         $threads = $conv->getTemplateData($conv_responses);
796                         if (!$threads) {
797                                 Logger::log('[ERROR] conversation : Failed to get template data.', Logger::DEBUG);
798                                 $threads = [];
799                         }
800                 }
801         }
802
803         $o = Renderer::replaceMacros($page_template, [
804                 '$baseurl' => DI::baseUrl()->get($ssl_state),
805                 '$return_path' => DI::args()->getQueryString(),
806                 '$live_update' => $live_update_div,
807                 '$remove' => DI::l10n()->t('remove'),
808                 '$mode' => $mode,
809                 '$user' => $a->user,
810                 '$threads' => $threads,
811                 '$dropping' => ($page_dropping ? DI::l10n()->t('Delete Selected Items') : False),
812         ]);
813
814         return $o;
815 }
816
817 /**
818  * Fetch all comments from a query. Additionally set the newest resharer as thread owner.
819  *
820  * @param mixed   $thread_items Database statement with thread posts
821  * @param boolean $pinned       Is the item pinned?
822  *
823  * @return array items with parents and comments
824  */
825 function conversation_fetch_comments($thread_items, $pinned) {
826         $comments = [];
827         $parentlines = [];
828         $lineno = 0;
829         $actor = [];
830         $received = '';
831
832         while ($row = Item::fetch($thread_items)) {
833                 if (($row['verb'] == Activity::ANNOUNCE) && !empty($row['contact-uid']) && ($row['received'] > $received) && ($row['thr-parent'] == $row['parent-uri'])) {
834                         $actor = ['link' => $row['author-link'], 'avatar' => $row['author-avatar'], 'name' => $row['author-name']];
835                         $received = $row['received'];
836                 }
837
838                 if ((($row['gravity'] == GRAVITY_PARENT) && !$row['origin'] && !in_array($row['network'], [Protocol::DIASPORA])) &&
839                         (empty($row['contact-uid']) || !in_array($row['network'], Protocol::NATIVE_SUPPORT))) {
840                         $parentlines[] = $lineno;
841                 }
842
843                 if ($row['gravity'] == GRAVITY_PARENT) {
844                         $row['pinned'] = $pinned;
845                 }
846
847                 $comments[] = $row;
848                 $lineno++;
849         }
850
851         DBA::close($thread_items);
852
853         if (!empty($actor)) {
854                 foreach ($parentlines as $line) {
855                         $comments[$line]['owner-link'] = $actor['link'];
856                         $comments[$line]['owner-avatar'] = $actor['avatar'];
857                         $comments[$line]['owner-name'] = $actor['name'];
858                 }
859         }
860         return $comments;
861 }
862
863 /**
864  * Add comments to top level entries that had been fetched before
865  *
866  * The system will fetch the comments for the local user whenever possible.
867  * This behaviour is currently needed to allow commenting on Friendica posts.
868  *
869  * @param array $parents Parent items
870  *
871  * @param       $block_authors
872  * @param       $order
873  * @param       $uid
874  * @return array items with parents and comments
875  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
876  */
877 function conversation_add_children(array $parents, $block_authors, $order, $uid) {
878         $max_comments = DI::config()->get('system', 'max_comments', 100);
879
880         $params = ['order' => ['uid', 'commented' => true]];
881
882         if ($max_comments > 0) {
883                 $params['limit'] = $max_comments;
884         }
885
886         $items = [];
887
888         foreach ($parents AS $parent) {
889                 $condition = ["`item`.`parent-uri` = ? AND `item`.`uid` IN (0, ?) ",
890                         $parent['uri'], $uid];
891                 if ($block_authors) {
892                         $condition[0] .= "AND NOT `author`.`hidden`";
893                 }
894
895                 $thread_items = Item::selectForUser(local_user(), array_merge(Item::DISPLAY_FIELDLIST, ['contact-uid', 'gravity']), $condition, $params);
896
897                 $comments = conversation_fetch_comments($thread_items, $parent['pinned'] ?? false);
898
899                 if (count($comments) != 0) {
900                         $items = array_merge($items, $comments);
901                 }
902         }
903
904         foreach ($items as $index => $item) {
905                 if ($item['uid'] == 0) {
906                         $items[$index]['writable'] = in_array($item['network'], Protocol::FEDERATED);
907                 }
908         }
909
910         $items = conv_sort($items, $order);
911
912         return $items;
913 }
914
915 function item_photo_menu($item) {
916         $sub_link = '';
917         $poke_link = '';
918         $contact_url = '';
919         $pm_url = '';
920         $status_link = '';
921         $photos_link = '';
922         $posts_link = '';
923         $block_link = '';
924         $ignore_link = '';
925
926         if (local_user() && local_user() == $item['uid'] && $item['parent'] == $item['id'] && !$item['self']) {
927                 $sub_link = 'javascript:dosubthread(' . $item['id'] . '); return false;';
928         }
929
930         $author = ['uid' => 0, 'id' => $item['author-id'],
931                 'network' => $item['author-network'], 'url' => $item['author-link']];
932         $profile_link = Contact::magicLinkByContact($author, $item['author-link']);
933         $sparkle = (strpos($profile_link, 'redir/') === 0);
934
935         $cid = 0;
936         $pcid = Contact::getIdForURL($item['author-link'], 0, true);
937         $network = '';
938         $rel = 0;
939         $condition = ['uid' => local_user(), 'nurl' => Strings::normaliseLink($item['author-link'])];
940         $contact = DBA::selectFirst('contact', ['id', 'network', 'rel'], $condition);
941         if (DBA::isResult($contact)) {
942                 $cid = $contact['id'];
943                 $network = $contact['network'];
944                 $rel = $contact['rel'];
945         }
946
947         if ($sparkle) {
948                 $status_link = $profile_link . '/status';
949                 $photos_link = str_replace('/profile/', '/photos/', $profile_link);
950                 $profile_link = $profile_link . '/profile';
951         }
952
953         if (!empty($pcid)) {
954                 $contact_url = 'contact/' . $pcid;
955                 $posts_link = 'contact/' . $pcid . '/posts';
956                 $block_link = 'contact/' . $pcid . '/block';
957                 $ignore_link = 'contact/' . $pcid . '/ignore';
958         }
959
960         if ($cid && !$item['self']) {
961                 $poke_link = 'poke?c=' . $cid;
962                 $contact_url = 'contact/' . $cid;
963                 $posts_link = 'contact/' . $cid . '/posts';
964
965                 if (in_array($network, [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA])) {
966                         $pm_url = 'message/new/' . $cid;
967                 }
968         }
969
970         if (local_user()) {
971                 $menu = [
972                         DI::l10n()->t('Follow Thread') => $sub_link,
973                         DI::l10n()->t('View Status') => $status_link,
974                         DI::l10n()->t('View Profile') => $profile_link,
975                         DI::l10n()->t('View Photos') => $photos_link,
976                         DI::l10n()->t('Network Posts') => $posts_link,
977                         DI::l10n()->t('View Contact') => $contact_url,
978                         DI::l10n()->t('Send PM') => $pm_url,
979                         DI::l10n()->t('Block') => $block_link,
980                         DI::l10n()->t('Ignore') => $ignore_link
981                 ];
982
983                 if ($network == Protocol::DFRN) {
984                         $menu[DI::l10n()->t("Poke")] = $poke_link;
985                 }
986
987                 if ((($cid == 0) || ($rel == Contact::FOLLOWER)) &&
988                         in_array($item['network'], Protocol::FEDERATED)) {
989                         $menu[DI::l10n()->t('Connect/Follow')] = 'follow?url=' . urlencode($item['author-link']);
990                 }
991         } else {
992                 $menu = [DI::l10n()->t('View Profile') => $item['author-link']];
993         }
994
995         $args = ['item' => $item, 'menu' => $menu];
996
997         Hook::callAll('item_photo_menu', $args);
998
999         $menu = $args['menu'];
1000
1001         $o = '';
1002         foreach ($menu as $k => $v) {
1003                 if (strpos($v, 'javascript:') === 0) {
1004                         $v = substr($v, 11);
1005                         $o .= '<li role="menuitem"><a onclick="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
1006                 } elseif ($v!='') {
1007                         $o .= '<li role="menuitem"><a href="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
1008                 }
1009         }
1010         return $o;
1011 }
1012
1013 /**
1014  * Checks item to see if it is one of the builtin activities (like/dislike, event attendance, consensus items, etc.)
1015  *
1016  * Increments the count of each matching activity and adds a link to the author as needed.
1017  *
1018  * @param array  $item
1019  * @param array &$conv_responses (already created with builtin activity structure)
1020  * @return void
1021  * @throws ImagickException
1022  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1023  */
1024 function builtin_activity_puller($item, &$conv_responses) {
1025         foreach ($conv_responses as $mode => $v) {
1026                 $sparkle = '';
1027
1028                 switch ($mode) {
1029                         case 'like':
1030                                 $verb = Activity::LIKE;
1031                                 break;
1032                         case 'dislike':
1033                                 $verb = Activity::DISLIKE;
1034                                 break;
1035                         case 'attendyes':
1036                                 $verb = Activity::ATTEND;
1037                                 break;
1038                         case 'attendno':
1039                                 $verb = Activity::ATTENDNO;
1040                                 break;
1041                         case 'attendmaybe':
1042                                 $verb = Activity::ATTENDMAYBE;
1043                                 break;
1044                         case 'announce':
1045                                 $verb = Activity::ANNOUNCE;
1046                                 break;
1047                         default:
1048                                 return;
1049                 }
1050
1051                 if (!empty($item['verb']) && DI::activity()->match($item['verb'], $verb) && ($item['id'] != $item['parent'])) {
1052                         $author = ['uid' => 0, 'id' => $item['author-id'],
1053                                 'network' => $item['author-network'], 'url' => $item['author-link']];
1054                         $url = Contact::magicLinkByContact($author);
1055                         if (strpos($url, 'redir/') === 0) {
1056                                 $sparkle = ' class="sparkle" ';
1057                         }
1058
1059                         $url = '<a href="'. $url . '"'. $sparkle .'>' . htmlentities($item['author-name']) . '</a>';
1060
1061                         if (empty($item['thr-parent'])) {
1062                                 $item['thr-parent'] = $item['parent-uri'];
1063                         }
1064
1065                         if (!(isset($conv_responses[$mode][$item['thr-parent'] . '-l'])
1066                                 && is_array($conv_responses[$mode][$item['thr-parent'] . '-l']))) {
1067                                 $conv_responses[$mode][$item['thr-parent'] . '-l'] = [];
1068                         }
1069
1070                         // only list each unique author once
1071                         if (in_array($url,$conv_responses[$mode][$item['thr-parent'] . '-l'])) {
1072                                 continue;
1073                         }
1074
1075                         if (!isset($conv_responses[$mode][$item['thr-parent']])) {
1076                                 $conv_responses[$mode][$item['thr-parent']] = 1;
1077                         } else {
1078                                 $conv_responses[$mode][$item['thr-parent']] ++;
1079                         }
1080
1081                         if (public_contact() == $item['author-id']) {
1082                                 $conv_responses[$mode][$item['thr-parent'] . '-self'] = 1;
1083                         }
1084
1085                         $conv_responses[$mode][$item['thr-parent'] . '-l'][] = $url;
1086
1087                         // there can only be one activity verb per item so if we found anything, we can stop looking
1088                         return;
1089                 }
1090         }
1091 }
1092
1093 /**
1094  * Format the vote text for a profile item
1095  *
1096  * @param int    $cnt  = number of people who vote the item
1097  * @param array  $arr  = array of pre-linked names of likers/dislikers
1098  * @param string $type = one of 'like, 'dislike', 'attendyes', 'attendno', 'attendmaybe'
1099  * @param int    $id   = item id
1100  * @return string formatted text
1101  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1102  */
1103 function format_like($cnt, array $arr, $type, $id) {
1104         $o = '';
1105         $expanded = '';
1106         $phrase = '';
1107
1108         if ($cnt == 1) {
1109                 $likers = $arr[0];
1110
1111                 // Phrase if there is only one liker. In other cases it will be uses for the expanded
1112                 // list which show all likers
1113                 switch ($type) {
1114                         case 'like' :
1115                                 $phrase = DI::l10n()->t('%s likes this.', $likers);
1116                                 break;
1117                         case 'dislike' :
1118                                 $phrase = DI::l10n()->t('%s doesn\'t like this.', $likers);
1119                                 break;
1120                         case 'attendyes' :
1121                                 $phrase = DI::l10n()->t('%s attends.', $likers);
1122                                 break;
1123                         case 'attendno' :
1124                                 $phrase = DI::l10n()->t('%s doesn\'t attend.', $likers);
1125                                 break;
1126                         case 'attendmaybe' :
1127                                 $phrase = DI::l10n()->t('%s attends maybe.', $likers);
1128                                 break;
1129                         case 'announce' :
1130                                 $phrase = DI::l10n()->t('%s reshared this.', $likers);
1131                                 break;
1132                 }
1133         }
1134
1135         if ($cnt > 1) {
1136                 $total = count($arr);
1137                 if ($total < MAX_LIKERS) {
1138                         $last = DI::l10n()->t('and') . ' ' . $arr[count($arr)-1];
1139                         $arr2 = array_slice($arr, 0, -1);
1140                         $likers = implode(', ', $arr2) . ' ' . $last;
1141                 } else  {
1142                         $arr = array_slice($arr, 0, MAX_LIKERS - 1);
1143                         $likers = implode(', ', $arr);
1144                         $likers .= DI::l10n()->t('and %d other people', $total - MAX_LIKERS);
1145                 }
1146
1147                 $spanatts = "class=\"fakelink\" onclick=\"openClose('{$type}list-$id');\"";
1148
1149                 $explikers = '';
1150                 switch ($type) {
1151                         case 'like':
1152                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> like this', $spanatts, $cnt);
1153                                 $explikers = DI::l10n()->t('%s like this.', $likers);
1154                                 break;
1155                         case 'dislike':
1156                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> don\'t like this', $spanatts, $cnt);
1157                                 $explikers = DI::l10n()->t('%s don\'t like this.', $likers);
1158                                 break;
1159                         case 'attendyes':
1160                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> attend', $spanatts, $cnt);
1161                                 $explikers = DI::l10n()->t('%s attend.', $likers);
1162                                 break;
1163                         case 'attendno':
1164                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> don\'t attend', $spanatts, $cnt);
1165                                 $explikers = DI::l10n()->t('%s don\'t attend.', $likers);
1166                                 break;
1167                         case 'attendmaybe':
1168                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> attend maybe', $spanatts, $cnt);
1169                                 $explikers = DI::l10n()->t('%s attend maybe.', $likers);
1170                                 break;
1171                         case 'announce':
1172                                 $phrase = DI::l10n()->t('<span  %1$s>%2$d people</span> reshared this', $spanatts, $cnt);
1173                                 $explikers = DI::l10n()->t('%s reshared this.', $likers);
1174                                 break;
1175                 }
1176
1177                 $expanded .= "\t" . '<p class="wall-item-' . $type . '-expanded" id="' . $type . 'list-' . $id . '" style="display: none;" >' . $explikers . EOL . '</p>';
1178         }
1179
1180         $o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('voting_fakelink.tpl'), [
1181                 '$phrase' => $phrase,
1182                 '$type' => $type,
1183                 '$id' => $id
1184         ]);
1185         $o .= $expanded;
1186
1187         return $o;
1188 }
1189
1190 function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
1191 {
1192         $o = '';
1193
1194         $geotag = !empty($x['allow_location']) ? Renderer::replaceMacros(Renderer::getMarkupTemplate('jot_geotag.tpl'), []) : '';
1195
1196         $tpl = Renderer::getMarkupTemplate('jot-header.tpl');
1197         DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
1198                 '$newpost'   => 'true',
1199                 '$baseurl'   => DI::baseUrl()->get(true),
1200                 '$geotag'    => $geotag,
1201                 '$nickname'  => $x['nickname'],
1202                 '$ispublic'  => DI::l10n()->t('Visible to <strong>everybody</strong>'),
1203                 '$linkurl'   => DI::l10n()->t('Please enter a image/video/audio/webpage URL:'),
1204                 '$term'      => DI::l10n()->t('Tag term:'),
1205                 '$fileas'    => DI::l10n()->t('Save to Folder:'),
1206                 '$whereareu' => DI::l10n()->t('Where are you right now?'),
1207                 '$delitems'  => DI::l10n()->t("Delete item\x28s\x29?")
1208         ]);
1209
1210         $jotplugins = '';
1211         Hook::callAll('jot_tool', $jotplugins);
1212
1213         // Private/public post links for the non-JS ACL form
1214         $private_post = 1;
1215         if (!empty($_REQUEST['public'])) {
1216                 $private_post = 0;
1217         }
1218
1219         $query_str = DI::args()->getQueryString();
1220         if (strpos($query_str, 'public=1') !== false) {
1221                 $query_str = str_replace(['?public=1', '&public=1'], ['', ''], $query_str);
1222         }
1223
1224         /*
1225          * I think $a->query_string may never have ? in it, but I could be wrong
1226          * It looks like it's from the index.php?q=[etc] rewrite that the web
1227          * server does, which converts any ? to &, e.g. suggest&ignore=61 for suggest?ignore=61
1228          */
1229         if (strpos($query_str, '?') === false) {
1230                 $public_post_link = '?public=1';
1231         } else {
1232                 $public_post_link = '&public=1';
1233         }
1234
1235         // $tpl = Renderer::replaceMacros($tpl,array('$jotplugins' => $jotplugins));
1236         $tpl = Renderer::getMarkupTemplate("jot.tpl");
1237
1238         $o .= Renderer::replaceMacros($tpl,[
1239                 '$new_post' => DI::l10n()->t('New Post'),
1240                 '$return_path'  => $query_str,
1241                 '$action'       => 'item',
1242                 '$share'        => ($x['button'] ?? '') ?: DI::l10n()->t('Share'),
1243                 '$loading'      => DI::l10n()->t('Loading...'),
1244                 '$upload'       => DI::l10n()->t('Upload photo'),
1245                 '$shortupload'  => DI::l10n()->t('upload photo'),
1246                 '$attach'       => DI::l10n()->t('Attach file'),
1247                 '$shortattach'  => DI::l10n()->t('attach file'),
1248                 '$edbold'       => DI::l10n()->t('Bold'),
1249                 '$editalic'     => DI::l10n()->t('Italic'),
1250                 '$eduline'      => DI::l10n()->t('Underline'),
1251                 '$edquote'      => DI::l10n()->t('Quote'),
1252                 '$edcode'       => DI::l10n()->t('Code'),
1253                 '$edimg'        => DI::l10n()->t('Image'),
1254                 '$edurl'        => DI::l10n()->t('Link'),
1255                 '$edattach'     => DI::l10n()->t('Link or Media'),
1256                 '$setloc'       => DI::l10n()->t('Set your location'),
1257                 '$shortsetloc'  => DI::l10n()->t('set location'),
1258                 '$noloc'        => DI::l10n()->t('Clear browser location'),
1259                 '$shortnoloc'   => DI::l10n()->t('clear location'),
1260                 '$title'        => $x['title'] ?? '',
1261                 '$placeholdertitle' => DI::l10n()->t('Set title'),
1262                 '$category'     => $x['category'] ?? '',
1263                 '$placeholdercategory' => Feature::isEnabled(local_user(), 'categories') ? DI::l10n()->t("Categories \x28comma-separated list\x29") : '',
1264                 '$wait'         => DI::l10n()->t('Please wait'),
1265                 '$permset'      => DI::l10n()->t('Permission settings'),
1266                 '$shortpermset' => DI::l10n()->t('permissions'),
1267                 '$wall'         => $notes_cid ? 0 : 1,
1268                 '$posttype'     => $notes_cid ? Item::PT_PERSONAL_NOTE : Item::PT_ARTICLE,
1269                 '$content'      => $x['content'] ?? '',
1270                 '$post_id'      => $x['post_id'] ?? '',
1271                 '$baseurl'      => DI::baseUrl()->get(true),
1272                 '$defloc'       => $x['default_location'],
1273                 '$visitor'      => $x['visitor'],
1274                 '$pvisit'       => $notes_cid ? 'none' : $x['visitor'],
1275                 '$public'       => DI::l10n()->t('Public post'),
1276                 '$lockstate'    => $x['lockstate'],
1277                 '$bang'         => $x['bang'],
1278                 '$profile_uid'  => $x['profile_uid'],
1279                 '$preview'      => DI::l10n()->t('Preview'),
1280                 '$jotplugins'   => $jotplugins,
1281                 '$notes_cid'    => $notes_cid,
1282                 '$sourceapp'    => DI::l10n()->t($a->sourcename),
1283                 '$cancel'       => DI::l10n()->t('Cancel'),
1284                 '$rand_num'     => Crypto::randomDigits(12),
1285
1286                 // ACL permissions box
1287                 '$acl'           => $x['acl'],
1288                 '$group_perms'   => DI::l10n()->t('Post to Groups'),
1289                 '$contact_perms' => DI::l10n()->t('Post to Contacts'),
1290                 '$private'       => DI::l10n()->t('Private post'),
1291                 '$is_private'    => $private_post,
1292                 '$public_link'   => $public_post_link,
1293
1294                 //jot nav tab (used in some themes)
1295                 '$message' => DI::l10n()->t('Message'),
1296                 '$browser' => DI::l10n()->t('Browser'),
1297         ]);
1298
1299
1300         if ($popup == true) {
1301                 $o = '<div id="jot-popup" style="display: none;">' . $o . '</div>';
1302         }
1303
1304         return $o;
1305 }
1306
1307 /**
1308  * Plucks the children of the given parent from a given item list.
1309  *
1310  * @param array $item_list
1311  * @param array $parent
1312  * @param bool  $recursive
1313  * @return array
1314  */
1315 function get_item_children(array &$item_list, array $parent, $recursive = true)
1316 {
1317         $children = [];
1318         foreach ($item_list as $i => $item) {
1319                 if ($item['id'] != $item['parent']) {
1320                         if ($recursive) {
1321                                 // Fallback to parent-uri if thr-parent is not set
1322                                 $thr_parent = $item['thr-parent'];
1323                                 if ($thr_parent == '') {
1324                                         $thr_parent = $item['parent-uri'];
1325                                 }
1326
1327                                 if ($thr_parent == $parent['uri']) {
1328                                         $item['children'] = get_item_children($item_list, $item);
1329                                         $children[] = $item;
1330                                         unset($item_list[$i]);
1331                                 }
1332                         } elseif ($item['parent'] == $parent['id']) {
1333                                 $children[] = $item;
1334                                 unset($item_list[$i]);
1335                         }
1336                 }
1337         }
1338         return $children;
1339 }
1340
1341 /**
1342  * Recursively sorts a tree-like item array
1343  *
1344  * @param array $items
1345  * @return array
1346  */
1347 function sort_item_children(array $items)
1348 {
1349         $result = $items;
1350         usort($result, 'sort_thr_received_rev');
1351         foreach ($result as $k => $i) {
1352                 if (isset($result[$k]['children'])) {
1353                         $result[$k]['children'] = sort_item_children($result[$k]['children']);
1354                 }
1355         }
1356         return $result;
1357 }
1358
1359 /**
1360  * Recursively add all children items at the top level of a list
1361  *
1362  * @param array $children List of items to append
1363  * @param array $item_list
1364  */
1365 function add_children_to_list(array $children, array &$item_list)
1366 {
1367         foreach ($children as $child) {
1368                 $item_list[] = $child;
1369                 if (isset($child['children'])) {
1370                         add_children_to_list($child['children'], $item_list);
1371                 }
1372         }
1373 }
1374
1375 /**
1376  * Selectively flattens a tree-like item structure to prevent threading stairs
1377  *
1378  * This recursive function takes the item tree structure created by conv_sort() and
1379  * flatten the extraneous depth levels when people reply sequentially, removing the
1380  * stairs effect in threaded conversations limiting the available content width.
1381  *
1382  * The basic principle is the following: if a post item has only one reply and is
1383  * the last reply of its parent, then the reply is moved to the parent.
1384  *
1385  * This process is rendered somewhat more complicated because items can be either
1386  * replies or likes, and these don't factor at all in the reply count/last reply.
1387  *
1388  * @param array $parent A tree-like array of items
1389  * @return array
1390  */
1391 function smart_flatten_conversation(array $parent)
1392 {
1393         if (!isset($parent['children']) || count($parent['children']) == 0) {
1394                 return $parent;
1395         }
1396
1397         // We use a for loop to ensure we process the newly-moved items
1398         for ($i = 0; $i < count($parent['children']); $i++) {
1399                 $child = $parent['children'][$i];
1400
1401                 if (isset($child['children']) && count($child['children'])) {
1402                         // This helps counting only the regular posts
1403                         $count_post_closure = function($var) {
1404                                 return $var['verb'] === Activity::POST;
1405                         };
1406
1407                         $child_post_count = count(array_filter($child['children'], $count_post_closure));
1408
1409                         $remaining_post_count = count(array_filter(array_slice($parent['children'], $i), $count_post_closure));
1410
1411                         // If there's only one child's children post and this is the last child post
1412                         if ($child_post_count == 1 && $remaining_post_count == 1) {
1413
1414                                 // Searches the post item in the children
1415                                 $j = 0;
1416                                 while($child['children'][$j]['verb'] !== Activity::POST && $j < count($child['children'])) {
1417                                         $j ++;
1418                                 }
1419
1420                                 $moved_item = $child['children'][$j];
1421                                 unset($parent['children'][$i]['children'][$j]);
1422                                 $parent['children'][] = $moved_item;
1423                         } else {
1424                                 $parent['children'][$i] = smart_flatten_conversation($child);
1425                         }
1426                 }
1427         }
1428
1429         return $parent;
1430 }
1431
1432
1433 /**
1434  * Expands a flat list of items into corresponding tree-like conversation structures.
1435  *
1436  * sort the top-level posts either on "received" or "commented", and finally
1437  * append all the items at the top level (???)
1438  *
1439  * @param array  $item_list A list of items belonging to one or more conversations
1440  * @param string $order     Either on "received" or "commented"
1441  * @return array
1442  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1443  */
1444 function conv_sort(array $item_list, $order)
1445 {
1446         $parents = [];
1447
1448         if (!(is_array($item_list) && count($item_list))) {
1449                 return $parents;
1450         }
1451
1452         $blocklist = conv_get_blocklist();
1453
1454         $item_array = [];
1455
1456         // Dedupes the item list on the uri to prevent infinite loops
1457         foreach ($item_list as $item) {
1458                 if (in_array($item['author-id'], $blocklist)) {
1459                         continue;
1460                 }
1461
1462                 $item_array[$item['uri']] = $item;
1463         }
1464
1465         // Extract the top level items
1466         foreach ($item_array as $item) {
1467                 if ($item['id'] == $item['parent']) {
1468                         $parents[] = $item;
1469                 }
1470         }
1471
1472         if (stristr($order, 'pinned_received')) {
1473                 usort($parents, 'sort_thr_pinned_received');
1474         } elseif (stristr($order, 'received')) {
1475                 usort($parents, 'sort_thr_received');
1476         } elseif (stristr($order, 'commented')) {
1477                 usort($parents, 'sort_thr_commented');
1478         }
1479
1480         /*
1481          * Plucks children from the item_array, second pass collects eventual orphan
1482          * items and add them as children of their top-level post.
1483          */
1484         foreach ($parents as $i => $parent) {
1485                 $parents[$i]['children'] =
1486                         array_merge(get_item_children($item_array, $parent, true),
1487                                 get_item_children($item_array, $parent, false));
1488         }
1489
1490         foreach ($parents as $i => $parent) {
1491                 $parents[$i]['children'] = sort_item_children($parents[$i]['children']);
1492         }
1493
1494         if (!DI::pConfig()->get(local_user(), 'system', 'no_smart_threading', 0)) {
1495                 foreach ($parents as $i => $parent) {
1496                         $parents[$i] = smart_flatten_conversation($parent);
1497                 }
1498         }
1499
1500         /// @TODO: Stop recusrsively adding all children back to the top level (!!!)
1501         /// However, this apparently ensures responses (likes, attendance) display (?!)
1502         foreach ($parents as $parent) {
1503                 if (count($parent['children'])) {
1504                         add_children_to_list($parent['children'], $parents);
1505                 }
1506         }
1507
1508         return $parents;
1509 }
1510
1511 /**
1512  * usort() callback to sort item arrays by pinned and the received key
1513  *
1514  * @param array $a
1515  * @param array $b
1516  * @return int
1517  */
1518 function sort_thr_pinned_received(array $a, array $b)
1519 {
1520         if ($b['pinned'] && !$a['pinned']) {
1521                 return 1;
1522         } elseif (!$b['pinned'] && $a['pinned']) {
1523                 return -1;
1524         }
1525
1526         return strcmp($b['received'], $a['received']);
1527 }
1528
1529 /**
1530  * usort() callback to sort item arrays by the received key
1531  *
1532  * @param array $a
1533  * @param array $b
1534  * @return int
1535  */
1536 function sort_thr_received(array $a, array $b)
1537 {
1538         return strcmp($b['received'], $a['received']);
1539 }
1540
1541 /**
1542  * usort() callback to reverse sort item arrays by the received key
1543  *
1544  * @param array $a
1545  * @param array $b
1546  * @return int
1547  */
1548 function sort_thr_received_rev(array $a, array $b)
1549 {
1550         return strcmp($a['received'], $b['received']);
1551 }
1552
1553 /**
1554  * usort() callback to sort item arrays by the commented key
1555  *
1556  * @param array $a
1557  * @param array $b
1558  * @return int
1559  */
1560 function sort_thr_commented(array $a, array $b)
1561 {
1562         return strcmp($b['commented'], $a['commented']);
1563 }
1564
1565 function render_location_dummy(array $item) {
1566         if (!empty($item['location']) && !empty($item['location'])) {
1567                 return $item['location'];
1568         }
1569
1570         if (!empty($item['coord']) && !empty($item['coord'])) {
1571                 return $item['coord'];
1572         }
1573 }