3 * @file include/conversation.php
7 use Friendica\Content\ContactSelector;
8 use Friendica\Content\Feature;
9 use Friendica\Content\Pager;
10 use Friendica\Content\Text\BBCode;
11 use Friendica\Core\Config;
12 use Friendica\Core\Hook;
13 use Friendica\Core\L10n;
14 use Friendica\Core\Logger;
15 use Friendica\Core\PConfig;
16 use Friendica\Core\Protocol;
17 use Friendica\Core\Renderer;
18 use Friendica\Core\Session;
19 use Friendica\Core\System;
20 use Friendica\Database\DBA;
22 use Friendica\Model\Contact;
23 use Friendica\Model\Item;
24 use Friendica\Model\Profile;
25 use Friendica\Model\Term;
26 use Friendica\Object\Post;
27 use Friendica\Object\Thread;
28 use Friendica\Protocol\Activity;
29 use Friendica\Util\Crypto;
30 use Friendica\Util\DateTimeFormat;
31 use Friendica\Util\Proxy as ProxyUtils;
32 use Friendica\Util\Strings;
33 use Friendica\Util\Temporal;
34 use Friendica\Util\XML;
36 function item_extract_images($body) {
43 $img_start = strpos($orig_body, '[img');
44 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
45 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
46 while (($img_st_close !== false) && ($img_end !== false)) {
48 $img_st_close++; // make it point to AFTER the closing bracket
49 $img_end += $img_start;
51 if (!strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
52 // This is an embedded image
54 $saved_image[$cnt] = substr($orig_body, $img_start + $img_st_close, $img_end - ($img_start + $img_st_close));
55 $new_body = $new_body . substr($orig_body, 0, $img_start) . '[!#saved_image' . $cnt . '#!]';
59 $new_body = $new_body . substr($orig_body, 0, $img_end + strlen('[/img]'));
62 $orig_body = substr($orig_body, $img_end + strlen('[/img]'));
64 if ($orig_body === false) {
65 // in case the body ends on a closing image tag
69 $img_start = strpos($orig_body, '[img');
70 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
71 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
74 $new_body = $new_body . $orig_body;
76 return ['body' => $new_body, 'images' => $saved_image];
79 function item_redir_and_replace_images($body, $images, $cid) {
85 $pos = BBCode::getTagPosition($origbody, 'url', 0);
86 while ($pos !== false && $cnt < 1000) {
88 $search = '/\[url\=(.*?)\]\[!#saved_image([0-9]*)#!\]\[\/url\]' . '/is';
89 $replace = '[url=' . System::baseUrl() . '/redir/' . $cid
90 . '?f=1&url=' . '$1' . '][!#saved_image' . '$2' .'#!][/url]';
92 $newbody .= substr($origbody, 0, $pos['start']['open']);
93 $subject = substr($origbody, $pos['start']['open'], $pos['end']['close'] - $pos['start']['open']);
94 $origbody = substr($origbody, $pos['end']['close']);
95 if ($origbody === false) {
99 $subject = preg_replace($search, $replace, $subject);
100 $newbody .= $subject;
103 // Isn't this supposed to use $cnt value for $occurrences? - @MrPetovan
104 $pos = BBCode::getTagPosition($origbody, 'url', 0);
106 $newbody .= $origbody;
109 foreach ($images as $image) {
111 * We're depending on the property of 'foreach' (specified on the PHP website) that
112 * it loops over the array starting from the first element and going sequentially
113 * to the last element.
115 $newbody = str_replace('[!#saved_image' . $cnt . '#!]', '[img]' . $image . '[/img]', $newbody);
122 * Render actions localized
125 * @throws ImagickException
126 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
128 function localize_item(&$item)
130 $extracted = item_extract_images($item['body']);
131 if ($extracted['images']) {
132 $item['body'] = item_redir_and_replace_images($extracted['body'], $extracted['images'], $item['contact-id']);
136 heluecht 2018-06-19: from my point of view this whole code part is useless.
137 It just renders the body message of technical posts (Like, dislike, ...).
138 But: The body isn't visible at all. So we do this stuff just because we can.
139 Even if these messages were visible, this would only mean that something went wrong.
140 During the further steps of the database restructuring I would like to address this issue.
143 $activity = DI::activity();
145 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
146 if ($activity->match($item['verb'], Activity::LIKE)
147 || $activity->match($item['verb'], Activity::DISLIKE)
148 || $activity->match($item['verb'], Activity::ATTEND)
149 || $activity->match($item['verb'], Activity::ATTENDNO)
150 || $activity->match($item['verb'], Activity::ATTENDMAYBE)) {
152 $fields = ['author-link', 'author-name', 'verb', 'object-type', 'resource-id', 'body', 'plink'];
153 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
154 if (!DBA::isResult($obj)) {
158 $author = '[url=' . $item['author-link'] . ']' . $item['author-name'] . '[/url]';
159 $objauthor = '[url=' . $obj['author-link'] . ']' . $obj['author-name'] . '[/url]';
161 switch ($obj['verb']) {
163 switch ($obj['object-type']) {
164 case Activity\ObjectType::EVENT:
165 $post_type = L10n::t('event');
168 $post_type = L10n::t('status');
172 if ($obj['resource-id']) {
173 $post_type = L10n::t('photo');
175 preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
176 $rr['plink'] = $m[1];
178 $post_type = L10n::t('status');
182 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
185 if ($activity->match($item['verb'], Activity::LIKE)) {
186 $bodyverb = L10n::t('%1$s likes %2$s\'s %3$s');
187 } elseif ($activity->match($item['verb'], Activity::DISLIKE)) {
188 $bodyverb = L10n::t('%1$s doesn\'t like %2$s\'s %3$s');
189 } elseif ($activity->match($item['verb'], Activity::ATTEND)) {
190 $bodyverb = L10n::t('%1$s attends %2$s\'s %3$s');
191 } elseif ($activity->match($item['verb'], Activity::ATTENDNO)) {
192 $bodyverb = L10n::t('%1$s doesn\'t attend %2$s\'s %3$s');
193 } elseif ($activity->match($item['verb'], Activity::ATTENDMAYBE)) {
194 $bodyverb = L10n::t('%1$s attends maybe %2$s\'s %3$s');
197 $item['body'] = sprintf($bodyverb, $author, $objauthor, $plink);
200 if ($activity->match($item['verb'], Activity::FRIEND)) {
202 if ($item['object-type']=="" || $item['object-type']!== Activity\ObjectType::PERSON) return;
204 $Aname = $item['author-name'];
205 $Alink = $item['author-link'];
207 $xmlhead="<"."?xml version='1.0' encoding='UTF-8' ?".">";
209 $obj = XML::parseString($xmlhead.$item['object']);
210 $links = XML::parseString($xmlhead."<links>".XML::unescape($obj->link)."</links>");
212 $Bname = $obj->title;
215 foreach ($links->link as $l) {
216 $atts = $l->attributes();
217 switch ($atts['rel']) {
218 case "alternate": $Blink = $atts['href']; break;
219 case "photo": $Bphoto = $atts['href']; break;
223 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
224 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
226 $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img]' . $Bphoto . '[/img][/url]';
229 $item['body'] = L10n::t('%1$s is now friends with %2$s', $A, $B)."\n\n\n".$Bphoto;
232 if (stristr($item['verb'], Activity::POKE)) {
233 $verb = urldecode(substr($item['verb'],strpos($item['verb'],'#')+1));
237 if ($item['object-type']=="" || $item['object-type']!== Activity\ObjectType::PERSON) {
241 $Aname = $item['author-name'];
242 $Alink = $item['author-link'];
244 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
246 $obj = XML::parseString($xmlhead.$item['object']);
248 $Bname = $obj->title;
252 foreach ($obj->link as $l) {
253 $atts = $l->attributes();
254 switch ($atts['rel']) {
255 case "alternate": $Blink = $atts['href'];
256 case "photo": $Bphoto = $atts['href'];
260 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
261 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
263 $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img=80x80]' . $Bphoto . '[/img][/url]';
267 * we can't have a translation string with three positions but no distinguishable text
268 * So here is the translate string.
270 $txt = L10n::t('%1$s poked %2$s');
272 // now translate the verb
273 $poked_t = trim(sprintf($txt, "", ""));
274 $txt = str_replace($poked_t, L10n::t($verb), $txt);
276 // then do the sprintf on the translation string
278 $item['body'] = sprintf($txt, $A, $B). "\n\n\n" . $Bphoto;
282 if ($activity->match($item['verb'], Activity::TAG)) {
283 $fields = ['author-id', 'author-link', 'author-name', 'author-network',
284 'verb', 'object-type', 'resource-id', 'body', 'plink'];
285 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
286 if (!DBA::isResult($obj)) {
290 $author_arr = ['uid' => 0, 'id' => $item['author-id'],
291 'network' => $item['author-network'], 'url' => $item['author-link']];
292 $author = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $item['author-name'] . '[/url]';
294 $author_arr = ['uid' => 0, 'id' => $obj['author-id'],
295 'network' => $obj['author-network'], 'url' => $obj['author-link']];
296 $objauthor = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $obj['author-name'] . '[/url]';
298 switch ($obj['verb']) {
300 switch ($obj['object-type']) {
301 case Activity\ObjectType::EVENT:
302 $post_type = L10n::t('event');
305 $post_type = L10n::t('status');
309 if ($obj['resource-id']) {
310 $post_type = L10n::t('photo');
311 $m=[]; preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
312 $rr['plink'] = $m[1];
314 $post_type = L10n::t('status');
316 // Let's break everthing ... ;-)
319 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
321 $parsedobj = XML::parseString($xmlhead.$item['object']);
323 $tag = sprintf('#[url=%s]%s[/url]', $parsedobj->id, $parsedobj->content);
324 $item['body'] = L10n::t('%1$s tagged %2$s\'s %3$s with %4$s', $author, $objauthor, $plink, $tag);
327 if ($activity->match($item['verb'], Activity::FAVORITE)) {
328 if ($item['object-type'] == "") {
332 $Aname = $item['author-name'];
333 $Alink = $item['author-link'];
335 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
337 $obj = XML::parseString($xmlhead.$item['object']);
338 if (strlen($obj->id)) {
339 $fields = ['author-link', 'author-name', 'plink'];
340 $target = Item::selectFirst($fields, ['uri' => $obj->id, 'uid' => $item['uid']]);
341 if (DBA::isResult($target) && $target['plink']) {
342 $Bname = $target['author-name'];
343 $Blink = $target['author-link'];
344 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
345 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
346 $P = '[url=' . $target['plink'] . ']' . L10n::t('post/item') . '[/url]';
347 $item['body'] = L10n::t('%1$s marked %2$s\'s %3$s as favorite', $A, $B, $P)."\n";
352 if (preg_match_all('/@\[url=(.*?)\]/is', $item['body'], $matches, PREG_SET_ORDER)) {
353 foreach ($matches as $mtch) {
354 if (!strpos($mtch[1], 'zrl=')) {
355 $item['body'] = str_replace($mtch[0], '@[url=' . Contact::magicLink($mtch[1]) . ']', $item['body']);
360 // add zrl's to public images
361 $photo_pattern = "/\[url=(.*?)\/photos\/(.*?)\/image\/(.*?)\]\[img(.*?)\]h(.*?)\[\/img\]\[\/url\]/is";
362 if (preg_match($photo_pattern, $item['body'])) {
363 $photo_replace = '[url=' . Profile::zrl('$1' . '/photos/' . '$2' . '/image/' . '$3' ,true) . '][img' . '$4' . ']h' . '$5' . '[/img][/url]';
364 $item['body'] = BBCode::pregReplaceInTag($photo_pattern, $photo_replace, 'url', $item['body']);
367 // add sparkle links to appropriate permalinks
368 $author = ['uid' => 0, 'id' => $item['author-id'],
369 'network' => $item['author-network'], 'url' => $item['author-link']];
371 // Only create a redirection to a magic link when logged in
372 if (!empty($item['plink']) && Session::isAuthenticated()) {
373 $item['plink'] = Contact::magicLinkByContact($author, $item['plink']);
378 * Count the total of comments on this item and its desendants
379 * @TODO proper type-hint + doc-tag
383 function count_descendants($item) {
384 $total = count($item['children']);
387 foreach ($item['children'] as $child) {
388 if (!visible_activity($child)) {
391 $total += count_descendants($child);
398 function visible_activity($item) {
400 $activity = DI::activity();
402 if (empty($item['verb']) || $activity->isHidden($item['verb'])) {
406 // @TODO below if() block can be rewritten to a single line: $isVisible = allConditionsHere;
407 if ($activity->match($item['verb'], Activity::FOLLOW) &&
408 $item['object-type'] === Activity\ObjectType::NOTE &&
409 empty($item['self']) &&
410 $item['uid'] == local_user()) {
417 function conv_get_blocklist()
423 $str_blocked = PConfig::get(local_user(), 'system', 'blocked');
424 if (empty($str_blocked)) {
430 foreach (explode(',', $str_blocked) as $entry) {
431 // The 4th parameter guarantees that there always will be a public contact entry
432 $cid = Contact::getIdForURL(trim($entry), 0, true, ['url' => trim($entry)]);
442 * "Render" a conversation or list of items for HTML display.
443 * There are two major forms of display:
444 * - Sequential or unthreaded ("New Item View" or search results)
445 * - conversation view
446 * The $mode parameter decides between the various renderings and also
447 * figures out how to determine page owner and other contextual items
448 * that are based on unique features of the calling module.
450 * @param array $items
451 * @param Pager $pager
454 * @param bool $preview
455 * @param string $order
458 * @throws ImagickException
459 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
461 function conversation(App $a, array $items, Pager $pager, $mode, $update, $preview = false, $order = 'commented', $uid = 0)
463 $ssl_state = (local_user() ? true : false);
466 $live_update_div = '';
468 $blocklist = conv_get_blocklist();
470 $previewing = (($preview) ? ' preview ' : '');
472 if ($mode === 'network') {
473 $items = conversation_add_children($items, false, $order, $uid);
474 $profile_owner = local_user();
477 * The special div is needed for liveUpdate to kick in for this page.
478 * We only launch liveUpdate if you aren't filtering in some incompatible
479 * way and also you aren't writing a comment (discovered in javascript).
481 $live_update_div = '<div id="live-network"></div>' . "\r\n"
482 . "<script> var profile_uid = " . $_SESSION['uid']
483 . "; var netargs = '" . substr($a->cmd, 8)
485 . (!empty($_GET['cid']) ? '&cid=' . rawurlencode($_GET['cid']) : '')
486 . (!empty($_GET['search']) ? '&search=' . rawurlencode($_GET['search']) : '')
487 . (!empty($_GET['star']) ? '&star=' . rawurlencode($_GET['star']) : '')
488 . (!empty($_GET['order']) ? '&order=' . rawurlencode($_GET['order']) : '')
489 . (!empty($_GET['bmark']) ? '&bmark=' . rawurlencode($_GET['bmark']) : '')
490 . (!empty($_GET['liked']) ? '&liked=' . rawurlencode($_GET['liked']) : '')
491 . (!empty($_GET['conv']) ? '&conv=' . rawurlencode($_GET['conv']) : '')
492 . (!empty($_GET['nets']) ? '&nets=' . rawurlencode($_GET['nets']) : '')
493 . (!empty($_GET['cmin']) ? '&cmin=' . rawurlencode($_GET['cmin']) : '')
494 . (!empty($_GET['cmax']) ? '&cmax=' . rawurlencode($_GET['cmax']) : '')
495 . (!empty($_GET['file']) ? '&file=' . rawurlencode($_GET['file']) : '')
497 . "'; var profile_page = " . $pager->getPage() . "; </script>\r\n";
499 } elseif ($mode === 'profile') {
500 $items = conversation_add_children($items, false, $order, $uid);
501 $profile_owner = $a->profile['profile_uid'];
505 if (!empty($_GET['tab'])) {
506 $tab = Strings::escapeTags(trim($_GET['tab']));
508 if ($tab === 'posts') {
510 * This is ugly, but we can't pass the profile_uid through the session to the ajax updater,
511 * because browser prefetching might change it on us. We have to deliver it with the page.
514 $live_update_div = '<div id="live-profile"></div>' . "\r\n"
515 . "<script> var profile_uid = " . $a->profile['profile_uid']
516 . "; var netargs = '?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
519 } elseif ($mode === 'notes') {
520 $items = conversation_add_children($items, false, $order, local_user());
521 $profile_owner = local_user();
524 $live_update_div = '<div id="live-notes"></div>' . "\r\n"
525 . "<script> var profile_uid = " . local_user()
526 . "; var netargs = '/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
528 } elseif ($mode === 'display') {
529 $items = conversation_add_children($items, false, $order, $uid);
530 $profile_owner = $a->profile['uid'];
533 $live_update_div = '<div id="live-display"></div>' . "\r\n"
534 . "<script> var profile_uid = " . Session::get('uid', 0) . ";"
535 . " var profile_page = 1; </script>";
537 } elseif ($mode === 'community') {
538 $items = conversation_add_children($items, true, $order, $uid);
542 $live_update_div = '<div id="live-community"></div>' . "\r\n"
543 . "<script> var profile_uid = -1; var netargs = '" . substr($a->cmd, 10)
544 ."/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
546 } elseif ($mode === 'contacts') {
547 $items = conversation_add_children($items, false, $order, $uid);
551 $live_update_div = '<div id="live-contacts"></div>' . "\r\n"
552 . "<script> var profile_uid = -1; var netargs = '" . substr($a->cmd, 9)
553 ."/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
555 } elseif ($mode === 'search') {
556 $live_update_div = '<div id="live-search"></div>' . "\r\n";
559 $page_dropping = ((local_user() && local_user() == $profile_owner) ? true : false);
562 $_SESSION['return_path'] = DI::args()->getQueryString();
565 $cb = ['items' => $items, 'mode' => $mode, 'update' => $update, 'preview' => $preview];
566 Hook::callAll('conversation_start',$cb);
568 $items = $cb['items'];
571 'like' => ['title' => L10n::t('Likes','title')],
572 'dislike' => ['title' => L10n::t('Dislikes','title')],
573 'attendyes' => ['title' => L10n::t('Attending','title')],
574 'attendno' => ['title' => L10n::t('Not attending','title')],
575 'attendmaybe' => ['title' => L10n::t('Might attend','title')],
576 'announce' => ['title' => L10n::t('Reshares','title')]
579 // array with html for each thread (parent+comments)
583 $page_template = Renderer::getMarkupTemplate("conversation.tpl");
585 if (!empty($items)) {
586 if (in_array($mode, ['community', 'contacts'])) {
589 $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED);
596 if (in_array($mode, ['network-new', 'search', 'contact-posts'])) {
599 * "New Item View" on network page or search page results
600 * - just loop through the items and format them minimally for display
603 $tpl = 'search_item.tpl';
605 foreach ($items as $item) {
607 if (!visible_activity($item)) {
611 if (in_array($item['author-id'], $blocklist)) {
621 // prevent private email from leaking.
622 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
626 $profile_name = $item['author-name'];
627 if (!empty($item['author-link']) && empty($item['author-name'])) {
628 $profile_name = $item['author-link'];
631 $tags = Term::populateTagsFromItem($item);
633 $author = ['uid' => 0, 'id' => $item['author-id'],
634 'network' => $item['author-network'], 'url' => $item['author-link']];
635 $profile_link = Contact::magicLinkByContact($author);
637 if (strpos($profile_link, 'redir/') === 0) {
638 $sparkle = ' sparkle';
641 $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
642 Hook::callAll('render_location',$locate);
644 $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
646 localize_item($item);
647 if ($mode === 'network-new') {
654 'dropping' => $dropping,
655 'pagedrop' => $page_dropping,
656 'select' => L10n::t('Select'),
657 'delete' => L10n::t('Delete'),
661 $isstarred = "unstarred";
664 $likebuttons = false;
666 $body = Item::prepareBody($item, true, $preview);
668 list($categories, $folders) = DI::contentItem()->determineCategoriesTerms($item);
670 if (!empty($item['content-warning']) && PConfig::get(local_user(), 'system', 'disable_cw', false)) {
671 $title = ucfirst($item['content-warning']);
673 $title = $item['title'];
678 'id' => ($preview ? 'P0' : $item['id']),
679 'guid' => ($preview ? 'Q0' : $item['guid']),
680 'network' => $item['network'],
681 'network_name' => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']),
682 'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link']),
683 'linktitle' => L10n::t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
684 'profile_url' => $profile_link,
685 'item_photo_menu' => item_photo_menu($item),
686 'name' => $profile_name,
687 'sparkle' => $sparkle,
689 'thumb' => System::removedBaseUrl(ProxyUtils::proxifyUrl($item['author-avatar'], false, ProxyUtils::SIZE_THUMB)),
692 'tags' => $tags['tags'],
693 'hashtags' => $tags['hashtags'],
694 'mentions' => $tags['mentions'],
695 'implicit_mentions' => $tags['implicit_mentions'],
696 'txt_cats' => L10n::t('Categories:'),
697 'txt_folders' => L10n::t('Filed under:'),
698 'has_cats' => ((count($categories)) ? 'true' : ''),
699 'has_folders' => ((count($folders)) ? 'true' : ''),
700 'categories' => $categories,
701 'folders' => $folders,
702 'text' => strip_tags($body),
703 'localtime' => DateTimeFormat::local($item['created'], 'r'),
704 'ago' => (($item['app']) ? L10n::t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
705 'location' => $location,
707 'owner_name' => $owner_name,
708 'owner_url' => $owner_url,
709 'owner_photo' => System::removedBaseUrl(ProxyUtils::proxifyUrl($item['owner-avatar'], false, ProxyUtils::SIZE_THUMB)),
710 'plink' => Item::getPlink($item),
712 'isstarred' => $isstarred,
715 'vote' => $likebuttons,
719 'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> L10n::t('View in context')]),
720 'previewing' => $previewing,
721 'wait' => L10n::t('Please wait'),
725 $arr = ['item' => $item, 'output' => $tmp_item];
726 Hook::callAll('display_item', $arr);
728 $threads[$threadsid]['id'] = $item['id'];
729 $threads[$threadsid]['network'] = $item['network'];
730 $threads[$threadsid]['items'] = [$arr['output']];
735 $page_template = Renderer::getMarkupTemplate("threaded_conversation.tpl");
737 $conv = new Thread($mode, $preview, $writable);
740 * get all the topmost parents
741 * this shouldn't be needed, as we should have only them in our array
742 * But for now, this array respects the old style, just in case
744 foreach ($items as $item) {
745 if (in_array($item['author-id'], $blocklist)) {
749 // Can we put this after the visibility check?
750 builtin_activity_puller($item, $conv_responses);
752 // Only add what is visible
753 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
757 if (!visible_activity($item)) {
761 /// @todo Check if this call is needed or not
762 $arr = ['item' => $item];
763 Hook::callAll('display_item', $arr);
765 $item['pagedrop'] = $page_dropping;
767 if ($item['id'] == $item['parent']) {
768 $item_object = new Post($item);
769 $conv->addParent($item_object);
773 $threads = $conv->getTemplateData($conv_responses);
775 Logger::log('[ERROR] conversation : Failed to get template data.', Logger::DEBUG);
781 $o = Renderer::replaceMacros($page_template, [
782 '$baseurl' => System::baseUrl($ssl_state),
783 '$return_path' => DI::args()->getQueryString(),
784 '$live_update' => $live_update_div,
785 '$remove' => L10n::t('remove'),
788 '$threads' => $threads,
789 '$dropping' => ($page_dropping ? L10n::t('Delete Selected Items') : False),
796 * Fetch all comments from a query. Additionally set the newest resharer as thread owner.
798 * @param array $thread_items Database statement with thread posts
799 * @param boolean $pinned Is the item pinned?
801 * @return array items with parents and comments
803 function conversation_fetch_comments($thread_items, $pinned) {
810 while ($row = Item::fetch($thread_items)) {
811 if (($row['verb'] == Activity::ANNOUNCE) && !empty($row['contact-uid']) && ($row['received'] > $received) && ($row['thr-parent'] == $row['parent-uri'])) {
812 $actor = ['link' => $row['author-link'], 'avatar' => $row['author-avatar'], 'name' => $row['author-name']];
813 $received = $row['received'];
816 if ((($row['gravity'] == GRAVITY_PARENT) && !$row['origin'] && !in_array($row['network'], [Protocol::DIASPORA])) &&
817 (empty($row['contact-uid']) || !in_array($row['network'], Protocol::NATIVE_SUPPORT))) {
818 $parentlines[] = $lineno;
821 if ($row['gravity'] == GRAVITY_PARENT) {
822 $row['pinned'] = $pinned;
829 DBA::close($thread_items);
831 if (!empty($actor)) {
832 foreach ($parentlines as $line) {
833 $comments[$line]['owner-link'] = $actor['link'];
834 $comments[$line]['owner-avatar'] = $actor['avatar'];
835 $comments[$line]['owner-name'] = $actor['name'];
842 * @brief Add comments to top level entries that had been fetched before
844 * The system will fetch the comments for the local user whenever possible.
845 * This behaviour is currently needed to allow commenting on Friendica posts.
847 * @param array $parents Parent items
849 * @param $block_authors
852 * @return array items with parents and comments
853 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
855 function conversation_add_children(array $parents, $block_authors, $order, $uid) {
856 $max_comments = Config::get('system', 'max_comments', 100);
858 $params = ['order' => ['uid', 'commented' => true]];
860 if ($max_comments > 0) {
861 $params['limit'] = $max_comments;
866 foreach ($parents AS $parent) {
867 $condition = ["`item`.`parent-uri` = ? AND `item`.`uid` IN (0, ?) ",
868 $parent['uri'], $uid];
869 if ($block_authors) {
870 $condition[0] .= "AND NOT `author`.`hidden`";
873 $thread_items = Item::selectForUser(local_user(), array_merge(Item::DISPLAY_FIELDLIST, ['contact-uid', 'gravity']), $condition, $params);
875 $comments = conversation_fetch_comments($thread_items, $parent['pinned'] ?? false);
877 if (count($comments) != 0) {
878 $items = array_merge($items, $comments);
882 foreach ($items as $index => $item) {
883 if ($item['uid'] == 0) {
884 $items[$index]['writable'] = in_array($item['network'], Protocol::FEDERATED);
888 $items = conv_sort($items, $order);
893 function item_photo_menu($item) {
904 if (local_user() && local_user() == $item['uid'] && $item['parent'] == $item['id'] && !$item['self']) {
905 $sub_link = 'javascript:dosubthread(' . $item['id'] . '); return false;';
908 $author = ['uid' => 0, 'id' => $item['author-id'],
909 'network' => $item['author-network'], 'url' => $item['author-link']];
910 $profile_link = Contact::magicLinkByContact($author, $item['author-link']);
911 $sparkle = (strpos($profile_link, 'redir/') === 0);
914 $pcid = Contact::getIdForURL($item['author-link'], 0, true);
917 $condition = ['uid' => local_user(), 'nurl' => Strings::normaliseLink($item['author-link'])];
918 $contact = DBA::selectFirst('contact', ['id', 'network', 'rel'], $condition);
919 if (DBA::isResult($contact)) {
920 $cid = $contact['id'];
921 $network = $contact['network'];
922 $rel = $contact['rel'];
926 $status_link = $profile_link . '?tab=status';
927 $photos_link = str_replace('/profile/', '/photos/', $profile_link);
928 $profile_link = $profile_link . '?=profile';
932 $contact_url = 'contact/' . $pcid;
933 $posts_link = 'contact/' . $pcid . '/posts';
934 $block_link = 'contact/' . $pcid . '/block';
935 $ignore_link = 'contact/' . $pcid . '/ignore';
938 if ($cid && !$item['self']) {
939 $poke_link = 'poke/?f=&c=' . $cid;
940 $contact_url = 'contact/' . $cid;
941 $posts_link = 'contact/' . $cid . '/posts';
943 if (in_array($network, [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA])) {
944 $pm_url = 'message/new/' . $cid;
950 L10n::t('Follow Thread') => $sub_link,
951 L10n::t('View Status') => $status_link,
952 L10n::t('View Profile') => $profile_link,
953 L10n::t('View Photos') => $photos_link,
954 L10n::t('Network Posts') => $posts_link,
955 L10n::t('View Contact') => $contact_url,
956 L10n::t('Send PM') => $pm_url,
957 L10n::t('Block') => $block_link,
958 L10n::t('Ignore') => $ignore_link
961 if ($network == Protocol::DFRN) {
962 $menu[L10n::t("Poke")] = $poke_link;
965 if ((($cid == 0) || ($rel == Contact::FOLLOWER)) &&
966 in_array($item['network'], Protocol::FEDERATED)) {
967 $menu[L10n::t('Connect/Follow')] = 'follow?url=' . urlencode($item['author-link']);
970 $menu = [L10n::t('View Profile') => $item['author-link']];
973 $args = ['item' => $item, 'menu' => $menu];
975 Hook::callAll('item_photo_menu', $args);
977 $menu = $args['menu'];
980 foreach ($menu as $k => $v) {
981 if (strpos($v, 'javascript:') === 0) {
983 $o .= '<li role="menuitem"><a onclick="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
985 $o .= '<li role="menuitem"><a href="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
992 * @brief Checks item to see if it is one of the builtin activities (like/dislike, event attendance, consensus items, etc.)
993 * Increments the count of each matching activity and adds a link to the author as needed.
996 * @param array &$conv_responses (already created with builtin activity structure)
998 * @throws ImagickException
999 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1001 function builtin_activity_puller($item, &$conv_responses) {
1002 foreach ($conv_responses as $mode => $v) {
1007 $verb = Activity::LIKE;
1010 $verb = Activity::DISLIKE;
1013 $verb = Activity::ATTEND;
1016 $verb = Activity::ATTENDNO;
1019 $verb = Activity::ATTENDMAYBE;
1022 $verb = Activity::ANNOUNCE;
1028 if (!empty($item['verb']) && DI::activity()->match($item['verb'], $verb) && ($item['id'] != $item['parent'])) {
1029 $author = ['uid' => 0, 'id' => $item['author-id'],
1030 'network' => $item['author-network'], 'url' => $item['author-link']];
1031 $url = Contact::magicLinkByContact($author);
1032 if (strpos($url, 'redir/') === 0) {
1033 $sparkle = ' class="sparkle" ';
1036 $url = '<a href="'. $url . '"'. $sparkle .'>' . htmlentities($item['author-name']) . '</a>';
1038 if (empty($item['thr-parent'])) {
1039 $item['thr-parent'] = $item['parent-uri'];
1042 if (!(isset($conv_responses[$mode][$item['thr-parent'] . '-l'])
1043 && is_array($conv_responses[$mode][$item['thr-parent'] . '-l']))) {
1044 $conv_responses[$mode][$item['thr-parent'] . '-l'] = [];
1047 // only list each unique author once
1048 if (in_array($url,$conv_responses[$mode][$item['thr-parent'] . '-l'])) {
1052 if (!isset($conv_responses[$mode][$item['thr-parent']])) {
1053 $conv_responses[$mode][$item['thr-parent']] = 1;
1055 $conv_responses[$mode][$item['thr-parent']] ++;
1058 if (public_contact() == $item['author-id']) {
1059 $conv_responses[$mode][$item['thr-parent'] . '-self'] = 1;
1062 $conv_responses[$mode][$item['thr-parent'] . '-l'][] = $url;
1064 // there can only be one activity verb per item so if we found anything, we can stop looking
1071 * Format the vote text for a profile item
1073 * @param int $cnt = number of people who vote the item
1074 * @param array $arr = array of pre-linked names of likers/dislikers
1075 * @param string $type = one of 'like, 'dislike', 'attendyes', 'attendno', 'attendmaybe'
1076 * @param int $id = item id
1077 * @return string formatted text
1078 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1080 function format_like($cnt, array $arr, $type, $id) {
1088 // Phrase if there is only one liker. In other cases it will be uses for the expanded
1089 // list which show all likers
1092 $phrase = L10n::t('%s likes this.', $likers);
1095 $phrase = L10n::t('%s doesn\'t like this.', $likers);
1098 $phrase = L10n::t('%s attends.', $likers);
1101 $phrase = L10n::t('%s doesn\'t attend.', $likers);
1103 case 'attendmaybe' :
1104 $phrase = L10n::t('%s attends maybe.', $likers);
1107 $phrase = L10n::t('%s reshared this.', $likers);
1113 $total = count($arr);
1114 if ($total < MAX_LIKERS) {
1115 $last = L10n::t('and') . ' ' . $arr[count($arr)-1];
1116 $arr2 = array_slice($arr, 0, -1);
1117 $likers = implode(', ', $arr2) . ' ' . $last;
1119 $arr = array_slice($arr, 0, MAX_LIKERS - 1);
1120 $likers = implode(', ', $arr);
1121 $likers .= L10n::t('and %d other people', $total - MAX_LIKERS);
1124 $spanatts = "class=\"fakelink\" onclick=\"openClose('{$type}list-$id');\"";
1129 $phrase = L10n::t('<span %1$s>%2$d people</span> like this', $spanatts, $cnt);
1130 $explikers = L10n::t('%s like this.', $likers);
1133 $phrase = L10n::t('<span %1$s>%2$d people</span> don\'t like this', $spanatts, $cnt);
1134 $explikers = L10n::t('%s don\'t like this.', $likers);
1137 $phrase = L10n::t('<span %1$s>%2$d people</span> attend', $spanatts, $cnt);
1138 $explikers = L10n::t('%s attend.', $likers);
1141 $phrase = L10n::t('<span %1$s>%2$d people</span> don\'t attend', $spanatts, $cnt);
1142 $explikers = L10n::t('%s don\'t attend.', $likers);
1145 $phrase = L10n::t('<span %1$s>%2$d people</span> attend maybe', $spanatts, $cnt);
1146 $explikers = L10n::t('%s attend maybe.', $likers);
1149 $phrase = L10n::t('<span %1$s>%2$d people</span> reshared this', $spanatts, $cnt);
1150 $explikers = L10n::t('%s reshared this.', $likers);
1154 $expanded .= "\t" . '<p class="wall-item-' . $type . '-expanded" id="' . $type . 'list-' . $id . '" style="display: none;" >' . $explikers . EOL . '</p>';
1157 $o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('voting_fakelink.tpl'), [
1158 '$phrase' => $phrase,
1167 function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
1171 $geotag = !empty($x['allow_location']) ? Renderer::replaceMacros(Renderer::getMarkupTemplate('jot_geotag.tpl'), []) : '';
1173 $tpl = Renderer::getMarkupTemplate('jot-header.tpl');
1174 $a->page['htmlhead'] .= Renderer::replaceMacros($tpl, [
1175 '$newpost' => 'true',
1176 '$baseurl' => System::baseUrl(true),
1177 '$geotag' => $geotag,
1178 '$nickname' => $x['nickname'],
1179 '$ispublic' => L10n::t('Visible to <strong>everybody</strong>'),
1180 '$linkurl' => L10n::t('Please enter a image/video/audio/webpage URL:'),
1181 '$term' => L10n::t('Tag term:'),
1182 '$fileas' => L10n::t('Save to Folder:'),
1183 '$whereareu' => L10n::t('Where are you right now?'),
1184 '$delitems' => L10n::t("Delete item\x28s\x29?")
1188 Hook::callAll('jot_tool', $jotplugins);
1190 // Private/public post links for the non-JS ACL form
1192 if (!empty($_REQUEST['public'])) {
1196 $query_str = DI::args()->getQueryString();
1197 if (strpos($query_str, 'public=1') !== false) {
1198 $query_str = str_replace(['?public=1', '&public=1'], ['', ''], $query_str);
1202 * I think $a->query_string may never have ? in it, but I could be wrong
1203 * It looks like it's from the index.php?q=[etc] rewrite that the web
1204 * server does, which converts any ? to &, e.g. suggest&ignore=61 for suggest?ignore=61
1206 if (strpos($query_str, '?') === false) {
1207 $public_post_link = '?public=1';
1209 $public_post_link = '&public=1';
1212 // $tpl = Renderer::replaceMacros($tpl,array('$jotplugins' => $jotplugins));
1213 $tpl = Renderer::getMarkupTemplate("jot.tpl");
1215 $o .= Renderer::replaceMacros($tpl,[
1216 '$new_post' => L10n::t('New Post'),
1217 '$return_path' => $query_str,
1218 '$action' => 'item',
1219 '$share' => ($x['button'] ?? '') ?: L10n::t('Share'),
1220 '$upload' => L10n::t('Upload photo'),
1221 '$shortupload' => L10n::t('upload photo'),
1222 '$attach' => L10n::t('Attach file'),
1223 '$shortattach' => L10n::t('attach file'),
1224 '$edbold' => L10n::t('Bold'),
1225 '$editalic' => L10n::t('Italic'),
1226 '$eduline' => L10n::t('Underline'),
1227 '$edquote' => L10n::t('Quote'),
1228 '$edcode' => L10n::t('Code'),
1229 '$edimg' => L10n::t('Image'),
1230 '$edurl' => L10n::t('Link'),
1231 '$edattach' => L10n::t('Link or Media'),
1232 '$setloc' => L10n::t('Set your location'),
1233 '$shortsetloc' => L10n::t('set location'),
1234 '$noloc' => L10n::t('Clear browser location'),
1235 '$shortnoloc' => L10n::t('clear location'),
1236 '$title' => $x['title'] ?? '',
1237 '$placeholdertitle' => L10n::t('Set title'),
1238 '$category' => $x['category'] ?? '',
1239 '$placeholdercategory' => Feature::isEnabled(local_user(), 'categories') ? L10n::t("Categories \x28comma-separated list\x29") : '',
1240 '$wait' => L10n::t('Please wait'),
1241 '$permset' => L10n::t('Permission settings'),
1242 '$shortpermset' => L10n::t('permissions'),
1243 '$wall' => $notes_cid ? 0 : 1,
1244 '$posttype' => $notes_cid ? Item::PT_PERSONAL_NOTE : Item::PT_ARTICLE,
1245 '$content' => $x['content'] ?? '',
1246 '$post_id' => $x['post_id'] ?? '',
1247 '$baseurl' => System::baseUrl(true),
1248 '$defloc' => $x['default_location'],
1249 '$visitor' => $x['visitor'],
1250 '$pvisit' => $notes_cid ? 'none' : $x['visitor'],
1251 '$public' => L10n::t('Public post'),
1252 '$lockstate' => $x['lockstate'],
1253 '$bang' => $x['bang'],
1254 '$profile_uid' => $x['profile_uid'],
1255 '$preview' => L10n::t('Preview'),
1256 '$jotplugins' => $jotplugins,
1257 '$notes_cid' => $notes_cid,
1258 '$sourceapp' => L10n::t($a->sourcename),
1259 '$cancel' => L10n::t('Cancel'),
1260 '$rand_num' => Crypto::randomDigits(12),
1262 // ACL permissions box
1263 '$acl' => $x['acl'],
1264 '$group_perms' => L10n::t('Post to Groups'),
1265 '$contact_perms' => L10n::t('Post to Contacts'),
1266 '$private' => L10n::t('Private post'),
1267 '$is_private' => $private_post,
1268 '$public_link' => $public_post_link,
1270 //jot nav tab (used in some themes)
1271 '$message' => L10n::t('Message'),
1272 '$browser' => L10n::t('Browser'),
1276 if ($popup == true) {
1277 $o = '<div id="jot-popup" style="display: none;">' . $o . '</div>';
1284 * Plucks the children of the given parent from a given item list.
1286 * @brief Plucks all the children in the given item list of the given parent
1288 * @param array $item_list
1289 * @param array $parent
1290 * @param bool $recursive
1293 function get_item_children(array &$item_list, array $parent, $recursive = true)
1296 foreach ($item_list as $i => $item) {
1297 if ($item['id'] != $item['parent']) {
1299 // Fallback to parent-uri if thr-parent is not set
1300 $thr_parent = $item['thr-parent'];
1301 if ($thr_parent == '') {
1302 $thr_parent = $item['parent-uri'];
1305 if ($thr_parent == $parent['uri']) {
1306 $item['children'] = get_item_children($item_list, $item);
1307 $children[] = $item;
1308 unset($item_list[$i]);
1310 } elseif ($item['parent'] == $parent['id']) {
1311 $children[] = $item;
1312 unset($item_list[$i]);
1320 * @brief Recursively sorts a tree-like item array
1322 * @param array $items
1325 function sort_item_children(array $items)
1328 usort($result, 'sort_thr_received_rev');
1329 foreach ($result as $k => $i) {
1330 if (isset($result[$k]['children'])) {
1331 $result[$k]['children'] = sort_item_children($result[$k]['children']);
1338 * @brief Recursively add all children items at the top level of a list
1340 * @param array $children List of items to append
1341 * @param array $item_list
1343 function add_children_to_list(array $children, array &$item_list)
1345 foreach ($children as $child) {
1346 $item_list[] = $child;
1347 if (isset($child['children'])) {
1348 add_children_to_list($child['children'], $item_list);
1354 * This recursive function takes the item tree structure created by conv_sort() and
1355 * flatten the extraneous depth levels when people reply sequentially, removing the
1356 * stairs effect in threaded conversations limiting the available content width.
1358 * The basic principle is the following: if a post item has only one reply and is
1359 * the last reply of its parent, then the reply is moved to the parent.
1361 * This process is rendered somewhat more complicated because items can be either
1362 * replies or likes, and these don't factor at all in the reply count/last reply.
1364 * @brief Selectively flattens a tree-like item structure to prevent threading stairs
1366 * @param array $parent A tree-like array of items
1369 function smart_flatten_conversation(array $parent)
1371 if (!isset($parent['children']) || count($parent['children']) == 0) {
1375 // We use a for loop to ensure we process the newly-moved items
1376 for ($i = 0; $i < count($parent['children']); $i++) {
1377 $child = $parent['children'][$i];
1379 if (isset($child['children']) && count($child['children'])) {
1380 // This helps counting only the regular posts
1381 $count_post_closure = function($var) {
1382 return $var['verb'] === Activity::POST;
1385 $child_post_count = count(array_filter($child['children'], $count_post_closure));
1387 $remaining_post_count = count(array_filter(array_slice($parent['children'], $i), $count_post_closure));
1389 // If there's only one child's children post and this is the last child post
1390 if ($child_post_count == 1 && $remaining_post_count == 1) {
1392 // Searches the post item in the children
1394 while($child['children'][$j]['verb'] !== Activity::POST && $j < count($child['children'])) {
1398 $moved_item = $child['children'][$j];
1399 unset($parent['children'][$i]['children'][$j]);
1400 $parent['children'][] = $moved_item;
1402 $parent['children'][$i] = smart_flatten_conversation($child);
1412 * Expands a flat list of items into corresponding tree-like conversation structures,
1413 * sort the top-level posts either on "received" or "commented", and finally
1414 * append all the items at the top level (???)
1416 * @brief Expands a flat item list into a conversation array for display
1418 * @param array $item_list A list of items belonging to one or more conversations
1419 * @param string $order Either on "received" or "commented"
1421 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1423 function conv_sort(array $item_list, $order)
1427 if (!(is_array($item_list) && count($item_list))) {
1431 $blocklist = conv_get_blocklist();
1435 // Dedupes the item list on the uri to prevent infinite loops
1436 foreach ($item_list as $item) {
1437 if (in_array($item['author-id'], $blocklist)) {
1441 $item_array[$item['uri']] = $item;
1444 // Extract the top level items
1445 foreach ($item_array as $item) {
1446 if ($item['id'] == $item['parent']) {
1451 if (stristr($order, 'pinned_received')) {
1452 usort($parents, 'sort_thr_pinned_received');
1453 } elseif (stristr($order, 'received')) {
1454 usort($parents, 'sort_thr_received');
1455 } elseif (stristr($order, 'commented')) {
1456 usort($parents, 'sort_thr_commented');
1460 * Plucks children from the item_array, second pass collects eventual orphan
1461 * items and add them as children of their top-level post.
1463 foreach ($parents as $i => $parent) {
1464 $parents[$i]['children'] =
1465 array_merge(get_item_children($item_array, $parent, true),
1466 get_item_children($item_array, $parent, false));
1469 foreach ($parents as $i => $parent) {
1470 $parents[$i]['children'] = sort_item_children($parents[$i]['children']);
1473 if (!PConfig::get(local_user(), 'system', 'no_smart_threading', 0)) {
1474 foreach ($parents as $i => $parent) {
1475 $parents[$i] = smart_flatten_conversation($parent);
1479 /// @TODO: Stop recusrsively adding all children back to the top level (!!!)
1480 /// However, this apparently ensures responses (likes, attendance) display (?!)
1481 foreach ($parents as $parent) {
1482 if (count($parent['children'])) {
1483 add_children_to_list($parent['children'], $parents);
1491 * @brief usort() callback to sort item arrays by pinned and the received key
1497 function sort_thr_pinned_received(array $a, array $b)
1499 if ($b['pinned'] && !$a['pinned']) {
1501 } elseif (!$b['pinned'] && $a['pinned']) {
1505 return strcmp($b['received'], $a['received']);
1509 * @brief usort() callback to sort item arrays by the received key
1515 function sort_thr_received(array $a, array $b)
1517 return strcmp($b['received'], $a['received']);
1521 * @brief usort() callback to reverse sort item arrays by the received key
1527 function sort_thr_received_rev(array $a, array $b)
1529 return strcmp($a['received'], $b['received']);
1533 * @brief usort() callback to sort item arrays by the commented key
1539 function sort_thr_commented(array $a, array $b)
1541 return strcmp($b['commented'], $a['commented']);
1544 function render_location_dummy(array $item) {
1545 if (!empty($item['location']) && !empty($item['location'])) {
1546 return $item['location'];
1549 if (!empty($item['coord']) && !empty($item['coord'])) {
1550 return $item['coord'];
1554 function get_responses(array $conv_responses, array $response_verbs, array $item, Post $ob = null) {
1556 foreach ($response_verbs as $v) {
1558 $ret[$v]['count'] = $conv_responses[$v][$item['uri']] ?? 0;
1559 $ret[$v]['list'] = $conv_responses[$v][$item['uri'] . '-l'] ?? [];
1560 $ret[$v]['self'] = $conv_responses[$v][$item['uri'] . '-self'] ?? '0';
1561 if (count($ret[$v]['list']) > MAX_LIKERS) {
1562 $ret[$v]['list_part'] = array_slice($ret[$v]['list'], 0, MAX_LIKERS);
1563 array_push($ret[$v]['list_part'], '<a href="#" data-toggle="modal" data-target="#' . $v . 'Modal-'
1564 . (($ob) ? $ob->getId() : $item['id']) . '"><b>' . L10n::t('View all') . '</b></a>');
1566 $ret[$v]['list_part'] = '';
1568 $ret[$v]['button'] = get_response_button_text($v, $ret[$v]['count']);
1569 $ret[$v]['title'] = $conv_responses[$v]['title'];
1573 foreach ($ret as $key) {
1574 if ($key['count'] == true) {
1578 $ret['count'] = $count;
1583 function get_response_button_text($v, $count)
1588 $return = L10n::tt('Like', 'Likes', $count);
1591 $return = L10n::tt('Dislike', 'Dislikes', $count);
1594 $return = L10n::tt('Attending', 'Attending', $count);
1597 $return = L10n::tt('Not Attending', 'Not Attending', $count);
1600 $return = L10n::tt('Undecided', 'Undecided', $count);