From b7ae01499b1dd84044b837ccedabc99ef0acfb31 Mon Sep 17 00:00:00 2001 From: FormindVER Date: Tue, 27 Feb 2024 18:36:09 -0800 Subject: [PATCH] Add a new `:chats` window that lists both DMs and Rooms (#184) Fixes #172 --- src/base.rs | 19 ++++- src/commands.rs | 16 ++++ src/config.rs | 6 +- src/message/mod.rs | 4 +- src/windows/mod.rs | 182 ++++++++++++++++++++++++++++++++++++++--- src/windows/welcome.md | 1 + src/worker.rs | 2 +- 7 files changed, 211 insertions(+), 19 deletions(-) diff --git a/src/base.rs b/src/base.rs index cfc1eac..0da9438 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1172,7 +1172,7 @@ pub enum IambId { /// A Matrix room. Room(OwnedRoomId), - /// The `:rooms` window. + /// The `:dms` window. DirectList, /// The `:members` window for a given Matrix room. @@ -1189,6 +1189,9 @@ pub enum IambId { /// The `:welcome` window. Welcome, + + /// The `:chats` window. + ChatList, } impl Display for IambId { @@ -1205,6 +1208,7 @@ impl Display for IambId { IambId::SpaceList => f.write_str("iamb://spaces"), IambId::VerifyList => f.write_str("iamb://verify"), IambId::Welcome => f.write_str("iamb://welcome"), + IambId::ChatList => f.write_str("iamb://chats"), } } } @@ -1317,6 +1321,13 @@ impl<'de> Visitor<'de> for IambIdVisitor { Ok(IambId::Welcome) }, + Some("chats") => { + if url.path() != "" { + return Err(E::custom("iamb://chats takes no path")); + } + + Ok(IambId::ChatList) + }, Some(s) => Err(E::custom(format!("{s:?} is not a valid window"))), None => Err(E::custom("Invalid iamb window URL")), } @@ -1374,6 +1385,9 @@ pub enum IambBufferId { /// The buffer for the `:rooms` window. Welcome, + + /// The `:chats` window. + ChatList, } impl IambBufferId { @@ -1388,6 +1402,7 @@ impl IambBufferId { IambBufferId::SpaceList => Some(IambId::SpaceList), IambBufferId::VerifyList => Some(IambId::VerifyList), IambBufferId::Welcome => Some(IambId::Welcome), + IambBufferId::ChatList => Some(IambId::ChatList), } } } @@ -1410,7 +1425,6 @@ impl ApplicationInfo for IambInfo { match content { IambBufferId::Command(CommandType::Command) => complete_cmdbar(text, cursor, store), IambBufferId::Command(CommandType::Search) => vec![], - IambBufferId::Room(_, RoomFocus::MessageBar) => complete_msgbar(text, cursor, store), IambBufferId::Room(_, RoomFocus::Scrollback) => vec![], @@ -1420,6 +1434,7 @@ impl ApplicationInfo for IambInfo { IambBufferId::SpaceList => vec![], IambBufferId::VerifyList => vec![], IambBufferId::Welcome => vec![], + IambBufferId::ChatList => vec![], } } diff --git a/src/commands.rs b/src/commands.rs index 6024239..b09f1f6 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -292,6 +292,17 @@ fn iamb_rooms(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { return Ok(step); } +fn iamb_chats(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + if !desc.arg.text.is_empty() { + return Result::Err(CommandError::InvalidArgument); + } + + let open = ctx.switch(OpenTarget::Application(IambId::ChatList)); + let step = CommandStep::Continue(open, ctx.context.take()); + + return Ok(step); +} + fn iamb_spaces(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { if !desc.arg.text.is_empty() { return Result::Err(CommandError::InvalidArgument); @@ -495,6 +506,11 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) { aliases: vec![], f: iamb_create, }); + cmds.add_command(ProgramCommand { + name: "chats".into(), + aliases: vec![], + f: iamb_chats, + }); cmds.add_command(ProgramCommand { name: "dms".into(), aliases: vec![], f: iamb_dms }); cmds.add_command(ProgramCommand { name: "download".into(), diff --git a/src/config.rs b/src/config.rs index 32c3d14..9349e92 100644 --- a/src/config.rs +++ b/src/config.rs @@ -227,6 +227,7 @@ pub type UserOverrides = HashMap; fn merge_sorts(a: SortOverrides, b: SortOverrides) -> SortOverrides { SortOverrides { + chats: b.chats.or(a.chats), dms: b.dms.or(a.dms), rooms: b.rooms.or(a.rooms), spaces: b.spaces.or(a.spaces), @@ -308,6 +309,7 @@ pub struct ImagePreviewProtocolValues { #[derive(Clone)] pub struct SortValues { + pub chats: Vec>, pub dms: Vec>, pub rooms: Vec>, pub spaces: Vec>, @@ -316,6 +318,7 @@ pub struct SortValues { #[derive(Clone, Default, Deserialize)] pub struct SortOverrides { + pub chats: Option>>, pub dms: Option>>, pub rooms: Option>>, pub spaces: Option>>, @@ -325,11 +328,12 @@ pub struct SortOverrides { impl SortOverrides { pub fn values(self) -> SortValues { let rooms = self.rooms.unwrap_or_else(|| Vec::from(DEFAULT_ROOM_SORT)); + let chats = self.chats.unwrap_or_else(|| rooms.clone()); let dms = self.dms.unwrap_or_else(|| rooms.clone()); let spaces = self.spaces.unwrap_or_else(|| rooms.clone()); let members = self.members.unwrap_or_else(|| Vec::from(DEFAULT_MEMBERS_SORT)); - SortValues { rooms, members, dms, spaces } + SortValues { rooms, members, chats, dms, spaces } } } diff --git a/src/message/mod.rs b/src/message/mod.rs index a58ed33..a343e89 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -196,7 +196,7 @@ impl Ord for MessageTimeStamp { impl PartialOrd for MessageTimeStamp { fn partial_cmp(&self, other: &Self) -> Option { - self.cmp(other).into() + Some(self.cmp(other)) } } @@ -340,7 +340,7 @@ impl Ord for MessageCursor { impl PartialOrd for MessageCursor { fn partial_cmp(&self, other: &Self) -> Option { - self.cmp(other).into() + Some(self.cmp(other)) } } diff --git a/src/windows/mod.rs b/src/windows/mod.rs index 73c7579..3b3fdd9 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -215,28 +215,34 @@ fn user_fields_cmp( user_cmp(a, b, &SortFieldUser::UserId) } -fn append_tags<'a>(tags: &'a Tags, spans: &mut Vec>, style: Style) { +fn tag_to_span(tag: &TagName, style: Style) -> Vec> { + match tag { + TagName::Favorite => vec![Span::styled("Favorite", style)], + TagName::LowPriority => vec![Span::styled("Low Priority", style)], + TagName::ServerNotice => vec![Span::styled("Server Notice", style)], + TagName::User(tag) => { + vec![ + Span::styled("User Tag: ", style), + Span::styled(tag.as_ref(), style), + ] + }, + tag => vec![Span::styled(format!("{tag:?}"), style)], + } +} + +fn append_tags<'a>(tags: Vec>>, spans: &mut Vec>, style: Style) { if tags.is_empty() { return; } spans.push(Span::styled(" (", style)); - for (i, tag) in tags.keys().enumerate() { + for (i, tag) in tags.into_iter().enumerate() { if i > 0 { spans.push(Span::styled(", ", style)); } - match tag { - TagName::Favorite => spans.push(Span::styled("Favorite", style)), - TagName::LowPriority => spans.push(Span::styled("Low Priority", style)), - TagName::ServerNotice => spans.push(Span::styled("Server Notice", style)), - TagName::User(tag) => { - spans.push(Span::styled("User Tag: ", style)); - spans.push(Span::styled(tag.as_ref(), style)); - }, - tag => spans.push(Span::styled(format!("{tag:?}"), style)), - } + spans.extend(tag); } spans.push(Span::styled(")", style)); @@ -289,6 +295,7 @@ macro_rules! delegate { IambWindow::SpaceList($id) => $e, IambWindow::VerifyList($id) => $e, IambWindow::Welcome($id) => $e, + IambWindow::ChatList($id) => $e, } }; } @@ -301,6 +308,7 @@ pub enum IambWindow { RoomList(RoomListState), SpaceList(SpaceListState), Welcome(WelcomeState), + ChatList(ChatListState), } impl IambWindow { @@ -355,9 +363,16 @@ impl IambWindow { pub type DirectListState = ListState; pub type MemberListState = ListState; pub type RoomListState = ListState; +pub type ChatListState = ListState; pub type SpaceListState = ListState; pub type VerifyListState = ListState; +impl From for IambWindow { + fn from(list: ChatListState) -> Self { + IambWindow::ChatList(list) + } +} + impl From for IambWindow { fn from(room: RoomState) -> Self { IambWindow::Room(room) @@ -514,6 +529,37 @@ impl WindowOps for IambWindow { .focus(focused) .render(area, buf, state); }, + IambWindow::ChatList(state) => { + let mut items = store + .application + .sync_info + .rooms + .clone() + .into_iter() + .map(|room_info| GenericChatItem::new(room_info, store, false)) + .collect::>(); + + let dms = store + .application + .sync_info + .dms + .clone() + .into_iter() + .map(|room_info| GenericChatItem::new(room_info, store, true)); + + 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 @@ -564,6 +610,7 @@ impl WindowOps for IambWindow { IambWindow::SpaceList(w) => w.dup(store).into(), IambWindow::VerifyList(w) => w.dup(store).into(), IambWindow::Welcome(w) => w.dup(store).into(), + IambWindow::ChatList(w) => w.dup(store).into(), } } @@ -603,6 +650,7 @@ impl Window for IambWindow { IambWindow::SpaceList(_) => IambId::SpaceList, IambWindow::VerifyList(_) => IambId::VerifyList, IambWindow::Welcome(_) => IambId::Welcome, + IambWindow::ChatList(_) => IambId::ChatList, } } @@ -613,6 +661,7 @@ impl Window for IambWindow { IambWindow::SpaceList(_) => bold_spans("Spaces"), IambWindow::VerifyList(_) => bold_spans("Verifications"), IambWindow::Welcome(_) => bold_spans("Welcome to iamb"), + IambWindow::ChatList(_) => bold_spans("DMs & Rooms"), IambWindow::Room(w) => { let title = store.application.get_room_title(w.id()); @@ -639,6 +688,7 @@ impl Window for IambWindow { IambWindow::SpaceList(_) => bold_spans("Spaces"), IambWindow::VerifyList(_) => bold_spans("Verifications"), IambWindow::Welcome(_) => bold_spans("Welcome to iamb"), + IambWindow::ChatList(_) => bold_spans("DMs & Rooms"), IambWindow::Room(w) => w.get_title(store), IambWindow::MemberList(state, room_id, _) => { @@ -695,6 +745,11 @@ impl Window for IambWindow { return Ok(win.into()); }, + IambId::ChatList => { + let list = ChatListState::new(IambBufferId::ChatList, vec![]); + + Ok(list.into()) + }, } } @@ -729,6 +784,105 @@ impl Window for IambWindow { } } +#[derive(Clone)] +pub struct GenericChatItem { + room_info: MatrixRoomInfo, + name: String, + alias: Option, + is_dm: bool, +} + +impl GenericChatItem { + fn new(room_info: MatrixRoomInfo, store: &mut ProgramStore, is_dm: bool) -> Self { + let room = &room_info.deref().0; + let room_id = room.room_id(); + + let info = store.application.get_room_info(room_id.to_owned()); + + let name = info.name.clone().unwrap_or_default(); + let alias = room.canonical_alias(); + info.tags = room_info.deref().1.clone(); + + if let Some(alias) = &alias { + store.application.names.insert(alias.to_string(), room_id.to_owned()); + } + + GenericChatItem { room_info, name, alias, is_dm } + } + + #[inline] + fn room(&self) -> &MatrixRoom { + &self.room_info.deref().0 + } + + #[inline] + fn tags(&self) -> &Option { + &self.room_info.deref().1 + } +} + +impl RoomLikeItem for GenericChatItem { + fn name(&self) -> &str { + self.name.as_str() + } + + fn alias(&self) -> Option<&RoomAliasId> { + self.alias.as_deref() + } + + fn room_id(&self) -> &RoomId { + self.room().room_id() + } + + fn has_tag(&self, tag: TagName) -> bool { + if let Some(tags) = &self.room_info.deref().1 { + tags.contains_key(&tag) + } else { + false + } + } +} + +impl ToString for GenericChatItem { + fn to_string(&self) -> String { + return self.name.clone(); + } +} + +impl ListItem for GenericChatItem { + fn show(&self, selected: bool, _: &ViewportContext, _: &mut ProgramStore) -> Text { + let style = selected_style(selected); + let mut spans = vec![Span::styled(self.name.as_str(), style)]; + let mut labels = if self.is_dm { + vec![vec![Span::styled("DM", style)]] + } else { + vec![vec![Span::styled("Room", style)]] + }; + + if let Some(tags) = &self.tags() { + labels.extend(tags.keys().map(|t| tag_to_span(t, style))); + } + + append_tags(labels, &mut spans, style); + Text::from(Line::from(spans)) + } + + fn get_word(&self) -> Option { + self.room_id().to_string().into() + } +} + +impl Promptable for GenericChatItem { + fn prompt( + &mut self, + act: &PromptAction, + ctx: &ProgramContext, + _: &mut ProgramStore, + ) -> EditResult, IambInfo> { + room_prompt(self.room_id(), act, ctx) + } +} + #[derive(Clone)] pub struct RoomItem { room_info: MatrixRoomInfo, @@ -797,6 +951,7 @@ impl ListItem for RoomItem { if let Some(tags) = &self.tags() { let style = selected_style(selected); let mut spans = vec![Span::styled(self.name.as_str(), style)]; + let tags = tags.keys().map(|t| tag_to_span(t, style)).collect(); append_tags(tags, &mut spans, style); @@ -882,6 +1037,7 @@ impl ListItem for DirectItem { if let Some(tags) = &self.tags() { let style = selected_style(selected); let mut spans = vec![Span::styled(self.name.as_str(), style)]; + let tags = tags.keys().map(|t| tag_to_span(t, style)).collect(); append_tags(tags, &mut spans, style); @@ -1069,7 +1225,7 @@ impl Ord for VerifyItem { impl PartialOrd for VerifyItem { fn partial_cmp(&self, other: &Self) -> Option { - self.cmp(other).into() + Some(self.cmp(other)) } } diff --git a/src/windows/welcome.md b/src/windows/welcome.md index 2c967ec..9e12b90 100644 --- a/src/windows/welcome.md +++ b/src/windows/welcome.md @@ -12,6 +12,7 @@ - `:dms` will open a list of direct messages - `:rooms` will open a list of joined rooms +- `:chats` will open a list containing both direct messages and rooms - `:members` will open a list of members for the currently focused room or space - `:spaces` will open a list of joined spaces - `:join` can be used to switch to join a new room or start a direct message diff --git a/src/worker.rs b/src/worker.rs index 6ad2d61..7cbdf02 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -832,7 +832,7 @@ impl ClientWorker { let room_id = room.room_id().to_owned(); let room_name = Some(room_name.to_string()); let mut locked = store.lock().await; - let mut info = locked.application.rooms.get_or_default(room_id.clone()); + let info = locked.application.rooms.get_or_default(room_id.clone()); info.name = room_name; } }