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