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