Add commands for viewing and clearing unreads (#332)

This commit is contained in:
Ulyssa 2024-08-20 19:33:46 -07:00 committed by GitHub
parent 4fc05c7b40
commit 480888a1fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 135 additions and 0 deletions

View file

@ -61,6 +61,8 @@ Log out of
View a list of joined rooms. View a list of joined rooms.
.It Sy ":spaces" .It Sy ":spaces"
View a list of joined spaces. View a list of joined spaces.
.It Sy ":unreads"
View a list of unread rooms.
.It Sy ":welcome" .It Sy ":welcome"
View the startup Welcome window. View the startup Welcome window.
.El .El
@ -95,6 +97,8 @@ React to the selected message with an Emoji.
Redact the selected message. Redact the selected message.
.It Sy ":reply" .It Sy ":reply"
Reply to the selected message. Reply to the selected message.
.It Sy ":unreads clear"
Mark all unread rooms as read.
.It Sy ":unreact [shortcode]" .It Sy ":unreact [shortcode]"
Remove your reaction from the selected message. Remove your reaction from the selected message.
When no arguments are given, remove all of your reactions from the message. When no arguments are given, remove all of your reactions from the message.

View file

@ -510,6 +510,9 @@ pub enum IambAction {
/// Toggle the focus within the focused room. /// Toggle the focus within the focused room.
ToggleScrollbackFocus, ToggleScrollbackFocus,
/// Clear all unread messages.
ClearUnreads,
} }
impl IambAction { impl IambAction {
@ -546,6 +549,7 @@ impl From<SendAction> for IambAction {
impl ApplicationAction for IambAction { impl ApplicationAction for IambAction {
fn is_edit_sequence(&self, _: &EditContext) -> SequenceStatus { fn is_edit_sequence(&self, _: &EditContext) -> SequenceStatus {
match self { match self {
IambAction::ClearUnreads => SequenceStatus::Break,
IambAction::Homeserver(..) => SequenceStatus::Break, IambAction::Homeserver(..) => SequenceStatus::Break,
IambAction::Keys(..) => SequenceStatus::Break, IambAction::Keys(..) => SequenceStatus::Break,
IambAction::Message(..) => SequenceStatus::Break, IambAction::Message(..) => SequenceStatus::Break,
@ -560,6 +564,7 @@ impl ApplicationAction for IambAction {
fn is_last_action(&self, _: &EditContext) -> SequenceStatus { fn is_last_action(&self, _: &EditContext) -> SequenceStatus {
match self { match self {
IambAction::ClearUnreads => SequenceStatus::Atom,
IambAction::Homeserver(..) => SequenceStatus::Atom, IambAction::Homeserver(..) => SequenceStatus::Atom,
IambAction::Keys(..) => SequenceStatus::Atom, IambAction::Keys(..) => SequenceStatus::Atom,
IambAction::Message(..) => SequenceStatus::Atom, IambAction::Message(..) => SequenceStatus::Atom,
@ -574,6 +579,7 @@ impl ApplicationAction for IambAction {
fn is_last_selection(&self, _: &EditContext) -> SequenceStatus { fn is_last_selection(&self, _: &EditContext) -> SequenceStatus {
match self { match self {
IambAction::ClearUnreads => SequenceStatus::Ignore,
IambAction::Homeserver(..) => SequenceStatus::Ignore, IambAction::Homeserver(..) => SequenceStatus::Ignore,
IambAction::Keys(..) => SequenceStatus::Ignore, IambAction::Keys(..) => SequenceStatus::Ignore,
IambAction::Message(..) => SequenceStatus::Ignore, IambAction::Message(..) => SequenceStatus::Ignore,
@ -588,6 +594,7 @@ impl ApplicationAction for IambAction {
fn is_switchable(&self, _: &EditContext) -> bool { fn is_switchable(&self, _: &EditContext) -> bool {
match self { match self {
IambAction::ClearUnreads => false,
IambAction::Homeserver(..) => false, IambAction::Homeserver(..) => false,
IambAction::Message(..) => false, IambAction::Message(..) => false,
IambAction::Room(..) => false, IambAction::Room(..) => false,
@ -1148,6 +1155,14 @@ impl RoomInfo {
self.user_receipts.insert(user_id, event_id); 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> { pub fn get_receipt(&self, user_id: &UserId) -> Option<&OwnedEventId> {
self.user_receipts.get(user_id) self.user_receipts.get(user_id)
} }
@ -1309,6 +1324,20 @@ pub struct SyncInfo {
pub dms: Vec<Arc<(MatrixRoom, Option<Tags>)>>, pub dms: Vec<Arc<(MatrixRoom, Option<Tags>)>>,
} }
impl SyncInfo {
pub fn rooms(&self) -> impl Iterator<Item = &RoomId> {
self.rooms.iter().map(|r| r.0.room_id())
}
pub fn dms(&self) -> impl Iterator<Item = &RoomId> {
self.dms.iter().map(|r| r.0.room_id())
}
pub fn chats(&self) -> impl Iterator<Item = &RoomId> {
self.rooms().chain(self.dms())
}
}
bitflags::bitflags! { bitflags::bitflags! {
/// Load-needs /// Load-needs
#[derive(Debug, Default, PartialEq)] #[derive(Debug, Default, PartialEq)]
@ -1480,6 +1509,9 @@ pub enum IambId {
/// The `:chats` window. /// The `:chats` window.
ChatList, ChatList,
/// The `:unreads` window.
UnreadList,
} }
impl Display for IambId { impl Display for IambId {
@ -1500,6 +1532,7 @@ impl Display for IambId {
IambId::VerifyList => f.write_str("iamb://verify"), IambId::VerifyList => f.write_str("iamb://verify"),
IambId::Welcome => f.write_str("iamb://welcome"), IambId::Welcome => f.write_str("iamb://welcome"),
IambId::ChatList => f.write_str("iamb://chats"), 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) 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"))), Some(s) => Err(E::custom(format!("{s:?} is not a valid window"))),
None => Err(E::custom("Invalid iamb window URL")), None => Err(E::custom("Invalid iamb window URL")),
} }
@ -1691,6 +1731,9 @@ pub enum IambBufferId {
/// The `:chats` window. /// The `:chats` window.
ChatList, ChatList,
/// The `:unreads` window.
UnreadList,
} }
impl IambBufferId { impl IambBufferId {
@ -1706,6 +1749,7 @@ impl IambBufferId {
IambBufferId::VerifyList => IambId::VerifyList, IambBufferId::VerifyList => IambId::VerifyList,
IambBufferId::Welcome => IambId::Welcome, IambBufferId::Welcome => IambId::Welcome,
IambBufferId::ChatList => IambId::ChatList, IambBufferId::ChatList => IambId::ChatList,
IambBufferId::UnreadList => IambId::UnreadList,
}; };
Some(id) Some(id)
@ -1740,6 +1784,7 @@ impl ApplicationInfo for IambInfo {
IambBufferId::VerifyList => vec![], IambBufferId::VerifyList => vec![],
IambBufferId::Welcome => vec![], IambBufferId::Welcome => vec![],
IambBufferId::ChatList => vec![], IambBufferId::ChatList => vec![],
IambBufferId::UnreadList => vec![],
} }
} }

View file

@ -307,6 +307,30 @@ fn iamb_chats(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
return Ok(step); 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 { fn iamb_spaces(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
if !desc.arg.text.is_empty() { if !desc.arg.text.is_empty() {
return Result::Err(CommandError::InvalidArgument); return Result::Err(CommandError::InvalidArgument);
@ -648,6 +672,11 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
aliases: vec![], aliases: vec![],
f: iamb_spaces, f: iamb_spaces,
}); });
cmds.add_command(ProgramCommand {
name: "unreads".into(),
aliases: vec![],
f: iamb_unreads,
});
cmds.add_command(ProgramCommand { cmds.add_command(ProgramCommand {
name: "unreact".into(), name: "unreact".into(),
aliases: vec![], aliases: vec![],

View file

@ -529,6 +529,18 @@ impl Application {
} }
let info = match action { 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 => { IambAction::ToggleScrollbackFocus => {
self.screen.current_window_mut()?.focus_toggle(); self.screen.current_window_mut()?.focus_toggle();

View file

@ -315,6 +315,7 @@ macro_rules! delegate {
IambWindow::VerifyList($id) => $e, IambWindow::VerifyList($id) => $e,
IambWindow::Welcome($id) => $e, IambWindow::Welcome($id) => $e,
IambWindow::ChatList($id) => $e, IambWindow::ChatList($id) => $e,
IambWindow::UnreadList($id) => $e,
} }
}; };
} }
@ -328,6 +329,7 @@ pub enum IambWindow {
SpaceList(SpaceListState), SpaceList(SpaceListState),
Welcome(WelcomeState), Welcome(WelcomeState),
ChatList(ChatListState), ChatList(ChatListState),
UnreadList(UnreadListState),
} }
impl IambWindow { impl IambWindow {
@ -383,6 +385,7 @@ pub type DirectListState = ListState<DirectItem, IambInfo>;
pub type MemberListState = ListState<MemberItem, IambInfo>; pub type MemberListState = ListState<MemberItem, IambInfo>;
pub type RoomListState = ListState<RoomItem, IambInfo>; pub type RoomListState = ListState<RoomItem, IambInfo>;
pub type ChatListState = ListState<GenericChatItem, IambInfo>; pub type ChatListState = ListState<GenericChatItem, IambInfo>;
pub type UnreadListState = ListState<GenericChatItem, IambInfo>;
pub type SpaceListState = ListState<SpaceItem, IambInfo>; pub type SpaceListState = ListState<SpaceItem, IambInfo>;
pub type VerifyListState = ListState<VerifyItem, IambInfo>; pub type VerifyListState = ListState<VerifyItem, IambInfo>;
@ -579,6 +582,39 @@ impl WindowOps<IambInfo> for IambWindow {
.focus(focused) .focus(focused)
.render(area, buf, state); .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::<Vec<_>>();
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) => { IambWindow::SpaceList(state) => {
let mut items = store let mut items = store
.application .application
@ -630,6 +666,7 @@ impl WindowOps<IambInfo> for IambWindow {
IambWindow::VerifyList(w) => w.dup(store).into(), IambWindow::VerifyList(w) => w.dup(store).into(),
IambWindow::Welcome(w) => w.dup(store).into(), IambWindow::Welcome(w) => w.dup(store).into(),
IambWindow::ChatList(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<IambInfo> for IambWindow {
IambWindow::VerifyList(_) => IambId::VerifyList, IambWindow::VerifyList(_) => IambId::VerifyList,
IambWindow::Welcome(_) => IambId::Welcome, IambWindow::Welcome(_) => IambId::Welcome,
IambWindow::ChatList(_) => IambId::ChatList, IambWindow::ChatList(_) => IambId::ChatList,
IambWindow::UnreadList(_) => IambId::UnreadList,
} }
} }
@ -681,6 +719,7 @@ impl Window<IambInfo> for IambWindow {
IambWindow::VerifyList(_) => bold_spans("Verifications"), IambWindow::VerifyList(_) => bold_spans("Verifications"),
IambWindow::Welcome(_) => bold_spans("Welcome to iamb"), IambWindow::Welcome(_) => bold_spans("Welcome to iamb"),
IambWindow::ChatList(_) => bold_spans("DMs & Rooms"), IambWindow::ChatList(_) => bold_spans("DMs & Rooms"),
IambWindow::UnreadList(_) => bold_spans("Unread Messages"),
IambWindow::Room(w) => { IambWindow::Room(w) => {
let title = store.application.get_room_title(w.id()); let title = store.application.get_room_title(w.id());
@ -708,6 +747,7 @@ impl Window<IambInfo> for IambWindow {
IambWindow::VerifyList(_) => bold_spans("Verifications"), IambWindow::VerifyList(_) => bold_spans("Verifications"),
IambWindow::Welcome(_) => bold_spans("Welcome to iamb"), IambWindow::Welcome(_) => bold_spans("Welcome to iamb"),
IambWindow::ChatList(_) => bold_spans("DMs & Rooms"), IambWindow::ChatList(_) => bold_spans("DMs & Rooms"),
IambWindow::UnreadList(_) => bold_spans("Unread Messages"),
IambWindow::Room(w) => w.get_title(store), IambWindow::Room(w) => w.get_title(store),
IambWindow::MemberList(state, room_id, _) => { IambWindow::MemberList(state, room_id, _) => {
@ -769,6 +809,11 @@ impl Window<IambInfo> for IambWindow {
Ok(list.into()) Ok(list.into())
}, },
IambId::UnreadList => {
let list = UnreadListState::new(IambBufferId::UnreadList, vec![]);
Ok(IambWindow::UnreadList(list))
},
} }
} }