Add :replied to go to the message the selected message replied to (#452)
This commit is contained in:
@@ -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
|
||||
|
||||
58
src/base.rs
58
src/base.rs
@@ -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()) }
|
||||
)],);
|
||||
}
|
||||
|
||||
|
||||
@@ -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![],
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(())
|
||||
|
||||
Reference in New Issue
Block a user