Skip to content

Commit

Permalink
Support hiding server part of username in message scrollback (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
ulyssa committed Jul 7, 2023
1 parent 61aba80 commit 64891ec
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 37 deletions.
13 changes: 8 additions & 5 deletions src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OwnedUserId>)>,

/// The display names for users in this room.
pub display_names: HashMap<OwnedUserId, String>,
}

impl RoomInfo {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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::<Vec<_>>().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 {
Expand Down
78 changes: 70 additions & 8 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use modalkit::tui::{
text::Span,
};

use super::base::IambId;
use super::base::{IambId, RoomInfo};

macro_rules! usage {
( $($args: tt)* ) => {
Expand Down Expand Up @@ -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)]
pub struct TunableValues {
pub log_level: Level,
Expand All @@ -238,6 +256,7 @@ pub struct TunableValues {
pub typing_notice_send: bool,
pub typing_notice_display: bool,
pub users: UserOverrides,
pub username_display: UserDisplayStyle,
pub default_room: Option<String>,
pub open_command: Option<Vec<String>>,
}
Expand All @@ -253,6 +272,7 @@ pub struct Tunables {
pub typing_notice_send: Option<bool>,
pub typing_notice_display: Option<bool>,
pub users: Option<UserOverrides>,
pub username_display: Option<UserDisplayStyle>,
pub default_room: Option<String>,
pub open_command: Option<Vec<String>>,
}
Expand All @@ -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),
}
Expand All @@ -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,
}
Expand Down Expand Up @@ -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<Color>, Option<Cow<'static, str>>) {
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)
}
Expand Down Expand Up @@ -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());
Expand Down
29 changes: 17 additions & 12 deletions src/message/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -655,23 +655,23 @@ 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();

MessageFormatter { settings, cols, orig, fill, user, date, time, read }
} 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();

MessageFormatter { settings, cols, orig, fill, user, date, time, read }
} 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();

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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<Span> {
info: &'a RoomInfo,
settings: &'a ApplicationSettings,
) -> Option<Span<'a>> {
if let Some(prev) = prev {
if self.sender == prev.sender &&
self.timestamp.same_day(&prev.timestamp) &&
Expand All @@ -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];

Expand Down
3 changes: 3 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use crate::{
ProfileConfig,
TunableValues,
UserColor,
UserDisplayStyle,
UserDisplayTunables,
},
message::{
Expand Down Expand Up @@ -160,6 +161,7 @@ pub fn mock_room() -> RoomInfo {
fetch_id: RoomFetchStatus::NotStarted,
fetch_last: None,
users_typing: None,
display_names: HashMap::new(),
}
}

Expand Down Expand Up @@ -189,6 +191,7 @@ pub fn mock_tunables() -> TunableValues {
.into_iter()
.collect::<HashMap<_, _>>(),
open_command: None,
username_display: UserDisplayStyle::Username,
}
}

Expand Down
39 changes: 29 additions & 10 deletions src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ impl WindowOps<IambInfo> 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());
}
Expand Down Expand Up @@ -1100,11 +1100,12 @@ impl Promptable<ProgramContext, ProgramStore, IambInfo> 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 }
}
}

Expand All @@ -1121,12 +1122,32 @@ impl ListItem<IambInfo> for MemberItem {
_: &ViewportContext<ListCursor>,
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(),
Expand All @@ -1136,11 +1157,9 @@ impl ListItem<IambInfo> 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<String> {
Expand Down
3 changes: 2 additions & 1 deletion src/windows/room/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion src/windows/room/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 64891ec

Please sign in to comment.