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