]> git.mxchange.org Git - friendica.git/blobdiff - src/Content/Conversation.php
Return early if no items are provided in Content\Conversation->getThreadList
[friendica.git] / src / Content / Conversation.php
index c5e854b1d9ece6a02f55ab98e9238aec0ef4022b..6cad2b9805ba6acbcdb1d7c5f26e31932fa3a93c 100644 (file)
@@ -41,6 +41,7 @@ use Friendica\Model\Post;
 use Friendica\Model\Tag;
 use Friendica\Model\User;
 use Friendica\Model\Verb;
+use Friendica\Network\HTTPException\InternalServerErrorException;
 use Friendica\Object\Post as PostObject;
 use Friendica\Object\Thread;
 use Friendica\Protocol\Activity;
@@ -153,7 +154,8 @@ class Conversation
                                        'uid'     => 0,
                                        'id'      => $activity['author-id'],
                                        'network' => $activity['author-network'],
-                                       'url'     => $activity['author-link']
+                                       'url'     => $activity['author-link'],
+                                       'alias'   => $activity['author-alias'],
                                ];
                                $url = Contact::magicLinkByContact($author);
                                if (strpos($url, 'contact/redir/') === 0) {
@@ -193,6 +195,52 @@ class Conversation
                }
        }
 
+       /**
+        * Returns the liker phrase based on a list of likers
+        *
+        * @param string $verb   the activity verb
+        * @param array  $likers a list of likers
+        *
+        * @return string the liker phrase
+        *
+        * @throws InternalServerErrorException in case either the verb is invalid or the list of likers is empty
+        */
+       private function getLikerPhrase(string $verb, array $likers): string
+       {
+               $total = count($likers);
+
+               if ($total === 0) {
+                       throw new InternalServerErrorException(sprintf('There has to be at least one Liker for verb "%s"', $verb));
+               } else if ($total === 1) {
+                       $likerString = $likers[0];
+               } else {
+                       if ($total < $this->config->get('system', 'max_likers')) {
+                               $likerString = implode(', ', array_slice($likers, 0, -1));
+                               $likerString .= ' ' . $this->l10n->t('and') . ' ' . $likers[count($likers) - 1];
+                       } else {
+                               $likerString = implode(', ', array_slice($likers, 0, $this->config->get('system', 'max_likers') - 1));
+                               $likerString .= ' ' . $this->l10n->t('and %d other people', $total - $this->config->get('system', 'max_likers'));
+                       }
+               }
+
+               switch ($verb) {
+                       case 'like':
+                               return $this->l10n->tt('%2$s likes this.', '%2$s like this.', $total, $likerString);
+                       case 'dislike':
+                               return $this->l10n->tt('%2$s doesn\'t like this.', '%2$s don\'t like this.', $total, $likerString);
+                       case 'attendyes':
+                               return $this->l10n->tt('%2$s attends.', '%2$s attend.', $total, $likerString);
+                       case 'attendno':
+                               return $this->l10n->tt('%2$s doesn\'t attend.', '%2$s don\'t attend.', $total, $likerString);
+                       case 'attendmaybe':
+                               return $this->l10n->tt('%2$s attends maybe.', '%2$s attend maybe.', $total, $likerString);
+                       case 'announce':
+                               return $this->l10n->tt('%2$s reshared this.', '%2$s reshared this.', $total, $likerString);
+                       default:
+                               throw new InternalServerErrorException(sprintf('Unknown verb "%s"', $verb));
+               }
+       }
+
        /**
         * Format the activity text for an item/photo/video
         *
@@ -205,87 +253,48 @@ class Conversation
        public function formatActivity(array $links, string $verb, int $id): string
        {
                $this->profiler->startRecording('rendering');
-               $o        = '';
                $expanded = '';
-               $phrase   = '';
-
-               $total = count($links);
-               if ($total == 1) {
-                       $likers = $links[0];
 
-                       // Phrase if there is only one liker. In other cases it will be uses for the expanded
-                       // list which show all likers
-                       switch ($verb) {
-                               case 'like':
-                                       $phrase = $this->l10n->t('%s likes this.', $likers);
-                                       break;
-                               case 'dislike':
-                                       $phrase = $this->l10n->t('%s doesn\'t like this.', $likers);
-                                       break;
-                               case 'attendyes':
-                                       $phrase = $this->l10n->t('%s attends.', $likers);
-                                       break;
-                               case 'attendno':
-                                       $phrase = $this->l10n->t('%s doesn\'t attend.', $likers);
-                                       break;
-                               case 'attendmaybe':
-                                       $phrase = $this->l10n->t('%s attends maybe.', $likers);
-                                       break;
-                               case 'announce':
-                                       $phrase = $this->l10n->t('%s reshared this.', $likers);
-                                       break;
-                       }
-               } elseif ($total > 1) {
-                       if ($total < $this->config->get('system', 'max_likers')) {
-                               $likers = implode(', ', array_slice($links, 0, -1));
-                               $likers .= ' ' . $this->l10n->t('and') . ' ' . $links[count($links) - 1];
-                       } else {
-                               $likers = implode(', ', array_slice($links, 0, $this->config->get('system', 'max_likers') - 1));
-                               $likers .= ' ' . $this->l10n->t('and %d other people', $total - $this->config->get('system', 'max_likers'));
-                       }
+               $phrase = $this->getLikerPhrase($verb, $links);
+               $total  = count($links);
 
-                       $spanatts = "class=\"btn btn-link fakelink\" onclick=\"openClose('{$verb}list-$id');\"";
+               if ($total > 1) {
+                       $spanatts  = "class=\"btn btn-link fakelink\" onclick=\"openClose('{$verb}list-$id');\"";
+                       $explikers = $phrase;
 
-                       $explikers = '';
                        switch ($verb) {
                                case 'like':
-                                       $phrase    = $this->l10n->t('<button type="button" %1$s>%2$d people</button> like this', $spanatts, $total);
-                                       $explikers = $this->l10n->t('%s like this.', $likers);
+                                       $phrase = $this->l10n->tt('<button type="button" %2$s>%1$d person</button> likes this', '<button type="button" %2$s>%1$d people</button> like this', $total, $spanatts);
                                        break;
                                case 'dislike':
-                                       $phrase    = $this->l10n->t('<button type="button" %1$s>%2$d people</button> don\'t like this', $spanatts, $total);
-                                       $explikers = $this->l10n->t('%s don\'t like this.', $likers);
+                                       $phrase = $this->l10n->tt('<button type="button" %2$s>%1$d person</button> doesn\'t like this', '<button type="button" %2$s>%1$d peiple</button> don\'t like this', $total, $spanatts);
                                        break;
                                case 'attendyes':
-                                       $phrase    = $this->l10n->t('<button type="button" %1$s>%2$d people</button> attend', $spanatts, $total);
-                                       $explikers = $this->l10n->t('%s attend.', $likers);
+                                       $phrase = $this->l10n->tt('<button type="button" %2$s>%1$d person</button> attends', '<button type="button" %2$s>%1$d people</button> attend', $total, $spanatts);
                                        break;
                                case 'attendno':
-                                       $phrase    = $this->l10n->t('<button type="button" %1$s>%2$d people</button> don\'t attend', $spanatts, $total);
-                                       $explikers = $this->l10n->t('%s don\'t attend.', $likers);
+                                       $phrase = $this->l10n->tt('<button type="button" %2$s>%1$d person</button> doesn\'t attend', '<button type="button" %2$s>%1$d people</button> don\'t attend', $total, $spanatts);
                                        break;
                                case 'attendmaybe':
-                                       $phrase    = $this->l10n->t('<button type="button" %1$s>%2$d people</button> attend maybe', $spanatts, $total);
-                                       $explikers = $this->l10n->t('%s attend maybe.', $likers);
+                                       $phrase = $this->l10n->tt('<button type="button" %2$s>%1$d person</button> attends maybe', '<button type="button" %2$s>%1$d people</button> attend maybe', $total, $spanatts);
                                        break;
                                case 'announce':
-                                       $phrase    = $this->l10n->t('<button type="button" %1$s>%2$d people</button> reshared this', $spanatts, $total);
-                                       $explikers = $this->l10n->t('%s reshared this.', $likers);
+                                       $phrase = $this->l10n->tt('<button type="button" %2$s>%1$d person</button> reshared this', '<button type="button" %2$s>%1$d people</button> reshared this', $total, $spanatts);
                                        break;
                        }
 
                        $expanded .= "\t" . '<p class="wall-item-' . $verb . '-expanded" id="' . $verb . 'list-' . $id . '" style="display: none;" >' . $explikers . '</p>';
                }
 
-               $o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('voting_fakelink.tpl'), [
+               $output = Renderer::replaceMacros(Renderer::getMarkupTemplate('voting_fakelink.tpl'), [
                        '$phrase' => $phrase,
                        '$type'   => $verb,
                        '$id'     => $id
                ]);
-               $o .= $expanded;
+               $output .= $expanded;
 
                $this->profiler->stopRecording();
-               return $o;
+               return $output;
        }
 
        public function statusEditor(array $x = [], int $notes_cid = 0, bool $popup = false): string
@@ -314,7 +323,7 @@ class Conversation
                $tpl = Renderer::getMarkupTemplate('jot-header.tpl');
                $this->page['htmlhead'] .= Renderer::replaceMacros($tpl, [
                        '$newpost'   => 'true',
-                       '$baseurl'   => $this->baseURL->get(true),
+                       '$baseurl'   => $this->baseURL,
                        '$geotag'    => $geotag,
                        '$nickname'  => $x['nickname'],
                        '$ispublic'  => $this->l10n->t('Visible to <strong>everybody</strong>'),
@@ -357,6 +366,8 @@ class Conversation
                        '$editalic'            => $this->l10n->t('Italic'),
                        '$eduline'             => $this->l10n->t('Underline'),
                        '$edquote'             => $this->l10n->t('Quote'),
+                       '$edemojis'            => $this->l10n->t('Add emojis'),
+                       '$contentwarn'         => $this->l10n->t('Content Warning'),
                        '$edcode'              => $this->l10n->t('Code'),
                        '$edimg'               => $this->l10n->t('Image'),
                        '$edurl'               => $this->l10n->t('Link'),
@@ -385,7 +396,7 @@ class Conversation
                        '$posttype'     => $notes_cid ? ItemModel::PT_PERSONAL_NOTE : ItemModel::PT_ARTICLE,
                        '$content'      => $x['content'] ?? '',
                        '$post_id'      => $x['post_id'] ?? '',
-                       '$baseurl'      => $this->baseURL->get(true),
+                       '$baseurl'      => $this->baseURL,
                        '$defloc'       => $x['default_location'],
                        '$visitor'      => $x['visitor'],
                        '$pvisit'       => $notes_cid ? 'none' : $x['visitor'],
@@ -427,17 +438,17 @@ class Conversation
         * The $mode parameter decides between the various renderings and also
         * figures out how to determine page owner and other contextual items
         * that are based on unique features of the calling module.
-        * @param array  $items
-        * @param string $mode
-        * @param        $update @TODO Which type?
-        * @param bool   $preview
-        * @param string $order
+        * @param array  $items   An array of Posts
+        * @param string $mode    One of self::MODE_*
+        * @param bool   $update  Asynchronous update rendering
+        * @param bool   $preview Post preview (no actual database record)
+        * @param string $order   Either "received" or "commented"
         * @param int    $uid
         * @return string
         * @throws ImagickException
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         */
-       public function create(array $items, string $mode, $update, bool $preview = false, string $order = 'commented', int $uid = 0): string
+       public function render(array $items, string $mode, bool $update = false, bool $preview = false, string $order = 'commented', int $uid = 0): string
        {
                $this->profiler->startRecording('rendering');
 
@@ -446,14 +457,8 @@ class Conversation
                $this->page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.css'));
                $this->page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput-typeahead.css'));
 
-               $ssl_state = (bool)$this->session->getLocalUserId();
-
                $live_update_div = '';
 
-               $blocklist = $this->getBlocklist();
-
-               $previewing = (($preview) ? ' preview ' : '');
-
                if ($mode === self::MODE_NETWORK) {
                        $items = $this->addChildren($items, false, $order, $uid, $mode);
                        if (!$update) {
@@ -530,7 +535,7 @@ class Conversation
                        if (!$update) {
                                $live_update_div = '<div id="live-contact"></div>' . "\r\n"
                                        . "<script> var profile_uid = -1; var netargs = '" . substr($this->args->getCommand(), 8)
-                                       ."?f='; </script>\r\n";
+                                       . "?f='; </script>\r\n";
                        }
                } elseif ($mode === self::MODE_SEARCH) {
                        $live_update_div = '<div id="live-search"></div>' . "\r\n";
@@ -547,27 +552,63 @@ class Conversation
 
                $items = $cb['items'];
 
-               $conv_responses = [
-                       'like'        => [],
-                       'dislike'     => [],
-                       'attendyes'   => [],
-                       'attendno'    => [],
-                       'attendmaybe' => [],
-                       'announce'    => [],
-               ];
+               $formSecurityToken = BaseModule::getFormSecurityToken('contact_action');
+
+               $threads = $this->getThreadList($items, $mode, $preview, $page_dropping, $formSecurityToken);
 
-               if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'hide_dislike')) {
-                       unset($conv_responses['dislike']);
+               if (in_array($mode, [self::MODE_FILED, self::MODE_SEARCH, self::MODE_CONTACT_POSTS])) {
+                       $page_template = Renderer::getMarkupTemplate('conversation.tpl');
+               } else {
+                       $page_template = Renderer::getMarkupTemplate('threaded_conversation.tpl');
                }
 
-               // array with html for each thread (parent+comments)
-               $threads   = [];
-               $threadsid = -1;
+               $o = Renderer::replaceMacros($page_template, [
+                       '$baseurl'     => $this->baseURL,
+                       '$return_path' => $this->args->getQueryString(),
+                       '$live_update' => $live_update_div,
+                       '$remove'      => $this->l10n->t('remove'),
+                       '$mode'        => $mode,
+                       '$update'      => $update,
+                       '$threads'     => $threads,
+                       '$dropping'    => ($page_dropping ? $this->l10n->t('Delete Selected Items') : false),
+               ]);
+
+               $this->profiler->stopRecording();
+               return $o;
+       }
 
-               $page_template     = Renderer::getMarkupTemplate("conversation.tpl");
-               $formSecurityToken = BaseModule::getFormSecurityToken('contact_action');
+       /**
+        * @param array  $items
+        * @param string $mode One of self::MODE_*
+        * @param bool   $preview
+        * @param bool   $pagedrop Whether to enable the user to select the thread for deletion
+        * @param string $formSecurityToken A 'contact_action' form security token
+        * @return array
+        * @throws InternalServerErrorException
+        * @throws \ImagickException
+        */
+       public function getThreadList(array $items, string $mode, bool $preview, bool $pagedrop, string $formSecurityToken): array
+       {
+               if (!$items) {
+                       return [];
+               }
+
+               if (in_array($mode, [self::MODE_FILED, self::MODE_SEARCH, self::MODE_CONTACT_POSTS])) {
+                       $threads = $this->getContextLessThreadList($items, $mode, $preview, $pagedrop, $formSecurityToken);
+               } else {
+                       $conv_responses = [
+                               'like'        => [],
+                               'dislike'     => [],
+                               'attendyes'   => [],
+                               'attendno'    => [],
+                               'attendmaybe' => [],
+                               'announce'    => [],
+                       ];
+
+                       if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'hide_dislike')) {
+                               unset($conv_responses['dislike']);
+                       }
 
-               if (!empty($items)) {
                        if (in_array($mode, [self::MODE_COMMUNITY, self::MODE_CONTACTS, self::MODE_PROFILE])) {
                                $writable = true;
                        } else {
@@ -578,224 +619,51 @@ class Conversation
                                $writable = false;
                        }
 
-                       if (in_array($mode, [self::MODE_FILED, self::MODE_SEARCH, self::MODE_CONTACT_POSTS])) {
-
-                               /*
-                               * "New Item View" on network page or search page results
-                               * - just loop through the items and format them minimally for display
-                               */
-
-                               $tpl = 'search_item.tpl';
-
-                               $uriids = [];
-
-                               foreach ($items as $item) {
-                                       if (in_array($item['uri-id'], $uriids)) {
-                                               continue;
-                                       }
-
-                                       $uriids[] = $item['uri-id'];
-
-                                       if (!$this->item->isVisibleActivity($item)) {
-                                               continue;
-                                       }
-
-                                       if (in_array($item['author-id'], $blocklist)) {
-                                               continue;
-                                       }
-
-                                       $threadsid++;
-
-                                       // prevent private email from leaking.
-                                       if ($item['network'] === Protocol::MAIL && $this->session->getLocalUserId() != $item['uid']) {
-                                               continue;
-                                       }
-
-                                       $profile_name = $item['author-name'];
-                                       if (!empty($item['author-link']) && empty($item['author-name'])) {
-                                               $profile_name = $item['author-link'];
-                                       }
-
-                                       $tags = Tag::populateFromItem($item);
-
-                                       $author       = ['uid' => 0, 'id' => $item['author-id'], 'network' => $item['author-network'], 'url' => $item['author-link']];
-                                       $profile_link = Contact::magicLinkByContact($author);
-
-                                       $sparkle = '';
-                                       if (strpos($profile_link, 'contact/redir/') === 0) {
-                                               $sparkle = ' sparkle';
-                                       }
-
-                                       $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
-                                       Hook::callAll('render_location', $locate);
-                                       $location_html = $locate['html'] ?: Strings::escapeHtml($locate['location'] ?: $locate['coord'] ?: '');
-
-                                       $this->item->localize($item);
-                                       if ($mode === self::MODE_FILED) {
-                                               $dropping = true;
-                                       } else {
-                                               $dropping = false;
-                                       }
-
-                                       $drop = [
-                                               'dropping' => $dropping,
-                                               'pagedrop' => $page_dropping,
-                                               'select'   => $this->l10n->t('Select'),
-                                               'delete'   => $this->l10n->t('Delete'),
-                                       ];
-
-                                       $likebuttons = [
-                                               'like'     => null,
-                                               'dislike'  => null,
-                                               'share'    => null,
-                                               'announce' => null,
-                                       ];
-
-                                       if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'hide_dislike')) {
-                                               unset($likebuttons['dislike']);
-                                       }
-
-                                       $body_html = ItemModel::prepareBody($item, true, $preview);
-
-                                       [$categories, $folders] = $this->item->determineCategoriesTerms($item, $this->session->getLocalUserId());
-
-                                       if (!empty($item['title'])) {
-                                               $title = $item['title'];
-                                       } elseif (!empty($item['content-warning']) && $this->pConfig->get($this->session->getLocalUserId(), 'system', 'disable_cw', false)) {
-                                               $title = ucfirst($item['content-warning']);
-                                       } else {
-                                               $title = '';
-                                       }
-
-                                       if (!empty($item['featured'])) {
-                                               $pinned = $this->l10n->t('Pinned item');
-                                       } else {
-                                               $pinned = '';
-                                       }
+                       // Normal View
+                       $conv = new Thread($mode, $preview, $writable);
 
-                                       $tmp_item = [
-                                               'template'             => $tpl,
-                                               'id'                   => ($preview ? 'P0' : $item['id']),
-                                               'guid'                 => ($preview ? 'Q0' : $item['guid']),
-                                               'commented'            => $item['commented'],
-                                               'received'             => $item['received'],
-                                               'created_date'         => $item['created'],
-                                               'uriid'                => $item['uri-id'],
-                                               'network'              => $item['network'],
-                                               'network_name'         => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network'], $item['author-gsid']),
-                                               'network_icon'         => ContactSelector::networkToIcon($item['network'], $item['author-link'], $item['author-gsid']),
-                                               'linktitle'            => $this->l10n->t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
-                                               'profile_url'          => $profile_link,
-                                               'item_photo_menu_html' => $this->item->photoMenu($item, $formSecurityToken),
-                                               'name'                 => $profile_name,
-                                               'sparkle'              => $sparkle,
-                                               'lock'                 => false,
-                                               'thumb'                => $this->baseURL->remove($this->item->getAuthorAvatar($item)),
-                                               'title'                => $title,
-                                               'body_html'            => $body_html,
-                                               'tags'                 => $tags['tags'],
-                                               'hashtags'             => $tags['hashtags'],
-                                               'mentions'             => $tags['mentions'],
-                                               'implicit_mentions'    => $tags['implicit_mentions'],
-                                               'txt_cats'             => $this->l10n->t('Categories:'),
-                                               'txt_folders'          => $this->l10n->t('Filed under:'),
-                                               'has_cats'             => ((count($categories)) ? 'true' : ''),
-                                               'has_folders'          => ((count($folders)) ? 'true' : ''),
-                                               'categories'           => $categories,
-                                               'folders'              => $folders,
-                                               'text'                 => strip_tags($body_html),
-                                               'localtime'            => DateTimeFormat::local($item['created'], 'r'),
-                                               'utc'                  => DateTimeFormat::utc($item['created'], 'c'),
-                                               'ago'                  => (($item['app']) ? $this->l10n->t('%s from %s', Temporal::getRelativeDate($item['created']), $item['app']) : Temporal::getRelativeDate($item['created'])),
-                                               'location_html'        => $location_html,
-                                               'indent'               => '',
-                                               'owner_name'           => '',
-                                               'owner_url'            => '',
-                                               'owner_photo'          => $this->baseURL->remove($this->item->getOwnerAvatar($item)),
-                                               'plink'                => ItemModel::getPlink($item),
-                                               'edpost'               => false,
-                                               'pinned'               => $pinned,
-                                               'isstarred'            => 'unstarred',
-                                               'star'                 => false,
-                                               'drop'                 => $drop,
-                                               'vote'                 => $likebuttons,
-                                               'like_html'            => '',
-                                               'dislike_html '        => '',
-                                               'comment_html'         => '',
-                                               'conv'                 => ($preview ? '' : ['href' => 'display/' . $item['guid'], 'title' => $this->l10n->t('View in context')]),
-                                               'previewing'           => $previewing,
-                                               'wait'                 => $this->l10n->t('Please wait'),
-                                               'thread_level'         => 1,
-                                       ];
-
-                                       $arr = ['item' => $item, 'output' => $tmp_item];
-                                       Hook::callAll('display_item', $arr);
-
-                                       $threads[$threadsid]['id']      = $item['id'];
-                                       $threads[$threadsid]['network'] = $item['network'];
-                                       $threads[$threadsid]['items']   = [$arr['output']];
+                       /*
+                       * get all the topmost parents
+                       * this shouldn't be needed, as we should have only them in our array
+                       * But for now, this array respects the old style, just in case
+                       */
+                       foreach ($items as $item) {
+                               if (in_array($item['author-id'], $this->getBlocklist())) {
+                                       continue;
                                }
-                       } else {
-                               // Normal View
-                               $page_template = Renderer::getMarkupTemplate("threaded_conversation.tpl");
-
-                               $conv = new Thread($mode, $preview, $writable);
-
-                               /*
-                               * get all the topmost parents
-                               * this shouldn't be needed, as we should have only them in our array
-                               * But for now, this array respects the old style, just in case
-                               */
-                               foreach ($items as $item) {
-                                       if (in_array($item['author-id'], $blocklist)) {
-                                               continue;
-                                       }
 
-                                       // Can we put this after the visibility check?
-                                       $this->builtinActivityPuller($item, $conv_responses);
+                               // Can we put this after the visibility check?
+                               $this->builtinActivityPuller($item, $conv_responses);
 
-                                       // Only add what is visible
-                                       if ($item['network'] === Protocol::MAIL && $this->session->getLocalUserId() != $item['uid']) {
-                                               continue;
-                                       }
+                               // Only add what is visible
+                               if ($item['network'] === Protocol::MAIL && $this->session->getLocalUserId() != $item['uid']) {
+                                       continue;
+                               }
 
-                                       if (!$this->item->isVisibleActivity($item)) {
-                                               continue;
-                                       }
+                               if (!$this->item->isVisibleActivity($item)) {
+                                       continue;
+                               }
 
-                                       /// @todo Check if this call is needed or not
-                                       $arr = ['item' => $item];
-                                       Hook::callAll('display_item', $arr);
+                               /// @todo Check if this call is needed or not
+                               $arr = ['item' => $item];
+                               Hook::callAll('display_item', $arr);
 
-                                       $item['pagedrop'] = $page_dropping;
+                               $item['pagedrop'] = $pagedrop;
 
-                                       if ($item['gravity'] == ItemModel::GRAVITY_PARENT) {
-                                               $item_object = new PostObject($item);
-                                               $conv->addParent($item_object);
-                                       }
+                               if ($item['gravity'] == ItemModel::GRAVITY_PARENT) {
+                                       $item_object = new PostObject($item);
+                                       $conv->addParent($item_object);
                                }
+                       }
 
-                               $threads = $conv->getTemplateData($conv_responses, $formSecurityToken);
-                               if (!$threads) {
-                                       $this->logger->info('[ERROR] conversation : Failed to get template data.');
-                                       $threads = [];
-                               }
+                       $threads = $conv->getTemplateData($conv_responses, $formSecurityToken);
+                       if (!$threads) {
+                               $this->logger->info('[ERROR] conversation : Failed to get template data.');
+                               $threads = [];
                        }
                }
 
-               $o = Renderer::replaceMacros($page_template, [
-                       '$baseurl'     => $this->baseURL->get($ssl_state),
-                       '$return_path' => $this->args->getQueryString(),
-                       '$live_update' => $live_update_div,
-                       '$remove'      => $this->l10n->t('remove'),
-                       '$mode'        => $mode,
-                       '$update'      => $update,
-                       '$threads'     => $threads,
-                       '$dropping'    => ($page_dropping ? $this->l10n->t('Delete Selected Items') : false),
-               ]);
-
-               $this->profiler->stopRecording();
-               return $o;
+               return $threads;
        }
 
        private function getBlocklist(): array
@@ -849,7 +717,8 @@ class Conversation
                                $row['causer-avatar'] = $contact['thumb'];
                                $row['causer-name']   = $contact['name'];
                        } elseif (($row['gravity'] == ItemModel::GRAVITY_ACTIVITY) && ($row['verb'] == Activity::ANNOUNCE) &&
-                               ($row['author-id'] == $activity['causer-id'])) {
+                               ($row['author-id'] == $activity['causer-id'])
+                       ) {
                                return $row;
                        }
                }
@@ -867,6 +736,9 @@ class Conversation
                        case ItemModel::PR_BCC:
                                $row['direction'] = ['direction' => 7, 'title' => $this->l10n->t('You had been addressed (%s).', 'bcc')];
                                break;
+                       case ItemModel::PR_AUDIENCE:
+                               $row['direction'] = ['direction' => 7, 'title' => $this->l10n->t('You had been addressed (%s).', 'audience')];
+                               break;
                        case ItemModel::PR_FOLLOWER:
                                $row['direction'] = ['direction' => 6, 'title' => $this->l10n->t('You are following %s.', $row['causer-name'] ?: $row['author-name'])];
                                break;
@@ -882,9 +754,15 @@ class Conversation
                                }
 
                                if (in_array($row['gravity'], [ItemModel::GRAVITY_PARENT, ItemModel::GRAVITY_COMMENT]) && !empty($row['causer-id'])) {
-                                       $causer = ['uid' => 0, 'id' => $row['causer-id'], 'network' => $row['causer-network'], 'url' => $row['causer-link']];
+                                       $causer = [
+                                               'uid'     => 0,
+                                               'id'      => $row['causer-id'],
+                                               'network' => $row['causer-network'],
+                                               'url'     => $row['causer-link'],
+                                               'alias'   => $row['causer-alias'],
+                                       ];
 
-                                       $row['reshared'] = $this->l10n->t('%s reshared this.', '<a href="'. htmlentities(Contact::magicLinkByContact($causer)) .'">' . htmlentities($row['causer-name']) . '</a>');
+                                       $row['reshared'] = $this->l10n->t('%s reshared this.', '<a href="' . htmlentities(Contact::magicLinkByContact($causer)) . '">' . htmlentities($row['causer-name']) . '</a>');
                                }
                                $row['direction'] = ['direction' => 3, 'title' => (empty($row['causer-id']) ? $this->l10n->t('Reshared') : $this->l10n->t('Reshared by %s <%s>', $row['causer-name'], $row['causer-link']))];
                                break;
@@ -979,13 +857,26 @@ class Conversation
                        $condition['author-hidden'] = false;
                }
 
-               $condition = DBA::mergeConditions($condition,
-                       ["`uid` IN (0, ?) AND (NOT `vid` IN (?, ?, ?) OR `vid` IS NULL)", $uid, Verb::getID(Activity::FOLLOW), Verb::getID(Activity::VIEW), Verb::getID(Activity::READ)]);
+               if ($this->config->get('system', 'emoji_activities')) {
+                       $emojis = $this->getEmojis($uriids);
+                       $condition = DBA::mergeConditions($condition, ["(`gravity` != ? OR `origin`)", ItemModel::GRAVITY_ACTIVITY]);
+               }
+
+               $condition = DBA::mergeConditions(
+                       $condition,
+                       ["`uid` IN (0, ?) AND (NOT `vid` IN (?, ?, ?) OR `vid` IS NULL)", $uid, Verb::getID(Activity::FOLLOW), Verb::getID(Activity::VIEW), Verb::getID(Activity::READ)]
+               );
+
+               $condition = DBA::mergeConditions($condition, ["(`uid` != ? OR `private` != ?)", 0, ItemModel::PRIVATE]);
 
-               $condition = DBA::mergeConditions($condition,
-                       ["`visible` AND NOT `deleted` AND NOT `author-blocked` AND NOT `owner-blocked`
+               $condition = DBA::mergeConditions(
+                       $condition,
+                       [
+                               "`visible` AND NOT `deleted` AND NOT `author-blocked` AND NOT `owner-blocked`
                        AND ((NOT `contact-pending` AND (`contact-rel` IN (?, ?))) OR `self` OR `contact-uid` = ?)",
-                       Contact::SHARING, Contact::FRIEND, 0]);
+                               Contact::SHARING, Contact::FRIEND, 0
+                       ]
+               );
 
                $thread_parents = Post::select(['uri-id', 'causer-id'], $condition, ['order' => ['uri-id' => false, 'uid']]);
 
@@ -1081,6 +972,8 @@ class Conversation
                }
 
                foreach ($items as $key => $row) {
+                       $items[$key]['emojis'] = $emojis[$key] ?? [];
+
                        $always_display = in_array($mode, [self::MODE_CONTACTS, self::MODE_CONTACT_POSTS]);
 
                        $items[$key]['user-blocked-author']   = !$always_display && in_array($row['author-id'], $blocks);
@@ -1090,8 +983,10 @@ class Conversation
                        $items[$key]['user-collapsed-author'] = !$always_display && in_array($row['author-id'], $collapses);
                        $items[$key]['user-collapsed-owner']  = !$always_display && in_array($row['owner-id'], $collapses);
 
-                       if (in_array($mode, [self::MODE_COMMUNITY, self::MODE_NETWORK]) &&
-                               (in_array($row['author-id'], $blocks) || in_array($row['owner-id'], $blocks) || in_array($row['author-id'], $ignores) || in_array($row['owner-id'], $ignores))) {
+                       if (
+                               in_array($mode, [self::MODE_COMMUNITY, self::MODE_NETWORK]) &&
+                               (in_array($row['author-id'], $blocks) || in_array($row['owner-id'], $blocks) || in_array($row['author-id'], $ignores) || in_array($row['owner-id'], $ignores))
+                       ) {
                                unset($items[$key]);
                        }
                }
@@ -1102,6 +997,53 @@ class Conversation
                return $items;
        }
 
+       /**
+        * Fetch emoji reaction from the conversation
+        *
+        * @param array $uriids
+        * @return array
+        */
+       private function getEmojis(array $uriids): array
+       {
+               $activity_emoji = [
+                       Activity::LIKE        => '👍',
+                       Activity::DISLIKE     => '👎',
+                       Activity::ATTEND      => '✔️',
+                       Activity::ATTENDMAYBE => '❓',
+                       Activity::ATTENDNO    => '❌',
+                       Activity::ANNOUNCE    => '♻',
+                       Activity::VIEW        => '📺',
+               ];
+
+               $index_list = array_values($activity_emoji);
+               $verbs      = array_merge(array_keys($activity_emoji), [Activity::EMOJIREACT]);
+
+               $condition = DBA::mergeConditions(['parent-uri-id' => $uriids, 'gravity' => ItemModel::GRAVITY_ACTIVITY, 'verb' => $verbs], ["NOT `deleted`"]);
+               $separator = chr(255) . chr(255) . chr(255);
+
+               $sql = "SELECT `thr-parent-id`, `body`, `verb`, COUNT(*) AS `total`, GROUP_CONCAT(REPLACE(`author-name`, '" . $separator . "', ' ') SEPARATOR '" . $separator . "' LIMIT 50) AS `title` FROM `post-view` WHERE " . array_shift($condition) . " GROUP BY `thr-parent-id`, `verb`, `body`";
+
+               $emojis = [];
+
+               $rows = DBA::p($sql, $condition);
+               while ($row = DBA::fetch($rows)) {
+                       $row['verb'] = $row['body'] ? Activity::EMOJIREACT : $row['verb'];
+                       $emoji       = $row['body'] ?: $activity_emoji[$row['verb']];
+                       if (!isset($index_list[$emoji])) {
+                               $index_list[] = $emoji;
+                       }
+                       $index = array_search($emoji, $index_list);
+
+                       $emojis[$row['thr-parent-id']][$index]['emoji'] = $emoji;
+                       $emojis[$row['thr-parent-id']][$index]['verb']  = $row['verb'];
+                       $emojis[$row['thr-parent-id']][$index]['total'] = ($emojis[$row['thr-parent-id']][$index]['total'] ?? 0) + $row['total'];
+                       $emojis[$row['thr-parent-id']][$index]['title'] = array_unique(array_merge($emojis[$row['thr-parent-id']][$index]['title'] ?? [], explode($separator, $row['title'])));
+               }
+               DBA::close($rows);
+
+               return $emojis;
+       }
+
        /**
         * Plucks the children of the given parent from a given item list.
         *
@@ -1220,7 +1162,7 @@ class Conversation
                                        // Searches the post item in the children
                                        $j = 0;
                                        while ($child['children'][$j]['verb'] !== Activity::POST && $j < count($child['children'])) {
-                                               $j ++;
+                                               $j++;
                                        }
 
                                        $moved_item = $child['children'][$j];
@@ -1294,8 +1236,10 @@ class Conversation
                * items and add them as children of their top-level post.
                */
                foreach ($parents as $i => $parent) {
-                       $parents[$i]['children'] = array_merge($this->getItemChildren($item_array, $parent, true),
-                               $this->getItemChildren($item_array, $parent, false));
+                       $parents[$i]['children'] = array_merge(
+                               $this->getItemChildren($item_array, $parent, true),
+                               $this->getItemChildren($item_array, $parent, false)
+                       );
                }
 
                foreach ($parents as $i => $parent) {
@@ -1308,7 +1252,7 @@ class Conversation
                        }
                }
 
-               /// @TODO: Stop recusrsively adding all children back to the top level (!!!)
+               /// @TODO: Stop recursively adding all children back to the top level (!!!)
                /// However, this apparently ensures responses (likes, attendance) display (?!)
                foreach ($parents as $parent) {
                        if (count($parent['children'])) {
@@ -1403,4 +1347,180 @@ class Conversation
        {
                return strcmp($b['created'], $a['created']);
        }
+
+       /**
+        * "New Item View" on network page or search page results
+        * - just loop through the items and format them minimally for display
+        *
+        * @param array  $items
+        * @param string $mode              One of self::MODE_*
+        * @param bool   $preview           Whether the display is a preview
+        * @param bool   $pagedrop          Whether the user can select the threads for deletion
+        * @param string $formSecurityToken A 'contact_action' form security token
+        * @return array
+        * @throws InternalServerErrorException
+        * @throws \ImagickException
+        */
+       public function getContextLessThreadList(array $items, string $mode, bool $preview, bool $pagedrop, string $formSecurityToken): array
+       {
+               $threads = [];
+               $uriids = [];
+
+               foreach ($items as $item) {
+                       if (in_array($item['uri-id'], $uriids)) {
+                               continue;
+                       }
+
+                       $uriids[] = $item['uri-id'];
+
+                       if (!$this->item->isVisibleActivity($item)) {
+                               continue;
+                       }
+
+                       if (in_array($item['author-id'], $this->getBlocklist())) {
+                               continue;
+                       }
+
+                       $threadsid++;
+
+                       // prevent private email from leaking.
+                       if ($item['network'] === Protocol::MAIL && $this->session->getLocalUserId() != $item['uid']) {
+                               continue;
+                       }
+
+                       $profile_name = $item['author-name'];
+                       if (!empty($item['author-link']) && empty($item['author-name'])) {
+                               $profile_name = $item['author-link'];
+                       }
+
+                       $tags = Tag::populateFromItem($item);
+
+                       $author       = [
+                               'uid'     => 0,
+                               'id'      => $item['author-id'],
+                               'network' => $item['author-network'],
+                               'url'     => $item['author-link'],
+                               'alias'   => $item['author-alias'],
+                       ];
+                       $profile_link = Contact::magicLinkByContact($author);
+
+                       $sparkle = '';
+                       if (strpos($profile_link, 'contact/redir/') === 0) {
+                               $sparkle = ' sparkle';
+                       }
+
+                       $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
+                       Hook::callAll('render_location', $locate);
+                       $location_html = $locate['html'] ?: Strings::escapeHtml($locate['location'] ?: $locate['coord'] ?: '');
+
+                       $this->item->localize($item);
+                       if ($mode === self::MODE_FILED) {
+                               $dropping = true;
+                       } else {
+                               $dropping = false;
+                       }
+
+                       $drop = [
+                               'dropping' => $dropping,
+                               'pagedrop' => $pagedrop,
+                               'select'   => $this->l10n->t('Select'),
+                               'delete'   => $this->l10n->t('Delete'),
+                       ];
+
+                       $likebuttons = [
+                               'like'     => null,
+                               'dislike'  => null,
+                               'share'    => null,
+                               'announce' => null,
+                       ];
+
+                       if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'hide_dislike')) {
+                               unset($likebuttons['dislike']);
+                       }
+
+                       $body_html = ItemModel::prepareBody($item, true, $preview);
+
+                       [$categories, $folders] = $this->item->determineCategoriesTerms($item, $this->session->getLocalUserId());
+
+                       if (!empty($item['title'])) {
+                               $title = $item['title'];
+                       } elseif (!empty($item['content-warning']) && $this->pConfig->get($this->session->getLocalUserId(), 'system', 'disable_cw', false)) {
+                               $title = ucfirst($item['content-warning']);
+                       } else {
+                               $title = '';
+                       }
+
+                       if (!empty($item['featured'])) {
+                               $pinned = $this->l10n->t('Pinned item');
+                       } else {
+                               $pinned = '';
+                       }
+
+                       $tmp_item = [
+                               'template'             => 'search_item.tpl',
+                               'id'                   => ($preview ? 'P0' : $item['id']),
+                               'guid'                 => ($preview ? 'Q0' : $item['guid']),
+                               'commented'            => $item['commented'],
+                               'received'             => $item['received'],
+                               'created_date'         => $item['created'],
+                               'uriid'                => $item['uri-id'],
+                               'network'              => $item['network'],
+                               'network_name'         => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network'], $item['author-gsid']),
+                               'network_icon'         => ContactSelector::networkToIcon($item['network'], $item['author-link'], $item['author-gsid']),
+                               'linktitle'            => $this->l10n->t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
+                               'profile_url'          => $profile_link,
+                               'item_photo_menu_html' => $this->item->photoMenu($item, $formSecurityToken),
+                               'name'                 => $profile_name,
+                               'sparkle'              => $sparkle,
+                               'lock'                 => false,
+                               'thumb'                => $this->baseURL->remove($this->item->getAuthorAvatar($item)),
+                               'title'                => $title,
+                               'body_html'            => $body_html,
+                               'tags'                 => $tags['tags'],
+                               'hashtags'             => $tags['hashtags'],
+                               'mentions'             => $tags['mentions'],
+                               'implicit_mentions'    => $tags['implicit_mentions'],
+                               'txt_cats'             => $this->l10n->t('Categories:'),
+                               'txt_folders'          => $this->l10n->t('Filed under:'),
+                               'has_cats'             => ((count($categories)) ? 'true' : ''),
+                               'has_folders'          => ((count($folders)) ? 'true' : ''),
+                               'categories'           => $categories,
+                               'folders'              => $folders,
+                               'text'                 => strip_tags($body_html),
+                               'localtime'            => DateTimeFormat::local($item['created'], 'r'),
+                               'utc'                  => DateTimeFormat::utc($item['created'], 'c'),
+                               'ago'                  => (($item['app']) ? $this->l10n->t('%s from %s', Temporal::getRelativeDate($item['created']), $item['app']) : Temporal::getRelativeDate($item['created'])),
+                               'location_html'        => $location_html,
+                               'indent'               => '',
+                               'owner_name'           => '',
+                               'owner_url'            => '',
+                               'owner_photo'          => $this->baseURL->remove($this->item->getOwnerAvatar($item)),
+                               'plink'                => ItemModel::getPlink($item),
+                               'edpost'               => false,
+                               'pinned'               => $pinned,
+                               'isstarred'            => 'unstarred',
+                               'star'                 => false,
+                               'drop'                 => $drop,
+                               'vote'                 => $likebuttons,
+                               'like_html'            => '',
+                               'dislike_html '        => '',
+                               'comment_html'         => '',
+                               'conv'                 => $preview ? '' : ['href' => 'display/' . $item['guid'], 'title' => $this->l10n->t('View in context')],
+                               'previewing'           => $preview ? ' preview ' : '',
+                               'wait'                 => $this->l10n->t('Please wait'),
+                               'thread_level'         => 1,
+                       ];
+
+                       $arr = ['item' => $item, 'output' => $tmp_item];
+                       Hook::callAll('display_item', $arr);
+
+                       $threads[] = [
+                               'id'      => $item['id'],
+                               'network' => $item['network'],
+                               'items'   => [$arr['output']],
+                       ];
+               }
+
+               return $threads;
+       }
 }