Support hiding server part of username in message scrollback (#71)

This commit is contained in:
Ulyssa 2023-07-06 23:15:58 -07:00
parent 61aba80be1
commit 64891ec68f
No known key found for this signature in database
GPG key ID: F2873CA2997B83C5
8 changed files with 164 additions and 37 deletions

View file

@ -445,6 +445,9 @@ pub struct RoomInfo {
/// Users currently typing in this room, and when we received notification of them doing so. /// Users currently typing in this room, and when we received notification of them doing so.
pub users_typing: Option<(Instant, Vec<OwnedUserId>)>, pub users_typing: Option<(Instant, Vec<OwnedUserId>)>,
/// The display names for users in this room.
pub display_names: HashMap<OwnedUserId, String>,
} }
impl RoomInfo { impl RoomInfo {
@ -583,13 +586,13 @@ impl RoomInfo {
match n { match n {
0 => Spans(vec![]), 0 => Spans(vec![]),
1 => { 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...")]) Spans(vec![user, Span::from(" is typing...")])
}, },
2 => { 2 => {
let user1 = settings.get_user_span(typers[0].as_ref()); let user1 = settings.get_user_span(typers[0].as_ref(), self);
let user2 = settings.get_user_span(typers[1].as_ref()); let user2 = settings.get_user_span(typers[1].as_ref(), self);
Spans(vec![ Spans(vec![
user1, user1,

View file

@ -19,7 +19,7 @@ use modalkit::tui::{
text::Span, text::Span,
}; };
use super::base::IambId; use super::base::{IambId, RoomInfo};
macro_rules! usage { macro_rules! usage {
( $($args: tt)* ) => { ( $($args: tt)* ) => {
@ -227,6 +227,24 @@ fn merge_users(a: Option<UserOverrides>, b: Option<UserOverrides>) -> Option<Use
} }
} }
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum UserDisplayStyle {
// The Matrix username for the sender (e.g., "@user:example.com").
#[default]
Username,
// The localpart of the Matrix username (e.g., "@user").
LocalPart,
// The display name for the Matrix user, calculated according to the rules from the spec.
//
// This is usually something like "Ada Lovelace" if the user has configured a display name, but
// it can wind up being the Matrix username if there are display name collisions in the room,
// in order to avoid any confusion.
DisplayName,
}
#[derive(Clone)] #[derive(Clone)]
pub struct TunableValues { pub struct TunableValues {
pub log_level: Level, pub log_level: Level,
@ -238,6 +256,7 @@ pub struct TunableValues {
pub typing_notice_send: bool, pub typing_notice_send: bool,
pub typing_notice_display: bool, pub typing_notice_display: bool,
pub users: UserOverrides, pub users: UserOverrides,
pub username_display: UserDisplayStyle,
pub default_room: Option<String>, pub default_room: Option<String>,
pub open_command: Option<Vec<String>>, pub open_command: Option<Vec<String>>,
} }
@ -253,6 +272,7 @@ pub struct Tunables {
pub typing_notice_send: Option<bool>, pub typing_notice_send: Option<bool>,
pub typing_notice_display: Option<bool>, pub typing_notice_display: Option<bool>,
pub users: Option<UserOverrides>, pub users: Option<UserOverrides>,
pub username_display: Option<UserDisplayStyle>,
pub default_room: Option<String>, pub default_room: Option<String>,
pub open_command: Option<Vec<String>>, pub open_command: Option<Vec<String>>,
} }
@ -271,6 +291,7 @@ impl Tunables {
typing_notice_send: self.typing_notice_send.or(other.typing_notice_send), typing_notice_send: self.typing_notice_send.or(other.typing_notice_send),
typing_notice_display: self.typing_notice_display.or(other.typing_notice_display), typing_notice_display: self.typing_notice_display.or(other.typing_notice_display),
users: merge_users(self.users, other.users), 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), default_room: self.default_room.or(other.default_room),
open_command: self.open_command.or(other.open_command), 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_send: self.typing_notice_send.unwrap_or(true),
typing_notice_display: self.typing_notice_display.unwrap_or(true), typing_notice_display: self.typing_notice_display.unwrap_or(true),
users: self.users.unwrap_or_default(), users: self.users.unwrap_or_default(),
username_display: self.username_display.unwrap_or_default(),
default_room: self.default_room, default_room: self.default_room,
open_command: self.open_command, open_command: self.open_command,
} }
@ -525,18 +547,45 @@ impl ApplicationSettings {
Span::styled(String::from(c), style) Span::styled(String::from(c), style)
} }
pub fn get_user_span<'a>(&self, user_id: &'a UserId) -> Span<'a> { pub fn get_user_overrides(
let (color, name) = self &self,
.tunables user_id: &UserId,
) -> (Option<Color>, Option<Cow<'static, str>>) {
self.tunables
.users .users
.get(user_id) .get(user_id)
.map(|user| (user.color.as_ref().map(|c| c.0), user.name.clone().map(Cow::Owned))) .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(); pub fn get_user_style(&self, user_id: &UserId) -> Style {
let color = color.unwrap_or_else(|| user_color(user_id)); 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 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) Span::styled(name, style)
} }
@ -641,6 +690,19 @@ mod tests {
assert_eq!(res.users, Some(users.into_iter().collect())); 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] #[test]
fn test_parse_layout() { fn test_parse_layout() {
let user = WindowPath::UserId(user_id!("@user:example.com").to_owned()); let user = WindowPath::UserId(user_id!("@user:example.com").to_owned());

View file

@ -644,7 +644,7 @@ impl Message {
{ {
let cols = MessageColumns::Four; let cols = MessageColumns::Four;
let fill = width - USER_GUTTER - TIME_GUTTER - READ_GUTTER; 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 time = self.timestamp.show_time();
let read = match info.receipts.get(self.event.event_id()) { let read = match info.receipts.get(self.event.event_id()) {
Some(read) => read.iter(), Some(read) => read.iter(),
@ -655,7 +655,7 @@ impl Message {
} else if USER_GUTTER + TIME_GUTTER + MIN_MSG_LEN <= width { } else if USER_GUTTER + TIME_GUTTER + MIN_MSG_LEN <= width {
let cols = MessageColumns::Three; let cols = MessageColumns::Three;
let fill = width - USER_GUTTER - TIME_GUTTER; 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 time = self.timestamp.show_time();
let read = [].iter(); let read = [].iter();
@ -663,7 +663,7 @@ impl Message {
} else if USER_GUTTER + MIN_MSG_LEN <= width { } else if USER_GUTTER + MIN_MSG_LEN <= width {
let cols = MessageColumns::Two; let cols = MessageColumns::Two;
let fill = width - USER_GUTTER; 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 time = None;
let read = [].iter(); let read = [].iter();
@ -671,7 +671,7 @@ impl Message {
} else { } else {
let cols = MessageColumns::One; let cols = MessageColumns::One;
let fill = width.saturating_sub(2); 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 time = None;
let read = [].iter(); let read = [].iter();
@ -700,7 +700,7 @@ impl Message {
if let Some(r) = &reply { if let Some(r) = &reply {
let w = width.saturating_sub(2); let w = width.saturating_sub(2);
let mut replied = r.show_msg(w, style, true); 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 sender_width = UnicodeWidthStr::width(sender.content.as_ref());
let trailing = w.saturating_sub(sender_width + 1); let trailing = w.saturating_sub(sender_width + 1);
@ -793,16 +793,21 @@ impl Message {
} }
} }
fn sender_span(&self, settings: &ApplicationSettings) -> Span { fn sender_span<'a>(
settings.get_user_span(self.sender.as_ref()) &'a self,
info: &'a RoomInfo,
settings: &'a ApplicationSettings,
) -> Span<'a> {
settings.get_user_span(self.sender.as_ref(), info)
} }
fn show_sender( fn show_sender<'a>(
&self, &'a self,
prev: Option<&Message>, prev: Option<&Message>,
align_right: bool, align_right: bool,
settings: &ApplicationSettings, info: &'a RoomInfo,
) -> Option<Span> { settings: &'a ApplicationSettings,
) -> Option<Span<'a>> {
if let Some(prev) = prev { if let Some(prev) = prev {
if self.sender == prev.sender && if self.sender == prev.sender &&
self.timestamp.same_day(&prev.timestamp) && 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 stop = content.len().min(28);
let s = &content[..stop]; let s = &content[..stop];

View file

@ -30,6 +30,7 @@ use crate::{
ProfileConfig, ProfileConfig,
TunableValues, TunableValues,
UserColor, UserColor,
UserDisplayStyle,
UserDisplayTunables, UserDisplayTunables,
}, },
message::{ message::{
@ -160,6 +161,7 @@ pub fn mock_room() -> RoomInfo {
fetch_id: RoomFetchStatus::NotStarted, fetch_id: RoomFetchStatus::NotStarted,
fetch_last: None, fetch_last: None,
users_typing: None, users_typing: None,
display_names: HashMap::new(),
} }
} }
@ -189,6 +191,7 @@ pub fn mock_tunables() -> TunableValues {
.into_iter() .into_iter()
.collect::<HashMap<_, _>>(), .collect::<HashMap<_, _>>(),
open_command: None, open_command: None,
username_display: UserDisplayStyle::Username,
} }
} }

View file

@ -409,7 +409,7 @@ impl WindowOps<IambInfo> for IambWindow {
if need_fetch { if need_fetch {
if let Ok(mems) = store.application.worker.members(room_id.clone()) { 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()); state.set(items.collect());
*last_fetch = Some(Instant::now()); *last_fetch = Some(Instant::now());
} }
@ -1100,11 +1100,12 @@ impl Promptable<ProgramContext, ProgramStore, IambInfo> for VerifyItem {
#[derive(Clone)] #[derive(Clone)]
pub struct MemberItem { pub struct MemberItem {
member: RoomMember, member: RoomMember,
room_id: OwnedRoomId,
} }
impl MemberItem { impl MemberItem {
fn new(member: RoomMember) -> Self { fn new(member: RoomMember, room_id: OwnedRoomId) -> Self {
Self { member } Self { member, room_id }
} }
} }
@ -1121,12 +1122,32 @@ impl ListItem<IambInfo> for MemberItem {
_: &ViewportContext<ListCursor>, _: &ViewportContext<ListCursor>,
store: &mut ProgramStore, store: &mut ProgramStore,
) -> Text { ) -> 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 { 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() { let state = match self.member.membership() {
MembershipState::Ban => Span::raw(" (banned)").into(), MembershipState::Ban => Span::raw(" (banned)").into(),
MembershipState::Invite => Span::raw(" (invited)").into(), MembershipState::Invite => Span::raw(" (invited)").into(),
@ -1136,11 +1157,9 @@ impl ListItem<IambInfo> for MemberItem {
_ => None, _ => None,
}; };
if let Some(state) = state { spans.extend(state);
Spans(vec![user, state]).into()
} else { return Spans(spans).into();
user.into()
}
} }
fn get_word(&self) -> Option<String> { fn get_word(&self) -> Option<String> {

View file

@ -808,7 +808,8 @@ impl<'a> StatefulWidget for Chat<'a> {
state.reply_to.as_ref().and_then(|k| { state.reply_to.as_ref().and_then(|k| {
let room = self.store.application.rooms.get(state.id())?; let room = self.store.application.rooms.get(state.id())?;
let msg = room.messages.get(k)?; 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() { let prefix = if editing.is_some() {
Span::from("Editing reply to ") Span::from("Editing reply to ")
} else { } else {

View file

@ -139,8 +139,9 @@ impl RoomState {
let mut invited = vec![Span::from(format!("You have been invited to join {name}"))]; let mut invited = vec![Span::from(format!("You have been invited to join {name}"))];
if let Ok(Some(inviter)) = &inviter { 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(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); let l1 = Spans(invited);

View file

@ -40,6 +40,7 @@ use matrix_sdk::{
reaction::ReactionEventContent, reaction::ReactionEventContent,
room::{ room::{
encryption::RoomEncryptionEventContent, encryption::RoomEncryptionEventContent,
member::OriginalSyncRoomMemberEvent,
message::{MessageType, RoomMessageEventContent}, message::{MessageType, RoomMessageEventContent},
name::RoomNameEventContent, name::RoomNameEventContent,
redaction::{OriginalSyncRoomRedactionEvent, SyncRoomRedactionEvent}, redaction::{OriginalSyncRoomRedactionEvent, SyncRoomRedactionEvent},
@ -838,6 +839,38 @@ impl ClientWorker {
}, },
); );
let _ = self.client.add_event_handler(
|ev: OriginalSyncRoomMemberEvent,
room: MatrixRoom,
client: Client,
store: Ctx<AsyncProgramStore>| {
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( let _ = self.client.add_event_handler(
|ev: OriginalSyncKeyVerificationStartEvent, |ev: OriginalSyncKeyVerificationStartEvent,
client: Client, client: Client,