From 549549b097928ce56e2cc8b8e07ba3530d8e418b Mon Sep 17 00:00:00 2001 From: clover caruso Date: Sat, 10 Oct 2026 19:19:21 -0700 Subject: [PATCH] fix: offer targeted mark-as-read actions in context menus Expose the existing Mark as Read title and artwork on page and section context menus and the target command palette. Mark only the clicked target, without opening another page, and keep the action idempotent. Assert both menus expose the action in the accessibility replay. Fixes #106 Assisted-by: gpt-6.1-sol --- crates/snowbound/src/menus.rs | 42 ++++++++++++++++++++++++++++++++ crates/snowbound/tests/replay.rs | 3 +++ 2 files changed, 45 insertions(+) diff --git a/crates/snowbound/src/menus.rs b/crates/snowbound/src/menus.rs index b226bf0e5a51cbe0cbdc18bc4164bcdae8b98b1e..ee15575d8abafad1e07a01e50bf542b2e7805f21 100644 --- a/crates/snowbound/src/menus.rs +++ b/crates/snowbound/src/menus.rs @@ -119,6 +119,7 @@ pub enum Action { /// Shows the notebook's recycle bin. RecycleBin, EmptyRecycleBin, + MarkRead, MarkNotebookRead, CopyLink, NewPage, @@ -162,6 +163,7 @@ impl Action { Action::SaveAs => art::SAVE, Action::RecycleBin => art::RECYCLE_BIN, Action::EmptyRecycleBin => art::EMPTY_RECYCLE_BIN, + Action::MarkRead => art::MARK_READ, Action::MarkNotebookRead => art::MARK_NOTEBOOK_READ, Action::CopyLink => art::COPY_LINK, Action::NewPage => art::NEW_PAGE, @@ -456,11 +458,21 @@ impl State { path, space, } => { + let unread = library + .section_identity(path) + .and_then(|section| self.reads.pages(&library.location, section)) + .is_some_and(|pages| pages.contains(space)); let mut actions = vec![ item(Action::Rename, "Rename", false, false), item(Action::Delete, "Delete", false, false), item(Action::Move, "Move to Section", nowhere(), false), item(Action::CopyLink, "Copy Link to Page", false, true), + item( + Action::MarkRead, + commands::command(commands::Id::MarkRead).title, + !unread, + true, + ), item(Action::NewPage, "New Page", false, true), // OneNote offers this beside New Page, which is a plain + here. item(Action::NewSubpage, "New Subpage", false, false), @@ -521,6 +533,16 @@ impl State { if section { let link = self.section_link(library, path).is_none(); actions.push(item(Action::CopyLink, "Copy Link to Section", link, true)); + let unread = library + .section_identity(path) + .and_then(|section| self.reads.pages(&library.location, section)) + .is_some_and(|pages| !pages.is_empty()); + actions.push(item( + Action::MarkRead, + commands::command(commands::Id::MarkRead).title, + !unread, + true, + )); } if section { let [mut first, rest] = new; @@ -807,6 +829,26 @@ impl State { } (Target::NewPage(title), _) => Some(Command::NewPage { under: None, title }), (Target::Text(text), _) => Some(Command::Text(text)), + ( + Target::Page { + library, + path, + space, + }, + Action::MarkRead, + ) => { + if let Some(section) = library.section_identity(&path) { + self.reads + .mark_read(&library.location, section, Some(space)); + } + None + } + (Target::Section { library, path }, Action::MarkRead) => { + if let Some(section) = library.section_identity(&path) { + self.reads.mark_read(&library.location, section, None); + } + None + } ( Target::Page { library, diff --git a/crates/snowbound/tests/replay.rs b/crates/snowbound/tests/replay.rs index 6adb9217a1f2f672972d57a2c0170bd20d9538d3..723fa558140a8d827c6c53b3bfa11caeefa726a0 100644 --- a/crates/snowbound/tests/replay.rs +++ b/crates/snowbound/tests/replay.rs @@ -261,6 +261,9 @@ fn every_context_menu_action_is_a_palette_command() { for (noun, tree) in ["", "page", "section", "notebook"].into_iter().zip(&menus) { let items = menu_items(tree); assert!(items.len() > 4, "{tree}"); + if matches!(noun, "page" | "section") { + assert!(items.contains(&"Mark as Read"), "{tree}"); + } // The sync status heads the notebook's. let skip = usize::from(noun == "notebook"); labels.extend(items[skip..].iter().map(|label| (noun, label.to_string()))); -- 2.54.0