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