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