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.
.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

View File

@@ -169,6 +169,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
@@ -1503,14 +1506,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<Vec<MessageNeed>>,
}
/// Things that need loading for different rooms.
@@ -1520,9 +1528,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<MessageNeed>) {
self.needs
.entry(room_id)
.or_default()
.messages
.get_or_insert_default()
.extend(message_needs);
}
pub fn rooms(&self) -> usize {
@@ -2300,12 +2330,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<(OwnedRoomId, Need)>>(), vec![(
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);
}
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);
@@ -767,6 +778,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![],

View File

@@ -66,7 +66,6 @@ use crate::base::{
IambInfo,
IambResult,
MessageAction,
Need,
ProgramAction,
ProgramContext,
ProgramStore,
@@ -801,7 +800,7 @@ impl Window<IambInfo> 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<IambInfo> 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())
}
}

View File

@@ -450,6 +450,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,

View File

@@ -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<ProgramContext, ProgramStore, IambInfo> 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<ProgramContext, ProgramStore, IambInfo> 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<(OwnedRoomId, Need)>>(),
vec![(room_id.clone(), Need::MESSAGES)]
vec![(room_id.clone(), Need { messages: Some(Vec::new()), members: false })]
);
// Search forward twice to MSG1.

View File

@@ -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<String>),
Messages(OwnedRoomId, Option<String>, Vec<MessageNeed>),
Members(OwnedRoomId),
}
@@ -225,8 +225,8 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec<Plan> {
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<Plan> {
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<Plan> {
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;
@@ -328,6 +323,7 @@ fn load_insert(
res: MessageFetchResult,
locked: &mut ProgramStore,
store: AsyncProgramStore,
message_needs: Vec<MessageNeed>,
) {
let ChatStore { presences, rooms, worker, picker, settings, .. } = &mut locked.application;
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);
// 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);
},
}
}
@@ -573,12 +582,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(())