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\Database\DBA;
21 use Friendica\Model\Contact;
22 use Friendica\Model\Item;
23 use Friendica\Model\Profile;
24 use Friendica\Model\Term;
25 use Friendica\Object\Post;
26 use Friendica\Object\Thread;
27 use Friendica\Protocol\Activity;
28 use Friendica\Util\Crypto;
29 use Friendica\Util\DateTimeFormat;
30 use Friendica\Util\Proxy as ProxyUtils;
31 use Friendica\Util\Strings;
32 use Friendica\Util\Temporal;
33 use Friendica\Util\XML;
35 function item_extract_images($body) {
42 $img_start = strpos($orig_body, '[img');
43 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
44 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
45 while (($img_st_close !== false) && ($img_end !== false)) {
47 $img_st_close++; // make it point to AFTER the closing bracket
48 $img_end += $img_start;
50 if (!strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
51 // This is an embedded image
53 $saved_image[$cnt] = substr($orig_body, $img_start + $img_st_close, $img_end - ($img_start + $img_st_close));
54 $new_body = $new_body . substr($orig_body, 0, $img_start) . '[!#saved_image' . $cnt . '#!]';
58 $new_body = $new_body . substr($orig_body, 0, $img_end + strlen('[/img]'));
61 $orig_body = substr($orig_body, $img_end + strlen('[/img]'));
63 if ($orig_body === false) {
64 // in case the body ends on a closing image tag
68 $img_start = strpos($orig_body, '[img');
69 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
70 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
73 $new_body = $new_body . $orig_body;
75 return ['body' => $new_body, 'images' => $saved_image];
78 function item_redir_and_replace_images($body, $images, $cid) {
84 $pos = BBCode::getTagPosition($origbody, 'url', 0);
85 while ($pos !== false && $cnt < 1000) {
87 $search = '/\[url\=(.*?)\]\[!#saved_image([0-9]*)#!\]\[\/url\]' . '/is';
88 $replace = '[url=' . DI::baseUrl() . '/redir/' . $cid
89 . '?f=1&url=' . '$1' . '][!#saved_image' . '$2' .'#!][/url]';
91 $newbody .= substr($origbody, 0, $pos['start']['open']);
92 $subject = substr($origbody, $pos['start']['open'], $pos['end']['close'] - $pos['start']['open']);
93 $origbody = substr($origbody, $pos['end']['close']);
94 if ($origbody === false) {
98 $subject = preg_replace($search, $replace, $subject);
102 // Isn't this supposed to use $cnt value for $occurrences? - @MrPetovan
103 $pos = BBCode::getTagPosition($origbody, 'url', 0);
105 $newbody .= $origbody;
108 foreach ($images as $image) {
110 * We're depending on the property of 'foreach' (specified on the PHP website) that
111 * it loops over the array starting from the first element and going sequentially
112 * to the last element.
114 $newbody = str_replace('[!#saved_image' . $cnt . '#!]', '[img]' . $image . '[/img]', $newbody);
121 * Render actions localized
124 * @throws ImagickException
125 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
127 function localize_item(&$item)
129 $extracted = item_extract_images($item['body']);
130 if ($extracted['images']) {
131 $item['body'] = item_redir_and_replace_images($extracted['body'], $extracted['images'], $item['contact-id']);
135 heluecht 2018-06-19: from my point of view this whole code part is useless.
136 It just renders the body message of technical posts (Like, dislike, ...).
137 But: The body isn't visible at all. So we do this stuff just because we can.
138 Even if these messages were visible, this would only mean that something went wrong.
139 During the further steps of the database restructuring I would like to address this issue.
142 $activity = DI::activity();
144 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
145 if ($activity->match($item['verb'], Activity::LIKE)
146 || $activity->match($item['verb'], Activity::DISLIKE)
147 || $activity->match($item['verb'], Activity::ATTEND)
148 || $activity->match($item['verb'], Activity::ATTENDNO)
149 || $activity->match($item['verb'], Activity::ATTENDMAYBE)) {
151 $fields = ['author-link', 'author-name', 'verb', 'object-type', 'resource-id', 'body', 'plink'];
152 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
153 if (!DBA::isResult($obj)) {
157 $author = '[url=' . $item['author-link'] . ']' . $item['author-name'] . '[/url]';
158 $objauthor = '[url=' . $obj['author-link'] . ']' . $obj['author-name'] . '[/url]';
160 switch ($obj['verb']) {
162 switch ($obj['object-type']) {
163 case Activity\ObjectType::EVENT:
164 $post_type = L10n::t('event');
167 $post_type = L10n::t('status');
171 if ($obj['resource-id']) {
172 $post_type = L10n::t('photo');
174 preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
175 $rr['plink'] = $m[1];
177 $post_type = L10n::t('status');
181 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
184 if ($activity->match($item['verb'], Activity::LIKE)) {
185 $bodyverb = L10n::t('%1$s likes %2$s\'s %3$s');
186 } elseif ($activity->match($item['verb'], Activity::DISLIKE)) {
187 $bodyverb = L10n::t('%1$s doesn\'t like %2$s\'s %3$s');
188 } elseif ($activity->match($item['verb'], Activity::ATTEND)) {
189 $bodyverb = L10n::t('%1$s attends %2$s\'s %3$s');
190 } elseif ($activity->match($item['verb'], Activity::ATTENDNO)) {
191 $bodyverb = L10n::t('%1$s doesn\'t attend %2$s\'s %3$s');
192 } elseif ($activity->match($item['verb'], Activity::ATTENDMAYBE)) {
193 $bodyverb = L10n::t('%1$s attends maybe %2$s\'s %3$s');
196 $item['body'] = sprintf($bodyverb, $author, $objauthor, $plink);
199 if ($activity->match($item['verb'], Activity::FRIEND)) {
201 if ($item['object-type']=="" || $item['object-type']!== Activity\ObjectType::PERSON) return;
203 $Aname = $item['author-name'];
204 $Alink = $item['author-link'];
206 $xmlhead="<"."?xml version='1.0' encoding='UTF-8' ?".">";
208 $obj = XML::parseString($xmlhead.$item['object']);
209 $links = XML::parseString($xmlhead."<links>".XML::unescape($obj->link)."</links>");
211 $Bname = $obj->title;
214 foreach ($links->link as $l) {
215 $atts = $l->attributes();
216 switch ($atts['rel']) {
217 case "alternate": $Blink = $atts['href']; break;
218 case "photo": $Bphoto = $atts['href']; break;
222 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
223 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
225 $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img]' . $Bphoto . '[/img][/url]';
228 $item['body'] = L10n::t('%1$s is now friends with %2$s', $A, $B)."\n\n\n".$Bphoto;
231 if (stristr($item['verb'], Activity::POKE)) {
232 $verb = urldecode(substr($item['verb'],strpos($item['verb'],'#')+1));
236 if ($item['object-type']=="" || $item['object-type']!== Activity\ObjectType::PERSON) {
240 $Aname = $item['author-name'];
241 $Alink = $item['author-link'];
243 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
245 $obj = XML::parseString($xmlhead.$item['object']);
247 $Bname = $obj->title;
251 foreach ($obj->link as $l) {
252 $atts = $l->attributes();
253 switch ($atts['rel']) {
254 case "alternate": $Blink = $atts['href'];
255 case "photo": $Bphoto = $atts['href'];
259 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
260 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
262 $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img=80x80]' . $Bphoto . '[/img][/url]';
266 * we can't have a translation string with three positions but no distinguishable text
267 * So here is the translate string.
269 $txt = L10n::t('%1$s poked %2$s');
271 // now translate the verb
272 $poked_t = trim(sprintf($txt, "", ""));
273 $txt = str_replace($poked_t, L10n::t($verb), $txt);
275 // then do the sprintf on the translation string
277 $item['body'] = sprintf($txt, $A, $B). "\n\n\n" . $Bphoto;
281 if ($activity->match($item['verb'], Activity::TAG)) {
282 $fields = ['author-id', 'author-link', 'author-name', 'author-network',
283 'verb', 'object-type', 'resource-id', 'body', 'plink'];
284 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
285 if (!DBA::isResult($obj)) {
289 $author_arr = ['uid' => 0, 'id' => $item['author-id'],
290 'network' => $item['author-network'], 'url' => $item['author-link']];
291 $author = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $item['author-name'] . '[/url]';
293 $author_arr = ['uid' => 0, 'id' => $obj['author-id'],
294 'network' => $obj['author-network'], 'url' => $obj['author-link']];
295 $objauthor = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $obj['author-name'] . '[/url]';
297 switch ($obj['verb']) {
299 switch ($obj['object-type']) {
300 case Activity\ObjectType::EVENT:
301 $post_type = L10n::t('event');
304 $post_type = L10n::t('status');
308 if ($obj['resource-id']) {
309 $post_type = L10n::t('photo');
310 $m=[]; preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
311 $rr['plink'] = $m[1];
313 $post_type = L10n::t('status');
315 // Let's break everthing ... ;-)
318 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
320 $parsedobj = XML::parseString($xmlhead.$item['object']);
322 $tag = sprintf('#[url=%s]%s[/url]', $parsedobj->id, $parsedobj->content);
323 $item['body'] = L10n::t('%1$s tagged %2$s\'s %3$s with %4$s', $author, $objauthor, $plink, $tag);
326 if ($activity->match($item['verb'], Activity::FAVORITE)) {
327 if ($item['object-type'] == "") {
331 $Aname = $item['author-name'];
332 $Alink = $item['author-link'];
334 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
336 $obj = XML::parseString($xmlhead.$item['object']);
337 if (strlen($obj->id)) {
338 $fields = ['author-link', 'author-name', 'plink'];
339 $target = Item::selectFirst($fields, ['uri' => $obj->id, 'uid' => $item['uid']]);
340 if (DBA::isResult($target) && $target['plink']) {
341 $Bname = $target['author-name'];
342 $Blink = $target['author-link'];
343 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
344 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
345 $P = '[url=' . $target['plink'] . ']' . L10n::t('post/item') . '[/url]';
346 $item['body'] = L10n::t('%1$s marked %2$s\'s %3$s as favorite', $A, $B, $P)."\n";
351 if (preg_match_all('/@\[url=(.*?)\]/is', $item['body'], $matches, PREG_SET_ORDER)) {
352 foreach ($matches as $mtch) {
353 if (!strpos($mtch[1], 'zrl=')) {
354 $item['body'] = str_replace($mtch[0], '@[url=' . Contact::magicLink($mtch[1]) . ']', $item['body']);
359 // add zrl's to public images
360 $photo_pattern = "/\[url=(.*?)\/photos\/(.*?)\/image\/(.*?)\]\[img(.*?)\]h(.*?)\[\/img\]\[\/url\]/is";
361 if (preg_match($photo_pattern, $item['body'])) {
362 $photo_replace = '[url=' . Profile::zrl('$1' . '/photos/' . '$2' . '/image/' . '$3' ,true) . '][img' . '$4' . ']h' . '$5' . '[/img][/url]';
363 $item['body'] = BBCode::pregReplaceInTag($photo_pattern, $photo_replace, 'url', $item['body']);
366 // add sparkle links to appropriate permalinks
367 $author = ['uid' => 0, 'id' => $item['author-id'],
368 'network' => $item['author-network'], 'url' => $item['author-link']];
370 // Only create a redirection to a magic link when logged in
371 if (!empty($item['plink']) && Session::isAuthenticated()) {
372 $item['plink'] = Contact::magicLinkByContact($author, $item['plink']);
377 * Count the total of comments on this item and its desendants
378 * @TODO proper type-hint + doc-tag
382 function count_descendants($item) {
383 $total = count($item['children']);
386 foreach ($item['children'] as $child) {
387 if (!visible_activity($child)) {
390 $total += count_descendants($child);
397 function visible_activity($item) {
399 $activity = DI::activity();
401 if (empty($item['verb']) || $activity->isHidden($item['verb'])) {
405 // @TODO below if() block can be rewritten to a single line: $isVisible = allConditionsHere;
406 if ($activity->match($item['verb'], Activity::FOLLOW) &&
407 $item['object-type'] === Activity\ObjectType::NOTE &&
408 empty($item['self']) &&
409 $item['uid'] == local_user()) {
416 function conv_get_blocklist()
422 $str_blocked = PConfig::get(local_user(), 'system', 'blocked');
423 if (empty($str_blocked)) {
429 foreach (explode(',', $str_blocked) as $entry) {
430 // The 4th parameter guarantees that there always will be a public contact entry
431 $cid = Contact::getIdForURL(trim($entry), 0, true, ['url' => trim($entry)]);
441 * "Render" a conversation or list of items for HTML display.
442 * There are two major forms of display:
443 * - Sequential or unthreaded ("New Item View" or search results)
444 * - conversation view
445 * The $mode parameter decides between the various renderings and also
446 * figures out how to determine page owner and other contextual items
447 * that are based on unique features of the calling module.
449 * @param array $items
450 * @param Pager $pager
453 * @param bool $preview
454 * @param string $order
457 * @throws ImagickException
458 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
460 function conversation(App $a, array $items, Pager $pager, $mode, $update, $preview = false, $order = 'commented', $uid = 0)
462 $ssl_state = (local_user() ? true : false);
465 $live_update_div = '';
467 $blocklist = conv_get_blocklist();
469 $previewing = (($preview) ? ' preview ' : '');
471 if ($mode === 'network') {
472 $items = conversation_add_children($items, false, $order, $uid);
473 $profile_owner = local_user();
476 * The special div is needed for liveUpdate to kick in for this page.
477 * We only launch liveUpdate if you aren't filtering in some incompatible
478 * way and also you aren't writing a comment (discovered in javascript).
480 $live_update_div = '<div id="live-network"></div>' . "\r\n"
481 . "<script> var profile_uid = " . $_SESSION['uid']
482 . "; var netargs = '" . substr(DI::args()->getCommand(), 8)
484 . (!empty($_GET['cid']) ? '&cid=' . rawurlencode($_GET['cid']) : '')
485 . (!empty($_GET['search']) ? '&search=' . rawurlencode($_GET['search']) : '')
486 . (!empty($_GET['star']) ? '&star=' . rawurlencode($_GET['star']) : '')
487 . (!empty($_GET['order']) ? '&order=' . rawurlencode($_GET['order']) : '')
488 . (!empty($_GET['bmark']) ? '&bmark=' . rawurlencode($_GET['bmark']) : '')
489 . (!empty($_GET['liked']) ? '&liked=' . rawurlencode($_GET['liked']) : '')
490 . (!empty($_GET['conv']) ? '&conv=' . rawurlencode($_GET['conv']) : '')
491 . (!empty($_GET['nets']) ? '&nets=' . rawurlencode($_GET['nets']) : '')
492 . (!empty($_GET['cmin']) ? '&cmin=' . rawurlencode($_GET['cmin']) : '')
493 . (!empty($_GET['cmax']) ? '&cmax=' . rawurlencode($_GET['cmax']) : '')
494 . (!empty($_GET['file']) ? '&file=' . rawurlencode($_GET['file']) : '')
496 . "'; var profile_page = " . $pager->getPage() . "; </script>\r\n";
498 } elseif ($mode === 'profile') {
499 $items = conversation_add_children($items, false, $order, $uid);
500 $profile_owner = $a->profile['profile_uid'];
504 if (!empty($_GET['tab'])) {
505 $tab = Strings::escapeTags(trim($_GET['tab']));
507 if ($tab === 'posts') {
509 * This is ugly, but we can't pass the profile_uid through the session to the ajax updater,
510 * because browser prefetching might change it on us. We have to deliver it with the page.
513 $live_update_div = '<div id="live-profile"></div>' . "\r\n"
514 . "<script> var profile_uid = " . $a->profile['profile_uid']
515 . "; var netargs = '?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
518 } elseif ($mode === 'notes') {
519 $items = conversation_add_children($items, false, $order, local_user());
520 $profile_owner = local_user();
523 $live_update_div = '<div id="live-notes"></div>' . "\r\n"
524 . "<script> var profile_uid = " . local_user()
525 . "; var netargs = '/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
527 } elseif ($mode === 'display') {
528 $items = conversation_add_children($items, false, $order, $uid);
529 $profile_owner = $a->profile['uid'];
532 $live_update_div = '<div id="live-display"></div>' . "\r\n"
533 . "<script> var profile_uid = " . Session::get('uid', 0) . ";"
534 . " var profile_page = 1; </script>";
536 } elseif ($mode === 'community') {
537 $items = conversation_add_children($items, true, $order, $uid);
541 $live_update_div = '<div id="live-community"></div>' . "\r\n"
542 . "<script> var profile_uid = -1; var netargs = '" . substr(DI::args()->getCommand(), 10)
543 ."/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
545 } elseif ($mode === 'contacts') {
546 $items = conversation_add_children($items, false, $order, $uid);
550 $live_update_div = '<div id="live-contacts"></div>' . "\r\n"
551 . "<script> var profile_uid = -1; var netargs = '" . substr(DI::args()->getCommand(), 9)
552 ."/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
554 } elseif ($mode === 'search') {
555 $live_update_div = '<div id="live-search"></div>' . "\r\n";
558 $page_dropping = ((local_user() && local_user() == $profile_owner) ? true : false);
561 $_SESSION['return_path'] = DI::args()->getQueryString();
564 $cb = ['items' => $items, 'mode' => $mode, 'update' => $update, 'preview' => $preview];
565 Hook::callAll('conversation_start',$cb);
567 $items = $cb['items'];
570 'like' => ['title' => L10n::t('Likes','title')],
571 'dislike' => ['title' => L10n::t('Dislikes','title')],
572 'attendyes' => ['title' => L10n::t('Attending','title')],
573 'attendno' => ['title' => L10n::t('Not attending','title')],
574 'attendmaybe' => ['title' => L10n::t('Might attend','title')],
575 'announce' => ['title' => L10n::t('Reshares','title')]
578 // array with html for each thread (parent+comments)
582 $page_template = Renderer::getMarkupTemplate("conversation.tpl");
584 if (!empty($items)) {
585 if (in_array($mode, ['community', 'contacts'])) {
588 $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED);
595 if (in_array($mode, ['network-new', 'search', 'contact-posts'])) {
598 * "New Item View" on network page or search page results
599 * - just loop through the items and format them minimally for display
602 $tpl = 'search_item.tpl';
604 foreach ($items as $item) {
606 if (!visible_activity($item)) {
610 if (in_array($item['author-id'], $blocklist)) {
620 // prevent private email from leaking.
621 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
625 $profile_name = $item['author-name'];
626 if (!empty($item['author-link']) && empty($item['author-name'])) {
627 $profile_name = $item['author-link'];
630 $tags = Term::populateTagsFromItem($item);
632 $author = ['uid' => 0, 'id' => $item['author-id'],
633 'network' => $item['author-network'], 'url' => $item['author-link']];
634 $profile_link = Contact::magicLinkByContact($author);
636 if (strpos($profile_link, 'redir/') === 0) {
637 $sparkle = ' sparkle';
640 $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
641 Hook::callAll('render_location',$locate);
643 $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
645 localize_item($item);
646 if ($mode === 'network-new') {
653 'dropping' => $dropping,
654 'pagedrop' => $page_dropping,
655 'select' => L10n::t('Select'),
656 'delete' => L10n::t('Delete'),
660 $isstarred = "unstarred";
663 $likebuttons = false;
665 $body = Item::prepareBody($item, true, $preview);
667 list($categories, $folders) = DI::contentItem()->determineCategoriesTerms($item);
669 if (!empty($item['content-warning']) && PConfig::get(local_user(), 'system', 'disable_cw', false)) {
670 $title = ucfirst($item['content-warning']);
672 $title = $item['title'];
677 'id' => ($preview ? 'P0' : $item['id']),
678 'guid' => ($preview ? 'Q0' : $item['guid']),
679 'network' => $item['network'],
680 'network_name' => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']),
681 'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link']),
682 'linktitle' => L10n::t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
683 'profile_url' => $profile_link,
684 'item_photo_menu' => item_photo_menu($item),
685 'name' => $profile_name,
686 'sparkle' => $sparkle,
688 'thumb' => DI::baseUrl()->remove(ProxyUtils::proxifyUrl($item['author-avatar'], false, ProxyUtils::SIZE_THUMB)),
691 'tags' => $tags['tags'],
692 'hashtags' => $tags['hashtags'],
693 'mentions' => $tags['mentions'],
694 'implicit_mentions' => $tags['implicit_mentions'],
695 'txt_cats' => L10n::t('Categories:'),
696 'txt_folders' => L10n::t('Filed under:'),
697 'has_cats' => ((count($categories)) ? 'true' : ''),
698 'has_folders' => ((count($folders)) ? 'true' : ''),
699 'categories' => $categories,
700 'folders' => $folders,
701 'text' => strip_tags($body),
702 'localtime' => DateTimeFormat::local($item['created'], 'r'),
703 'ago' => (($item['app']) ? L10n::t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
704 'location' => $location,
706 'owner_name' => $owner_name,
707 'owner_url' => $owner_url,
708 'owner_photo' => DI::baseUrl()->remove(ProxyUtils::proxifyUrl($item['owner-avatar'], false, ProxyUtils::SIZE_THUMB)),
709 'plink' => Item::getPlink($item),
711 'isstarred' => $isstarred,
714 'vote' => $likebuttons,
718 'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> L10n::t('View in context')]),
719 'previewing' => $previewing,
720 'wait' => L10n::t('Please wait'),
724 $arr = ['item' => $item, 'output' => $tmp_item];
725 Hook::callAll('display_item', $arr);
727 $threads[$threadsid]['id'] = $item['id'];
728 $threads[$threadsid]['network'] = $item['network'];
729 $threads[$threadsid]['items'] = [$arr['output']];
734 $page_template = Renderer::getMarkupTemplate("threaded_conversation.tpl");
736 $conv = new Thread($mode, $preview, $writable);
739 * get all the topmost parents
740 * this shouldn't be needed, as we should have only them in our array
741 * But for now, this array respects the old style, just in case
743 foreach ($items as $item) {
744 if (in_array($item['author-id'], $blocklist)) {
748 // Can we put this after the visibility check?
749 builtin_activity_puller($item, $conv_responses);
751 // Only add what is visible
752 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
756 if (!visible_activity($item)) {
760 /// @todo Check if this call is needed or not
761 $arr = ['item' => $item];
762 Hook::callAll('display_item', $arr);
764 $item['pagedrop'] = $page_dropping;
766 if ($item['id'] == $item['parent']) {
767 $item_object = new Post($item);
768 $conv->addParent($item_object);
772 $threads = $conv->getTemplateData($conv_responses);
774 Logger::log('[ERROR] conversation : Failed to get template data.', Logger::DEBUG);
780 $o = Renderer::replaceMacros($page_template, [
781 '$baseurl' => DI::baseUrl()->get($ssl_state),
782 '$return_path' => DI::args()->getQueryString(),
783 '$live_update' => $live_update_div,
784 '$remove' => L10n::t('remove'),
787 '$threads' => $threads,
788 '$dropping' => ($page_dropping ? L10n::t('Delete Selected Items') : False),
795 * Fetch all comments from a query. Additionally set the newest resharer as thread owner.
797 * @param array $thread_items Database statement with thread posts
798 * @param boolean $pinned Is the item pinned?
800 * @return array items with parents and comments
802 function conversation_fetch_comments($thread_items, $pinned) {
809 while ($row = Item::fetch($thread_items)) {
810 if (($row['verb'] == Activity::ANNOUNCE) && !empty($row['contact-uid']) && ($row['received'] > $received) && ($row['thr-parent'] == $row['parent-uri'])) {
811 $actor = ['link' => $row['author-link'], 'avatar' => $row['author-avatar'], 'name' => $row['author-name']];
812 $received = $row['received'];
815 if ((($row['gravity'] == GRAVITY_PARENT) && !$row['origin'] && !in_array($row['network'], [Protocol::DIASPORA])) &&
816 (empty($row['contact-uid']) || !in_array($row['network'], Protocol::NATIVE_SUPPORT))) {
817 $parentlines[] = $lineno;
820 if ($row['gravity'] == GRAVITY_PARENT) {
821 $row['pinned'] = $pinned;
828 DBA::close($thread_items);
830 if (!empty($actor)) {
831 foreach ($parentlines as $line) {
832 $comments[$line]['owner-link'] = $actor['link'];
833 $comments[$line]['owner-avatar'] = $actor['avatar'];
834 $comments[$line]['owner-name'] = $actor['name'];
841 * @brief Add comments to top level entries that had been fetched before
843 * The system will fetch the comments for the local user whenever possible.
844 * This behaviour is currently needed to allow commenting on Friendica posts.
846 * @param array $parents Parent items
848 * @param $block_authors
851 * @return array items with parents and comments
852 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
854 function conversation_add_children(array $parents, $block_authors, $order, $uid) {
855 $max_comments = Config::get('system', 'max_comments', 100);
857 $params = ['order' => ['uid', 'commented' => true]];
859 if ($max_comments > 0) {
860 $params['limit'] = $max_comments;
865 foreach ($parents AS $parent) {
866 $condition = ["`item`.`parent-uri` = ? AND `item`.`uid` IN (0, ?) ",
867 $parent['uri'], $uid];
868 if ($block_authors) {
869 $condition[0] .= "AND NOT `author`.`hidden`";
872 $thread_items = Item::selectForUser(local_user(), array_merge(Item::DISPLAY_FIELDLIST, ['contact-uid', 'gravity']), $condition, $params);
874 $comments = conversation_fetch_comments($thread_items, $parent['pinned'] ?? false);
876 if (count($comments) != 0) {
877 $items = array_merge($items, $comments);
881 foreach ($items as $index => $item) {
882 if ($item['uid'] == 0) {
883 $items[$index]['writable'] = in_array($item['network'], Protocol::FEDERATED);
887 $items = conv_sort($items, $order);
892 function item_photo_menu($item) {
903 if (local_user() && local_user() == $item['uid'] && $item['parent'] == $item['id'] && !$item['self']) {
904 $sub_link = 'javascript:dosubthread(' . $item['id'] . '); return false;';
907 $author = ['uid' => 0, 'id' => $item['author-id'],
908 'network' => $item['author-network'], 'url' => $item['author-link']];
909 $profile_link = Contact::magicLinkByContact($author, $item['author-link']);
910 $sparkle = (strpos($profile_link, 'redir/') === 0);
913 $pcid = Contact::getIdForURL($item['author-link'], 0, true);
916 $condition = ['uid' => local_user(), 'nurl' => Strings::normaliseLink($item['author-link'])];
917 $contact = DBA::selectFirst('contact', ['id', 'network', 'rel'], $condition);
918 if (DBA::isResult($contact)) {
919 $cid = $contact['id'];
920 $network = $contact['network'];
921 $rel = $contact['rel'];
925 $status_link = $profile_link . '?tab=status';
926 $photos_link = str_replace('/profile/', '/photos/', $profile_link);
927 $profile_link = $profile_link . '?=profile';
931 $contact_url = 'contact/' . $pcid;
932 $posts_link = 'contact/' . $pcid . '/posts';
933 $block_link = 'contact/' . $pcid . '/block';
934 $ignore_link = 'contact/' . $pcid . '/ignore';
937 if ($cid && !$item['self']) {
938 $poke_link = 'poke/?f=&c=' . $cid;
939 $contact_url = 'contact/' . $cid;
940 $posts_link = 'contact/' . $cid . '/posts';
942 if (in_array($network, [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA])) {
943 $pm_url = 'message/new/' . $cid;
949 L10n::t('Follow Thread') => $sub_link,
950 L10n::t('View Status') => $status_link,
951 L10n::t('View Profile') => $profile_link,
952 L10n::t('View Photos') => $photos_link,
953 L10n::t('Network Posts') => $posts_link,
954 L10n::t('View Contact') => $contact_url,
955 L10n::t('Send PM') => $pm_url,
956 L10n::t('Block') => $block_link,
957 L10n::t('Ignore') => $ignore_link
960 if ($network == Protocol::DFRN) {
961 $menu[L10n::t("Poke")] = $poke_link;
964 if ((($cid == 0) || ($rel == Contact::FOLLOWER)) &&
965 in_array($item['network'], Protocol::FEDERATED)) {
966 $menu[L10n::t('Connect/Follow')] = 'follow?url=' . urlencode($item['author-link']);
969 $menu = [L10n::t('View Profile') => $item['author-link']];
972 $args = ['item' => $item, 'menu' => $menu];
974 Hook::callAll('item_photo_menu', $args);
976 $menu = $args['menu'];
979 foreach ($menu as $k => $v) {
980 if (strpos($v, 'javascript:') === 0) {
982 $o .= '<li role="menuitem"><a onclick="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
984 $o .= '<li role="menuitem"><a href="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
991 * @brief Checks item to see if it is one of the builtin activities (like/dislike, event attendance, consensus items, etc.)
992 * Increments the count of each matching activity and adds a link to the author as needed.
995 * @param array &$conv_responses (already created with builtin activity structure)
997 * @throws ImagickException
998 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1000 function builtin_activity_puller($item, &$conv_responses) {
1001 foreach ($conv_responses as $mode => $v) {
1006 $verb = Activity::LIKE;
1009 $verb = Activity::DISLIKE;
1012 $verb = Activity::ATTEND;
1015 $verb = Activity::ATTENDNO;
1018 $verb = Activity::ATTENDMAYBE;
1021 $verb = Activity::ANNOUNCE;
1027 if (!empty($item['verb']) && DI::activity()->match($item['verb'], $verb) && ($item['id'] != $item['parent'])) {
1028 $author = ['uid' => 0, 'id' => $item['author-id'],
1029 'network' => $item['author-network'], 'url' => $item['author-link']];
1030 $url = Contact::magicLinkByContact($author);
1031 if (strpos($url, 'redir/') === 0) {
1032 $sparkle = ' class="sparkle" ';
1035 $url = '<a href="'. $url . '"'. $sparkle .'>' . htmlentities($item['author-name']) . '</a>';
1037 if (empty($item['thr-parent'])) {
1038 $item['thr-parent'] = $item['parent-uri'];
1041 if (!(isset($conv_responses[$mode][$item['thr-parent'] . '-l'])
1042 && is_array($conv_responses[$mode][$item['thr-parent'] . '-l']))) {
1043 $conv_responses[$mode][$item['thr-parent'] . '-l'] = [];
1046 // only list each unique author once
1047 if (in_array($url,$conv_responses[$mode][$item['thr-parent'] . '-l'])) {
1051 if (!isset($conv_responses[$mode][$item['thr-parent']])) {
1052 $conv_responses[$mode][$item['thr-parent']] = 1;
1054 $conv_responses[$mode][$item['thr-parent']] ++;
1057 if (public_contact() == $item['author-id']) {
1058 $conv_responses[$mode][$item['thr-parent'] . '-self'] = 1;
1061 $conv_responses[$mode][$item['thr-parent'] . '-l'][] = $url;
1063 // there can only be one activity verb per item so if we found anything, we can stop looking
1070 * Format the vote text for a profile item
1072 * @param int $cnt = number of people who vote the item
1073 * @param array $arr = array of pre-linked names of likers/dislikers
1074 * @param string $type = one of 'like, 'dislike', 'attendyes', 'attendno', 'attendmaybe'
1075 * @param int $id = item id
1076 * @return string formatted text
1077 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1079 function format_like($cnt, array $arr, $type, $id) {
1087 // Phrase if there is only one liker. In other cases it will be uses for the expanded
1088 // list which show all likers
1091 $phrase = L10n::t('%s likes this.', $likers);
1094 $phrase = L10n::t('%s doesn\'t like this.', $likers);
1097 $phrase = L10n::t('%s attends.', $likers);
1100 $phrase = L10n::t('%s doesn\'t attend.', $likers);
1102 case 'attendmaybe' :
1103 $phrase = L10n::t('%s attends maybe.', $likers);
1106 $phrase = L10n::t('%s reshared this.', $likers);
1112 $total = count($arr);
1113 if ($total < MAX_LIKERS) {
1114 $last = L10n::t('and') . ' ' . $arr[count($arr)-1];
1115 $arr2 = array_slice($arr, 0, -1);
1116 $likers = implode(', ', $arr2) . ' ' . $last;
1118 $arr = array_slice($arr, 0, MAX_LIKERS - 1);
1119 $likers = implode(', ', $arr);
1120 $likers .= L10n::t('and %d other people', $total - MAX_LIKERS);
1123 $spanatts = "class=\"fakelink\" onclick=\"openClose('{$type}list-$id');\"";
1128 $phrase = L10n::t('<span %1$s>%2$d people</span> like this', $spanatts, $cnt);
1129 $explikers = L10n::t('%s like this.', $likers);
1132 $phrase = L10n::t('<span %1$s>%2$d people</span> don\'t like this', $spanatts, $cnt);
1133 $explikers = L10n::t('%s don\'t like this.', $likers);
1136 $phrase = L10n::t('<span %1$s>%2$d people</span> attend', $spanatts, $cnt);
1137 $explikers = L10n::t('%s attend.', $likers);
1140 $phrase = L10n::t('<span %1$s>%2$d people</span> don\'t attend', $spanatts, $cnt);
1141 $explikers = L10n::t('%s don\'t attend.', $likers);
1144 $phrase = L10n::t('<span %1$s>%2$d people</span> attend maybe', $spanatts, $cnt);
1145 $explikers = L10n::t('%s attend maybe.', $likers);
1148 $phrase = L10n::t('<span %1$s>%2$d people</span> reshared this', $spanatts, $cnt);
1149 $explikers = L10n::t('%s reshared this.', $likers);
1153 $expanded .= "\t" . '<p class="wall-item-' . $type . '-expanded" id="' . $type . 'list-' . $id . '" style="display: none;" >' . $explikers . EOL . '</p>';
1156 $o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('voting_fakelink.tpl'), [
1157 '$phrase' => $phrase,
1166 function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
1170 $geotag = !empty($x['allow_location']) ? Renderer::replaceMacros(Renderer::getMarkupTemplate('jot_geotag.tpl'), []) : '';
1172 $tpl = Renderer::getMarkupTemplate('jot-header.tpl');
1173 DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
1174 '$newpost' => 'true',
1175 '$baseurl' => DI::baseUrl()->get(true),
1176 '$geotag' => $geotag,
1177 '$nickname' => $x['nickname'],
1178 '$ispublic' => L10n::t('Visible to <strong>everybody</strong>'),
1179 '$linkurl' => L10n::t('Please enter a image/video/audio/webpage URL:'),
1180 '$term' => L10n::t('Tag term:'),
1181 '$fileas' => L10n::t('Save to Folder:'),
1182 '$whereareu' => L10n::t('Where are you right now?'),
1183 '$delitems' => L10n::t("Delete item\x28s\x29?")
1187 Hook::callAll('jot_tool', $jotplugins);
1189 // Private/public post links for the non-JS ACL form
1191 if (!empty($_REQUEST['public'])) {
1195 $query_str = DI::args()->getQueryString();
1196 if (strpos($query_str, 'public=1') !== false) {
1197 $query_str = str_replace(['?public=1', '&public=1'], ['', ''], $query_str);
1201 * I think $a->query_string may never have ? in it, but I could be wrong
1202 * It looks like it's from the index.php?q=[etc] rewrite that the web
1203 * server does, which converts any ? to &, e.g. suggest&ignore=61 for suggest?ignore=61
1205 if (strpos($query_str, '?') === false) {
1206 $public_post_link = '?public=1';
1208 $public_post_link = '&public=1';
1211 // $tpl = Renderer::replaceMacros($tpl,array('$jotplugins' => $jotplugins));
1212 $tpl = Renderer::getMarkupTemplate("jot.tpl");
1214 $o .= Renderer::replaceMacros($tpl,[
1215 '$new_post' => L10n::t('New Post'),
1216 '$return_path' => $query_str,
1217 '$action' => 'item',
1218 '$share' => ($x['button'] ?? '') ?: L10n::t('Share'),
1219 '$upload' => L10n::t('Upload photo'),
1220 '$shortupload' => L10n::t('upload photo'),
1221 '$attach' => L10n::t('Attach file'),
1222 '$shortattach' => L10n::t('attach file'),
1223 '$edbold' => L10n::t('Bold'),
1224 '$editalic' => L10n::t('Italic'),
1225 '$eduline' => L10n::t('Underline'),
1226 '$edquote' => L10n::t('Quote'),
1227 '$edcode' => L10n::t('Code'),
1228 '$edimg' => L10n::t('Image'),
1229 '$edurl' => L10n::t('Link'),
1230 '$edattach' => L10n::t('Link or Media'),
1231 '$setloc' => L10n::t('Set your location'),
1232 '$shortsetloc' => L10n::t('set location'),
1233 '$noloc' => L10n::t('Clear browser location'),
1234 '$shortnoloc' => L10n::t('clear location'),
1235 '$title' => $x['title'] ?? '',
1236 '$placeholdertitle' => L10n::t('Set title'),
1237 '$category' => $x['category'] ?? '',
1238 '$placeholdercategory' => Feature::isEnabled(local_user(), 'categories') ? L10n::t("Categories \x28comma-separated list\x29") : '',
1239 '$wait' => L10n::t('Please wait'),
1240 '$permset' => L10n::t('Permission settings'),
1241 '$shortpermset' => L10n::t('permissions'),
1242 '$wall' => $notes_cid ? 0 : 1,
1243 '$posttype' => $notes_cid ? Item::PT_PERSONAL_NOTE : Item::PT_ARTICLE,
1244 '$content' => $x['content'] ?? '',
1245 '$post_id' => $x['post_id'] ?? '',
1246 '$baseurl' => DI::baseUrl()->get(true),
1247 '$defloc' => $x['default_location'],
1248 '$visitor' => $x['visitor'],
1249 '$pvisit' => $notes_cid ? 'none' : $x['visitor'],
1250 '$public' => L10n::t('Public post'),
1251 '$lockstate' => $x['lockstate'],
1252 '$bang' => $x['bang'],
1253 '$profile_uid' => $x['profile_uid'],
1254 '$preview' => L10n::t('Preview'),
1255 '$jotplugins' => $jotplugins,
1256 '$notes_cid' => $notes_cid,
1257 '$sourceapp' => L10n::t($a->sourcename),
1258 '$cancel' => L10n::t('Cancel'),
1259 '$rand_num' => Crypto::randomDigits(12),
1261 // ACL permissions box
1262 '$acl' => $x['acl'],
1263 '$group_perms' => L10n::t('Post to Groups'),
1264 '$contact_perms' => L10n::t('Post to Contacts'),
1265 '$private' => L10n::t('Private post'),
1266 '$is_private' => $private_post,
1267 '$public_link' => $public_post_link,
1269 //jot nav tab (used in some themes)
1270 '$message' => L10n::t('Message'),
1271 '$browser' => L10n::t('Browser'),
1275 if ($popup == true) {
1276 $o = '<div id="jot-popup" style="display: none;">' . $o . '</div>';
1283 * Plucks the children of the given parent from a given item list.
1285 * @brief Plucks all the children in the given item list of the given parent
1287 * @param array $item_list
1288 * @param array $parent
1289 * @param bool $recursive
1292 function get_item_children(array &$item_list, array $parent, $recursive = true)
1295 foreach ($item_list as $i => $item) {
1296 if ($item['id'] != $item['parent']) {
1298 // Fallback to parent-uri if thr-parent is not set
1299 $thr_parent = $item['thr-parent'];
1300 if ($thr_parent == '') {
1301 $thr_parent = $item['parent-uri'];
1304 if ($thr_parent == $parent['uri']) {
1305 $item['children'] = get_item_children($item_list, $item);
1306 $children[] = $item;
1307 unset($item_list[$i]);
1309 } elseif ($item['parent'] == $parent['id']) {
1310 $children[] = $item;
1311 unset($item_list[$i]);
1319 * @brief Recursively sorts a tree-like item array
1321 * @param array $items
1324 function sort_item_children(array $items)
1327 usort($result, 'sort_thr_received_rev');
1328 foreach ($result as $k => $i) {
1329 if (isset($result[$k]['children'])) {
1330 $result[$k]['children'] = sort_item_children($result[$k]['children']);
1337 * @brief Recursively add all children items at the top level of a list
1339 * @param array $children List of items to append
1340 * @param array $item_list
1342 function add_children_to_list(array $children, array &$item_list)
1344 foreach ($children as $child) {
1345 $item_list[] = $child;
1346 if (isset($child['children'])) {
1347 add_children_to_list($child['children'], $item_list);
1353 * This recursive function takes the item tree structure created by conv_sort() and
1354 * flatten the extraneous depth levels when people reply sequentially, removing the
1355 * stairs effect in threaded conversations limiting the available content width.
1357 * The basic principle is the following: if a post item has only one reply and is
1358 * the last reply of its parent, then the reply is moved to the parent.
1360 * This process is rendered somewhat more complicated because items can be either
1361 * replies or likes, and these don't factor at all in the reply count/last reply.
1363 * @brief Selectively flattens a tree-like item structure to prevent threading stairs
1365 * @param array $parent A tree-like array of items
1368 function smart_flatten_conversation(array $parent)
1370 if (!isset($parent['children']) || count($parent['children']) == 0) {
1374 // We use a for loop to ensure we process the newly-moved items
1375 for ($i = 0; $i < count($parent['children']); $i++) {
1376 $child = $parent['children'][$i];
1378 if (isset($child['children']) && count($child['children'])) {
1379 // This helps counting only the regular posts
1380 $count_post_closure = function($var) {
1381 return $var['verb'] === Activity::POST;
1384 $child_post_count = count(array_filter($child['children'], $count_post_closure));
1386 $remaining_post_count = count(array_filter(array_slice($parent['children'], $i), $count_post_closure));
1388 // If there's only one child's children post and this is the last child post
1389 if ($child_post_count == 1 && $remaining_post_count == 1) {
1391 // Searches the post item in the children
1393 while($child['children'][$j]['verb'] !== Activity::POST && $j < count($child['children'])) {
1397 $moved_item = $child['children'][$j];
1398 unset($parent['children'][$i]['children'][$j]);
1399 $parent['children'][] = $moved_item;
1401 $parent['children'][$i] = smart_flatten_conversation($child);
1411 * Expands a flat list of items into corresponding tree-like conversation structures,
1412 * sort the top-level posts either on "received" or "commented", and finally
1413 * append all the items at the top level (???)
1415 * @brief Expands a flat item list into a conversation array for display
1417 * @param array $item_list A list of items belonging to one or more conversations
1418 * @param string $order Either on "received" or "commented"
1420 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1422 function conv_sort(array $item_list, $order)
1426 if (!(is_array($item_list) && count($item_list))) {
1430 $blocklist = conv_get_blocklist();
1434 // Dedupes the item list on the uri to prevent infinite loops
1435 foreach ($item_list as $item) {
1436 if (in_array($item['author-id'], $blocklist)) {
1440 $item_array[$item['uri']] = $item;
1443 // Extract the top level items
1444 foreach ($item_array as $item) {
1445 if ($item['id'] == $item['parent']) {
1450 if (stristr($order, 'pinned_received')) {
1451 usort($parents, 'sort_thr_pinned_received');
1452 } elseif (stristr($order, 'received')) {
1453 usort($parents, 'sort_thr_received');
1454 } elseif (stristr($order, 'commented')) {
1455 usort($parents, 'sort_thr_commented');
1459 * Plucks children from the item_array, second pass collects eventual orphan
1460 * items and add them as children of their top-level post.
1462 foreach ($parents as $i => $parent) {
1463 $parents[$i]['children'] =
1464 array_merge(get_item_children($item_array, $parent, true),
1465 get_item_children($item_array, $parent, false));
1468 foreach ($parents as $i => $parent) {
1469 $parents[$i]['children'] = sort_item_children($parents[$i]['children']);
1472 if (!PConfig::get(local_user(), 'system', 'no_smart_threading', 0)) {
1473 foreach ($parents as $i => $parent) {
1474 $parents[$i] = smart_flatten_conversation($parent);
1478 /// @TODO: Stop recusrsively adding all children back to the top level (!!!)
1479 /// However, this apparently ensures responses (likes, attendance) display (?!)
1480 foreach ($parents as $parent) {
1481 if (count($parent['children'])) {
1482 add_children_to_list($parent['children'], $parents);
1490 * @brief usort() callback to sort item arrays by pinned and the received key
1496 function sort_thr_pinned_received(array $a, array $b)
1498 if ($b['pinned'] && !$a['pinned']) {
1500 } elseif (!$b['pinned'] && $a['pinned']) {
1504 return strcmp($b['received'], $a['received']);
1508 * @brief usort() callback to sort item arrays by the received key
1514 function sort_thr_received(array $a, array $b)
1516 return strcmp($b['received'], $a['received']);
1520 * @brief usort() callback to reverse sort item arrays by the received key
1526 function sort_thr_received_rev(array $a, array $b)
1528 return strcmp($a['received'], $b['received']);
1532 * @brief usort() callback to sort item arrays by the commented key
1538 function sort_thr_commented(array $a, array $b)
1540 return strcmp($b['commented'], $a['commented']);
1543 function render_location_dummy(array $item) {
1544 if (!empty($item['location']) && !empty($item['location'])) {
1545 return $item['location'];
1548 if (!empty($item['coord']) && !empty($item['coord'])) {
1549 return $item['coord'];
1553 function get_responses(array $conv_responses, array $response_verbs, array $item, Post $ob = null) {
1555 foreach ($response_verbs as $v) {
1557 $ret[$v]['count'] = $conv_responses[$v][$item['uri']] ?? 0;
1558 $ret[$v]['list'] = $conv_responses[$v][$item['uri'] . '-l'] ?? [];
1559 $ret[$v]['self'] = $conv_responses[$v][$item['uri'] . '-self'] ?? '0';
1560 if (count($ret[$v]['list']) > MAX_LIKERS) {
1561 $ret[$v]['list_part'] = array_slice($ret[$v]['list'], 0, MAX_LIKERS);
1562 array_push($ret[$v]['list_part'], '<a href="#" data-toggle="modal" data-target="#' . $v . 'Modal-'
1563 . (($ob) ? $ob->getId() : $item['id']) . '"><b>' . L10n::t('View all') . '</b></a>');
1565 $ret[$v]['list_part'] = '';
1567 $ret[$v]['button'] = get_response_button_text($v, $ret[$v]['count']);
1568 $ret[$v]['title'] = $conv_responses[$v]['title'];
1572 foreach ($ret as $key) {
1573 if ($key['count'] == true) {
1577 $ret['count'] = $count;
1582 function get_response_button_text($v, $count)
1587 $return = L10n::tt('Like', 'Likes', $count);
1590 $return = L10n::tt('Dislike', 'Dislikes', $count);
1593 $return = L10n::tt('Attending', 'Attending', $count);
1596 $return = L10n::tt('Not Attending', 'Not Attending', $count);
1599 $return = L10n::tt('Undecided', 'Undecided', $count);