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())));