X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FContent%2FConversation.php;h=458a0a690f19fe97ed5a481723cc4d9b2340e0fd;hb=ae2e8beaaca342a0d5ced45beb0fc6c1b4f8a4a4;hp=879e47c24d3e1762a7c11fdeb8e80a7ca8fb229c;hpb=865006682a3952565035207898b80dfbd6d390b1;p=friendica.git diff --git a/src/Content/Conversation.php b/src/Content/Conversation.php index 879e47c24d..458a0a690f 100644 --- a/src/Content/Conversation.php +++ b/src/Content/Conversation.php @@ -1,6 +1,6 @@ activity = $activity; $this->item = $item; @@ -93,6 +105,7 @@ class Conversation $this->pConfig = $pConfig; $this->page = $page; $this->app = $app; + $this->session = $session; } /** @@ -108,6 +121,8 @@ class Conversation */ public function builtinActivityPuller(array $activity, array &$conv_responses) { + $thread_parent = $activity['thr-parent-row'] ?? []; + foreach ($conv_responses as $mode => $v) { $sparkle = ''; @@ -134,7 +149,7 @@ class Conversation return; } - if (!empty($activity['verb']) && $this->activity->match($activity['verb'], $verb) && ($activity['gravity'] != GRAVITY_PARENT)) { + if (!empty($activity['verb']) && $this->activity->match($activity['verb'], $verb) && ($activity['gravity'] != ItemModel::GRAVITY_PARENT)) { $author = [ 'uid' => 0, 'id' => $activity['author-id'], @@ -142,7 +157,7 @@ class Conversation 'url' => $activity['author-link'] ]; $url = Contact::magicLinkByContact($author); - if (strpos($url, 'redir/') === 0) { + if (strpos($url, 'contact/redir/') === 0) { $sparkle = ' class="sparkle" '; } @@ -152,9 +167,8 @@ class Conversation $activity['thr-parent-id'] = $activity['parent-uri-id']; } - // Skip when the causer of the parent is the same than the author of the announce - if (($verb == Activity::ANNOUNCE) && Post::exists(['uri-id' => $activity['thr-parent-id'], - 'uid' => $activity['uid'], 'causer-id' => $activity['author-id'], 'gravity' => GRAVITY_PARENT])) { + // Skip when the causer of the parent is the same as the author of the announce + if (($verb == Activity::ANNOUNCE) && !empty($thread_parent['causer-id']) && ($thread_parent['causer-id'] == $activity['author-id'])) { continue; } @@ -168,7 +182,7 @@ class Conversation continue; } - if (public_contact() == $activity['author-id']) { + if ($this->session->getPublicContactId() == $activity['author-id']) { $conv_responses[$mode][$activity['thr-parent-id']]['self'] = 1; } @@ -180,6 +194,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 * @@ -189,93 +249,54 @@ class Conversation * @return string formatted text * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public function formatActivity(array $links, $verb, $id) + 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 = $this->getLikerPhrase($verb, $links); + $total = count($links); - // 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 < 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, MAX_LIKERS - 1)); - $likers .= ' ' . $this->l10n->t('and %d other people', $total - MAX_LIKERS); - } + if ($total > 1) { + $spanatts = "class=\"btn btn-link fakelink\" onclick=\"openClose('{$verb}list-$id');\""; + $explikers = $phrase; - $spanatts = "class=\"fakelink\" onclick=\"openClose('{$verb}list-$id');\""; - - $explikers = ''; switch ($verb) { case 'like': - $phrase = $this->l10n->t('%2$d people 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('%2$d people 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('%2$d people 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('%2$d people 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('%2$d people 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('%2$d people 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" . ''; + $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 = [], $notes_cid = 0, $popup = false) + public function statusEditor(array $x = [], int $notes_cid = 0, bool $popup = false): string { $user = User::getById($this->app->getLoggedInUserId(), ['uid', 'nickname', 'allow_location', 'default-location']); if (empty($user['uid'])) { @@ -293,7 +314,7 @@ class Conversation $x['bang'] = $x['bang'] ?? ''; $x['visitor'] = $x['visitor'] ?? 'block'; $x['is_owner'] = $x['is_owner'] ?? true; - $x['profile_uid'] = $x['profile_uid'] ?? local_user(); + $x['profile_uid'] = $x['profile_uid'] ?? $this->session->getLocalUserId(); $geotag = !empty($x['allow_location']) ? Renderer::replaceMacros(Renderer::getMarkupTemplate('jot_geotag.tpl'), []) : ''; @@ -301,7 +322,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'), @@ -316,7 +337,19 @@ class Conversation $jotplugins = ''; Hook::callAll('jot_tool', $jotplugins); - $tpl = Renderer::getMarkupTemplate("jot.tpl"); + if ($this->config->get('system', 'set_creation_date')) { + $created_at = Temporal::getDateTimeField( + new \DateTime(DBA::NULL_DATETIME), + new \DateTime('now'), + null, + $this->l10n->t('Created at'), + 'created_at' + ); + } else { + $created_at = ''; + } + + $tpl = Renderer::getMarkupTemplate('jot.tpl'); $o .= Renderer::replaceMacros($tpl, [ '$new_post' => $this->l10n->t('New Post'), @@ -344,7 +377,7 @@ class Conversation '$title' => $x['title'] ?? '', '$placeholdertitle' => $this->l10n->t('Set title'), '$category' => $x['category'] ?? '', - '$placeholdercategory' => Feature::isEnabled(local_user(), 'categories') ? $this->l10n->t("Categories \x28comma-separated list\x29") : '', + '$placeholdercategory' => Feature::isEnabled($this->session->getLocalUserId(), 'categories') ? $this->l10n->t("Categories \x28comma-separated list\x29") : '', '$scheduled_at' => Temporal::getDateTimeField( new \DateTime(), new \DateTime('now + 6 months'), @@ -352,6 +385,7 @@ class Conversation $this->l10n->t('Scheduled at'), 'scheduled_at' ), + '$created_at' => $created_at, '$wait' => $this->l10n->t('Please wait'), '$permset' => $this->l10n->t('Permission settings'), '$shortpermset' => $this->l10n->t('Permissions'), @@ -359,7 +393,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'], @@ -380,7 +414,8 @@ class Conversation '$message' => $this->l10n->t('Message'), '$browser' => $this->l10n->t('Browser'), - '$compose_link_title' => $this->l10n->t('Open Compose page'), + '$compose_link_title' => $this->l10n->t('Open Compose page'), + '$always_open_compose' => $this->pConfig->get($this->session->getLocalUserId(), 'frio', 'always_open_compose', false), ]); @@ -401,8 +436,8 @@ class Conversation * 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 $mode - * @param $update + * @param string $mode + * @param $update @TODO Which type? * @param bool $preview * @param string $order * @param int $uid @@ -410,7 +445,7 @@ class Conversation * @throws ImagickException * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public function create(array $items, $mode, $update, $preview = false, $order = 'commented', $uid = 0) + public function create(array $items, string $mode, $update, bool $preview = false, string $order = 'commented', int $uid = 0): string { $this->profiler->startRecording('rendering'); @@ -419,16 +454,14 @@ 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 = (local_user() ? true : false); - $live_update_div = ''; $blocklist = $this->getBlocklist(); $previewing = (($preview) ? ' preview ' : ''); - if ($mode === 'network') { - $items = $this->addChildren($items, false, $order, $uid); + if ($mode === self::MODE_NETWORK) { + $items = $this->addChildren($items, false, $order, $uid, $mode); if (!$update) { /* * The special div is needed for liveUpdate to kick in for this page. @@ -453,8 +486,8 @@ class Conversation . "'; \r\n"; } - } elseif ($mode === 'profile') { - $items = $this->addChildren($items, false, $order, local_user()); + } elseif ($mode === self::MODE_PROFILE) { + $items = $this->addChildren($items, false, $order, $uid, $mode); if (!$update) { $tab = !empty($_GET['tab']) ? trim($_GET['tab']) : 'posts'; @@ -470,45 +503,46 @@ class Conversation . "; var netargs = '?f='; \r\n"; } } - } elseif ($mode === 'notes') { - $items = $this->addChildren($items, false, $order, local_user()); + } elseif ($mode === self::MODE_NOTES) { + $items = $this->addChildren($items, false, $order, $this->session->getLocalUserId(), $mode); if (!$update) { $live_update_div = '
' . "\r\n" - . "\r\n"; + . "\r\n"; } - } elseif ($mode === 'display') { - $items = $this->addChildren($items, false, $order, $uid); + } elseif ($mode === self::MODE_DISPLAY) { + $items = $this->addChildren($items, false, $order, $uid, $mode); if (!$update) { $live_update_div = '
' . "\r\n" - . ""; } - } elseif ($mode === 'community') { - $items = $this->addChildren($items, true, $order, $uid); + } elseif ($mode === self::MODE_COMMUNITY) { + $items = $this->addChildren($items, true, $order, $uid, $mode); if (!$update) { $live_update_div = '
' . "\r\n" . "\r\n"; } - } elseif ($mode === 'contacts') { - $items = $this->addChildren($items, false, $order, $uid); + } elseif ($mode === self::MODE_CONTACTS) { + $items = $this->addChildren($items, false, $order, $uid, $mode); if (!$update) { $live_update_div = '
' . "\r\n" . "\r\n"; + ."?f='; \r\n"; } - } elseif ($mode === 'search') { + } elseif ($mode === self::MODE_SEARCH) { $live_update_div = '' . "\r\n"; } - $page_dropping = ((local_user() && local_user() == $uid) ? true : false); + $page_dropping = $this->session->getLocalUserId() && $this->session->getLocalUserId() == $uid && $mode != self::MODE_SEARCH; if (!$update) { $_SESSION['return_path'] = $this->args->getQueryString(); @@ -528,7 +562,7 @@ class Conversation 'announce' => [], ]; - if ($this->pConfig->get(local_user(), 'system', 'hide_dislike')) { + if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'hide_dislike')) { unset($conv_responses['dislike']); } @@ -540,17 +574,17 @@ class Conversation $formSecurityToken = BaseModule::getFormSecurityToken('contact_action'); if (!empty($items)) { - if (in_array($mode, ['community', 'contacts', 'profile'])) { + if (in_array($mode, [self::MODE_COMMUNITY, self::MODE_CONTACTS, self::MODE_PROFILE])) { $writable = true; } else { - $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED); + $writable = $items[0]['writable'] || ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED); } - if (!local_user()) { + if (!$this->session->getLocalUserId()) { $writable = false; } - if (in_array($mode, ['filed', 'search', 'contact-posts'])) { + if (in_array($mode, [self::MODE_FILED, self::MODE_SEARCH, self::MODE_CONTACT_POSTS])) { /* * "New Item View" on network page or search page results @@ -568,7 +602,7 @@ class Conversation $uriids[] = $item['uri-id']; - if (!$this->item->visibleActivity($item)) { + if (!$this->item->isVisibleActivity($item)) { continue; } @@ -579,7 +613,7 @@ class Conversation $threadsid++; // prevent private email from leaking. - if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) { + if ($item['network'] === Protocol::MAIL && $this->session->getLocalUserId() != $item['uid']) { continue; } @@ -594,7 +628,7 @@ class Conversation $profile_link = Contact::magicLinkByContact($author); $sparkle = ''; - if (strpos($profile_link, 'redir/') === 0) { + if (strpos($profile_link, 'contact/redir/') === 0) { $sparkle = ' sparkle'; } @@ -603,7 +637,7 @@ class Conversation $location_html = $locate['html'] ?: Strings::escapeHtml($locate['location'] ?: $locate['coord'] ?: ''); $this->item->localize($item); - if ($mode === 'filed') { + if ($mode === self::MODE_FILED) { $dropping = true; } else { $dropping = false; @@ -623,18 +657,26 @@ class Conversation 'announce' => null, ]; - if ($this->pConfig->get(local_user(), 'system', 'hide_dislike')) { + if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'hide_dislike')) { unset($likebuttons['dislike']); } $body_html = ItemModel::prepareBody($item, true, $preview); - list($categories, $folders) = $this->item->determineCategoriesTerms($item, local_user()); + [$categories, $folders] = $this->item->determineCategoriesTerms($item, $this->session->getLocalUserId()); - if (!empty($item['content-warning']) && $this->pConfig->get(local_user(), 'system', 'disable_cw', false)) { + 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 = $item['title']; + $title = ''; + } + + if (!empty($item['featured'])) { + $pinned = $this->l10n->t('Pinned item'); + } else { + $pinned = ''; } $tmp_item = [ @@ -646,15 +688,15 @@ class Conversation 'created_date' => $item['created'], 'uriid' => $item['uri-id'], 'network' => $item['network'], - 'network_name' => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']), - 'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link']), + '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(Contact::getAvatarUrlForUrl($item['author-link'], $item['uid'], Proxy::SIZE_THUMB)), + 'thumb' => $this->baseURL->remove($this->item->getAuthorAvatar($item)), 'title' => $title, 'body_html' => $body_html, 'tags' => $tags['tags'], @@ -669,14 +711,16 @@ class Conversation '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(Contact::getAvatarUrlForUrl($item['owner-link'], $item['uid'], Proxy::SIZE_THUMB)), + 'owner_photo' => $this->baseURL->remove($this->item->getOwnerAvatar($item)), 'plink' => ItemModel::getPlink($item), 'edpost' => false, + 'pinned' => $pinned, 'isstarred' => 'unstarred', 'star' => false, 'drop' => $drop, @@ -717,11 +761,11 @@ class Conversation $this->builtinActivityPuller($item, $conv_responses); // Only add what is visible - if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) { + if ($item['network'] === Protocol::MAIL && $this->session->getLocalUserId() != $item['uid']) { continue; } - if (!$this->item->visibleActivity($item)) { + if (!$this->item->isVisibleActivity($item)) { continue; } @@ -731,7 +775,7 @@ class Conversation $item['pagedrop'] = $page_dropping; - if ($item['gravity'] == GRAVITY_PARENT) { + if ($item['gravity'] == ItemModel::GRAVITY_PARENT) { $item_object = new PostObject($item); $conv->addParent($item_object); } @@ -746,7 +790,7 @@ class Conversation } $o = Renderer::replaceMacros($page_template, [ - '$baseurl' => $this->baseURL->get($ssl_state), + '$baseurl' => $this->baseURL, '$return_path' => $this->args->getQueryString(), '$live_update' => $live_update_div, '$remove' => $this->l10n->t('remove'), @@ -760,13 +804,13 @@ class Conversation return $o; } - private function getBlocklist() + private function getBlocklist(): array { - if (!local_user()) { + if (!$this->session->getLocalUserId()) { return []; } - $str_blocked = str_replace(["\n", "\r"], ",", $this->pConfig->get(local_user(), 'system', 'blocked')); + $str_blocked = str_replace(["\n", "\r"], ",", $this->pConfig->get($this->session->getLocalUserId(), 'system', 'blocked') ?? ''); if (empty($str_blocked)) { return []; } @@ -786,21 +830,22 @@ class Conversation /** * Adds some information (Causer, post reason, direction) to the fetched post row. * - * @param array $row Post row - * @param array $activity Contact data of the resharer + * @param array $row Post row + * @param array $activity Contact data of the resharer + * @param array $thr_parent Thread parent row * * @return array items with parents and comments */ - private function addRowInformation(array $row, array $activity) + private function addRowInformation(array $row, array $activity, array $thr_parent): array { $this->profiler->startRecording('rendering'); - if ($row['uid'] == 0) { + if (!$row['writable']) { $row['writable'] = in_array($row['network'], Protocol::FEDERATED); } if (!empty($activity)) { - if (($row['gravity'] == GRAVITY_PARENT)) { + if (($row['gravity'] == ItemModel::GRAVITY_PARENT)) { $row['post-reason'] = ItemModel::PR_ANNOUNCEMENT; $row = array_merge($row, $activity); @@ -809,7 +854,7 @@ class Conversation $row['causer-link'] = $contact['url']; $row['causer-avatar'] = $contact['thumb']; $row['causer-name'] = $contact['name']; - } elseif (($row['gravity'] == GRAVITY_ACTIVITY) && ($row['verb'] == Activity::ANNOUNCE) && + } elseif (($row['gravity'] == ItemModel::GRAVITY_ACTIVITY) && ($row['verb'] == Activity::ANNOUNCE) && ($row['author-id'] == $activity['causer-id'])) { return $row; } @@ -829,20 +874,20 @@ class Conversation $row['direction'] = ['direction' => 7, 'title' => $this->l10n->t('You had been addressed (%s).', 'bcc')]; break; case ItemModel::PR_FOLLOWER: - $row['direction'] = ['direction' => 6, 'title' => $this->l10n->t('You are following %s.', $row['author-name'])]; + $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('Tagged')]; + $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(local_user(), 'system', 'display_resharer')) { + if (!empty($row['causer-id']) && $this->pConfig->get($this->session->getLocalUserId(), 'system', 'display_resharer')) { $row['owner-id'] = $row['causer-id']; $row['owner-link'] = $row['causer-link']; $row['owner-avatar'] = $row['causer-avatar']; $row['owner-name'] = $row['causer-name']; } - if (($row['gravity'] == GRAVITY_PARENT) && !empty($row['causer-id'])) { + 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']]; $row['reshared'] = $this->l10n->t('%s reshared this.', '' . htmlentities($row['causer-name']) . ''); @@ -853,19 +898,36 @@ class Conversation $row['direction'] = ['direction' => 5, 'title' => $this->l10n->t('%s is participating in this thread.', $row['author-name'])]; break; case ItemModel::PR_STORED: - $row['direction'] = ['direction' => 8, 'title' => $this->l10n->t('Stored')]; + $row['direction'] = ['direction' => 8, 'title' => $this->l10n->t('Stored for general reasons')]; break; case ItemModel::PR_GLOBAL: - $row['direction'] = ['direction' => 9, 'title' => $this->l10n->t('Global')]; + $row['direction'] = ['direction' => 9, 'title' => $this->l10n->t('Global post')]; break; case ItemModel::PR_RELAY: - $row['direction'] = ['direction' => 10, 'title' => (empty($row['causer-id']) ? $this->l10n->t('Relayed') : $this->l10n->t('Relayed by %s <%s>', $row['causer-name'], $row['causer-link']))]; + $row['direction'] = ['direction' => 10, 'title' => (empty($row['causer-id']) ? $this->l10n->t('Sent via an relay server') : $this->l10n->t('Sent via the relay server %s <%s>', $row['causer-name'], $row['causer-link']))]; break; case ItemModel::PR_FETCHED: $row['direction'] = ['direction' => 2, 'title' => (empty($row['causer-id']) ? $this->l10n->t('Fetched') : $this->l10n->t('Fetched because of %s <%s>', $row['causer-name'], $row['causer-link']))]; break; + case ItemModel::PR_COMPLETION: + $row['direction'] = ['direction' => 2, 'title' => $this->l10n->t('Stored because of a child post to complete this thread.')]; + break; + case ItemModel::PR_DIRECT: + $row['direction'] = ['direction' => 6, 'title' => $this->l10n->t('Local delivery')]; + break; + case ItemModel::PR_ACTIVITY: + $row['direction'] = ['direction' => 2, 'title' => $this->l10n->t('Stored because of your activity (like, comment, star, ...)')]; + break; + case ItemModel::PR_DISTRIBUTE: + $row['direction'] = ['direction' => 6, 'title' => $this->l10n->t('Distributed')]; + break; + case ItemModel::PR_PUSHED: + $row['direction'] = ['direction' => 1, 'title' => $this->l10n->t('Pushed to us')]; + break; } + $row['thr-parent-row'] = $thr_parent; + $this->profiler->stopRecording(); return $row; } @@ -876,15 +938,15 @@ class Conversation * The system will fetch the comments for the local user whenever possible. * This behaviour is currently needed to allow commenting on Friendica posts. * - * @param array $parents Parent items - * - * @param $block_authors - * @param $order - * @param $uid + * @param array $parents Parent items + * @param bool $block_authors + * @param bool $order + * @param int $uid + * @param string $mode * @return array items with parents and comments * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private function addChildren(array $parents, $block_authors, $order, $uid) + private function addChildren(array $parents, bool $block_authors, string $order, int $uid, string $mode): array { $this->profiler->startRecording('rendering'); if (count($parents) > 1) { @@ -893,15 +955,13 @@ class Conversation $max_comments = $this->config->get('system', 'max_display_comments', 1000); } - $params = ['order' => ['uri-id' => true, 'uid' => true]]; - $activities = []; $uriids = []; $commentcounter = []; $activitycounter = []; foreach ($parents as $parent) { - if (!empty($parent['thr-parent-id']) && !empty($parent['gravity']) && ($parent['gravity'] == GRAVITY_ACTIVITY)) { + if (!empty($parent['thr-parent-id']) && !empty($parent['gravity']) && ($parent['gravity'] == ItemModel::GRAVITY_ACTIVITY)) { $uriid = $parent['thr-parent-id']; if (!empty($parent['author-id'])) { $activities[$uriid] = ['causer-id' => $parent['author-id']]; @@ -925,37 +985,185 @@ class Conversation $condition['author-hidden'] = false; } + 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 (`vid` != ? OR `vid` IS NULL)", $uid, Verb::getID(Activity::FOLLOW)]); + ["`uid` IN (0, ?) AND (NOT `vid` IN (?, ?, ?) OR `vid` IS NULL)", $uid, Verb::getID(Activity::FOLLOW), Verb::getID(Activity::VIEW), Verb::getID(Activity::READ)]); - $thread_items = Post::selectForUser(local_user(), array_merge(ItemModel::DISPLAY_FIELDLIST, ['pinned', 'contact-uid', 'gravity', 'post-type', 'post-reason']), $condition, $params); + $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]); + + $thread_parents = Post::select(['uri-id', 'causer-id'], $condition, ['order' => ['uri-id' => false, 'uid']]); + + $thr_parent = []; + + while ($row = Post::fetch($thread_parents)) { + $thr_parent[$row['uri-id']] = $row; + } + DBA::close($thread_parents); + + $params = ['order' => ['uri-id' => true, 'uid' => true]]; - $items = []; + $thread_items = Post::select(array_merge(ItemModel::DISPLAY_FIELDLIST, ['featured', 'contact-uid', 'gravity', 'post-type', 'post-reason']), $condition, $params); + + $items = []; + $quote_uri_ids = []; + $authors = []; while ($row = Post::fetch($thread_items)) { if (!empty($items[$row['uri-id']]) && ($row['uid'] == 0)) { continue; } + if (($mode != self::MODE_CONTACTS) && !$row['origin']) { + $row['featured'] = false; + } + if ($max_comments > 0) { - if (($row['gravity'] == GRAVITY_COMMENT) && (++$commentcounter[$row['parent-uri-id']] > $max_comments)) { + if (($row['gravity'] == ItemModel::GRAVITY_COMMENT) && (++$commentcounter[$row['parent-uri-id']] > $max_comments)) { continue; } - if (($row['gravity'] == GRAVITY_ACTIVITY) && (++$activitycounter[$row['parent-uri-id']] > $max_comments)) { + if (($row['gravity'] == ItemModel::GRAVITY_ACTIVITY) && (++$activitycounter[$row['parent-uri-id']] > $max_comments)) { continue; } } - $items[$row['uri-id']] = $this->addRowInformation($row, $activities[$row['uri-id']] ?? []); + + $authors[] = $row['author-id']; + $authors[] = $row['owner-id']; + + if (in_array($row['gravity'], [ItemModel::GRAVITY_PARENT, ItemModel::GRAVITY_COMMENT])) { + $quote_uri_ids[$row['uri-id']] = [ + 'uri-id' => $row['uri-id'], + 'uri' => $row['uri'], + 'parent-uri-id' => $row['parent-uri-id'], + 'parent-uri' => $row['parent-uri'], + ]; + } + + $items[$row['uri-id']] = $this->addRowInformation($row, $activities[$row['uri-id']] ?? [], $thr_parent[$row['thr-parent-id']] ?? []); } DBA::close($thread_items); + $quotes = Post::select(array_merge(ItemModel::DISPLAY_FIELDLIST, ['featured', 'contact-uid', 'gravity', 'post-type', 'post-reason']), ['quote-uri-id' => array_column($quote_uri_ids, 'uri-id'), 'body' => '', 'uid' => 0]); + while ($quote = Post::fetch($quotes)) { + $row = $quote; + + $row['uid'] = $uid; + $row['verb'] = $row['body'] = $row['raw-body'] = Activity::ANNOUNCE; + $row['gravity'] = ItemModel::GRAVITY_ACTIVITY; + $row['object-type'] = Activity\ObjectType::NOTE; + $row['parent-uri'] = $quote_uri_ids[$quote['quote-uri-id']]['parent-uri']; + $row['parent-uri-id'] = $quote_uri_ids[$quote['quote-uri-id']]['parent-uri-id']; + $row['thr-parent'] = $quote_uri_ids[$quote['quote-uri-id']]['uri']; + $row['thr-parent-id'] = $quote_uri_ids[$quote['quote-uri-id']]['uri-id']; + + $authors[] = $row['author-id']; + $authors[] = $row['owner-id']; + + $items[$row['uri-id']] = $this->addRowInformation($row, [], []); + } + DBA::close($quotes); + + $authors = array_unique($authors); + + $blocks = []; + $ignores = []; + $collapses = []; + if (!empty($authors)) { + $usercontacts = DBA::select('user-contact', ['cid', 'blocked', 'ignored', 'collapsed'], ['uid' => $uid, 'cid' => $authors]); + while ($usercontact = DBA::fetch($usercontacts)) { + if ($usercontact['blocked']) { + $blocks[] = $usercontact['cid']; + } + if ($usercontact['ignored']) { + $ignores[] = $usercontact['cid']; + } + if ($usercontact['collapsed']) { + $collapses[] = $usercontact['cid']; + } + } + DBA::close($usercontacts); + } + + 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); + $items[$key]['user-ignored-author'] = !$always_display && in_array($row['author-id'], $ignores); + $items[$key]['user-blocked-owner'] = !$always_display && in_array($row['owner-id'], $blocks); + $items[$key]['user-ignored-owner'] = !$always_display && in_array($row['owner-id'], $ignores); + $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))) { + unset($items[$key]); + } + } + $items = $this->convSort($items, $order); $this->profiler->stopRecording(); 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. * @@ -964,12 +1172,12 @@ class Conversation * @param bool $recursive * @return array */ - private function getItemChildren(array &$item_list, array $parent, $recursive = true) + private function getItemChildren(array &$item_list, array $parent, bool $recursive = true): array { $this->profiler->startRecording('rendering'); $children = []; foreach ($item_list as $i => $item) { - if ($item['gravity'] != GRAVITY_PARENT) { + if ($item['gravity'] != ItemModel::GRAVITY_PARENT) { if ($recursive) { // Fallback to parent-uri if thr-parent is not set $thr_parent = $item['thr-parent-id']; @@ -999,7 +1207,7 @@ class Conversation * @param array $items * @return array */ - private function sortItemChildren(array $items) + private function sortItemChildren(array $items): array { $this->profiler->startRecording('rendering'); $result = $items; @@ -1045,7 +1253,7 @@ class Conversation * @param array $parent A tree-like array of items * @return array */ - private function smartFlattenConversation(array $parent) + private function smartFlattenConversation(array $parent): array { $this->profiler->startRecording('rendering'); if (!isset($parent['children']) || count($parent['children']) == 0) { @@ -1101,7 +1309,7 @@ class Conversation * @return array * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private function convSort(array $item_list, $order) + private function convSort(array $item_list, string $order): array { $this->profiler->startRecording('rendering'); $parents = []; @@ -1126,17 +1334,21 @@ class Conversation // Extract the top level items foreach ($item_array as $item) { - if ($item['gravity'] == GRAVITY_PARENT) { + if ($item['gravity'] == ItemModel::GRAVITY_PARENT) { $parents[] = $item; } } if (stristr($order, 'pinned_received')) { - usort($parents, [$this, 'sortThrPinnedReceived']); + usort($parents, [$this, 'sortThrFeaturedReceived']); + } elseif (stristr($order, 'pinned_commented')) { + usort($parents, [$this, 'sortThrFeaturedCommented']); } elseif (stristr($order, 'received')) { usort($parents, [$this, 'sortThrReceived']); } elseif (stristr($order, 'commented')) { usort($parents, [$this, 'sortThrCommented']); + } elseif (stristr($order, 'created')) { + usort($parents, [$this, 'sortThrCreated']); } /* @@ -1152,13 +1364,13 @@ class Conversation $parents[$i]['children'] = $this->sortItemChildren($parents[$i]['children']); } - if (!$this->pConfig->get(local_user(), 'system', 'no_smart_threading', 0)) { + if (!$this->pConfig->get($this->session->getLocalUserId(), 'system', 'no_smart_threading', 0)) { foreach ($parents as $i => $parent) { $parents[$i] = $this->smartFlattenConversation($parent); } } - /// @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'])) { @@ -1171,23 +1383,41 @@ class Conversation } /** - * usort() callback to sort item arrays by pinned and the received key + * usort() callback to sort item arrays by featured and the received key * * @param array $a * @param array $b * @return int */ - private function sortThrPinnedReceived(array $a, array $b) + private function sortThrFeaturedReceived(array $a, array $b): int { - if ($b['pinned'] && !$a['pinned']) { + if ($b['featured'] && !$a['featured']) { return 1; - } elseif (!$b['pinned'] && $a['pinned']) { + } elseif (!$b['featured'] && $a['featured']) { return -1; } return strcmp($b['received'], $a['received']); } + /** + * usort() callback to sort item arrays by featured and the received key + * + * @param array $a + * @param array $b + * @return int + */ + private function sortThrFeaturedCommented(array $a, array $b): int + { + if ($b['featured'] && !$a['featured']) { + return 1; + } elseif (!$b['featured'] && $a['featured']) { + return -1; + } + + return strcmp($b['commented'], $a['commented']); + } + /** * usort() callback to sort item arrays by the received key * @@ -1195,7 +1425,7 @@ class Conversation * @param array $b * @return int */ - private function sortThrReceived(array $a, array $b) + private function sortThrReceived(array $a, array $b): int { return strcmp($b['received'], $a['received']); } @@ -1207,7 +1437,7 @@ class Conversation * @param array $b * @return int */ - private function sortThrReceivedRev(array $a, array $b) + private function sortThrReceivedRev(array $a, array $b): int { return strcmp($a['received'], $b['received']); } @@ -1219,8 +1449,20 @@ class Conversation * @param array $b * @return int */ - private function sortThrCommented(array $a, array $b) + private function sortThrCommented(array $a, array $b): int { return strcmp($b['commented'], $a['commented']); } + + /** + * usort() callback to sort item arrays by the created key + * + * @param array $a + * @param array $b + * @return int + */ + private function sortThrCreated(array $a, array $b): int + { + return strcmp($b['created'], $a['created']); + } }