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