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