Support editing messages (#4)

This commit is contained in:
Ulyssa 2023-01-19 16:05:02 -08:00
parent 69125e3fc4
commit 8966644f6e
No known key found for this signature in database
GPG key ID: 1B3965A3D18B9B64
6 changed files with 195 additions and 37 deletions

View file

@ -74,7 +74,7 @@ two other TUI clients and Element Web:
| New user registration | ❌ | ❌ | ❌ | ✔️ |
| VOIP | ❌ | ❌ | ❌ | ✔️ |
| Reactions | ❌ ([#2]) | ✔️ | ❌ | ✔️ |
| Message editing | ❌ ([#4]) | ✔️ | ❌ | ✔️ |
| Message editing | ✔️ | ✔️ | ❌ | ✔️ |
| Room upgrades | ❌ | ✔️ | ❌ | ✔️ |
| Localisations | ❌ | 1 | ❌ | 44 |
| SSO Support | ❌ | ✔️ | ✔️ | ✔️ |

View file

@ -8,7 +8,20 @@ use tracing::warn;
use matrix_sdk::{
encryption::verification::SasVerification,
ruma::{OwnedRoomId, OwnedUserId, RoomId},
ruma::{
events::room::message::{
OriginalRoomMessageEvent,
Relation,
Replacement,
RoomMessageEvent,
RoomMessageEventContent,
},
EventId,
OwnedEventId,
OwnedRoomId,
OwnedUserId,
RoomId,
},
};
use modalkit::{
@ -41,7 +54,7 @@ use modalkit::{
};
use crate::{
message::{Message, Messages},
message::{Message, MessageEvent, MessageKey, MessageTimeStamp, Messages},
worker::Requester,
ApplicationSettings,
};
@ -61,9 +74,22 @@ pub enum VerifyAction {
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MessageAction {
/// Cance the current reply or edit.
Cancel,
/// Download an attachment to the given path.
///
/// The [bool] argument controls whether to overwrite any already existing file at the
/// destination path.
Download(Option<String>, bool),
/// Edit a sent message.
Edit,
/// Redact a message.
Redact(Option<String>),
/// Reply to a message.
Reply,
}
@ -251,13 +277,72 @@ pub enum RoomFetchStatus {
#[derive(Default)]
pub struct RoomInfo {
pub name: Option<String>,
pub keys: HashMap<OwnedEventId, MessageKey>,
pub messages: Messages,
pub fetch_id: RoomFetchStatus,
pub fetch_last: Option<Instant>,
pub users_typing: Option<(Instant, Vec<OwnedUserId>)>,
}
impl RoomInfo {
pub fn get_event(&self, event_id: &EventId) -> Option<&Message> {
self.messages.get(self.keys.get(event_id)?)
}
pub fn insert_edit(&mut self, msg: Replacement) {
let event_id = msg.event_id;
let new_content = msg.new_content;
let key = if let Some(k) = self.keys.get(&event_id) {
k
} else {
return;
};
let msg = if let Some(msg) = self.messages.get_mut(key) {
msg
} else {
return;
};
match &mut msg.event {
MessageEvent::Original(orig) => {
orig.content = *new_content;
},
MessageEvent::Local(content) => {
*content = new_content;
},
MessageEvent::Redacted(_) => {
return;
},
}
}
pub fn insert_message(&mut self, msg: RoomMessageEvent) {
let event_id = msg.event_id().to_owned();
let key = (msg.origin_server_ts().into(), event_id.clone());
self.keys.insert(event_id.clone(), key.clone());
self.messages.insert(key, msg.into());
// Remove any echo.
let key = (MessageTimeStamp::LocalEcho, event_id);
let _ = self.messages.remove(&key);
}
pub fn insert(&mut self, msg: RoomMessageEvent) {
match msg {
RoomMessageEvent::Original(OriginalRoomMessageEvent {
content:
RoomMessageEventContent { relates_to: Some(Relation::Replacement(repl)), .. },
..
}) => self.insert_edit(repl),
_ => self.insert_message(msg),
}
}
fn recently_fetched(&self) -> bool {
self.fetch_last.map_or(false, |i| i.elapsed() < ROOM_FETCH_DEBOUNCE)
}
@ -388,9 +473,7 @@ impl ChatStore {
match res {
Ok((fetch_id, msgs)) => {
for msg in msgs.into_iter() {
let key = (msg.origin_server_ts().into(), msg.event_id().to_owned());
info.messages.insert(key, Message::from(msg));
info.insert(msg);
}
info.fetch_id =

View file

@ -144,6 +144,17 @@ fn iamb_cancel(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
return Ok(step);
}
fn iamb_edit(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
if !desc.arg.text.is_empty() {
return Result::Err(CommandError::InvalidArgument);
}
let ract = IambAction::from(MessageAction::Edit);
let step = CommandStep::Continue(ract.into(), ctx.context.take());
return Ok(step);
}
fn iamb_redact(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
let args = desc.arg.strings()?;
@ -269,6 +280,7 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
cmds.add_command(ProgramCommand { names: vec!["cancel".into()], f: iamb_cancel });
cmds.add_command(ProgramCommand { names: vec!["dms".into()], f: iamb_dms });
cmds.add_command(ProgramCommand { names: vec!["download".into()], f: iamb_download });
cmds.add_command(ProgramCommand { names: vec!["edit".into()], f: iamb_edit });
cmds.add_command(ProgramCommand { names: vec!["invite".into()], f: iamb_invite });
cmds.add_command(ProgramCommand { names: vec!["join".into()], f: iamb_join });
cmds.add_command(ProgramCommand { names: vec!["members".into()], f: iamb_members });

View file

@ -111,6 +111,18 @@ pub fn mock_message5() -> Message {
mock_room1_message(content, TEST_USER2.clone(), MSG4_KEY.clone())
}
pub fn mock_keys() -> HashMap<OwnedEventId, MessageKey> {
let mut keys = HashMap::new();
keys.insert(MSG1_EVID.clone(), MSG1_KEY.clone());
keys.insert(MSG2_EVID.clone(), MSG2_KEY.clone());
keys.insert(MSG3_EVID.clone(), MSG3_KEY.clone());
keys.insert(MSG4_EVID.clone(), MSG4_KEY.clone());
keys.insert(MSG5_EVID.clone(), MSG5_KEY.clone());
keys
}
pub fn mock_messages() -> Messages {
let mut messages = BTreeMap::new();
@ -126,7 +138,10 @@ pub fn mock_messages() -> Messages {
pub fn mock_room() -> RoomInfo {
RoomInfo {
name: Some("Watercooler Discussion".into()),
keys: mock_keys(),
messages: mock_messages(),
fetch_id: RoomFetchStatus::NotStarted,
fetch_last: None,
users_typing: None,

View file

@ -1,6 +1,7 @@
use std::borrow::Cow;
use std::ffi::OsStr;
use std::fs;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use matrix_sdk::{
@ -11,6 +12,8 @@ use matrix_sdk::{
events::room::message::{
MessageType,
OriginalRoomMessageEvent,
Relation,
Replacement,
RoomMessageEventContent,
TextMessageEventContent,
},
@ -82,6 +85,7 @@ pub struct ChatState {
focus: RoomFocus,
reply_to: Option<MessageKey>,
editing: Option<MessageKey>,
}
impl ChatState {
@ -104,6 +108,7 @@ impl ChatState {
focus: RoomFocus::MessageBar,
reply_to: None,
editing: None,
}
}
@ -120,6 +125,7 @@ impl ChatState {
fn reset(&mut self) -> EditRope {
self.reply_to = None;
self.editing = None;
self.tbox.reset()
}
@ -145,6 +151,7 @@ impl ChatState {
match act {
MessageAction::Cancel => {
self.reply_to = None;
self.editing = None;
Ok(None)
},
@ -224,6 +231,40 @@ impl ChatState {
Err(IambError::NoAttachment.into())
},
MessageAction::Edit => {
if msg.sender != settings.profile.user_id {
let msg = "Cannot edit messages sent by someone else";
let err = UIError::Failure(msg.into());
return Err(err);
}
let ev = match &msg.event {
MessageEvent::Original(ev) => &ev.content,
MessageEvent::Local(ev) => ev.deref(),
_ => {
let msg = "Cannot edit a redacted message";
let err = UIError::Failure(msg.into());
return Err(err);
},
};
let text = match &ev.msgtype {
MessageType::Text(msg) => msg.body.as_str(),
_ => {
let msg = "Cannot edit a non-text message";
let err = UIError::Failure(msg.into());
return Err(err);
},
};
self.tbox.set_text(text);
self.editing = self.scrollback.get_key(info);
Ok(None)
},
MessageAction::Redact(reason) => {
let room = store
.application
@ -273,6 +314,7 @@ impl ChatState {
.get_joined_room(self.id())
.ok_or(IambError::NotJoined)?;
let info = store.application.rooms.entry(self.id().to_owned()).or_default();
let mut show_echo = true;
let (event_id, msg) = match act {
SendAction::Submit => {
@ -287,7 +329,14 @@ impl ChatState {
let mut msg = RoomMessageEventContent::new(msg);
if let Some(m) = self.get_reply_to(info) {
if let Some((_, event_id)) = &self.editing {
msg.relates_to = Some(Relation::Replacement(Replacement::new(
event_id.clone(),
Box::new(msg.clone()),
)));
show_echo = false;
} else if let Some(m) = self.get_reply_to(info) {
// XXX: Switch to RoomMessageEventContent::reply() once it's stable?
msg = msg.make_reply_to(m);
}
@ -327,11 +376,13 @@ impl ChatState {
},
};
if show_echo {
let user = store.application.settings.profile.user_id.clone();
let key = (MessageTimeStamp::LocalEcho, event_id);
let msg = MessageEvent::Local(msg.into());
let msg = Message::new(msg, user, MessageTimeStamp::LocalEcho);
info.messages.insert(key, msg);
}
// Jump to the end of the scrollback to show the message.
self.scrollback.goto_latest();
@ -413,6 +464,7 @@ impl WindowOps<IambInfo> for ChatState {
focus: self.focus,
reply_to: None,
editing: None,
}
}
@ -601,14 +653,20 @@ impl<'a> StatefulWidget for Chat<'a> {
let scrollback = Scrollback::new(self.store).focus(scrollback_focused);
scrollback.render(scrollarea, buf, &mut state.scrollback);
let desc_spans = state.reply_to.as_ref().and_then(|k| {
let desc_spans = match (&state.editing, &state.reply_to) {
(None, None) => None,
(Some(_), _) => Some(Spans::from("Editing message")),
(_, Some(_)) => {
state.reply_to.as_ref().and_then(|k| {
let room = self.store.application.rooms.get(state.id())?;
let msg = room.messages.get(k)?;
let user = self.store.application.settings.get_user_span(msg.sender.as_ref());
let spans = Spans(vec![Span::from("Replying to "), user]);
spans.into()
});
})
},
};
if let Some(desc_spans) = desc_spans {
Paragraph::new(desc_spans).render(descarea, buf);

View file

@ -58,7 +58,7 @@ use modalkit::editing::action::{EditInfo, InfoMessage, UIError};
use crate::{
base::{AsyncProgramStore, IambError, IambResult, SetRoomField, VerifyAction},
message::{Message, MessageFetchResult, MessageTimeStamp},
message::MessageFetchResult,
ApplicationSettings,
};
@ -536,15 +536,7 @@ impl ClientWorker {
let mut locked = store.lock().await;
let mut info = locked.application.get_room_info(room_id.to_owned());
info.name = room_name;
let event_id = ev.event_id().to_owned();
let key = (ev.origin_server_ts().into(), event_id.clone());
let msg = Message::from(ev.into_full_event(room_id.to_owned()));
info.messages.insert(key, msg);
// Remove the echo.
let key = (MessageTimeStamp::LocalEcho, event_id);
let _ = info.messages.remove(&key);
info.insert(ev.into_full_event(room_id.to_owned()));
}
},
);
@ -561,17 +553,15 @@ impl ClientWorker {
let mut locked = store.lock().await;
let info = locked.application.get_room_info(room_id.to_owned());
// XXX: need to store a mapping of EventId -> MessageKey somewhere
// to avoid having to iterate over the messages here.
for ((_, id), msg) in info.messages.iter_mut().rev() {
if id != &ev.redacts {
continue;
}
let key = if let Some(k) = info.keys.get(&ev.redacts) {
k
} else {
return;
};
if let Some(msg) = info.messages.get_mut(key) {
let ev = SyncRoomRedactionEvent::Original(ev);
msg.event.redact(ev, room_version);
break;
}
}
},