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