diff --git a/src/base.rs b/src/base.rs index 3266f44..6a0ea6d 100644 --- a/src/base.rs +++ b/src/base.rs @@ -445,6 +445,9 @@ pub struct RoomInfo { /// Users currently typing in this room, and when we received notification of them doing so. pub users_typing: Option<(Instant, Vec)>, + + /// The display names for users in this room. + pub display_names: HashMap, } impl RoomInfo { @@ -583,13 +586,13 @@ impl RoomInfo { match n { 0 => Spans(vec![]), 1 => { - let user = settings.get_user_span(typers[0].as_ref()); + let user = settings.get_user_span(typers[0].as_ref(), self); Spans(vec![user, Span::from(" is typing...")]) }, 2 => { - let user1 = settings.get_user_span(typers[0].as_ref()); - let user2 = settings.get_user_span(typers[1].as_ref()); + let user1 = settings.get_user_span(typers[0].as_ref(), self); + let user2 = settings.get_user_span(typers[1].as_ref(), self); Spans(vec![ user1, @@ -835,11 +838,11 @@ impl<'de> Visitor<'de> for IambIdVisitor { }, Some("members") => { let Some(path) = url.path_segments() else { - return Err(E::custom( "Invalid members window URL")); + return Err(E::custom("Invalid members window URL")); }; let &[room_id] = path.collect::>().as_slice() else { - return Err(E::custom( "Invalid members window URL")); + return Err(E::custom("Invalid members window URL")); }; let Ok(room_id) = OwnedRoomId::try_from(room_id) else { diff --git a/src/config.rs b/src/config.rs index 7b91f50..7682732 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,7 +19,7 @@ use modalkit::tui::{ text::Span, }; -use super::base::IambId; +use super::base::{IambId, RoomInfo}; macro_rules! usage { ( $($args: tt)* ) => { @@ -227,6 +227,24 @@ fn merge_users(a: Option, b: Option) -> Option, pub open_command: Option>, } @@ -253,6 +272,7 @@ pub struct Tunables { pub typing_notice_send: Option, pub typing_notice_display: Option, pub users: Option, + pub username_display: Option, pub default_room: Option, pub open_command: Option>, } @@ -271,6 +291,7 @@ impl Tunables { typing_notice_send: self.typing_notice_send.or(other.typing_notice_send), typing_notice_display: self.typing_notice_display.or(other.typing_notice_display), users: merge_users(self.users, other.users), + username_display: self.username_display.or(other.username_display), default_room: self.default_room.or(other.default_room), open_command: self.open_command.or(other.open_command), } @@ -287,6 +308,7 @@ impl Tunables { typing_notice_send: self.typing_notice_send.unwrap_or(true), typing_notice_display: self.typing_notice_display.unwrap_or(true), users: self.users.unwrap_or_default(), + username_display: self.username_display.unwrap_or_default(), default_room: self.default_room, open_command: self.open_command, } @@ -525,18 +547,45 @@ impl ApplicationSettings { Span::styled(String::from(c), style) } - pub fn get_user_span<'a>(&self, user_id: &'a UserId) -> Span<'a> { - let (color, name) = self - .tunables + pub fn get_user_overrides( + &self, + user_id: &UserId, + ) -> (Option, Option>) { + self.tunables .users .get(user_id) .map(|user| (user.color.as_ref().map(|c| c.0), user.name.clone().map(Cow::Owned))) - .unwrap_or_default(); + .unwrap_or_default() + } - let user_id = user_id.as_str(); - let color = color.unwrap_or_else(|| user_color(user_id)); + pub fn get_user_style(&self, user_id: &UserId) -> Style { + let color = self + .tunables + .users + .get(user_id) + .and_then(|user| user.color.as_ref().map(|c| c.0)) + .unwrap_or_else(|| user_color(user_id.as_str())); + + user_style_from_color(color) + } + + pub fn get_user_span<'a>(&self, user_id: &'a UserId, info: &'a RoomInfo) -> Span<'a> { + let (color, name) = self.get_user_overrides(user_id); + + let color = color.unwrap_or_else(|| user_color(user_id.as_str())); let style = user_style_from_color(color); - let name = name.unwrap_or(Cow::Borrowed(user_id)); + let name = match (name, &self.tunables.username_display) { + (Some(name), _) => name, + (None, UserDisplayStyle::Username) => Cow::Borrowed(user_id.as_str()), + (None, UserDisplayStyle::LocalPart) => Cow::Borrowed(user_id.localpart()), + (None, UserDisplayStyle::DisplayName) => { + if let Some(display) = info.display_names.get(user_id) { + Cow::Borrowed(display.as_str()) + } else { + Cow::Borrowed(user_id.as_str()) + } + }, + }; Span::styled(name, style) } @@ -641,6 +690,19 @@ mod tests { assert_eq!(res.users, Some(users.into_iter().collect())); } + #[test] + fn test_parse_tunables_username_display() { + let res: Tunables = serde_json::from_str("{\"username_display\": \"username\"}").unwrap(); + assert_eq!(res.username_display, Some(UserDisplayStyle::Username)); + + let res: Tunables = serde_json::from_str("{\"username_display\": \"localpart\"}").unwrap(); + assert_eq!(res.username_display, Some(UserDisplayStyle::LocalPart)); + + let res: Tunables = + serde_json::from_str("{\"username_display\": \"displayname\"}").unwrap(); + assert_eq!(res.username_display, Some(UserDisplayStyle::DisplayName)); + } + #[test] fn test_parse_layout() { let user = WindowPath::UserId(user_id!("@user:example.com").to_owned()); diff --git a/src/message/mod.rs b/src/message/mod.rs index 4f1746d..b00c82c 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -644,7 +644,7 @@ impl Message { { let cols = MessageColumns::Four; let fill = width - USER_GUTTER - TIME_GUTTER - READ_GUTTER; - let user = self.show_sender(prev, true, settings); + let user = self.show_sender(prev, true, info, settings); let time = self.timestamp.show_time(); let read = match info.receipts.get(self.event.event_id()) { Some(read) => read.iter(), @@ -655,7 +655,7 @@ impl Message { } else if USER_GUTTER + TIME_GUTTER + MIN_MSG_LEN <= width { let cols = MessageColumns::Three; let fill = width - USER_GUTTER - TIME_GUTTER; - let user = self.show_sender(prev, true, settings); + let user = self.show_sender(prev, true, info, settings); let time = self.timestamp.show_time(); let read = [].iter(); @@ -663,7 +663,7 @@ impl Message { } else if USER_GUTTER + MIN_MSG_LEN <= width { let cols = MessageColumns::Two; let fill = width - USER_GUTTER; - let user = self.show_sender(prev, true, settings); + let user = self.show_sender(prev, true, info, settings); let time = None; let read = [].iter(); @@ -671,7 +671,7 @@ impl Message { } else { let cols = MessageColumns::One; let fill = width.saturating_sub(2); - let user = self.show_sender(prev, false, settings); + let user = self.show_sender(prev, false, info, settings); let time = None; let read = [].iter(); @@ -700,7 +700,7 @@ impl Message { if let Some(r) = &reply { let w = width.saturating_sub(2); let mut replied = r.show_msg(w, style, true); - let mut sender = r.sender_span(settings); + let mut sender = r.sender_span(info, settings); let sender_width = UnicodeWidthStr::width(sender.content.as_ref()); let trailing = w.saturating_sub(sender_width + 1); @@ -793,16 +793,21 @@ impl Message { } } - fn sender_span(&self, settings: &ApplicationSettings) -> Span { - settings.get_user_span(self.sender.as_ref()) + fn sender_span<'a>( + &'a self, + info: &'a RoomInfo, + settings: &'a ApplicationSettings, + ) -> Span<'a> { + settings.get_user_span(self.sender.as_ref(), info) } - fn show_sender( - &self, + fn show_sender<'a>( + &'a self, prev: Option<&Message>, align_right: bool, - settings: &ApplicationSettings, - ) -> Option { + info: &'a RoomInfo, + settings: &'a ApplicationSettings, + ) -> Option> { if let Some(prev) = prev { if self.sender == prev.sender && self.timestamp.same_day(&prev.timestamp) && @@ -812,7 +817,7 @@ impl Message { } } - let Span { content, style } = self.sender_span(settings); + let Span { content, style } = self.sender_span(info, settings); let stop = content.len().min(28); let s = &content[..stop]; diff --git a/src/tests.rs b/src/tests.rs index f1d7692..668ac1e 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -30,6 +30,7 @@ use crate::{ ProfileConfig, TunableValues, UserColor, + UserDisplayStyle, UserDisplayTunables, }, message::{ @@ -160,6 +161,7 @@ pub fn mock_room() -> RoomInfo { fetch_id: RoomFetchStatus::NotStarted, fetch_last: None, users_typing: None, + display_names: HashMap::new(), } } @@ -189,6 +191,7 @@ pub fn mock_tunables() -> TunableValues { .into_iter() .collect::>(), open_command: None, + username_display: UserDisplayStyle::Username, } } diff --git a/src/windows/mod.rs b/src/windows/mod.rs index 0e4fbb9..9f72a5b 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -409,7 +409,7 @@ impl WindowOps for IambWindow { if need_fetch { if let Ok(mems) = store.application.worker.members(room_id.clone()) { - let items = mems.into_iter().map(MemberItem::new); + let items = mems.into_iter().map(|m| MemberItem::new(m, room_id.clone())); state.set(items.collect()); *last_fetch = Some(Instant::now()); } @@ -1100,11 +1100,12 @@ impl Promptable for VerifyItem { #[derive(Clone)] pub struct MemberItem { member: RoomMember, + room_id: OwnedRoomId, } impl MemberItem { - fn new(member: RoomMember) -> Self { - Self { member } + fn new(member: RoomMember, room_id: OwnedRoomId) -> Self { + Self { member, room_id } } } @@ -1121,12 +1122,32 @@ impl ListItem for MemberItem { _: &ViewportContext, store: &mut ProgramStore, ) -> Text { - let mut user = store.application.settings.get_user_span(self.member.user_id()); + let info = store.application.rooms.get_or_default(self.room_id.clone()); + let user_id = self.member.user_id(); + + let (color, name) = store.application.settings.get_user_overrides(self.member.user_id()); + let color = color.unwrap_or_else(|| super::config::user_color(user_id.as_str())); + let mut style = super::config::user_style_from_color(color); if selected { - user.style = user.style.add_modifier(StyleModifier::REVERSED); + style = style.add_modifier(StyleModifier::REVERSED); } + let mut spans = vec![]; + let mut parens = false; + + if let Some(name) = name { + spans.push(Span::styled(name, style)); + parens = true; + } else if let Some(display) = info.display_names.get(user_id) { + spans.push(Span::styled(display.clone(), style)); + parens = true; + } + + spans.extend(parens.then_some(Span::styled(" (", style))); + spans.push(Span::styled(user_id.as_str(), style)); + spans.extend(parens.then_some(Span::styled(")", style))); + let state = match self.member.membership() { MembershipState::Ban => Span::raw(" (banned)").into(), MembershipState::Invite => Span::raw(" (invited)").into(), @@ -1136,11 +1157,9 @@ impl ListItem for MemberItem { _ => None, }; - if let Some(state) = state { - Spans(vec![user, state]).into() - } else { - user.into() - } + spans.extend(state); + + return Spans(spans).into(); } fn get_word(&self) -> Option { diff --git a/src/windows/room/chat.rs b/src/windows/room/chat.rs index c04bc4e..d01963a 100644 --- a/src/windows/room/chat.rs +++ b/src/windows/room/chat.rs @@ -808,7 +808,8 @@ impl<'a> StatefulWidget for Chat<'a> { state.reply_to.as_ref().and_then(|k| { let room = self.store.application.rooms.get(state.id())?; let msg = room.messages.get(k)?; - let user = self.store.application.settings.get_user_span(msg.sender.as_ref()); + let user = + self.store.application.settings.get_user_span(msg.sender.as_ref(), room); let prefix = if editing.is_some() { Span::from("Editing reply to ") } else { diff --git a/src/windows/room/mod.rs b/src/windows/room/mod.rs index f82a2f6..73d99b2 100644 --- a/src/windows/room/mod.rs +++ b/src/windows/room/mod.rs @@ -139,8 +139,9 @@ impl RoomState { let mut invited = vec![Span::from(format!("You have been invited to join {name}"))]; if let Ok(Some(inviter)) = &inviter { + let info = store.application.rooms.get_or_default(self.id().to_owned()); invited.push(Span::from(" by ")); - invited.push(store.application.settings.get_user_span(inviter.user_id())); + invited.push(store.application.settings.get_user_span(inviter.user_id(), info)); } let l1 = Spans(invited); diff --git a/src/worker.rs b/src/worker.rs index 85e2441..6cd13d4 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -40,6 +40,7 @@ use matrix_sdk::{ reaction::ReactionEventContent, room::{ encryption::RoomEncryptionEventContent, + member::OriginalSyncRoomMemberEvent, message::{MessageType, RoomMessageEventContent}, name::RoomNameEventContent, redaction::{OriginalSyncRoomRedactionEvent, SyncRoomRedactionEvent}, @@ -838,6 +839,38 @@ impl ClientWorker { }, ); + let _ = self.client.add_event_handler( + |ev: OriginalSyncRoomMemberEvent, + room: MatrixRoom, + client: Client, + store: Ctx| { + async move { + let room_id = room.room_id(); + let user_id = ev.state_key; + + let ambiguous_name = + ev.content.displayname.as_deref().unwrap_or_else(|| user_id.localpart()); + let ambiguous = client + .store() + .get_users_with_display_name(room_id, ambiguous_name) + .await + .map(|users| users.len() > 1) + .unwrap_or_default(); + + let mut locked = store.lock().await; + let info = locked.application.get_room_info(room_id.to_owned()); + + if ambiguous { + info.display_names.remove(&user_id); + } else if let Some(display) = ev.content.displayname { + info.display_names.insert(user_id, display); + } else { + info.display_names.remove(&user_id); + } + } + }, + ); + let _ = self.client.add_event_handler( |ev: OriginalSyncKeyVerificationStartEvent, client: Client,