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