Commit 00eb9bff2d7b
tarball14 files changed+284-33
@@ -1794,7 +1794,7 @@ impl StateStoreIntegrationTests for DynStateStore {
17941794
self.update_dependent_queued_request(17951795
room_id,17961796
&child_txn,1797
- DependentQueuedRequestKind::ReactEvent { key: "👍".to_owned() },1797
+ DependentQueuedRequestKind::ReactEvent { key: "👍".to_owned(), shortcode: None },17981798
)17991799
.await?;18001800
@@ -1806,7 +1806,7 @@ impl StateStoreIntegrationTests for DynStateStore {
18061806
assert!(dependents[0].parent_key.is_none());18071807
assert_matches!(18081808
&dependents[0].kind,1809
- DependentQueuedRequestKind::ReactEvent { key } => {1809
+ DependentQueuedRequestKind::ReactEvent { key, .. } => {18101810
assert_eq!(key, "👍");18111811
}18121812
);@@ -237,6 +237,12 @@ pub enum DependentQueuedRequestKind {
237237
ReactEvent {238238
/// Key used for the reaction.239239
key: String,240
+241
+ /// Shortcode of the custom emote used as the key, as per [MSC4027].242
+ ///243
+ /// [MSC4027]: https://github.com/matrix-org/matrix-spec-proposals/pull/4027244
+ #[serde(default, skip_serializing_if = "Option::is_none")]245
+ shortcode: Option<String>,240246
},241247
242248
/// Upload a file or thumbnail depending on another file or thumbnail@@ -116,6 +116,11 @@ pub(crate) enum AggregationKind {
116116
Reaction {117117
/// The reaction "key" displayed by the client, often an emoji.118118
key: String,119
+ /// The shortcode of the custom emote used as the key, as per120
+ /// [MSC4027].121
+ ///122
+ /// [MSC4027]: https://github.com/matrix-org/matrix-spec-proposals/pull/4027123
+ shortcode: Option<String>,119124
/// Sender of the reaction.120125
sender: OwnedUserId,121126
/// Timestamp at which the reaction has been sent.@@ -286,7 +291,7 @@ impl Aggregation {
286291
Err(err) => ApplyAggregationResult::Error(err),287292
},288293
289
- AggregationKind::Reaction { key, sender, timestamp, reaction_status } => {294
+ AggregationKind::Reaction { key, shortcode, sender, timestamp, reaction_status } => {290295
let Some(reactions) = event.content().reactions() else {291296
// An item that can't hold any reactions.292297
return ApplyAggregationResult::LeftItemIntact;@@ -325,7 +330,11 @@ impl Aggregation {
325330
326331
reactions.entry(key.clone()).or_default().insert(327332
sender.clone(),328
- ReactionInfo { timestamp: *timestamp, status: reaction_status.clone() },333
+ ReactionInfo {334
+ timestamp: *timestamp,335
+ status: reaction_status.clone(),336
+ shortcode: shortcode.clone(),337
+ },329338
);330339
331340
ApplyAggregationResult::UpdatedItem@@ -30,7 +30,8 @@ use matrix_sdk::{
3030
TimelineVectorDiffs,3131
},3232
send_queue::{33
- LocalEcho, LocalEchoContent, RoomSendQueueUpdate, SendHandle, SendReactionHandle,33
+ LocalEcho, LocalEchoContent, RoomSendQueueStorageError, RoomSendQueueUpdate, SendHandle,34
+ SendReactionHandle,3435
},3536
task_monitor::BackgroundTaskHandle,3637
};@@ -71,6 +72,7 @@ use super::{
7172
TimelineFocus, TimelineItem, TimelineItemContent, TimelineItemKind,7273
TimelineReadReceiptTracking, VirtualTimelineItem,7374
algorithms::{rfind_event_by_id, rfind_event_item},75
+ event_handler::{REACTION_SHORTCODE_FIELD, reaction_shortcode},7476
event_item::{ReactionStatus, RemoteEventOrigin},7577
item::TimelineUniqueId,7678
subscriber::TimelineSubscriber,@@ -492,6 +494,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
492494
&self,493495
item_id: &TimelineEventItemId,494496
key: &str,497
+ shortcode: Option<&str>,495498
) -> Result<bool, Error> {496499
let mut state = self.state.write().await;497500
@@ -511,7 +514,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
511514
match item.handle() {512515
TimelineItemHandle::Local(send_handle) => {513516
if send_handle514
- .react(key.to_owned())517
+ .react_with_shortcode(key.to_owned(), shortcode.map(ToOwned::to_owned))515518
.await516519
.map_err(|err| Error::SendQueueError(err.into()))?517520
.is_some()@@ -530,9 +533,27 @@ impl<P: RoomDataProvider> TimelineController<P> {
530533
// take care of it.531534
trace!("adding a reaction to a remote echo");532535
let annotation = Annotation::new(event_id.to_owned(), key.to_owned());533
- self.room_data_provider534
- .send(ReactionEventContent::from(annotation).into())535
- .await?;536
+ let content = ReactionEventContent::from(annotation);537
+ if let Some(shortcode) = shortcode {538
+ let content = serde_json::to_value(&content)539
+ .and_then(|mut content| {540
+ content[REACTION_SHORTCODE_FIELD] = shortcode.into();541
+ Raw::new(&content)542
+ })543
+ .map_err(|err| {544
+ Error::SendQueueError(545
+ RoomSendQueueStorageError::JsonSerialization(err).into(),546
+ )547
+ })?;548
+ self.room_data_provider549
+ .send_raw(550
+ content.cast_unchecked(),551
+ MessageLikeEventType::Reaction.to_string(),552
+ )553
+ .await?;554
+ } else {555
+ self.room_data_provider.send(content.into()).await?;556
+ }536557
return Ok(true);537558
}538559
}@@ -746,6 +767,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
746767
&self,747768
txn_id: OwnedTransactionId,748769
content: AnyMessageLikeEventContent,770
+ reaction_shortcode: Option<String>,749771
send_handle: Option<SendHandle>,750772
) {751773
let sender = self.room_data_provider.own_user_id().to_owned();@@ -755,7 +777,15 @@ impl<P: RoomDataProvider> TimelineController<P> {
755777
756778
let mut state = self.state.write().await;757779
state758
- .handle_local_event(sender, profile, date_divider_mode, txn_id, send_handle, content)780
+ .handle_local_event(781
+ sender,782
+ profile,783
+ date_divider_mode,784
+ txn_id,785
+ send_handle,786
+ content,787
+ reaction_shortcode,788
+ )759789
.await;760790
}761791
@@ -1130,8 +1160,17 @@ impl<P: RoomDataProvider> TimelineController<P> {
11301160
}11311161
};11321162
1133
- self.handle_local_event(echo.transaction_id.clone(), content, Some(send_handle))1134
- .await;1163
+ let shortcode = matches!(content, AnyMessageLikeEventContent::Reaction(_))1164
+ .then(|| reaction_shortcode(serialized_event.raw().0))1165
+ .flatten();1166
+1167
+ self.handle_local_event(1168
+ echo.transaction_id.clone(),1169
+ content,1170
+ shortcode,1171
+ Some(send_handle),1172
+ )1173
+ .await;11351174
11361175
if let Some(send_error) = send_error {11371176
self.update_event_send_state(@@ -1147,8 +1186,8 @@ impl<P: RoomDataProvider> TimelineController<P> {
11471186
}11481187
}11491188
1150
- LocalEchoContent::React { key, send_handle, applies_to } => {1151
- self.handle_local_reaction(key, send_handle, applies_to).await;1189
+ LocalEchoContent::React { key, shortcode, send_handle, applies_to } => {1190
+ self.handle_local_reaction(key, shortcode, send_handle, applies_to).await;11521191
}11531192
11541193
LocalEchoContent::Redaction { redacts, send_error, .. } => {@@ -1175,6 +1214,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
11751214
async fn handle_local_reaction(11761215
&self,11771216
reaction_key: String,1217
+ shortcode: Option<String>,11781218
send_handle: SendReactionHandle,11791219
applies_to: OwnedTransactionId,11801220
) {@@ -1189,6 +1229,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
11891229
TimelineEventItemId::TransactionId(reaction_txn_id),11901230
AggregationKind::Reaction {11911231
key: reaction_key.clone(),1232
+ shortcode,11921233
sender: self.room_data_provider.own_user_id().to_owned(),11931234
timestamp: MilliSecondsSinceUnixEpoch::now(),11941235
reaction_status,@@ -157,6 +157,7 @@ impl<P: RoomDataProvider> TimelineState<P> {
157157
txn_id: OwnedTransactionId,158158
send_handle: Option<SendHandle>,159159
content: AnyMessageLikeEventContent,160
+ reaction_shortcode: Option<String>,160161
) {161162
let mut txn = self.transaction();162163
@@ -194,7 +195,8 @@ impl<P: RoomDataProvider> TimelineState<P> {
194195
should_add_new_items,195196
};196197
197
- let timeline_action = TimelineAction::from_content(content, in_reply_to, thread_root, None);198
+ let timeline_action = TimelineAction::from_content(content, in_reply_to, thread_root, None)199
+ .with_reaction_shortcode(|| reaction_shortcode);198200
TimelineEventHandler::new(&mut txn, ctx)199201
.handle_event(&mut date_divider_adjuster, timeline_action, None)200202
.await;@@ -65,6 +65,19 @@ use crate::{
6565
unable_to_decrypt_hook::UtdHookManager,6666
};6767
68
+/// The field holding the shortcode of a custom emote reaction, as per69
+/// [MSC4027], which ruma's `ReactionEventContent` has no field for.70
+///71
+/// [MSC4027]: https://github.com/matrix-org/matrix-spec-proposals/pull/402772
+pub(super) const REACTION_SHORTCODE_FIELD: &str = "com.beeper.reaction.shortcode";73
+74
+/// Reads the [MSC4027] shortcode from a reaction's raw content.75
+///76
+/// [MSC4027]: https://github.com/matrix-org/matrix-spec-proposals/pull/402777
+pub(super) fn reaction_shortcode(content: &Raw<AnyMessageLikeEventContent>) -> Option<String> {78
+ content.get_field(REACTION_SHORTCODE_FIELD).ok().flatten()79
+}80
+6881
/// When adding an event, useful information related to the source of the event.6982
pub(super) enum Flow {7083
/// The event was locally created.@@ -140,7 +153,7 @@ pub(super) struct TimelineEventContext {
140153
#[derive(Clone, Debug)]141154
pub(super) enum HandleAggregationKind {142155
/// Adding a reaction to the related event.143
- Reaction { key: String },156
+ Reaction { key: String, shortcode: Option<String> },144157
145158
/// Redacting (removing) the related event.146159
Redaction,@@ -221,6 +234,22 @@ impl TimelineAction {
221234
Self::AddItem { content }222235
}223236
237
+ /// If this is a reaction, attach the shortcode computed by `shortcode`,238
+ /// that [`TimelineAction::from_content`] can't see in typed content.239
+ pub(super) fn with_reaction_shortcode(240
+ mut self,241
+ shortcode: impl FnOnce() -> Option<String>,242
+ ) -> Self {243
+ if let Self::HandleAggregation {244
+ kind: HandleAggregationKind::Reaction { shortcode: slot, .. },245
+ ..246
+ } = &mut self247
+ {248
+ *slot = shortcode();249
+ }250
+ self251
+ }252
+224253
/// Create a new [`TimelineAction`] from a given remote event.225254
///226255
/// The return value may be `None` if the event was a redacted reaction.@@ -298,6 +327,13 @@ impl TimelineAction {
298327
299328
Some(content) => {300329
Self::from_content(content, in_reply_to, thread_root, thread_summary)330
+ .with_reaction_shortcode(|| {331
+ raw_event332
+ .get_field::<Raw<AnyMessageLikeEventContent>>("content")333
+ .ok()334
+ .flatten()335
+ .and_then(|content| reaction_shortcode(&content))336
+ })301337
}302338
303339
None => Self::add_item(redacted_message_or_none(ev.event_type())?),@@ -382,7 +418,10 @@ impl TimelineAction {
382418
// This is a reaction to a message.383419
Self::HandleAggregation {384420
related_event: c.relates_to.event_id.clone(),385
- kind: HandleAggregationKind::Reaction { key: c.relates_to.key },421
+ kind: HandleAggregationKind::Reaction {422
+ key: c.relates_to.key,423
+ shortcode: None,424
+ },386425
}387426
}388427
@@ -626,8 +665,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
626665
}627666
628667
TimelineAction::HandleAggregation { related_event, kind } => match kind {629
- HandleAggregationKind::Reaction { key } => {630
- self.handle_reaction(related_event, key);668
+ HandleAggregationKind::Reaction { key, shortcode } => {669
+ self.handle_reaction(related_event, key, shortcode);631670
}632671
HandleAggregationKind::Redaction => {633672
self.handle_redaction(related_event);@@ -715,7 +754,12 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
715754
/// Reactions to local events are applied in716755
/// [`crate::timeline::TimelineController::handle_local_echo`].717756
#[instrument(skip(self))]718
- fn handle_reaction(&mut self, relates_to: OwnedEventId, reaction_key: String) {757
+ fn handle_reaction(758
+ &mut self,759
+ relates_to: OwnedEventId,760
+ reaction_key: String,761
+ shortcode: Option<String>,762
+ ) {719763
let target = TimelineEventItemId::EventId(relates_to);720764
721765
// Add the aggregation to the manager.@@ -734,6 +778,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
734778
self.ctx.flow.timeline_item_id(),735779
AggregationKind::Reaction {736780
key: reaction_key,781
+ shortcode,737782
sender: self.ctx.sender.clone(),738783
timestamp: self.ctx.timestamp,739784
reaction_status,@@ -799,6 +799,10 @@ pub struct ReactionInfo {
799799
pub timestamp: MilliSecondsSinceUnixEpoch,800800
/// Current status of this reaction.801801
pub status: ReactionStatus,802
+ /// The shortcode of the custom emote used as the key, as per [MSC4027].803
+ ///804
+ /// [MSC4027]: https://github.com/matrix-org/matrix-spec-proposals/pull/4027805
+ pub shortcode: Option<String>,802806
}803807
804808
/// Reactions grouped by key first, then by sender.@@ -556,7 +556,25 @@ impl Timeline {
556556
item_id: &TimelineEventItemId,557557
reaction_key: &str,558558
) -> Result<bool, Error> {559
- self.controller.toggle_reaction_local(item_id, reaction_key).await559
+ self.toggle_reaction_with_shortcode(item_id, reaction_key, None).await560
+ }561
+562
+ /// Toggle a reaction on an event, like [`Self::toggle_reaction`].563
+ ///564
+ /// For a custom emote reaction, `reaction_key` is the emote's MXC URI and565
+ /// `shortcode` its shortcode, sent along as per [MSC4027]. The shortcode566
+ /// is ignored when the reaction is removed.567
+ ///568
+ /// Returns `true` if the reaction was added, `false` if it was removed.569
+ ///570
+ /// [MSC4027]: https://github.com/matrix-org/matrix-spec-proposals/pull/4027571
+ pub async fn toggle_reaction_with_shortcode(572
+ &self,573
+ item_id: &TimelineEventItemId,574
+ reaction_key: &str,575
+ shortcode: Option<&str>,576
+ ) -> Result<bool, Error> {577
+ self.controller.toggle_reaction_local(item_id, reaction_key, shortcode).await560578
}561579
562580
/// Sends an attachment to the room.@@ -169,7 +169,7 @@ impl TestTimeline {
169169
170170
async fn handle_local_event(&self, content: AnyMessageLikeEventContent) -> OwnedTransactionId {171171
let txn_id = TransactionId::new();172
- self.controller.handle_local_event(txn_id.clone(), content, None).await;172
+ self.controller.handle_local_event(txn_id.clone(), content, None, None).await;173173
txn_id174174
}175175
@@ -206,7 +206,7 @@ impl TestTimeline {
206206
item_id: &TimelineEventItemId,207207
key: &str,208208
) -> Result<(), super::Error> {209
- if self.controller.toggle_reaction_local(item_id, key).await? {209
+ if self.controller.toggle_reaction_local(item_id, key, None).await? {210210
let items = self.controller.items().await;211211
if let Some(event_id) = rfind_event_by_item_id(&items, item_id)212212
.and_then(|(_pos, item)| item.event_id().map(ToOwned::to_owned))@@ -255,6 +255,9 @@ struct TestRoomDataProvider {
255255
/// Events sent with that room data provider.256256
pub sent_events: Arc<RwLock<Vec<AnyMessageLikeEventContent>>>,257257
258
+ /// Raw events sent with that room data provider.259
+ pub sent_raw_events: Arc<RwLock<Vec<Raw<AnyMessageLikeEventContent>>>>,260
+258261
/// Events redacted with that room data providier.259262
pub redacted: Arc<RwLock<Vec<OwnedEventId>>>,260263
}@@ -368,6 +371,15 @@ impl RoomDataProvider for TestRoomDataProvider {
368371
Ok(())369372
}370373
374
+ async fn send_raw(375
+ &self,376
+ content: Raw<AnyMessageLikeEventContent>,377
+ _event_type: String,378
+ ) -> Result<(), super::Error> {379
+ self.sent_raw_events.write().await.push(content);380
+ Ok(())381
+ }382
+371383
async fn redact<'a>(372384
&'a self,373385
event_id: &'a EventId,@@ -19,11 +19,13 @@ use eyeball_im::VectorDiff;
1919
use futures_core::Stream;2020
use futures_util::{FutureExt as _, StreamExt as _};2121
use imbl::vector;22
-use matrix_sdk::assert_next_matches_with_timeout;22
+use matrix_sdk::{assert_next_matches_with_timeout, deserialized_responses::TimelineEvent};2323
use matrix_sdk_test::{ALICE, BOB, async_test};2424
use ruma::{2525
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, event_id,26
- events::AnyMessageLikeEventContent, server_name, uint,26
+ events::{AnyMessageLikeEventContent, reaction::ReactionEventContent},27
+ serde::Raw,28
+ server_name, uint,2729
};2830
use stream_assert::{assert_next_matches, assert_pending};2931
use tokio::time::timeout;@@ -125,6 +127,55 @@ async fn test_add_reaction_success() {
125127
}126128
127129
#[async_test]130
+async fn test_add_reaction_with_shortcode_sends_it() {131
+ let timeline = TestTimeline::new();132
+ let mut stream = timeline.subscribe().await;133
+ let (item_id, _, _) = send_first_message(&timeline, &mut stream).await;134
+135
+ timeline136
+ .controller137
+ .toggle_reaction_local(&item_id, "mxc://example.org/blobcat", Some("blobcat"))138
+ .await139
+ .unwrap();140
+141
+ assert!(timeline.data().sent_events.read().await.is_empty());142
+ let sent_raw_events = &timeline.data().sent_raw_events.read().await;143
+ assert_eq!(sent_raw_events.len(), 1);144
+ let reaction = sent_raw_events[0].deserialize_as_unchecked::<ReactionEventContent>().unwrap();145
+ assert_eq!(reaction.relates_to.key, "mxc://example.org/blobcat");146
+ assert_eq!(147
+ sent_raw_events[0].get_field::<String>("com.beeper.reaction.shortcode").unwrap().as_deref(),148
+ Some("blobcat")149
+ );150
+}151
+152
+#[async_test]153
+async fn test_remote_reaction_shortcode_is_surfaced() {154
+ let timeline = TestTimeline::new();155
+ let mut stream = timeline.subscribe().await;156
+ let (_, event_id, item_pos) = send_first_message(&timeline, &mut stream).await;157
+158
+ let mut reaction = timeline159
+ .factory160
+ .reaction(&event_id, "mxc://example.org/blobcat")161
+ .sender(*ALICE)162
+ .into_raw_sync()163
+ .deserialize_as_unchecked::<serde_json::Value>()164
+ .unwrap();165
+ reaction["content"]["com.beeper.reaction.shortcode"] = "blobcat".into();166
+ timeline167
+ .handle_live_event(TimelineEvent::from_plaintext(168
+ Raw::new(&reaction).unwrap().cast_unchecked(),169
+ ))170
+ .await;171
+172
+ let event = assert_item_update!(stream, &event_id, item_pos);173
+ let reactions = event.content().reactions().cloned().unwrap_or_default();174
+ let reaction = reactions.get("mxc://example.org/blobcat").unwrap().get(*ALICE).unwrap();175
+ assert_eq!(reaction.shortcode.as_deref(), Some("blobcat"));176
+}177
+178
+#[async_test]128179
async fn test_redact_reaction_success() {129180
let timeline = TestTimeline::new();130181
let f = &timeline.factory;@@ -30,6 +30,7 @@ use ruma::{
3030
receipt::{Receipt, ReceiptThread, ReceiptType},3131
},3232
room_version_rules::RoomVersionRules,33
+ serde::Raw,3334
};3435
use tracing::error;3536
@@ -133,6 +134,13 @@ pub(super) trait RoomDataProvider:
133134
content: AnyMessageLikeEventContent,134135
) -> impl Future<Output = Result<(), super::Error>> + SendOutsideWasm + '_;135136
137
+ /// Send an event to that room, from its raw content.138
+ fn send_raw(139
+ &self,140
+ content: Raw<AnyMessageLikeEventContent>,141
+ event_type: String,142
+ ) -> impl Future<Output = Result<(), super::Error>> + SendOutsideWasm + '_;143
+136144
/// Redact an event from that room.137145
fn redact<'a>(138146
&'a self,@@ -223,6 +231,15 @@ impl RoomDataProvider for Room {
223231
Ok(())224232
}225233
234
+ async fn send_raw(235
+ &self,236
+ content: Raw<AnyMessageLikeEventContent>,237
+ event_type: String,238
+ ) -> Result<(), super::Error> {239
+ let _ = self.send_queue().send_raw(content, event_type).await?;240
+ Ok(())241
+ }242
+226243
async fn redact<'a>(227244
&'a self,228245
event_id: &'a EventId,@@ -2855,6 +2855,7 @@ mod builder_tests {
28552855
let transaction_id = OwnedTransactionId::from("txnid1");28562856
let content = LocalEchoContent::React {28572857
key: "<< 1".to_owned(),2858
+ shortcode: None,28582859
send_handle: SendReactionHandle::new(28592860
room_send_queue.clone(),28602861
ChildTransactionId::new(),@@ -1940,6 +1940,7 @@ impl QueueStorage {
19401940
&self,19411941
transaction_id: &TransactionId,19421942
key: String,1943
+ shortcode: Option<String>,19431944
created_at: MilliSecondsSinceUnixEpoch,19441945
) -> Result<Option<ChildTransactionId>, RoomSendQueueStorageError> {19451946
let guard = self.store.lock().await;@@ -1971,7 +1972,7 @@ impl QueueStorage {
19711972
transaction_id,19721973
reaction_txn_id.clone(),19731974
created_at,1974
- DependentQueuedRequestKind::ReactEvent { key },1975
+ DependentQueuedRequestKind::ReactEvent { key, shortcode },19751976
)19761977
.await?;19771978
@@ -2036,10 +2037,11 @@ impl QueueStorage {
20362037
None20372038
}20382039
2039
- DependentQueuedRequestKind::ReactEvent { key } => Some(LocalEcho {2040
+ DependentQueuedRequestKind::ReactEvent { key, shortcode } => Some(LocalEcho {20402041
transaction_id: dep.own_transaction_id.clone().into(),20412042
content: LocalEchoContent::React {20422043
key,2044
+ shortcode,20432045
send_handle: SendReactionHandle {20442046
room: room.clone(),20452047
transaction_id: dep.own_transaction_id,@@ -2271,7 +2273,7 @@ impl QueueStorage {
22712273
}22722274
}22732275
2274
- DependentQueuedRequestKind::ReactEvent { key } => {2276
+ DependentQueuedRequestKind::ReactEvent { key, shortcode } => {22752277
if let Some(parent_key) = parent_key {22762278
let Some(parent_event_id) = parent_key.into_event_id() else {22772279
return Err(RoomSendQueueError::StorageError(@@ -2282,9 +2284,14 @@ impl QueueStorage {
22822284
// Queue the reaction event in the send queue 🧠.22832285
let react_event =22842286
ReactionEventContent::new(Annotation::new(parent_event_id, key)).into();2287
+ let mut raw = Raw::new(&react_event)2288
+ .map_err(RoomSendQueueStorageError::JsonSerialization)?;2289
+ if let Some(shortcode) = shortcode {2290
+ raw = with_reaction_shortcode(raw, shortcode)2291
+ .map_err(RoomSendQueueStorageError::JsonSerialization)?;2292
+ }22852293
let serializable = SerializableEventContent::from_raw(2286
- Raw::new(&react_event)2287
- .map_err(RoomSendQueueStorageError::JsonSerialization)?,2294
+ raw,22882295
react_event.event_type().to_string(),22892296
);22902297
@@ -2485,6 +2492,11 @@ pub enum LocalEchoContent {
24852492
React {24862493
/// The key with which the local echo has been reacted to.24872494
key: String,2495
+ /// The shortcode of the custom emote used as the key, as per2496
+ /// [MSC4027].2497
+ ///2498
+ /// [MSC4027]: https://github.com/matrix-org/matrix-spec-proposals/pull/40272499
+ shortcode: Option<String>,24882500
/// A handle to manipulate the sending of the reaction.24892501
send_handle: SendReactionHandle,24902502
/// The local echo which has been reacted to.@@ -2903,11 +2915,28 @@ impl SendHandle {
29032915
&self,29042916
key: String,29052917
) -> Result<Option<SendReactionHandle>, RoomSendQueueStorageError> {2918
+ self.react_with_shortcode(key, None).await2919
+ }2920
+2921
+ /// Like [`Self::react`], for a custom emote reaction whose key is the2922
+ /// emote's MXC URI, carrying its shortcode as per [MSC4027].2923
+ ///2924
+ /// [MSC4027]: https://github.com/matrix-org/matrix-spec-proposals/pull/40272925
+ #[instrument(skip(self), fields(room_id = %self.room.inner.room.room_id(), txn_id = %self.transaction_id))]2926
+ pub async fn react_with_shortcode(2927
+ &self,2928
+ key: String,2929
+ shortcode: Option<String>,2930
+ ) -> Result<Option<SendReactionHandle>, RoomSendQueueStorageError> {29062931
trace!("received an intent to react");29072932
29082933
let created_at = MilliSecondsSinceUnixEpoch::now();2909
- if let Some(reaction_txn_id) =2910
- self.room.inner.queue.react(&self.transaction_id, key.clone(), created_at).await?2934
+ if let Some(reaction_txn_id) = self2935
+ .room2936
+ .inner2937
+ .queue2938
+ .react(&self.transaction_id, key.clone(), shortcode.clone(), created_at)2939
+ .await?29112940
{29122941
trace!("successfully queued react");29132942
@@ -2926,6 +2955,7 @@ impl SendHandle {
29262955
transaction_id: reaction_txn_id.into(),29272956
content: LocalEchoContent::React {29282957
key,2958
+ shortcode,29292959
send_handle: send_handle.clone(),29302960
applies_to: self.transaction_id.clone(),29312961
},@@ -3086,6 +3116,20 @@ fn canonicalize_dependent_requests(
30863116
by_txn.into_values().flat_map(|entries| entries.into_iter().cloned()).collect()30873117
}30883118
3119
+/// Adds the [MSC4027] shortcode of a custom emote to a reaction's content,3120
+/// which ruma's [`ReactionEventContent`] has no field for.3121
+///3122
+/// [MSC4027]: https://github.com/matrix-org/matrix-spec-proposals/pull/40273123
+fn with_reaction_shortcode(3124
+ content: Raw<AnyMessageLikeEventContent>,3125
+ shortcode: String,3126
+) -> Result<Raw<AnyMessageLikeEventContent>, serde_json::Error> {3127
+ let mut content =3128
+ content.deserialize_as_unchecked::<serde_json::Map<String, serde_json::Value>>()?;3129
+ content.insert("com.beeper.reaction.shortcode".to_owned(), shortcode.into());3130
+ Ok(Raw::new(&content)?.cast_unchecked())3131
+}3132
+30893133
#[cfg(all(test, not(target_family = "wasm")))]30903134
mod tests {30913135
use std::{sync::Arc, time::Duration};@@ -3344,7 +3388,7 @@ mod tests {
33443388
let react_id = ChildTransactionId::new();33453389
let react = DependentQueuedRequest {33463390
own_transaction_id: react_id.clone(),3347
- kind: DependentQueuedRequestKind::ReactEvent { key: "🧠".to_owned() },3391
+ kind: DependentQueuedRequestKind::ReactEvent { key: "🧠".to_owned(), shortcode: None },33483392
parent_transaction_id: txn.clone(),33493393
parent_key: None,33503394
created_at: MilliSecondsSinceUnixEpoch::now(),@@ -232,6 +232,7 @@ macro_rules! assert_update {
232232
key,233233
applies_to,234234
send_handle: _,235
+ ..235236
},236237
transaction_id: txn,237238
}))) = timeout(Duration::from_secs(1), $watch.recv()).await