use Friendica\Database\Database;
use Friendica\Module\Response;
use Friendica\Navigation\SystemMessages;
+use Friendica\Util\DateTimeFormat;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
class Channel extends Timeline
{
- /** @var TimelineFactory */
- protected $timeline;
+ const WHATSHOT = 'whatshot';
+ const FORYOU = 'foryou';
+ const FOLLOWERS = 'followers';
+ const SHARERSOFSHARERS = 'sharersofsharers';
+ const IMAGE = 'image';
+ const VIDEO = 'video';
+ const AUDIO = 'audio';
+ const LANGUAGE = 'language';
+
+ 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 Conversation */
protected $conversation;
/** @var App\Page */
$o .= Renderer::replaceMacros($tpl, ['$reload_uri' => $this->args->getQueryString()]);
}
- if (!$this->raw) {
- $tabs = $this->getTabArray($this->channel->getTimelines($this->session->getLocalUserId()), 'channel');
- $tabs = array_merge($tabs, $this->getTabArray($this->channelRepository->selectByUid($this->session->getLocalUserId()), 'channel'));
- $tabs = array_merge($tabs, $this->getTabArray($this->community->getTimelines(true), 'channel'));
+ if (empty($request['mode']) || ($request['mode'] != 'raw')) {
+ $tabs = [];
+
+ $tabs[] = [
+ 'label' => $this->l10n->t('For you'),
+ 'url' => 'channel/' . self::FORYOU,
+ 'sel' => self::$content == self::FORYOU ? 'active' : '',
+ 'title' => $this->l10n->t('Posts from contacts you interact with and who interact with you'),
+ 'id' => 'channel-foryou-tab',
+ 'accesskey' => 'y'
+ ];
+
+ $tabs[] = [
+ 'label' => $this->l10n->t('What\'s Hot'),
+ 'url' => 'channel/' . self::WHATSHOT,
+ 'sel' => self::$content == self::WHATSHOT ? 'active' : '',
+ 'title' => $this->l10n->t('Posts with a lot of interactions'),
+ 'id' => 'channel-whatshot-tab',
+ 'accesskey' => 'h'
+ ];
+
+ $language = User::getLanguageCode($this->session->getLocalUserId());
+ $languages = $this->l10n->getAvailableLanguages(true);
+
+ $tabs[] = [
+ 'label' => $languages[$language],
+ 'url' => 'channel/' . self::LANGUAGE,
+ 'sel' => self::$content == self::LANGUAGE ? 'active' : '',
+ 'title' => $this->l10n->t('Posts in %s', $languages[$language]),
+ 'id' => 'channel-language-tab',
+ 'accesskey' => 'g'
+ ];
+
+ $tabs[] = [
+ 'label' => $this->l10n->t('Followers'),
+ 'url' => 'channel/' . self::FOLLOWERS,
+ 'sel' => self::$content == self::FOLLOWERS ? 'active' : '',
+ 'title' => $this->l10n->t('Posts from your followers that you don\'t follow'),
+ 'id' => 'channel-followers-tab',
+ 'accesskey' => 'f'
+ ];
+
+ $tabs[] = [
+ 'label' => $this->l10n->t('Sharers of sharers'),
+ 'url' => 'channel/' . self::SHARERSOFSHARERS,
+ 'sel' => self::$content == self::SHARERSOFSHARERS ? 'active' : '',
+ 'title' => $this->l10n->t('Posts from accounts that are followed by accounts that you follow'),
+ 'id' => 'channel-' . self::SHARERSOFSHARERS . '-tab',
+ 'accesskey' => 'f'
+ ];
+
+ $tabs[] = [
+ 'label' => $this->l10n->t('Images'),
+ 'url' => 'channel/' . self::IMAGE,
+ 'sel' => self::$content == self::IMAGE ? 'active' : '',
+ 'title' => $this->l10n->t('Posts with images'),
+ 'id' => 'channel-image-tab',
+ 'accesskey' => 'i'
+ ];
+
+ $tabs[] = [
+ 'label' => $this->l10n->t('Audio'),
+ 'url' => 'channel/' . self::AUDIO,
+ 'sel' => self::$content == self::AUDIO ? 'active' : '',
+ 'title' => $this->l10n->t('Posts with audio'),
+ 'id' => 'channel-audio-tab',
+ 'accesskey' => 'd'
+ ];
+
+ $tabs[] = [
+ 'label' => $this->l10n->t('Videos'),
+ 'url' => 'channel/' . self::VIDEO,
+ 'sel' => self::$content == self::VIDEO ? 'active' : '',
+ 'title' => $this->l10n->t('Posts with videos'),
+ 'id' => 'channel-video-tab',
+ 'accesskey' => 'v'
+ ];
$tab_tpl = Renderer::getMarkupTemplate('common_tabs.tpl');
$o .= Renderer::replaceMacros($tab_tpl, ['$tabs' => $tabs]);
$this->selectedTab = ChannelEntity::FORYOU;
}
+<<<<<<< HEAD
if (!$this->channel->isTimeline($this->selectedTab) && !$this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId()) && !$this->community->isTimeline($this->selectedTab)) {
+=======
+ if (!in_array(self::$content, [self::WHATSHOT, self::FORYOU, self::FOLLOWERS, self::SHARERSOFSHARERS, self::IMAGE, self::VIDEO, self::AUDIO, self::LANGUAGE])) {
+>>>>>>> 0818b4d1b3 (Fixes the score calculation concerning the relation-cid / cid interaction)
throw new HTTPException\BadRequestException($this->l10n->t('Channel not available.'));
}
- $this->maxId = $request['last_created'] ?? $this->maxId;
- $this->minId = $request['first_created'] ?? $this->minId;
+ 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;
+ } else {
+ self::$item_id = 0;
+ }
+
+ self::$min_id = $request['min_id'] ?? null;
+ self::$max_id = $request['last_created'] ?? $request['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 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 == self::FOLLOWERS) {
+ $condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` = ?)", $this->session->getLocalUserId(), Contact::FOLLOWER];
+ } elseif (self::$content == self::SHARERSOFSHARERS) {
+ $cid = Contact::getPublicIdByUserId($this->session->getLocalUserId());
+
+ $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 - 90 day'), $cid, $this->getMedianRelationThreadScore($cid, 4), $cid
+ ];
+ } elseif (self::$content == self::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
+ {
+ $cache_key = 'Channel:getMedianComments:' . $divider;
+ $comments = $this->cache->get($cache_key);
+ if (!empty($comments)) {
+ return $comments;
+ }
+
+ $limit = $this->database->count('post-engagement', ["`contact-type` != ? AND `comments` > ?", Contact::TYPE_COMMUNITY, 0]) / $divider;
+ $post = $this->database->selectToArray('post-engagement', ['comments'], ["`contact-type` != ?", Contact::TYPE_COMMUNITY], ['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, 'median' => $comments]);
+ return $comments;
+ }
+
+ private function getMedianActivities(int $divider): int
+ {
+ $cache_key = 'Channel:getMedianActivities:' . $divider;
+ $activities = $this->cache->get($cache_key);
+ if (!empty($activities)) {
+ return $activities;
+ }
+
+ $limit = $this->database->count('post-engagement', ["`contact-type` != ? AND `activities` > ?", Contact::TYPE_COMMUNITY, 0]) / $divider;
+ $post = $this->database->selectToArray('post-engagement', ['activities'], ["`contact-type` != ?", Contact::TYPE_COMMUNITY], ['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, '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;
+ }
+
+ $limit = $this->database->count('contact-relation', ["`relation-cid` = ? AND `relation-thread-score` > ?", $cid, 0]) / $divider;
+ $relation = $this->database->selectToArray('contact-relation', ['relation-thread-score'], ['relation-cid' => $cid], ['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;
}
}