diff --git a/src/Content/Conversation.php b/src/Content/Conversation.php index 9e7348469..289b7851c 100644 --- a/src/Content/Conversation.php +++ b/src/Content/Conversation.php @@ -495,7 +495,8 @@ 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['no_sharer']) ? '&no_sharer=' . rawurlencode($_GET['no_sharer']) : '') + . (!empty($_GET['accounttype']) ? '&accounttype=' . rawurlencode($_GET['accounttype']) : '') . "'; \r\n"; } } elseif ($mode === self::MODE_PROFILE) { @@ -930,7 +931,8 @@ class Conversation continue; } - if (in_array($row['author-gsid'], $ignoredGsids) + if ( + in_array($row['author-gsid'], $ignoredGsids) || in_array($row['owner-gsid'], $ignoredGsids) || in_array($row['causer-gsid'], $ignoredGsids) ) { diff --git a/src/Content/Conversation/Collection/Channels.php b/src/Content/Conversation/Collection/Timelines.php similarity index 95% rename from src/Content/Conversation/Collection/Channels.php rename to src/Content/Conversation/Collection/Timelines.php index a523cc7b2..da9c7c9e6 100644 --- a/src/Content/Conversation/Collection/Channels.php +++ b/src/Content/Conversation/Collection/Timelines.php @@ -23,6 +23,6 @@ namespace Friendica\Content\Conversation\Collection; use Friendica\BaseCollection; -class Channels extends BaseCollection +class Timelines extends BaseCollection { } diff --git a/src/Content/Conversation/Entity/Channel.php b/src/Content/Conversation/Entity/Timeline.php similarity index 78% rename from src/Content/Conversation/Entity/Channel.php rename to src/Content/Conversation/Entity/Timeline.php index b8e0e2bb2..b9ab1e1a0 100644 --- a/src/Content/Conversation/Entity/Channel.php +++ b/src/Content/Conversation/Entity/Timeline.php @@ -26,8 +26,9 @@ namespace Friendica\Content\Conversation\Entity; * @property-read string $label Channel label * @property-read string $description Channel description * @property-read string $accessKey Access key + * @property-read string $path Path */ -final class Channel extends \Friendica\BaseEntity +final class Timeline extends \Friendica\BaseEntity { const WHATSHOT = 'whatshot'; const FORYOU = 'foryou'; @@ -37,6 +38,13 @@ final class Channel extends \Friendica\BaseEntity const VIDEO = 'video'; const AUDIO = 'audio'; const LANGUAGE = 'language'; + const LOCAL = 'local'; + const GLOBAL = 'global'; + const STAR = 'star'; + const MENTION = 'mention'; + const RECEIVED = 'received'; + const COMMENTED = 'commented'; + const CREATED = 'created'; /** @var string */ protected $code; @@ -46,12 +54,15 @@ final class Channel extends \Friendica\BaseEntity protected $description; /** @var string */ protected $accessKey; + /** @var string */ + protected $path; - public function __construct(string $code, string $label, string $description, string $accessKey) + public function __construct(string $code, string $label, string $description, string $accessKey, string $path = null) { $this->code = $code; $this->label = $label; $this->description = $description; $this->accessKey = $accessKey; + $this->path = $path; } } diff --git a/src/Content/Conversation/Factory/Channel.php b/src/Content/Conversation/Factory/Channel.php deleted file mode 100644 index c5d172225..000000000 --- a/src/Content/Conversation/Factory/Channel.php +++ /dev/null @@ -1,65 +0,0 @@ -. - * - */ - -namespace Friendica\Content\Conversation\Factory; - -use Friendica\Content\Conversation\Collection\Channels; -use Friendica\Model\User; -use Friendica\Content\Conversation\Entity\Channel as ChannelEntity; -use Friendica\Core\L10n; -use Psr\Log\LoggerInterface; - -final class Channel extends \Friendica\BaseFactory -{ - /** @var L10n */ - protected $l10n; - - public function __construct(L10n $l10n, LoggerInterface $logger) - { - parent::__construct($logger); - - $this->l10n = $l10n; - } - - /** - * List of available channels - * - * @param integer $uid - * @return array - */ - public function getForUser(int $uid): Channels - { - $language = User::getLanguageCode($uid); - $languages = $this->l10n->getAvailableLanguages(true); - - $tabs = [ - new ChannelEntity(ChannelEntity::FORYOU, $this->l10n->t('For you'), $this->l10n->t('Posts from contacts you interact with and who interact with you'), 'y'), - new ChannelEntity(ChannelEntity::WHATSHOT, $this->l10n->t('What\'s Hot'), $this->l10n->t('Posts with a lot of interactions'), 'h'), - new ChannelEntity(ChannelEntity::LANGUAGE, $languages[$language], $this->l10n->t('Posts in %s', $languages[$language]), 'g'), - new ChannelEntity(ChannelEntity::FOLLOWERS, $this->l10n->t('Followers'), $this->l10n->t('Posts from your followers that you don\'t follow'), 'f'), - new ChannelEntity(ChannelEntity::SHARERSOFSHARERS, $this->l10n->t('Sharers of sharers'), $this->l10n->t('Posts from accounts that are followed by accounts that you follow'), 'r'), - new ChannelEntity(ChannelEntity::IMAGE, $this->l10n->t('Images'), $this->l10n->t('Posts with images'), 'i'), - new ChannelEntity(ChannelEntity::AUDIO, $this->l10n->t('Audio'), $this->l10n->t('Posts with audio'), 'd'), - new ChannelEntity(ChannelEntity::VIDEO, $this->l10n->t('Videos'), $this->l10n->t('Posts with videos'), 'v'), - ]; - return new Channels($tabs); - } -} diff --git a/src/Content/Conversation/Factory/Timeline.php b/src/Content/Conversation/Factory/Timeline.php new file mode 100644 index 000000000..160e55d1c --- /dev/null +++ b/src/Content/Conversation/Factory/Timeline.php @@ -0,0 +1,119 @@ +. + * + */ + +namespace Friendica\Content\Conversation\Factory; + +use Friendica\Content\Conversation\Collection\Timelines; +use Friendica\Model\User; +use Friendica\Content\Conversation\Entity\Timeline as TimelineEntity; +use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Core\L10n; +use Friendica\Module\Conversation\Community; +use Psr\Log\LoggerInterface; + +final class Timeline extends \Friendica\BaseFactory +{ + /** @var L10n */ + protected $l10n; + /** @var IManageConfigValues The config */ + protected $config; + + public function __construct(L10n $l10n, LoggerInterface $logger, IManageConfigValues $config) + { + parent::__construct($logger); + + $this->l10n = $l10n; + $this->config = $config; + } + + /** + * List of available channels + * + * @param integer $uid + * @return Timelines + */ + public function getChannelsForUser(int $uid): Timelines + { + $language = User::getLanguageCode($uid); + $languages = $this->l10n->getAvailableLanguages(true); + + $tabs = [ + new TimelineEntity(TimelineEntity::FORYOU, $this->l10n->t('For you'), $this->l10n->t('Posts from contacts you interact with and who interact with you'), 'y'), + new TimelineEntity(TimelineEntity::WHATSHOT, $this->l10n->t('What\'s Hot'), $this->l10n->t('Posts with a lot of interactions'), 'h'), + new TimelineEntity(TimelineEntity::LANGUAGE, $languages[$language], $this->l10n->t('Posts in %s', $languages[$language]), 'g'), + new TimelineEntity(TimelineEntity::FOLLOWERS, $this->l10n->t('Followers'), $this->l10n->t('Posts from your followers that you don\'t follow'), 'f'), + new TimelineEntity(TimelineEntity::SHARERSOFSHARERS, $this->l10n->t('Sharers of sharers'), $this->l10n->t('Posts from accounts that are followed by accounts that you follow'), 'r'), + new TimelineEntity(TimelineEntity::IMAGE, $this->l10n->t('Images'), $this->l10n->t('Posts with images'), 'i'), + new TimelineEntity(TimelineEntity::AUDIO, $this->l10n->t('Audio'), $this->l10n->t('Posts with audio'), 'd'), + new TimelineEntity(TimelineEntity::VIDEO, $this->l10n->t('Videos'), $this->l10n->t('Posts with videos'), 'v'), + ]; + return new Timelines($tabs); + } + + /** + * List of available communities + * + * @param boolean $authenticated + * @return Timelines + */ + public function getCommunities(bool $authenticated): Timelines + { + $page_style = $this->config->get('system', 'community_page_style'); + $tabs = []; + + if (($authenticated || in_array($page_style, [Community::LOCAL_AND_GLOBAL, Community::LOCAL])) && empty($this->config->get('system', 'singleuser'))) { + $tabs[] = new TimelineEntity(TimelineEntity::LOCAL, $this->l10n->t('Local Community'), $this->l10n->t('Posts from local users on this server'), 'l'); + } + + if ($authenticated || in_array($page_style, [Community::LOCAL_AND_GLOBAL, Community::GLOBAL])) { + $tabs[] = new TimelineEntity(TimelineEntity::GLOBAL, $this->l10n->t('Global Community'), $this->l10n->t('Posts from users of the whole federated network'), 'g'); + } + return new Timelines($tabs); + } + + /** + * List of available network feeds + * + * @param string $command + * @return Timelines + */ + public function getNetworkFeeds(string $command): Timelines + { + $tabs = [ + new TimelineEntity(TimelineEntity::COMMENTED, $this->l10n->t('Latest Activity'), $this->l10n->t('Sort by latest activity'), 'e', $command . '?' . http_build_query(['order' => 'commented'])), + new TimelineEntity(TimelineEntity::RECEIVED, $this->l10n->t('Latest Posts'), $this->l10n->t('Sort by post received date'), 't', $command . '?' . http_build_query(['order' => 'received'])), + new TimelineEntity(TimelineEntity::CREATED, $this->l10n->t('Latest Creation'), $this->l10n->t('Sort by post creation date'), 'q', $command . '?' . http_build_query(['order' => 'created'])), + new TimelineEntity(TimelineEntity::MENTION, $this->l10n->t('Personal'), $this->l10n->t('Posts that mention or involve you'), 'r', $command . '?' . http_build_query(['mention' => true])), + new TimelineEntity(TimelineEntity::STAR, $this->l10n->t('Starred'), $this->l10n->t('Favourite Posts'), 'm', $command . '?' . http_build_query(['star' => true])), + ]; + return new Timelines($tabs); + } + + public function isCommunity(string $selectedTab): bool + { + return in_array($selectedTab, [TimelineEntity::LOCAL, TimelineEntity::GLOBAL]); + } + + public function isChannel(string $selectedTab): bool + { + return in_array($selectedTab, [TimelineEntity::WHATSHOT, TimelineEntity::FORYOU, TimelineEntity::FOLLOWERS, TimelineEntity::SHARERSOFSHARERS, TimelineEntity::IMAGE, TimelineEntity::VIDEO, TimelineEntity::AUDIO, TimelineEntity::LANGUAGE]); + } +} diff --git a/src/Content/Nav.php b/src/Content/Nav.php index cfebc08f3..6cc799eb6 100644 --- a/src/Content/Nav.php +++ b/src/Content/Nav.php @@ -52,7 +52,7 @@ class Nav 'directory' => null, 'settings' => null, 'contacts' => null, - 'delegation'=> null, + 'delegation' => null, 'calendar' => null, 'register' => null ]; @@ -284,14 +284,14 @@ class Nav $gdirpath = Profile::zrl($this->config->get('system', 'directory'), true); } - if (($this->session->getLocalUserId() || $this->config->get('system', 'community_page_style') != Community::DISABLED_VISITOR) && - !($this->config->get('system', 'community_page_style') == Community::DISABLED)) { + if ((!$this->session->isAuthenticated() && $this->config->get('system', 'community_page_style') != Community::DISABLED_VISITOR) && + !($this->config->get('system', 'community_page_style') == Community::DISABLED) + ) { $nav['community'] = ['community', $this->l10n->t('Community'), '', $this->l10n->t('Conversations on this and other servers')]; } - $nav['channel'] = ['channel', $this->l10n->t('Channels'), '', $this->l10n->t('Current posts, filtered by several rules')]; - if ($this->session->getLocalUserId()) { + $nav['channel'] = ['channel', $this->l10n->t('Channels'), '', $this->l10n->t('Current posts, filtered by several rules')]; $nav['calendar'] = ['calendar', $this->l10n->t('Calendar'), '', $this->l10n->t('Calendar')]; } diff --git a/src/Module/Conversation/Channel.php b/src/Module/Conversation/Channel.php index 3717a4d25..12d8ae035 100644 --- a/src/Module/Conversation/Channel.php +++ b/src/Module/Conversation/Channel.php @@ -23,83 +23,49 @@ namespace Friendica\Module\Conversation; use Friendica\App; use Friendica\App\Mode; -use Friendica\BaseModule; use Friendica\Content\BoundariesPager; use Friendica\Content\Conversation; -use Friendica\Content\Conversation\Entity\Channel as ChannelEntity; -use Friendica\Content\Conversation\Factory\Channel as ChannelFactory; +use Friendica\Content\Conversation\Entity\Timeline as TimelineEntity; +use Friendica\Content\Conversation\Factory\Timeline as TimelineFactory; use Friendica\Content\Feature; use Friendica\Content\Nav; use Friendica\Content\Text\HTML; use Friendica\Content\Widget; use Friendica\Content\Widget\TrendingTags; use Friendica\Core\Cache\Capability\ICanCache; -use Friendica\Core\Cache\Enum\Duration; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\L10n; use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; use Friendica\Core\Renderer; use Friendica\Core\Session\Capability\IHandleUserSessions; -use Friendica\Model\Contact; use Friendica\Model\Post; -use Friendica\Model\User; use Friendica\Module\Security\Login; use Friendica\Network\HTTPException; -use Friendica\Core\Session\Model\UserSession; use Friendica\Database\Database; -use Friendica\Model\Item; use Friendica\Module\Response; use Friendica\Navigation\SystemMessages; -use Friendica\Util\DateTimeFormat; use Friendica\Util\Profiler; use Psr\Log\LoggerInterface; -class Channel extends BaseModule +class Channel extends Timeline { - protected static $content; - protected static $accountTypeString; - protected static $accountType; - protected static $itemsPerPage; - protected static $min_id; - protected static $max_id; - protected static $item_id; - - /** @var UserSession */ - protected $session; - /** @var ICanCache */ - protected $cache; - /** @var IManageConfigValues The config */ - protected $config; - /** @var SystemMessages */ - protected $systemMessages; - /** @var App\Page */ - protected $page; + /** @var TimelineFactory */ + protected $timeline; /** @var Conversation */ protected $conversation; - /** @var App\Mode $mode */ - protected $mode; - /** @var IManagePersonalConfigValues */ - protected $pConfig; - /** @var Database */ - protected $database; - /** @var ChannelFactory */ - protected $channel; + /** @var App\Page */ + protected $page; + /** @var SystemMessages */ + protected $systemMessages; - - public function __construct(ChannelFactory $channel, SystemMessages $systemMessages, Database $database, IManagePersonalConfigValues $pConfig, Mode $mode, Conversation $conversation, App\Page $page, IManageConfigValues $config, ICanCache $cache, IHandleUserSessions $session, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) + public function __construct(TimelineFactory $timeline, Conversation $conversation, App\Page $page, SystemMessages $systemMessages, Mode $mode, IHandleUserSessions $session, Database $database, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, ICanCache $cache, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) { - parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); + parent::__construct($mode, $session, $database, $pConfig, $config, $cache, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); - $this->channel = $channel; - $this->systemMessages = $systemMessages; - $this->database = $database; - $this->pConfig = $pConfig; - $this->mode = $mode; + $this->timeline = $timeline; $this->conversation = $conversation; $this->page = $page; - $this->config = $config; - $this->cache = $cache; - $this->session = $session; + $this->systemMessages = $systemMessages; } protected function content(array $request = []): string @@ -122,78 +88,48 @@ class Channel extends BaseModule } if (empty($request['mode']) || ($request['mode'] != 'raw')) { - $tabs = []; - - foreach ($this->channel->getForUser($this->session->getLocalUserId()) as $tab) { - $tabs[] = [ - 'label' => $tab->label, - 'url' => 'channel/' . $tab->code, - 'sel' => self::$content == $tab->code ? 'active' : '', - 'title' => $tab->description, - 'id' => 'channel-' . $tab->code . '-tab', - 'accesskey' => $tab->accessKey, - ]; - } + $tabs = $this->getTabArray($this->timeline->getChannelsForUser($this->session->getLocalUserId()), 'channel'); + $tabs = array_merge($tabs, $this->getTabArray($this->timeline->getCommunities(true), 'channel')); $tab_tpl = Renderer::getMarkupTemplate('common_tabs.tpl'); $o .= Renderer::replaceMacros($tab_tpl, ['$tabs' => $tabs]); Nav::setSelected('channel'); - $this->page['aside'] .= Widget::accountTypes('channel/' . self::$content, self::$accountTypeString); + $this->page['aside'] .= Widget::accountTypes('channel/' . self::$selectedTab, self::$accountTypeString); - if (!in_array(self::$content, [ChannelEntity::FOLLOWERS, ChannelEntity::FORYOU]) && $this->config->get('system', 'community_no_sharer')) { - $path = self::$content; - if (!empty($this->parameters['accounttype'])) { - $path .= '/' . $this->parameters['accounttype']; - } - $query_parameters = []; - - if (!empty($request['min_id'])) { - $query_parameters['min_id'] = $request['min_id']; - } - if (!empty($request['max_id'])) { - $query_parameters['max_id'] = $request['max_id']; - } - if (!empty($request['last_created'])) { - $query_parameters['max_id'] = $request['last_created']; - } - - $path_all = $path . (!empty($query_parameters) ? '?' . http_build_query($query_parameters) : ''); - $path_no_sharer = $path . '?' . http_build_query(array_merge($query_parameters, ['no_sharer' => true])); - $this->page['aside'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('widget/community_sharer.tpl'), [ - '$title' => $this->l10n->t('Own Contacts'), - '$path_all' => $path_all, - '$path_no_sharer' => $path_no_sharer, - '$no_sharer' => !empty($request['no_sharer']), - '$all' => $this->l10n->t('Include'), - '$no_sharer_label' => $this->l10n->t('Hide'), - '$base' => 'channel', - ]); + if (!in_array(self::$selectedTab, [TimelineEntity::FOLLOWERS, TimelineEntity::FORYOU]) && $this->config->get('system', 'community_no_sharer')) { + $this->page['aside'] .= $this->getNoSharerWidget('channel'); } if (Feature::isEnabled($this->session->getLocalUserId(), 'trending_tags')) { - $this->page['aside'] .= TrendingTags::getHTML(self::$content); + $this->page['aside'] .= TrendingTags::getHTML(self::$selectedTab); } // We need the editor here to be able to reshare an item. $o .= $this->conversation->statusEditor([], 0, true); } - $items = $this->getItems($request); + if ($this->timeline->isChannel(self::$selectedTab)) { + $items = $this->getChannelItems(); + $order = 'created'; + } else { + $items = $this->getCommunityItems(); + $order = 'commented'; + } if (!$this->database->isResult($items)) { $this->systemMessages->addNotice($this->l10n->t('No results.')); return $o; } - $o .= $this->conversation->render($items, Conversation::MODE_CHANNEL, false, false, 'created', $this->session->getLocalUserId()); + $o .= $this->conversation->render($items, Conversation::MODE_CHANNEL, false, false, $order, $this->session->getLocalUserId()); $pager = new BoundariesPager( $this->l10n, $this->args->getQueryString(), - $items[0]['created'], - $items[count($items) - 1]['created'], + $items[0][$order], + $items[count($items) - 1][$order], self::$itemsPerPage ); @@ -214,34 +150,16 @@ class Channel extends BaseModule */ protected function parseRequest(array $request) { - self::$accountTypeString = $request['accounttype'] ?? $this->parameters['accounttype'] ?? ''; - self::$accountType = User::getAccountTypeByString(self::$accountTypeString); + parent::parseRequest($request); - self::$content = $this->parameters['content'] ?? ''; - if (!self::$content) { - self::$content = ChannelEntity::FORYOU; + if (!self::$selectedTab) { + self::$selectedTab = TimelineEntity::FORYOU; } - if (!in_array(self::$content, [ChannelEntity::WHATSHOT, ChannelEntity::FORYOU, ChannelEntity::FOLLOWERS, ChannelEntity::SHARERSOFSHARERS, ChannelEntity::IMAGE, ChannelEntity::VIDEO, ChannelEntity::AUDIO, ChannelEntity::LANGUAGE])) { + if (!$this->timeline->isChannel(self::$selectedTab) && !$this->timeline->isCommunity(self::$selectedTab)) { throw new HTTPException\BadRequestException($this->l10n->t('Channel not available.')); } - if ($this->mode->isMobile()) { - self::$itemsPerPage = $this->pConfig->get( - $this->session->getLocalUserId(), - 'system', - 'itemspage_mobile_network', - $this->config->get('system', 'itemspage_network_mobile') - ); - } else { - self::$itemsPerPage = $this->pConfig->get( - $this->session->getLocalUserId(), - 'system', - 'itemspage_network', - $this->config->get('system', 'itemspage_network') - ); - } - if (!empty($request['item'])) { $item = Post::selectFirst(['parent-uri-id'], ['id' => $request['item']]); self::$item_id = $item['parent-uri-id'] ?? 0; @@ -249,191 +167,6 @@ class Channel extends BaseModule self::$item_id = 0; } - self::$min_id = $request['min_id'] ?? null; - self::$max_id = $request['last_created'] ?? $request['max_id'] ?? null; - } - - /** - * Database query for the channel page - * - * @return array - * @throws \Exception - */ - protected function getItems(array $request) - { - if (self::$content == ChannelEntity::WHATSHOT) { - if (!is_null(self::$accountType)) { - $condition = ["(`comments` >= ? OR `activities` >= ?) AND `contact-type` = ?", $this->getMedianComments(4), $this->getMedianActivities(4), self::$accountType]; - } else { - $condition = ["(`comments` >= ? OR `activities` >= ?) AND `contact-type` != ?", $this->getMedianComments(4), $this->getMedianActivities(4), Contact::TYPE_COMMUNITY]; - } - } elseif (self::$content == ChannelEntity::FORYOU) { - $cid = Contact::getPublicIdByUserId($this->session->getLocalUserId()); - $condition = [ - "(`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `relation-thread-score` > ?) OR - ((`comments` >= ? OR `activities` >= ?) AND `owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ?)) OR - (`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` IN (?, ?) AND `notify_new_posts`)))", - $cid, $this->getMedianRelationThreadScore($cid, 4), $this->getMedianComments(4), $this->getMedianActivities(4), $cid, - $this->session->getLocalUserId(), Contact::FRIEND, Contact::SHARING - ]; - } elseif (self::$content == ChannelEntity::FOLLOWERS) { - $condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` = ?)", $this->session->getLocalUserId(), Contact::FOLLOWER]; - } elseif (self::$content == ChannelEntity::SHARERSOFSHARERS) { - $cid = Contact::getPublicIdByUserId($this->session->getLocalUserId()); - - // @todo Suggest posts from contacts that are followed most by our followers - $condition = [ - "`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `last-interaction` > ? - AND `relation-cid` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ? AND `relation-thread-score` >= ?) - AND NOT `cid` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ?))", - DateTimeFormat::utc('now - ' . $this->config->get('channel', 'sharer_interaction_days') .' day'), $cid, $this->getMedianRelationThreadScore($cid, 4), $cid - ]; - } elseif (self::$content == ChannelEntity::IMAGE) { - $condition = ["`media-type` & ?", 1]; - } elseif (self::$content == ChannelEntity::VIDEO) { - $condition = ["`media-type` & ?", 2]; - } elseif (self::$content == ChannelEntity::AUDIO) { - $condition = ["`media-type` & ?", 4]; - } elseif (self::$content == ChannelEntity::LANGUAGE) { - $condition = ["JSON_EXTRACT(JSON_KEYS(language), '$[0]') = ?", $this->l10n->convertCodeForLanguageDetection(User::getLanguageCode($this->session->getLocalUserId()))]; - } - - if (self::$content != ChannelEntity::LANGUAGE) { - $condition = $this->addLanguageCondition($condition); - } - - $condition[0] .= " AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `post-engagement`.`owner-id` AND (`ignored` OR `blocked` OR `collapsed`))"; - $condition[] = $this->session->getLocalUserId(); - - if ((self::$content != ChannelEntity::WHATSHOT) && !is_null(self::$accountType)) { - $condition[0] .= " AND `contact-type` = ?"; - $condition[] = self::$accountType; - } - - $params = ['order' => ['created' => true], 'limit' => self::$itemsPerPage]; - - if (!empty(self::$item_id)) { - $condition[0] .= " AND `uri-id` = ?"; - $condition[] = self::$item_id; - } else { - if (!empty($request['no_sharer'])) { - $condition[0] .= " AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `post-user`.`uid` = ? AND `post-user`.`uri-id` = `post-engagement`.`uri-id`)"; - $condition[] = $this->session->getLocalUserId(); - } - - if (isset(self::$max_id)) { - $condition[0] .= " AND `created` < ?"; - $condition[] = self::$max_id; - } - - if (isset(self::$min_id)) { - $condition[0] .= " AND `created` > ?"; - $condition[] = self::$min_id; - - // Previous page case: we want the items closest to min_id but for that we need to reverse the query order - if (!isset(self::$max_id)) { - $params['order']['created'] = false; - } - } - } - - $items = $this->database->selectToArray('post-engagement', ['uri-id', 'created'], $condition, $params); - if (empty($items)) { - return []; - } - - // Previous page case: once we get the relevant items closest to min_id, we need to restore the expected display order - if (empty(self::$item_id) && isset(self::$min_id) && !isset(self::$max_id)) { - $items = array_reverse($items); - } - - Item::update(['unseen' => false], ['unseen' => true, 'uid' => $this->session->getLocalUserId(), 'uri-id' => array_column($items, 'uri-id')]); - - return $items; - } - - private function addLanguageCondition(array $condition): array - { - $conditions = []; - $languages = $this->pConfig->get($this->session->getLocalUserId(), 'channel', 'languages', [User::getLanguageCode($this->session->getLocalUserId())]); - $languages = $this->l10n->convertForLanguageDetection($languages); - foreach ($languages as $language) { - $conditions[] = "JSON_EXTRACT(JSON_KEYS(language), '$[0]') = ?"; - $condition[] = $language; - } - if (!empty($conditions)) { - $condition[0] .= " AND (`language` IS NULL OR " . implode(' OR ', $conditions) . ")"; - } - return $condition; - } - - private function getMedianComments(int $divider): int - { - $languages = $this->pConfig->get($this->session->getLocalUserId(), 'channel', 'languages', [User::getLanguageCode($this->session->getLocalUserId())]); - $cache_key = 'Channel:getMedianComments:' . $divider . ':' . implode(':', $languages); - $comments = $this->cache->get($cache_key); - if (!empty($comments)) { - return $comments; - } - - $condition = ["`contact-type` != ? AND `comments` > ?", Contact::TYPE_COMMUNITY, 0]; - $condition = $this->addLanguageCondition($condition); - - $limit = $this->database->count('post-engagement', $condition) / $divider; - $post = $this->database->selectToArray('post-engagement', ['comments'], $condition, ['order' => ['comments' => true], 'limit' => [$limit, 1]]); - $comments = $post[0]['comments'] ?? 0; - if (empty($comments)) { - return 0; - } - - $this->cache->set($cache_key, $comments, Duration::HALF_HOUR); - $this->logger->debug('Calculated median comments', ['divider' => $divider, 'languages' => $languages, 'median' => $comments]); - return $comments; - } - - private function getMedianActivities(int $divider): int - { - $languages = $this->pConfig->get($this->session->getLocalUserId(), 'channel', 'languages', [User::getLanguageCode($this->session->getLocalUserId())]); - $cache_key = 'Channel:getMedianActivities:' . $divider . ':' . implode(':', $languages); - $activities = $this->cache->get($cache_key); - if (!empty($activities)) { - return $activities; - } - - $condition = ["`contact-type` != ? AND `activities` > ?", Contact::TYPE_COMMUNITY, 0]; - $condition = $this->addLanguageCondition($condition); - - $limit = $this->database->count('post-engagement', $condition) / $divider; - $post = $this->database->selectToArray('post-engagement', ['activities'], $condition, ['order' => ['activities' => true], 'limit' => [$limit, 1]]); - $activities = $post[0]['activities'] ?? 0; - if (empty($activities)) { - return 0; - } - - $this->cache->set($cache_key, $activities, Duration::HALF_HOUR); - $this->logger->debug('Calculated median activities', ['divider' => $divider, 'languages' => $languages, 'median' => $activities]); - return $activities; - } - - private function getMedianRelationThreadScore(int $cid, int $divider): int - { - $cache_key = 'Channel:getThreadScore:' . $cid . ':' . $divider; - $score = $this->cache->get($cache_key); - if (!empty($score)) { - return $score; - } - - $condition = ["`relation-cid` = ? AND `relation-thread-score` > ?", $cid, 0]; - - $limit = $this->database->count('contact-relation', $condition) / $divider; - $relation = $this->database->selectToArray('contact-relation', ['relation-thread-score'], $condition, ['order' => ['relation-thread-score' => true], 'limit' => [$limit, 1]]); - $score = $relation[0]['relation-thread-score'] ?? 0; - if (empty($score)) { - return 0; - } - - $this->cache->set($cache_key, $score, Duration::HALF_HOUR); - $this->logger->debug('Calculated median score', ['cid' => $cid, 'divider' => $divider, 'median' => $score]); - return $score; + self::$max_id = $request['last_created'] ?? self::$max_id; } } diff --git a/src/Module/Conversation/Community.php b/src/Module/Conversation/Community.php index 08fcf52cc..9ab5f731e 100644 --- a/src/Module/Conversation/Community.php +++ b/src/Module/Conversation/Community.php @@ -22,23 +22,32 @@ namespace Friendica\Module\Conversation; -use Friendica\BaseModule; +use Friendica\App; +use Friendica\App\Mode; use Friendica\Content\BoundariesPager; use Friendica\Content\Conversation; +use Friendica\Content\Conversation\Entity\Timeline as TimelineEntity; +use Friendica\Content\Conversation\Factory\Timeline as TimelineFactory; use Friendica\Content\Feature; use Friendica\Content\Nav; use Friendica\Content\Text\HTML; use Friendica\Content\Widget; use Friendica\Content\Widget\TrendingTags; +use Friendica\Core\Cache\Capability\ICanCache; +use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Core\L10n; +use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; use Friendica\Core\Renderer; -use Friendica\Database\DBA; -use Friendica\DI; -use Friendica\Model\Item; +use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Model\Post; -use Friendica\Model\User; use Friendica\Network\HTTPException; +use Friendica\Database\Database; +use Friendica\Module\Response; +use Friendica\Navigation\SystemMessages; +use Friendica\Util\Profiler; +use Psr\Log\LoggerInterface; -class Community extends BaseModule +class Community extends Timeline { /** * Type of the community page @@ -49,126 +58,86 @@ class Community extends BaseModule const LOCAL = 0; const GLOBAL = 1; const LOCAL_AND_GLOBAL = 2; - /** - * @} - */ protected static $page_style; - protected static $content; - protected static $accountTypeString; - protected static $accountType; - protected static $itemsPerPage; - protected static $min_id; - protected static $max_id; - protected static $item_id; + + /** @var TimelineFactory */ + protected $timeline; + /** @var Conversation */ + protected $conversation; + /** @var App\Page */ + protected $page; + /** @var SystemMessages */ + protected $systemMessages; + + public function __construct(TimelineFactory $timeline, Conversation $conversation, App\Page $page, SystemMessages $systemMessages, Mode $mode, IHandleUserSessions $session, Database $database, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, ICanCache $cache, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) + { + parent::__construct($mode, $session, $database, $pConfig, $config, $cache, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); + + $this->timeline = $timeline; + $this->conversation = $conversation; + $this->page = $page; + $this->systemMessages = $systemMessages; + } protected function content(array $request = []): string { - $this->parseRequest(); + $this->parseRequest($request); $t = Renderer::getMarkupTemplate("community.tpl"); $o = Renderer::replaceMacros($t, [ '$content' => '', '$header' => '', - '$show_global_community_hint' => (self::$content == 'global') && DI::config()->get('system', 'show_global_community_hint'), - '$global_community_hint' => DI::l10n()->t("This community stream shows all public posts received by this node. They may not reflect the opinions of this node’s users.") + '$show_global_community_hint' => (self::$selectedTab == TimelineEntity::GLOBAL) && $this->config->get('system', 'show_global_community_hint'), + '$global_community_hint' => $this->l10n->t("This community stream shows all public posts received by this node. They may not reflect the opinions of this node’s users.") ]); - if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'infinite_scroll')) { + if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'infinite_scroll')) { $tpl = Renderer::getMarkupTemplate('infinite_scroll_head.tpl'); - $o .= Renderer::replaceMacros($tpl, ['$reload_uri' => DI::args()->getQueryString()]); + $o .= Renderer::replaceMacros($tpl, ['$reload_uri' => $this->args->getQueryString()]); } - if (empty($_GET['mode']) || ($_GET['mode'] != 'raw')) { - $tabs = []; - - if ((DI::userSession()->isAuthenticated() || in_array(self::$page_style, [self::LOCAL_AND_GLOBAL, self::LOCAL])) && empty(DI::config()->get('system', 'singleuser'))) { - $tabs[] = [ - 'label' => DI::l10n()->t('Local Community'), - 'url' => 'community/local', - 'sel' => self::$content == 'local' ? 'active' : '', - 'title' => DI::l10n()->t('Posts from local users on this server'), - 'id' => 'community-local-tab', - 'accesskey' => 'l' - ]; - } - - if (DI::userSession()->isAuthenticated() || in_array(self::$page_style, [self::LOCAL_AND_GLOBAL, self::GLOBAL])) { - $tabs[] = [ - 'label' => DI::l10n()->t('Global Community'), - 'url' => 'community/global', - 'sel' => self::$content == 'global' ? 'active' : '', - 'title' => DI::l10n()->t('Posts from users of the whole federated network'), - 'id' => 'community-global-tab', - 'accesskey' => 'g' - ]; - } - + if (empty($request['mode']) || ($request['mode'] != 'raw')) { + $tabs = $this->getTabArray($this->timeline->getCommunities($this->session->isAuthenticated()), 'community'); $tab_tpl = Renderer::getMarkupTemplate('common_tabs.tpl'); $o .= Renderer::replaceMacros($tab_tpl, ['$tabs' => $tabs]); Nav::setSelected('community'); - DI::page()['aside'] .= Widget::accountTypes('community/' . self::$content, self::$accountTypeString); + $this->page['aside'] .= Widget::accountTypes('community/' . self::$selectedTab, self::$accountTypeString); - if (DI::userSession()->getLocalUserId() && DI::config()->get('system', 'community_no_sharer')) { - $path = self::$content; - if (!empty($this->parameters['accounttype'])) { - $path .= '/' . $this->parameters['accounttype']; - } - $query_parameters = []; - - if (!empty($_GET['min_id'])) { - $query_parameters['min_id'] = $_GET['min_id']; - } - if (!empty($_GET['max_id'])) { - $query_parameters['max_id'] = $_GET['max_id']; - } - if (!empty($_GET['last_commented'])) { - $query_parameters['max_id'] = $_GET['last_commented']; - } - - $path_all = $path . (!empty($query_parameters) ? '?' . http_build_query($query_parameters) : ''); - $path_no_sharer = $path . '?' . http_build_query(array_merge($query_parameters, ['no_sharer' => true])); - DI::page()['aside'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('widget/community_sharer.tpl'), [ - '$title' => DI::l10n()->t('Own Contacts'), - '$path_all' => $path_all, - '$path_no_sharer' => $path_no_sharer, - '$no_sharer' => !empty($_REQUEST['no_sharer']), - '$all' => DI::l10n()->t('Include'), - '$no_sharer_label' => DI::l10n()->t('Hide'), - '$base' => 'community', - ]); + if ($this->session->getLocalUserId() && $this->config->get('system', 'community_no_sharer')) { + $this->page['aside'] .= $this->getNoSharerWidget('community'); } - if (Feature::isEnabled(DI::userSession()->getLocalUserId(), 'trending_tags')) { - DI::page()['aside'] .= TrendingTags::getHTML(self::$content); + if (Feature::isEnabled($this->session->getLocalUserId(), 'trending_tags')) { + $this->page['aside'] .= TrendingTags::getHTML(self::$selectedTab); } // We need the editor here to be able to reshare an item. - if (DI::userSession()->isAuthenticated()) { - $o .= DI::conversation()->statusEditor([], 0, true); + if ($this->session->isAuthenticated()) { + $o .= $this->conversation->statusEditor([], 0, true); } } - $items = self::getItems(); + $items = $this->getCommunityItems(); - if (!DBA::isResult($items)) { - DI::sysmsg()->addNotice(DI::l10n()->t('No results.')); + if (!$this->database->isResult($items)) { + $this->systemMessages->addNotice($this->l10n->t('No results.')); return $o; } - $o .= DI::conversation()->render($items, Conversation::MODE_COMMUNITY, false, false, 'commented', DI::userSession()->getLocalUserId()); + $o .= $this->conversation->render($items, Conversation::MODE_COMMUNITY, false, false, 'commented', $this->session->getLocalUserId()); $pager = new BoundariesPager( - DI::l10n(), - DI::args()->getQueryString(), + $this->l10n, + $this->args->getQueryString(), $items[0]['commented'], $items[count($items) - 1]['commented'], self::$itemsPerPage ); - if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'infinite_scroll')) { + if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'infinite_scroll')) { $o .= HTML::scrollLoader(); } else { $o .= $pager->renderMinimal(count($items)); @@ -183,191 +152,58 @@ class Community extends BaseModule * @throws HTTPException\BadRequestException * @throws HTTPException\ForbiddenException */ - protected function parseRequest() + protected function parseRequest($request) { - if (DI::config()->get('system', 'block_public') && !DI::userSession()->isAuthenticated()) { - throw new HTTPException\ForbiddenException(DI::l10n()->t('Public access denied.')); + parent::parseRequest($request); + + if ($this->config->get('system', 'block_public') && !$this->session->isAuthenticated()) { + throw new HTTPException\ForbiddenException($this->l10n->t('Public access denied.')); } - self::$page_style = DI::config()->get('system', 'community_page_style'); + self::$page_style = $this->config->get('system', 'community_page_style'); if (self::$page_style == self::DISABLED) { - throw new HTTPException\ForbiddenException(DI::l10n()->t('Access denied.')); + throw new HTTPException\ForbiddenException($this->l10n->t('Access denied.')); } - self::$accountTypeString = $_GET['accounttype'] ?? $this->parameters['accounttype'] ?? ''; - self::$accountType = User::getAccountTypeByString(self::$accountTypeString); - - self::$content = $this->parameters['content'] ?? ''; - if (!self::$content) { - if (!empty(DI::config()->get('system', 'singleuser'))) { + if (!self::$selectedTab) { + if (!empty($this->config->get('system', 'singleuser'))) { // On single user systems only the global page does make sense - self::$content = 'global'; + self::$selectedTab = TimelineEntity::GLOBAL; } else { // When only the global community is allowed, we use this as default - self::$content = self::$page_style == self::GLOBAL ? 'global' : 'local'; + self::$selectedTab = self::$page_style == self::GLOBAL ? TimelineEntity::GLOBAL : TimelineEntity::LOCAL; } } - if (!in_array(self::$content, ['local', 'global'])) { - throw new HTTPException\BadRequestException(DI::l10n()->t('Community option not available.')); + if (!$this->timeline->isCommunity(self::$selectedTab)) { + throw new HTTPException\BadRequestException($this->l10n->t('Community option not available.')); } // Check if we are allowed to display the content to visitors - if (!DI::userSession()->isAuthenticated()) { + if (!$this->session->isAuthenticated()) { $available = self::$page_style == self::LOCAL_AND_GLOBAL; if (!$available) { - $available = (self::$page_style == self::LOCAL) && (self::$content == 'local'); + $available = (self::$page_style == self::LOCAL) && (self::$selectedTab == TimelineEntity::LOCAL); } if (!$available) { - $available = (self::$page_style == self::GLOBAL) && (self::$content == 'global'); + $available = (self::$page_style == self::GLOBAL) && (self::$selectedTab == TimelineEntity::GLOBAL); } if (!$available) { - throw new HTTPException\ForbiddenException(DI::l10n()->t('Not available.')); + throw new HTTPException\ForbiddenException($this->l10n->t('Not available.')); } } - if (DI::mode()->isMobile()) { - self::$itemsPerPage = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'itemspage_mobile_network', - DI::config()->get('system', 'itemspage_network_mobile')); - } else { - self::$itemsPerPage = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'itemspage_network', - DI::config()->get('system', 'itemspage_network')); - } - - if (!empty($_GET['item'])) { - $item = Post::selectFirst(['parent'], ['id' => $_GET['item']]); + if (!empty($request['item'])) { + $item = Post::selectFirst(['parent'], ['id' => $request['item']]); self::$item_id = $item['parent'] ?? 0; } else { self::$item_id = 0; } - self::$min_id = $_GET['min_id'] ?? null; - self::$max_id = $_GET['last_commented'] ?? $_GET['max_id'] ?? null; - } - - /** - * Computes the displayed items. - * - * Community pages have a restriction on how many successive posts by the same author can show on any given page, - * so we may have to retrieve more content beyond the first query - * - * @return array - * @throws \Exception - */ - protected static function getItems() - { - $items = self::selectItems(self::$min_id, self::$max_id, self::$item_id, self::$itemsPerPage); - - $maxpostperauthor = (int) DI::config()->get('system', 'max_author_posts_community_page'); - if ($maxpostperauthor != 0 && self::$content == 'local') { - $count = 1; - $previousauthor = ''; - $numposts = 0; - $selected_items = []; - - while (count($selected_items) < self::$itemsPerPage && ++$count < 50 && count($items) > 0) { - foreach ($items as $item) { - if ($previousauthor == $item["author-link"]) { - ++$numposts; - } else { - $numposts = 0; - } - $previousauthor = $item["author-link"]; - - if (($numposts < $maxpostperauthor) && (count($selected_items) < self::$itemsPerPage)) { - $selected_items[] = $item; - } - } - - // If we're looking at a "previous page", the lookup continues forward in time because the list is - // sorted in chronologically decreasing order - if (isset(self::$min_id)) { - self::$min_id = $items[0]['commented']; - } else { - // In any other case, the lookup continues backwards in time - self::$max_id = $items[count($items) - 1]['commented']; - } - - $items = self::selectItems(self::$min_id, self::$max_id, self::$item_id, self::$itemsPerPage); - } - } else { - $selected_items = $items; - } - - return $selected_items; - } - - /** - * Database query for the community page - * - * @param $min_id - * @param $max_id - * @param $itemspage - * @return array - * @throws \Exception - * @TODO Move to repository/factory - */ - private static function selectItems($min_id, $max_id, $item_id, $itemspage) - { - if (self::$content == 'local') { - if (!is_null(self::$accountType)) { - $condition = ["`wall` AND `origin` AND `private` = ? AND `owner-contact-type` = ?", Item::PUBLIC, self::$accountType]; - } else { - $condition = ["`wall` AND `origin` AND `private` = ?", Item::PUBLIC]; - } - } elseif (self::$content == 'global') { - if (!is_null(self::$accountType)) { - $condition = ["`uid` = ? AND `private` = ? AND `owner-contact-type` = ?", 0, Item::PUBLIC, self::$accountType]; - } else { - $condition = ["`uid` = ? AND `private` = ?", 0, Item::PUBLIC]; - } - } else { - return []; - } - - $params = ['order' => ['commented' => true], 'limit' => $itemspage]; - - if (!empty($item_id)) { - $condition[0] .= " AND `id` = ?"; - $condition[] = $item_id; - } else { - if (DI::userSession()->getLocalUserId() && !empty($_REQUEST['no_sharer'])) { - $condition[0] .= " AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `post-user`.`uid` = ? AND `post-user`.`uri-id` = `post-thread-user-view`.`uri-id`)"; - $condition[] = DI::userSession()->getLocalUserId(); - } - - if (isset($max_id)) { - $condition[0] .= " AND `commented` < ?"; - $condition[] = $max_id; - } - - if (isset($min_id)) { - $condition[0] .= " AND `commented` > ?"; - $condition[] = $min_id; - - // Previous page case: we want the items closest to min_id but for that we need to reverse the query order - if (!isset($max_id)) { - $params['order']['commented'] = false; - } - } - } - - $r = Post::selectThreadForUser(DI::userSession()->getLocalUserId() ?: 0, ['uri-id', 'commented', 'author-link'], $condition, $params); - - $items = Post::toArray($r); - if (empty($items)) { - return []; - } - - // Previous page case: once we get the relevant items closest to min_id, we need to restore the expected display order - if (empty($item_id) && isset($min_id) && !isset($max_id)) { - $items = array_reverse($items); - } - - return $items; + self::$max_id = $request['last_commented'] ?? self::$max_id; } } diff --git a/src/Module/Conversation/Network.php b/src/Module/Conversation/Network.php index ed53f7d23..b5b8a3d94 100644 --- a/src/Module/Conversation/Network.php +++ b/src/Module/Conversation/Network.php @@ -21,52 +21,53 @@ namespace Friendica\Module\Conversation; -use Friendica\BaseModule; +use Friendica\App; +use Friendica\App\Mode; use Friendica\Content\BoundariesPager; use Friendica\Content\Conversation; +use Friendica\Content\Conversation\Entity\Timeline as TimelineEntity; +use Friendica\Content\Conversation\Factory\Timeline as TimelineFactory; +use Friendica\Content\Feature; use Friendica\Content\GroupManager; use Friendica\Content\Nav; use Friendica\Content\Widget; use Friendica\Content\Text\HTML; +use Friendica\Content\Widget\TrendingTags; use Friendica\Core\ACL; +use Friendica\Core\Cache\Capability\ICanCache; +use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Hook; +use Friendica\Core\L10n; use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; use Friendica\Core\Renderer; use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Database\DBA; +use Friendica\Database\Database; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Circle; use Friendica\Model\Item; use Friendica\Model\Post; use Friendica\Model\Profile; -use Friendica\Model\User; use Friendica\Model\Verb; use Friendica\Module\Contact as ModuleContact; +use Friendica\Module\Response; use Friendica\Module\Security\Login; -use Friendica\Protocol\Activity; +use Friendica\Network\HTTPException; +use Friendica\Navigation\SystemMessages; use Friendica\Util\DateTimeFormat; +use Friendica\Util\Profiler; +use Friendica\Protocol\Activity; +use Psr\Log\LoggerInterface; -class Network extends BaseModule +class Network extends Timeline { /** @var int */ private static $circleId; /** @var int */ private static $groupContactId; /** @var string */ - private static $selectedTab; - /** @var mixed */ - private static $min_id; - /** @var mixed */ - private static $max_id; - /** @var string */ - private static $accountTypeString; - /** @var int */ - private static $accountType; - /** @var string */ private static $network; - /** @var int */ - private static $itemsPerPage; /** @var string */ private static $dateFrom; /** @var string */ @@ -78,44 +79,94 @@ class Network extends BaseModule /** @var string */ protected static $order; + /** @var ICanCache */ + protected $cache; + /** @var IManageConfigValues The config */ + protected $config; + /** @var SystemMessages */ + protected $systemMessages; + /** @var App\Page */ + protected $page; + /** @var Conversation */ + protected $conversation; + /** @var IManagePersonalConfigValues */ + protected $pConfig; + /** @var Database */ + protected $database; + /** @var TimelineFactory */ + protected $timeline; + + public function __construct(TimelineFactory $timeline, SystemMessages $systemMessages, Mode $mode, Conversation $conversation, App\Page $page, IHandleUserSessions $session, Database $database, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, ICanCache $cache, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) + { + parent::__construct($mode, $session, $database, $pConfig, $config, $cache, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); + + $this->timeline = $timeline; + $this->systemMessages = $systemMessages; + $this->conversation = $conversation; + $this->page = $page; + } + protected function content(array $request = []): string { - if (!DI::userSession()->getLocalUserId()) { + if (!$this->session->getLocalUserId()) { return Login::form(); } - $this->parseRequest($_GET); + $this->parseRequest($request); $module = 'network'; - DI::page()['aside'] .= Widget::accountTypes($module, self::$accountTypeString); - DI::page()['aside'] .= Circle::sidebarWidget($module, $module . '/circle', 'standard', self::$circleId); - DI::page()['aside'] .= GroupManager::widget($module . '/group', DI::userSession()->getLocalUserId(), self::$groupContactId); - DI::page()['aside'] .= Widget::postedByYear($module . '/archive', DI::userSession()->getLocalUserId(), false); - DI::page()['aside'] .= Widget::networks($module, !self::$groupContactId ? self::$network : ''); - DI::page()['aside'] .= Widget\SavedSearches::getHTML(DI::args()->getQueryString()); - DI::page()['aside'] .= Widget::fileAs('filed', ''); + $this->page['aside'] .= Widget::accountTypes($module, self::$accountTypeString); - $arr = ['query' => DI::args()->getQueryString()]; + $arr = ['query' => $this->args->getQueryString()]; Hook::callAll('network_content_init', $arr); $o = ''; - // Fetch a page full of parent items for this page - $params = ['limit' => self::$itemsPerPage]; - $table = 'network-thread-view'; + if ($this->timeline->isChannel(self::$selectedTab)) { + if (!in_array(self::$selectedTab, [TimelineEntity::FOLLOWERS, TimelineEntity::FORYOU]) && $this->config->get('system', 'community_no_sharer')) { + $this->page['aside'] .= $this->getNoSharerWidget($module); + } - $items = self::getItems($table, $params); + if (Feature::isEnabled($this->session->getLocalUserId(), 'trending_tags')) { + $this->page['aside'] .= TrendingTags::getHTML(self::$selectedTab); + } - if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'infinite_scroll') && ($_GET['mode'] ?? '') != 'minimal') { - $tpl = Renderer::getMarkupTemplate('infinite_scroll_head.tpl'); - $o .= Renderer::replaceMacros($tpl, ['$reload_uri' => DI::args()->getQueryString()]); + $items = $this->getChannelItems(); + } elseif ($this->timeline->isCommunity(self::$selectedTab)) { + if ($this->session->getLocalUserId() && $this->config->get('system', 'community_no_sharer')) { + $this->page['aside'] .= $this->getNoSharerWidget($module); + } + + if (Feature::isEnabled($this->session->getLocalUserId(), 'trending_tags')) { + $this->page['aside'] .= TrendingTags::getHTML(self::$selectedTab); + } + + $items = $this->getCommunityItems(); + } else { + $this->page['aside'] .= Circle::sidebarWidget($module, $module . '/circle', 'standard', self::$circleId); + $this->page['aside'] .= GroupManager::widget($module . '/group', $this->session->getLocalUserId(), self::$groupContactId); + $this->page['aside'] .= Widget::postedByYear($module . '/archive', $this->session->getLocalUserId(), false); + $this->page['aside'] .= Widget::networks($module, !self::$groupContactId ? self::$network : ''); + $this->page['aside'] .= Widget\SavedSearches::getHTML($this->args->getQueryString()); + $this->page['aside'] .= Widget::fileAs('filed', ''); + + // Fetch a page full of parent items for this page + $params = ['limit' => self::$itemsPerPage]; + $table = 'network-thread-view'; + + $items = $this->getItems($table, $params); } - if (!(isset($_GET['mode']) AND ($_GET['mode'] == 'raw'))) { - $o .= self::getTabsHTML(self::$selectedTab); + if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'infinite_scroll') && ($_GET['mode'] ?? '') != 'minimal') { + $tpl = Renderer::getMarkupTemplate('infinite_scroll_head.tpl'); + $o .= Renderer::replaceMacros($tpl, ['$reload_uri' => $this->args->getQueryString()]); + } - Nav::setSelected(DI::args()->get(0)); + if (!(isset($_GET['mode']) and ($_GET['mode'] == 'raw'))) { + $o .= $this->getTabsHTML(); + + Nav::setSelected($this->args->get(0)); $content = ''; @@ -128,8 +179,6 @@ class Network extends BaseModule } } - $a = DI::app(); - $default_permissions = []; if (self::$circleId) { $default_permissions['allow_gid'] = [self::$circleId]; @@ -140,7 +189,7 @@ class Network extends BaseModule $allowedCids[] = (int) self::$groupContactId; } elseif (self::$network) { $condition = [ - 'uid' => DI::userSession()->getLocalUserId(), + 'uid' => $this->session->getLocalUserId(), 'network' => self::$network, 'self' => false, 'blocked' => false, @@ -160,35 +209,35 @@ class Network extends BaseModule } $x = [ - 'lockstate' => self::$circleId || self::$groupContactId || self::$network || ACL::getLockstateForUserId($a->getLoggedInUserId()) ? 'lock' : 'unlock', - 'acl' => ACL::getFullSelectorHTML(DI::page(), $a->getLoggedInUserId(), true, $default_permissions), + 'lockstate' => self::$circleId || self::$groupContactId || self::$network || ACL::getLockstateForUserId($this->session->getLocalUserId()) ? 'lock' : 'unlock', + 'acl' => ACL::getFullSelectorHTML($this->page, $this->session->getLocalUserId(), true, $default_permissions), 'bang' => ((self::$circleId || self::$groupContactId || self::$network) ? '!' : ''), 'content' => $content, ]; - $o .= DI::conversation()->statusEditor($x); + $o .= $this->conversation->statusEditor($x); } if (self::$circleId) { - $circle = DBA::selectFirst('group', ['name'], ['id' => self::$circleId, 'uid' => DI::userSession()->getLocalUserId()]); + $circle = DBA::selectFirst('group', ['name'], ['id' => self::$circleId, 'uid' => $this->session->getLocalUserId()]); if (!DBA::isResult($circle)) { - DI::sysmsg()->addNotice(DI::l10n()->t('No such circle')); + $this->systemMessages->addNotice($this->l10n->t('No such circle')); } $o = Renderer::replaceMacros(Renderer::getMarkupTemplate('section_title.tpl'), [ - '$title' => DI::l10n()->t('Circle: %s', $circle['name']) + '$title' => $this->l10n->t('Circle: %s', $circle['name']) ]) . $o; } elseif (self::$groupContactId) { $contact = Contact::getById(self::$groupContactId); if (DBA::isResult($contact)) { $o = Renderer::replaceMacros(Renderer::getMarkupTemplate('contact/list.tpl'), [ 'contacts' => [ModuleContact::getContactTemplateVars($contact)], - 'id' => DI::args()->get(0), + 'id' => $this->args->get(0), ]) . $o; } else { - DI::sysmsg()->addNotice(DI::l10n()->t('Invalid contact.')); + $this->systemMessages->addNotice($this->l10n->t('Invalid contact.')); } - } elseif (!DI::config()->get('theme', 'hide_eventlist')) { + } elseif (!$this->config->get('theme', 'hide_eventlist')) { $o .= Profile::getBirthdays(); $o .= Profile::getEventsReminderHTML(); } @@ -201,14 +250,14 @@ class Network extends BaseModule $ordering = '`commented`'; } - $o .= DI::conversation()->render($items, Conversation::MODE_NETWORK, false, false, $ordering, DI::userSession()->getLocalUserId()); + $o .= $this->conversation->render($items, Conversation::MODE_NETWORK, false, false, $ordering, $this->session->getLocalUserId()); - if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'infinite_scroll')) { + if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'infinite_scroll')) { $o .= HTML::scrollLoader(); } else { $pager = new BoundariesPager( - DI::l10n(), - DI::args()->getQueryString(), + $this->l10n, + $this->args->getQueryString(), $items[0][self::$order] ?? null, $items[count($items) - 1][self::$order] ?? null, self::$itemsPerPage @@ -243,57 +292,13 @@ class Network extends BaseModule /** * Get the network tabs menu * - * @param string $selectedTab * @return string Html of the network tabs * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function getTabsHTML(string $selectedTab) + private function getTabsHTML() { - $cmd = DI::args()->getCommand(); - - // tabs - $tabs = [ - [ - 'label' => DI::l10n()->t('Latest Activity'), - 'url' => $cmd . '?' . http_build_query(['order' => 'commented']), - 'sel' => !$selectedTab || $selectedTab == 'commented' ? 'active' : '', - 'title' => DI::l10n()->t('Sort by latest activity'), - 'id' => 'activity-order-tab', - 'accesskey' => 'e', - ], - [ - 'label' => DI::l10n()->t('Latest Posts'), - 'url' => $cmd . '?' . http_build_query(['order' => 'received']), - 'sel' => $selectedTab == 'received' ? 'active' : '', - 'title' => DI::l10n()->t('Sort by post received date'), - 'id' => 'post-order-tab', - 'accesskey' => 't', - ], - [ - 'label' => DI::l10n()->t('Latest Creation'), - 'url' => $cmd . '?' . http_build_query(['order' => 'created']), - 'sel' => $selectedTab == 'created' ? 'active' : '', - 'title' => DI::l10n()->t('Sort by post creation date'), - 'id' => 'creation-order-tab', - 'accesskey' => 'q', - ], - [ - 'label' => DI::l10n()->t('Personal'), - 'url' => $cmd . '?' . http_build_query(['mention' => true]), - 'sel' => $selectedTab == 'mention' ? 'active' : '', - 'title' => DI::l10n()->t('Posts that mention or involve you'), - 'id' => 'personal-tab', - 'accesskey' => 'r', - ], - [ - 'label' => DI::l10n()->t('Starred'), - 'url' => $cmd . '?' . http_build_query(['star' => true]), - 'sel' => $selectedTab == 'star' ? 'active' : '', - 'title' => DI::l10n()->t('Favourite Posts'), - 'id' => 'starred-posts-tab', - 'accesskey' => 'm', - ], - ]; + // @todo user confgurable selection of tabs + $tabs = $this->getTabArray($this->timeline->getNetworkFeeds($this->args->getCommand()), 'network'); $arr = ['tabs' => $tabs]; Hook::callAll('network_tabs', $arr); @@ -303,36 +308,43 @@ class Network extends BaseModule return Renderer::replaceMacros($tpl, ['$tabs' => $arr['tabs']]); } - protected function parseRequest(array $get) + protected function parseRequest(array $request) { + parent::parseRequest($request); + self::$circleId = (int)($this->parameters['circle_id'] ?? 0); self::$groupContactId = (int)($this->parameters['contact_id'] ?? 0); - self::$selectedTab = self::getTimelineOrderBySession(DI::userSession(), DI::pConfig()); + if (!self::$selectedTab) { + self::$selectedTab = self::getTimelineOrderBySession(DI::userSession(), $this->pConfig); + } elseif (!$this->timeline->isChannel(self::$selectedTab) && !$this->timeline->isCommunity(self::$selectedTab)) { + throw new HTTPException\BadRequestException($this->l10n->t('Network feed not available.')); + } - if (!empty($get['star'])) { - self::$selectedTab = 'star'; + + if (!empty($request['star'])) { + self::$selectedTab = TimelineEntity::STAR; self::$star = true; } else { - self::$star = self::$selectedTab == 'star'; + self::$star = self::$selectedTab == TimelineEntity::STAR; } - if (!empty($get['mention'])) { - self::$selectedTab = 'mention'; + if (!empty($request['mention'])) { + self::$selectedTab = TimelineEntity::MENTION; self::$mention = true; } else { - self::$mention = self::$selectedTab == 'mention'; + self::$mention = self::$selectedTab == TimelineEntity::MENTION; } - if (!empty($get['order'])) { - self::$selectedTab = $get['order']; - self::$order = $get['order']; + if (!empty($request['order'])) { + self::$selectedTab = $request['order']; + self::$order = $request['order']; self::$star = false; self::$mention = false; - } elseif (in_array(self::$selectedTab, ['received', 'star'])) { + } elseif (in_array(self::$selectedTab, [TimelineEntity::RECEIVED, TimelineEntity::STAR])) { self::$order = 'received'; - } elseif (self::$selectedTab == 'created') { + } elseif ((self::$selectedTab == TimelineEntity::CREATED) || $this->timeline->isChannel(self::$selectedTab)) { self::$order = 'created'; } else { self::$order = 'commented'; @@ -341,53 +353,39 @@ class Network extends BaseModule self::$selectedTab = self::$selectedTab ?? self::$order; // Prohibit combined usage of "star" and "mention" - if (self::$selectedTab == 'star') { + if (self::$selectedTab == TimelineEntity::STAR) { self::$mention = false; - } elseif (self::$selectedTab == 'mention') { + } elseif (self::$selectedTab == TimelineEntity::MENTION) { self::$star = false; } - DI::session()->set('network-tab', self::$selectedTab); - DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'network.view', 'selected_tab', self::$selectedTab); + $this->session->set('network-tab', self::$selectedTab); + $this->pConfig->set($this->session->getLocalUserId(), 'network.view', 'selected_tab', self::$selectedTab); - self::$accountTypeString = $get['accounttype'] ?? $this->parameters['accounttype'] ?? ''; - self::$accountType = User::getAccountTypeByString(self::$accountTypeString); - - self::$network = $get['nets'] ?? ''; + self::$network = $request['nets'] ?? ''; self::$dateFrom = $this->parameters['from'] ?? ''; self::$dateTo = $this->parameters['to'] ?? ''; - if (DI::mode()->isMobile()) { - self::$itemsPerPage = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'itemspage_mobile_network', - DI::config()->get('system', 'itemspage_network_mobile')); - } else { - self::$itemsPerPage = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'itemspage_network', - DI::config()->get('system', 'itemspage_network')); - } - - self::$min_id = $get['min_id'] ?? null; - self::$max_id = $get['max_id'] ?? null; - switch (self::$order) { case 'received': - self::$max_id = $get['last_received'] ?? self::$max_id; + self::$max_id = $request['last_received'] ?? self::$max_id; break; case 'created': - self::$max_id = $get['last_created'] ?? self::$max_id; + self::$max_id = $request['last_created'] ?? self::$max_id; break; case 'uriid': - self::$max_id = $get['last_uriid'] ?? self::$max_id; + self::$max_id = $request['last_uriid'] ?? self::$max_id; break; default: self::$order = 'commented'; - self::$max_id = $get['last_commented'] ?? self::$max_id; + self::$max_id = $request['last_commented'] ?? self::$max_id; } } - protected static function getItems(string $table, array $params, array $conditionFields = []) + protected function getItems(string $table, array $params, array $conditionFields = []) { - $conditionFields['uid'] = DI::userSession()->getLocalUserId(); + $conditionFields['uid'] = $this->session->getLocalUserId(); $conditionStrings = []; if (!is_null(self::$accountType)) { @@ -416,7 +414,7 @@ class Network extends BaseModule } elseif (self::$groupContactId) { $conditionStrings = DBA::mergeConditions($conditionStrings, ["((`contact-id` = ?) OR `uri-id` IN (SELECT `parent-uri-id` FROM `post-user-view` WHERE (`contact-id` = ? AND `gravity` = ? AND `vid` = ? AND `uid` = ?)))", - self::$groupContactId, self::$groupContactId, Item::GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE), DI::userSession()->getLocalUserId()]); + self::$groupContactId, self::$groupContactId, Item::GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE), $this->session->getLocalUserId()]); } // Currently only the order modes "received" and "commented" are in use @@ -480,10 +478,10 @@ class Network extends BaseModule // level which items you've seen and which you haven't. If you're looking // at the top level network page just mark everything seen. if (!self::$circleId && !self::$groupContactId && !self::$star && !self::$mention) { - $condition = ['unseen' => true, 'uid' => DI::userSession()->getLocalUserId()]; + $condition = ['unseen' => true, 'uid' => $this->session->getLocalUserId()]; self::setItemsSeenByCondition($condition); } elseif (!empty($parents)) { - $condition = ['unseen' => true, 'uid' => DI::userSession()->getLocalUserId(), 'parent-uri-id' => $parents]; + $condition = ['unseen' => true, 'uid' => $this->session->getLocalUserId(), 'parent-uri-id' => $parents]; self::setItemsSeenByCondition($condition); } diff --git a/src/Module/Conversation/Timeline.php b/src/Module/Conversation/Timeline.php new file mode 100644 index 000000000..8faa1c48a --- /dev/null +++ b/src/Module/Conversation/Timeline.php @@ -0,0 +1,474 @@ +. + * + */ + +namespace Friendica\Module\Conversation; + +use Friendica\App; +use Friendica\App\Mode; +use Friendica\BaseModule; +use Friendica\Content\Conversation\Collection\Timelines; +use Friendica\Content\Conversation\Entity\Timeline as TimelineEntity; +use Friendica\Core\Cache\Capability\ICanCache; +use Friendica\Core\Cache\Enum\Duration; +use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Core\L10n; +use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; +use Friendica\Core\Renderer; +use Friendica\Core\Session\Capability\IHandleUserSessions; +use Friendica\Model\Contact; +use Friendica\Model\User; +use Friendica\Database\Database; +use Friendica\Model\Item; +use Friendica\Model\Post; +use Friendica\Module\Response; +use Friendica\Util\DateTimeFormat; +use Friendica\Util\Profiler; +use Psr\Log\LoggerInterface; + +class Timeline extends BaseModule +{ + /** @var string */ + protected static $selectedTab; + /** @var mixed */ + protected static $min_id; + /** @var mixed */ + protected static $max_id; + /** @var string */ + protected static $accountTypeString; + /** @var int */ + protected static $accountType; + /** @var int */ + protected static $item_id; + /** @var int */ + protected static $itemsPerPage; + /** @var bool */ + protected static $no_sharer; + + /** @var App\Mode $mode */ + protected $mode; + /** @var UserSession */ + protected $session; + /** @var Database */ + protected $database; + /** @var IManagePersonalConfigValues */ + protected $pConfig; + /** @var IManageConfigValues The config */ + protected $config; + /** @var ICanCache */ + protected $cache; + + public function __construct(Mode $mode, IHandleUserSessions $session, Database $database, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, ICanCache $cache, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) + { + parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); + + $this->mode = $mode; + $this->session = $session; + $this->database = $database; + $this->pConfig = $pConfig; + $this->config = $config; + $this->cache = $cache; + } + + /** + * Computes module parameters from the request and local configuration + * + * @throws HTTPException\BadRequestException + * @throws HTTPException\ForbiddenException + */ + protected function parseRequest(array $request) + { + self::$selectedTab = $this->parameters['content'] ?? ''; + + self::$accountTypeString = $request['accounttype'] ?? $this->parameters['accounttype'] ?? ''; + self::$accountType = User::getAccountTypeByString(self::$accountTypeString); + + if ($this->mode->isMobile()) { + self::$itemsPerPage = $this->pConfig->get( + $this->session->getLocalUserId(), + 'system', + 'itemspage_mobile_network', + $this->config->get('system', 'itemspage_network_mobile') + ); + } else { + self::$itemsPerPage = $this->pConfig->get( + $this->session->getLocalUserId(), + 'system', + 'itemspage_network', + $this->config->get('system', 'itemspage_network') + ); + } + + self::$min_id = $request['min_id'] ?? null; + self::$max_id = $request['max_id'] ?? null; + + self::$no_sharer = !empty($request['no_sharer']); + } + + protected function getNoSharerWidget(string $base): string + { + $path = self::$selectedTab; + if (!empty(self::$accountTypeString)) { + $path .= '/' . self::$accountTypeString; + } + $query_parameters = []; + + if (!empty(self::$min_id)) { + $query_parameters['min_id'] = self::$min_id; + } + if (!empty(self::$max_id)) { + $query_parameters['max_id'] = self::$max_id; + } + + $path_all = $path . (!empty($query_parameters) ? '?' . http_build_query($query_parameters) : ''); + $path_no_sharer = $path . '?' . http_build_query(array_merge($query_parameters, ['no_sharer' => true])); + return Renderer::replaceMacros(Renderer::getMarkupTemplate('widget/community_sharer.tpl'), [ + '$title' => $this->l10n->t('Own Contacts'), + '$path_all' => $path_all, + '$path_no_sharer' => $path_no_sharer, + '$no_sharer' => self::$no_sharer, + '$all' => $this->l10n->t('Include'), + '$no_sharer_label' => $this->l10n->t('Hide'), + '$base' => $base, + ]); + } + + protected function getTabArray(Timelines $timelines, string $prefix): array + { + $tabs = []; + + foreach ($timelines as $tab) { + $tabs[] = [ + 'label' => $tab->label, + 'url' => $tab->path ?? $prefix . '/' . $tab->code, + 'sel' => self::$selectedTab == $tab->code ? 'active' : '', + 'title' => $tab->description, + 'id' => $prefix . '-' . $tab->code . '-tab', + 'accesskey' => $tab->accessKey, + ]; + } + return $tabs; + } + + /** + * Database query for the channel page + * + * @return array + * @throws \Exception + */ + protected function getChannelItems() + { + $uid = $this->session->getLocalUserId(); + + if (self::$selectedTab == TimelineEntity::WHATSHOT) { + if (!is_null(self::$accountType)) { + $condition = ["(`comments` >= ? OR `activities` >= ?) AND `contact-type` = ?", $this->getMedianComments($uid, 4), $this->getMedianActivities($uid, 4), self::$accountType]; + } else { + $condition = ["(`comments` >= ? OR `activities` >= ?) AND `contact-type` != ?", $this->getMedianComments($uid, 4), $this->getMedianActivities($uid, 4), Contact::TYPE_COMMUNITY]; + } + } elseif (self::$selectedTab == TimelineEntity::FORYOU) { + $cid = Contact::getPublicIdByUserId($uid); + $condition = [ + "(`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `relation-thread-score` > ?) OR + ((`comments` >= ? OR `activities` >= ?) AND `owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ?)) OR + (`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` IN (?, ?) AND `notify_new_posts`)))", + $cid, $this->getMedianRelationThreadScore($cid, 4), $this->getMedianComments($uid, 4), $this->getMedianActivities($uid, 4), $cid, + $uid, Contact::FRIEND, Contact::SHARING + ]; + } elseif (self::$selectedTab == TimelineEntity::FOLLOWERS) { + $condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` = ?)", $uid, Contact::FOLLOWER]; + } elseif (self::$selectedTab == TimelineEntity::SHARERSOFSHARERS) { + $cid = Contact::getPublicIdByUserId($uid); + + // @todo Suggest posts from contacts that are followed most by our followers + $condition = [ + "`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `last-interaction` > ? + AND `relation-cid` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ? AND `relation-thread-score` >= ?) + AND NOT `cid` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ?))", + DateTimeFormat::utc('now - ' . $this->config->get('channel', 'sharer_interaction_days') . ' day'), $cid, $this->getMedianRelationThreadScore($cid, 4), $cid + ]; + } elseif (self::$selectedTab == TimelineEntity::IMAGE) { + $condition = ["`media-type` & ?", 1]; + } elseif (self::$selectedTab == TimelineEntity::VIDEO) { + $condition = ["`media-type` & ?", 2]; + } elseif (self::$selectedTab == TimelineEntity::AUDIO) { + $condition = ["`media-type` & ?", 4]; + } elseif (self::$selectedTab == TimelineEntity::LANGUAGE) { + $condition = ["JSON_EXTRACT(JSON_KEYS(language), '$[0]') = ?", $this->l10n->convertCodeForLanguageDetection(User::getLanguageCode($uid))]; + } + + if (self::$selectedTab != TimelineEntity::LANGUAGE) { + $condition = $this->addLanguageCondition($uid, $condition); + } + + $condition[0] .= " AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `post-engagement`.`owner-id` AND (`ignored` OR `blocked` OR `collapsed`))"; + $condition[] = $uid; + + if ((self::$selectedTab != TimelineEntity::WHATSHOT) && !is_null(self::$accountType)) { + $condition[0] .= " AND `contact-type` = ?"; + $condition[] = self::$accountType; + } + + $params = ['order' => ['created' => true], 'limit' => self::$itemsPerPage]; + + if (!empty(self::$item_id)) { + $condition[0] .= " AND `uri-id` = ?"; + $condition[] = self::$item_id; + } else { + if (self::$no_sharer) { + $condition[0] .= " AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `post-user`.`uid` = ? AND `post-user`.`uri-id` = `post-engagement`.`uri-id`)"; + $condition[] = $uid; + } + + if (isset(self::$max_id)) { + $condition[0] .= " AND `created` < ?"; + $condition[] = self::$max_id; + } + + if (isset(self::$min_id)) { + $condition[0] .= " AND `created` > ?"; + $condition[] = self::$min_id; + + // Previous page case: we want the items closest to min_id but for that we need to reverse the query order + if (!isset(self::$max_id)) { + $params['order']['created'] = false; + } + } + } + + $items = $this->database->selectToArray('post-engagement', ['uri-id', 'created'], $condition, $params); + if (empty($items)) { + return []; + } + + // Previous page case: once we get the relevant items closest to min_id, we need to restore the expected display order + if (empty(self::$item_id) && isset(self::$min_id) && !isset(self::$max_id)) { + $items = array_reverse($items); + } + + Item::update(['unseen' => false], ['unseen' => true, 'uid' => $uid, 'uri-id' => array_column($items, 'uri-id')]); + + return $items; + } + + private function addLanguageCondition(int $uid, array $condition): array + { + $conditions = []; + $languages = $this->pConfig->get($uid, 'channel', 'languages', [User::getLanguageCode($uid)]); + $languages = $this->l10n->convertForLanguageDetection($languages); + foreach ($languages as $language) { + $conditions[] = "JSON_EXTRACT(JSON_KEYS(language), '$[0]') = ?"; + $condition[] = $language; + } + if (!empty($conditions)) { + $condition[0] .= " AND (`language` IS NULL OR " . implode(' OR ', $conditions) . ")"; + } + return $condition; + } + + private function getMedianComments(int $uid, int $divider): int + { + $languages = $this->pConfig->get($uid, 'channel', 'languages', [User::getLanguageCode($uid)]); + $cache_key = 'Channel:getMedianComments:' . $divider . ':' . implode(':', $languages); + $comments = $this->cache->get($cache_key); + if (!empty($comments)) { + return $comments; + } + + $condition = ["`contact-type` != ? AND `comments` > ?", Contact::TYPE_COMMUNITY, 0]; + $condition = $this->addLanguageCondition($uid, $condition); + + $limit = $this->database->count('post-engagement', $condition) / $divider; + $post = $this->database->selectToArray('post-engagement', ['comments'], $condition, ['order' => ['comments' => true], 'limit' => [$limit, 1]]); + $comments = $post[0]['comments'] ?? 0; + if (empty($comments)) { + return 0; + } + + $this->cache->set($cache_key, $comments, Duration::HALF_HOUR); + $this->logger->debug('Calculated median comments', ['divider' => $divider, 'languages' => $languages, 'median' => $comments]); + return $comments; + } + + private function getMedianActivities(int $uid, int $divider): int + { + $languages = $this->pConfig->get($uid, 'channel', 'languages', [User::getLanguageCode($uid)]); + $cache_key = 'Channel:getMedianActivities:' . $divider . ':' . implode(':', $languages); + $activities = $this->cache->get($cache_key); + if (!empty($activities)) { + return $activities; + } + + $condition = ["`contact-type` != ? AND `activities` > ?", Contact::TYPE_COMMUNITY, 0]; + $condition = $this->addLanguageCondition($uid, $condition); + + $limit = $this->database->count('post-engagement', $condition) / $divider; + $post = $this->database->selectToArray('post-engagement', ['activities'], $condition, ['order' => ['activities' => true], 'limit' => [$limit, 1]]); + $activities = $post[0]['activities'] ?? 0; + if (empty($activities)) { + return 0; + } + + $this->cache->set($cache_key, $activities, Duration::HALF_HOUR); + $this->logger->debug('Calculated median activities', ['divider' => $divider, 'languages' => $languages, 'median' => $activities]); + return $activities; + } + + private function getMedianRelationThreadScore(int $cid, int $divider): int + { + $cache_key = 'Channel:getThreadScore:' . $cid . ':' . $divider; + $score = $this->cache->get($cache_key); + if (!empty($score)) { + return $score; + } + + $condition = ["`relation-cid` = ? AND `relation-thread-score` > ?", $cid, 0]; + + $limit = $this->database->count('contact-relation', $condition) / $divider; + $relation = $this->database->selectToArray('contact-relation', ['relation-thread-score'], $condition, ['order' => ['relation-thread-score' => true], 'limit' => [$limit, 1]]); + $score = $relation[0]['relation-thread-score'] ?? 0; + if (empty($score)) { + return 0; + } + + $this->cache->set($cache_key, $score, Duration::HALF_HOUR); + $this->logger->debug('Calculated median score', ['cid' => $cid, 'divider' => $divider, 'median' => $score]); + return $score; + } + + /** + * Computes the displayed items. + * + * Community pages have a restriction on how many successive posts by the same author can show on any given page, + * so we may have to retrieve more content beyond the first query + * + * @return array + * @throws \Exception + */ + protected function getCommunityItems() + { + $items = $this->selectItems(); + + $maxpostperauthor = (int) $this->config->get('system', 'max_author_posts_community_page'); + if ($maxpostperauthor != 0 && self::$selectedTab == 'local') { + $count = 1; + $previousauthor = ''; + $numposts = 0; + $selected_items = []; + + while (count($selected_items) < self::$itemsPerPage && ++$count < 50 && count($items) > 0) { + foreach ($items as $item) { + if ($previousauthor == $item["author-link"]) { + ++$numposts; + } else { + $numposts = 0; + } + $previousauthor = $item["author-link"]; + + if (($numposts < $maxpostperauthor) && (count($selected_items) < self::$itemsPerPage)) { + $selected_items[] = $item; + } + } + + // If we're looking at a "previous page", the lookup continues forward in time because the list is + // sorted in chronologically decreasing order + if (isset(self::$min_id)) { + self::$min_id = $items[0]['commented']; + } else { + // In any other case, the lookup continues backwards in time + self::$max_id = $items[count($items) - 1]['commented']; + } + + $items = $this->selectItems(); + } + } else { + $selected_items = $items; + } + + return $selected_items; + } + + /** + * Database query for the community page + * + * @return array + * @throws \Exception + * @TODO Move to repository/factory + */ + private function selectItems() + { + if (self::$selectedTab == 'local') { + if (!is_null(self::$accountType)) { + $condition = ["`wall` AND `origin` AND `private` = ? AND `owner-contact-type` = ?", Item::PUBLIC, self::$accountType]; + } else { + $condition = ["`wall` AND `origin` AND `private` = ?", Item::PUBLIC]; + } + } elseif (self::$selectedTab == 'global') { + if (!is_null(self::$accountType)) { + $condition = ["`uid` = ? AND `private` = ? AND `owner-contact-type` = ?", 0, Item::PUBLIC, self::$accountType]; + } else { + $condition = ["`uid` = ? AND `private` = ?", 0, Item::PUBLIC]; + } + } else { + return []; + } + + $params = ['order' => ['commented' => true], 'limit' => self::$itemsPerPage]; + + if (!empty(self::$item_id)) { + $condition[0] .= " AND `id` = ?"; + $condition[] = self::$item_id; + } else { + if ($this->session->getLocalUserId() && self::$no_sharer) { + $condition[0] .= " AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `post-user`.`uid` = ? AND `post-user`.`uri-id` = `post-thread-user-view`.`uri-id`)"; + $condition[] = $this->session->getLocalUserId(); + } + + if (isset(self::$max_id)) { + $condition[0] .= " AND `commented` < ?"; + $condition[] = self::$max_id; + } + + if (isset(self::$min_id)) { + $condition[0] .= " AND `commented` > ?"; + $condition[] = self::$min_id; + + // Previous page case: we want the items closest to min_id but for that we need to reverse the query order + if (!isset(self::$max_id)) { + $params['order']['commented'] = false; + } + } + } + + $r = Post::selectThreadForUser($this->session->getLocalUserId() ?: 0, ['uri-id', 'commented', 'author-link'], $condition, $params); + + $items = Post::toArray($r); + if (empty($items)) { + return []; + } + + // Previous page case: once we get the relevant items closest to min_id, we need to restore the expected display order + if (empty(self::$item_id) && isset(self::$min_id) && !isset(self::$max_id)) { + $items = array_reverse($items); + } + + return $items; + } +} diff --git a/src/Module/Update/Channel.php b/src/Module/Update/Channel.php index 099611195..333c63b2d 100644 --- a/src/Module/Update/Channel.php +++ b/src/Module/Update/Channel.php @@ -38,7 +38,13 @@ class Channel extends ChannelModule $o = ''; if (!empty($request['force'])) { - $o = $this->conversation->render($this->getItems($request), Conversation::MODE_CHANNEL, true, false, 'created', $this->session->getLocalUserId()); + if ($this->timeline->isChannel(self::$selectedTab)) { + $items = $this->getChannelItems(); + } else { + $items = $this->getCommunityItems(); + } + + $o = $this->conversation->render($items, Conversation::MODE_CHANNEL, true, false, 'created', $this->session->getLocalUserId()); } System::htmlUpdateExit($o); diff --git a/src/Module/Update/Community.php b/src/Module/Update/Community.php index 7edb2949b..88467f252 100644 --- a/src/Module/Update/Community.php +++ b/src/Module/Update/Community.php @@ -36,11 +36,11 @@ class Community extends CommunityModule { protected function rawContent(array $request = []) { - $this->parseRequest(); + $this->parseRequest($request); $o = ''; if (!empty($request['force'])) { - $o = DI::conversation()->render(self::getItems(), Conversation::MODE_COMMUNITY, true, false, 'commented', DI::userSession()->getLocalUserId()); + $o = DI::conversation()->render($this->getCommunityItems(), Conversation::MODE_COMMUNITY, true, false, 'commented', DI::userSession()->getLocalUserId()); } System::htmlUpdateExit($o); diff --git a/src/Module/Update/Network.php b/src/Module/Update/Network.php index 7c9a07f36..bf33c6c05 100644 --- a/src/Module/Update/Network.php +++ b/src/Module/Update/Network.php @@ -23,7 +23,6 @@ namespace Friendica\Module\Update; use Friendica\Content\Conversation; use Friendica\Core\System; -use Friendica\DI; use Friendica\Model\Item; use Friendica\Model\Post; use Friendica\Module\Conversation\Network as NetworkModule; @@ -69,7 +68,7 @@ class Network extends NetworkModule $params = ['limit' => 100]; $table = 'network-item-view'; - $items = self::getItems($table, $params, $conditionFields); + $items = $this->getItems($table, $params, $conditionFields); if (self::$order === 'received') { $ordering = '`received`'; @@ -79,7 +78,7 @@ class Network extends NetworkModule $ordering = '`commented`'; } - $o = DI::conversation()->render($items, Conversation::MODE_NETWORK, $profile_uid, false, $ordering, DI::userSession()->getLocalUserId()); + $o = $this->conversation->render($items, Conversation::MODE_NETWORK, $profile_uid, false, $ordering, $this->session->getLocalUserId()); System::htmlUpdateExit($o); } diff --git a/static/routes.config.php b/static/routes.config.php index 55b40bcf3..6cd979d8e 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -672,7 +672,7 @@ return [ ], '/network' => [ - '[/]' => [Module\Conversation\Network::class, [R::GET]], + '[/{content}]' => [Module\Conversation\Network::class, [R::GET]], '/archive/{from:\d\d\d\d-\d\d-\d\d}[/{to:\d\d\d\d-\d\d-\d\d}]' => [Module\Conversation\Network::class, [R::GET]], '/group/{contact_id:\d+}' => [Module\Conversation\Network::class, [R::GET]], '/circle/{circle_id:\d+}' => [Module\Conversation\Network::class, [R::GET]],