Keep a feed, an OPML or a Patreon creator out of All Subscriptions (#288)
A switch in the feed's settings, In All Subscriptions, on its subscription as hide_from_all. All Subscriptions' list and count leave out a feed hidden on its own or through the OPML it is in, and the page's unread count does the same. Signals read every feed as before, and Currently Listening, which is All Subscriptions' in-progress filter, keeps an episode under way whatever the feed's setting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -122,6 +122,10 @@ async fn create_missing(orm: &sea_orm::DatabaseConnection) -> Result<()> {
|
||||
let sql = "ALTER TABLE subscriptions ADD COLUMN kept BOOLEAN NOT NULL DEFAULT false";
|
||||
orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?;
|
||||
}
|
||||
if orm.execute_unprepared("SELECT hide_from_all FROM subscriptions LIMIT 0").await.is_err() {
|
||||
let sql = "ALTER TABLE subscriptions ADD COLUMN hide_from_all BOOLEAN NOT NULL DEFAULT false";
|
||||
orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?;
|
||||
}
|
||||
if orm.execute_unprepared("SELECT description FROM signals LIMIT 0").await.is_err() {
|
||||
// Every verdict so far answered the old question, any one keyword on its own, which put
|
||||
// a game's remaster in AI Development (#259): all of them are asked again.
|
||||
@@ -1186,6 +1190,15 @@ fn entries_from(
|
||||
Some([f]) => format!("e.feed_id = {}", a.p(*f)),
|
||||
Some([]) => "false".into(),
|
||||
Some(fs) => format!("e.feed_id IN ({})", fs.iter().map(|f| a.p(*f)).collect::<Vec<_>>().join(", ")),
|
||||
// All Subscriptions leaves out what this person hid from it, on the feed or on its OPML
|
||||
// (#288). A Signal reads every feed, and an episode under way still shows in Currently
|
||||
// Listening, which is this list's in-progress filter.
|
||||
None if signal.is_none() && filter != Filter::InProgress => format!(
|
||||
"e.feed_id IN (SELECT m.feed_id FROM subscriptions m JOIN feeds f ON f.id = m.feed_id
|
||||
WHERE m.user_id = {user} AND NOT m.hide_from_all
|
||||
AND NOT EXISTS (SELECT 1 FROM subscriptions g
|
||||
WHERE g.user_id = {user} AND g.feed_id = f.group_id AND g.hide_from_all))"
|
||||
),
|
||||
None => format!("e.feed_id IN (SELECT feed_id FROM subscriptions WHERE user_id = {user})"),
|
||||
};
|
||||
if let Some(id) = signal {
|
||||
@@ -1867,6 +1880,29 @@ impl Db {
|
||||
Ok(r.rows_affected > 0)
|
||||
}
|
||||
|
||||
/// The feeds and lists this person hid from All Subscriptions (#288).
|
||||
pub async fn hidden_from_all(&self, user_id: i64) -> Result<std::collections::HashSet<String>> {
|
||||
Ok(subscriptions::Entity::find()
|
||||
.filter(subscriptions::Column::UserId.eq(user_id))
|
||||
.filter(subscriptions::Column::HideFromAll.eq(true))
|
||||
.all(&self.orm)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|s| s.feed_id)
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// False when they do not subscribe to it, as `set_pinned`.
|
||||
pub async fn set_hide_from_all(&self, user_id: i64, feed_id: &str, on: bool) -> Result<bool> {
|
||||
let r = subscriptions::Entity::update_many()
|
||||
.col_expr(subscriptions::Column::HideFromAll, Expr::val(on))
|
||||
.filter(subscriptions::Column::UserId.eq(user_id))
|
||||
.filter(subscriptions::Column::FeedId.eq(feed_id))
|
||||
.exec(&self.orm)
|
||||
.await?;
|
||||
Ok(r.rows_affected > 0)
|
||||
}
|
||||
|
||||
/// Who subscribes to a feed, and whether each keeps it (#280).
|
||||
pub async fn subscribers_of(&self, feed_id: &str) -> Result<Vec<(i64, bool)>> {
|
||||
Ok(subscriptions::Entity::find()
|
||||
@@ -3731,6 +3767,37 @@ mod tests {
|
||||
assert_eq!(db.count_of(1, Some(&[]), Filter::All, None).await.unwrap(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn all_subscriptions_leaves_out_what_was_hidden_from_it() {
|
||||
let db = Db::memory().await.unwrap();
|
||||
db.exec_for_test(
|
||||
"INSERT INTO users (id, name, is_admin) VALUES (1,'a',true), (2,'b',false);
|
||||
INSERT INTO feeds (id, url, group_id, managed) VALUES ('a','u1','o',true), ('b','u2','o',true), ('c','u3',NULL,false), ('d','u4',NULL,false);
|
||||
INSERT INTO subscriptions (user_id, feed_id) VALUES (1,'o'), (1,'a'), (1,'b'), (1,'c'), (1,'d'), (2,'a'), (2,'o');
|
||||
INSERT INTO entries (feed_id, guid, title, first_seen) VALUES
|
||||
('a','1','A one',1), ('b','2','B two',2), ('c','3','C three',3), ('d','4','D four',4);
|
||||
INSERT INTO entry_state (user_id, feed_id, guid, position) VALUES (1,'a','1',60);",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let order = order_sql("published", "desc");
|
||||
let all = async |user, filter| {
|
||||
let rows = db.entries_of(user, None, filter, None, 0, 50, &order).await.unwrap();
|
||||
let n = db.count_of(user, None, filter, None).await.unwrap();
|
||||
assert_eq!(rows.len() as i64, n, "the count is of the same list");
|
||||
rows.into_iter().map(|r| r.title.unwrap()).collect::<Vec<_>>()
|
||||
};
|
||||
assert!(db.set_hide_from_all(1, "o", true).await.unwrap());
|
||||
assert!(db.set_hide_from_all(1, "c", true).await.unwrap());
|
||||
assert!(!db.set_hide_from_all(1, "x", true).await.unwrap(), "not subscribed");
|
||||
assert_eq!(all(1, Filter::All).await, ["D four"], "the OPML's shows and the hidden feed left out");
|
||||
assert_eq!(all(1, Filter::InProgress).await, ["A one"], "an episode under way still shows");
|
||||
assert_eq!(all(2, Filter::All).await, ["A one"], "someone else's All Subscriptions is their own");
|
||||
assert_eq!(db.count_of(1, Some(&["c"]), Filter::All, None).await.unwrap(), 1, "its own page still lists it");
|
||||
db.set_hide_from_all(1, "o", false).await.unwrap();
|
||||
assert_eq!(all(1, Filter::All).await, ["D four", "B two", "A one"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pending_takes_the_latest_episodes_first() {
|
||||
// A cap of 3 must mean the three newest, not the three recorded first.
|
||||
|
||||
Reference in New Issue
Block a user