]> git.mxchange.org Git - friendica.git/blob - include/conversation.php
Pinned items are always at the top
[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\Crypto;
31 use Friendica\Util\DateTimeFormat;
32 use Friendica\Util\Proxy as ProxyUtils;
33 use Friendica\Util\Strings;
34 use Friendica\Util\Temporal;
35 use Friendica\Util\XML;
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\ObjectType::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\ObjectType::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\ObjectType::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\ObjectType::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         if ($activity->isHidden($item['verb'])) {
406                 return false;
407         }
408
409         // @TODO below if() block can be rewritten to a single line: $isVisible = allConditionsHere;
410         if ($activity->match($item['verb'], Activity::FOLLOW) &&
411             $item['object-type'] === Activity\ObjectType::NOTE &&
412             empty($item['self']) &&
413             $item['uid'] == local_user()) {
414                 return false;
415         }
416
417         return true;
418 }
419
420 function conv_get_blocklist()
421 {
422         if (!local_user()) {
423                 return [];
424         }
425
426         $str_blocked = PConfig::get(local_user(), 'system', 'blocked');
427         if (empty($str_blocked)) {
428                 return [];
429         }
430
431         $blocklist = [];
432
433         foreach (explode(',', $str_blocked) as $entry) {
434                 // The 4th parameter guarantees that there always will be a public contact entry
435                 $cid = Contact::getIdForURL(trim($entry), 0, true, ['url' => trim($entry)]);
436                 if (!empty($cid)) {
437                         $blocklist[] = $cid;
438                 }
439         }
440
441         return $blocklist;
442 }
443
444 /**
445  * "Render" a conversation or list of items for HTML display.
446  * There are two major forms of display:
447  *      - Sequential or unthreaded ("New Item View" or search results)
448  *      - conversation view
449  * The $mode parameter decides between the various renderings and also
450  * figures out how to determine page owner and other contextual items
451  * that are based on unique features of the calling module.
452  * @param App    $a
453  * @param array  $items
454  * @param Pager  $pager
455  * @param        $mode
456  * @param        $update
457  * @param bool   $preview
458  * @param string $order
459  * @param int    $uid
460  * @return string
461  * @throws ImagickException
462  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
463  */
464 function conversation(App $a, array $items, Pager $pager, $mode, $update, $preview = false, $order = 'commented', $uid = 0)
465 {
466         $ssl_state = (local_user() ? true : false);
467
468         $profile_owner = 0;
469         $live_update_div = '';
470
471         $blocklist = conv_get_blocklist();
472
473         $previewing = (($preview) ? ' preview ' : '');
474
475         if ($mode === 'network') {
476                 $items = conversation_add_children($items, false, $order, $uid);
477                 $profile_owner = local_user();
478                 if (!$update) {
479                         /*
480                          * The special div is needed for liveUpdate to kick in for this page.
481                          * We only launch liveUpdate if you aren't filtering in some incompatible
482                          * way and also you aren't writing a comment (discovered in javascript).
483                          */
484                         $live_update_div = '<div id="live-network"></div>' . "\r\n"
485                                 . "<script> var profile_uid = " . $_SESSION['uid']
486                                 . "; var netargs = '" . substr($a->cmd, 8)
487                                 . '?f='
488                                 . (!empty($_GET['cid'])    ? '&cid='    . rawurlencode($_GET['cid'])    : '')
489                                 . (!empty($_GET['search']) ? '&search=' . rawurlencode($_GET['search']) : '')
490                                 . (!empty($_GET['star'])   ? '&star='   . rawurlencode($_GET['star'])   : '')
491                                 . (!empty($_GET['order'])  ? '&order='  . rawurlencode($_GET['order'])  : '')
492                                 . (!empty($_GET['bmark'])  ? '&bmark='  . rawurlencode($_GET['bmark'])  : '')
493                                 . (!empty($_GET['liked'])  ? '&liked='  . rawurlencode($_GET['liked'])  : '')
494                                 . (!empty($_GET['conv'])   ? '&conv='   . rawurlencode($_GET['conv'])   : '')
495                                 . (!empty($_GET['nets'])   ? '&nets='   . rawurlencode($_GET['nets'])   : '')
496                                 . (!empty($_GET['cmin'])   ? '&cmin='   . rawurlencode($_GET['cmin'])   : '')
497                                 . (!empty($_GET['cmax'])   ? '&cmax='   . rawurlencode($_GET['cmax'])   : '')
498                                 . (!empty($_GET['file'])   ? '&file='   . rawurlencode($_GET['file'])   : '')
499
500                                 . "'; var profile_page = " . $pager->getPage() . "; </script>\r\n";
501                 }
502         } elseif ($mode === 'profile') {
503                 $items = conversation_add_children($items, false, $order, $uid);
504                 $profile_owner = $a->profile['profile_uid'];
505
506                 if (!$update) {
507                         $tab = 'posts';
508                         if (!empty($_GET['tab'])) {
509                                 $tab = Strings::escapeTags(trim($_GET['tab']));
510                         }
511                         if ($tab === 'posts') {
512                                 /*
513                                  * This is ugly, but we can't pass the profile_uid through the session to the ajax updater,
514                                  * because browser prefetching might change it on us. We have to deliver it with the page.
515                                  */
516
517                                 $live_update_div = '<div id="live-profile"></div>' . "\r\n"
518                                         . "<script> var profile_uid = " . $a->profile['profile_uid']
519                                         . "; var netargs = '?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
520                         }
521                 }
522         } elseif ($mode === 'notes') {
523                 $items = conversation_add_children($items, false, $order, local_user());
524                 $profile_owner = local_user();
525
526                 if (!$update) {
527                         $live_update_div = '<div id="live-notes"></div>' . "\r\n"
528                                 . "<script> var profile_uid = " . local_user()
529                                 . "; var netargs = '/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
530                 }
531         } elseif ($mode === 'display') {
532                 $items = conversation_add_children($items, false, $order, $uid);
533                 $profile_owner = $a->profile['uid'];
534
535                 if (!$update) {
536                         $live_update_div = '<div id="live-display"></div>' . "\r\n"
537                                 . "<script> var profile_uid = " . Session::get('uid', 0) . ";"
538                                 . " var profile_page = 1; </script>";
539                 }
540         } elseif ($mode === 'community') {
541                 $items = conversation_add_children($items, true, $order, $uid);
542                 $profile_owner = 0;
543
544                 if (!$update) {
545                         $live_update_div = '<div id="live-community"></div>' . "\r\n"
546                                 . "<script> var profile_uid = -1; var netargs = '" . substr($a->cmd, 10)
547                                 ."/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
548                 }
549         } elseif ($mode === 'contacts') {
550                 $items = conversation_add_children($items, false, $order, $uid);
551                 $profile_owner = 0;
552
553                 if (!$update) {
554                         $live_update_div = '<div id="live-contacts"></div>' . "\r\n"
555                                 . "<script> var profile_uid = -1; var netargs = '" . substr($a->cmd, 9)
556                                 ."/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
557                 }
558         } elseif ($mode === 'search') {
559                 $live_update_div = '<div id="live-search"></div>' . "\r\n";
560         }
561
562         $page_dropping = ((local_user() && local_user() == $profile_owner) ? true : false);
563
564         if (!$update) {
565                 $_SESSION['return_path'] = $a->query_string;
566         }
567
568         $cb = ['items' => $items, 'mode' => $mode, 'update' => $update, 'preview' => $preview];
569         Hook::callAll('conversation_start',$cb);
570
571         $items = $cb['items'];
572
573         $conv_responses = [
574                 'like' => ['title' => L10n::t('Likes','title')],
575                 'dislike' => ['title' => L10n::t('Dislikes','title')],
576                 'attendyes' => ['title' => L10n::t('Attending','title')],
577                 'attendno' => ['title' => L10n::t('Not attending','title')],
578                 'attendmaybe' => ['title' => L10n::t('Might attend','title')],
579                 'announce' => ['title' => L10n::t('Reshares','title')]
580         ];
581
582         // array with html for each thread (parent+comments)
583         $threads = [];
584         $threadsid = -1;
585
586         $page_template = Renderer::getMarkupTemplate("conversation.tpl");
587
588         if (!empty($items)) {
589                 if (in_array($mode, ['community', 'contacts'])) {
590                         $writable = true;
591                 } else {
592                         $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED);
593                 }
594
595                 if (!local_user()) {
596                         $writable = false;
597                 }
598
599                 if (in_array($mode, ['network-new', 'search', 'contact-posts'])) {
600
601                         /*
602                          * "New Item View" on network page or search page results
603                          * - just loop through the items and format them minimally for display
604                          */
605
606                         $tpl = 'search_item.tpl';
607
608                         foreach ($items as $item) {
609
610                                 if (!visible_activity($item)) {
611                                         continue;
612                                 }
613
614                                 if (in_array($item['author-id'], $blocklist)) {
615                                         continue;
616                                 }
617
618                                 $threadsid++;
619
620                                 $owner_url   = '';
621                                 $owner_name  = '';
622                                 $sparkle     = '';
623
624                                 // prevent private email from leaking.
625                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
626                                         continue;
627                                 }
628
629                                 $profile_name = $item['author-name'];
630                                 if (!empty($item['author-link']) && empty($item['author-name'])) {
631                                         $profile_name = $item['author-link'];
632                                 }
633
634                                 $tags = Term::populateTagsFromItem($item);
635
636                                 $author = ['uid' => 0, 'id' => $item['author-id'],
637                                         'network' => $item['author-network'], 'url' => $item['author-link']];
638                                 $profile_link = Contact::magicLinkByContact($author);
639
640                                 if (strpos($profile_link, 'redir/') === 0) {
641                                         $sparkle = ' sparkle';
642                                 }
643
644                                 $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
645                                 Hook::callAll('render_location',$locate);
646
647                                 $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
648
649                                 localize_item($item);
650                                 if ($mode === 'network-new') {
651                                         $dropping = true;
652                                 } else {
653                                         $dropping = false;
654                                 }
655
656                                 $drop = [
657                                         'dropping' => $dropping,
658                                         'pagedrop' => $page_dropping,
659                                         'select' => L10n::t('Select'),
660                                         'delete' => L10n::t('Delete'),
661                                 ];
662
663                                 $star = false;
664                                 $isstarred = "unstarred";
665
666                                 $lock = false;
667                                 $likebuttons = false;
668
669                                 $body = Item::prepareBody($item, true, $preview);
670
671                                 /** @var ContentItem $contItem */
672                                 $contItem = BaseObject::getClass(ContentItem::class);
673
674                                 list($categories, $folders) = $contItem->determineCategoriesTerms($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'] == Activity::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 = Activity::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         if ($b['pinned'] && !$a['pinned']) {
1501                 return 1;
1502         } elseif (!$b['pinned'] && $a['pinned']) {
1503                 return -1;
1504         }
1505
1506         return strcmp($b['received'], $a['received']);
1507 }
1508
1509 /**
1510  * @brief usort() callback to reverse sort item arrays by the received key
1511  *
1512  * @param array $a
1513  * @param array $b
1514  * @return int
1515  */
1516 function sort_thr_received_rev(array $a, array $b)
1517 {
1518         if ($b['pinned'] && !$a['pinned']) {
1519                 return -1;
1520         } elseif (!$b['pinned'] && $a['pinned']) {
1521                 return 1;
1522         }
1523
1524         return strcmp($a['received'], $b['received']);
1525 }
1526
1527 /**
1528  * @brief usort() callback to sort item arrays by the commented key
1529  *
1530  * @param array $a
1531  * @param array $b
1532  * @return int
1533  */
1534 function sort_thr_commented(array $a, array $b)
1535 {
1536         if ($b['pinned'] && !$a['pinned']) {
1537                 return 1;
1538         } elseif (!$b['pinned'] && $a['pinned']) {
1539                 return -1;
1540         }
1541
1542         return strcmp($b['commented'], $a['commented']);
1543 }
1544
1545 function render_location_dummy(array $item) {
1546         if (!empty($item['location']) && !empty($item['location'])) {
1547                 return $item['location'];
1548         }
1549
1550         if (!empty($item['coord']) && !empty($item['coord'])) {
1551                 return $item['coord'];
1552         }
1553 }
1554
1555 function get_responses(array $conv_responses, array $response_verbs, array $item, Post $ob = null) {
1556         $ret = [];
1557         foreach ($response_verbs as $v) {
1558                 $ret[$v] = [];
1559                 $ret[$v]['count'] = $conv_responses[$v][$item['uri']] ?? 0;
1560                 $ret[$v]['list']  = $conv_responses[$v][$item['uri'] . '-l'] ?? [];
1561                 $ret[$v]['self']  = $conv_responses[$v][$item['uri'] . '-self'] ?? '0';
1562                 if (count($ret[$v]['list']) > MAX_LIKERS) {
1563                         $ret[$v]['list_part'] = array_slice($ret[$v]['list'], 0, MAX_LIKERS);
1564                         array_push($ret[$v]['list_part'], '<a href="#" data-toggle="modal" data-target="#' . $v . 'Modal-'
1565                                 . (($ob) ? $ob->getId() : $item['id']) . '"><b>' . L10n::t('View all') . '</b></a>');
1566                 } else {
1567                         $ret[$v]['list_part'] = '';
1568                 }
1569                 $ret[$v]['button'] = get_response_button_text($v, $ret[$v]['count']);
1570                 $ret[$v]['title'] = $conv_responses[$v]['title'];
1571         }
1572
1573         $count = 0;
1574         foreach ($ret as $key) {
1575                 if ($key['count'] == true) {
1576                         $count++;
1577                 }
1578         }
1579         $ret['count'] = $count;
1580
1581         return $ret;
1582 }
1583
1584 function get_response_button_text($v, $count)
1585 {
1586         $return = '';
1587         switch ($v) {
1588                 case 'like':
1589                         $return = L10n::tt('Like', 'Likes', $count);
1590                         break;
1591                 case 'dislike':
1592                         $return = L10n::tt('Dislike', 'Dislikes', $count);
1593                         break;
1594                 case 'attendyes':
1595                         $return = L10n::tt('Attending', 'Attending', $count);
1596                         break;
1597                 case 'attendno':
1598                         $return = L10n::tt('Not Attending', 'Not Attending', $count);
1599                         break;
1600                 case 'attendmaybe':
1601                         $return = L10n::tt('Undecided', 'Undecided', $count);
1602                         break;
1603         }
1604
1605         return $return;
1606 }