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:
18
src/web.rs
18
src/web.rs
@@ -614,6 +614,8 @@ struct FeedRow {
|
||||
unread: i64,
|
||||
/// Including you. More than one means every file here is shared.
|
||||
subscribers: i64,
|
||||
/// Pinned to the top of your list.
|
||||
pinned: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -641,6 +643,7 @@ async fn feeds(
|
||||
.map(|s| (s.feed_id.clone(), s))
|
||||
.collect();
|
||||
let counts = state.ctx.db.subscriber_counts()?;
|
||||
let pinned = state.ctx.db.pinned_feeds(user.id)?;
|
||||
let mut out = Vec::with_capacity(mine.len());
|
||||
for sub in &subs {
|
||||
let (id, feed) = (&sub.id, &sub.cfg);
|
||||
@@ -701,6 +704,7 @@ async fn feeds(
|
||||
downloaded: s.downloaded,
|
||||
unread: state.ctx.db.unread_count(user.id, id)?,
|
||||
subscribers: counts.get(id).copied().unwrap_or(0),
|
||||
pinned: pinned.contains(id),
|
||||
});
|
||||
}
|
||||
Ok(Json(out))
|
||||
@@ -884,6 +888,13 @@ impl ApiError {
|
||||
status: StatusCode::FORBIDDEN,
|
||||
}
|
||||
}
|
||||
|
||||
fn not_found(msg: impl Into<String>) -> Self {
|
||||
Self {
|
||||
error: anyhow::Error::msg(msg.into()),
|
||||
status: StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
@@ -1218,6 +1229,7 @@ struct FeedPatch {
|
||||
auto_download: Option<bool>,
|
||||
#[serde(default, deserialize_with = "double_option")]
|
||||
max_new_per_check: Option<Option<usize>>,
|
||||
pinned: Option<bool>,
|
||||
}
|
||||
|
||||
fn double_option<'de, T, D>(de: D) -> Result<Option<Option<T>>, D::Error>
|
||||
@@ -1234,6 +1246,12 @@ async fn patch_feed(
|
||||
user: crate::db::User,
|
||||
Json(body): Json<FeedPatch>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
// Pinning is yours alone too, and means nothing for a feed you do not subscribe to.
|
||||
if let Some(on) = body.pinned
|
||||
&& !state.ctx.db.set_pinned(user.id, &id, on)?
|
||||
{
|
||||
return Err(ApiError::not_found("you do not subscribe to that feed"));
|
||||
}
|
||||
// What one person wants -- which items, whether to fetch them, how many at a time --
|
||||
// is theirs. It goes on their subscription and nobody else sees the change.
|
||||
if state.ctx.db.subscription(user.id, &id)?.is_some() {
|
||||
|
||||
Reference in New Issue
Block a user