diff --git a/src/base.rs b/src/base.rs index c5850b7..370a42c 100644 --- a/src/base.rs +++ b/src/base.rs @@ -36,7 +36,7 @@ use matrix_sdk::{ ruma::{ events::{ reaction::ReactionEvent, - relation::Replacement, + relation::{Replacement, Thread}, room::encrypted::RoomEncryptedEvent, room::message::{ OriginalRoomMessageEvent, @@ -681,7 +681,10 @@ pub enum RoomFetchStatus { /// Indicates where an [EventId] lives in the [ChatStore]. pub enum EventLocation { /// The [EventId] belongs to a message. - Message(MessageKey), + /// + /// If the first argument is [None], then it's part of the main scrollback. When [Some], + /// it specifies which thread it's in reply to. + Message(Option, MessageKey), /// The [EventId] belongs to a reaction to the given event. Reaction(OwnedEventId), @@ -689,7 +692,7 @@ pub enum EventLocation { impl EventLocation { fn to_message_key(&self) -> Option<&MessageKey> { - if let EventLocation::Message(key) = self { + if let EventLocation::Message(_, key) = self { Some(key) } else { None @@ -741,6 +744,9 @@ pub struct RoomInfo { /// A map of message identifiers to a map of reaction events. pub reactions: HashMap, + /// A map of message identifiers to thread replies. + pub threads: HashMap, + /// Whether the scrollback for this room is currently being fetched. pub fetching: bool, @@ -819,15 +825,17 @@ impl RoomInfo { let event_id = msg.event_id; let new_msgtype = msg.new_content; - let key = if let Some(EventLocation::Message(k)) = self.keys.get(&event_id) { - k - } else { + let Some(EventLocation::Message(thread, key)) = self.keys.get(&event_id) else { return; }; - let msg = if let Some(msg) = self.messages.get_mut(key) { - msg + let source = if let Some(thread) = thread { + self.threads.entry(thread.clone()).or_default() } else { + &mut self.messages + }; + + let Some(msg) = source.get_mut(key) else { return; }; @@ -867,7 +875,7 @@ impl RoomInfo { let event_id = msg.event_id().to_owned(); let key = (msg.origin_server_ts().into(), event_id.clone()); - self.keys.insert(event_id, EventLocation::Message(key.clone())); + self.keys.insert(event_id, EventLocation::Message(None, key.clone())); self.messages.insert(key, msg.into()); } @@ -876,22 +884,38 @@ impl RoomInfo { let event_id = msg.event_id().to_owned(); let key = (msg.origin_server_ts().into(), event_id.clone()); - self.keys.insert(event_id.clone(), EventLocation::Message(key.clone())); - self.messages.insert(key, msg.into()); + let loc = EventLocation::Message(None, key.clone()); + self.keys.insert(event_id, loc); + self.messages.insert_message(key, msg); + } - // Remove any echo. - let key = (MessageTimeStamp::LocalEcho, event_id); - let _ = self.messages.remove(&key); + fn insert_thread(&mut self, msg: RoomMessageEvent, thread_root: OwnedEventId) { + let event_id = msg.event_id().to_owned(); + let key = (msg.origin_server_ts().into(), event_id.clone()); + + let replies = self.threads.entry(thread_root.clone()).or_default(); + let loc = EventLocation::Message(Some(thread_root), key.clone()); + self.keys.insert(event_id, loc); + replies.insert_message(key, msg); } /// Insert a new message event. pub fn insert(&mut self, msg: RoomMessageEvent) { match msg { RoomMessageEvent::Original(OriginalRoomMessageEvent { - content: - RoomMessageEventContent { relates_to: Some(Relation::Replacement(repl)), .. }, + content: RoomMessageEventContent { relates_to: Some(ref relates_to), .. }, .. - }) => self.insert_edit(repl), + }) => { + match relates_to { + Relation::Replacement(repl) => self.insert_edit(repl.clone()), + Relation::Thread(Thread { event_id, .. }) => { + let event_id = event_id.clone(); + self.insert_thread(msg, event_id); + }, + Relation::Reply { .. } => self.insert_message(msg), + _ => self.insert_message(msg), + } + }, _ => self.insert_message(msg), } } @@ -1236,8 +1260,8 @@ impl ApplicationStore for ChatStore {} /// Identified used to track window content. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum IambId { - /// A Matrix room. - Room(OwnedRoomId), + /// A Matrix room, with an optional thread to show. + Room(OwnedRoomId, Option), /// The `:dms` window. DirectList, @@ -1264,9 +1288,12 @@ pub enum IambId { impl Display for IambId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - IambId::Room(room_id) => { + IambId::Room(room_id, None) => { write!(f, "iamb://room/{room_id}") }, + IambId::Room(room_id, Some(thread)) => { + write!(f, "iamb://room/{room_id}/threads/{thread}") + }, IambId::MemberList(room_id) => { write!(f, "iamb://members/{room_id}") }, @@ -1328,15 +1355,27 @@ impl<'de> Visitor<'de> for IambIdVisitor { return Err(E::custom("Invalid members window URL")); }; - let &[room_id] = path.collect::>().as_slice() else { - return Err(E::custom("Invalid members window URL")); - }; + match *path.collect::>().as_slice() { + [room_id] => { + let Ok(room_id) = OwnedRoomId::try_from(room_id) else { + return Err(E::custom("Invalid room identifier")); + }; - let Ok(room_id) = OwnedRoomId::try_from(room_id) else { - return Err(E::custom("Invalid room identifier")); - }; + Ok(IambId::Room(room_id, None)) + }, + [room_id, "threads", thread_root] => { + let Ok(room_id) = OwnedRoomId::try_from(room_id) else { + return Err(E::custom("Invalid room identifier")); + }; - Ok(IambId::Room(room_id)) + let Ok(thread_root) = OwnedEventId::try_from(thread_root) else { + return Err(E::custom("Invalid thread root identifier")); + }; + + Ok(IambId::Room(room_id, Some(thread_root))) + }, + _ => return Err(E::custom("Invalid members window URL")), + } }, Some("members") => { let Some(path) = url.path_segments() else { @@ -1433,7 +1472,7 @@ pub enum IambBufferId { Command(CommandType), /// The message buffer or a specific message in a room. - Room(OwnedRoomId, RoomFocus), + Room(OwnedRoomId, Option, RoomFocus), /// The `:dms` window. DirectList, @@ -1460,17 +1499,19 @@ pub enum IambBufferId { impl IambBufferId { /// Get the identifier for the window that contains this buffer. pub fn to_window(&self) -> Option { - match self { - IambBufferId::Command(_) => None, - IambBufferId::Room(room, _) => Some(IambId::Room(room.clone())), - IambBufferId::DirectList => Some(IambId::DirectList), - IambBufferId::MemberList(room) => Some(IambId::MemberList(room.clone())), - IambBufferId::RoomList => Some(IambId::RoomList), - IambBufferId::SpaceList => Some(IambId::SpaceList), - IambBufferId::VerifyList => Some(IambId::VerifyList), - IambBufferId::Welcome => Some(IambId::Welcome), - IambBufferId::ChatList => Some(IambId::ChatList), - } + let id = match self { + IambBufferId::Command(_) => return None, + IambBufferId::Room(room, thread, _) => IambId::Room(room.clone(), thread.clone()), + IambBufferId::DirectList => IambId::DirectList, + IambBufferId::MemberList(room) => IambId::MemberList(room.clone()), + IambBufferId::RoomList => IambId::RoomList, + IambBufferId::SpaceList => IambId::SpaceList, + IambBufferId::VerifyList => IambId::VerifyList, + IambBufferId::Welcome => IambId::Welcome, + IambBufferId::ChatList => IambId::ChatList, + }; + + Some(id) } } @@ -1492,8 +1533,8 @@ impl ApplicationInfo for IambInfo { match content { IambBufferId::Command(CommandType::Command) => complete_cmdbar(text, cursor, store), IambBufferId::Command(CommandType::Search) => vec![], - IambBufferId::Room(_, RoomFocus::MessageBar) => complete_msgbar(text, cursor, store), - IambBufferId::Room(_, RoomFocus::Scrollback) => vec![], + IambBufferId::Room(_, _, RoomFocus::MessageBar) => complete_msgbar(text, cursor, store), + IambBufferId::Room(_, _, RoomFocus::Scrollback) => vec![], IambBufferId::DirectList => vec![], IambBufferId::MemberList(_) => vec![], diff --git a/src/main.rs b/src/main.rs index 0e11203..6e5494f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -141,14 +141,14 @@ fn config_tab_to_desc( let name = user_id.to_string(); let room_id = worker.join_room(name.clone())?; names.insert(name, room_id.clone()); - IambId::Room(room_id) + IambId::Room(room_id, None) }, - config::WindowPath::RoomId(room_id) => IambId::Room(room_id), + config::WindowPath::RoomId(room_id) => IambId::Room(room_id, None), config::WindowPath::AliasId(alias) => { let name = alias.to_string(); let room_id = worker.join_room(name.clone())?; names.insert(name, room_id.clone()); - IambId::Room(room_id) + IambId::Room(room_id, None) }, config::WindowPath::Window(id) => id, }; @@ -563,7 +563,7 @@ impl Application { HomeserverAction::CreateRoom(alias, vis, flags) => { let client = &store.application.worker.client; let room_id = create_room(client, alias, vis, flags).await?; - let room = IambId::Room(room_id); + let room = IambId::Room(room_id, None); let target = OpenTarget::Application(room); let action = WindowAction::Switch(target); diff --git a/src/message/mod.rs b/src/message/mod.rs index 504071b..c7632c1 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -6,6 +6,7 @@ use std::collections::hash_set; use std::collections::BTreeMap; use std::convert::TryFrom; use std::hash::{Hash, Hasher}; +use std::ops::{Deref, DerefMut}; use chrono::{DateTime, Local as LocalTz, NaiveDateTime, TimeZone}; use comrak::{markdown_to_html, ComrakOptions}; @@ -14,6 +15,7 @@ use unicode_width::UnicodeWidthStr; use matrix_sdk::ruma::{ events::{ + relation::Thread, room::{ encrypted::{ OriginalRoomEncryptedEvent, @@ -66,7 +68,36 @@ mod html; mod printer; pub type MessageKey = (MessageTimeStamp, OwnedEventId); -pub type Messages = BTreeMap; + +#[derive(Default)] +pub struct Messages(BTreeMap); + +impl Deref for Messages { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Messages { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Messages { + pub fn insert_message(&mut self, key: MessageKey, msg: impl Into) { + let event_id = key.1.clone(); + let msg = msg.into(); + + self.0.insert(key, msg); + + // Remove any echo. + let key = (MessageTimeStamp::LocalEcho, event_id); + let _ = self.0.remove(&key); + } +} const fn span_static(s: &'static str) -> Span<'static> { Span { @@ -260,33 +291,27 @@ impl MessageCursor { MessageCursor::default() } - pub fn to_key<'a>(&'a self, info: &'a RoomInfo) -> Option<&'a MessageKey> { + pub fn to_key<'a>(&'a self, thread: &'a Messages) -> Option<&'a MessageKey> { if let Some(ref key) = self.timestamp { Some(key) } else { - Some(info.messages.last_key_value()?.0) + Some(thread.last_key_value()?.0) } } - pub fn from_cursor(cursor: &Cursor, info: &RoomInfo) -> Option { + pub fn from_cursor(cursor: &Cursor, thread: &Messages) -> Option { let ev_hash = u64::try_from(cursor.get_x()).ok()?; let ev_term = OwnedEventId::try_from("$").ok()?; let ts_start = MessageTimeStamp::try_from(cursor.get_y()).ok()?; let start = (ts_start, ev_term); - let mut mc = None; - for ((ts, event_id), _) in info.messages.range(start..) { + for ((ts, event_id), _) in thread.range(&start..) { let mut hasher = DefaultHasher::new(); event_id.hash(&mut hasher); if hasher.finish() == ev_hash { - mc = Self::from((*ts, event_id.clone())).into(); - break; - } - - if mc.is_none() { - mc = Self::from((*ts, event_id.clone())).into(); + return Self::from((*ts, event_id.clone())).into(); } if ts > &ts_start { @@ -294,11 +319,15 @@ impl MessageCursor { } } - return mc; + // If we can't find the cursor, then go to the nearest timestamp. + thread + .range(start..) + .next() + .map(|((ts, ev), _)| Self::from((*ts, ev.clone()))) } - pub fn to_cursor(&self, info: &RoomInfo) -> Option { - let (ts, event_id) = self.to_key(info)?; + pub fn to_cursor(&self, thread: &Messages) -> Option { + let (ts, event_id) = self.to_key(thread)?; let y: usize = usize::try_from(ts).ok()?; @@ -652,10 +681,34 @@ impl Message { MessageEvent::Redacted(_) => return None, }; - if let Some(Relation::Reply { in_reply_to }) = &content.relates_to { - Some(in_reply_to.event_id.clone()) - } else { - None + match &content.relates_to { + Some(Relation::Reply { in_reply_to }) => Some(in_reply_to.event_id.clone()), + Some(Relation::Thread(Thread { + in_reply_to: Some(in_reply_to), + is_falling_back: false, + .. + })) => Some(in_reply_to.event_id.clone()), + Some(_) | None => None, + } + } + + fn thread_root(&self) -> Option { + let content = match &self.event { + MessageEvent::EncryptedOriginal(_) => return None, + MessageEvent::EncryptedRedacted(_) => return None, + MessageEvent::Local(_, content) => content, + MessageEvent::Original(ev) => &ev.content, + MessageEvent::Redacted(_) => return None, + }; + + match &content.relates_to { + Some(Relation::Thread(Thread { + event_id, + in_reply_to: Some(in_reply_to), + is_falling_back: true, + .. + })) if event_id == &in_reply_to.event_id => Some(event_id.clone()), + Some(_) | None => None, } } @@ -774,7 +827,10 @@ impl Message { let width = fmt.width(); // Show the message that this one replied to, if any. - let reply = self.reply_to().and_then(|e| info.get_event(&e)); + let reply = self + .reply_to() + .or_else(|| self.thread_root()) + .and_then(|e| info.get_event(&e)); if let Some(r) = &reply { let w = width.saturating_sub(2); @@ -855,7 +911,22 @@ impl Message { } } - return text; + if let Some(thread) = info.threads.get(self.event.event_id()) { + // If we have threaded replies to this message, show how many. + let len = thread.len(); + + if len > 0 { + let style = Style::default(); + let mut threaded = printer::TextPrinter::new(width, style, false).literal(true); + let len = Span::styled(len.to_string(), style.add_modifier(StyleModifier::BOLD)); + threaded.push_str(" \u{2937} ", style); + threaded.push_span_nobreak(len); + threaded.push_str(" replies in thread", style); + fmt.push_text(threaded.finish(), style, &mut text); + } + } + + text } pub fn show_msg(&self, width: usize, style: Style, hide_reply: bool) -> Text { @@ -1058,7 +1129,7 @@ pub mod tests { #[test] fn test_mc_to_key() { - let info = mock_room(); + let messages = mock_messages(); let mc1 = MessageCursor::from(MSG1_KEY.clone()); let mc2 = MessageCursor::from(MSG2_KEY.clone()); let mc3 = MessageCursor::from(MSG3_KEY.clone()); @@ -1066,12 +1137,12 @@ pub mod tests { let mc5 = MessageCursor::from(MSG5_KEY.clone()); let mc6 = MessageCursor::latest(); - let k1 = mc1.to_key(&info).unwrap(); - let k2 = mc2.to_key(&info).unwrap(); - let k3 = mc3.to_key(&info).unwrap(); - let k4 = mc4.to_key(&info).unwrap(); - let k5 = mc5.to_key(&info).unwrap(); - let k6 = mc6.to_key(&info).unwrap(); + let k1 = mc1.to_key(&messages).unwrap(); + let k2 = mc2.to_key(&messages).unwrap(); + let k3 = mc3.to_key(&messages).unwrap(); + let k4 = mc4.to_key(&messages).unwrap(); + let k5 = mc5.to_key(&messages).unwrap(); + let k6 = mc6.to_key(&messages).unwrap(); // These should all be equal to their MSGN_KEYs. assert_eq!(k1, &MSG1_KEY.clone()); @@ -1084,13 +1155,13 @@ pub mod tests { assert_eq!(k6, &MSG1_KEY.clone()); // MessageCursor::latest() fails to convert for a room w/o messages. - let info_empty = RoomInfo::default(); - assert_eq!(mc6.to_key(&info_empty), None); + let messages_empty = Messages::default(); + assert_eq!(mc6.to_key(&messages_empty), None); } #[test] fn test_mc_to_from_cursor() { - let info = mock_room(); + let messages = mock_messages(); let mc1 = MessageCursor::from(MSG1_KEY.clone()); let mc2 = MessageCursor::from(MSG2_KEY.clone()); let mc3 = MessageCursor::from(MSG3_KEY.clone()); @@ -1099,9 +1170,9 @@ pub mod tests { let mc6 = MessageCursor::latest(); let identity = |mc: &MessageCursor| { - let c = mc.to_cursor(&info).unwrap(); + let c = mc.to_cursor(&messages).unwrap(); - MessageCursor::from_cursor(&c, &info).unwrap() + MessageCursor::from_cursor(&c, &messages).unwrap() }; // These should all convert to a Cursor and back to the original value. diff --git a/src/tests.rs b/src/tests.rs index f2ffb83..645e5c9 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; use std::path::PathBuf; use matrix_sdk::ruma::{ @@ -125,17 +125,17 @@ pub fn mock_message5() -> Message { pub fn mock_keys() -> HashMap { let mut keys = HashMap::new(); - keys.insert(MSG1_EVID.clone(), EventLocation::Message(MSG1_KEY.clone())); - keys.insert(MSG2_EVID.clone(), EventLocation::Message(MSG2_KEY.clone())); - keys.insert(MSG3_EVID.clone(), EventLocation::Message(MSG3_KEY.clone())); - keys.insert(MSG4_EVID.clone(), EventLocation::Message(MSG4_KEY.clone())); - keys.insert(MSG5_EVID.clone(), EventLocation::Message(MSG5_KEY.clone())); + keys.insert(MSG1_EVID.clone(), EventLocation::Message(None, MSG1_KEY.clone())); + keys.insert(MSG2_EVID.clone(), EventLocation::Message(None, MSG2_KEY.clone())); + keys.insert(MSG3_EVID.clone(), EventLocation::Message(None, MSG3_KEY.clone())); + keys.insert(MSG4_EVID.clone(), EventLocation::Message(None, MSG4_KEY.clone())); + keys.insert(MSG5_EVID.clone(), EventLocation::Message(None, MSG5_KEY.clone())); keys } pub fn mock_messages() -> Messages { - let mut messages = BTreeMap::new(); + let mut messages = Messages::default(); messages.insert(MSG1_KEY.clone(), mock_message1()); messages.insert(MSG2_KEY.clone(), mock_message2()); @@ -153,6 +153,7 @@ pub fn mock_room() -> RoomInfo { keys: mock_keys(), messages: mock_messages(), + threads: HashMap::default(), event_receipts: HashMap::new(), user_receipts: HashMap::new(), diff --git a/src/windows/mod.rs b/src/windows/mod.rs index ead099a..c8efe7f 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -282,7 +282,7 @@ fn room_prompt( ) -> EditResult, IambInfo> { match act { PromptAction::Submit => { - let room = IambId::Room(room_id.to_owned()); + let room = IambId::Room(room_id.to_owned(), None); let open = WindowAction::Switch(OpenTarget::Application(room)); let acts = vec![(open.into(), ctx.clone())]; @@ -661,7 +661,7 @@ impl WindowOps for IambWindow { impl Window for IambWindow { fn id(&self) -> IambId { match self { - IambWindow::Room(room) => IambId::Room(room.id().to_owned()), + IambWindow::Room(room) => IambId::Room(room.id().to_owned(), room.thread().cloned()), IambWindow::DirectList(_) => IambId::DirectList, IambWindow::MemberList(_, room_id, _) => IambId::MemberList(room_id.clone()), IambWindow::RoomList(_) => IambId::RoomList, @@ -724,9 +724,9 @@ impl Window for IambWindow { fn open(id: IambId, store: &mut ProgramStore) -> IambResult { match id { - IambId::Room(room_id) => { + IambId::Room(room_id, thread) => { let (room, name, tags) = store.application.worker.get_room(room_id)?; - let room = RoomState::new(room, name, tags, store); + let room = RoomState::new(room, thread, name, tags, store); store.application.need_load.insert(room.id().to_owned(), Need::MEMBERS); return Ok(room.into()); @@ -775,7 +775,7 @@ impl Window for IambWindow { let ChatStore { names, worker, .. } = &mut store.application; if let Some(room) = names.get_mut(&name) { - let id = IambId::Room(room.clone()); + let id = IambId::Room(room.clone(), None); IambWindow::open(id, store) } else { @@ -783,7 +783,7 @@ impl Window for IambWindow { names.insert(name, room_id.clone()); let (room, name, tags) = store.application.worker.get_room(room_id)?; - let room = RoomState::new(room, name, tags, store); + let room = RoomState::new(room, None, name, tags, store); store.application.need_load.insert(room.id().to_owned(), Need::MEMBERS); Ok(room.into()) diff --git a/src/windows/room/chat.rs b/src/windows/room/chat.rs index dc171ab..d872f5a 100644 --- a/src/windows/room/chat.rs +++ b/src/windows/room/chat.rs @@ -24,10 +24,12 @@ use matrix_sdk::{ MessageType, OriginalRoomMessageEvent, Relation, + ReplyWithinThread, RoomMessageEventContent, TextMessageEventContent, }, EventId, + OwnedEventId, OwnedRoomId, RoomId, }, @@ -70,6 +72,7 @@ use modalkit::prelude::*; use crate::base::{ DownloadFlags, + EventLocation, IambAction, IambBufferId, IambError, @@ -106,10 +109,10 @@ pub struct ChatState { } impl ChatState { - pub fn new(room: MatrixRoom, store: &mut ProgramStore) -> Self { + pub fn new(room: MatrixRoom, thread: Option, store: &mut ProgramStore) -> Self { let room_id = room.room_id().to_owned(); - let scrollback = ScrollbackState::new(room_id.clone()); - let id = IambBufferId::Room(room_id.clone(), RoomFocus::MessageBar); + let scrollback = ScrollbackState::new(room_id.clone(), thread.clone()); + let id = IambBufferId::Room(room_id.clone(), thread, RoomFocus::MessageBar); let ebuf = store.load_buffer(id); let tbox = TextBoxState::new(ebuf); @@ -129,6 +132,10 @@ impl ChatState { } } + pub fn thread(&self) -> Option<&OwnedEventId> { + self.scrollback.thread() + } + fn get_joined(&self, worker: &Requester) -> Result { let Some(room) = worker.client.get_room(self.id()) else { return Err(IambError::NotJoined); @@ -141,6 +148,29 @@ impl ChatState { } } + fn get_thread_last<'a>( + &self, + thread_root: &OwnedEventId, + info: &'a RoomInfo, + ) -> Option<&'a OriginalRoomMessageEvent> { + let last = info.threads.get(thread_root).and_then(|t| Some(t.last_key_value()?.1)); + + let msg = if let Some(last) = last { + &last.event + } else if let EventLocation::Message(_, key) = info.keys.get(thread_root)? { + let msg = info.messages.get(key)?; + &msg.event + } else { + return None; + }; + + if let MessageEvent::Original(ev) = &msg { + Some(ev) + } else { + None + } + } + fn get_reply_to<'a>(&self, info: &'a RoomInfo) -> Option<&'a OriginalRoomMessageEvent> { let key = self.reply_to.as_ref()?; let msg = info.messages.get(key)?; @@ -485,8 +515,15 @@ impl ChatState { ))); show_echo = false; + } else if let Some(thread_root) = self.scrollback.thread() { + if let Some(m) = self.get_reply_to(info) { + msg = msg.make_for_thread(m, ReplyWithinThread::Yes, AddMentions::No); + } else if let Some(m) = self.get_thread_last(thread_root, info) { + msg = msg.make_for_thread(m, ReplyWithinThread::No, AddMentions::No); + } else { + // Internal state is wonky? + } } else if let Some(m) = self.get_reply_to(info) { - // XXX: Switch to RoomMessageEventContent::reply() once it's stable? msg = msg.make_reply_to(m, ForwardThread::Yes, AddMentions::No); } @@ -560,7 +597,8 @@ impl ChatState { 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); + let thread = self.scrollback.get_thread_mut(info); + thread.insert(key, msg); } // Jump to the end of the scrollback to show the message. @@ -627,12 +665,14 @@ impl WindowOps for ChatState { fn dup(&self, store: &mut ProgramStore) -> Self { // XXX: I want each WindowSlot to have its own shared buffer, instead of each Room; need to // find a good way to pass that info here so that it can be part of the content id. - let id = IambBufferId::Room(self.room_id.clone(), RoomFocus::MessageBar); + let room_id = self.room_id.clone(); + let thread = self.thread().cloned(); + let id = IambBufferId::Room(room_id.clone(), thread, RoomFocus::MessageBar); let ebuf = store.load_buffer(id); let tbox = TextBoxState::new(ebuf); ChatState { - room_id: self.room_id.clone(), + room_id, room: self.room.clone(), tbox, @@ -688,8 +728,10 @@ impl Editable for ChatState { match delegate!(self, w => w.editor_command(act, ctx, store)) { res @ Ok(_) => res, - Err(EditError::WrongBuffer(IambBufferId::Room(room_id, focus))) - if room_id == self.room_id && act.is_switchable(ctx) => + Err(EditError::WrongBuffer(IambBufferId::Room(room_id, thread, focus))) + if room_id == self.room_id && + thread.as_ref() == self.thread() && + act.is_switchable(ctx) => { // Switch focus. self.focus = focus; @@ -807,7 +849,7 @@ impl Promptable for ChatState { store: &mut ProgramStore, ) -> EditResult, IambInfo> { if let RoomFocus::Scrollback = self.focus { - return Ok(vec![]); + return self.scrollback.prompt(act, ctx, store); } match act { diff --git a/src/windows/room/mod.rs b/src/windows/room/mod.rs index d304fd6..afe4547 100644 --- a/src/windows/room/mod.rs +++ b/src/windows/room/mod.rs @@ -6,6 +6,7 @@ use matrix_sdk::{ room::{name::RoomNameEventContent, topic::RoomTopicEventContent}, tag::{TagInfo, Tags}, }, + OwnedEventId, RoomId, }, DisplayName, @@ -90,6 +91,7 @@ impl From for RoomState { impl RoomState { pub fn new( room: MatrixRoom, + thread: Option, name: DisplayName, tags: Option, store: &mut ProgramStore, @@ -102,7 +104,14 @@ impl RoomState { if room.is_space() { SpaceState::new(room).into() } else { - ChatState::new(room, store).into() + ChatState::new(room, thread, store).into() + } + } + + pub fn thread(&self) -> Option<&OwnedEventId> { + match self { + RoomState::Chat(chat) => chat.thread(), + RoomState::Space(_) => None, } } @@ -293,7 +302,15 @@ impl RoomState { pub fn get_title(&self, store: &mut ProgramStore) -> Line { let title = store.application.get_room_title(self.id()); let style = Style::default().add_modifier(StyleModifier::BOLD); - let mut spans = vec![Span::styled(title, style)]; + let mut spans = vec![]; + + if let RoomState::Chat(chat) = self { + if chat.thread().is_some() { + spans.push("Thread in ".into()); + } + } + + spans.push(Span::styled(title, style)); match self.room().topic() { Some(desc) if !desc.is_empty() => { diff --git a/src/windows/room/scrollback.rs b/src/windows/room/scrollback.rs index d51bc69..b423e49 100644 --- a/src/windows/room/scrollback.rs +++ b/src/windows/room/scrollback.rs @@ -3,7 +3,7 @@ use ratatui_image::Image; use regex::Regex; -use matrix_sdk::ruma::OwnedRoomId; +use matrix_sdk::ruma::{OwnedEventId, OwnedRoomId}; use modalkit_ratatui::{ScrollActions, TerminalCursor, WindowOps}; use ratatui::{ @@ -29,6 +29,7 @@ use modalkit::actions::{ Scrollable, Searchable, SelectionAction, + WindowAction, }; use modalkit::editing::{ completion::CompletionList, @@ -44,11 +45,13 @@ use modalkit::prelude::*; use crate::{ base::{ IambBufferId, + IambId, IambInfo, IambResult, Need, ProgramContext, ProgramStore, + RoomFetchStatus, RoomFocus, RoomInfo, }, @@ -56,9 +59,9 @@ use crate::{ message::{Message, MessageCursor, MessageKey, Messages}, }; -fn nth_key_before(pos: MessageKey, n: usize, info: &RoomInfo) -> MessageKey { +fn nth_key_before(pos: MessageKey, n: usize, thread: &Messages) -> MessageKey { let mut end = &pos; - let iter = info.messages.range(..=&pos).rev().enumerate(); + let iter = thread.range(..=&pos).rev().enumerate(); for (i, (key, _)) in iter { end = key; @@ -71,13 +74,13 @@ fn nth_key_before(pos: MessageKey, n: usize, info: &RoomInfo) -> MessageKey { end.clone() } -fn nth_before(pos: MessageKey, n: usize, info: &RoomInfo) -> MessageCursor { - nth_key_before(pos, n, info).into() +fn nth_before(pos: MessageKey, n: usize, thread: &Messages) -> MessageCursor { + nth_key_before(pos, n, thread).into() } -fn nth_key_after(pos: MessageKey, n: usize, info: &RoomInfo) -> MessageKey { +fn nth_key_after(pos: MessageKey, n: usize, thread: &Messages) -> MessageKey { let mut end = &pos; - let iter = info.messages.range(&pos..).enumerate(); + let iter = thread.range(&pos..).enumerate(); for (i, (key, _)) in iter { end = key; @@ -90,12 +93,12 @@ fn nth_key_after(pos: MessageKey, n: usize, info: &RoomInfo) -> MessageKey { end.clone() } -fn nth_after(pos: MessageKey, n: usize, info: &RoomInfo) -> MessageCursor { - nth_key_after(pos, n, info).into() +fn nth_after(pos: MessageKey, n: usize, thread: &Messages) -> MessageCursor { + nth_key_after(pos, n, thread).into() } -fn prevmsg<'a>(key: &MessageKey, info: &'a RoomInfo) -> Option<&'a Message> { - info.messages.range(..key).next_back().map(|(_, v)| v) +fn prevmsg<'a>(key: &MessageKey, thread: &'a Messages) -> Option<&'a Message> { + thread.range(..key).next_back().map(|(_, v)| v) } pub struct ScrollbackState { @@ -105,6 +108,9 @@ pub struct ScrollbackState { /// The buffer identifier used for saving marks, etc. id: IambBufferId, + /// The currently focused thread in this room. + thread: Option, + /// The currently selected message in the scrollback. cursor: MessageCursor, @@ -122,8 +128,8 @@ pub struct ScrollbackState { } impl ScrollbackState { - pub fn new(room_id: OwnedRoomId) -> ScrollbackState { - let id = IambBufferId::Room(room_id.to_owned(), RoomFocus::Scrollback); + pub fn new(room_id: OwnedRoomId, thread: Option) -> ScrollbackState { + let id = IambBufferId::Room(room_id.to_owned(), thread.clone(), RoomFocus::Scrollback); let cursor = MessageCursor::default(); let viewctx = ViewportContext::default(); let jumped = HistoryList::default(); @@ -132,6 +138,7 @@ impl ScrollbackState { ScrollbackState { room_id, id, + thread, cursor, viewctx, jumped, @@ -152,7 +159,7 @@ impl ScrollbackState { self.cursor .timestamp .clone() - .or_else(|| info.messages.last_key_value().map(|kv| kv.0.clone())) + .or_else(|| self.get_thread(info).last_key_value().map(|kv| kv.0.clone())) } pub fn get_mut<'a>(&mut self, messages: &'a mut Messages) -> Option<&'a mut Message> { @@ -163,26 +170,80 @@ impl ScrollbackState { } } + pub fn thread(&self) -> Option<&OwnedEventId> { + self.thread.as_ref() + } + + pub fn get_thread<'a>(&self, info: &'a RoomInfo) -> &'a Messages { + if let Some(thread_root) = self.thread.as_ref() { + info.threads.get(thread_root).unwrap_or(&info.messages) + } else { + &info.messages + } + } + + pub fn get_thread_mut<'a>(&self, info: &'a mut RoomInfo) -> &'a mut Messages { + if let Some(thread_root) = self.thread.as_ref() { + info.threads.get_mut(thread_root).unwrap_or(&mut info.messages) + } else { + &mut info.messages + } + } + pub fn messages<'a>( &self, range: EditRange, info: &'a RoomInfo, ) -> impl Iterator { - let start = range.start.to_key(info); - let end = range.end.to_key(info); + let thread = self.get_thread(info); + let start = range.start.to_key(thread); + let end = range.end.to_key(thread); let (start, end) = if let (Some(start), Some(end)) = (start, end) { (start, end) - } else if let Some((last, _)) = info.messages.last_key_value() { + } else if let Some((last, _)) = thread.last_key_value() { (last, last) } else { - return info.messages.range(..); + return thread.range(..); }; if range.inclusive { - info.messages.range(start..=end) + thread.range(start..=end) } else { - info.messages.range(start..end) + thread.range(start..end) + } + } + + fn need_more_messages(&self, info: &RoomInfo) -> bool { + match info.fetch_id { + // Don't fetch if we've already hit the end of history. + RoomFetchStatus::Done => return false, + // Fetch at least once if we're viewing a room. + RoomFetchStatus::NotStarted => return true, + _ => {}, + } + + let first_key = self.get_thread(info).first_key_value().map(|(k, _)| k); + let at_top = first_key == self.viewctx.corner.timestamp.as_ref(); + + match (at_top, self.thread.as_ref()) { + (false, _) => { + // Not scrolled to top, don't fetch. + false + }, + (true, None) => { + // Scrolled to top in non-thread, fetch. + true + }, + (true, Some(thread_root)) => { + // Scrolled to top in thread, fetch until we have the thread root. + // + // Typically, if the user has entered a thread view, we should already have fetched + // all the way back to the thread root, but it is technically possible via :threads + // or when restoring a thread view in the layout at startup to not have the message + // yet. + !info.keys.contains_key(thread_root) + }, } } @@ -193,7 +254,8 @@ impl ScrollbackState { info: &RoomInfo, settings: &ApplicationSettings, ) { - let selidx = if let Some(key) = self.cursor.to_key(info) { + let thread = self.get_thread(info); + let selidx = if let Some(key) = self.cursor.to_key(thread) { key } else { return; @@ -207,9 +269,9 @@ impl ScrollbackState { let mut lines = 0; let target = self.viewctx.get_height() / 2; - for (key, item) in info.messages.range(..=&idx).rev() { + for (key, item) in thread.range(..=&idx).rev() { let sel = selidx == key; - let prev = prevmsg(key, info); + let prev = prevmsg(key, thread); let len = item.show(prev, sel, &self.viewctx, info, settings).lines.len(); if key == &idx { @@ -230,9 +292,9 @@ impl ScrollbackState { let mut lines = 0; let target = self.viewctx.get_height(); - for (key, item) in info.messages.range(..=&idx).rev() { + for (key, item) in thread.range(..=&idx).rev() { let sel = key == selidx; - let prev = prevmsg(key, info); + let prev = prevmsg(key, thread); let len = item.show(prev, sel, &self.viewctx, info, settings).lines.len(); lines += len; @@ -248,8 +310,18 @@ impl ScrollbackState { } } + fn jump_changed(&mut self) -> bool { + self.jumped.current() != &self.cursor + } + + fn push_jump(&mut self) { + self.jumped.push(self.cursor.clone()); + } + fn shift_cursor(&mut self, info: &RoomInfo, settings: &ApplicationSettings) { - let last_key = if let Some(k) = info.messages.last_key_value() { + let thread = self.get_thread(info); + + let last_key = if let Some(k) = thread.last_key_value() { k.0 } else { return; @@ -266,9 +338,9 @@ impl ScrollbackState { let mut lines = 0; let cursor_key = self.cursor.timestamp.as_ref().unwrap_or(last_key); - let mut prev = prevmsg(cursor_key, info); + let mut prev = prevmsg(cursor_key, thread); - for (idx, item) in info.messages.range(corner_key.clone()..) { + for (idx, item) in thread.range(corner_key.clone()..) { if idx == cursor_key { // Cursor is already within the viewport. break; @@ -317,7 +389,7 @@ impl ScrollbackState { MoveType::BufferLineOffset => None, MoveType::BufferLinePercent => None, MoveType::BufferPos(MovePosition::Beginning) => { - let start = info.messages.first_key_value()?.0.clone(); + let start = self.get_thread(info).first_key_value()?.0.clone(); Some(start.into()) }, @@ -330,9 +402,11 @@ impl ScrollbackState { MoveType::ParagraphBegin(dir) | MoveType::SectionBegin(dir) | MoveType::SectionEnd(dir) => { + let thread = self.get_thread(info); + match dir { - MoveDir1D::Previous => nth_before(pos, count, info).into(), - MoveDir1D::Next => nth_after(pos, count, info).into(), + MoveDir1D::Previous => nth_before(pos, count, thread).into(), + MoveDir1D::Next => nth_after(pos, count, thread).into(), } }, MoveType::ViewportPos(MovePosition::Beginning) => { @@ -381,12 +455,14 @@ impl ScrollbackState { RangeType::XmlTag => None, RangeType::Buffer => { - let start = info.messages.first_key_value()?.0.clone(); - let end = info.messages.last_key_value()?.0.clone(); + let thread = self.get_thread(info); + let start = thread.first_key_value()?.0.clone(); + let end = thread.last_key_value()?.0.clone(); Some(EditRange::inclusive(start.into(), end.into(), TargetShape::LineWise)) }, RangeType::Line | RangeType::Paragraph | RangeType::Sentence => { + let thread = self.get_thread(info); let count = ctx.resolve(count); if count == 0 { @@ -395,7 +471,7 @@ impl ScrollbackState { let mut end = &pos; - for (i, (key, _)) in info.messages.range(&pos..).enumerate() { + for (i, (key, _)) in thread.range(&pos..).enumerate() { if i >= count { break; } @@ -420,9 +496,10 @@ impl ScrollbackState { mut count: usize, info: &RoomInfo, ) -> Option { + let thread = self.get_thread(info); let mut mc = None; - for (key, msg) in info.messages.range(&start..) { + for (key, msg) in thread.range(&start..) { if count == 0 { break; } @@ -447,9 +524,10 @@ impl ScrollbackState { mut count: usize, info: &RoomInfo, ) -> (Option, bool) { + let thread = self.get_thread(info); let mut mc = None; - for (key, msg) in info.messages.range(..&end).rev() { + for (key, msg) in thread.range(..&end).rev() { if count == 0 { break; } @@ -487,6 +565,7 @@ impl WindowOps for ScrollbackState { ScrollbackState { room_id: self.room_id.clone(), id: self.id.clone(), + thread: self.thread.clone(), cursor: self.cursor.clone(), viewctx: self.viewctx.clone(), jumped: self.jumped.clone(), @@ -535,7 +614,8 @@ impl EditorActions for ScrollbackState { store: &mut ProgramStore, ) -> EditResult { let info = store.application.rooms.get_or_default(self.room_id.clone()); - let key = if let Some(k) = self.cursor.to_key(info) { + let thread = self.get_thread(info); + let key = if let Some(k) = self.cursor.to_key(thread) { k.clone() } else { let msg = "No messages to select."; @@ -546,7 +626,7 @@ impl EditorActions for ScrollbackState { match operation { EditAction::Motion => { if motion.is_jumping() { - self.jumped.push(self.cursor.clone()); + self.push_jump(); } let pos = match motion { @@ -564,9 +644,10 @@ impl EditorActions for ScrollbackState { EditTarget::CharJump(mark) | EditTarget::LineJump(mark) => { let mark = ctx.resolve(mark); let cursor = store.cursors.get_mark(self.id.clone(), mark)?; + let thread = self.get_thread(info); - if let mc @ Some(_) = MessageCursor::from_cursor(&cursor, info) { - mc + if let Some(mc) = MessageCursor::from_cursor(&cursor, thread) { + Some(mc) } else { let msg = "Failed to restore mark"; let err = EditError::Failure(msg.into()); @@ -642,8 +723,9 @@ impl EditorActions for ScrollbackState { EditTarget::CharJump(mark) | EditTarget::LineJump(mark) => { let mark = ctx.resolve(mark); let cursor = store.cursors.get_mark(self.id.clone(), mark)?; + let thread = self.get_thread(info); - if let Some(c) = MessageCursor::from_cursor(&cursor, info) { + if let Some(c) = MessageCursor::from_cursor(&cursor, thread) { self._range_to(c).into() } else { let msg = "Failed to restore mark"; @@ -739,8 +821,9 @@ impl EditorActions for ScrollbackState { store: &mut ProgramStore, ) -> EditResult { let info = store.application.get_room_info(self.room_id.clone()); + let thread = self.get_thread(info); - if let Some(cursor) = self.cursor.to_cursor(info) { + if let Some(cursor) = self.cursor.to_cursor(thread) { store.cursors.set_mark(self.id.clone(), name, cursor); Ok(None) @@ -814,11 +897,13 @@ impl EditorActions for ScrollbackState { let ngroup = store.cursors.get_group(self.id.clone(), ®)?; // Lists don't have groups; override current position. - if self.jumped.current() != &self.cursor { - self.jumped.push(self.cursor.clone()); + if self.jump_changed() { + self.push_jump(); } - if let Some(mc) = MessageCursor::from_cursor(ngroup.leader.cursor(), info) { + let thread = self.get_thread(info); + + if let Some(mc) = MessageCursor::from_cursor(ngroup.leader.cursor(), thread) { self.cursor = mc; Ok(None) @@ -831,9 +916,10 @@ impl EditorActions for ScrollbackState { }, CursorAction::Save(_) => { let reg = ctx.get_register().unwrap_or(Register::UnnamedCursorGroup); + let thread = self.get_thread(info); // Lists don't have groups; override any previously saved group. - let cursor = self.cursor.to_cursor(info).ok_or_else(|| { + let cursor = self.cursor.to_cursor(thread).ok_or_else(|| { let msg = "Cannot save position in message history"; EditError::Failure(msg.into()) })?; @@ -892,14 +978,14 @@ impl Jumpable for ScrollbackState { let msg = "No changes to jump to within the list"; let err = UIError::Failure(msg.into()); - return Err(err); + Err(err) }, PositionList::JumpList => { let (len, pos) = match dir { MoveDir1D::Previous => { - if self.jumped.future_len() == 0 && *self.jumped.current() != self.cursor { + if self.jumped.future_len() == 0 && self.jump_changed() { // Push current position if this is the first jump backwards. - self.jumped.push(self.cursor.clone()); + self.push_jump(); } let plen = self.jumped.past_len(); @@ -919,7 +1005,7 @@ impl Jumpable for ScrollbackState { self.cursor = pos.clone(); } - return Ok(count.saturating_sub(len)); + Ok(count.saturating_sub(len)) }, } } @@ -929,48 +1015,42 @@ impl Promptable for ScrollbackState { fn prompt( &mut self, act: &PromptAction, - _: &ProgramContext, + ctx: &ProgramContext, store: &mut ProgramStore, ) -> EditResult, ProgramContext)>, IambInfo> { let info = store.application.get_room_info(self.room_id.clone()); + let thread = self.get_thread(info); - let _ = if let Some(key) = self.cursor.to_key(info) { - key - } else { + let Some(key) = self.cursor.to_key(thread) else { let msg = "No message currently selected"; let err = EditError::Failure(msg.into()); - return Err(err); }; match act { PromptAction::Submit => { - // XXX: I'm not sure exactly what to do here yet. I think I want this to display a - // pop-over ListState with actions that can then be submitted: - // - // - Create a reply - // - Edit a message - // - Redact a message - // - React to a message - // - Report a message - // - Download an attachment - // - // Each of these should correspond to a command that a user can run. For example, - // running `:reply` when hovering over a message should be equivalent to opening - // the pop-up and selecting "Reply To This Message". - return Ok(vec![]); + if self.thread.is_some() { + let msg = + "You are already in a thread. Use :reply to reply to a specific message."; + let err = EditError::Failure(msg.into()); + Err(err) + } else { + let root = key.1.clone(); + let room_id = self.room_id.clone(); + let id = IambId::Room(room_id, Some(root)); + let open = WindowAction::Switch(OpenTarget::Application(id)); + Ok(vec![(open.into(), ctx.clone())]) + } }, PromptAction::Abort(..) => { let msg = "Cannot abort a message."; let err = EditError::Failure(msg.into()); - - return Err(err); + Err(err) }, PromptAction::Recall(..) => { let msg = "Cannot recall previous messages."; let err = EditError::Failure(msg.into()); - - return Err(err); + Err(err) }, } } @@ -988,8 +1068,9 @@ impl ScrollActions for ScrollbackState { let info = store.application.rooms.get_or_default(self.room_id.clone()); let settings = &store.application.settings; let mut corner = self.viewctx.corner.clone(); + let thread = self.get_thread(info); - let last_key = if let Some(k) = info.messages.last_key_value() { + let last_key = if let Some(k) = thread.last_key_value() { k.0 } else { return Ok(None); @@ -1008,11 +1089,11 @@ impl ScrollActions for ScrollbackState { match dir { MoveDir2D::Up => { - let first_key = info.messages.first_key_value().map(|f| f.0.clone()); + let first_key = thread.first_key_value().map(|f| f.0.clone()); - for (key, item) in info.messages.range(..=&corner_key).rev() { + for (key, item) in thread.range(..=&corner_key).rev() { let sel = key == cursor_key; - let prev = prevmsg(key, info); + let prev = prevmsg(key, thread); let txt = item.show(prev, sel, &self.viewctx, info, settings); let len = txt.height().max(1); let max = len.saturating_sub(1); @@ -1037,9 +1118,9 @@ impl ScrollActions for ScrollbackState { } }, MoveDir2D::Down => { - let mut prev = prevmsg(&corner_key, info); + let mut prev = prevmsg(&corner_key, thread); - for (key, item) in info.messages.range(&corner_key..) { + for (key, item) in thread.range(&corner_key..) { let sel = key == cursor_key; let txt = item.show(prev, sel, &self.viewctx, info, settings); let len = txt.height().max(1); @@ -1101,8 +1182,9 @@ impl ScrollActions for ScrollbackState { Axis::Vertical => { let info = store.application.rooms.get_or_default(self.room_id.clone()); let settings = &store.application.settings; + let thread = self.get_thread(info); - if let Some(key) = self.cursor.to_key(info).cloned() { + if let Some(key) = self.cursor.to_key(thread).cloned() { self.scrollview(key, pos, info, settings); } @@ -1237,14 +1319,18 @@ impl<'a> StatefulWidget for Scrollback<'a> { state.viewctx.corner = state.cursor.clone(); } + let thread = state.get_thread(info); + let cursor = &state.cursor; - let cursor_key = if let Some(k) = cursor.to_key(info) { + let cursor_key = if let Some(k) = cursor.to_key(thread) { k } else { - self.store - .application - .need_load - .insert(state.room_id.to_owned(), Need::MESSAGES); + if state.need_more_messages(info) { + self.store + .application + .need_load + .insert(state.room_id.to_owned(), Need::MESSAGES); + } return; }; @@ -1252,16 +1338,16 @@ impl<'a> StatefulWidget for Scrollback<'a> { let corner_key = if let Some(k) = &corner.timestamp { k.clone() } else { - nth_key_before(cursor_key.clone(), height, info) + nth_key_before(cursor_key.clone(), height, thread) }; let foc = self.focused || cursor.timestamp.is_some(); let full = std::mem::take(&mut state.show_full_on_redraw) || cursor.timestamp.is_none(); let mut lines = vec![]; let mut sawit = false; - let mut prev = prevmsg(&corner_key, info); + let mut prev = prevmsg(&corner_key, thread); - for (key, item) in info.messages.range(&corner_key..) { + for (key, item) in thread.range(&corner_key..) { let sel = key == cursor_key; let txt = item.show(prev, foc && sel, &state.viewctx, info, settings); @@ -1338,14 +1424,13 @@ impl<'a> StatefulWidget for Scrollback<'a> { state.cursor.timestamp.is_none() { // If the cursor is at the last message, then update the read marker. - if let Some((k, _)) = info.messages.last_key_value() { + if let Some((k, _)) = thread.last_key_value() { info.set_receipt(settings.profile.user_id.clone(), 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 state.need_more_messages(info) { // If the top of the screen is the older message, load more. self.store .application @@ -1364,7 +1449,7 @@ mod tests { async fn test_search_messages() { let room_id = TEST_ROOM1_ID.clone(); let mut store = mock_store().await; - let mut scrollback = ScrollbackState::new(room_id.clone()); + let mut scrollback = ScrollbackState::new(room_id.clone(), None); let ctx = ProgramContext::default(); let next = MoveDirMod::Exact(MoveDir1D::Next); @@ -1418,7 +1503,7 @@ mod tests { #[tokio::test] async fn test_movement() { let mut store = mock_store().await; - let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone()); + let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone(), None); let ctx = ProgramContext::default(); let prev = |n: usize| EditTarget::Motion(MoveType::Line(MoveDir1D::Previous), n.into()); @@ -1452,7 +1537,7 @@ mod tests { #[tokio::test] async fn test_dirscroll() { let mut store = mock_store().await; - let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone()); + let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone(), None); let ctx = ProgramContext::default(); let prev = MoveDir2D::Up; @@ -1603,7 +1688,7 @@ mod tests { #[tokio::test] async fn test_cursorpos() { let mut store = mock_store().await; - let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone()); + let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone(), None); let ctx = ProgramContext::default(); // Skip rendering typing notices. diff --git a/src/windows/room/space.rs b/src/windows/room/space.rs index 8218982..c3893ae 100644 --- a/src/windows/room/space.rs +++ b/src/windows/room/space.rs @@ -39,7 +39,7 @@ pub struct SpaceState { impl SpaceState { pub fn new(room: MatrixRoom) -> Self { let room_id = room.room_id().to_owned(); - let content = IambBufferId::Room(room_id.clone(), RoomFocus::Scrollback); + let content = IambBufferId::Room(room_id.clone(), None, RoomFocus::Scrollback); let list = ListState::new(content, vec![]); let last_fetch = None; diff --git a/src/worker.rs b/src/worker.rs index f71d673..c6dd3fc 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -409,7 +409,7 @@ fn members_insert( ) { if let Ok(members) = res { let ChatStore { rooms, .. } = &mut store.application; - let info = rooms.get_or_default(room_id.clone()); + let info = rooms.get_or_default(room_id); for member in members { let user_id = member.user_id(); @@ -1069,12 +1069,20 @@ impl ClientWorker { match info.keys.get(redacts) { None => return, - Some(EventLocation::Message(key)) => { + Some(EventLocation::Message(None, key)) => { if let Some(msg) = info.messages.get_mut(key) { let ev = SyncRoomRedactionEvent::Original(ev); msg.redact(ev, room_version); } }, + Some(EventLocation::Message(Some(root), key)) => { + if let Some(thread) = info.threads.get_mut(root) { + if let Some(msg) = thread.get_mut(key) { + let ev = SyncRoomRedactionEvent::Original(ev); + msg.redact(ev, room_version); + } + } + }, Some(EventLocation::Reaction(event_id)) => { if let Some(reactions) = info.reactions.get_mut(event_id) { reactions.remove(redacts);