add bookmarks - #1494
add bookmarks#1494flyingisawol wants to merge 2 commits into
Conversation
Adds a "Bookmark Note"/"Remove Bookmark" option to the note 3-dot menu, backed by a NIP-51 kind-10003 bookmark list that mirrors the existing mute-list architecture (AccountBookmarksData, local+remote subscription, publish/retract helpers). Also adds a standalone Bookmarks column that lists all bookmarked notes sorted most-recently-bookmarked-first. Since the standard Timeline insertion path always sorts by a note's own created_at, bookmarks use a bespoke rebuild step (Timeline::rebuild_bookmarks_view) that assigns a synthetic rank from bookmark order instead, only rebuilding the view when the bookmark list actually changes. Progresses damus-io#226 (bookmark + bookmark column; "clear all bookmarks" confirmation flow not yet implemented). Changelog-Added: Add a bookmark option to the note menu and a Bookmarks column to view saved notes
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
🚧 Files skipped from review as they are similar to previous changes (8)
📝 WalkthroughWalkthroughChangesThe PR adds NIP-51 bookmark parsing and account synchronization, bookmark and unbookmark publishing, note-menu actions, and a dedicated bookmarks timeline with ordered refresh behavior and column-picker integration. Bookmarks feature
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant NoteView
participant NoteContextSelection
participant PublishApi
participant NostrDB
participant BookmarksTimeline
NoteView->>NoteContextSelection: select bookmark action
NoteContextSelection->>PublishApi: publish updated kind-10003 list
PublishApi->>NostrDB: store and poll bookmark list
NostrDB-->>BookmarksTimeline: provide current bookmark IDs
BookmarksTimeline->>BookmarksTimeline: rebuild ordered note view
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
crates/notedeck/src/bookmarks.rs (1)
24-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd Rustdoc for the new bookmark APIs.
crates/notedeck/src/bookmarks.rs#L24-L26: document membership semantics.crates/notedeck/src/account/bookmarks.rs#L8-L15: document the account bookmark cache and constructor.crates/notedeck/src/account/bookmarks.rs#L28-L28: document initial loading behavior.crates/notedeck/src/account/bookmarks.rs#L46-L50: document harvesting/current-event assumptions.crates/notedeck/src/account/bookmarks.rs#L62-L63: document polling and replacement behavior.crates/notedeck/src/account/accounts.rs#L332-L335: document the ownership/snapshot returned byAccounts::bookmarks().As per coding guidelines, “Ensure docstring coverage for any code added or modified.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/notedeck/src/bookmarks.rs` around lines 24 - 26, crates/notedeck/src/bookmarks.rs:24-26 — Add Rustdoc to Bookmarks::is_bookmarked describing note-ID membership semantics. crates/notedeck/src/account/bookmarks.rs:8-15 — Document the account bookmark cache and its constructor; 28 — document initial loading behavior; 46-50 — document harvesting and current-event assumptions; 62-63 — document polling and replacement behavior. crates/notedeck/src/account/accounts.rs:332-335 — Document Accounts::bookmarks ownership and the snapshot it returns.Source: Coding guidelines
crates/notedeck/src/note/publish.rs (1)
211-218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocument the modified Rust symbols.
crates/notedeck/src/note/publish.rs#L211-L218: add Rustdoc forsend_unbookmark_event.crates/notedeck/src/note/publish.rs#L265-L272: add Rustdoc forsend_bookmark_event.crates/notedeck/src/note/context.rs#L17-L27: documentBookmarkNote.crates/notedeck/src/note/context.rs#L129-L154: document the bookmark toggle behavior inprocess_selection.crates/notedeck_ui/src/note/context.rs#L65-L73: documentmenuand its bookmark-state parameter.crates/notedeck_ui/src/note/mod.rs#L661-L674: document the modifiedshow_standardbehavior.As per coding guidelines, “Ensure docstring coverage for any code added or modified.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/notedeck/src/note/publish.rs` around lines 211 - 218, Document all modified Rust symbols: add Rustdoc describing send_unbookmark_event and send_bookmark_event, document the BookmarkNote type and bookmark-toggle behavior in process_selection, describe menu and its bookmark-state parameter, and document the changed show_standard behavior. Apply these updates at crates/notedeck/src/note/publish.rs lines 211-218 and 265-272, crates/notedeck/src/note/context.rs lines 17-27 and 129-154, crates/notedeck_ui/src/note/context.rs lines 65-73, and crates/notedeck_ui/src/note/mod.rs lines 661-674.Source: Coding guidelines
crates/notedeck_columns/src/timeline/kind.rs (1)
992-999: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate kind-10003 bookmarks-list filter construction.
bookmarks_note_filterrebuilds the sameFilter::new().authors([pk.bytes()]).kinds([10003]).limit(1).build()shape that already exists (per graph evidence) incrates/notedeck/src/account/bookmarks.rsand twice incrates/notedeck/src/note/publish.rs(send_bookmark_event/send_unbookmark_event). Consider hoisting a single shared helper (e.g. alongsidebookmarks_from_noteincrates/notedeck/src/bookmarks.rs) so all four call sites stay in sync if the shape ever changes (e.g. addingsince/until).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/notedeck_columns/src/timeline/kind.rs` around lines 992 - 999, Centralize the kind-10003 bookmarks-list filter construction in a shared helper alongside bookmarks_from_note, then update bookmarks_note_filter and the matching constructions in the account/bookmarks and publish bookmark flows to call it. Preserve the existing authors, kind, and limit behavior so all four call sites remain synchronized.crates/notedeck_columns/src/timeline/mod.rs (1)
663-709: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winRecomputes the full bookmark→NoteRef mapping every frame, even when nothing changed.
rebuild_bookmarks_viewis called every frame while the Bookmarks column is ready (perapp.rs), and unconditionally re-queries the kind-10003 note plus does aget_note_by_idlookup per bookmarked id before diffing against the current view. Since NIP-51 kind-10003 is a single replaceable event, itsNoteKeyonly changes when the list is actually republished — caching the last-seen bookmarks-note key and short-circuiting the rest of the function when it's unchanged would avoid the O(n) per-bookmark lookups on every render pass.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/notedeck_columns/src/timeline/mod.rs` around lines 663 - 709, Cache the last-seen bookmarks kind-10003 NoteKey in the timeline state and update rebuild_bookmarks_view to compare the queried note’s key before rebuilding the bookmark-to-NoteRef mapping. Return early when the key is unchanged, while preserving the existing lookup and view-reset behavior when the bookmark list is newly published or changed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/notedeck_columns/src/timeline/kind.rs`:
- Around line 1006-1027: Update bookmarks_filter_state so it returns
FilterState::Broken(FilterError::EmptyList) when the collected ids vector is
empty. Only construct and return the ready Filter with kinds, ids, and limit
when at least one bookmark ID exists, preserving the existing query-error
behavior.
In `@crates/notedeck_columns/src/timeline/mod.rs`:
- Around line 672-677: Update the bookmarks query flow around list_filter and
ndb.query to handle errors explicitly instead of calling .ok() and silently
defaulting to empty bookmarks. Log the query failure with error! consistently
with bookmarks_filter_state in kind.rs, while preserving the existing
successful-result mapping and default fallback behavior.
In `@crates/notedeck/src/account/bookmarks.rs`:
- Around line 62-72: The poll_for_updates flow should refresh bookmark state
from the canonical current kind-10003 event after detecting a notification,
rather than harvesting the potentially stale nks result directly. Re-query using
the selected account’s existing filter, then pass that current event set to
harvest_nip51_bookmarks before replacing self.bookmarks; preserve the early
return when no notification is received.
- Around line 28-43: Update Bookmarks::query so an error from ndb.query is
logged and returns without replacing self.bookmarks, preserving the existing
snapshot or default initial value; retain the current mapping and harvest flow
for successful queries.
In `@crates/notedeck/src/note/publish.rs`:
- Around line 287-291: The bookmark publishing path around the existing_note
lookup must preserve database errors instead of converting them to an empty
list: explicitly match query and note-read results, log failures, and return
before publishing. Apply the same failure-versus-missing distinction in the
unbookmark path at crates/notedeck/src/note/publish.rs#L235-L239 so errors are
not reported as an absent list; the existing missing-list behavior should remain
unchanged.
---
Nitpick comments:
In `@crates/notedeck_columns/src/timeline/kind.rs`:
- Around line 992-999: Centralize the kind-10003 bookmarks-list filter
construction in a shared helper alongside bookmarks_from_note, then update
bookmarks_note_filter and the matching constructions in the account/bookmarks
and publish bookmark flows to call it. Preserve the existing authors, kind, and
limit behavior so all four call sites remain synchronized.
In `@crates/notedeck_columns/src/timeline/mod.rs`:
- Around line 663-709: Cache the last-seen bookmarks kind-10003 NoteKey in the
timeline state and update rebuild_bookmarks_view to compare the queried note’s
key before rebuilding the bookmark-to-NoteRef mapping. Return early when the key
is unchanged, while preserving the existing lookup and view-reset behavior when
the bookmark list is newly published or changed.
In `@crates/notedeck/src/bookmarks.rs`:
- Around line 24-26: crates/notedeck/src/bookmarks.rs:24-26 — Add Rustdoc to
Bookmarks::is_bookmarked describing note-ID membership semantics.
crates/notedeck/src/account/bookmarks.rs:8-15 — Document the account bookmark
cache and its constructor; 28 — document initial loading behavior; 46-50 —
document harvesting and current-event assumptions; 62-63 — document polling and
replacement behavior. crates/notedeck/src/account/accounts.rs:332-335 — Document
Accounts::bookmarks ownership and the snapshot it returns.
In `@crates/notedeck/src/note/publish.rs`:
- Around line 211-218: Document all modified Rust symbols: add Rustdoc
describing send_unbookmark_event and send_bookmark_event, document the
BookmarkNote type and bookmark-toggle behavior in process_selection, describe
menu and its bookmark-state parameter, and document the changed show_standard
behavior. Apply these updates at crates/notedeck/src/note/publish.rs lines
211-218 and 265-272, crates/notedeck/src/note/context.rs lines 17-27 and
129-154, crates/notedeck_ui/src/note/context.rs lines 65-73, and
crates/notedeck_ui/src/note/mod.rs lines 661-674.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0d56b483-1909-4a08-ba40-9792f272d734
📒 Files selected for processing (16)
crates/notedeck/src/account/accounts.rscrates/notedeck/src/account/bookmarks.rscrates/notedeck/src/account/mod.rscrates/notedeck/src/bookmarks.rscrates/notedeck/src/lib.rscrates/notedeck/src/note/context.rscrates/notedeck/src/note/mod.rscrates/notedeck/src/note/publish.rscrates/notedeck_columns/src/app.rscrates/notedeck_columns/src/timeline/kind.rscrates/notedeck_columns/src/timeline/mod.rscrates/notedeck_columns/src/timeline/route.rscrates/notedeck_columns/src/ui/add_column.rscrates/notedeck_columns/src/ui/column/header.rscrates/notedeck_ui/src/note/context.rscrates/notedeck_ui/src/note/mod.rs
| fn bookmarks_filter_state(txn: &Transaction, ndb: &Ndb, pk: &Pubkey) -> FilterState { | ||
| let list_filter = bookmarks_note_filter(pk); | ||
|
|
||
| let results = match ndb.query(txn, std::slice::from_ref(&list_filter), 1) { | ||
| Ok(results) => results, | ||
| Err(err) => { | ||
| error!("bookmarks list query failed: {err}"); | ||
| return FilterState::Broken(FilterError::EmptyList); | ||
| } | ||
| }; | ||
|
|
||
| let ids: Vec<[u8; 32]> = results | ||
| .first() | ||
| .map(|qr| bookmarks_from_note(&qr.note).note_ids.into_iter().collect()) | ||
| .unwrap_or_default(); | ||
|
|
||
| FilterState::ready(vec![Filter::new() | ||
| .kinds([1]) | ||
| .ids(ids.iter()) | ||
| .limit(default_limit()) | ||
| .build()]) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Handle the empty bookmark list before building the filter.
When bookmarks_filter_state() gets no bookmarks, ids is empty but the code still builds Filter::new().kinds([1]).ids(ids.iter()).... Build the ready filter only when there are bookmark IDs; otherwise return FilterState::Broken(FilterError::EmptyList) to avoid producing a list-filter edge case for the Bookmarks column.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/notedeck_columns/src/timeline/kind.rs` around lines 1006 - 1027,
Update bookmarks_filter_state so it returns
FilterState::Broken(FilterError::EmptyList) when the collected ids vector is
empty. Only construct and return the ready Filter with kinds, ids, and limit
when at least one bookmark ID exists, preserving the existing query-error
behavior.
| let list_filter = bookmarks_note_filter(pk); | ||
| let bookmarks = ndb | ||
| .query(txn, std::slice::from_ref(&list_filter), 1) | ||
| .ok() | ||
| .and_then(|results| results.first().map(|qr| bookmarks_from_note(&qr.note))) | ||
| .unwrap_or_default(); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Bookmarks-note query errors are silently swallowed.
.ok() discards any ndb.query error and falls back to an empty Bookmarks, which would blank the bookmarks view on a transient failure with no trace. The sibling bookmarks_filter_state in kind.rs logs via error! on the same kind of failure — consider doing the same here for consistency and debuggability.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/notedeck_columns/src/timeline/mod.rs` around lines 672 - 677, Update
the bookmarks query flow around list_filter and ndb.query to handle errors
explicitly instead of calling .ok() and silently defaulting to empty bookmarks.
Log the query failure with error! consistently with bookmarks_filter_state in
kind.rs, while preserving the existing successful-result mapping and default
fallback behavior.
| pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) { | ||
| // Query the ndb immediately to see if the user's bookmarks list is already there | ||
| let lim = self | ||
| .filter | ||
| .limit() | ||
| .unwrap_or(crate::filter::default_limit()) as i32; | ||
| let nks = ndb | ||
| .query(txn, std::slice::from_ref(&self.filter), lim) | ||
| .expect("query user bookmarks results") | ||
| .iter() | ||
| .map(|qr| qr.note_key) | ||
| .collect::<Vec<NoteKey>>(); | ||
| let bookmarks = Self::harvest_nip51_bookmarks(ndb, txn, &nks); | ||
| debug!("initial bookmarks {:?}", bookmarks); | ||
|
|
||
| self.bookmarks = Arc::new(bookmarks); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Do not crash on a bookmark-list query failure.
Line 36 panics during startup or account switching if ndb.query fails. Log the error and retain the existing snapshot (or the default initial value) instead.
Proposed fix
-use tracing::debug;
+use tracing::{debug, error};
- let nks = ndb
- .query(txn, std::slice::from_ref(&self.filter), lim)
- .expect("query user bookmarks results")
+ let nks = match ndb.query(txn, std::slice::from_ref(&self.filter), lim) {
+ Ok(results) => results,
+ Err(err) => {
+ error!("query user bookmarks failed: {err}");
+ return;
+ }
+ }
.iter()
.map(|qr| qr.note_key)
.collect::<Vec<NoteKey>>();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) { | |
| // Query the ndb immediately to see if the user's bookmarks list is already there | |
| let lim = self | |
| .filter | |
| .limit() | |
| .unwrap_or(crate::filter::default_limit()) as i32; | |
| let nks = ndb | |
| .query(txn, std::slice::from_ref(&self.filter), lim) | |
| .expect("query user bookmarks results") | |
| .iter() | |
| .map(|qr| qr.note_key) | |
| .collect::<Vec<NoteKey>>(); | |
| let bookmarks = Self::harvest_nip51_bookmarks(ndb, txn, &nks); | |
| debug!("initial bookmarks {:?}", bookmarks); | |
| self.bookmarks = Arc::new(bookmarks); | |
| use tracing::{debug, error}; | |
| pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) { | |
| // Query the ndb immediately to see if the user's bookmarks list is already there | |
| let lim = self | |
| .filter | |
| .limit() | |
| .unwrap_or(crate::filter::default_limit()) as i32; | |
| let nks = match ndb.query(txn, std::slice::from_ref(&self.filter), lim) { | |
| Ok(results) => results, | |
| Err(err) => { | |
| error!("query user bookmarks failed: {err}"); | |
| return; | |
| } | |
| } | |
| .iter() | |
| .map(|qr| qr.note_key) | |
| .collect::<Vec<NoteKey>>(); | |
| let bookmarks = Self::harvest_nip51_bookmarks(ndb, txn, &nks); | |
| debug!("initial bookmarks {:?}", bookmarks); | |
| self.bookmarks = Arc::new(bookmarks); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/notedeck/src/account/bookmarks.rs` around lines 28 - 43, Update
Bookmarks::query so an error from ndb.query is logged and returns without
replacing self.bookmarks, preserving the existing snapshot or default initial
value; retain the current mapping and harvest flow for successful queries.
| #[profiling::function] | ||
| pub(super) fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) { | ||
| let nks = ndb.poll_for_notes(sub, 1); | ||
|
|
||
| if nks.is_empty() { | ||
| return; | ||
| } | ||
|
|
||
| let bookmarks = Self::harvest_nip51_bookmarks(ndb, txn, &nks); | ||
| debug!("updated bookmarks {:?}", bookmarks); | ||
| self.bookmarks = Arc::new(bookmarks); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Refresh from the canonical replaceable event after a notification.
Line 70 replaces state with whichever matching event was polled. A delayed older kind-10003 event can therefore regress the selected account’s bookmark state. After detecting an update, re-query the account filter and derive state from the current replaceable event rather than directly harvesting nks.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/notedeck/src/account/bookmarks.rs` around lines 62 - 72, The
poll_for_updates flow should refresh bookmark state from the canonical current
kind-10003 event after detecting a notification, rather than harvesting the
potentially stale nks result directly. Re-query using the selected account’s
existing filter, then pass that current event set to harvest_nip51_bookmarks
before replacing self.bookmarks; preserve the early return when no notification
is received.
| let existing_note = ndb | ||
| .query(txn, std::slice::from_ref(&filter), lim) | ||
| .ok() | ||
| .and_then(|results| results.first().map(|qr| qr.note_key)) | ||
| .and_then(|nk| ndb.get_note_by_key(txn, nk).ok()); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Do not treat database failures as an empty bookmark list.
ok() turns query/read failures into None. In the bookmark path, that enters the fresh-list branch and publishes a replacement kind-10003 event, discarding every existing bookmark. Match and log errors explicitly, then return without publishing.
crates/notedeck/src/note/publish.rs#L287-L291: distinguish an empty query from query/read failure before creating a new list.crates/notedeck/src/note/publish.rs#L235-L239: distinguish failure from a missing list so unbookmark errors are not reported as absence.
📍 Affects 1 file
crates/notedeck/src/note/publish.rs#L287-L291(this comment)crates/notedeck/src/note/publish.rs#L235-L239
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/notedeck/src/note/publish.rs` around lines 287 - 291, The bookmark
publishing path around the existing_note lookup must preserve database errors
instead of converting them to an empty list: explicitly match query and
note-read results, log failures, and return before publishing. Apply the same
failure-versus-missing distinction in the unbookmark path at
crates/notedeck/src/note/publish.rs#L235-L239 so errors are not reported as an
absent list; the existing missing-list behavior should remain unchanged.
Adds a "Bookmark Note"/"Remove Bookmark" option to the note 3-dot menu, backed by a NIP-51 kind-10003 bookmark list that mirrors the existing mute-list architecture (AccountBookmarksData, local+remote subscription, publish/retract helpers).
Also adds a standalone Bookmarks column that lists all bookmarked notes sorted most-recently-bookmarked-first. Since the standard Timeline insertion path always sorts by a note's own created_at, bookmarks use a bespoke rebuild step (Timeline::rebuild_bookmarks_view) that assigns a synthetic rank from bookmark order instead, only rebuilding the view when the bookmark list actually changes.
Progresses #226 (bookmark + bookmark column; "clear all bookmarks" confirmation flow not yet implemented).
Changelog-Added: Add a bookmark option to the note menu and a Bookmarks column to view saved notes
Summary by CodeRabbit