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