Support configuring a user's color and name (#19)

This commit is contained in:
Ulyssa 2023-01-06 16:56:28 -08:00
parent 739aab1534
commit 504b520fe1
No known key found for this signature in database
GPG key ID: 1B3965A3D18B9B64
8 changed files with 343 additions and 98 deletions

2
Cargo.lock generated
View file

@ -1128,7 +1128,7 @@ dependencies = [
[[package]] [[package]]
name = "iamb" name = "iamb"
version = "0.0.1" version = "0.0.2"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",

View file

@ -1,5 +1,9 @@
# iamb # iamb
[![Build Status](https://github.com/ulyssa/iamb/workflows/CI/badge.svg)](https://github.com/ulyssa/iamb/actions?query=workflow%3ACI+)
[![License: Apache 2.0](https://img.shields.io/crates/l/iamb.svg?logo=apache)](https://crates.io/crates/iamb)
[![Latest Version](https://img.shields.io/crates/v/iamb.svg?logo=rust)](https://crates.io/crates/iamb)
## About ## About
`iamb` is a Matrix client for the terminal that uses Vim keybindings. `iamb` is a Matrix client for the terminal that uses Vim keybindings.

View file

@ -41,7 +41,7 @@ use modalkit::{
}; };
use crate::{ use crate::{
message::{user_style, Message, Messages}, message::{Message, Messages},
worker::Requester, worker::Requester,
ApplicationSettings, ApplicationSettings,
}; };
@ -222,24 +222,20 @@ impl RoomInfo {
} }
} }
fn get_typing_spans(&self) -> Spans { fn get_typing_spans(&self, settings: &ApplicationSettings) -> Spans {
let typers = self.get_typers(); let typers = self.get_typers();
let n = typers.len(); let n = typers.len();
match n { match n {
0 => Spans(vec![]), 0 => Spans(vec![]),
1 => { 1 => {
let user = typers[0].as_str(); let user = settings.get_user_span(typers[0].as_ref());
let user = Span::styled(user, user_style(user));
Spans(vec![user, Span::from(" is typing...")]) Spans(vec![user, Span::from(" is typing...")])
}, },
2 => { 2 => {
let user1 = typers[0].as_str(); let user1 = settings.get_user_span(typers[0].as_ref());
let user1 = Span::styled(user1, user_style(user1)); let user2 = settings.get_user_span(typers[1].as_ref());
let user2 = typers[1].as_str();
let user2 = Span::styled(user2, user_style(user2));
Spans(vec![ Spans(vec![
user1, user1,
@ -274,7 +270,7 @@ impl RoomInfo {
let top = Rect::new(area.x, area.y, area.width, area.height - 1); let top = Rect::new(area.x, area.y, area.width, area.height - 1);
let bar = Rect::new(area.x, area.y + top.height, area.width, 1); let bar = Rect::new(area.x, area.y + top.height, area.width, 1);
Paragraph::new(self.get_typing_spans()) Paragraph::new(self.get_typing_spans(settings))
.alignment(Alignment::Center) .alignment(Alignment::Center)
.render(bar, buf); .render(bar, buf);
@ -448,11 +444,14 @@ impl ApplicationInfo for IambInfo {
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use super::*; use super::*;
use crate::config::{user_style, user_style_from_color};
use crate::tests::*; use crate::tests::*;
use modalkit::tui::style::Color;
#[test] #[test]
fn test_typing_spans() { fn test_typing_spans() {
let mut info = RoomInfo::default(); let mut info = RoomInfo::default();
let settings = mock_settings();
let users0 = vec![]; let users0 = vec![];
let users1 = vec![TEST_USER1.clone()]; let users1 = vec![TEST_USER1.clone()];
@ -473,18 +472,18 @@ pub mod tests {
// Nothing set. // Nothing set.
assert_eq!(info.users_typing, None); assert_eq!(info.users_typing, None);
assert_eq!(info.get_typing_spans(), Spans(vec![])); assert_eq!(info.get_typing_spans(&settings), Spans(vec![]));
// Empty typing list. // Empty typing list.
info.set_typing(users0); info.set_typing(users0);
assert!(info.users_typing.is_some()); assert!(info.users_typing.is_some());
assert_eq!(info.get_typing_spans(), Spans(vec![])); assert_eq!(info.get_typing_spans(&settings), Spans(vec![]));
// Single user typing. // Single user typing.
info.set_typing(users1); info.set_typing(users1);
assert!(info.users_typing.is_some()); assert!(info.users_typing.is_some());
assert_eq!( assert_eq!(
info.get_typing_spans(), info.get_typing_spans(&settings),
Spans(vec![ Spans(vec![
Span::styled("@user1:example.com", user_style("@user1:example.com")), Span::styled("@user1:example.com", user_style("@user1:example.com")),
Span::from(" is typing...") Span::from(" is typing...")
@ -495,7 +494,7 @@ pub mod tests {
info.set_typing(users2); info.set_typing(users2);
assert!(info.users_typing.is_some()); assert!(info.users_typing.is_some());
assert_eq!( assert_eq!(
info.get_typing_spans(), info.get_typing_spans(&settings),
Spans(vec![ Spans(vec![
Span::styled("@user1:example.com", user_style("@user1:example.com")), Span::styled("@user1:example.com", user_style("@user1:example.com")),
Span::raw(" and "), Span::raw(" and "),
@ -507,11 +506,22 @@ pub mod tests {
// Four users typing. // Four users typing.
info.set_typing(users4); info.set_typing(users4);
assert!(info.users_typing.is_some()); assert!(info.users_typing.is_some());
assert_eq!(info.get_typing_spans(), Spans::from("Several people are typing...")); assert_eq!(info.get_typing_spans(&settings), Spans::from("Several people are typing..."));
// Five users typing. // Five users typing.
info.set_typing(users5); info.set_typing(users5);
assert!(info.users_typing.is_some()); assert!(info.users_typing.is_some());
assert_eq!(info.get_typing_spans(), Spans::from("Many people are typing...")); assert_eq!(info.get_typing_spans(&settings), Spans::from("Many people are typing..."));
// Test that USER5 gets rendered using the configured color and name.
info.set_typing(vec![TEST_USER5.clone()]);
assert!(info.users_typing.is_some());
assert_eq!(
info.get_typing_spans(&settings),
Spans(vec![
Span::styled("USER 5", user_style_from_color(Color::Black)),
Span::from(" is typing...")
])
);
} }
} }

View file

@ -1,14 +1,22 @@
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt;
use std::fs::File; use std::fs::File;
use std::hash::{Hash, Hasher};
use std::io::BufReader; use std::io::BufReader;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process; use std::process;
use clap::Parser; use clap::Parser;
use matrix_sdk::ruma::OwnedUserId; use matrix_sdk::ruma::{OwnedUserId, UserId};
use serde::Deserialize; use serde::{de::Error as SerdeError, de::Visitor, Deserialize, Deserializer};
use url::Url; use url::Url;
use modalkit::tui::{
style::{Color, Modifier as StyleModifier, Style},
text::Span,
};
macro_rules! usage { macro_rules! usage {
( $($args: tt)* ) => { ( $($args: tt)* ) => {
println!($($args)*); println!($($args)*);
@ -16,6 +24,38 @@ macro_rules! usage {
} }
} }
const COLORS: [Color; 13] = [
Color::Blue,
Color::Cyan,
Color::Green,
Color::LightBlue,
Color::LightGreen,
Color::LightCyan,
Color::LightMagenta,
Color::LightRed,
Color::LightYellow,
Color::Magenta,
Color::Red,
Color::Reset,
Color::Yellow,
];
pub fn user_color(user: &str) -> Color {
let mut hasher = DefaultHasher::new();
user.hash(&mut hasher);
let color = hasher.finish() as usize % COLORS.len();
COLORS[color]
}
pub fn user_style_from_color(color: Color) -> Style {
Style::default().fg(color).add_modifier(StyleModifier::BOLD)
}
pub fn user_style(user: &str) -> Style {
user_style_from_color(user_color(user))
}
fn is_profile_char(c: char) -> bool { fn is_profile_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '.' || c == '-' c.is_ascii_alphanumeric() || c == '.' || c == '-'
} }
@ -69,16 +109,88 @@ pub enum ConfigError {
Invalid(#[from] serde_json::Error), Invalid(#[from] serde_json::Error),
} }
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UserColor(pub Color);
pub struct UserColorVisitor;
impl<'de> Visitor<'de> for UserColorVisitor {
type Value = UserColor;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a valid color")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: SerdeError,
{
match value {
"none" => Ok(UserColor(Color::Reset)),
"red" => Ok(UserColor(Color::Red)),
"black" => Ok(UserColor(Color::Black)),
"green" => Ok(UserColor(Color::Green)),
"yellow" => Ok(UserColor(Color::Yellow)),
"blue" => Ok(UserColor(Color::Blue)),
"magenta" => Ok(UserColor(Color::Magenta)),
"cyan" => Ok(UserColor(Color::Cyan)),
"gray" => Ok(UserColor(Color::Gray)),
"dark-gray" => Ok(UserColor(Color::DarkGray)),
"light-red" => Ok(UserColor(Color::LightRed)),
"light-green" => Ok(UserColor(Color::LightGreen)),
"light-yellow" => Ok(UserColor(Color::LightYellow)),
"light-blue" => Ok(UserColor(Color::LightBlue)),
"light-magenta" => Ok(UserColor(Color::LightMagenta)),
"light-cyan" => Ok(UserColor(Color::LightCyan)),
"white" => Ok(UserColor(Color::White)),
_ => Err(E::custom("Could not parse color")),
}
}
}
impl<'de> Deserialize<'de> for UserColor {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(UserColorVisitor)
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct UserDisplayTunables {
pub color: Option<UserColor>,
pub name: Option<String>,
}
pub type UserOverrides = HashMap<OwnedUserId, UserDisplayTunables>;
fn merge_users(a: Option<UserOverrides>, b: Option<UserOverrides>) -> Option<UserOverrides> {
match (a, b) {
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(Some(mut a), Some(b)) => {
for (k, v) in b {
a.insert(k, v);
}
Some(a)
},
(None, None) => None,
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct TunableValues { pub struct TunableValues {
pub typing_notice: bool, pub typing_notice: bool,
pub typing_notice_display: bool, pub typing_notice_display: bool,
pub users: UserOverrides,
} }
#[derive(Clone, Default, Deserialize)] #[derive(Clone, Default, Deserialize)]
pub struct Tunables { pub struct Tunables {
pub typing_notice: Option<bool>, pub typing_notice: Option<bool>,
pub typing_notice_display: Option<bool>, pub typing_notice_display: Option<bool>,
pub users: Option<UserOverrides>,
} }
impl Tunables { impl Tunables {
@ -86,6 +198,7 @@ impl Tunables {
Tunables { Tunables {
typing_notice: self.typing_notice.or(other.typing_notice), typing_notice: self.typing_notice.or(other.typing_notice),
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),
} }
} }
@ -93,6 +206,7 @@ impl Tunables {
TunableValues { TunableValues {
typing_notice: self.typing_notice.unwrap_or(true), typing_notice: self.typing_notice.unwrap_or(true),
typing_notice_display: self.typing_notice.unwrap_or(true), typing_notice_display: self.typing_notice.unwrap_or(true),
users: self.users.unwrap_or_default(),
} }
} }
} }
@ -255,11 +369,32 @@ impl ApplicationSettings {
Ok(settings) Ok(settings)
} }
pub fn get_user_span<'a>(&self, user_id: &'a UserId) -> Span<'a> {
if let Some(user) = self.tunables.users.get(user_id) {
let color = if let Some(UserColor(c)) = user.color {
c
} else {
user_color(user_id.as_str())
};
let style = user_style_from_color(color);
if let Some(name) = &user.name {
Span::styled(name.clone(), style)
} else {
Span::styled(user_id.as_str(), style)
}
} else {
Span::styled(user_id.as_str(), user_style(user_id.as_str()))
}
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use matrix_sdk::ruma::user_id;
#[test] #[test]
fn test_profile_name_invalid() { fn test_profile_name_invalid() {
@ -283,4 +418,74 @@ mod tests {
assert_eq!(validate_profile_name("a.b-c"), true); assert_eq!(validate_profile_name("a.b-c"), true);
assert_eq!(validate_profile_name("a.B-c"), true); assert_eq!(validate_profile_name("a.B-c"), true);
} }
#[test]
fn test_merge_users() {
let a = None;
let b = vec![(user_id!("@a:b.c").to_owned(), UserDisplayTunables {
color: Some(UserColor(Color::Red)),
name: Some("Hello".into()),
})]
.into_iter()
.collect::<HashMap<_, _>>();
let c = vec![(user_id!("@a:b.c").to_owned(), UserDisplayTunables {
color: Some(UserColor(Color::Green)),
name: Some("World".into()),
})]
.into_iter()
.collect::<HashMap<_, _>>();
let res = merge_users(a.clone(), a.clone());
assert_eq!(res, None);
let res = merge_users(a.clone(), Some(b.clone()));
assert_eq!(res, Some(b.clone()));
let res = merge_users(Some(b.clone()), a.clone());
assert_eq!(res, Some(b.clone()));
let res = merge_users(Some(b.clone()), Some(b.clone()));
assert_eq!(res, Some(b.clone()));
let res = merge_users(Some(b.clone()), Some(c.clone()));
assert_eq!(res, Some(c.clone()));
let res = merge_users(Some(c.clone()), Some(b.clone()));
assert_eq!(res, Some(b.clone()));
}
#[test]
fn test_parse_tunables() {
let res: Tunables = serde_json::from_str("{}").unwrap();
assert_eq!(res.typing_notice, None);
assert_eq!(res.typing_notice_display, None);
assert_eq!(res.users, None);
let res: Tunables = serde_json::from_str("{\"typing_notice\": true}").unwrap();
assert_eq!(res.typing_notice, Some(true));
assert_eq!(res.typing_notice_display, None);
assert_eq!(res.users, None);
let res: Tunables = serde_json::from_str("{\"typing_notice\": false}").unwrap();
assert_eq!(res.typing_notice, Some(false));
assert_eq!(res.typing_notice_display, None);
assert_eq!(res.users, None);
let res: Tunables = serde_json::from_str("{\"users\": {}}").unwrap();
assert_eq!(res.typing_notice, None);
assert_eq!(res.typing_notice_display, None);
assert_eq!(res.users, Some(HashMap::new()));
let res: Tunables = serde_json::from_str(
"{\"users\": {\"@a:b.c\": {\"color\": \"black\", \"name\": \"Tim\"}}}",
)
.unwrap();
assert_eq!(res.typing_notice, None);
assert_eq!(res.typing_notice_display, None);
let users = vec![(user_id!("@a:b.c").to_owned(), UserDisplayTunables {
color: Some(UserColor(Color::Black)),
name: Some("Tim".into()),
})];
assert_eq!(res.users, Some(users.into_iter().collect()));
}
} }

View file

@ -22,35 +22,22 @@ use matrix_sdk::ruma::{
}; };
use modalkit::tui::{ use modalkit::tui::{
style::{Color, Modifier as StyleModifier, Style}, style::{Modifier as StyleModifier, Style},
text::{Span, Spans, Text}, text::{Span, Spans, Text},
}; };
use modalkit::editing::{base::ViewportContext, cursor::Cursor}; use modalkit::editing::{base::ViewportContext, cursor::Cursor};
use crate::base::{IambResult, RoomInfo}; use crate::{
base::{IambResult, RoomInfo},
config::ApplicationSettings,
};
pub type MessageEvent = MessageLikeEvent<RoomMessageEventContent>; pub type MessageEvent = MessageLikeEvent<RoomMessageEventContent>;
pub type MessageFetchResult = IambResult<(Option<String>, Vec<MessageEvent>)>; pub type MessageFetchResult = IambResult<(Option<String>, Vec<MessageEvent>)>;
pub type MessageKey = (MessageTimeStamp, OwnedEventId); pub type MessageKey = (MessageTimeStamp, OwnedEventId);
pub type Messages = BTreeMap<MessageKey, Message>; pub type Messages = BTreeMap<MessageKey, Message>;
const COLORS: [Color; 13] = [
Color::Blue,
Color::Cyan,
Color::Green,
Color::LightBlue,
Color::LightGreen,
Color::LightCyan,
Color::LightMagenta,
Color::LightRed,
Color::LightYellow,
Color::Magenta,
Color::Red,
Color::Reset,
Color::Yellow,
];
const USER_GUTTER: usize = 30; const USER_GUTTER: usize = 30;
const TIME_GUTTER: usize = 12; const TIME_GUTTER: usize = 12;
const MIN_MSG_LEN: usize = 30; const MIN_MSG_LEN: usize = 30;
@ -66,18 +53,6 @@ const USER_GUTTER_EMPTY_SPAN: Span<'static> = Span {
}, },
}; };
pub(crate) fn user_color(user: &str) -> Color {
let mut hasher = DefaultHasher::new();
user.hash(&mut hasher);
let color = hasher.finish() as usize % COLORS.len();
COLORS[color]
}
pub(crate) fn user_style(user: &str) -> Style {
Style::default().fg(user_color(user)).add_modifier(StyleModifier::BOLD)
}
struct WrappedLinesIterator<'a> { struct WrappedLinesIterator<'a> {
iter: Lines<'a>, iter: Lines<'a>,
curr: Option<&'a str>, curr: Option<&'a str>,
@ -390,7 +365,13 @@ impl Message {
Message { content, sender, timestamp } Message { content, sender, timestamp }
} }
pub fn show(&self, selected: bool, vwctx: &ViewportContext<MessageCursor>) -> Text { pub fn show(
&self,
prev: Option<&Message>,
selected: bool,
vwctx: &ViewportContext<MessageCursor>,
settings: &ApplicationSettings,
) -> Text {
let width = vwctx.get_width(); let width = vwctx.get_width();
let msg = self.as_ref(); let msg = self.as_ref();
@ -414,7 +395,7 @@ impl Message {
let trailing = Span::styled(space(lw.saturating_sub(w)), style); let trailing = Span::styled(space(lw.saturating_sub(w)), style);
if i == 0 { if i == 0 {
let user = self.show_sender(true); let user = self.show_sender(prev, true, settings);
if let Some(time) = self.timestamp.show() { if let Some(time) = self.timestamp.show() {
lines.push(Spans(vec![user, line, trailing, time])) lines.push(Spans(vec![user, line, trailing, time]))
@ -435,7 +416,7 @@ impl Message {
let trailing = Span::styled(space(lw.saturating_sub(w)), style); let trailing = Span::styled(space(lw.saturating_sub(w)), style);
let prefix = if i == 0 { let prefix = if i == 0 {
self.show_sender(true) self.show_sender(prev, true, settings)
} else { } else {
USER_GUTTER_EMPTY_SPAN USER_GUTTER_EMPTY_SPAN
}; };
@ -443,7 +424,7 @@ impl Message {
lines.push(Spans(vec![prefix, line, trailing])) lines.push(Spans(vec![prefix, line, trailing]))
} }
} else { } else {
lines.push(Spans::from(self.show_sender(false))); lines.push(Spans::from(self.show_sender(prev, false, settings)));
for (line, _) in wrap(msg, width.saturating_sub(2)) { for (line, _) in wrap(msg, width.saturating_sub(2)) {
let line = format!(" {}", line); let line = format!(" {}", line);
@ -456,14 +437,26 @@ impl Message {
return Text { lines }; return Text { lines };
} }
fn show_sender(&self, align_right: bool) -> Span { fn show_sender(
let sender = self.sender.to_string(); &self,
let style = user_style(sender.as_str()); prev: Option<&Message>,
align_right: bool,
settings: &ApplicationSettings,
) -> Span {
let user = if matches!(prev, Some(prev) if self.sender == prev.sender) {
USER_GUTTER_EMPTY_SPAN
} else {
settings.get_user_span(self.sender.as_ref())
};
let Span { content, style } = user;
let stop = content.len().min(28);
let s = &content[..stop];
let sender = if align_right { let sender = if align_right {
format!("{: >width$} ", sender, width = 28) format!("{: >width$} ", s, width = 28)
} else { } else {
format!("{: <width$} ", sender, width = 28) format!("{: <width$} ", s, width = 28)
}; };
Span::styled(sender, style) Span::styled(sender, style)

View file

@ -1,4 +1,6 @@
use std::collections::BTreeMap; use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf;
use std::sync::mpsc::sync_channel;
use matrix_sdk::ruma::{ use matrix_sdk::ruma::{
event_id, event_id,
@ -12,15 +14,20 @@ use matrix_sdk::ruma::{
UInt, UInt,
}; };
use std::path::PathBuf;
use std::sync::mpsc::sync_channel;
use url::Url;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use modalkit::tui::style::Color;
use url::Url;
use crate::{ use crate::{
base::{ChatStore, ProgramStore, RoomFetchStatus, RoomInfo}, base::{ChatStore, ProgramStore, RoomFetchStatus, RoomInfo},
config::{ApplicationSettings, DirectoryValues, ProfileConfig, TunableValues}, config::{
ApplicationSettings,
DirectoryValues,
ProfileConfig,
TunableValues,
UserColor,
UserDisplayTunables,
},
message::{ message::{
Message, Message,
MessageContent, MessageContent,
@ -35,9 +42,9 @@ lazy_static! {
pub static ref TEST_ROOM1_ID: OwnedRoomId = RoomId::new(server_name!("example.com")).to_owned(); pub static ref TEST_ROOM1_ID: OwnedRoomId = RoomId::new(server_name!("example.com")).to_owned();
pub static ref TEST_USER1: OwnedUserId = user_id!("@user1:example.com").to_owned(); pub static ref TEST_USER1: OwnedUserId = user_id!("@user1:example.com").to_owned();
pub static ref TEST_USER2: OwnedUserId = user_id!("@user2:example.com").to_owned(); pub static ref TEST_USER2: OwnedUserId = user_id!("@user2:example.com").to_owned();
pub static ref TEST_USER3: OwnedUserId = user_id!("@user2:example.com").to_owned(); pub static ref TEST_USER3: OwnedUserId = user_id!("@user3:example.com").to_owned();
pub static ref TEST_USER4: OwnedUserId = user_id!("@user2:example.com").to_owned(); pub static ref TEST_USER4: OwnedUserId = user_id!("@user4:example.com").to_owned();
pub static ref TEST_USER5: OwnedUserId = user_id!("@user2:example.com").to_owned(); pub static ref TEST_USER5: OwnedUserId = user_id!("@user5:example.com").to_owned();
pub static ref MSG1_KEY: MessageKey = (LocalEcho, EventId::new(server_name!("example.com"))); pub static ref MSG1_KEY: MessageKey = (LocalEcho, EventId::new(server_name!("example.com")));
pub static ref MSG2_KEY: MessageKey = pub static ref MSG2_KEY: MessageKey =
(OriginServer(UInt::new(1).unwrap()), EventId::new(server_name!("example.com"))); (OriginServer(UInt::new(1).unwrap()), EventId::new(server_name!("example.com")));
@ -118,6 +125,19 @@ pub fn mock_dirs() -> DirectoryValues {
} }
} }
pub fn mock_tunables() -> TunableValues {
TunableValues {
typing_notice: true,
typing_notice_display: true,
users: vec![(TEST_USER5.clone(), UserDisplayTunables {
color: Some(UserColor(Color::Black)),
name: Some("USER 5".into()),
})]
.into_iter()
.collect::<HashMap<_, _>>(),
}
}
pub fn mock_settings() -> ApplicationSettings { pub fn mock_settings() -> ApplicationSettings {
ApplicationSettings { ApplicationSettings {
matrix_dir: PathBuf::new(), matrix_dir: PathBuf::new(),
@ -129,7 +149,7 @@ pub fn mock_settings() -> ApplicationSettings {
settings: None, settings: None,
dirs: None, dirs: None,
}, },
tunables: TunableValues { typing_notice: true, typing_notice_display: true }, tunables: mock_tunables(),
dirs: mock_dirs(), dirs: mock_dirs(),
} }
} }

View file

@ -51,19 +51,16 @@ use modalkit::{
}, },
}; };
use crate::{ use crate::base::{
base::{ ChatStore,
ChatStore, IambBufferId,
IambBufferId, IambId,
IambId, IambInfo,
IambInfo, IambResult,
IambResult, ProgramAction,
ProgramAction, ProgramContext,
ProgramContext, ProgramStore,
ProgramStore, RoomAction,
RoomAction,
},
message::user_style,
}; };
use self::{room::RoomState, welcome::WelcomeState}; use self::{room::RoomState, welcome::WelcomeState};
@ -845,15 +842,18 @@ impl ToString for MemberItem {
} }
impl ListItem<IambInfo> for MemberItem { impl ListItem<IambInfo> for MemberItem {
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text { fn show(
let mut style = user_style(self.member.user_id().as_str()); &self,
selected: bool,
_: &ViewportContext<ListCursor>,
store: &mut ProgramStore,
) -> Text {
let mut user = store.application.settings.get_user_span(self.member.user_id());
if selected { if selected {
style = style.add_modifier(StyleModifier::REVERSED); user.style = user.style.add_modifier(StyleModifier::REVERSED);
} }
let user = Span::styled(self.to_string(), 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(),

View file

@ -61,6 +61,7 @@ use modalkit::editing::{
use crate::{ use crate::{
base::{IambBufferId, IambInfo, ProgramContext, ProgramStore, RoomFocus, RoomInfo}, base::{IambBufferId, IambInfo, ProgramContext, ProgramStore, RoomFocus, RoomInfo},
config::ApplicationSettings,
message::{Message, MessageCursor, MessageKey}, message::{Message, MessageCursor, MessageKey},
}; };
@ -148,7 +149,13 @@ impl ScrollbackState {
} }
} }
fn scrollview(&mut self, idx: MessageKey, pos: MovePosition, info: &RoomInfo) { fn scrollview(
&mut self,
idx: MessageKey,
pos: MovePosition,
info: &RoomInfo,
settings: &ApplicationSettings,
) {
let selidx = if let Some(key) = self.cursor.to_key(info) { let selidx = if let Some(key) = self.cursor.to_key(info) {
key key
} else { } else {
@ -165,7 +172,7 @@ impl ScrollbackState {
for (key, item) in info.messages.range(..=&idx).rev() { for (key, item) in info.messages.range(..=&idx).rev() {
let sel = selidx == key; let sel = selidx == key;
let len = item.show(sel, &self.viewctx).lines.len(); let len = item.show(None, sel, &self.viewctx, settings).lines.len();
if key == &idx { if key == &idx {
lines += len / 2; lines += len / 2;
@ -187,7 +194,7 @@ impl ScrollbackState {
for (key, item) in info.messages.range(..=&idx).rev() { for (key, item) in info.messages.range(..=&idx).rev() {
let sel = key == selidx; let sel = key == selidx;
let len = item.show(sel, &self.viewctx).lines.len(); let len = item.show(None, sel, &self.viewctx, settings).lines.len();
lines += len; lines += len;
@ -202,7 +209,7 @@ impl ScrollbackState {
} }
} }
fn shift_cursor(&mut self, info: &RoomInfo) { fn shift_cursor(&mut self, info: &RoomInfo, settings: &ApplicationSettings) {
let last_key = if let Some(k) = info.messages.last_key_value() { let last_key = if let Some(k) = info.messages.last_key_value() {
k.0 k.0
} else { } else {
@ -227,7 +234,7 @@ impl ScrollbackState {
break; break;
} }
lines += item.show(false, &self.viewctx).height().max(1); lines += item.show(None, false, &self.viewctx, settings).height().max(1);
if lines >= self.viewctx.get_height() { if lines >= self.viewctx.get_height() {
// We've reached the end of the viewport; move cursor into it. // We've reached the end of the viewport; move cursor into it.
@ -930,7 +937,8 @@ impl ScrollActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
ctx: &ProgramContext, ctx: &ProgramContext,
store: &mut ProgramStore, store: &mut ProgramStore,
) -> EditResult<EditInfo, IambInfo> { ) -> EditResult<EditInfo, IambInfo> {
let info = store.application.get_room_info(self.room_id.clone()); let info = store.application.rooms.entry(self.room_id.clone()).or_default();
let settings = &store.application.settings;
let mut corner = self.viewctx.corner.clone(); let mut corner = self.viewctx.corner.clone();
let last_key = if let Some(k) = info.messages.last_key_value() { let last_key = if let Some(k) = info.messages.last_key_value() {
@ -956,7 +964,7 @@ impl ScrollActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
for (key, item) in info.messages.range(..=&corner_key).rev() { for (key, item) in info.messages.range(..=&corner_key).rev() {
let sel = key == cursor_key; let sel = key == cursor_key;
let txt = item.show(sel, &self.viewctx); let txt = item.show(None, sel, &self.viewctx, settings);
let len = txt.height().max(1); let len = txt.height().max(1);
let max = len.saturating_sub(1); let max = len.saturating_sub(1);
@ -982,7 +990,7 @@ impl ScrollActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
MoveDir2D::Down => { MoveDir2D::Down => {
for (key, item) in info.messages.range(&corner_key..) { for (key, item) in info.messages.range(&corner_key..) {
let sel = key == cursor_key; let sel = key == cursor_key;
let txt = item.show(sel, &self.viewctx); let txt = item.show(None, sel, &self.viewctx, settings);
let len = txt.height().max(1); let len = txt.height().max(1);
let max = len.saturating_sub(1); let max = len.saturating_sub(1);
@ -1018,7 +1026,7 @@ impl ScrollActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
} }
self.viewctx.corner = corner; self.viewctx.corner = corner;
self.shift_cursor(info); self.shift_cursor(info, settings);
Ok(None) Ok(None)
} }
@ -1038,10 +1046,11 @@ impl ScrollActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
Err(err) Err(err)
}, },
Axis::Vertical => { Axis::Vertical => {
let info = store.application.get_room_info(self.room_id.clone()); let info = store.application.rooms.entry(self.room_id.clone()).or_default();
let settings = &store.application.settings;
if let Some(key) = self.cursor.to_key(info).cloned() { if let Some(key) = self.cursor.to_key(info).cloned() {
self.scrollview(key, pos, info); self.scrollview(key, pos, info, settings);
} }
Ok(None) Ok(None)
@ -1126,6 +1135,7 @@ impl<'a> StatefulWidget for Scrollback<'a> {
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let info = self.store.application.rooms.entry(state.room_id.clone()).or_default(); let info = self.store.application.rooms.entry(state.room_id.clone()).or_default();
let settings = &self.store.application.settings;
let area = info.render_typing(area, buf, &self.store.application.settings); let area = info.render_typing(area, buf, &self.store.application.settings);
state.set_term_info(area); state.set_term_info(area);
@ -1157,10 +1167,13 @@ impl<'a> StatefulWidget for Scrollback<'a> {
let mut lines = vec![]; let mut lines = vec![];
let mut sawit = false; let mut sawit = false;
let mut prev = None;
for (key, item) in info.messages.range(&corner_key..) { for (key, item) in info.messages.range(&corner_key..) {
let sel = key == cursor_key; let sel = key == cursor_key;
let txt = item.show(self.focused && sel, &state.viewctx); let txt = item.show(prev, self.focused && sel, &state.viewctx, settings);
prev = Some(item);
for (row, line) in txt.lines.into_iter().enumerate() { for (row, line) in txt.lines.into_iter().enumerate() {
if sawit && lines.len() >= height { if sawit && lines.len() >= height {