X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FContent%2FConversation.php;h=6edef06fc65c56b0b4b7d49a00c753b21b351277;hb=ddd2c72be8e7245389f97d74dd847f5a20410936;hp=458a0a690f19fe97ed5a481723cc4d9b2340e0fd;hpb=47802fbebd1f25f6d83dba7168c3801e18399d03;p=friendica.git diff --git a/src/Content/Conversation.php b/src/Content/Conversation.php index 458a0a690f..6edef06fc6 100644 --- a/src/Content/Conversation.php +++ b/src/Content/Conversation.php @@ -1,6 +1,6 @@ activity = $activity; - $this->item = $item; - $this->config = $config; - $this->mode = $mode; - $this->baseURL = $baseURL; - $this->profiler = $profiler; - $this->logger = $logger; - $this->l10n = $l10n; - $this->args = $args; - $this->pConfig = $pConfig; - $this->page = $page; - $this->app = $app; - $this->session = $session; + $this->activity = $activity; + $this->item = $item; + $this->config = $config; + $this->mode = $mode; + $this->baseURL = $baseURL; + $this->profiler = $profiler; + $this->logger = $logger; + $this->l10n = $l10n; + $this->args = $args; + $this->pConfig = $pConfig; + $this->page = $page; + $this->app = $app; + $this->session = $session; + $this->userGServer = $userGServer; } /** @@ -154,7 +161,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) { @@ -243,19 +251,21 @@ class Conversation /** * Format the activity text for an item/photo/video * - * @param array $links = array of pre-linked names of actors - * @param string $verb = one of 'like, 'dislike', 'attendyes', 'attendno', 'attendmaybe' - * @param int $id = item id + * @param array $links array of pre-linked names of actors + * @param string $verb one of 'like, 'dislike', 'attendyes', 'attendno', 'attendmaybe' + * @param int $id item id + * @param string $activity Activity URI + * @param array $emojis Array with emoji reactions * @return string formatted text * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public function formatActivity(array $links, string $verb, int $id): string + public function formatActivity(array $links, string $verb, int $id, string $activity, array $emojis): string { $this->profiler->startRecording('rendering'); $expanded = ''; $phrase = $this->getLikerPhrase($verb, $links); - $total = count($links); + $total = max(count($links), $emojis[$activity]['total'] ?? 0); if ($total > 1) { $spanatts = "class=\"btn btn-link fakelink\" onclick=\"openClose('{$verb}list-$id');\""; @@ -272,7 +282,7 @@ class Conversation $phrase = $this->l10n->tt(' attends', ' attend', $total, $spanatts); break; case 'attendno': - $phrase = $this->l10n->tt(' doesn\'t attend',' don\'t attend', $total, $spanatts); + $phrase = $this->l10n->tt(' doesn\'t attend', ' don\'t attend', $total, $spanatts); break; case 'attendmaybe': $phrase = $this->l10n->tt(' attends maybe', ' attend maybe', $total, $spanatts); @@ -365,6 +375,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'), @@ -435,17 +447,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'); @@ -456,12 +468,14 @@ class Conversation $live_update_div = ''; - $blocklist = $this->getBlocklist(); + $userGservers = $this->userGServer->listIgnoredByUser($this->session->getLocalUserId()); - $previewing = (($preview) ? ' preview ' : ''); + $ignoredGsids = array_map(function (UserGServer $userGServer) { + return $userGServer->gsid; + }, $userGservers->getArrayCopy()); if ($mode === self::MODE_NETWORK) { - $items = $this->addChildren($items, false, $order, $uid, $mode); + $items = $this->addChildren($items, false, $order, $uid, $mode, $ignoredGsids); if (!$update) { /* * The special div is needed for liveUpdate to kick in for this page. @@ -483,11 +497,13 @@ class Conversation . (!empty($_GET['cmin']) ? '&cmin=' . rawurlencode($_GET['cmin']) : '') . (!empty($_GET['cmax']) ? '&cmax=' . rawurlencode($_GET['cmax']) : '') . (!empty($_GET['file']) ? '&file=' . rawurlencode($_GET['file']) : '') - + . (!empty($_GET['channel']) ? '&channel=' . rawurlencode($_GET['channel']) : '') + . (!empty($_GET['no_sharer']) ? '&no_sharer=' . rawurlencode($_GET['no_sharer']) : '') + . (!empty($_GET['accounttype']) ? '&accounttype=' . rawurlencode($_GET['accounttype']) : '') . "'; \r\n"; } } elseif ($mode === self::MODE_PROFILE) { - $items = $this->addChildren($items, false, $order, $uid, $mode); + $items = $this->addChildren($items, false, $order, $uid, $mode, $ignoredGsids); if (!$update) { $tab = !empty($_GET['tab']) ? trim($_GET['tab']) : 'posts'; @@ -512,15 +528,26 @@ class Conversation . "; var netargs = '?f='; \r\n"; } } elseif ($mode === self::MODE_DISPLAY) { - $items = $this->addChildren($items, false, $order, $uid, $mode); + $items = $this->addChildren($items, false, $order, $uid, $mode, $ignoredGsids); if (!$update) { $live_update_div = '
' . "\r\n" . ""; } + } elseif ($mode === self::MODE_CHANNEL) { + $items = $this->addChildren($items, true, $order, $uid, $mode, $ignoredGsids); + + if (!$update) { + $live_update_div = '
' . "\r\n" + . "\r\n"; + } } elseif ($mode === self::MODE_COMMUNITY) { - $items = $this->addChildren($items, true, $order, $uid, $mode); + $items = $this->addChildren($items, true, $order, $uid, $mode, $ignoredGsids); if (!$update) { $live_update_div = '
' . "\r\n" @@ -531,18 +558,18 @@ class Conversation . "'; \r\n"; } } elseif ($mode === self::MODE_CONTACTS) { - $items = $this->addChildren($items, false, $order, $uid, $mode); + $items = $this->addChildren($items, false, $order, $uid, $mode, $ignoredGsids); if (!$update) { $live_update_div = '
' . "\r\n" . "\r\n"; + . "?f='; \r\n"; } } elseif ($mode === self::MODE_SEARCH) { $live_update_div = '' . "\r\n"; } - $page_dropping = $this->session->getLocalUserId() && $this->session->getLocalUserId() == $uid && $mode != self::MODE_SEARCH; + $page_dropping = $this->session->getLocalUserId() && $this->pConfig->get($this->session->getLocalUserId(), 'system', 'show_page_drop', true) && ($this->session->getLocalUserId() == $uid && $mode != self::MODE_SEARCH); if (!$update) { $_SESSION['return_path'] = $this->args->getQueryString(); @@ -553,240 +580,14 @@ class Conversation $items = $cb['items']; - $conv_responses = [ - 'like' => [], - 'dislike' => [], - 'attendyes' => [], - 'attendno' => [], - 'attendmaybe' => [], - 'announce' => [], - ]; - - if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'hide_dislike')) { - unset($conv_responses['dislike']); - } - - // array with html for each thread (parent+comments) - $threads = []; - $threadsid = -1; - - $page_template = Renderer::getMarkupTemplate("conversation.tpl"); $formSecurityToken = BaseModule::getFormSecurityToken('contact_action'); - if (!empty($items)) { - if (in_array($mode, [self::MODE_COMMUNITY, self::MODE_CONTACTS, self::MODE_PROFILE])) { - $writable = true; - } else { - $writable = $items[0]['writable'] || ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED); - } - - if (!$this->session->getLocalUserId()) { - $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']); - } + $threads = $this->getThreadList($items, $mode, $preview, $page_dropping, $formSecurityToken); - $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' => $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']]; - } - } 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); - - // Only add what is visible - if ($item['network'] === Protocol::MAIL && $this->session->getLocalUserId() != $item['uid']) { - continue; - } - - if (!$this->item->isVisibleActivity($item)) { - continue; - } - - /// @todo Check if this call is needed or not - $arr = ['item' => $item]; - Hook::callAll('display_item', $arr); - - $item['pagedrop'] = $page_dropping; - - 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 = []; - } - } + 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'); } $o = Renderer::replaceMacros($page_template, [ @@ -804,27 +605,89 @@ class Conversation return $o; } - private function getBlocklist(): array + /** + * @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 (!$this->session->getLocalUserId()) { + if (!$items) { return []; } - $str_blocked = str_replace(["\n", "\r"], ",", $this->pConfig->get($this->session->getLocalUserId(), 'system', 'blocked') ?? ''); - if (empty($str_blocked)) { - 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 (in_array($mode, [self::MODE_CHANNEL, self::MODE_COMMUNITY, self::MODE_CONTACTS, self::MODE_PROFILE])) { + $writable = true; + } else { + $writable = $items[0]['writable'] || ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED); + } + + if (!$this->session->getLocalUserId()) { + $writable = false; + } + + // Normal View + $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) { + // 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; + } + + if (!$this->item->isVisibleActivity($item)) { + continue; + } - $blocklist = []; + /// @todo Check if this call is needed or not + $arr = ['item' => $item]; + Hook::callAll('display_item', $arr); - foreach (explode(',', $str_blocked) as $entry) { - $cid = Contact::getIdForURL(trim($entry), 0, false); - if (!empty($cid)) { - $blocklist[] = $cid; + $item['pagedrop'] = $pagedrop; + + 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 = []; } } - return $blocklist; + return $threads; } /** @@ -855,7 +718,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; } } @@ -873,11 +737,19 @@ 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; case ItemModel::PR_TAG: - $row['direction'] = ['direction' => 4, 'title' => $this->l10n->t('You subscribed to one or more tags in this post.')]; + $tags = Category::getArrayByURIId($row['uri-id'], $row['uid'], Category::SUBCRIPTION); + if (!empty($tags)) { + $row['direction'] = ['direction' => 4, 'title' => $this->l10n->t('You subscribed to %s.', implode(', ', $tags))]; + } else { + $row['direction'] = ['direction' => 4, 'title' => $this->l10n->t('You subscribed to one or more tags in this post.')]; + } break; case ItemModel::PR_ANNOUNCEMENT: if (!empty($row['causer-id']) && $this->pConfig->get($this->session->getLocalUserId(), 'system', 'display_resharer')) { @@ -888,9 +760,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.', '' . htmlentities($row['causer-name']) . ''); + $row['reshared'] = $this->l10n->t('%s reshared this.', '' . htmlentities($row['causer-name']) . ''); } $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; @@ -940,13 +818,14 @@ class Conversation * * @param array $parents Parent items * @param bool $block_authors - * @param bool $order + * @param string $order Either "received" or "commented" * @param int $uid - * @param string $mode + * @param string $mode One of self::MODE_* + * @param array $ignoredGsids List of ids of servers ignored by the user * @return array items with parents and comments - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws InternalServerErrorException */ - private function addChildren(array $parents, bool $block_authors, string $order, int $uid, string $mode): array + private function addChildren(array $parents, bool $block_authors, string $order, int $uid, string $mode, array $ignoredGsids = []): array { $this->profiler->startRecording('rendering'); if (count($parents) > 1) { @@ -985,20 +864,29 @@ class Conversation $condition['author-hidden'] = false; } - if ($this->config->get('system', 'emoji_activities')) { - $emojis = $this->getEmojis($uriids); + $emojis = $this->getEmojis($uriids); + $quoteshares = $this->getQuoteShares($uriids); + $counts = $this->getCounts($uriids); + + if (!$this->config->get('system', 'legacy_activities')) { $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` IN (0, ?) AND (NOT `verb` IN (?, ?, ?) OR `verb` IS NULL)", $uid, Activity::FOLLOW, Activity::VIEW, 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']]); @@ -1022,6 +910,14 @@ class Conversation continue; } + if ( + in_array($row['author-gsid'], $ignoredGsids) + || in_array($row['owner-gsid'], $ignoredGsids) + || in_array($row['causer-gsid'], $ignoredGsids) + ) { + continue; + } + if (($mode != self::MODE_CONTACTS) && !$row['origin']) { $row['featured'] = false; } @@ -1094,7 +990,9 @@ class Conversation } foreach ($items as $key => $row) { - $items[$key]['emojis'] = $emojis[$key] ?? []; + $items[$key]['emojis'] = $emojis[$key] ?? []; + $items[$key]['counts'] = $counts[$key] ?? 0; + $items[$key]['quoteshares'] = $quoteshares[$key] ?? []; $always_display = in_array($mode, [self::MODE_CONTACTS, self::MODE_CONTACT_POSTS]); @@ -1105,8 +1003,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_CHANNEL, 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]); } } @@ -1125,6 +1025,16 @@ class Conversation */ private function getEmojis(array $uriids): array { + $emojis = []; + + foreach (Post\Counts::get(['parent-uri-id' => $uriids]) as $count) { + $emojis[$count['uri-id']][$count['reaction']]['emoji'] = $count['reaction']; + $emojis[$count['uri-id']][$count['reaction']]['verb'] = Verb::getByID($count['vid']); + $emojis[$count['uri-id']][$count['reaction']]['total'] = $count['count']; + $emojis[$count['uri-id']][$count['reaction']]['title'] = []; + } + + // @todo The following code should be removed, once that we display activity authors on demand $activity_emoji = [ Activity::LIKE => '👍', Activity::DISLIKE => '👎', @@ -1133,37 +1043,74 @@ class Conversation Activity::ATTENDNO => '❌', Activity::ANNOUNCE => '♻', Activity::VIEW => '📺', + Activity::READ => '📖', ]; - $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`"]); + $verbs = array_merge(array_keys($activity_emoji), [Activity::EMOJIREACT, Activity::POST]); + $condition = DBA::mergeConditions(['parent-uri-id' => $uriids, 'gravity' => [ItemModel::GRAVITY_ACTIVITY, ItemModel::GRAVITY_COMMENT], '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 = []; + $sql = "SELECT `parent-uri-id`, `thr-parent-id`, `body`, `verb`, `gravity`, GROUP_CONCAT(REPLACE(`author-name`, '" . $separator . "', ' ') SEPARATOR '" . $separator . "' LIMIT 50) AS `title` FROM `post-view` WHERE " . array_shift($condition) . " GROUP BY `parent-uri-id`, `thr-parent-id`, `verb`, `body`, `gravity`"; $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; + if ($row['gravity'] == ItemModel::GRAVITY_ACTIVITY) { + $emoji = $row['body'] ?: $activity_emoji[$row['verb']]; + } else { + $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']))); + if (isset($emojis[$row['thr-parent-id']][$emoji]['title'])) { + $emojis[$row['thr-parent-id']][$emoji]['title'] = array_unique(array_merge($emojis[$row['thr-parent-id']][$emoji]['title'] ?? [], explode($separator, $row['title']))); + } } DBA::close($rows); return $emojis; } + /** + * Fetch comment counts from the conversation + * + * @param array $uriids + * @return array + */ + private function getCounts(array $uriids): array + { + $counts = []; + + foreach (Post\Counts::get(['parent-uri-id' => $uriids, 'verb' => Activity::POST]) as $count) { + $counts[$count['parent-uri-id']] = ($counts[$count['parent-uri-id']] ?? 0) + $count['count']; + } + + return $counts; + } + + /** + * Fetch quote shares from the conversation + * + * @param array $uriids + * @return array + */ + private function getQuoteShares(array $uriids): array + { + $condition = DBA::mergeConditions(['quote-uri-id' => $uriids], ["NOT `quote-uri-id` IS NULL"]); + $separator = chr(255) . chr(255) . chr(255); + + $sql = "SELECT `quote-uri-id`, COUNT(*) AS `total`, GROUP_CONCAT(REPLACE(`name`, '" . $separator . "', ' ') SEPARATOR '" . $separator . "' LIMIT 50) AS `title` FROM `post-content` INNER JOIN `post` ON `post`.`uri-id` = `post-content`.`uri-id` INNER JOIN `contact` ON `post`.`author-id` = `contact`.`id` WHERE " . array_shift($condition) . " GROUP BY `quote-uri-id`"; + + $quotes = []; + + $rows = DBA::p($sql, $condition); + while ($row = DBA::fetch($rows)) { + $quotes[$row['quote-uri-id']]['total'] = $row['total']; + $quotes[$row['quote-uri-id']]['title'] = array_unique(explode($separator, $row['title'])); + } + DBA::close($rows); + + return $quotes; + } + /** * Plucks the children of the given parent from a given item list. * @@ -1282,7 +1229,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]; @@ -1319,16 +1266,10 @@ class Conversation return $parents; } - $blocklist = $this->getBlocklist(); - $item_array = []; // Dedupes the item list on the uri to prevent infinite loops foreach ($item_list as $item) { - if (in_array($item['author-id'], $blocklist)) { - continue; - } - $item_array[$item['uri-id']] = $item; } @@ -1343,6 +1284,8 @@ class Conversation usort($parents, [$this, 'sortThrFeaturedReceived']); } elseif (stristr($order, 'pinned_commented')) { usort($parents, [$this, 'sortThrFeaturedCommented']); + } elseif (stristr($order, 'pinned_created')) { + usort($parents, [$this, 'sortThrFeaturedCreated']); } elseif (stristr($order, 'received')) { usort($parents, [$this, 'sortThrReceived']); } elseif (stristr($order, 'commented')) { @@ -1356,8 +1299,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) { @@ -1418,6 +1363,24 @@ class Conversation return strcmp($b['commented'], $a['commented']); } + /** + * usort() callback to sort item arrays by featured and the created key + * + * @param array $a + * @param array $b + * @return int + */ + private function sortThrFeaturedCreated(array $a, array $b): int + { + if ($b['featured'] && !$a['featured']) { + return 1; + } elseif (!$b['featured'] && $a['featured']) { + return -1; + } + + return strcmp($b['created'], $a['created']); + } + /** * usort() callback to sort item arrays by the received key * @@ -1465,4 +1428,175 @@ 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; + } + + // 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'], + 'author_gsid' => $item['author-gsid'], + '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; + } }