From 480888a1fc911b394969087278f1886690400509 Mon Sep 17 00:00:00 2001 From: Ulyssa Date: Tue, 20 Aug 2024 19:33:46 -0700 Subject: [PATCH] Add commands for viewing and clearing unreads (#332) --- docs/iamb.1 | 4 ++++ src/base.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/commands.rs | 29 +++++++++++++++++++++++++++++ src/main.rs | 12 ++++++++++++ src/windows/mod.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 135 insertions(+) diff --git a/docs/iamb.1 b/docs/iamb.1 index dcc8abb..38070a2 100644 --- a/docs/iamb.1 +++ b/docs/iamb.1 @@ -61,6 +61,8 @@ Log out of View a list of joined rooms. .It Sy ":spaces" View a list of joined spaces. +.It Sy ":unreads" +View a list of unread rooms. .It Sy ":welcome" View the startup Welcome window. .El @@ -95,6 +97,8 @@ React to the selected message with an Emoji. Redact the selected message. .It Sy ":reply" Reply to the selected message. +.It Sy ":unreads clear" +Mark all unread rooms as read. .It Sy ":unreact [shortcode]" Remove your reaction from the selected message. When no arguments are given, remove all of your reactions from the message. diff --git a/src/base.rs b/src/base.rs index 420d4fe..5c6bdb9 100644 --- a/src/base.rs +++ b/src/base.rs @@ -510,6 +510,9 @@ pub enum IambAction { /// Toggle the focus within the focused room. ToggleScrollbackFocus, + + /// Clear all unread messages. + ClearUnreads, } impl IambAction { @@ -546,6 +549,7 @@ impl From for IambAction { impl ApplicationAction for IambAction { fn is_edit_sequence(&self, _: &EditContext) -> SequenceStatus { match self { + IambAction::ClearUnreads => SequenceStatus::Break, IambAction::Homeserver(..) => SequenceStatus::Break, IambAction::Keys(..) => SequenceStatus::Break, IambAction::Message(..) => SequenceStatus::Break, @@ -560,6 +564,7 @@ impl ApplicationAction for IambAction { fn is_last_action(&self, _: &EditContext) -> SequenceStatus { match self { + IambAction::ClearUnreads => SequenceStatus::Atom, IambAction::Homeserver(..) => SequenceStatus::Atom, IambAction::Keys(..) => SequenceStatus::Atom, IambAction::Message(..) => SequenceStatus::Atom, @@ -574,6 +579,7 @@ impl ApplicationAction for IambAction { fn is_last_selection(&self, _: &EditContext) -> SequenceStatus { match self { + IambAction::ClearUnreads => SequenceStatus::Ignore, IambAction::Homeserver(..) => SequenceStatus::Ignore, IambAction::Keys(..) => SequenceStatus::Ignore, IambAction::Message(..) => SequenceStatus::Ignore, @@ -588,6 +594,7 @@ impl ApplicationAction for IambAction { fn is_switchable(&self, _: &EditContext) -> bool { match self { + IambAction::ClearUnreads => false, IambAction::Homeserver(..) => false, IambAction::Message(..) => false, IambAction::Room(..) => false, @@ -1148,6 +1155,14 @@ impl RoomInfo { self.user_receipts.insert(user_id, event_id); } + pub fn fully_read(&mut self, user_id: OwnedUserId) { + let Some(((_, event_id), _)) = self.messages.last_key_value() else { + return; + }; + + self.set_receipt(user_id, event_id.clone()); + } + pub fn get_receipt(&self, user_id: &UserId) -> Option<&OwnedEventId> { self.user_receipts.get(user_id) } @@ -1309,6 +1324,20 @@ pub struct SyncInfo { pub dms: Vec)>>, } +impl SyncInfo { + pub fn rooms(&self) -> impl Iterator { + self.rooms.iter().map(|r| r.0.room_id()) + } + + pub fn dms(&self) -> impl Iterator { + self.dms.iter().map(|r| r.0.room_id()) + } + + pub fn chats(&self) -> impl Iterator { + self.rooms().chain(self.dms()) + } +} + bitflags::bitflags! { /// Load-needs #[derive(Debug, Default, PartialEq)] @@ -1480,6 +1509,9 @@ pub enum IambId { /// The `:chats` window. ChatList, + + /// The `:unreads` window. + UnreadList, } impl Display for IambId { @@ -1500,6 +1532,7 @@ impl Display for IambId { IambId::VerifyList => f.write_str("iamb://verify"), IambId::Welcome => f.write_str("iamb://welcome"), IambId::ChatList => f.write_str("iamb://chats"), + IambId::UnreadList => f.write_str("iamb://unreads"), } } } @@ -1631,6 +1664,13 @@ impl<'de> Visitor<'de> for IambIdVisitor { Ok(IambId::ChatList) }, + Some("unreads") => { + if url.path() != "" { + return Err(E::custom("iamb://unreads takes no path")); + } + + Ok(IambId::UnreadList) + }, Some(s) => Err(E::custom(format!("{s:?} is not a valid window"))), None => Err(E::custom("Invalid iamb window URL")), } @@ -1691,6 +1731,9 @@ pub enum IambBufferId { /// The `:chats` window. ChatList, + + /// The `:unreads` window. + UnreadList, } impl IambBufferId { @@ -1706,6 +1749,7 @@ impl IambBufferId { IambBufferId::VerifyList => IambId::VerifyList, IambBufferId::Welcome => IambId::Welcome, IambBufferId::ChatList => IambId::ChatList, + IambBufferId::UnreadList => IambId::UnreadList, }; Some(id) @@ -1740,6 +1784,7 @@ impl ApplicationInfo for IambInfo { IambBufferId::VerifyList => vec![], IambBufferId::Welcome => vec![], IambBufferId::ChatList => vec![], + IambBufferId::UnreadList => vec![], } } diff --git a/src/commands.rs b/src/commands.rs index 913e5f6..4335eb7 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -307,6 +307,30 @@ fn iamb_chats(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { return Ok(step); } +fn iamb_unreads(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + let mut args = desc.arg.strings()?; + + if args.len() > 1 { + return Result::Err(CommandError::InvalidArgument); + } + + match args.pop().as_deref() { + Some("clear") => { + let clear = IambAction::ClearUnreads; + let step = CommandStep::Continue(clear.into(), ctx.context.clone()); + + return Ok(step); + }, + Some(_) => return Result::Err(CommandError::InvalidArgument), + None => { + let open = ctx.switch(OpenTarget::Application(IambId::UnreadList)); + let step = CommandStep::Continue(open, ctx.context.clone()); + + return Ok(step); + }, + } +} + fn iamb_spaces(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { if !desc.arg.text.is_empty() { return Result::Err(CommandError::InvalidArgument); @@ -648,6 +672,11 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) { aliases: vec![], f: iamb_spaces, }); + cmds.add_command(ProgramCommand { + name: "unreads".into(), + aliases: vec![], + f: iamb_unreads, + }); cmds.add_command(ProgramCommand { name: "unreact".into(), aliases: vec![], diff --git a/src/main.rs b/src/main.rs index 02bdc4c..d0b265c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -529,6 +529,18 @@ impl Application { } let info = match action { + IambAction::ClearUnreads => { + let user_id = &store.application.settings.profile.user_id; + + for room_id in store.application.sync_info.chats() { + if let Some(room) = store.application.rooms.get_mut(room_id) { + room.fully_read(user_id.clone()); + } + } + + None + }, + IambAction::ToggleScrollbackFocus => { self.screen.current_window_mut()?.focus_toggle(); diff --git a/src/windows/mod.rs b/src/windows/mod.rs index eba9675..c7882be 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -315,6 +315,7 @@ macro_rules! delegate { IambWindow::VerifyList($id) => $e, IambWindow::Welcome($id) => $e, IambWindow::ChatList($id) => $e, + IambWindow::UnreadList($id) => $e, } }; } @@ -328,6 +329,7 @@ pub enum IambWindow { SpaceList(SpaceListState), Welcome(WelcomeState), ChatList(ChatListState), + UnreadList(UnreadListState), } impl IambWindow { @@ -383,6 +385,7 @@ pub type DirectListState = ListState; pub type MemberListState = ListState; pub type RoomListState = ListState; pub type ChatListState = ListState; +pub type UnreadListState = ListState; pub type SpaceListState = ListState; pub type VerifyListState = ListState; @@ -579,6 +582,39 @@ impl WindowOps for IambWindow { .focus(focused) .render(area, buf, state); }, + IambWindow::UnreadList(state) => { + let mut items = store + .application + .sync_info + .rooms + .clone() + .into_iter() + .map(|room_info| GenericChatItem::new(room_info, store, false)) + .filter(RoomLikeItem::is_unread) + .collect::>(); + + let dms = store + .application + .sync_info + .dms + .clone() + .into_iter() + .map(|room_info| GenericChatItem::new(room_info, store, true)) + .filter(RoomLikeItem::is_unread); + + items.extend(dms); + + let fields = &store.application.settings.tunables.sort.chats; + items.sort_by(|a, b| room_fields_cmp(a, b, fields)); + + state.set(items); + + List::new(store) + .empty_message("You do not have rooms or dms yet") + .empty_alignment(Alignment::Center) + .focus(focused) + .render(area, buf, state); + }, IambWindow::SpaceList(state) => { let mut items = store .application @@ -630,6 +666,7 @@ impl WindowOps for IambWindow { IambWindow::VerifyList(w) => w.dup(store).into(), IambWindow::Welcome(w) => w.dup(store).into(), IambWindow::ChatList(w) => w.dup(store).into(), + IambWindow::UnreadList(w) => w.dup(store).into(), } } @@ -670,6 +707,7 @@ impl Window for IambWindow { IambWindow::VerifyList(_) => IambId::VerifyList, IambWindow::Welcome(_) => IambId::Welcome, IambWindow::ChatList(_) => IambId::ChatList, + IambWindow::UnreadList(_) => IambId::UnreadList, } } @@ -681,6 +719,7 @@ impl Window for IambWindow { IambWindow::VerifyList(_) => bold_spans("Verifications"), IambWindow::Welcome(_) => bold_spans("Welcome to iamb"), IambWindow::ChatList(_) => bold_spans("DMs & Rooms"), + IambWindow::UnreadList(_) => bold_spans("Unread Messages"), IambWindow::Room(w) => { let title = store.application.get_room_title(w.id()); @@ -708,6 +747,7 @@ impl Window for IambWindow { IambWindow::VerifyList(_) => bold_spans("Verifications"), IambWindow::Welcome(_) => bold_spans("Welcome to iamb"), IambWindow::ChatList(_) => bold_spans("DMs & Rooms"), + IambWindow::UnreadList(_) => bold_spans("Unread Messages"), IambWindow::Room(w) => w.get_title(store), IambWindow::MemberList(state, room_id, _) => { @@ -769,6 +809,11 @@ impl Window for IambWindow { Ok(list.into()) }, + IambId::UnreadList => { + let list = UnreadListState::new(IambBufferId::UnreadList, vec![]); + + Ok(IambWindow::UnreadList(list)) + }, } }