From c9c547acc1b46ff378fc2b954767459852608b37 Mon Sep 17 00:00:00 2001 From: Ulyssa Date: Thu, 9 Feb 2023 17:53:33 -0800 Subject: [PATCH] Support sending and displaying message reactions (#2) --- Cargo.lock | 40 +++++++++-- Cargo.toml | 1 + src/base.rs | 125 +++++++++++++++++++++++++++++---- src/commands.rs | 57 +++++++++++++-- src/config.rs | 10 +++ src/message/mod.rs | 40 ++++++++++- src/tests.rs | 17 +++-- src/windows/room/chat.rs | 85 +++++++++++++++++++--- src/windows/room/scrollback.rs | 8 +-- src/worker.rs | 48 ++++++++----- 10 files changed, 368 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a08a4be..7daf8fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -752,6 +752,15 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "emojis" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fe60b864b6544ad211d4053ced474a9b9d2c8d66b77f01d6c6bcfed10c6bf0" +dependencies = [ + "phf 0.11.1", +] + [[package]] name = "encoding_rs" version = "0.8.31" @@ -1165,6 +1174,7 @@ dependencies = [ "clap", "css-color-parser", "dirs", + "emojis", "gethostname", "html5ever", "lazy_static 1.4.0", @@ -1434,7 +1444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" dependencies = [ "log", - "phf", + "phf 0.10.1", "phf_codegen", "string_cache", "string_cache_codegen", @@ -1884,7 +1894,16 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ - "phf_shared", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +dependencies = [ + "phf_shared 0.11.1", ] [[package]] @@ -1894,7 +1913,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" dependencies = [ "phf_generator", - "phf_shared", + "phf_shared 0.10.0", ] [[package]] @@ -1903,7 +1922,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" dependencies = [ - "phf_shared", + "phf_shared 0.10.0", "rand 0.8.5", ] @@ -1916,6 +1935,15 @@ dependencies = [ "siphasher", ] +[[package]] +name = "phf_shared" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.0.12" @@ -2637,7 +2665,7 @@ dependencies = [ "new_debug_unreachable", "once_cell", "parking_lot 0.12.1", - "phf_shared", + "phf_shared 0.10.0", "precomputed-hash", "serde", ] @@ -2649,7 +2677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" dependencies = [ "phf_generator", - "phf_shared", + "phf_shared 0.10.0", "proc-macro2", "quote", ] diff --git a/Cargo.toml b/Cargo.toml index 03a45a2..2fd007d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ chrono = "0.4" clap = {version = "4.0", features = ["derive"]} css-color-parser = "0.1.2" dirs = "4.0.0" +emojis = "~0.5.2" gethostname = "0.4.1" html5ever = "0.26.0" markup5ever_rcdom = "0.2.0" diff --git a/src/base.rs b/src/base.rs index 313728f..8c085d2 100644 --- a/src/base.rs +++ b/src/base.rs @@ -10,14 +10,19 @@ use matrix_sdk::{ encryption::verification::SasVerification, room::Joined, ruma::{ - events::room::message::{ - OriginalRoomMessageEvent, - Relation, - Replacement, - RoomMessageEvent, - RoomMessageEventContent, + events::{ + reaction::ReactionEvent, + room::message::{ + OriginalRoomMessageEvent, + Relation, + Replacement, + RoomMessageEvent, + RoomMessageEventContent, + }, + tag::{TagName, Tags}, + AnyMessageLikeEvent, + MessageLikeEvent, }, - events::tag::{TagName, Tags}, EventId, OwnedEventId, OwnedRoomId, @@ -88,11 +93,20 @@ pub enum MessageAction { /// Edit a sent message. Edit, - /// Redact a message. + /// React to a message with an Emoji. + React(String), + + /// Redact a message, with an optional reason. Redact(Option), /// Reply to a message. Reply, + + /// Unreact to a message. + /// + /// If no specific Emoji to remove to is specified, then all reactions from the user on the + /// message are removed. + Unreact(Option), } bitflags::bitflags! { @@ -226,6 +240,12 @@ pub type AsyncProgramStore = Arc>; pub type IambResult = UIResult; +/// Reaction events for some message. +/// +/// The event identifier used as a key here is the ID for the reaction, and not for the message +/// it's reacting to. +pub type MessageReactions = HashMap; + pub type Receipts = HashMap>; #[derive(thiserror::Error, Debug)] @@ -292,32 +312,103 @@ pub enum RoomFetchStatus { NotStarted, } +pub enum EventLocation { + Message(MessageKey), + Reaction(OwnedEventId), +} + +impl EventLocation { + fn to_message_key(&self) -> Option<&MessageKey> { + if let EventLocation::Message(key) = self { + Some(key) + } else { + None + } + } +} + #[derive(Default)] pub struct RoomInfo { + /// The display name for this room. pub name: Option, + + /// The tags placed on this room. pub tags: Option, - pub keys: HashMap, + /// A map of event IDs to where they are stored in this struct. + pub keys: HashMap, + + /// The messages loaded for this room. pub messages: Messages, + /// A map of read markers to display on different events. pub receipts: HashMap>, + + /// An event ID for where we should indicate we've read up to. pub read_till: Option, + /// A map of message identifiers to a map of reaction events. + pub reactions: HashMap, + + /// Where to continue fetching from when we continue loading scrollback history. pub fetch_id: RoomFetchStatus, + + /// The time that we last fetched scrollback for this room. pub fetch_last: Option, + + /// Users currently typing in this room, and when we received notification of them doing so. pub users_typing: Option<(Instant, Vec)>, } impl RoomInfo { + pub fn get_reactions(&self, event_id: &EventId) -> Vec<(&str, usize)> { + if let Some(reacts) = self.reactions.get(event_id) { + let mut counts = HashMap::new(); + + for (key, _) in reacts.values() { + let count = counts.entry(key.as_str()).or_default(); + *count += 1; + } + + let mut reactions = counts.into_iter().collect::>(); + reactions.sort(); + + reactions + } else { + vec![] + } + } + pub fn get_event(&self, event_id: &EventId) -> Option<&Message> { - self.messages.get(self.keys.get(event_id)?) + self.messages.get(self.keys.get(event_id)?.to_message_key()?) + } + + pub fn insert_reaction(&mut self, react: ReactionEvent) { + match react { + MessageLikeEvent::Original(react) => { + let rel_id = react.content.relates_to.event_id; + let key = react.content.relates_to.key; + + let message = self.reactions.entry(rel_id.clone()).or_default(); + let event_id = react.event_id; + let user_id = react.sender; + + message.insert(event_id.clone(), (key, user_id)); + + let loc = EventLocation::Reaction(rel_id); + self.keys.insert(event_id, loc); + }, + MessageLikeEvent::Redacted(_) => { + return; + }, + } } 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) { + let key = if let Some(EventLocation::Message(k)) = self.keys.get(&event_id) { k } else { return; @@ -346,7 +437,7 @@ impl RoomInfo { 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.keys.insert(event_id.clone(), EventLocation::Message(key.clone())); self.messages.insert(key, msg.into()); // Remove any echo. @@ -519,7 +610,15 @@ impl ChatStore { match res { Ok((fetch_id, msgs)) => { for msg in msgs.into_iter() { - info.insert(msg); + match msg { + AnyMessageLikeEvent::RoomMessage(msg) => { + info.insert(msg); + }, + AnyMessageLikeEvent::Reaction(ev) => { + info.insert_reaction(ev); + }, + _ => continue, + } } info.fetch_id = diff --git a/src/commands.rs b/src/commands.rs index 62bd5b1..b3c061a 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -163,8 +163,8 @@ fn iamb_cancel(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { return Result::Err(CommandError::InvalidArgument); } - let ract = IambAction::from(MessageAction::Cancel); - let step = CommandStep::Continue(ract.into(), ctx.context.take()); + let mact = IambAction::from(MessageAction::Cancel); + let step = CommandStep::Continue(mact.into(), ctx.context.take()); return Ok(step); } @@ -174,8 +174,55 @@ fn iamb_edit(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { return Result::Err(CommandError::InvalidArgument); } - let ract = IambAction::from(MessageAction::Edit); - let step = CommandStep::Continue(ract.into(), ctx.context.take()); + let mact = IambAction::from(MessageAction::Edit); + let step = CommandStep::Continue(mact.into(), ctx.context.take()); + + return Ok(step); +} + +fn iamb_react(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + let args = desc.arg.strings()?; + + if args.len() != 1 { + return Result::Err(CommandError::InvalidArgument); + } + + let k = args[0].as_str(); + + if let Some(emoji) = emojis::get(k).or_else(|| emojis::get_by_shortcode(k)) { + let mact = IambAction::from(MessageAction::React(emoji.to_string())); + let step = CommandStep::Continue(mact.into(), ctx.context.take()); + + return Ok(step); + } else { + let msg = format!("Invalid Emoji or shortcode: {k}"); + + return Result::Err(CommandError::Error(msg)); + } +} + +fn iamb_unreact(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + let mut args = desc.arg.strings()?; + + if args.len() > 1 { + return Result::Err(CommandError::InvalidArgument); + } + + let mact = if let Some(k) = args.pop() { + let k = k.as_str(); + + if let Some(emoji) = emojis::get(k).or_else(|| emojis::get_by_shortcode(k)) { + IambAction::from(MessageAction::Unreact(Some(emoji.to_string()))) + } else { + let msg = format!("Invalid Emoji or shortcode: {k}"); + + return Result::Err(CommandError::Error(msg)); + } + } else { + IambAction::from(MessageAction::Unreact(None)) + }; + + let step = CommandStep::Continue(mact.into(), ctx.context.take()); return Ok(step); } @@ -356,11 +403,13 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) { 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 }); + cmds.add_command(ProgramCommand { names: vec!["react".into()], f: iamb_react }); cmds.add_command(ProgramCommand { names: vec!["redact".into()], f: iamb_redact }); cmds.add_command(ProgramCommand { names: vec!["reply".into()], f: iamb_reply }); cmds.add_command(ProgramCommand { names: vec!["rooms".into()], f: iamb_rooms }); cmds.add_command(ProgramCommand { names: vec!["room".into()], f: iamb_room }); cmds.add_command(ProgramCommand { names: vec!["spaces".into()], f: iamb_spaces }); + cmds.add_command(ProgramCommand { names: vec!["unreact".into()], f: iamb_unreact }); cmds.add_command(ProgramCommand { names: vec!["upload".into()], f: iamb_upload }); cmds.add_command(ProgramCommand { names: vec!["verify".into()], f: iamb_verify }); cmds.add_command(ProgramCommand { names: vec!["welcome".into()], f: iamb_welcome }); diff --git a/src/config.rs b/src/config.rs index cfb1ba2..8d69e6a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -178,6 +178,8 @@ fn merge_users(a: Option, b: Option) -> Option, + pub reaction_shortcode_display: Option, pub read_receipt_send: Option, pub read_receipt_display: Option, pub typing_notice_send: Option, @@ -199,6 +203,10 @@ pub struct Tunables { impl Tunables { fn merge(self, other: Self) -> Self { Tunables { + reaction_display: self.reaction_display.or(other.reaction_display), + reaction_shortcode_display: self + .reaction_shortcode_display + .or(other.reaction_shortcode_display), read_receipt_send: self.read_receipt_send.or(other.read_receipt_send), read_receipt_display: self.read_receipt_display.or(other.read_receipt_display), typing_notice_send: self.typing_notice_send.or(other.typing_notice_send), @@ -210,6 +218,8 @@ impl Tunables { fn values(self) -> TunableValues { TunableValues { + reaction_display: self.reaction_display.unwrap_or(true), + reaction_shortcode_display: self.reaction_shortcode_display.unwrap_or(false), read_receipt_send: self.read_receipt_send.unwrap_or(true), read_receipt_display: self.read_receipt_display.unwrap_or(true), typing_notice_send: self.typing_notice_send.unwrap_or(true), diff --git a/src/message/mod.rs b/src/message/mod.rs index 52f34d7..04b8d16 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -24,6 +24,7 @@ use matrix_sdk::ruma::{ }, redaction::SyncRoomRedactionEvent, }, + AnyMessageLikeEvent, Redact, }, EventId, @@ -52,7 +53,7 @@ use crate::{ mod html; mod printer; -pub type MessageFetchResult = IambResult<(Option, Vec)>; +pub type MessageFetchResult = IambResult<(Option, Vec)>; pub type MessageKey = (MessageTimeStamp, OwnedEventId); pub type Messages = BTreeMap; @@ -682,6 +683,43 @@ impl Message { fmt.push_spans(space_span(width, style).into(), style, &mut text); } + if settings.tunables.reaction_display { + let mut emojis = printer::TextPrinter::new(width, style, false); + let mut reactions = 0; + + for (key, count) in info.get_reactions(self.event.event_id()).into_iter() { + if reactions != 0 { + emojis.push_str(" ", style); + } + + let name = if settings.tunables.reaction_shortcode_display { + if let Some(emoji) = emojis::get(key) { + if let Some(short) = emoji.shortcode() { + short + } else { + // No ASCII shortcode name to show. + continue; + } + } else if key.chars().all(|c| c.is_ascii_alphanumeric()) { + key + } else { + // Not an Emoji or a printable ASCII string. + continue; + } + } else { + key + }; + + emojis.push_str(format!("[{name} {count}]"), style); + + reactions += 1; + } + + if reactions > 0 { + fmt.push_text(emojis.finish(), style, &mut text); + } + } + return text; } diff --git a/src/tests.rs b/src/tests.rs index 73243dd..8fd44c8 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -20,7 +20,7 @@ use tokio::sync::mpsc::unbounded_channel; use url::Url; use crate::{ - base::{ChatStore, ProgramStore, RoomFetchStatus, RoomInfo}, + base::{ChatStore, EventLocation, ProgramStore, RoomFetchStatus, RoomInfo}, config::{ user_color, user_style_from_color, @@ -117,14 +117,14 @@ pub fn mock_message5() -> Message { mock_room1_message(content, TEST_USER2.clone(), MSG4_KEY.clone()) } -pub fn mock_keys() -> HashMap { +pub fn mock_keys() -> HashMap { 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.insert(MSG1_EVID.clone(), EventLocation::Message(MSG1_KEY.clone())); + keys.insert(MSG2_EVID.clone(), EventLocation::Message(MSG2_KEY.clone())); + keys.insert(MSG3_EVID.clone(), EventLocation::Message(MSG3_KEY.clone())); + keys.insert(MSG4_EVID.clone(), EventLocation::Message(MSG4_KEY.clone())); + keys.insert(MSG5_EVID.clone(), EventLocation::Message(MSG5_KEY.clone())); keys } @@ -151,6 +151,7 @@ pub fn mock_room() -> RoomInfo { receipts: HashMap::new(), read_till: None, + reactions: HashMap::new(), fetch_id: RoomFetchStatus::NotStarted, fetch_last: None, @@ -169,6 +170,8 @@ pub fn mock_dirs() -> DirectoryValues { pub fn mock_tunables() -> TunableValues { TunableValues { default_room: None, + reaction_display: true, + reaction_shortcode_display: false, read_receipt_send: true, read_receipt_display: true, typing_notice_send: true, diff --git a/src/windows/room/chat.rs b/src/windows/room/chat.rs index 69c0354..b8a34c0 100644 --- a/src/windows/room/chat.rs +++ b/src/windows/room/chat.rs @@ -9,8 +9,9 @@ use tokio; use matrix_sdk::{ attachment::AttachmentConfig, media::{MediaFormat, MediaRequest}, - room::Room as MatrixRoom, + room::{Joined, Room as MatrixRoom}, ruma::{ + events::reaction::{ReactionEventContent, Relation as Reaction}, events::room::message::{ MessageType, OriginalRoomMessageEvent, @@ -19,6 +20,7 @@ use matrix_sdk::{ RoomMessageEventContent, TextMessageEventContent, }, + EventId, OwnedRoomId, RoomId, }, @@ -73,6 +75,7 @@ use crate::base::{ }; use crate::message::{Message, MessageEvent, MessageKey, MessageTimeStamp}; +use crate::worker::Requester; use super::scrollback::{Scrollback, ScrollbackState}; @@ -115,6 +118,10 @@ impl ChatState { } } + fn get_joined(&self, worker: &Requester) -> Result { + worker.client.get_joined_room(self.id()).ok_or(IambError::NotJoined) + } + fn get_reply_to<'a>(&self, info: &'a RoomInfo) -> Option<&'a OriginalRoomMessageEvent> { let key = self.reply_to.as_ref()?; let msg = info.messages.get(key)?; @@ -149,7 +156,10 @@ impl ChatState { let settings = &store.application.settings; let info = store.application.rooms.entry(self.room_id.clone()).or_default(); - let msg = self.scrollback.get_mut(info).ok_or(IambError::NoSelectedMessage)?; + let msg = self + .scrollback + .get_mut(&mut info.messages) + .ok_or(IambError::NoSelectedMessage)?; match act { MessageAction::Cancel => { @@ -282,19 +292,32 @@ impl ChatState { Ok(None) }, - MessageAction::Redact(reason) => { - let room = store - .application - .worker - .client - .get_joined_room(self.id()) - .ok_or(IambError::NotJoined)?; - + MessageAction::React(emoji) => { + let room = self.get_joined(&store.application.worker)?; let event_id = match &msg.event { MessageEvent::Original(ev) => ev.event_id.clone(), MessageEvent::Local(event_id, _) => event_id.clone(), MessageEvent::Redacted(_) => { - let msg = ""; + let msg = "Cannot react to a redacted message"; + let err = UIError::Failure(msg.into()); + + return Err(err); + }, + }; + + let reaction = Reaction::new(event_id, emoji); + let msg = ReactionEventContent::new(reaction); + let _ = room.send(msg, None).await.map_err(IambError::from)?; + + Ok(None) + }, + MessageAction::Redact(reason) => { + let room = self.get_joined(&store.application.worker)?; + let event_id = match &msg.event { + MessageEvent::Original(ev) => ev.event_id.clone(), + MessageEvent::Local(event_id, _) => event_id.clone(), + MessageEvent::Redacted(_) => { + let msg = "Cannot redact already redacted message"; let err = UIError::Failure(msg.into()); return Err(err); @@ -311,6 +334,46 @@ impl ChatState { self.reply_to = self.scrollback.get_key(info); self.focus = RoomFocus::MessageBar; + Ok(None) + }, + MessageAction::Unreact(emoji) => { + let room = self.get_joined(&store.application.worker)?; + let event_id: &EventId = match &msg.event { + MessageEvent::Original(ev) => ev.event_id.as_ref(), + MessageEvent::Local(event_id, _) => event_id.as_ref(), + MessageEvent::Redacted(_) => { + let msg = "Cannot unreact to a redacted message"; + let err = UIError::Failure(msg.into()); + + return Err(err); + }, + }; + + let reactions = match info.reactions.get(event_id) { + Some(r) => r, + None => return Ok(None), + }; + + let reactions = reactions.iter().filter_map(|(event_id, (reaction, user_id))| { + if user_id != &settings.profile.user_id { + return None; + } + + if let Some(emoji) = &emoji { + if emoji == reaction { + return Some(event_id); + } else { + return None; + } + } else { + return Some(event_id); + } + }); + + for reaction in reactions { + let _ = room.redact(reaction, None, None).await.map_err(IambError::from)?; + } + Ok(None) }, } diff --git a/src/windows/room/scrollback.rs b/src/windows/room/scrollback.rs index 3734cb8..5cbf19f 100644 --- a/src/windows/room/scrollback.rs +++ b/src/windows/room/scrollback.rs @@ -62,7 +62,7 @@ use modalkit::editing::{ use crate::{ base::{IambBufferId, IambInfo, ProgramContext, ProgramStore, RoomFocus, RoomInfo}, config::ApplicationSettings, - message::{Message, MessageCursor, MessageKey}, + message::{Message, MessageCursor, MessageKey, Messages}, }; fn nth_key_before(pos: MessageKey, n: usize, info: &RoomInfo) -> MessageKey { @@ -164,11 +164,11 @@ impl ScrollbackState { .or_else(|| info.messages.last_key_value().map(|kv| kv.0.clone())) } - pub fn get_mut<'a>(&mut self, info: &'a mut RoomInfo) -> Option<&'a mut Message> { + pub fn get_mut<'a>(&mut self, messages: &'a mut Messages) -> Option<&'a mut Message> { if let Some(k) = &self.cursor.timestamp { - info.messages.get_mut(k) + messages.get_mut(k) } else { - info.messages.last_entry().map(|o| o.into_mut()) + messages.last_entry().map(|o| o.into_mut()) } } diff --git a/src/worker.rs b/src/worker.rs index 15a2cf9..27988be 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -32,6 +32,7 @@ use matrix_sdk::{ start::{OriginalSyncKeyVerificationStartEvent, ToDeviceKeyVerificationStartEvent}, VerificationMethod, }, + reaction::ReactionEventContent, room::{ message::{MessageType, RoomMessageEventContent}, name::RoomNameEventContent, @@ -39,7 +40,6 @@ use matrix_sdk::{ }, tag::Tags, typing::SyncTypingEvent, - AnyMessageLikeEvent, AnyTimelineEvent, SyncMessageLikeEvent, SyncStateEvent, @@ -57,7 +57,7 @@ use matrix_sdk::{ use modalkit::editing::action::{EditInfo, InfoMessage, UIError}; use crate::{ - base::{AsyncProgramStore, IambError, IambResult, Receipts, VerifyAction}, + base::{AsyncProgramStore, EventLocation, IambError, IambResult, Receipts, VerifyAction}, message::MessageFetchResult, ApplicationSettings, }; @@ -552,6 +552,20 @@ impl ClientWorker { }, ); + let _ = self.client.add_event_handler( + |ev: SyncMessageLikeEvent, + room: MatrixRoom, + store: Ctx| { + async move { + let room_id = room.room_id(); + + let mut locked = store.lock().await; + let info = locked.application.get_room_info(room_id.to_owned()); + info.insert_reaction(ev.into_full_event(room_id.to_owned())); + } + }, + ); + let _ = self.client.add_event_handler( |ev: OriginalSyncRoomRedactionEvent, room: MatrixRoom, @@ -564,15 +578,21 @@ impl ClientWorker { let mut locked = store.lock().await; let info = locked.application.get_room_info(room_id.to_owned()); - let key = if let Some(k) = info.keys.get(&ev.redacts) { - k - } else { - return; - }; + match info.keys.get(&ev.redacts) { + None => return, + Some(EventLocation::Message(key)) => { + if let Some(msg) = info.messages.get_mut(key) { + let ev = SyncRoomRedactionEvent::Original(ev); + msg.event.redact(ev, room_version); + } + }, + Some(EventLocation::Reaction(event_id)) => { + if let Some(reactions) = info.reactions.get_mut(event_id) { + reactions.remove(&ev.redacts); + } - if let Some(msg) = info.messages.get_mut(key) { - let ev = SyncRoomRedactionEvent::Original(ev); - msg.event.redact(ev, room_version); + info.keys.remove(&ev.redacts); + }, } } }, @@ -891,13 +911,7 @@ impl ClientWorker { let msgs = chunk.into_iter().filter_map(|ev| { match ev.event.deserialize() { - Ok(AnyTimelineEvent::MessageLike(msg)) => { - if let AnyMessageLikeEvent::RoomMessage(msg) = msg { - Some(msg) - } else { - None - } - }, + Ok(AnyTimelineEvent::MessageLike(msg)) => Some(msg), Ok(AnyTimelineEvent::State(_)) => None, Err(_) => None, }