Pin a feed to the top of the feed list
subscriptions.pinned, per person, set by PATCH /api/feeds/{id} {pinned} and
returned as FeedRow.pinned. Kept out of Sub, which the scanner merges into its
policy; set_subscription names its columns, so saving a feed's settings leaves
the pin alone (tested).
Pinned feeds come first in the list, a pin before the name and a rule under the
block: a pinned folder with its feeds under it, a feed from inside one lifted out
of it. The pin button is on both the feed and the folder page.
Closes #33.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
36
src/db.rs
36
src/db.rs
@@ -96,6 +96,8 @@ CREATE TABLE IF NOT EXISTS subscriptions (
|
||||
auto_download INTEGER,
|
||||
allow_explicit INTEGER,
|
||||
max_new_per_check INTEGER,
|
||||
-- Pinned to the top of this person's feed list, a feed inside a folder included.
|
||||
pinned INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (user_id, feed_id)
|
||||
);
|
||||
|
||||
@@ -182,6 +184,7 @@ fn migrate(conn: &Connection) -> Result<()> {
|
||||
// Kept per account so a theme follows you to another browser; it was in localStorage.
|
||||
("users", "theme", "TEXT"),
|
||||
("users", "theme_mode", "TEXT"),
|
||||
("subscriptions", "pinned", "INTEGER NOT NULL DEFAULT 0"),
|
||||
];
|
||||
let retired: &[(&str, &str)] = &[
|
||||
// Read state from before accounts, long since moved to entry_state. Two bugs came from
|
||||
@@ -1060,6 +1063,24 @@ impl Db {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The feeds this person pinned to the top of their list. Kept apart from `Sub`, which is
|
||||
/// what the scanner merges into its policy, and which a pin has nothing to do with.
|
||||
pub fn pinned_feeds(&self, user_id: i64) -> Result<std::collections::HashSet<String>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare("SELECT feed_id FROM subscriptions WHERE user_id = ?1 AND pinned")?;
|
||||
let ids = stmt.query_map([user_id], |r| r.get(0))?.collect::<rusqlite::Result<_>>()?;
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
/// False when they do not subscribe to it, since there is then no row in their list to pin.
|
||||
pub fn set_pinned(&self, user_id: i64, feed_id: &str, on: bool) -> Result<bool> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
Ok(conn.execute(
|
||||
"UPDATE subscriptions SET pinned = ?3 WHERE user_id = ?1 AND feed_id = ?2",
|
||||
params![user_id, feed_id, on as i64],
|
||||
)? > 0)
|
||||
}
|
||||
|
||||
pub fn unsubscribe(&self, user_id: i64, feed_id: &str) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
@@ -2029,6 +2050,21 @@ mod tests {
|
||||
assert_eq!(after.error_since, None, "a clean check ends the run of failures");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pin_survives_saving_the_feeds_settings_and_needs_a_subscription() {
|
||||
let db = Db::memory().unwrap();
|
||||
let me = db.create_user("pat", None, false).unwrap();
|
||||
assert!(!db.set_pinned(me, "f", true).unwrap(), "not subscribed: nothing to pin");
|
||||
db.subscribe(me, "f").unwrap();
|
||||
assert!(db.set_pinned(me, "f", true).unwrap());
|
||||
// set_subscription writes the rest of the row; it must leave the pin alone.
|
||||
db.set_subscription(me, &Sub { feed_id: "f".into(), auto_download: Some(false), ..Default::default() })
|
||||
.unwrap();
|
||||
assert!(db.pinned_feeds(me).unwrap().contains("f"));
|
||||
db.set_pinned(me, "f", false).unwrap();
|
||||
assert!(db.pinned_feeds(me).unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enclosure_url_is_the_dedupe_key() {
|
||||
let db = Db::memory().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user