diff --git a/docs/iamb.1 b/docs/iamb.1 index 2487b927..5b6b09e2 100644 --- a/docs/iamb.1 +++ b/docs/iamb.1 @@ -114,6 +114,8 @@ Redact the selected message with the optional reason. Reply to the selected message. .It Sy ":cancel" Cancel the currently drafted message including replies. +.It Sy ":replied" +Go to the message the current message replied to. .It Sy ":upload [path]" Upload an attachment and send it to the currently selected room. .El diff --git a/src/base.rs b/src/base.rs index 1cd3071e..c6c485af 100644 --- a/src/base.rs +++ b/src/base.rs @@ -168,6 +168,9 @@ pub enum MessageAction { /// Reply to a message. Reply, + /// Go to the message the hovered message replied to. + Replied, + /// Unreact to a message. /// /// If no specific Emoji to remove to is specified, then all reactions from the user on the @@ -1474,14 +1477,19 @@ impl SyncInfo { } } -bitflags::bitflags! { - /// Load-needs - #[derive(Debug, Default, PartialEq)] - pub struct Need: u32 { - const EMPTY = 0b00000000; - const MESSAGES = 0b00000001; - const MEMBERS = 0b00000010; - } +static MESSAGE_NEED_TTL: u8 = 30; + +#[derive(Debug, PartialEq)] +/// Load messages until the event is loaded or `ttl` loads are exceeded +pub struct MessageNeed { + pub event_id: OwnedEventId, + pub ttl: u8, +} + +#[derive(Default, Debug, PartialEq)] +pub struct Need { + pub members: bool, + pub messages: Option>, } /// Things that need loading for different rooms. @@ -1491,9 +1499,31 @@ pub struct RoomNeeds { } impl RoomNeeds { - /// Mark a room for needing something to be loaded. - pub fn insert(&mut self, room_id: OwnedRoomId, need: Need) { - self.needs.entry(room_id).or_default().insert(need); + /// Mark a room for needing to load members. + pub fn need_members(&mut self, room_id: OwnedRoomId) { + self.needs.entry(room_id).or_default().members = true; + } + + /// Mark a room for needing to load messages. + pub fn need_messages(&mut self, room_id: OwnedRoomId) { + self.needs.entry(room_id).or_default().messages.get_or_insert_default(); + } + + /// Mark a room for needing to load messages until the given message is loaded or a retry limit + /// is exceeded. + pub fn need_message(&mut self, room_id: OwnedRoomId, event_id: OwnedEventId) { + let messages = &mut self.needs.entry(room_id).or_default().messages.get_or_insert_default(); + + messages.push(MessageNeed { event_id, ttl: MESSAGE_NEED_TTL }); + } + + pub fn need_messages_all(&mut self, room_id: OwnedRoomId, message_needs: Vec) { + self.needs + .entry(room_id) + .or_default() + .messages + .get_or_insert_default() + .extend(message_needs); } pub fn rooms(&self) -> usize { @@ -2275,12 +2305,12 @@ pub mod tests { let mut need_load = RoomNeeds::default(); - need_load.insert(room_id.clone(), Need::MESSAGES); - need_load.insert(room_id.clone(), Need::MEMBERS); + need_load.need_messages(room_id.clone()); + need_load.need_members(room_id.clone()); assert_eq!(need_load.into_iter().collect::>(), vec![( room_id, - Need::MESSAGES | Need::MEMBERS, + Need { members: true, messages: Some(Vec::new()) } )],); } diff --git a/src/commands.rs b/src/commands.rs index c70a2aff..6a15e0ea 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -275,6 +275,17 @@ fn iamb_reply(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { return Ok(step); } +fn iamb_replied(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + if !desc.arg.text.is_empty() { + return Result::Err(CommandError::InvalidArgument); + } + + let ract = IambAction::from(MessageAction::Replied); + let step = CommandStep::Continue(ract.into(), ctx.context.clone()); + + return Ok(step); +} + fn iamb_editor(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { if !desc.arg.text.is_empty() { return Result::Err(CommandError::InvalidArgument); @@ -751,6 +762,11 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) { aliases: vec![], f: iamb_reply, }); + cmds.add_command(ProgramCommand { + name: "replied".into(), + aliases: vec![], + f: iamb_replied, + }); cmds.add_command(ProgramCommand { name: "rooms".into(), aliases: vec![], diff --git a/src/windows/mod.rs b/src/windows/mod.rs index 12287817..4ed719b7 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -66,7 +66,6 @@ use crate::base::{ IambInfo, IambResult, MessageAction, - Need, ProgramAction, ProgramContext, ProgramStore, @@ -801,7 +800,7 @@ impl Window for IambWindow { let (room, name, tags) = store.application.worker.get_room(room_id)?; let room = RoomState::new(room, thread, name, tags, store); - store.application.need_load.insert(room.id().to_owned(), Need::MEMBERS); + store.application.need_load.need_members(room.id().to_owned()); return Ok(room.into()); }, IambId::DirectList => { @@ -863,7 +862,7 @@ impl Window for IambWindow { let (room, name, tags) = store.application.worker.get_room(room_id)?; let room = RoomState::new(room, None, name, tags, store); - store.application.need_load.insert(room.id().to_owned(), Need::MEMBERS); + store.application.need_load.need_members(room.id().to_owned()); Ok(room.into()) } } diff --git a/src/windows/room/chat.rs b/src/windows/room/chat.rs index cb584f46..f347c579 100644 --- a/src/windows/room/chat.rs +++ b/src/windows/room/chat.rs @@ -439,6 +439,21 @@ impl ChatState { Ok(None) }, + MessageAction::Replied => { + let Some(reply) = msg.reply_to() else { + let msg = "Selected message is not a reply"; + return Err(UIError::Failure(msg.into())); + }; + + let Some(key) = info.get_message_key(&reply) else { + store.application.need_load.need_message(self.room_id.clone(), reply); + let msg = "Replied to message will be loaded in the background"; + return Err(UIError::Failure(msg.into())); + }; + + self.scrollback.goto_message(key.clone()); + Ok(None) + }, MessageAction::Unreact(reaction, literal) => { let emoji = match reaction { reaction if literal => reaction, diff --git a/src/windows/room/scrollback.rs b/src/windows/room/scrollback.rs index 3d8ec7f4..6ad63d24 100644 --- a/src/windows/room/scrollback.rs +++ b/src/windows/room/scrollback.rs @@ -47,7 +47,6 @@ use crate::{ IambId, IambInfo, IambResult, - Need, ProgramContext, ProgramStore, RoomFetchStatus, @@ -165,6 +164,12 @@ impl ScrollbackState { self.cursor = MessageCursor::latest(); } + pub fn goto_message(&mut self, target: MessageKey) { + let mut cursor = MessageCursor::new(target, 0); + std::mem::swap(&mut cursor, &mut self.cursor); + self.jumped.push(cursor); + } + /// Set the dimensions and placement within the terminal window for this list. pub fn set_term_info(&mut self, area: Rect) { self.viewctx.dimensions = (area.width as usize, area.height as usize); @@ -689,10 +694,7 @@ impl EditorActions for ScrollbackState { let (mc, needs_load) = self.find_message(key, dir, &needle, count, info); if needs_load { - store - .application - .need_load - .insert(self.room_id.clone(), Need::MESSAGES); + store.application.need_load.need_messages(self.room_id.clone()); } mc }, @@ -768,10 +770,7 @@ impl EditorActions for ScrollbackState { let (mc, needs_load) = self.find_message(key, dir, &needle, count, info); if needs_load { - store - .application - .need_load - .insert(self.room_id.to_owned(), Need::MESSAGES); + store.application.need_load.need_messages(self.room_id.to_owned()); } mc.map(|c| self._range_to(c)) @@ -1328,10 +1327,7 @@ impl StatefulWidget for Scrollback<'_> { k } else { if state.need_more_messages(info) { - self.store - .application - .need_load - .insert(state.room_id.to_owned(), Need::MESSAGES); + self.store.application.need_load.need_messages(state.room_id.to_owned()); } return; }; @@ -1435,10 +1431,7 @@ impl StatefulWidget for Scrollback<'_> { // Check whether we should load older messages for this room. if state.need_more_messages(info) { // If the top of the screen is the older message, load more. - self.store - .application - .need_load - .insert(state.room_id.to_owned(), Need::MESSAGES); + self.store.application.need_load.need_messages(state.room_id.to_owned()); } info.draw_last = self.store.application.draw_curr; @@ -1448,7 +1441,7 @@ impl StatefulWidget for Scrollback<'_> { #[cfg(test)] mod tests { use super::*; - use crate::tests::*; + use crate::{base::Need, tests::*}; #[tokio::test] async fn test_search_messages() { @@ -1493,7 +1486,7 @@ mod tests { std::mem::take(&mut store.application.need_load) .into_iter() .collect::>(), - vec![(room_id.clone(), Need::MESSAGES)] + vec![(room_id.clone(), Need { messages: Some(Vec::new()), members: false })] ); // Search forward twice to MSG1. diff --git a/src/worker.rs b/src/worker.rs index 144c34cf..7f91a259 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -88,7 +88,7 @@ use matrix_sdk::{ use modalkit::errors::UIError; use modalkit::prelude::{EditInfo, InfoMessage}; -use crate::base::Need; +use crate::base::MessageNeed; use crate::notifications::register_notifications; use crate::{ base::{ @@ -216,7 +216,7 @@ async fn update_event_receipts(info: &mut RoomInfo, room: &MatrixRoom, event_id: #[derive(Debug)] enum Plan { - Messages(OwnedRoomId, Option), + Messages(OwnedRoomId, Option, Vec), Members(OwnedRoomId), } @@ -225,8 +225,8 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec { let ChatStore { need_load, rooms, .. } = &mut locked.application; let mut plan = Vec::with_capacity(need_load.rooms() * 2); - for (room_id, mut need) in std::mem::take(need_load).into_iter() { - if need.contains(Need::MESSAGES) { + for (room_id, need) in std::mem::take(need_load).into_iter() { + if let Some(message_need) = need.messages { let info = rooms.get_or_default(room_id.clone()); if !info.recently_fetched() && !info.fetching { @@ -239,16 +239,11 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec { RoomFetchStatus::NotStarted => None, }; - plan.push(Plan::Messages(room_id.to_owned(), fetch_id)); - need.remove(Need::MESSAGES); + plan.push(Plan::Messages(room_id.to_owned(), fetch_id, message_need)); } } - if need.contains(Need::MEMBERS) { + if need.members { plan.push(Plan::Members(room_id.to_owned())); - need.remove(Need::MEMBERS); - } - if !need.is_empty() { - need_load.insert(room_id, need); } } @@ -258,14 +253,14 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec { async fn run_plan(client: &Client, store: &AsyncProgramStore, plan: Plan, permits: &Semaphore) { let permit = permits.acquire().await; match plan { - Plan::Messages(room_id, fetch_id) => { + Plan::Messages(room_id, fetch_id, message_need) => { let limit = MIN_MSG_LOAD; let client = client.clone(); let store_clone = store.clone(); let res = load_older_one(&client, &room_id, fetch_id, limit).await; let mut locked = store.lock().await; - load_insert(room_id, res, locked.deref_mut(), store_clone); + load_insert(room_id, res, locked.deref_mut(), store_clone, message_need); }, Plan::Members(room_id) => { let res = members_load(client, &room_id).await; @@ -325,6 +320,7 @@ fn load_insert( res: MessageFetchResult, locked: &mut ProgramStore, store: AsyncProgramStore, + message_needs: Vec, ) { let ChatStore { presences, rooms, worker, picker, settings, .. } = &mut locked.application; let info = rooms.get_or_default(room_id.clone()); @@ -370,12 +366,25 @@ fn load_insert( } info.fetch_id = fetch_id.map_or(RoomFetchStatus::Done, RoomFetchStatus::HaveMore); + + // check if more are needed + let needs: Vec<_> = message_needs + .into_iter() + .filter(|need| !info.keys.contains_key(&need.event_id) && need.ttl > 0) + .map(|mut need| { + need.ttl -= 1; + need + }) + .collect(); + if !needs.is_empty() { + locked.application.need_load.need_messages_all(room_id, needs); + } }, Err(e) => { warn!(room_id = room_id.as_str(), err = e.to_string(), "Failed to load older messages"); // Wait and try again. - locked.application.need_load.insert(room_id, Need::MESSAGES); + locked.application.need_load.need_messages_all(room_id, message_needs); }, } } @@ -566,12 +575,12 @@ pub async fn do_first_sync(client: &Client, store: &AsyncProgramStore) -> Result for room in sync_info.rooms.iter() { let room_id = room.as_ref().0.room_id().to_owned(); - need_load.insert(room_id, Need::MESSAGES); + need_load.need_messages(room_id); } for room in sync_info.dms.iter() { let room_id = room.as_ref().0.room_id().to_owned(); - need_load.insert(room_id, Need::MESSAGES); + need_load.need_messages(room_id); } Ok(())