]> git.mxchange.org Git - friendica.git/blob - include/conversation.php
Merge pull request #5294 from annando/use-gravity
[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                                 $profile_name = $item['author-name'];
566                                 if (!empty($item['author-link']) && empty($item['author-name'])) {
567                                         $profile_name = $item['author-link'];
568                                 }
569
570                                 $tags = \Friendica\Model\Term::populateTagsFromItem($item);
571
572                                 $profile_link = Contact::magicLinkbyId($item['author-id']);
573
574                                 if (strpos($profile_link, 'redir/') === 0) {
575                                         $sparkle = ' sparkle';
576                                 }
577
578                                 $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
579                                 Addon::callHooks('render_location',$locate);
580
581                                 $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
582
583                                 localize_item($item);
584                                 if ($mode === 'network-new') {
585                                         $dropping = true;
586                                 } else {
587                                         $dropping = false;
588                                 }
589
590                                 $drop = [
591                                         'dropping' => $dropping,
592                                         'pagedrop' => $page_dropping,
593                                         'select' => L10n::t('Select'),
594                                         'delete' => L10n::t('Delete'),
595                                 ];
596
597                                 $star = false;
598                                 $isstarred = "unstarred";
599
600                                 $lock = false;
601                                 $likebuttons = false;
602
603                                 $body = prepare_body($item, true, $preview);
604
605                                 list($categories, $folders) = get_cats_and_terms($item);
606
607                                 $profile_name_e = $profile_name;
608
609                                 if (!empty($item['content-warning']) && PConfig::get(local_user(), 'system', 'disable_cw', false)) {
610                                         $title_e = ucfirst($item['content-warning']);
611                                 } else {
612                                         $title_e = $item['title'];
613                                 }
614
615                                 $body_e = $body;
616                                 $tags_e = $tags['tags'];
617                                 $hashtags_e = $tags['hashtags'];
618                                 $mentions_e = $tags['mentions'];
619                                 $location_e = $location;
620                                 $owner_name_e = $owner_name;
621
622                                 $tmp_item = [
623                                         'template' => $tpl,
624                                         'id' => (($preview) ? 'P0' : $item['item_id']),
625                                         'guid' => (($preview) ? 'Q0' : $item['guid']),
626                                         'network' => $item['network'],
627                                         'network_name' => ContactSelector::networkToName($item['network'], $profile_link),
628                                         'linktitle' => L10n::t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
629                                         'profile_url' => $profile_link,
630                                         'item_photo_menu' => item_photo_menu($item),
631                                         'name' => $profile_name_e,
632                                         'sparkle' => $sparkle,
633                                         'lock' => $lock,
634                                         'thumb' => System::removedBaseUrl(proxy_url($item['author-avatar'], false, PROXY_SIZE_THUMB)),
635                                         'title' => $title_e,
636                                         'body' => $body_e,
637                                         'tags' => $tags_e,
638                                         'hashtags' => $hashtags_e,
639                                         'mentions' => $mentions_e,
640                                         'txt_cats' => L10n::t('Categories:'),
641                                         'txt_folders' => L10n::t('Filed under:'),
642                                         'has_cats' => ((count($categories)) ? 'true' : ''),
643                                         'has_folders' => ((count($folders)) ? 'true' : ''),
644                                         'categories' => $categories,
645                                         'folders' => $folders,
646                                         'text' => strip_tags($body_e),
647                                         'localtime' => DateTimeFormat::local($item['created'], 'r'),
648                                         'ago' => (($item['app']) ? L10n::t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
649                                         'location' => $location_e,
650                                         'indent' => '',
651                                         'owner_name' => $owner_name_e,
652                                         'owner_url' => $owner_url,
653                                         'owner_photo' => System::removedBaseUrl(proxy_url($item['owner-avatar'], false, PROXY_SIZE_THUMB)),
654                                         'plink' => get_plink($item),
655                                         'edpost' => false,
656                                         'isstarred' => $isstarred,
657                                         'star' => $star,
658                                         'drop' => $drop,
659                                         'vote' => $likebuttons,
660                                         'like' => '',
661                                         'dislike' => '',
662                                         'comment' => '',
663                                         'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> L10n::t('View in context')]),
664                                         'previewing' => $previewing,
665                                         'wait' => L10n::t('Please wait'),
666                                         'thread_level' => 1,
667                                 ];
668
669                                 $arr = ['item' => $item, 'output' => $tmp_item];
670                                 Addon::callHooks('display_item', $arr);
671
672                                 $threads[$threadsid]['id'] = $item['item_id'];
673                                 $threads[$threadsid]['network'] = $item['network'];
674                                 $threads[$threadsid]['items'] = [$arr['output']];
675
676                         }
677                 } else {
678                         // Normal View
679                         $page_template = get_markup_template("threaded_conversation.tpl");
680
681                         $conv = new Thread($mode, $preview, $writable);
682
683                         /*
684                          * get all the topmost parents
685                          * this shouldn't be needed, as we should have only them in our array
686                          * But for now, this array respects the old style, just in case
687                          */
688                         foreach ($items as $item) {
689                                 if ($arr_blocked) {
690                                         $blocked = false;
691                                         foreach ($arr_blocked as $b) {
692                                                 if ($b && link_compare($item['author-link'], $b)) {
693                                                         $blocked = true;
694                                                         break;
695                                                 }
696                                         }
697                                         if ($blocked) {
698                                                 continue;
699                                         }
700                                 }
701
702                                 // Can we put this after the visibility check?
703                                 builtin_activity_puller($item, $conv_responses);
704
705                                 // Only add what is visible
706                                 if ($item['network'] === NETWORK_MAIL && local_user() != $item['uid']) {
707                                         continue;
708                                 }
709
710                                 if (!visible_activity($item)) {
711                                         continue;
712                                 }
713
714                                 Addon::callHooks('display_item', $arr);
715
716                                 $item['pagedrop'] = $page_dropping;
717
718                                 if ($item['id'] == $item['parent']) {
719                                         $item_object = new Post($item);
720                                         $conv->addParent($item_object);
721                                 }
722                         }
723
724                         $threads = $conv->getTemplateData($conv_responses);
725                         if (!$threads) {
726                                 logger('[ERROR] conversation : Failed to get template data.', LOGGER_DEBUG);
727                                 $threads = [];
728                         }
729                 }
730         }
731
732         $o = replace_macros($page_template, [
733                 '$baseurl' => System::baseUrl($ssl_state),
734                 '$return_path' => $a->query_string,
735                 '$live_update' => $live_update_div,
736                 '$remove' => L10n::t('remove'),
737                 '$mode' => $mode,
738                 '$user' => $a->user,
739                 '$threads' => $threads,
740                 '$dropping' => ($page_dropping && Feature::isEnabled(local_user(), 'multi_delete') ? L10n::t('Delete Selected Items') : False),
741         ]);
742
743         return $o;
744 }
745
746 /**
747  * @brief Add comments to top level entries that had been fetched before
748  *
749  * The system will fetch the comments for the local user whenever possible.
750  * This behaviour is currently needed to allow commenting on Friendica posts.
751  *
752  * @param array $parents Parent items
753  *
754  * @return array items with parents and comments
755  */
756 function conversation_add_children($parents, $block_authors, $order, $uid) {
757         $max_comments = Config::get('system', 'max_comments', 100);
758
759         $params = ['order' => ['uid', 'commented' => true]];
760
761         if ($max_comments > 0) {
762                 $params['limit'] = $max_comments;
763         }
764
765         $items = [];
766
767         foreach ($parents AS $parent) {
768                 $condition = ["`item`.`parent-uri` = ? AND `item`.`uid` IN (0, ?) ",
769                         $parent['uri'], local_user()];
770                 if ($block_authors) {
771                         $condition[0] .= "AND NOT `author`.`hidden`";
772                 }
773                 $thread_items = Item::selectForUser(local_user(), [], $condition, $params);
774
775                 $comments = Item::inArray($thread_items);
776
777                 if (count($comments) != 0) {
778                         $items = array_merge($items, $comments);
779                 }
780         }
781
782         foreach ($items as $index => $item) {
783                 if ($item['uid'] == 0) {
784                         $items[$index]['writable'] = in_array($item['network'], [NETWORK_OSTATUS, NETWORK_DIASPORA, NETWORK_DFRN]);
785                 }
786         }
787
788         $items = conv_sort($items, $order);
789
790         return $items;
791 }
792
793 function item_photo_menu($item) {
794         $sub_link = '';
795         $poke_link = '';
796         $contact_url = '';
797         $pm_url = '';
798         $status_link = '';
799         $photos_link = '';
800         $posts_link = '';
801
802         if (local_user() && local_user() == $item['uid'] && $item['parent'] == $item['id'] && !$item['self']) {
803                 $sub_link = 'javascript:dosubthread(' . $item['id'] . '); return false;';
804         }
805
806         $profile_link = Contact::magicLinkById($item['author-id']);
807         $sparkle = (strpos($profile_link, 'redir/') === 0);
808
809         $cid = 0;
810         $network = '';
811         $rel = 0;
812         $condition = ['uid' => local_user(), 'nurl' => normalise_link($item['author-link'])];
813         $contact = dba::selectFirst('contact', ['id', 'network', 'rel'], $condition);
814         if (DBM::is_result($contact)) {
815                 $cid = $contact['id'];
816                 $network = $contact['network'];
817                 $rel = $contact['rel'];
818         }
819
820         if ($sparkle) {
821                 $status_link = $profile_link . '?url=status';
822                 $photos_link = $profile_link . '?url=photos';
823                 $profile_link = $profile_link . '?url=profile';
824         }
825
826         if ($cid && !$item['self']) {
827                 $poke_link = 'poke/?f=&c=' . $cid;
828                 $contact_url = 'contacts/' . $cid;
829                 $posts_link = 'contacts/' . $cid . '/posts';
830
831                 if (in_array($network, [NETWORK_DFRN, NETWORK_DIASPORA])) {
832                         $pm_url = 'message/new/' . $cid;
833                 }
834         }
835
836         if (local_user()) {
837                 $menu = [
838                         L10n::t('Follow Thread') => $sub_link,
839                         L10n::t('View Status') => $status_link,
840                         L10n::t('View Profile') => $profile_link,
841                         L10n::t('View Photos') => $photos_link,
842                         L10n::t('Network Posts') => $posts_link,
843                         L10n::t('View Contact') => $contact_url,
844                         L10n::t('Send PM') => $pm_url
845                 ];
846
847                 if ($network == NETWORK_DFRN) {
848                         $menu[L10n::t("Poke")] = $poke_link;
849                 }
850
851                 if ((($cid == 0) || ($rel == CONTACT_IS_FOLLOWER)) &&
852                         in_array($item['network'], [NETWORK_DFRN, NETWORK_OSTATUS, NETWORK_DIASPORA])) {
853                         $menu[L10n::t('Connect/Follow')] = 'follow?url=' . urlencode($item['author-link']);
854                 }
855         } else {
856                 $menu = [L10n::t('View Profile') => $item['author-link']];
857         }
858
859         $args = ['item' => $item, 'menu' => $menu];
860
861         Addon::callHooks('item_photo_menu', $args);
862
863         $menu = $args['menu'];
864
865         $o = '';
866         foreach ($menu as $k => $v) {
867                 if (strpos($v, 'javascript:') === 0) {
868                         $v = substr($v, 11);
869                         $o .= '<li role="menuitem"><a onclick="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
870                 } elseif ($v!='') {
871                         $o .= '<li role="menuitem"><a href="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
872                 }
873         }
874         return $o;
875 }
876
877 /**
878  * @brief Checks item to see if it is one of the builtin activities (like/dislike, event attendance, consensus items, etc.)
879  * Increments the count of each matching activity and adds a link to the author as needed.
880  *
881  * @param array $item
882  * @param array &$conv_responses (already created with builtin activity structure)
883  * @return void
884  */
885 function builtin_activity_puller($item, &$conv_responses) {
886         foreach ($conv_responses as $mode => $v) {
887                 $url = '';
888                 $sparkle = '';
889
890                 switch ($mode) {
891                         case 'like':
892                                 $verb = ACTIVITY_LIKE;
893                                 break;
894                         case 'dislike':
895                                 $verb = ACTIVITY_DISLIKE;
896                                 break;
897                         case 'attendyes':
898                                 $verb = ACTIVITY_ATTEND;
899                                 break;
900                         case 'attendno':
901                                 $verb = ACTIVITY_ATTENDNO;
902                                 break;
903                         case 'attendmaybe':
904                                 $verb = ACTIVITY_ATTENDMAYBE;
905                                 break;
906                         default:
907                                 return;
908                 }
909
910                 if (activity_match($item['verb'], $verb) && ($item['id'] != $item['parent'])) {
911                         $url = Contact::MagicLinkbyId($item['author-id']);
912                         if (strpos($url, 'redir/') === 0) {
913                                 $sparkle = ' class="sparkle" ';
914                         }
915
916                         $url = '<a href="'. $url . '"'. $sparkle .'>' . htmlentities($item['author-name']) . '</a>';
917
918                         if (!x($item, 'thr-parent')) {
919                                 $item['thr-parent'] = $item['parent-uri'];
920                         }
921
922                         if (!(isset($conv_responses[$mode][$item['thr-parent'] . '-l'])
923                                 && is_array($conv_responses[$mode][$item['thr-parent'] . '-l']))) {
924                                 $conv_responses[$mode][$item['thr-parent'] . '-l'] = [];
925                         }
926
927                         // only list each unique author once
928                         if (in_array($url,$conv_responses[$mode][$item['thr-parent'] . '-l'])) {
929                                 continue;
930                         }
931
932                         if (!isset($conv_responses[$mode][$item['thr-parent']])) {
933                                 $conv_responses[$mode][$item['thr-parent']] = 1;
934                         } else {
935                                 $conv_responses[$mode][$item['thr-parent']] ++;
936                         }
937
938                         if (public_contact() == $item['author-id']) {
939                                 $conv_responses[$mode][$item['thr-parent'] . '-self'] = 1;
940                         }
941
942                         $conv_responses[$mode][$item['thr-parent'] . '-l'][] = $url;
943
944                         // there can only be one activity verb per item so if we found anything, we can stop looking
945                         return;
946                 }
947         }
948 }
949
950 /**
951  * Format the vote text for a profile item
952  * @param int $cnt = number of people who vote the item
953  * @param array $arr = array of pre-linked names of likers/dislikers
954  * @param string $type = one of 'like, 'dislike', 'attendyes', 'attendno', 'attendmaybe'
955  * @param int $id  = item id
956  * @return string formatted text
957  */
958 function format_like($cnt, array $arr, $type, $id) {
959         $o = '';
960         $expanded = '';
961
962         if ($cnt == 1) {
963                 $likers = $arr[0];
964
965                 // Phrase if there is only one liker. In other cases it will be uses for the expanded
966                 // list which show all likers
967                 switch ($type) {
968                         case 'like' :
969                                 $phrase = L10n::t('%s likes this.', $likers);
970                                 break;
971                         case 'dislike' :
972                                 $phrase = L10n::t('%s doesn\'t like this.', $likers);
973                                 break;
974                         case 'attendyes' :
975                                 $phrase = L10n::t('%s attends.', $likers);
976                                 break;
977                         case 'attendno' :
978                                 $phrase = L10n::t('%s doesn\'t attend.', $likers);
979                                 break;
980                         case 'attendmaybe' :
981                                 $phrase = L10n::t('%s attends maybe.', $likers);
982                                 break;
983                 }
984         }
985
986         if ($cnt > 1) {
987                 $total = count($arr);
988                 if ($total >= MAX_LIKERS) {
989                         $arr = array_slice($arr, 0, MAX_LIKERS - 1);
990                 }
991                 if ($total < MAX_LIKERS) {
992                         $last = L10n::t('and') . ' ' . $arr[count($arr)-1];
993                         $arr2 = array_slice($arr, 0, -1);
994                         $str = implode(', ', $arr2) . ' ' . $last;
995                 }
996                 if ($total >= MAX_LIKERS) {
997                         $str = implode(', ', $arr);
998                         $str .= L10n::t('and %d other people', $total - MAX_LIKERS);
999                 }
1000
1001                 $likers = $str;
1002
1003                 $spanatts = "class=\"fakelink\" onclick=\"openClose('{$type}list-$id');\"";
1004
1005                 switch ($type) {
1006                         case 'like':
1007                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> like this', $spanatts, $cnt);
1008                                 $explikers = L10n::t('%s like this.', $likers);
1009                                 break;
1010                         case 'dislike':
1011                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> don\'t like this', $spanatts, $cnt);
1012                                 $explikers = L10n::t('%s don\'t like this.', $likers);
1013                                 break;
1014                         case 'attendyes':
1015                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> attend', $spanatts, $cnt);
1016                                 $explikers = L10n::t('%s attend.', $likers);
1017                                 break;
1018                         case 'attendno':
1019                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> don\'t attend', $spanatts, $cnt);
1020                                 $explikers = L10n::t('%s don\'t attend.', $likers);
1021                                 break;
1022                         case 'attendmaybe':
1023                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> attend maybe', $spanatts, $cnt);
1024                                 $explikers = L10n::t('%s attend maybe.', $likers);
1025                                 break;
1026                 }
1027
1028                 $expanded .= "\t" . '<div class="wall-item-' . $type . '-expanded" id="' . $type . 'list-' . $id . '" style="display: none;" >' . $explikers . EOL . '</div>';
1029         }
1030
1031         $phrase .= EOL ;
1032         $o .= replace_macros(get_markup_template('voting_fakelink.tpl'), [
1033                 '$phrase' => $phrase,
1034                 '$type' => $type,
1035                 '$id' => $id
1036         ]);
1037         $o .= $expanded;
1038
1039         return $o;
1040 }
1041
1042 function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
1043 {
1044         $o = '';
1045
1046         $geotag = x($x, 'allow_location') ? replace_macros(get_markup_template('jot_geotag.tpl'), []) : '';
1047
1048         $tpl = get_markup_template('jot-header.tpl');
1049         $a->page['htmlhead'] .= replace_macros($tpl, [
1050                 '$newpost'   => 'true',
1051                 '$baseurl'   => System::baseUrl(true),
1052                 '$geotag'    => $geotag,
1053                 '$nickname'  => $x['nickname'],
1054                 '$ispublic'  => L10n::t('Visible to <strong>everybody</strong>'),
1055                 '$linkurl'   => L10n::t('Please enter a link URL:'),
1056                 '$vidurl'    => L10n::t("Please enter a video link/URL:"),
1057                 '$audurl'    => L10n::t("Please enter an audio link/URL:"),
1058                 '$term'      => L10n::t('Tag term:'),
1059                 '$fileas'    => L10n::t('Save to Folder:'),
1060                 '$whereareu' => L10n::t('Where are you right now?'),
1061                 '$delitems'  => L10n::t("Delete item\x28s\x29?")
1062         ]);
1063
1064         $tpl = get_markup_template('jot-end.tpl');
1065         $a->page['end'] .= replace_macros($tpl, [
1066                 '$newpost'   => 'true',
1067                 '$baseurl'   => System::baseUrl(true),
1068                 '$geotag'    => $geotag,
1069                 '$nickname'  => $x['nickname'],
1070                 '$ispublic'  => L10n::t('Visible to <strong>everybody</strong>'),
1071                 '$linkurl'   => L10n::t('Please enter a link URL:'),
1072                 '$vidurl'    => L10n::t("Please enter a video link/URL:"),
1073                 '$audurl'    => L10n::t("Please enter an audio link/URL:"),
1074                 '$term'      => L10n::t('Tag term:'),
1075                 '$fileas'    => L10n::t('Save to Folder:'),
1076                 '$whereareu' => L10n::t('Where are you right now?')
1077         ]);
1078
1079         $jotplugins = '';
1080         Addon::callHooks('jot_tool', $jotplugins);
1081
1082         // Private/public post links for the non-JS ACL form
1083         $private_post = 1;
1084         if (x($_REQUEST, 'public')) {
1085                 $private_post = 0;
1086         }
1087
1088         $query_str = $a->query_string;
1089         if (strpos($query_str, 'public=1') !== false) {
1090                 $query_str = str_replace(['?public=1', '&public=1'], ['', ''], $query_str);
1091         }
1092
1093         /*
1094          * I think $a->query_string may never have ? in it, but I could be wrong
1095          * It looks like it's from the index.php?q=[etc] rewrite that the web
1096          * server does, which converts any ? to &, e.g. suggest&ignore=61 for suggest?ignore=61
1097          */
1098         if (strpos($query_str, '?') === false) {
1099                 $public_post_link = '?public=1';
1100         } else {
1101                 $public_post_link = '&public=1';
1102         }
1103
1104         // $tpl = replace_macros($tpl,array('$jotplugins' => $jotplugins));
1105         $tpl = get_markup_template("jot.tpl");
1106
1107         $o .= replace_macros($tpl,[
1108                 '$new_post' => L10n::t('New Post'),
1109                 '$return_path'  => $query_str,
1110                 '$action'       => 'item',
1111                 '$share'        => defaults($x, 'button', L10n::t('Share')),
1112                 '$upload'       => L10n::t('Upload photo'),
1113                 '$shortupload'  => L10n::t('upload photo'),
1114                 '$attach'       => L10n::t('Attach file'),
1115                 '$shortattach'  => L10n::t('attach file'),
1116                 '$weblink'      => L10n::t('Insert web link'),
1117                 '$shortweblink' => L10n::t('web link'),
1118                 '$video'        => L10n::t('Insert video link'),
1119                 '$shortvideo'   => L10n::t('video link'),
1120                 '$audio'        => L10n::t('Insert audio link'),
1121                 '$shortaudio'   => L10n::t('audio link'),
1122                 '$setloc'       => L10n::t('Set your location'),
1123                 '$shortsetloc'  => L10n::t('set location'),
1124                 '$noloc'        => L10n::t('Clear browser location'),
1125                 '$shortnoloc'   => L10n::t('clear location'),
1126                 '$title'        => defaults($x, 'title', ''),
1127                 '$placeholdertitle' => L10n::t('Set title'),
1128                 '$category'     => defaults($x, 'category', ''),
1129                 '$placeholdercategory' => Feature::isEnabled(local_user(), 'categories') ? L10n::t("Categories \x28comma-separated list\x29") : '',
1130                 '$wait'         => L10n::t('Please wait'),
1131                 '$permset'      => L10n::t('Permission settings'),
1132                 '$shortpermset' => L10n::t('permissions'),
1133                 '$ptyp'         => $notes_cid ? 'note' : 'wall',
1134                 '$content'      => defaults($x, 'content', ''),
1135                 '$post_id'      => defaults($x, 'post_id', ''),
1136                 '$baseurl'      => System::baseUrl(true),
1137                 '$defloc'       => $x['default_location'],
1138                 '$visitor'      => $x['visitor'],
1139                 '$pvisit'       => $notes_cid ? 'none' : $x['visitor'],
1140                 '$public'       => L10n::t('Public post'),
1141                 '$lockstate'    => $x['lockstate'],
1142                 '$bang'         => $x['bang'],
1143                 '$profile_uid'  => $x['profile_uid'],
1144                 '$preview'      => Feature::isEnabled($x['profile_uid'], 'preview') ? L10n::t('Preview') : '',
1145                 '$jotplugins'   => $jotplugins,
1146                 '$notes_cid'    => $notes_cid,
1147                 '$sourceapp'    => L10n::t($a->sourcename),
1148                 '$cancel'       => L10n::t('Cancel'),
1149                 '$rand_num'     => random_digits(12),
1150
1151                 // ACL permissions box
1152                 '$acl'           => $x['acl'],
1153                 '$group_perms'   => L10n::t('Post to Groups'),
1154                 '$contact_perms' => L10n::t('Post to Contacts'),
1155                 '$private'       => L10n::t('Private post'),
1156                 '$is_private'    => $private_post,
1157                 '$public_link'   => $public_post_link,
1158
1159                 //jot nav tab (used in some themes)
1160                 '$message' => L10n::t('Message'),
1161                 '$browser' => L10n::t('Browser'),
1162         ]);
1163
1164
1165         if ($popup == true) {
1166                 $o = '<div id="jot-popup" style="display: none;">' . $o . '</div>';
1167         }
1168
1169         return $o;
1170 }
1171
1172 /**
1173  * Plucks the children of the given parent from a given item list.
1174  *
1175  * @brief Plucks all the children in the given item list of the given parent
1176  *
1177  * @param array $item_list
1178  * @param array $parent
1179  * @param bool $recursive
1180  * @return type
1181  */
1182 function get_item_children(array &$item_list, array $parent, $recursive = true)
1183 {
1184         $children = [];
1185         foreach ($item_list as $i => $item) {
1186                 if ($item['id'] != $item['parent']) {
1187                         if ($recursive) {
1188                                 // Fallback to parent-uri if thr-parent is not set
1189                                 $thr_parent = $item['thr-parent'];
1190                                 if ($thr_parent == '') {
1191                                         $thr_parent = $item['parent-uri'];
1192                                 }
1193
1194                                 if ($thr_parent == $parent['uri']) {
1195                                         $item['children'] = get_item_children($item_list, $item);
1196                                         $children[] = $item;
1197                                         unset($item_list[$i]);
1198                                 }
1199                         } elseif ($item['parent'] == $parent['id']) {
1200                                 $children[] = $item;
1201                                 unset($item_list[$i]);
1202                         }
1203                 }
1204         }
1205         return $children;
1206 }
1207
1208 /**
1209  * @brief Recursively sorts a tree-like item array
1210  *
1211  * @param array $items
1212  * @return array
1213  */
1214 function sort_item_children(array $items)
1215 {
1216         $result = $items;
1217         usort($result, 'sort_thr_created_rev');
1218         foreach ($result as $k => $i) {
1219                 if (isset($result[$k]['children'])) {
1220                         $result[$k]['children'] = sort_item_children($result[$k]['children']);
1221                 }
1222         }
1223         return $result;
1224 }
1225
1226 /**
1227  * @brief Recursively add all children items at the top level of a list
1228  *
1229  * @param array $children List of items to append
1230  * @param array $item_list
1231  */
1232 function add_children_to_list(array $children, array &$item_list)
1233 {
1234         foreach ($children as $child) {
1235                 $item_list[] = $child;
1236                 if (isset($child['children'])) {
1237                         add_children_to_list($child['children'], $item_list);
1238                 }
1239         }
1240 }
1241
1242 /**
1243  * This recursive function takes the item tree structure created by conv_sort() and
1244  * flatten the extraneous depth levels when people reply sequentially, removing the
1245  * stairs effect in threaded conversations limiting the available content width.
1246  *
1247  * The basic principle is the following: if a post item has only one reply and is
1248  * the last reply of its parent, then the reply is moved to the parent.
1249  *
1250  * This process is rendered somewhat more complicated because items can be either
1251  * replies or likes, and these don't factor at all in the reply count/last reply.
1252  *
1253  * @brief Selectively flattens a tree-like item structure to prevent threading stairs
1254  *
1255  * @param array $parent A tree-like array of items
1256  * @return array
1257  */
1258 function smart_flatten_conversation(array $parent)
1259 {
1260         if (!isset($parent['children']) || count($parent['children']) == 0) {
1261                 return $parent;
1262         }
1263
1264         // We use a for loop to ensure we process the newly-moved items
1265         for ($i = 0; $i < count($parent['children']); $i++) {
1266                 $child = $parent['children'][$i];
1267
1268                 if (isset($child['children']) && count($child['children'])) {
1269                         // This helps counting only the regular posts
1270                         $count_post_closure = function($var) {
1271                                 return $var['verb'] === ACTIVITY_POST;
1272                         };
1273
1274                         $child_post_count = count(array_filter($child['children'], $count_post_closure));
1275
1276                         $remaining_post_count = count(array_filter(array_slice($parent['children'], $i), $count_post_closure));
1277
1278                         // If there's only one child's children post and this is the last child post
1279                         if ($child_post_count == 1 && $remaining_post_count == 1) {
1280
1281                                 // Searches the post item in the children
1282                                 $j = 0;
1283                                 while($child['children'][$j]['verb'] !== ACTIVITY_POST && $j < count($child['children'])) {
1284                                         $j ++;
1285                                 }
1286
1287                                 $moved_item = $child['children'][$j];
1288                                 unset($parent['children'][$i]['children'][$j]);
1289                                 $parent['children'][] = $moved_item;
1290                         } else {
1291                                 $parent['children'][$i] = smart_flatten_conversation($child);
1292                         }
1293                 }
1294         }
1295
1296         return $parent;
1297 }
1298
1299
1300 /**
1301  * Expands a flat list of items into corresponding tree-like conversation structures,
1302  * sort the top-level posts either on "created" or "commented", and finally
1303  * append all the items at the top level (???)
1304  *
1305  * @brief Expands a flat item list into a conversation array for display
1306  *
1307  * @param array  $item_list A list of items belonging to one or more conversations
1308  * @param string $order     Either on "created" or "commented"
1309  * @return array
1310  */
1311 function conv_sort(array $item_list, $order)
1312 {
1313         $parents = [];
1314
1315         if (!(is_array($item_list) && count($item_list))) {
1316                 return $parents;
1317         }
1318
1319         $item_array = [];
1320
1321         // Dedupes the item list on the uri to prevent infinite loops
1322         foreach ($item_list as $item) {
1323                 $item_array[$item['uri']] = $item;
1324         }
1325
1326         // Extract the top level items
1327         foreach ($item_array as $item) {
1328                 if ($item['id'] == $item['parent']) {
1329                         $parents[] = $item;
1330                 }
1331         }
1332
1333         if (stristr($order, 'created')) {
1334                 usort($parents, 'sort_thr_created');
1335         } elseif (stristr($order, 'commented')) {
1336                 usort($parents, 'sort_thr_commented');
1337         }
1338
1339         /*
1340          * Plucks children from the item_array, second pass collects eventual orphan
1341          * items and add them as children of their top-level post.
1342          */
1343         foreach ($parents as $i => $parent) {
1344                 $parents[$i]['children'] =
1345                         array_merge(get_item_children($item_array, $parent, true),
1346                                 get_item_children($item_array, $parent, false));
1347         }
1348
1349         foreach ($parents as $i => $parent) {
1350                 $parents[$i]['children'] = sort_item_children($parents[$i]['children']);
1351         }
1352
1353         if (PConfig::get(local_user(), 'system', 'smart_threading', 0)) {
1354                 foreach ($parents as $i => $parent) {
1355                         $parents[$i] = smart_flatten_conversation($parent);
1356                 }
1357         }
1358
1359         /// @TODO: Stop recusrsively adding all children back to the top level (!!!)
1360         /// However, this apparently ensures responses (likes, attendance) display (?!)
1361         foreach ($parents as $parent) {
1362                 if (count($parent['children'])) {
1363                         add_children_to_list($parent['children'], $parents);
1364                 }
1365         }
1366
1367         return $parents;
1368 }
1369
1370 /**
1371  * @brief usort() callback to sort item arrays by the created key
1372  *
1373  * @param array $a
1374  * @param array $b
1375  * @return int
1376  */
1377 function sort_thr_created(array $a, array $b)
1378 {
1379         return strcmp($b['created'], $a['created']);
1380 }
1381
1382 /**
1383  * @brief usort() callback to reverse sort item arrays by the created key
1384  *
1385  * @param array $a
1386  * @param array $b
1387  * @return int
1388  */
1389 function sort_thr_created_rev(array $a, array $b)
1390 {
1391         return strcmp($a['created'], $b['created']);
1392 }
1393
1394 /**
1395  * @brief usort() callback to sort item arrays by the commented key
1396  *
1397  * @param array $a
1398  * @param array $b
1399  * @return type
1400  */
1401 function sort_thr_commented(array $a, array $b)
1402 {
1403         return strcmp($b['commented'], $a['commented']);
1404 }
1405
1406 function render_location_dummy(array $item) {
1407         if (x($item, 'location') && !empty($item['location'])) {
1408                 return $item['location'];
1409         }
1410
1411         if (x($item, 'coord') && !empty($item['coord'])) {
1412                 return $item['coord'];
1413         }
1414 }
1415
1416 function get_responses(array $conv_responses, array $response_verbs, $ob, array $item) {
1417         $ret = [];
1418         foreach ($response_verbs as $v) {
1419                 $ret[$v] = [];
1420                 $ret[$v]['count'] = defaults($conv_responses[$v], $item['uri'], '');
1421                 $ret[$v]['list']  = defaults($conv_responses[$v], $item['uri'] . '-l', []);
1422                 $ret[$v]['self']  = defaults($conv_responses[$v], $item['uri'] . '-self', '0');
1423                 if (count($ret[$v]['list']) > MAX_LIKERS) {
1424                         $ret[$v]['list_part'] = array_slice($ret[$v]['list'], 0, MAX_LIKERS);
1425                         array_push($ret[$v]['list_part'], '<a href="#" data-toggle="modal" data-target="#' . $v . 'Modal-'
1426                                 . (($ob) ? $ob->getId() : $item['id']) . '"><b>' . L10n::t('View all') . '</b></a>');
1427                 } else {
1428                         $ret[$v]['list_part'] = '';
1429                 }
1430                 $ret[$v]['button'] = get_response_button_text($v, $ret[$v]['count']);
1431                 $ret[$v]['title'] = $conv_responses[$v]['title'];
1432         }
1433
1434         $count = 0;
1435         foreach ($ret as $key) {
1436                 if ($key['count'] == true) {
1437                         $count++;
1438                 }
1439         }
1440         $ret['count'] = $count;
1441
1442         return $ret;
1443 }
1444
1445 function get_response_button_text($v, $count)
1446 {
1447         switch ($v) {
1448                 case 'like':
1449                         $return = L10n::tt('Like', 'Likes', $count);
1450                         break;
1451                 case 'dislike':
1452                         $return = L10n::tt('Dislike', 'Dislikes', $count);
1453                         break;
1454                 case 'attendyes':
1455                         $return = L10n::tt('Attending', 'Attending', $count);
1456                         break;
1457                 case 'attendno':
1458                         $return = L10n::tt('Not Attending', 'Not Attending', $count);
1459                         break;
1460                 case 'attendmaybe':
1461                         $return = L10n::tt('Undecided', 'Undecided', $count);
1462                         break;
1463         }
1464
1465         return $return;
1466 }