From c248452b8322d759603573b79540fba2f6168cef Mon Sep 17 00:00:00 2001 From: vaw Date: Sat, 7 Jun 2025 23:57:23 +0200 Subject: [PATCH 1/6] Implement goto replied for loaded messages --- src/base.rs | 3 +++ src/commands.rs | 16 ++++++++++++++++ src/windows/room/chat.rs | 14 ++++++++++++++ src/windows/room/scrollback.rs | 6 ++++++ 4 files changed, 39 insertions(+) diff --git a/src/base.rs b/src/base.rs index 1cd3071e..6b3ea51c 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 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/room/chat.rs b/src/windows/room/chat.rs index cb584f46..893db37d 100644 --- a/src/windows/room/chat.rs +++ b/src/windows/room/chat.rs @@ -439,6 +439,20 @@ 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 { + let msg = "Replied to message not loaded"; + 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..7062d661 100644 --- a/src/windows/room/scrollback.rs +++ b/src/windows/room/scrollback.rs @@ -165,6 +165,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); From 035fb74f2efcfee91a3405fe37ea7df07d69d5c8 Mon Sep 17 00:00:00 2001 From: vaw Date: Sun, 8 Jun 2025 00:11:39 +0200 Subject: [PATCH 2/6] Load older messages once --- src/windows/room/chat.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/windows/room/chat.rs b/src/windows/room/chat.rs index 893db37d..7748108f 100644 --- a/src/windows/room/chat.rs +++ b/src/windows/room/chat.rs @@ -78,6 +78,7 @@ use crate::base::{ IambInfo, IambResult, MessageAction, + Need, ProgramAction, ProgramContext, ProgramStore, @@ -446,6 +447,7 @@ impl ChatState { }; let Some(key) = info.get_message_key(&reply) else { + store.application.need_load.insert(self.room_id.clone(), Need::MESSAGES); let msg = "Replied to message not loaded"; return Err(UIError::Failure(msg.into())); }; From ffd16a8acf6daac1c55332ec9c39910e081e44cc Mon Sep 17 00:00:00 2001 From: vaw Date: Sun, 8 Jun 2025 00:26:36 +0200 Subject: [PATCH 3/6] Change `Need` to be a struct --- src/base.rs | 29 +++++++++++++++-------------- src/windows/mod.rs | 5 ++--- src/windows/room/chat.rs | 3 +-- src/windows/room/scrollback.rs | 25 ++++++------------------- src/worker.rs | 18 ++++++------------ 5 files changed, 30 insertions(+), 50 deletions(-) diff --git a/src/base.rs b/src/base.rs index 6b3ea51c..6fcc446d 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1477,14 +1477,10 @@ impl SyncInfo { } } -bitflags::bitflags! { - /// Load-needs - #[derive(Debug, Default, PartialEq)] - pub struct Need: u32 { - const EMPTY = 0b00000000; - const MESSAGES = 0b00000001; - const MEMBERS = 0b00000010; - } +#[derive(Default, Debug, PartialEq)] +pub struct Need { + pub members: bool, + pub messages: bool, } /// Things that need loading for different rooms. @@ -1494,9 +1490,14 @@ 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 = true; } pub fn rooms(&self) -> usize { @@ -2278,12 +2279,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: true } )],); } 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 7748108f..3621aab4 100644 --- a/src/windows/room/chat.rs +++ b/src/windows/room/chat.rs @@ -78,7 +78,6 @@ use crate::base::{ IambInfo, IambResult, MessageAction, - Need, ProgramAction, ProgramContext, ProgramStore, @@ -447,7 +446,7 @@ impl ChatState { }; let Some(key) = info.get_message_key(&reply) else { - store.application.need_load.insert(self.room_id.clone(), Need::MESSAGES); + store.application.need_load.need_messages(self.room_id.clone()); let msg = "Replied to message not loaded"; return Err(UIError::Failure(msg.into())); }; diff --git a/src/windows/room/scrollback.rs b/src/windows/room/scrollback.rs index 7062d661..dced3bc4 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, @@ -695,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 }, @@ -774,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)) @@ -1334,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; }; @@ -1441,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; @@ -1454,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() { @@ -1499,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: true, members: false })] ); // Search forward twice to MSG1. diff --git a/src/worker.rs b/src/worker.rs index 144c34cf..e9448fa9 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -88,7 +88,6 @@ use matrix_sdk::{ use modalkit::errors::UIError; use modalkit::prelude::{EditInfo, InfoMessage}; -use crate::base::Need; use crate::notifications::register_notifications; use crate::{ base::{ @@ -225,8 +224,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 need.messages { let info = rooms.get_or_default(room_id.clone()); if !info.recently_fetched() && !info.fetching { @@ -240,15 +239,10 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec { }; plan.push(Plan::Messages(room_id.to_owned(), fetch_id)); - need.remove(Need::MESSAGES); } } - 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); } } @@ -375,7 +369,7 @@ fn load_insert( 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(room_id); }, } } @@ -566,12 +560,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(()) From 55b250f39c8ab22adf44121b0b7d10e27bd25355 Mon Sep 17 00:00:00 2001 From: vaw Date: Sun, 8 Jun 2025 01:13:03 +0200 Subject: [PATCH 4/6] Add ability to load until specific message --- src/base.rs | 31 ++++++++++++++++++++++++++++--- src/windows/room/chat.rs | 4 ++-- src/windows/room/scrollback.rs | 2 +- src/worker.rs | 27 +++++++++++++++++++++------ 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/base.rs b/src/base.rs index 6fcc446d..5f9311f9 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1477,10 +1477,18 @@ impl SyncInfo { } } +static MESSAGE_NEED_TTL: u8 = 10; +#[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: bool, + pub messages: Option>, } /// Things that need loading for different rooms. @@ -1497,7 +1505,24 @@ impl RoomNeeds { /// 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 = true; + 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 { @@ -2284,7 +2309,7 @@ pub mod tests { assert_eq!(need_load.into_iter().collect::>(), vec![( room_id, - Need { members: true, messages: true } + Need { members: true, messages: Some(Vec::new()) } )],); } diff --git a/src/windows/room/chat.rs b/src/windows/room/chat.rs index 3621aab4..f347c579 100644 --- a/src/windows/room/chat.rs +++ b/src/windows/room/chat.rs @@ -446,8 +446,8 @@ impl ChatState { }; let Some(key) = info.get_message_key(&reply) else { - store.application.need_load.need_messages(self.room_id.clone()); - let msg = "Replied to message not loaded"; + 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())); }; diff --git a/src/windows/room/scrollback.rs b/src/windows/room/scrollback.rs index dced3bc4..6ad63d24 100644 --- a/src/windows/room/scrollback.rs +++ b/src/windows/room/scrollback.rs @@ -1486,7 +1486,7 @@ mod tests { std::mem::take(&mut store.application.need_load) .into_iter() .collect::>(), - vec![(room_id.clone(), Need { messages: true, members: false })] + 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 e9448fa9..7f91a259 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -88,6 +88,7 @@ use matrix_sdk::{ use modalkit::errors::UIError; use modalkit::prelude::{EditInfo, InfoMessage}; +use crate::base::MessageNeed; use crate::notifications::register_notifications; use crate::{ base::{ @@ -215,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,7 +226,7 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec { let mut plan = Vec::with_capacity(need_load.rooms() * 2); for (room_id, need) in std::mem::take(need_load).into_iter() { - if need.messages { + if let Some(message_need) = need.messages { let info = rooms.get_or_default(room_id.clone()); if !info.recently_fetched() && !info.fetching { @@ -238,7 +239,7 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec { RoomFetchStatus::NotStarted => None, }; - plan.push(Plan::Messages(room_id.to_owned(), fetch_id)); + plan.push(Plan::Messages(room_id.to_owned(), fetch_id, message_need)); } } if need.members { @@ -252,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; @@ -319,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()); @@ -364,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.need_messages(room_id); + locked.application.need_load.need_messages_all(room_id, message_needs); }, } } From e95244dc99ea21ca14d54ecfdc773b3a2c2386c2 Mon Sep 17 00:00:00 2001 From: vaw Date: Sun, 8 Jun 2025 20:07:42 +0200 Subject: [PATCH 5/6] Document `:replied` --- docs/iamb.1 | 2 ++ 1 file changed, 2 insertions(+) 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 From 14f18df344b463393223a821b9a4c4231daf2bc2 Mon Sep 17 00:00:00 2001 From: Ulyssa Date: Sun, 26 Oct 2025 07:30:13 -0700 Subject: [PATCH 6/6] Bump `MESSAGE_NEED_TTL` --- src/base.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/base.rs b/src/base.rs index 5f9311f9..c6c485af 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1477,7 +1477,8 @@ impl SyncInfo { } } -static MESSAGE_NEED_TTL: u8 = 10; +static MESSAGE_NEED_TTL: u8 = 30; + #[derive(Debug, PartialEq)] /// Load messages until the event is loaded or `ttl` loads are exceeded pub struct MessageNeed {