use Friendica\App\Mode;
use Friendica\BaseModule;
use Friendica\Content\Conversation\Collection\Timelines;
-use Friendica\Content\Conversation\Entity\Timeline as TimelineEntity;
+use Friendica\Content\Conversation\Entity\Channel as ChannelEntity;
+use Friendica\Content\Conversation\Repository\UserDefinedChannel;
use Friendica\Core\Cache\Capability\ICanCache;
use Friendica\Core\Cache\Enum\Duration;
use Friendica\Core\Config\Capability\IManageConfigValues;
/** @var string */
protected $selectedTab;
/** @var mixed */
- protected $min_id;
+ protected $minId;
/** @var mixed */
- protected $max_id;
+ protected $maxId;
/** @var string */
protected $accountTypeString;
/** @var int */
protected $itemsPerPage;
/** @var bool */
protected $noSharer;
+ /** @var bool */
+ protected $force;
+ /** @var bool */
+ protected $update;
/** @var App\Mode $mode */
protected $mode;
- /** @var UserSession */
+ /** @var IHandleUserSessions */
protected $session;
/** @var Database */
protected $database;
protected $config;
/** @var ICanCache */
protected $cache;
+ /** @var UserDefinedChannel */
+ protected $channelRepository;
- 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 = [])
+ public function __construct(UserDefinedChannel $channel, 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;
+ $this->channelRepository = $channel;
+ $this->mode = $mode;
+ $this->session = $session;
+ $this->database = $database;
+ $this->pConfig = $pConfig;
+ $this->config = $config;
+ $this->cache = $cache;
}
/**
protected function parseRequest(array $request)
{
$this->logger->debug('Got request', $request);
- $this->selectedTab = $this->parameters['content'] ?? '';
+ $this->selectedTab = $this->parameters['content'] ?? $request['channel'] ?? '';
$this->accountTypeString = $request['accounttype'] ?? $this->parameters['accounttype'] ?? '';
$this->accountType = User::getAccountTypeByString($this->accountTypeString);
}
if (!empty($request['item'])) {
- $item = Post::selectFirst(['parent', 'parent-uri-id'], ['id' => $request['item']]);
+ $item = Post::selectFirst(['parent', 'parent-uri-id'], ['id' => $request['item']]);
$this->itemUriId = $item['parent-uri-id'] ?? 0;
} else {
$this->itemUriId = 0;
}
- $this->min_id = $request['min_id'] ?? null;
- $this->max_id = $request['max_id'] ?? null;
+ $this->minId = $request['min_id'] ?? null;
+ $this->maxId = $request['max_id'] ?? null;
$this->noSharer = !empty($request['no_sharer']);
+ $this->force = !empty($request['force']) && !empty($request['item']);
+ $this->update = !empty($request['force']) && !empty($request['first_received']) && !empty($request['first_created']) && !empty($request['first_uriid']) && !empty($request['first_commented']);
}
protected function getNoSharerWidget(string $base): string
}
$query_parameters = [];
- if (!empty($this->min_id)) {
- $query_parameters['min_id'] = $this->min_id;
+ if (!empty($this->minId)) {
+ $query_parameters['min_id'] = $this->minId;
}
- if (!empty($this->max_id)) {
- $query_parameters['max_id'] = $this->max_id;
+ if (!empty($this->maxId)) {
+ $query_parameters['max_id'] = $this->maxId;
}
$path_all = $path . (!empty($query_parameters) ? '?' . http_build_query($query_parameters) : '');
]);
}
- protected function getTabArray(Timelines $timelines, string $prefix): array
+ protected function getTabArray(Timelines $timelines, string $prefix, string $parameter = ''): array
{
$tabs = [];
foreach ($timelines as $tab) {
- $tabs[] = [
+ if (is_null($tab->path) && !empty($parameter)) {
+ $path = $prefix . '?' . http_build_query([$parameter => $tab->code]);
+ } else {
+ $path = $tab->path ?? $prefix . '/' . $tab->code;
+ }
+ $tabs[$tab->code] = [
+ 'code' => $tab->code,
'label' => $tab->label,
- 'url' => $tab->path ?? $prefix . '/' . $tab->code,
+ 'url' => $path,
'sel' => $this->selectedTab == $tab->code ? 'active' : '',
'title' => $tab->description,
'id' => $prefix . '-' . $tab->code . '-tab',
* @throws \Exception
*/
protected function getChannelItems()
+ {
+ $items = $this->getRawChannelItems();
+
+ $contacts = $this->database->selectToArray('user-contact', ['cid'], ['channel-frequency' => Contact\User::FREQUENCY_REDUCED, 'cid' => array_column($items, 'owner-id')]);
+ $reduced = array_column($contacts, 'cid');
+
+ $maxpostperauthor = $this->config->get('channel', 'max_posts_per_author');
+
+ if ($maxpostperauthor != 0) {
+ $count = 1;
+ $owner_posts = [];
+ $selected_items = [];
+
+ while (count($selected_items) < $this->itemsPerPage && ++$count < 50 && count($items) > 0) {
+ $maxposts = round((count($items) / $this->itemsPerPage) * $maxpostperauthor);
+ $minId = $items[array_key_first($items)]['created'];
+ $maxId = $items[array_key_last($items)]['created'];
+
+ foreach ($items as $item) {
+ if (!in_array($item['owner-id'], $reduced)) {
+ continue;
+ }
+ $owner_posts[$item['owner-id']][$item['uri-id']] = (($item['comments'] * 100) + $item['activities']);
+ }
+ foreach ($owner_posts as $posts) {
+ if (count($posts) <= $maxposts) {
+ continue;
+ }
+ asort($posts);
+ while (count($posts) > $maxposts) {
+ $uri_id = array_key_first($posts);
+ unset($posts[$uri_id]);
+ unset($items[$uri_id]);
+ }
+ }
+ $selected_items = array_merge($selected_items, $items);
+
+ // If we're looking at a "previous page", the lookup continues forward in time because the list is
+ // sorted in chronologically decreasing order
+ if (!empty($this->minId)) {
+ $this->minId = $minId;
+ } else {
+ // In any other case, the lookup continues backwards in time
+ $this->maxId = $maxId;
+ }
+
+ if (count($selected_items) < $this->itemsPerPage) {
+ $items = $this->getRawChannelItems();
+ }
+ }
+ } else {
+ $selected_items = $items;
+ }
+
+ $condition = ['unseen' => true, 'uid' => $this->session->getLocalUserId(), 'parent-uri-id' => array_column($selected_items, 'uri-id')];
+ $this->setItemsSeenByCondition($condition);
+
+ return $selected_items;
+ }
+
+ /**
+ * Database query for the channel page
+ *
+ * @return array
+ * @throws \Exception
+ */
+ private function getRawChannelItems()
{
$uid = $this->session->getLocalUserId();
- if ($this->selectedTab == TimelineEntity::WHATSHOT) {
+ if ($this->selectedTab == ChannelEntity::WHATSHOT) {
if (!is_null($this->accountType)) {
- $condition = ["(`comments` >= ? OR `activities` >= ?) AND `contact-type` = ?", $this->getMedianComments($uid, 4), $this->getMedianActivities($uid, 4), $this->accountType];
+ $condition = ["(`comments` > ? OR `activities` > ?) AND `contact-type` = ?", $this->getMedianComments($uid, 4), $this->getMedianActivities($uid, 4), $this->accountType];
} else {
- $condition = ["(`comments` >= ? OR `activities` >= ?) AND `contact-type` != ?", $this->getMedianComments($uid, 4), $this->getMedianActivities($uid, 4), Contact::TYPE_COMMUNITY];
+ $condition = ["(`comments` > ? OR `activities` > ?) AND `contact-type` != ?", $this->getMedianComments($uid, 4), $this->getMedianActivities($uid, 4), Contact::TYPE_COMMUNITY];
}
- } elseif ($this->selectedTab == TimelineEntity::FORYOU) {
+ } elseif ($this->selectedTab == ChannelEntity::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`)))",
+ (`owner-id` IN (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND (`notify_new_posts` OR `channel-frequency` = ?))))",
$cid, $this->getMedianRelationThreadScore($cid, 4), $this->getMedianComments($uid, 4), $this->getMedianActivities($uid, 4), $cid,
- $uid, Contact::FRIEND, Contact::SHARING
+ $uid, Contact\User::FREQUENCY_ALWAYS
];
- } elseif ($this->selectedTab == TimelineEntity::FOLLOWERS) {
+ } elseif ($this->selectedTab == ChannelEntity::FOLLOWERS) {
$condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` = ?)", $uid, Contact::FOLLOWER];
- } elseif ($this->selectedTab == TimelineEntity::SHARERSOFSHARERS) {
+ } elseif ($this->selectedTab == ChannelEntity::SHARERSOFSHARERS) {
$cid = Contact::getPublicIdByUserId($uid);
// @todo Suggest posts from contacts that are followed most by our followers
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 ($this->selectedTab == TimelineEntity::IMAGE) {
+ } elseif ($this->selectedTab == ChannelEntity::IMAGE) {
$condition = ["`media-type` & ?", 1];
- } elseif ($this->selectedTab == TimelineEntity::VIDEO) {
+ } elseif ($this->selectedTab == ChannelEntity::VIDEO) {
$condition = ["`media-type` & ?", 2];
- } elseif ($this->selectedTab == TimelineEntity::AUDIO) {
+ } elseif ($this->selectedTab == ChannelEntity::AUDIO) {
$condition = ["`media-type` & ?", 4];
- } elseif ($this->selectedTab == TimelineEntity::LANGUAGE) {
- $condition = ["JSON_EXTRACT(JSON_KEYS(language), '$[0]') = ?", $this->l10n->convertCodeForLanguageDetection(User::getLanguageCode($uid))];
+ } elseif ($this->selectedTab == ChannelEntity::LANGUAGE) {
+ $condition = ["JSON_EXTRACT(JSON_KEYS(language), '$[0]') = ?", User::getLanguageCode($uid)];
+ } elseif (is_numeric($this->selectedTab)) {
+ $condition = $this->getUserChannelConditions($this->selectedTab, $this->session->getLocalUserId());
}
- if ($this->selectedTab != TimelineEntity::LANGUAGE) {
+ if ($this->selectedTab != ChannelEntity::LANGUAGE) {
$condition = $this->addLanguageCondition($uid, $condition);
}
- $condition = DBA::mergeConditions($condition, ["NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `post-engagement`.`owner-id` AND (`ignored` OR `blocked` OR `collapsed`))", $uid]);
+ $condition = DBA::mergeConditions($condition, ["(NOT `restricted` OR EXISTS(SELECT `id` FROM `post-user` WHERE `uid` = ? AND `uri-id` = `post-engagement`.`uri-id`))", $uid]);
+
+ $condition = DBA::mergeConditions($condition, ["NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `post-engagement`.`owner-id` AND (`ignored` OR `blocked` OR `collapsed` OR `is-blocked` OR `channel-frequency` = ?))", $uid, Contact\User::FREQUENCY_NEVER]);
- if (($this->selectedTab != TimelineEntity::WHATSHOT) && !is_null($this->accountType)) {
+ if (($this->selectedTab != ChannelEntity::WHATSHOT) && !is_null($this->accountType)) {
$condition = DBA::mergeConditions($condition, ['contact-type' => $this->accountType]);
}
$condition = DBA::mergeConditions($condition, ["NOT `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `post-user`.`uid` = ? AND `post-user`.`uri-id` = `post-engagement`.`uri-id`)", $this->session->getLocalUserId()]);
}
- if (isset($this->max_id)) {
- $condition = DBA::mergeConditions($condition, ["`created` < ?", $this->max_id]);
+ if (isset($this->maxId)) {
+ $condition = DBA::mergeConditions($condition, ["`created` < ?", $this->maxId]);
}
- if (isset($this->min_id)) {
- $condition = DBA::mergeConditions($condition, ["`created` > ?", $this->min_id]);
+ if (isset($this->minId)) {
+ $condition = DBA::mergeConditions($condition, ["`created` > ?", $this->minId]);
// Previous page case: we want the items closest to min_id but for that we need to reverse the query order
- if (!isset($this->max_id)) {
+ if (!isset($this->maxId)) {
$params['order']['created'] = false;
}
}
}
- $items = $this->database->selectToArray('post-engagement', ['uri-id', 'created'], $condition, $params);
+ $items = [];
+ $result = $this->database->select('post-engagement', ['uri-id', 'created', 'owner-id', 'comments', 'activities'], $condition, $params);
+ if ($this->database->errorNo()) {
+ throw new \Exception($this->database->errorMessage(), $this->database->errorNo());
+ }
+
+ while ($item = $this->database->fetch($result)) {
+ $items[$item['uri-id']] = $item;
+ }
+ $this->database->close($result);
+
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($this->itemUriId) && isset($this->min_id) && !isset($this->max_id)) {
- $items = array_reverse($items);
+ if (empty($this->itemUriId) && isset($this->minId) && !isset($this->maxId)) {
+ $items = array_reverse($items, true);
}
- Item::update(['unseen' => false], ['unseen' => true, 'uid' => $uid, 'uri-id' => array_column($items, 'uri-id')]);
+ $condition = ['unseen' => true, 'uid' => $uid, 'parent-uri-id' => array_column($items, 'uri-id')];
+ $this->setItemsSeenByCondition($condition);
return $items;
}
+ private function getUserChannelConditions(int $id, int $uid): array
+ {
+ $channel = $this->channelRepository->selectById($id, $uid);
+ if (empty($channel)) {
+ return [];
+ }
+
+ $condition = [];
+
+ if (!empty($channel->circle)) {
+ if ($channel->circle == -1) {
+ $condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` IN (?, ?))", $uid, Contact::SHARING, Contact::FRIEND];
+ } elseif ($channel->circle == -2) {
+ $condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` = ?)", $uid, Contact::FOLLOWER];
+ } elseif ($channel->circle > 0) {
+ $condition = DBA::mergeConditions($condition, ["`owner-id` IN (SELECT `pid` FROM `group_member` INNER JOIN `account-user-view` ON `group_member`.`contact-id` = `account-user-view`.`id` WHERE `gid` = ? AND `account-user-view`.`uid` = ?)", $channel->circle, $uid]);
+ }
+ }
+
+ if (!empty($channel->fullTextSearch)) {
+ $search = $channel->fullTextSearch;
+ foreach (['from', 'to', 'group', 'tag', 'network', 'visibility'] as $keyword) {
+ $search = preg_replace('~(' . $keyword . ':.[\w@\.-]+)~', '"$1"', $search);
+ }
+ $condition = DBA::mergeConditions($condition, ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE)", $search]);
+ }
+
+ if (!empty($channel->includeTags)) {
+ $search = explode(',', mb_strtolower($channel->includeTags));
+ $placeholders = substr(str_repeat("?, ", count($search)), 0, -2);
+ $condition = DBA::mergeConditions($condition, array_merge(["`uri-id` IN (SELECT `uri-id` FROM `post-tag` INNER JOIN `tag` ON `tag`.`id` = `post-tag`.`tid` WHERE `post-tag`.`type` = 1 AND `name` IN (" . $placeholders . "))"], $search));
+ }
+
+ if (!empty($channel->excludeTags)) {
+ $search = explode(',', mb_strtolower($channel->excludeTags));
+ $placeholders = substr(str_repeat("?, ", count($search)), 0, -2);
+ $condition = DBA::mergeConditions($condition, array_merge(["NOT `uri-id` IN (SELECT `uri-id` FROM `post-tag` INNER JOIN `tag` ON `tag`.`id` = `post-tag`.`tid` WHERE `post-tag`.`type` = 1 AND `name` IN (" . $placeholders . "))"], $search));
+ }
+
+ if (!empty($channel->mediaType)) {
+ $condition = DBA::mergeConditions($condition, ["`media-type` & ?", $channel->mediaType]);
+ }
+
+ // For "addLanguageCondition" to work, the condition must not be empty
+ return $condition ?: ["true"];
+ }
+
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;
return $comments;
}
- $condition = ["`contact-type` != ? AND `comments` > ?", Contact::TYPE_COMMUNITY, 0];
+ $condition = ["`contact-type` != ? AND `comments` > ? AND NOT `restricted`", Contact::TYPE_COMMUNITY, 0];
$condition = $this->addLanguageCondition($uid, $condition);
$limit = $this->database->count('post-engagement', $condition) / $divider;
return $activities;
}
- $condition = ["`contact-type` != ? AND `activities` > ?", Contact::TYPE_COMMUNITY, 0];
+ $condition = ["`contact-type` != ? AND `activities` > ? AND NOT `restricted`", Contact::TYPE_COMMUNITY, 0];
$condition = $this->addLanguageCondition($uid, $condition);
$limit = $this->database->count('post-engagement', $condition) / $divider;
// 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($this->min_id)) {
- $this->min_id = $items[0]['commented'];
+ if (isset($this->minId)) {
+ $this->minId = $items[0]['received'];
} else {
// In any other case, the lookup continues backwards in time
- $this->max_id = $items[count($items) - 1]['commented'];
+ $this->maxId = $items[count($items) - 1]['received'];
}
$items = $this->selectItems();
$selected_items = $items;
}
+ $condition = ['unseen' => true, 'uid' => $this->session->getLocalUserId(), 'parent-uri-id' => array_column($selected_items, 'uri-id')];
+ $this->setItemsSeenByCondition($condition);
+
return $selected_items;
}
private function selectItems()
{
if ($this->selectedTab == 'local') {
- if (!is_null($this->accountType)) {
- $condition = ["`wall` AND `origin` AND `private` = ? AND `owner-contact-type` = ?", Item::PUBLIC, $this->accountType];
- } else {
- $condition = ["`wall` AND `origin` AND `private` = ?", Item::PUBLIC];
- }
+ $condition = ["`wall` AND `origin` AND `private` = ?", Item::PUBLIC];
} elseif ($this->selectedTab == 'global') {
- if (!is_null($this->accountType)) {
- $condition = ["`uid` = ? AND `private` = ? AND `owner-contact-type` = ?", 0, Item::PUBLIC, $this->accountType];
- } else {
- $condition = ["`uid` = ? AND `private` = ?", 0, Item::PUBLIC];
- }
+ $condition = ["`uid` = ? AND `private` = ?", 0, Item::PUBLIC];
} else {
return [];
}
- $params = ['order' => ['commented' => true], 'limit' => $this->itemsPerPage];
+ if (!is_null($this->accountType)) {
+ $condition = DBA::mergeConditions($condition, ['owner-contact-type' => $this->accountType]);
+ }
+
+ $params = ['order' => ['received' => true], 'limit' => $this->itemsPerPage];
if (!empty($this->itemUriId)) {
$condition = DBA::mergeConditions($condition, ['uri-id' => $this->itemUriId]);
$condition = DBA::mergeConditions($condition, ["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`)", $this->session->getLocalUserId()]);
}
- if (isset($this->max_id)) {
- $condition = DBA::mergeConditions($condition, ["`commented` < ?", $this->max_id]);
+ if (isset($this->maxId)) {
+ $condition = DBA::mergeConditions($condition, ["`received` < ?", $this->maxId]);
}
- if (isset($this->min_id)) {
- $condition = DBA::mergeConditions($condition, ["`commented` > ?", $this->min_id]);
+ if (isset($this->minId)) {
+ $condition = DBA::mergeConditions($condition, ["`received` > ?", $this->minId]);
// Previous page case: we want the items closest to min_id but for that we need to reverse the query order
- if (!isset($this->max_id)) {
- $params['order']['commented'] = false;
+ if (!isset($this->maxId)) {
+ $params['order']['received'] = false;
}
}
}
- $r = Post::selectThreadForUser($this->session->getLocalUserId() ?: 0, ['uri-id', 'commented', 'author-link'], $condition, $params);
+ $r = Post::selectThreadForUser($this->session->getLocalUserId() ?: 0, ['uri-id', 'received', 'author-link'], $condition, $params);
$items = Post::toArray($r);
if (empty($items)) {
}
// Previous page case: once we get the relevant items closest to min_id, we need to restore the expected display order
- if (empty($this->itemUriId) && isset($this->min_id) && !isset($this->max_id)) {
+ if (empty($this->itemUriId) && isset($this->minId) && !isset($this->maxId)) {
$items = array_reverse($items);
}
return $items;
}
+
+ /**
+ * Sets items as seen
+ *
+ * @param array $condition The array with the SQL condition
+ * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+ */
+ protected function setItemsSeenByCondition(array $condition)
+ {
+ if (empty($condition)) {
+ return;
+ }
+
+ $unseen = Post::exists($condition);
+
+ if ($unseen) {
+ /// @todo handle huge "unseen" updates in the background to avoid timeout errors
+ Item::update(['unseen' => false], $condition);
+ }
+ }
}