X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FContent%2FConversation.php;h=151a2bfc7e10a4c72e3254b23cf04e4488d727b7;hb=06a1db4f24bffbb6027f6341879345bde7212ecc;hp=c5e854b1d9ece6a02f55ab98e9238aec0ef4022b;hpb=91ff24b0e4cb4b01f0af2a457bdf66945090ffb0;p=friendica.git diff --git a/src/Content/Conversation.php b/src/Content/Conversation.php index c5e854b1d9..151a2bfc7e 100644 --- a/src/Content/Conversation.php +++ b/src/Content/Conversation.php @@ -41,9 +41,12 @@ 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; +use Friendica\User\Settings\Entity\UserGServer; +use Friendica\User\Settings\Repository; use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; use Friendica\Util\Profiler; @@ -89,22 +92,25 @@ class Conversation private $mode; /** @var IHandleUserSessions */ private $session; + /** @var Repository\UserGServer */ + private $userGServer; - public function __construct(LoggerInterface $logger, Profiler $profiler, Activity $activity, L10n $l10n, Item $item, Arguments $args, BaseURL $baseURL, IManageConfigValues $config, IManagePersonalConfigValues $pConfig, App\Page $page, App\Mode $mode, App $app, IHandleUserSessions $session) + public function __construct(Repository\UserGServer $userGServer, LoggerInterface $logger, Profiler $profiler, Activity $activity, L10n $l10n, Item $item, Arguments $args, BaseURL $baseURL, IManageConfigValues $config, IManagePersonalConfigValues $pConfig, App\Page $page, App\Mode $mode, App $app, IHandleUserSessions $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->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; } /** @@ -153,7 +159,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 +200,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 +258,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(' like this', $spanatts, $total); - $explikers = $this->l10n->t('%s like this.', $likers); + $phrase = $this->l10n->tt(' likes this', ' like this', $total, $spanatts); break; case 'dislike': - $phrase = $this->l10n->t(' don\'t like this', $spanatts, $total); - $explikers = $this->l10n->t('%s don\'t like this.', $likers); + $phrase = $this->l10n->tt(' doesn\'t like this', ' don\'t like this', $total, $spanatts); break; case 'attendyes': - $phrase = $this->l10n->t(' attend', $spanatts, $total); - $explikers = $this->l10n->t('%s attend.', $likers); + $phrase = $this->l10n->tt(' attends', ' attend', $total, $spanatts); break; case 'attendno': - $phrase = $this->l10n->t(' don\'t attend', $spanatts, $total); - $explikers = $this->l10n->t('%s don\'t attend.', $likers); + $phrase = $this->l10n->tt(' doesn\'t attend', ' don\'t attend', $total, $spanatts); break; case 'attendmaybe': - $phrase = $this->l10n->t(' attend maybe', $spanatts, $total); - $explikers = $this->l10n->t('%s attend maybe.', $likers); + $phrase = $this->l10n->tt(' attends maybe', ' attend maybe', $total, $spanatts); break; case 'announce': - $phrase = $this->l10n->t(' reshared this', $spanatts, $total); - $explikers = $this->l10n->t('%s reshared this.', $likers); + $phrase = $this->l10n->tt(' reshared this', ' reshared this', $total, $spanatts); break; } $expanded .= "\t" . '
'; } - $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 +328,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 everybody'), @@ -357,6 +371,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 +401,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 +443,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,16 +462,16 @@ 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(); + $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. @@ -481,7 +497,7 @@ class Conversation . "'; \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'; @@ -506,7 +522,7 @@ 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" @@ -514,7 +530,7 @@ class Conversation . ""; } } 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" @@ -525,12 +541,12 @@ 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"; @@ -547,27 +563,63 @@ class Conversation $items = $cb['items']; - $conv_responses = [ - 'like' => [], - 'dislike' => [], - 'attendyes' => [], - 'attendno' => [], - 'attendmaybe' => [], - 'announce' => [], - ]; + $formSecurityToken = BaseModule::getFormSecurityToken('contact_action'); - if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'hide_dislike')) { - unset($conv_responses['dislike']); + $threads = $this->getThreadList($items, $mode, $preview, $page_dropping, $formSecurityToken); + + 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 +630,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 = ''; - } - - $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); + // Normal View + $conv = new Thread($mode, $preview, $writable); - $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); + // Can we put this after the visibility check? + $this->builtinActivityPuller($item, $conv_responses); - /* - * 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; - } + // 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 +728,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 +747,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 +765,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; @@ -934,13 +823,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) { @@ -979,13 +869,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, - ["`visible` AND NOT `deleted` AND NOT `author-blocked` AND NOT `owner-blocked` + $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` 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']]); @@ -1009,6 +912,13 @@ 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; } @@ -1081,6 +991,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 +1002,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 +1016,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 +1181,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 +1255,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 +1271,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 +1366,179 @@ 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; + } + + // 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; + } }