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