Add a new :chats window that lists both DMs and Rooms (#184)

Fixes #172
This commit is contained in:
FormindVER 2024-02-27 18:36:09 -08:00 committed by Ulyssa
parent 88af9bfec3
commit b7ae01499b
No known key found for this signature in database
GPG key ID: F2873CA2997B83C5
7 changed files with 211 additions and 19 deletions

View file

@ -1172,7 +1172,7 @@ pub enum IambId {
/// A Matrix room. /// A Matrix room.
Room(OwnedRoomId), Room(OwnedRoomId),
/// The `:rooms` window. /// The `:dms` window.
DirectList, DirectList,
/// The `:members` window for a given Matrix room. /// The `:members` window for a given Matrix room.
@ -1189,6 +1189,9 @@ pub enum IambId {
/// The `:welcome` window. /// The `:welcome` window.
Welcome, Welcome,
/// The `:chats` window.
ChatList,
} }
impl Display for IambId { impl Display for IambId {
@ -1205,6 +1208,7 @@ impl Display for IambId {
IambId::SpaceList => f.write_str("iamb://spaces"), IambId::SpaceList => f.write_str("iamb://spaces"),
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"),
} }
} }
} }
@ -1317,6 +1321,13 @@ impl<'de> Visitor<'de> for IambIdVisitor {
Ok(IambId::Welcome) 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"))), 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")),
} }
@ -1374,6 +1385,9 @@ pub enum IambBufferId {
/// The buffer for the `:rooms` window. /// The buffer for the `:rooms` window.
Welcome, Welcome,
/// The `:chats` window.
ChatList,
} }
impl IambBufferId { impl IambBufferId {
@ -1388,6 +1402,7 @@ impl IambBufferId {
IambBufferId::SpaceList => Some(IambId::SpaceList), IambBufferId::SpaceList => Some(IambId::SpaceList),
IambBufferId::VerifyList => Some(IambId::VerifyList), IambBufferId::VerifyList => Some(IambId::VerifyList),
IambBufferId::Welcome => Some(IambId::Welcome), IambBufferId::Welcome => Some(IambId::Welcome),
IambBufferId::ChatList => Some(IambId::ChatList),
} }
} }
} }
@ -1410,7 +1425,6 @@ impl ApplicationInfo for IambInfo {
match content { match content {
IambBufferId::Command(CommandType::Command) => complete_cmdbar(text, cursor, store), IambBufferId::Command(CommandType::Command) => complete_cmdbar(text, cursor, store),
IambBufferId::Command(CommandType::Search) => vec![], IambBufferId::Command(CommandType::Search) => vec![],
IambBufferId::Room(_, RoomFocus::MessageBar) => complete_msgbar(text, cursor, store), IambBufferId::Room(_, RoomFocus::MessageBar) => complete_msgbar(text, cursor, store),
IambBufferId::Room(_, RoomFocus::Scrollback) => vec![], IambBufferId::Room(_, RoomFocus::Scrollback) => vec![],
@ -1420,6 +1434,7 @@ impl ApplicationInfo for IambInfo {
IambBufferId::SpaceList => vec![], IambBufferId::SpaceList => vec![],
IambBufferId::VerifyList => vec![], IambBufferId::VerifyList => vec![],
IambBufferId::Welcome => vec![], IambBufferId::Welcome => vec![],
IambBufferId::ChatList => vec![],
} }
} }

View file

@ -292,6 +292,17 @@ fn iamb_rooms(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
return Ok(step); 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 { 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);
@ -495,6 +506,11 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
aliases: vec![], aliases: vec![],
f: iamb_create, 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: "dms".into(), aliases: vec![], f: iamb_dms });
cmds.add_command(ProgramCommand { cmds.add_command(ProgramCommand {
name: "download".into(), name: "download".into(),

View file

@ -227,6 +227,7 @@ pub type UserOverrides = HashMap<OwnedUserId, UserDisplayTunables>;
fn merge_sorts(a: SortOverrides, b: SortOverrides) -> SortOverrides { fn merge_sorts(a: SortOverrides, b: SortOverrides) -> SortOverrides {
SortOverrides { SortOverrides {
chats: b.chats.or(a.chats),
dms: b.dms.or(a.dms), dms: b.dms.or(a.dms),
rooms: b.rooms.or(a.rooms), rooms: b.rooms.or(a.rooms),
spaces: b.spaces.or(a.spaces), spaces: b.spaces.or(a.spaces),
@ -308,6 +309,7 @@ pub struct ImagePreviewProtocolValues {
#[derive(Clone)] #[derive(Clone)]
pub struct SortValues { pub struct SortValues {
pub chats: Vec<SortColumn<SortFieldRoom>>,
pub dms: Vec<SortColumn<SortFieldRoom>>, pub dms: Vec<SortColumn<SortFieldRoom>>,
pub rooms: Vec<SortColumn<SortFieldRoom>>, pub rooms: Vec<SortColumn<SortFieldRoom>>,
pub spaces: Vec<SortColumn<SortFieldRoom>>, pub spaces: Vec<SortColumn<SortFieldRoom>>,
@ -316,6 +318,7 @@ pub struct SortValues {
#[derive(Clone, Default, Deserialize)] #[derive(Clone, Default, Deserialize)]
pub struct SortOverrides { pub struct SortOverrides {
pub chats: Option<Vec<SortColumn<SortFieldRoom>>>,
pub dms: Option<Vec<SortColumn<SortFieldRoom>>>, pub dms: Option<Vec<SortColumn<SortFieldRoom>>>,
pub rooms: Option<Vec<SortColumn<SortFieldRoom>>>, pub rooms: Option<Vec<SortColumn<SortFieldRoom>>>,
pub spaces: Option<Vec<SortColumn<SortFieldRoom>>>, pub spaces: Option<Vec<SortColumn<SortFieldRoom>>>,
@ -325,11 +328,12 @@ pub struct SortOverrides {
impl SortOverrides { impl SortOverrides {
pub fn values(self) -> SortValues { pub fn values(self) -> SortValues {
let rooms = self.rooms.unwrap_or_else(|| Vec::from(DEFAULT_ROOM_SORT)); 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 dms = self.dms.unwrap_or_else(|| rooms.clone());
let spaces = self.spaces.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)); let members = self.members.unwrap_or_else(|| Vec::from(DEFAULT_MEMBERS_SORT));
SortValues { rooms, members, dms, spaces } SortValues { rooms, members, chats, dms, spaces }
} }
} }

View file

@ -196,7 +196,7 @@ impl Ord for MessageTimeStamp {
impl PartialOrd for MessageTimeStamp { impl PartialOrd for MessageTimeStamp {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.cmp(other).into() Some(self.cmp(other))
} }
} }
@ -340,7 +340,7 @@ impl Ord for MessageCursor {
impl PartialOrd for MessageCursor { impl PartialOrd for MessageCursor {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.cmp(other).into() Some(self.cmp(other))
} }
} }

View file

@ -215,28 +215,34 @@ fn user_fields_cmp(
user_cmp(a, b, &SortFieldUser::UserId) user_cmp(a, b, &SortFieldUser::UserId)
} }
fn append_tags<'a>(tags: &'a Tags, spans: &mut Vec<Span<'a>>, style: Style) { fn tag_to_span(tag: &TagName, style: Style) -> Vec<Span<'_>> {
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<Vec<Span<'a>>>, spans: &mut Vec<Span<'a>>, style: Style) {
if tags.is_empty() { if tags.is_empty() {
return; return;
} }
spans.push(Span::styled(" (", style)); spans.push(Span::styled(" (", style));
for (i, tag) in tags.keys().enumerate() { for (i, tag) in tags.into_iter().enumerate() {
if i > 0 { if i > 0 {
spans.push(Span::styled(", ", style)); spans.push(Span::styled(", ", style));
} }
match tag { spans.extend(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.push(Span::styled(")", style)); spans.push(Span::styled(")", style));
@ -289,6 +295,7 @@ macro_rules! delegate {
IambWindow::SpaceList($id) => $e, IambWindow::SpaceList($id) => $e,
IambWindow::VerifyList($id) => $e, IambWindow::VerifyList($id) => $e,
IambWindow::Welcome($id) => $e, IambWindow::Welcome($id) => $e,
IambWindow::ChatList($id) => $e,
} }
}; };
} }
@ -301,6 +308,7 @@ pub enum IambWindow {
RoomList(RoomListState), RoomList(RoomListState),
SpaceList(SpaceListState), SpaceList(SpaceListState),
Welcome(WelcomeState), Welcome(WelcomeState),
ChatList(ChatListState),
} }
impl IambWindow { impl IambWindow {
@ -355,9 +363,16 @@ impl IambWindow {
pub type DirectListState = ListState<DirectItem, IambInfo>; 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 SpaceListState = ListState<SpaceItem, IambInfo>; pub type SpaceListState = ListState<SpaceItem, IambInfo>;
pub type VerifyListState = ListState<VerifyItem, IambInfo>; pub type VerifyListState = ListState<VerifyItem, IambInfo>;
impl From<ChatListState> for IambWindow {
fn from(list: ChatListState) -> Self {
IambWindow::ChatList(list)
}
}
impl From<RoomState> for IambWindow { impl From<RoomState> for IambWindow {
fn from(room: RoomState) -> Self { fn from(room: RoomState) -> Self {
IambWindow::Room(room) IambWindow::Room(room)
@ -514,6 +529,37 @@ impl WindowOps<IambInfo> for IambWindow {
.focus(focused) .focus(focused)
.render(area, buf, state); .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::<Vec<_>>();
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) => { IambWindow::SpaceList(state) => {
let mut items = store let mut items = store
.application .application
@ -564,6 +610,7 @@ impl WindowOps<IambInfo> for IambWindow {
IambWindow::SpaceList(w) => w.dup(store).into(), IambWindow::SpaceList(w) => w.dup(store).into(),
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(),
} }
} }
@ -603,6 +650,7 @@ impl Window<IambInfo> for IambWindow {
IambWindow::SpaceList(_) => IambId::SpaceList, IambWindow::SpaceList(_) => IambId::SpaceList,
IambWindow::VerifyList(_) => IambId::VerifyList, IambWindow::VerifyList(_) => IambId::VerifyList,
IambWindow::Welcome(_) => IambId::Welcome, IambWindow::Welcome(_) => IambId::Welcome,
IambWindow::ChatList(_) => IambId::ChatList,
} }
} }
@ -613,6 +661,7 @@ impl Window<IambInfo> for IambWindow {
IambWindow::SpaceList(_) => bold_spans("Spaces"), IambWindow::SpaceList(_) => bold_spans("Spaces"),
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::Room(w) => { IambWindow::Room(w) => {
let title = store.application.get_room_title(w.id()); let title = store.application.get_room_title(w.id());
@ -639,6 +688,7 @@ impl Window<IambInfo> for IambWindow {
IambWindow::SpaceList(_) => bold_spans("Spaces"), IambWindow::SpaceList(_) => bold_spans("Spaces"),
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::Room(w) => w.get_title(store), IambWindow::Room(w) => w.get_title(store),
IambWindow::MemberList(state, room_id, _) => { IambWindow::MemberList(state, room_id, _) => {
@ -695,6 +745,11 @@ impl Window<IambInfo> for IambWindow {
return Ok(win.into()); return Ok(win.into());
}, },
IambId::ChatList => {
let list = ChatListState::new(IambBufferId::ChatList, vec![]);
Ok(list.into())
},
} }
} }
@ -729,6 +784,105 @@ impl Window<IambInfo> for IambWindow {
} }
} }
#[derive(Clone)]
pub struct GenericChatItem {
room_info: MatrixRoomInfo,
name: String,
alias: Option<OwnedRoomAliasId>,
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<Tags> {
&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<IambInfo> for GenericChatItem {
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &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<String> {
self.room_id().to_string().into()
}
}
impl Promptable<ProgramContext, ProgramStore, IambInfo> for GenericChatItem {
fn prompt(
&mut self,
act: &PromptAction,
ctx: &ProgramContext,
_: &mut ProgramStore,
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
room_prompt(self.room_id(), act, ctx)
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct RoomItem { pub struct RoomItem {
room_info: MatrixRoomInfo, room_info: MatrixRoomInfo,
@ -797,6 +951,7 @@ impl ListItem<IambInfo> for RoomItem {
if let Some(tags) = &self.tags() { if let Some(tags) = &self.tags() {
let style = selected_style(selected); let style = selected_style(selected);
let mut spans = vec![Span::styled(self.name.as_str(), style)]; 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); append_tags(tags, &mut spans, style);
@ -882,6 +1037,7 @@ impl ListItem<IambInfo> for DirectItem {
if let Some(tags) = &self.tags() { if let Some(tags) = &self.tags() {
let style = selected_style(selected); let style = selected_style(selected);
let mut spans = vec![Span::styled(self.name.as_str(), style)]; 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); append_tags(tags, &mut spans, style);
@ -1069,7 +1225,7 @@ impl Ord for VerifyItem {
impl PartialOrd for VerifyItem { impl PartialOrd for VerifyItem {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.cmp(other).into() Some(self.cmp(other))
} }
} }

View file

@ -12,6 +12,7 @@
- `:dms` will open a list of direct messages - `:dms` will open a list of direct messages
- `:rooms` will open a list of joined rooms - `: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 - `:members` will open a list of members for the currently focused room or space
- `:spaces` will open a list of joined spaces - `:spaces` will open a list of joined spaces
- `:join` can be used to switch to join a new room or start a direct message - `:join` can be used to switch to join a new room or start a direct message

View file

@ -832,7 +832,7 @@ impl ClientWorker {
let room_id = room.room_id().to_owned(); let room_id = room.room_id().to_owned();
let room_name = Some(room_name.to_string()); let room_name = Some(room_name.to_string());
let mut locked = store.lock().await; 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; info.name = room_name;
} }
} }