mirror of
https://github.com/youwen5/iamb.git
synced 2025-06-20 05:39:52 -07:00
Support sending and displaying read markers (#11)
This commit is contained in:
parent
d8713141f2
commit
afe892c7fe
7 changed files with 270 additions and 94 deletions
31
src/base.rs
31
src/base.rs
|
@ -214,6 +214,8 @@ pub type AsyncProgramStore = Arc<AsyncMutex<ProgramStore>>;
|
|||
|
||||
pub type IambResult<T> = UIResult<T, IambInfo>;
|
||||
|
||||
pub type Receipts = HashMap<OwnedEventId, Vec<OwnedUserId>>;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum IambError {
|
||||
#[error("Invalid user identifier: {0}")]
|
||||
|
@ -286,6 +288,9 @@ pub struct RoomInfo {
|
|||
pub keys: HashMap<OwnedEventId, MessageKey>,
|
||||
pub messages: Messages,
|
||||
|
||||
pub receipts: HashMap<OwnedEventId, Vec<OwnedUserId>>,
|
||||
pub read_till: Option<OwnedEventId>,
|
||||
|
||||
pub fetch_id: RoomFetchStatus,
|
||||
pub fetch_last: Option<Instant>,
|
||||
pub users_typing: Option<(Instant, Vec<OwnedUserId>)>,
|
||||
|
@ -316,7 +321,7 @@ impl RoomInfo {
|
|||
MessageEvent::Original(orig) => {
|
||||
orig.content = *new_content;
|
||||
},
|
||||
MessageEvent::Local(content) => {
|
||||
MessageEvent::Local(_, content) => {
|
||||
*content = new_content;
|
||||
},
|
||||
MessageEvent::Redacted(_) => {
|
||||
|
@ -364,7 +369,7 @@ impl RoomInfo {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_typing_spans(&self, settings: &ApplicationSettings) -> Spans {
|
||||
fn get_typing_spans<'a>(&'a self, settings: &'a ApplicationSettings) -> Spans<'a> {
|
||||
let typers = self.get_typers();
|
||||
let n = typers.len();
|
||||
|
||||
|
@ -454,6 +459,26 @@ impl ChatStore {
|
|||
.unwrap_or_else(|| "Untitled Matrix Room".to_string())
|
||||
}
|
||||
|
||||
pub async fn set_receipts(&mut self, receipts: Vec<(OwnedRoomId, Receipts)>) {
|
||||
let mut updates = vec![];
|
||||
|
||||
for (room_id, receipts) in receipts.into_iter() {
|
||||
if let Some(info) = self.rooms.get_mut(&room_id) {
|
||||
info.receipts = receipts;
|
||||
|
||||
if let Some(read_till) = info.read_till.take() {
|
||||
updates.push((room_id, read_till));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (room_id, read_till) in updates.into_iter() {
|
||||
if let Some(room) = self.worker.client.get_joined_room(&room_id) {
|
||||
let _ = room.read_receipt(read_till.as_ref()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_for_load(&mut self, room_id: OwnedRoomId) {
|
||||
self.need_load.insert(room_id);
|
||||
}
|
||||
|
@ -588,7 +613,7 @@ impl ApplicationInfo for IambInfo {
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::config::{user_style, user_style_from_color};
|
||||
use crate::config::user_style_from_color;
|
||||
use crate::tests::*;
|
||||
use modalkit::tui::style::Color;
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
|
@ -52,10 +53,6 @@ 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 {
|
||||
c.is_ascii_alphanumeric() || c == '.' || c == '-'
|
||||
}
|
||||
|
@ -181,14 +178,18 @@ fn merge_users(a: Option<UserOverrides>, b: Option<UserOverrides>) -> Option<Use
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct TunableValues {
|
||||
pub typing_notice: bool,
|
||||
pub read_receipt_send: bool,
|
||||
pub read_receipt_display: bool,
|
||||
pub typing_notice_send: bool,
|
||||
pub typing_notice_display: bool,
|
||||
pub users: UserOverrides,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize)]
|
||||
pub struct Tunables {
|
||||
pub typing_notice: Option<bool>,
|
||||
pub read_receipt_send: Option<bool>,
|
||||
pub read_receipt_display: Option<bool>,
|
||||
pub typing_notice_send: Option<bool>,
|
||||
pub typing_notice_display: Option<bool>,
|
||||
pub users: Option<UserOverrides>,
|
||||
}
|
||||
|
@ -196,7 +197,9 @@ pub struct Tunables {
|
|||
impl Tunables {
|
||||
fn merge(self, other: Self) -> Self {
|
||||
Tunables {
|
||||
typing_notice: self.typing_notice.or(other.typing_notice),
|
||||
read_receipt_send: self.read_receipt_send.or(other.read_receipt_send),
|
||||
read_receipt_display: self.read_receipt_display.or(other.read_receipt_display),
|
||||
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),
|
||||
}
|
||||
|
@ -204,8 +207,10 @@ impl Tunables {
|
|||
|
||||
fn values(self) -> TunableValues {
|
||||
TunableValues {
|
||||
typing_notice: self.typing_notice.unwrap_or(true),
|
||||
typing_notice_display: self.typing_notice.unwrap_or(true),
|
||||
read_receipt_send: self.read_receipt_send.unwrap_or(true),
|
||||
read_receipt_display: self.read_receipt_display.unwrap_or(true),
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
@ -374,24 +379,41 @@ impl ApplicationSettings {
|
|||
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())
|
||||
};
|
||||
pub fn get_user_char_span<'a>(&self, user_id: &'a UserId) -> Span<'a> {
|
||||
let (color, c) = self
|
||||
.tunables
|
||||
.users
|
||||
.get(user_id)
|
||||
.map(|user| {
|
||||
(
|
||||
user.color.as_ref().map(|c| c.0),
|
||||
user.name.as_ref().and_then(|s| s.chars().next()),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let color = color.unwrap_or_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()))
|
||||
let c = c.unwrap_or_else(|| user_id.localpart().chars().next().unwrap_or(' '));
|
||||
|
||||
Span::styled(String::from(c), style)
|
||||
}
|
||||
|
||||
pub fn get_user_span<'a>(&self, user_id: &'a UserId) -> Span<'a> {
|
||||
let (color, name) = 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();
|
||||
|
||||
let user_id = user_id.as_str();
|
||||
let color = color.unwrap_or_else(|| user_color(user_id));
|
||||
let style = user_style_from_color(color);
|
||||
let name = name.unwrap_or(Cow::Borrowed(user_id));
|
||||
|
||||
Span::styled(name, style)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -461,22 +483,22 @@ mod tests {
|
|||
#[test]
|
||||
fn test_parse_tunables() {
|
||||
let res: Tunables = serde_json::from_str("{}").unwrap();
|
||||
assert_eq!(res.typing_notice, None);
|
||||
assert_eq!(res.typing_notice_send, 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));
|
||||
let res: Tunables = serde_json::from_str("{\"typing_notice_send\": true}").unwrap();
|
||||
assert_eq!(res.typing_notice_send, 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));
|
||||
let res: Tunables = serde_json::from_str("{\"typing_notice_send\": false}").unwrap();
|
||||
assert_eq!(res.typing_notice_send, 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_send, None);
|
||||
assert_eq!(res.typing_notice_display, None);
|
||||
assert_eq!(res.users, Some(HashMap::new()));
|
||||
|
||||
|
@ -484,7 +506,7 @@ mod tests {
|
|||
"{\"users\": {\"@a:b.c\": {\"color\": \"black\", \"name\": \"Tim\"}}}",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res.typing_notice, None);
|
||||
assert_eq!(res.typing_notice_send, None);
|
||||
assert_eq!(res.typing_notice_display, None);
|
||||
let users = vec![(user_id!("@a:b.c").to_owned(), UserDisplayTunables {
|
||||
color: Some(UserColor(Color::Black)),
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::collections::hash_map::DefaultHasher;
|
|||
use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::slice::Iter;
|
||||
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
@ -25,6 +26,7 @@ use matrix_sdk::ruma::{
|
|||
},
|
||||
Redact,
|
||||
},
|
||||
EventId,
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
OwnedEventId,
|
||||
OwnedUserId,
|
||||
|
@ -54,20 +56,28 @@ pub type MessageFetchResult = IambResult<(Option<String>, Vec<RoomMessageEvent>)
|
|||
pub type MessageKey = (MessageTimeStamp, OwnedEventId);
|
||||
pub type Messages = BTreeMap<MessageKey, Message>;
|
||||
|
||||
const USER_GUTTER: usize = 30;
|
||||
const TIME_GUTTER: usize = 12;
|
||||
const MIN_MSG_LEN: usize = 30;
|
||||
|
||||
const USER_GUTTER_EMPTY: &str = " ";
|
||||
const USER_GUTTER_EMPTY_SPAN: Span<'static> = Span {
|
||||
content: Cow::Borrowed(USER_GUTTER_EMPTY),
|
||||
const fn span_static(s: &'static str) -> Span<'static> {
|
||||
Span {
|
||||
content: Cow::Borrowed(s),
|
||||
style: Style {
|
||||
fg: None,
|
||||
bg: None,
|
||||
add_modifier: StyleModifier::empty(),
|
||||
sub_modifier: StyleModifier::empty(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const USER_GUTTER: usize = 30;
|
||||
const TIME_GUTTER: usize = 12;
|
||||
const READ_GUTTER: usize = 5;
|
||||
const MIN_MSG_LEN: usize = 30;
|
||||
|
||||
const USER_GUTTER_EMPTY: &str = " ";
|
||||
const USER_GUTTER_EMPTY_SPAN: Span<'static> = span_static(USER_GUTTER_EMPTY);
|
||||
|
||||
const TIME_GUTTER_EMPTY: &str = " ";
|
||||
const TIME_GUTTER_EMPTY_SPAN: Span<'static> = span_static(TIME_GUTTER_EMPTY);
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum TimeStampIntError {
|
||||
|
@ -271,10 +281,18 @@ impl PartialOrd for MessageCursor {
|
|||
pub enum MessageEvent {
|
||||
Original(Box<OriginalRoomMessageEvent>),
|
||||
Redacted(Box<RedactedRoomMessageEvent>),
|
||||
Local(Box<RoomMessageEventContent>),
|
||||
Local(OwnedEventId, Box<RoomMessageEventContent>),
|
||||
}
|
||||
|
||||
impl MessageEvent {
|
||||
pub fn event_id(&self) -> &EventId {
|
||||
match self {
|
||||
MessageEvent::Original(ev) => ev.event_id.as_ref(),
|
||||
MessageEvent::Redacted(ev) => ev.event_id.as_ref(),
|
||||
MessageEvent::Local(event_id, _) => event_id.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn body(&self) -> Cow<'_, str> {
|
||||
match self {
|
||||
MessageEvent::Original(ev) => body_cow_content(&ev.content),
|
||||
|
@ -292,7 +310,7 @@ impl MessageEvent {
|
|||
Cow::Borrowed("[Redacted]")
|
||||
}
|
||||
},
|
||||
MessageEvent::Local(content) => body_cow_content(content),
|
||||
MessageEvent::Local(_, content) => body_cow_content(content),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -300,7 +318,7 @@ impl MessageEvent {
|
|||
let content = match self {
|
||||
MessageEvent::Original(ev) => &ev.content,
|
||||
MessageEvent::Redacted(_) => return None,
|
||||
MessageEvent::Local(content) => content,
|
||||
MessageEvent::Local(_, content) => content,
|
||||
};
|
||||
|
||||
if let MessageType::Text(content) = &content.msgtype {
|
||||
|
@ -317,7 +335,7 @@ impl MessageEvent {
|
|||
pub fn redact(&mut self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) {
|
||||
match self {
|
||||
MessageEvent::Redacted(_) => return,
|
||||
MessageEvent::Local(_) => return,
|
||||
MessageEvent::Local(_, _) => return,
|
||||
MessageEvent::Original(ev) => {
|
||||
let redacted = ev.clone().redact(redaction, version);
|
||||
*self = MessageEvent::Redacted(Box::new(redacted));
|
||||
|
@ -354,27 +372,65 @@ fn body_cow_content(content: &RoomMessageEventContent) -> Cow<'_, str> {
|
|||
Cow::Borrowed(s)
|
||||
}
|
||||
|
||||
enum MessageColumns<'a> {
|
||||
Three(usize, Option<Span<'a>>, Option<Span<'a>>),
|
||||
Two(usize, Option<Span<'a>>),
|
||||
One(usize, Option<Span<'a>>),
|
||||
enum MessageColumns {
|
||||
/// Four columns: sender, message, timestamp, read receipts.
|
||||
Four,
|
||||
|
||||
/// Three columns: sender, message, timestamp.
|
||||
Three,
|
||||
|
||||
/// Two columns: sender, message.
|
||||
Two,
|
||||
|
||||
/// One column: message with sender on line before the message.
|
||||
One,
|
||||
}
|
||||
|
||||
impl<'a> MessageColumns<'a> {
|
||||
struct MessageFormatter<'a> {
|
||||
settings: &'a ApplicationSettings,
|
||||
cols: MessageColumns,
|
||||
fill: usize,
|
||||
user: Option<Span<'a>>,
|
||||
time: Option<Span<'a>>,
|
||||
read: Iter<'a, OwnedUserId>,
|
||||
}
|
||||
|
||||
impl<'a> MessageFormatter<'a> {
|
||||
fn width(&self) -> usize {
|
||||
match self {
|
||||
MessageColumns::Three(fill, _, _) => *fill,
|
||||
MessageColumns::Two(fill, _) => *fill,
|
||||
MessageColumns::One(fill, _) => *fill,
|
||||
}
|
||||
self.fill
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn push_spans(&mut self, spans: Spans<'a>, style: Style, text: &mut Text<'a>) {
|
||||
match self {
|
||||
MessageColumns::Three(_, user, time) => {
|
||||
let user = user.take().unwrap_or(USER_GUTTER_EMPTY_SPAN);
|
||||
let time = time.take().unwrap_or_else(|| Span::from(""));
|
||||
match self.cols {
|
||||
MessageColumns::Four => {
|
||||
let settings = self.settings;
|
||||
let user = self.user.take().unwrap_or(USER_GUTTER_EMPTY_SPAN);
|
||||
let time = self.time.take().unwrap_or(TIME_GUTTER_EMPTY_SPAN);
|
||||
|
||||
let mut line = vec![user];
|
||||
line.extend(spans.0);
|
||||
line.push(time);
|
||||
|
||||
// Show read receipts.
|
||||
let user_char =
|
||||
|user: &'a OwnedUserId| -> Span<'a> { settings.get_user_char_span(user) };
|
||||
|
||||
let a = self.read.next().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
||||
let b = self.read.next().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
||||
let c = self.read.next().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
||||
|
||||
line.push(Span::raw(" "));
|
||||
line.push(c);
|
||||
line.push(b);
|
||||
line.push(a);
|
||||
line.push(Span::raw(" "));
|
||||
|
||||
text.lines.push(Spans(line))
|
||||
},
|
||||
MessageColumns::Three => {
|
||||
let user = self.user.take().unwrap_or(USER_GUTTER_EMPTY_SPAN);
|
||||
let time = self.time.take().unwrap_or_else(|| Span::from(""));
|
||||
|
||||
let mut line = vec![user];
|
||||
line.extend(spans.0);
|
||||
|
@ -382,15 +438,15 @@ impl<'a> MessageColumns<'a> {
|
|||
|
||||
text.lines.push(Spans(line))
|
||||
},
|
||||
MessageColumns::Two(_, opt) => {
|
||||
let user = opt.take().unwrap_or(USER_GUTTER_EMPTY_SPAN);
|
||||
MessageColumns::Two => {
|
||||
let user = self.user.take().unwrap_or(USER_GUTTER_EMPTY_SPAN);
|
||||
let mut line = vec![user];
|
||||
line.extend(spans.0);
|
||||
|
||||
text.lines.push(Spans(line));
|
||||
},
|
||||
MessageColumns::One(_, opt) => {
|
||||
if let Some(user) = opt.take() {
|
||||
MessageColumns::One => {
|
||||
if let Some(user) = self.user.take() {
|
||||
text.lines.push(Spans(vec![user]));
|
||||
}
|
||||
|
||||
|
@ -428,7 +484,7 @@ impl Message {
|
|||
|
||||
pub fn reply_to(&self) -> Option<OwnedEventId> {
|
||||
let content = match &self.event {
|
||||
MessageEvent::Local(content) => content,
|
||||
MessageEvent::Local(_, content) => content,
|
||||
MessageEvent::Original(ev) => &ev.content,
|
||||
MessageEvent::Redacted(_) => return None,
|
||||
};
|
||||
|
@ -454,28 +510,50 @@ impl Message {
|
|||
return style;
|
||||
}
|
||||
|
||||
fn get_render_format(
|
||||
&self,
|
||||
fn get_render_format<'a>(
|
||||
&'a self,
|
||||
prev: Option<&Message>,
|
||||
width: usize,
|
||||
settings: &ApplicationSettings,
|
||||
) -> MessageColumns {
|
||||
if USER_GUTTER + TIME_GUTTER + MIN_MSG_LEN <= width {
|
||||
let lw = width - USER_GUTTER - TIME_GUTTER;
|
||||
info: &'a RoomInfo,
|
||||
settings: &'a ApplicationSettings,
|
||||
) -> MessageFormatter<'a> {
|
||||
if USER_GUTTER + TIME_GUTTER + READ_GUTTER + MIN_MSG_LEN <= width &&
|
||||
settings.tunables.read_receipt_display
|
||||
{
|
||||
let cols = MessageColumns::Four;
|
||||
let fill = width - USER_GUTTER - TIME_GUTTER - READ_GUTTER;
|
||||
let user = self.show_sender(prev, true, settings);
|
||||
let time = self.timestamp.show();
|
||||
let read = match info.receipts.get(self.event.event_id()) {
|
||||
Some(read) => read.iter(),
|
||||
None => [].iter(),
|
||||
};
|
||||
|
||||
MessageColumns::Three(lw, user, time)
|
||||
} else if USER_GUTTER + MIN_MSG_LEN <= width {
|
||||
let lw = width - USER_GUTTER;
|
||||
MessageFormatter { settings, cols, fill, user, time, read }
|
||||
} 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 time = self.timestamp.show();
|
||||
let read = [].iter();
|
||||
|
||||
MessageColumns::Two(lw, user)
|
||||
MessageFormatter { settings, cols, fill, user, 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 time = None;
|
||||
let read = [].iter();
|
||||
|
||||
MessageFormatter { settings, cols, fill, user, time, read }
|
||||
} else {
|
||||
let lw = width.saturating_sub(2);
|
||||
let cols = MessageColumns::One;
|
||||
let fill = width.saturating_sub(2);
|
||||
let user = self.show_sender(prev, false, settings);
|
||||
let time = None;
|
||||
let read = [].iter();
|
||||
|
||||
MessageColumns::One(lw, user)
|
||||
MessageFormatter { settings, cols, fill, user, time, read }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -485,12 +563,12 @@ impl Message {
|
|||
selected: bool,
|
||||
vwctx: &ViewportContext<MessageCursor>,
|
||||
info: &'a RoomInfo,
|
||||
settings: &ApplicationSettings,
|
||||
settings: &'a ApplicationSettings,
|
||||
) -> Text<'a> {
|
||||
let width = vwctx.get_width();
|
||||
|
||||
let style = self.get_render_style(selected);
|
||||
let mut fmt = self.get_render_format(prev, width, settings);
|
||||
let mut fmt = self.get_render_format(prev, width, info, settings);
|
||||
let mut text = Text { lines: vec![] };
|
||||
let width = fmt.width();
|
||||
|
||||
|
|
19
src/tests.rs
19
src/tests.rs
|
@ -15,13 +15,15 @@ use matrix_sdk::ruma::{
|
|||
};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use modalkit::tui::style::Color;
|
||||
use modalkit::tui::style::{Color, Style};
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
base::{ChatStore, ProgramStore, RoomFetchStatus, RoomInfo},
|
||||
config::{
|
||||
user_color,
|
||||
user_style_from_color,
|
||||
ApplicationSettings,
|
||||
DirectoryValues,
|
||||
ProfileConfig,
|
||||
|
@ -60,6 +62,10 @@ lazy_static! {
|
|||
pub static ref MSG5_KEY: MessageKey = (OriginServer(UInt::new(8).unwrap()), MSG5_EVID.clone());
|
||||
}
|
||||
|
||||
pub fn user_style(user: &str) -> Style {
|
||||
user_style_from_color(user_color(user))
|
||||
}
|
||||
|
||||
pub fn mock_room1_message(
|
||||
content: RoomMessageEventContent,
|
||||
sender: OwnedUserId,
|
||||
|
@ -82,7 +88,7 @@ pub fn mock_room1_message(
|
|||
|
||||
pub fn mock_message1() -> Message {
|
||||
let content = RoomMessageEventContent::text_plain("writhe");
|
||||
let content = MessageEvent::Local(content.into());
|
||||
let content = MessageEvent::Local(MSG1_EVID.clone(), content.into());
|
||||
|
||||
Message::new(content, TEST_USER1.clone(), MSG1_KEY.0)
|
||||
}
|
||||
|
@ -138,10 +144,13 @@ pub fn mock_messages() -> Messages {
|
|||
pub fn mock_room() -> RoomInfo {
|
||||
RoomInfo {
|
||||
name: Some("Watercooler Discussion".into()),
|
||||
tags: None,
|
||||
|
||||
keys: mock_keys(),
|
||||
messages: mock_messages(),
|
||||
tags: None,
|
||||
|
||||
receipts: HashMap::new(),
|
||||
read_till: None,
|
||||
|
||||
fetch_id: RoomFetchStatus::NotStarted,
|
||||
fetch_last: None,
|
||||
|
@ -159,7 +168,9 @@ pub fn mock_dirs() -> DirectoryValues {
|
|||
|
||||
pub fn mock_tunables() -> TunableValues {
|
||||
TunableValues {
|
||||
typing_notice: true,
|
||||
read_receipt_send: true,
|
||||
read_receipt_display: true,
|
||||
typing_notice_send: true,
|
||||
typing_notice_display: true,
|
||||
users: vec![(TEST_USER5.clone(), UserDisplayTunables {
|
||||
color: Some(UserColor(Color::Black)),
|
||||
|
|
|
@ -241,7 +241,7 @@ impl ChatState {
|
|||
|
||||
let ev = match &msg.event {
|
||||
MessageEvent::Original(ev) => &ev.content,
|
||||
MessageEvent::Local(ev) => ev.deref(),
|
||||
MessageEvent::Local(_, ev) => ev.deref(),
|
||||
_ => {
|
||||
let msg = "Cannot edit a redacted message";
|
||||
let err = UIError::Failure(msg.into());
|
||||
|
@ -275,9 +275,7 @@ impl ChatState {
|
|||
|
||||
let event_id = match &msg.event {
|
||||
MessageEvent::Original(ev) => ev.event_id.clone(),
|
||||
MessageEvent::Local(_) => {
|
||||
self.scrollback.get_key(info).ok_or(IambError::NoSelectedMessage)?.1
|
||||
},
|
||||
MessageEvent::Local(event_id, _) => event_id.clone(),
|
||||
MessageEvent::Redacted(_) => {
|
||||
let msg = "";
|
||||
let err = UIError::Failure(msg.into());
|
||||
|
@ -378,8 +376,8 @@ impl ChatState {
|
|||
|
||||
if show_echo {
|
||||
let user = store.application.settings.profile.user_id.clone();
|
||||
let key = (MessageTimeStamp::LocalEcho, event_id);
|
||||
let msg = MessageEvent::Local(msg.into());
|
||||
let key = (MessageTimeStamp::LocalEcho, event_id.clone());
|
||||
let msg = MessageEvent::Local(event_id, msg.into());
|
||||
let msg = Message::new(msg, user, MessageTimeStamp::LocalEcho);
|
||||
info.messages.insert(key, msg);
|
||||
}
|
||||
|
@ -415,7 +413,7 @@ impl ChatState {
|
|||
return;
|
||||
}
|
||||
|
||||
if !store.application.settings.tunables.typing_notice {
|
||||
if !store.application.settings.tunables.typing_notice_send {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1260,7 +1260,13 @@ impl<'a> StatefulWidget for Scrollback<'a> {
|
|||
y += 1;
|
||||
}
|
||||
|
||||
let first_key = info.messages.first_key_value().map(|f| f.0.clone());
|
||||
if settings.tunables.read_receipt_send && state.cursor.timestamp.is_none() {
|
||||
// If the cursor is at the last message, then update the read marker.
|
||||
info.read_till = info.messages.last_key_value().map(|(k, _)| k.1.clone());
|
||||
}
|
||||
|
||||
// Check whether we should load older messages for this room.
|
||||
let first_key = info.messages.first_key_value().map(|(k, _)| k.clone());
|
||||
if first_key == state.viewctx.corner.timestamp {
|
||||
// If the top of the screen is the older message, load more.
|
||||
self.store.application.mark_for_load(state.room_id.clone());
|
||||
|
|
|
@ -57,7 +57,7 @@ use matrix_sdk::{
|
|||
use modalkit::editing::action::{EditInfo, InfoMessage, UIError};
|
||||
|
||||
use crate::{
|
||||
base::{AsyncProgramStore, IambError, IambResult, VerifyAction},
|
||||
base::{AsyncProgramStore, IambError, IambResult, Receipts, VerifyAction},
|
||||
message::MessageFetchResult,
|
||||
ApplicationSettings,
|
||||
};
|
||||
|
@ -99,6 +99,29 @@ fn oneshot<T>() -> (ClientReply<T>, ClientResponse<T>) {
|
|||
return (reply, response);
|
||||
}
|
||||
|
||||
async fn update_receipts(client: &Client) -> Vec<(OwnedRoomId, Receipts)> {
|
||||
let mut rooms = vec![];
|
||||
|
||||
for room in client.joined_rooms() {
|
||||
if let Ok(users) = room.active_members_no_sync().await {
|
||||
let mut receipts = Receipts::new();
|
||||
|
||||
for member in users {
|
||||
let res = room.user_read_receipt(member.user_id()).await;
|
||||
|
||||
if let Ok(Some((event_id, _))) = res {
|
||||
let user_id = member.user_id().to_owned();
|
||||
receipts.entry(event_id).or_default().push(user_id);
|
||||
}
|
||||
}
|
||||
|
||||
rooms.push((room.room_id().to_owned(), receipts));
|
||||
}
|
||||
}
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
pub type FetchedRoom = (MatrixRoom, DisplayName, Option<Tags>);
|
||||
|
||||
pub enum WorkerTask {
|
||||
|
@ -454,7 +477,7 @@ impl ClientWorker {
|
|||
}
|
||||
|
||||
async fn init(&mut self, store: AsyncProgramStore) {
|
||||
self.client.add_event_handler_context(store);
|
||||
self.client.add_event_handler_context(store.clone());
|
||||
|
||||
let _ = self.client.add_event_handler(
|
||||
|ev: SyncTypingEvent, room: MatrixRoom, store: Ctx<AsyncProgramStore>| {
|
||||
|
@ -661,6 +684,19 @@ impl ClientWorker {
|
|||
},
|
||||
);
|
||||
|
||||
let client = self.client.clone();
|
||||
let _ = tokio::spawn(async move {
|
||||
// Update the displayed read receipts ever 5 seconds.
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(5));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let receipts = update_receipts(&client).await;
|
||||
store.lock().await.application.set_receipts(receipts).await;
|
||||
}
|
||||
});
|
||||
|
||||
self.initialized = true;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue