Add :replied to go to the message the selected message replied to (#452)

This commit is contained in:
vaw
2025-10-26 14:36:46 +00:00
committed by GitHub
parent 7ccb1cbf2c
commit 3149f79d11
7 changed files with 116 additions and 52 deletions

View File

@@ -116,6 +116,8 @@ Redact the selected message with the optional reason.
Reply to the selected message. Reply to the selected message.
.It Sy ":cancel" .It Sy ":cancel"
Cancel the currently drafted message including replies. Cancel the currently drafted message including replies.
.It Sy ":replied"
Go to the message the current message replied to.
.It Sy ":upload [path]" .It Sy ":upload [path]"
Upload an attachment and send it to the currently selected room. Upload an attachment and send it to the currently selected room.
.El .El

View File

@@ -169,6 +169,9 @@ pub enum MessageAction {
/// Reply to a message. /// Reply to a message.
Reply, Reply,
/// Go to the message the hovered message replied to.
Replied,
/// Unreact to a message. /// Unreact to a message.
/// ///
/// If no specific Emoji to remove to is specified, then all reactions from the user on the /// If no specific Emoji to remove to is specified, then all reactions from the user on the
@@ -1503,14 +1506,19 @@ impl SyncInfo {
} }
} }
bitflags::bitflags! { static MESSAGE_NEED_TTL: u8 = 30;
/// Load-needs
#[derive(Debug, Default, PartialEq)] #[derive(Debug, PartialEq)]
pub struct Need: u32 { /// Load messages until the event is loaded or `ttl` loads are exceeded
const EMPTY = 0b00000000; pub struct MessageNeed {
const MESSAGES = 0b00000001; pub event_id: OwnedEventId,
const MEMBERS = 0b00000010; pub ttl: u8,
} }
#[derive(Default, Debug, PartialEq)]
pub struct Need {
pub members: bool,
pub messages: Option<Vec<MessageNeed>>,
} }
/// Things that need loading for different rooms. /// Things that need loading for different rooms.
@@ -1520,9 +1528,31 @@ pub struct RoomNeeds {
} }
impl RoomNeeds { impl RoomNeeds {
/// Mark a room for needing something to be loaded. /// Mark a room for needing to load members.
pub fn insert(&mut self, room_id: OwnedRoomId, need: Need) { pub fn need_members(&mut self, room_id: OwnedRoomId) {
self.needs.entry(room_id).or_default().insert(need); 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<MessageNeed>) {
self.needs
.entry(room_id)
.or_default()
.messages
.get_or_insert_default()
.extend(message_needs);
} }
pub fn rooms(&self) -> usize { pub fn rooms(&self) -> usize {
@@ -2300,12 +2330,12 @@ pub mod tests {
let mut need_load = RoomNeeds::default(); let mut need_load = RoomNeeds::default();
need_load.insert(room_id.clone(), Need::MESSAGES); need_load.need_messages(room_id.clone());
need_load.insert(room_id.clone(), Need::MEMBERS); need_load.need_members(room_id.clone());
assert_eq!(need_load.into_iter().collect::<Vec<(OwnedRoomId, Need)>>(), vec![( assert_eq!(need_load.into_iter().collect::<Vec<(OwnedRoomId, Need)>>(), vec![(
room_id, room_id,
Need::MESSAGES | Need::MEMBERS, Need { members: true, messages: Some(Vec::new()) }
)],); )],);
} }

View File

@@ -286,6 +286,17 @@ fn iamb_reply(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
return Ok(step); 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 { fn iamb_editor(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
if !desc.arg.text.is_empty() { if !desc.arg.text.is_empty() {
return Result::Err(CommandError::InvalidArgument); return Result::Err(CommandError::InvalidArgument);
@@ -767,6 +778,11 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
aliases: vec![], aliases: vec![],
f: iamb_reply, f: iamb_reply,
}); });
cmds.add_command(ProgramCommand {
name: "replied".into(),
aliases: vec![],
f: iamb_replied,
});
cmds.add_command(ProgramCommand { cmds.add_command(ProgramCommand {
name: "rooms".into(), name: "rooms".into(),
aliases: vec![], aliases: vec![],

View File

@@ -66,7 +66,6 @@ use crate::base::{
IambInfo, IambInfo,
IambResult, IambResult,
MessageAction, MessageAction,
Need,
ProgramAction, ProgramAction,
ProgramContext, ProgramContext,
ProgramStore, ProgramStore,
@@ -801,7 +800,7 @@ impl Window<IambInfo> for IambWindow {
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, thread, 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.need_members(room.id().to_owned());
return Ok(room.into()); return Ok(room.into());
}, },
IambId::DirectList => { IambId::DirectList => {
@@ -863,7 +862,7 @@ impl Window<IambInfo> for IambWindow {
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, None, 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.need_members(room.id().to_owned());
Ok(room.into()) Ok(room.into())
} }
} }

View File

@@ -450,6 +450,21 @@ impl ChatState {
Ok(None) 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) => { MessageAction::Unreact(reaction, literal) => {
let emoji = match reaction { let emoji = match reaction {
reaction if literal => reaction, reaction if literal => reaction,

View File

@@ -47,7 +47,6 @@ use crate::{
IambId, IambId,
IambInfo, IambInfo,
IambResult, IambResult,
Need,
ProgramContext, ProgramContext,
ProgramStore, ProgramStore,
RoomFetchStatus, RoomFetchStatus,
@@ -165,6 +164,12 @@ impl ScrollbackState {
self.cursor = MessageCursor::latest(); 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. /// Set the dimensions and placement within the terminal window for this list.
pub fn set_term_info(&mut self, area: Rect) { pub fn set_term_info(&mut self, area: Rect) {
self.viewctx.dimensions = (area.width as usize, area.height as usize); self.viewctx.dimensions = (area.width as usize, area.height as usize);
@@ -689,10 +694,7 @@ impl EditorActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
let (mc, needs_load) = self.find_message(key, dir, &needle, count, info); let (mc, needs_load) = self.find_message(key, dir, &needle, count, info);
if needs_load { if needs_load {
store store.application.need_load.need_messages(self.room_id.clone());
.application
.need_load
.insert(self.room_id.clone(), Need::MESSAGES);
} }
mc mc
}, },
@@ -768,10 +770,7 @@ impl EditorActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
let (mc, needs_load) = self.find_message(key, dir, &needle, count, info); let (mc, needs_load) = self.find_message(key, dir, &needle, count, info);
if needs_load { if needs_load {
store store.application.need_load.need_messages(self.room_id.to_owned());
.application
.need_load
.insert(self.room_id.to_owned(), Need::MESSAGES);
} }
mc.map(|c| self._range_to(c)) mc.map(|c| self._range_to(c))
@@ -1328,10 +1327,7 @@ impl StatefulWidget for Scrollback<'_> {
k k
} else { } else {
if state.need_more_messages(info) { if state.need_more_messages(info) {
self.store self.store.application.need_load.need_messages(state.room_id.to_owned());
.application
.need_load
.insert(state.room_id.to_owned(), Need::MESSAGES);
} }
return; return;
}; };
@@ -1435,10 +1431,7 @@ impl StatefulWidget for Scrollback<'_> {
// Check whether we should load older messages for this room. // Check whether we should load older messages for this room.
if state.need_more_messages(info) { if state.need_more_messages(info) {
// 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.need_load.need_messages(state.room_id.to_owned());
.application
.need_load
.insert(state.room_id.to_owned(), Need::MESSAGES);
} }
info.draw_last = self.store.application.draw_curr; info.draw_last = self.store.application.draw_curr;
@@ -1448,7 +1441,7 @@ impl StatefulWidget for Scrollback<'_> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::tests::*; use crate::{base::Need, tests::*};
#[tokio::test] #[tokio::test]
async fn test_search_messages() { async fn test_search_messages() {
@@ -1493,7 +1486,7 @@ mod tests {
std::mem::take(&mut store.application.need_load) std::mem::take(&mut store.application.need_load)
.into_iter() .into_iter()
.collect::<Vec<(OwnedRoomId, Need)>>(), .collect::<Vec<(OwnedRoomId, Need)>>(),
vec![(room_id.clone(), Need::MESSAGES)] vec![(room_id.clone(), Need { messages: Some(Vec::new()), members: false })]
); );
// Search forward twice to MSG1. // Search forward twice to MSG1.

View File

@@ -88,7 +88,7 @@ use matrix_sdk::{
use modalkit::errors::UIError; use modalkit::errors::UIError;
use modalkit::prelude::{EditInfo, InfoMessage}; use modalkit::prelude::{EditInfo, InfoMessage};
use crate::base::Need; use crate::base::MessageNeed;
use crate::notifications::register_notifications; use crate::notifications::register_notifications;
use crate::{ use crate::{
base::{ base::{
@@ -216,7 +216,7 @@ async fn update_event_receipts(info: &mut RoomInfo, room: &MatrixRoom, event_id:
#[derive(Debug)] #[derive(Debug)]
enum Plan { enum Plan {
Messages(OwnedRoomId, Option<String>), Messages(OwnedRoomId, Option<String>, Vec<MessageNeed>),
Members(OwnedRoomId), Members(OwnedRoomId),
} }
@@ -225,8 +225,8 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec<Plan> {
let ChatStore { need_load, rooms, .. } = &mut locked.application; let ChatStore { need_load, rooms, .. } = &mut locked.application;
let mut plan = Vec::with_capacity(need_load.rooms() * 2); let mut plan = Vec::with_capacity(need_load.rooms() * 2);
for (room_id, mut need) in std::mem::take(need_load).into_iter() { for (room_id, need) in std::mem::take(need_load).into_iter() {
if need.contains(Need::MESSAGES) { if let Some(message_need) = need.messages {
let info = rooms.get_or_default(room_id.clone()); let info = rooms.get_or_default(room_id.clone());
if !info.recently_fetched() && !info.fetching { if !info.recently_fetched() && !info.fetching {
@@ -239,16 +239,11 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec<Plan> {
RoomFetchStatus::NotStarted => None, 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));
need.remove(Need::MESSAGES);
} }
} }
if need.contains(Need::MEMBERS) { if need.members {
plan.push(Plan::Members(room_id.to_owned())); 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<Plan> {
async fn run_plan(client: &Client, store: &AsyncProgramStore, plan: Plan, permits: &Semaphore) { async fn run_plan(client: &Client, store: &AsyncProgramStore, plan: Plan, permits: &Semaphore) {
let permit = permits.acquire().await; let permit = permits.acquire().await;
match plan { match plan {
Plan::Messages(room_id, fetch_id) => { Plan::Messages(room_id, fetch_id, message_need) => {
let limit = MIN_MSG_LOAD; let limit = MIN_MSG_LOAD;
let client = client.clone(); let client = client.clone();
let store_clone = store.clone(); let store_clone = store.clone();
let res = load_older_one(&client, &room_id, fetch_id, limit).await; let res = load_older_one(&client, &room_id, fetch_id, limit).await;
let mut locked = store.lock().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) => { Plan::Members(room_id) => {
let res = members_load(client, &room_id).await; let res = members_load(client, &room_id).await;
@@ -328,6 +323,7 @@ fn load_insert(
res: MessageFetchResult, res: MessageFetchResult,
locked: &mut ProgramStore, locked: &mut ProgramStore,
store: AsyncProgramStore, store: AsyncProgramStore,
message_needs: Vec<MessageNeed>,
) { ) {
let ChatStore { presences, rooms, worker, picker, settings, .. } = &mut locked.application; let ChatStore { presences, rooms, worker, picker, settings, .. } = &mut locked.application;
let info = rooms.get_or_default(room_id.clone()); let info = rooms.get_or_default(room_id.clone());
@@ -373,12 +369,25 @@ fn load_insert(
} }
info.fetch_id = fetch_id.map_or(RoomFetchStatus::Done, RoomFetchStatus::HaveMore); 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) => { Err(e) => {
warn!(room_id = room_id.as_str(), err = e.to_string(), "Failed to load older messages"); warn!(room_id = room_id.as_str(), err = e.to_string(), "Failed to load older messages");
// Wait and try again. // Wait and try again.
locked.application.need_load.insert(room_id, Need::MESSAGES); locked.application.need_load.need_messages_all(room_id, message_needs);
}, },
} }
} }
@@ -573,12 +582,12 @@ pub async fn do_first_sync(client: &Client, store: &AsyncProgramStore) -> Result
for room in sync_info.rooms.iter() { for room in sync_info.rooms.iter() {
let room_id = room.as_ref().0.room_id().to_owned(); 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() { for room in sync_info.dms.iter() {
let room_id = room.as_ref().0.room_id().to_owned(); 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(()) Ok(())