mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2026-03-26 10:12:52 -04:00
New automatic feed visibility/priority during search (#8609)
When the search query includes some feed IDs or category IDs, adjust feed visibility/priority filter to include at minimum feed or category visibility. Fix: https://github.com/FreshRSS/FreshRSS/issues/8602
This commit is contained in:
committed by
GitHub
parent
b19060aa1f
commit
1b90c40fd6
@@ -89,33 +89,45 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
|
||||
$type_get = $get[0];
|
||||
$get = (int)substr($get, 2);
|
||||
switch ($type_get) {
|
||||
case 'c':
|
||||
$entryDAO->markReadCat($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
case 'c': // Category
|
||||
$entryDAO->markReadCat($get, $id_max,
|
||||
priorityMin: min(FreshRSS_Feed::PRIORITY_CATEGORY, FreshRSS_Context::$search->needVisibility() ?? FreshRSS_Feed::PRIORITY_IMPORTANT),
|
||||
filters: FreshRSS_Context::$search, state: FreshRSS_Context::$state, is_read: $is_read);
|
||||
break;
|
||||
case 'f':
|
||||
case 'f': // Feed
|
||||
$entryDAO->markReadFeed($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
break;
|
||||
case 's':
|
||||
$entryDAO->markReadEntries($id_max, true, null, FreshRSS_Feed::PRIORITY_IMPORTANT,
|
||||
FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
case 's': // Starred. Deprecated: use $state instead
|
||||
$entryDAO->markReadEntries($id_max, onlyFavorites: true,
|
||||
priorityMin: null,
|
||||
priorityMax: null,
|
||||
filters: FreshRSS_Context::$search, state: FreshRSS_Context::$state, is_read: $is_read);
|
||||
break;
|
||||
case 'a':
|
||||
$entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_MAIN_STREAM, FreshRSS_Feed::PRIORITY_IMPORTANT,
|
||||
FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
case 'a': // All PRIORITY_MAIN_STREAM
|
||||
$entryDAO->markReadEntries($id_max, onlyFavorites: false,
|
||||
priorityMin: min(FreshRSS_Feed::PRIORITY_MAIN_STREAM, FreshRSS_Context::$search->needVisibility() ?? FreshRSS_Feed::PRIORITY_IMPORTANT),
|
||||
priorityMax: null,
|
||||
filters: FreshRSS_Context::$search, state: FreshRSS_Context::$state, is_read: $is_read);
|
||||
break;
|
||||
case 'A':
|
||||
$entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_CATEGORY, FreshRSS_Feed::PRIORITY_IMPORTANT,
|
||||
FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
case 'A': // All except PRIORITY_HIDDEN
|
||||
$entryDAO->markReadEntries($id_max, onlyFavorites: false,
|
||||
priorityMin: min(FreshRSS_Feed::PRIORITY_CATEGORY, FreshRSS_Context::$search->needVisibility() ?? FreshRSS_Feed::PRIORITY_IMPORTANT),
|
||||
priorityMax: null,
|
||||
filters: FreshRSS_Context::$search, state: FreshRSS_Context::$state, is_read: $is_read);
|
||||
break;
|
||||
case 'Z':
|
||||
$entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_HIDDEN, FreshRSS_Feed::PRIORITY_IMPORTANT,
|
||||
FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
case 'Z': // All including PRIORITY_HIDDEN
|
||||
$entryDAO->markReadEntries($id_max, onlyFavorites: false,
|
||||
priorityMin: FreshRSS_Feed::PRIORITY_HIDDEN,
|
||||
priorityMax: null,
|
||||
filters: FreshRSS_Context::$search, state: FreshRSS_Context::$state, is_read: $is_read);
|
||||
break;
|
||||
case 'i':
|
||||
$entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_IMPORTANT, null,
|
||||
FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
case 'i': // Priority important feeds
|
||||
$entryDAO->markReadEntries($id_max, onlyFavorites: false,
|
||||
priorityMin: min(FreshRSS_Feed::PRIORITY_IMPORTANT, FreshRSS_Context::$search->needVisibility() ?? FreshRSS_Feed::PRIORITY_IMPORTANT),
|
||||
priorityMax: null,
|
||||
filters: FreshRSS_Context::$search, state: FreshRSS_Context::$state, is_read: $is_read);
|
||||
break;
|
||||
case 't':
|
||||
case 't': // Tag (label)
|
||||
$entryDAO->markReadTag($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
// Marking all entries in a tag as read can result in other tags also having all entries marked as read,
|
||||
// so the next unread tag calculation is deferred by passing next_get = 'a' instead of the current get ID.
|
||||
@@ -157,7 +169,7 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'T':
|
||||
case 'T': // Any tag (label)
|
||||
$entryDAO->markReadTag(0, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
|
||||
case 'Z': // All including PRIORITY_HIDDEN
|
||||
$this->view->categories = FreshRSS_Context::categories();
|
||||
break;
|
||||
case 'c':
|
||||
case 'c': // Category
|
||||
$cat = FreshRSS_Context::categories()[$id] ?? null;
|
||||
if ($cat == null) {
|
||||
Minz_Error::error(404);
|
||||
@@ -308,7 +308,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
|
||||
}
|
||||
$this->view->categories = [$cat->id() => $cat];
|
||||
break;
|
||||
case 'f':
|
||||
case 'f': // Feed
|
||||
// We most likely already have the feed object in cache
|
||||
$feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);
|
||||
if ($feed === null) {
|
||||
@@ -321,9 +321,6 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
|
||||
}
|
||||
$this->view->feeds = [$feed->id() => $feed];
|
||||
break;
|
||||
case 's':
|
||||
case 't':
|
||||
case 'T':
|
||||
default:
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
|
||||
@@ -516,6 +516,29 @@ class FreshRSS_BooleanSearch implements \Stringable {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum visibility (priority) level needed for this Boolean search, or null if it does not require any specific visibility level.
|
||||
* For instance, if the search includes some feed IDs then it will return PRIORITY_HIDDEN,
|
||||
* and if it includes some category IDs then it will return PRIORITY_CATEGORY.
|
||||
*/
|
||||
public function needVisibility(): ?int {
|
||||
$minVisibility = FreshRSS_Feed::PRIORITY_IMPORTANT + 1;
|
||||
foreach ($this->searches as $search) {
|
||||
if ($search instanceof FreshRSS_BooleanSearch) {
|
||||
$visibility = $search->needVisibility();
|
||||
if ($visibility !== null) {
|
||||
$minVisibility = min($minVisibility, $visibility);
|
||||
}
|
||||
} elseif ($search instanceof FreshRSS_Search) {
|
||||
$visibility = $search->needVisibility();
|
||||
if ($visibility !== null) {
|
||||
$minVisibility = min($minVisibility, $visibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $minVisibility < FreshRSS_Feed::PRIORITY_IMPORTANT ? $minVisibility : null;
|
||||
}
|
||||
|
||||
private ?string $expanded = null;
|
||||
|
||||
#[\Override]
|
||||
|
||||
@@ -489,7 +489,7 @@ final class FreshRSS_Context {
|
||||
self::$description = FreshRSS_Context::systemConf()->meta_description;
|
||||
self::$get_unread = self::$total_unread;
|
||||
break;
|
||||
case 's':
|
||||
case 's': // Starred. Deprecated: use $state instead
|
||||
self::$current_get['starred'] = true;
|
||||
self::$name = _t('index.feed.title_fav');
|
||||
self::$description = FreshRSS_Context::systemConf()->meta_description;
|
||||
@@ -497,7 +497,7 @@ final class FreshRSS_Context {
|
||||
// Update state if favorite is not yet enabled.
|
||||
self::$state = self::$state | FreshRSS_Entry::STATE_FAVORITE;
|
||||
break;
|
||||
case 'f':
|
||||
case 'f': // Feed
|
||||
// We try to find the corresponding feed. When allowing robots, always retrieve the full feed including description
|
||||
$feed = FreshRSS_Context::systemConf()->allow_robots ? null : FreshRSS_Category::findFeed(self::$categories, $id);
|
||||
if ($feed === null) {
|
||||
@@ -509,7 +509,7 @@ final class FreshRSS_Context {
|
||||
self::$description = $feed->description();
|
||||
self::$get_unread = $feed->nbNotRead();
|
||||
break;
|
||||
case 'c':
|
||||
case 'c': // Category
|
||||
// We try to find the corresponding category.
|
||||
self::$current_get['category'] = $id;
|
||||
$cat = null;
|
||||
@@ -525,7 +525,7 @@ final class FreshRSS_Context {
|
||||
self::$name = $cat->name();
|
||||
self::$get_unread = $cat->nbNotRead();
|
||||
break;
|
||||
case 't':
|
||||
case 't': // Tag (label)
|
||||
// We try to find the corresponding tag.
|
||||
self::$current_get['tag'] = $id;
|
||||
$tag = null;
|
||||
@@ -545,7 +545,7 @@ final class FreshRSS_Context {
|
||||
self::$name = $tag->name();
|
||||
self::$get_unread = $tag->nbUnread();
|
||||
break;
|
||||
case 'T':
|
||||
case 'T': // Any tag (label)
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
self::$current_get['tags'] = true;
|
||||
self::$name = _t('index.menu.mylabels');
|
||||
|
||||
@@ -644,9 +644,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
|
||||
*
|
||||
* @param int $id category ID
|
||||
* @param numeric-string $idMax fail safe article ID
|
||||
* @param int $priorityMin minimum feed priority to include
|
||||
* @return int|false affected rows
|
||||
*/
|
||||
public function markReadCat(int $id, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true): int|false {
|
||||
public function markReadCat(int $id, string $idMax = '0', int $priorityMin = FreshRSS_Feed::PRIORITY_CATEGORY,
|
||||
?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true): int|false {
|
||||
FreshRSS_UserDAO::touch();
|
||||
if ($idMax == '0') {
|
||||
$idMax = uTimeString();
|
||||
@@ -659,7 +661,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
|
||||
WHERE is_read <> ? AND id <= ?
|
||||
AND id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category=? AND f.priority >= ? AND f.priority < ?)
|
||||
SQL;
|
||||
$values = [$is_read ? 1 : 0, time(), $is_read ? 1 : 0, $idMax, $id, FreshRSS_Feed::PRIORITY_CATEGORY, FreshRSS_Feed::PRIORITY_IMPORTANT];
|
||||
$values = [$is_read ? 1 : 0, time(), $is_read ? 1 : 0, $idMax, $id, $priorityMin, FreshRSS_Feed::PRIORITY_IMPORTANT];
|
||||
|
||||
[$searchValues, $search] = $this->sqlListEntriesWhere(alias: '', state: $state, filters: $filters);
|
||||
|
||||
@@ -1541,41 +1543,46 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
|
||||
$values = [];
|
||||
switch ($type) {
|
||||
case 'a': // All PRIORITY_MAIN_STREAM
|
||||
$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_MAIN_STREAM . ' ';
|
||||
$where .= 'f.priority >= ' .
|
||||
min(FreshRSS_Feed::PRIORITY_MAIN_STREAM, FreshRSS_Context::$search->needVisibility() ?? FreshRSS_Feed::PRIORITY_IMPORTANT) . ' ';
|
||||
break;
|
||||
case 'A': // All except PRIORITY_HIDDEN
|
||||
$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_FEED . ' ';
|
||||
$where .= 'f.priority >= ' .
|
||||
min(FreshRSS_Feed::PRIORITY_CATEGORY, FreshRSS_Context::$search->needVisibility() ?? FreshRSS_Feed::PRIORITY_IMPORTANT) . ' ';
|
||||
break;
|
||||
case 'Z': // All including PRIORITY_HIDDEN
|
||||
$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_HIDDEN . ' ';
|
||||
$where .= '1=1 ';
|
||||
break;
|
||||
case 'i': // Priority important feeds
|
||||
$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_IMPORTANT . ' ';
|
||||
$where .= 'f.priority >= ' .
|
||||
min(FreshRSS_Feed::PRIORITY_IMPORTANT, FreshRSS_Context::$search->needVisibility() ?? FreshRSS_Feed::PRIORITY_IMPORTANT) . ' ';
|
||||
break;
|
||||
case 's': //Starred. Deprecated: use $state instead
|
||||
$where .= 'f.priority > ' . FreshRSS_Feed::PRIORITY_HIDDEN . ' ';
|
||||
case 's': // Starred. Deprecated: use $state instead
|
||||
$where .= 'f.priority > ' .
|
||||
min(FreshRSS_Feed::PRIORITY_HIDDEN, FreshRSS_Context::$search->needVisibility() ?? FreshRSS_Feed::PRIORITY_IMPORTANT) . ' ';
|
||||
$where .= 'AND e.is_favorite=1 ';
|
||||
break;
|
||||
case 'S': //Starred
|
||||
case 'S': // Starred
|
||||
$where .= 'e.is_favorite=1 ';
|
||||
break;
|
||||
case 'c': //Category
|
||||
$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_CATEGORY . ' ';
|
||||
case 'c': // Category
|
||||
$where .= 'f.priority >= ' .
|
||||
min(FreshRSS_Feed::PRIORITY_CATEGORY, FreshRSS_Context::$search->needVisibility() ?? FreshRSS_Feed::PRIORITY_IMPORTANT) . ' ';
|
||||
$where .= 'AND f.category=? ';
|
||||
$values[] = $id;
|
||||
break;
|
||||
case 'f': //Feed
|
||||
case 'f': // Feed
|
||||
$where .= 'e.id_feed=? ';
|
||||
$values[] = $id;
|
||||
break;
|
||||
case 't': //Tag (label)
|
||||
case 't': // Tag (label)
|
||||
$where .= 'et.id_tag=? ';
|
||||
$values[] = $id;
|
||||
break;
|
||||
case 'T': //Any tag (label)
|
||||
case 'T': // Any tag (label)
|
||||
$where .= '1=1 ';
|
||||
break;
|
||||
case 'ST': //Starred or tagged (label)
|
||||
case 'ST': // Starred or tagged (label)
|
||||
$where .= 'e.is_favorite=1 OR EXISTS (SELECT et2.id_tag FROM `_entrytag` et2 WHERE et2.id_entry = e.id) ';
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -529,6 +529,21 @@ class FreshRSS_Search implements \Stringable {
|
||||
return $this->not_category_ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum visibility (priority) level needed for this search,
|
||||
* or null if it does not require any specific visibility level.
|
||||
* For instance, if the search includes some feed IDs then it will return PRIORITY_HIDDEN,
|
||||
* and if it includes some category IDs then it will return PRIORITY_CATEGORY.
|
||||
*/
|
||||
public function needVisibility(): ?int {
|
||||
if ($this->feed_ids !== null && count($this->feed_ids) > 0) {
|
||||
return FreshRSS_Feed::PRIORITY_HIDDEN;
|
||||
} elseif ($this->category_ids !== null && count($this->category_ids) > 0) {
|
||||
return FreshRSS_Feed::PRIORITY_CATEGORY;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return list<list<int>|'*'>|null */
|
||||
public function getLabelIds(): array|null {
|
||||
return $this->label_ids;
|
||||
|
||||
@@ -154,28 +154,28 @@ class FreshRSS_UserQuery {
|
||||
case 'Z': // All including PRIORITY_HIDDEN
|
||||
$this->get_type = 'Z';
|
||||
break;
|
||||
case 'c':
|
||||
case 'c': // Category
|
||||
$this->get_type = 'category';
|
||||
$c = $this->categories[$id] ?? null;
|
||||
$this->get_name = $c === null ? '' : $c->name();
|
||||
break;
|
||||
case 'f':
|
||||
case 'f': // Feed
|
||||
$this->get_type = 'feed';
|
||||
$f = FreshRSS_Category::findFeed($this->categories, $id);
|
||||
$this->get_name = $f === null ? '' : $f->name();
|
||||
break;
|
||||
case 'i':
|
||||
case 'i': // Priority important feeds
|
||||
$this->get_type = 'important';
|
||||
break;
|
||||
case 's':
|
||||
case 's': // Starred. Deprecated: use $state instead
|
||||
$this->get_type = 'favorite';
|
||||
break;
|
||||
case 't':
|
||||
case 't': // Tag (label)
|
||||
$this->get_type = 'label';
|
||||
$l = $this->labels[$id] ?? null;
|
||||
$this->get_name = $l === null ? '' : $l->name();
|
||||
break;
|
||||
case 'T':
|
||||
case 'T': // Any tag (label)
|
||||
$this->get_type = 'all_labels';
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1322,4 +1322,22 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
|
||||
['date:2024/ a', 'date:/2025', 'a'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideNeedVisibility')]
|
||||
public function testNeedVisibility(string $input, ?int $expected): void {
|
||||
$search = new FreshRSS_Search($input);
|
||||
self::assertSame($expected, $search->needVisibility());
|
||||
}
|
||||
|
||||
/** @return list<list<string|int|null>> */
|
||||
public static function provideNeedVisibility(): array {
|
||||
return [
|
||||
['', null],
|
||||
['f:1', FreshRSS_Feed::PRIORITY_HIDDEN],
|
||||
['c:2', FreshRSS_Feed::PRIORITY_CATEGORY],
|
||||
['f:1 c:2', FreshRSS_Feed::PRIORITY_HIDDEN],
|
||||
['-f:1', null],
|
||||
['-c:2', null],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user