matrix_sdk_ui/timeline/event_item/content/
message.rs1use std::{fmt, sync::Arc};
18
19use imbl::{vector, Vector};
20use matrix_sdk::{
21 crypto::types::events::UtdCause,
22 deserialized_responses::{TimelineEvent, TimelineEventKind},
23 Room,
24};
25use ruma::{
26 assign,
27 events::{
28 poll::unstable_start::{
29 NewUnstablePollStartEventContentWithoutRelation, SyncUnstablePollStartEvent,
30 UnstablePollStartEventContent,
31 },
32 relation::{InReplyTo, Thread},
33 room::message::{
34 MessageType, Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
35 SyncRoomMessageEvent,
36 },
37 AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
38 BundledMessageLikeRelations, Mentions,
39 },
40 html::RemoveReplyFallback,
41 serde::Raw,
42 OwnedEventId, OwnedUserId, UserId,
43};
44use tracing::{debug, error, instrument, trace, warn};
45
46use super::TimelineItemContent;
47use crate::{
48 timeline::{
49 event_item::{EventTimelineItem, Profile, TimelineDetails},
50 traits::RoomDataProvider,
51 EncryptedMessage, Error as TimelineError, PollState, ReactionsByKeyBySender, Sticker,
52 TimelineItem,
53 },
54 DEFAULT_SANITIZER_MODE,
55};
56
57#[derive(Clone)]
59pub struct Message {
60 pub(in crate::timeline) msgtype: MessageType,
61 pub(in crate::timeline) in_reply_to: Option<InReplyToDetails>,
62 pub(in crate::timeline) thread_root: Option<OwnedEventId>,
64 pub(in crate::timeline) edited: bool,
65 pub(in crate::timeline) mentions: Option<Mentions>,
66 pub(in crate::timeline) reactions: ReactionsByKeyBySender,
67}
68
69impl Message {
70 pub(in crate::timeline) fn from_event(
72 c: RoomMessageEventContent,
73 edit: Option<RoomMessageEventContentWithoutRelation>,
74 timeline_items: &Vector<Arc<TimelineItem>>,
75 reactions: ReactionsByKeyBySender,
76 ) -> Self {
77 let mut thread_root = None;
78 let in_reply_to = c.relates_to.and_then(|relation| match relation {
79 Relation::Reply { in_reply_to } => {
80 Some(InReplyToDetails::new(in_reply_to.event_id, timeline_items))
81 }
82 Relation::Thread(thread) => {
83 thread_root = Some(thread.event_id);
84 thread
85 .in_reply_to
86 .map(|in_reply_to| InReplyToDetails::new(in_reply_to.event_id, timeline_items))
87 }
88 _ => None,
89 });
90
91 let remove_reply_fallback =
92 if in_reply_to.is_some() { RemoveReplyFallback::Yes } else { RemoveReplyFallback::No };
93
94 let mut msgtype = c.msgtype;
95 msgtype.sanitize(DEFAULT_SANITIZER_MODE, remove_reply_fallback);
96
97 let mut ret = Self {
98 msgtype,
99 in_reply_to,
100 thread_root,
101 edited: false,
102 mentions: c.mentions,
103 reactions,
104 };
105
106 if let Some(edit) = edit {
107 ret.apply_edit(edit);
108 }
109
110 ret
111 }
112
113 pub(crate) fn apply_edit(&mut self, mut new_content: RoomMessageEventContentWithoutRelation) {
115 trace!("applying edit to a Message");
116 new_content.msgtype.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::No);
118 self.msgtype = new_content.msgtype;
119 self.mentions = new_content.mentions;
120 self.edited = true;
121 }
122
123 pub fn msgtype(&self) -> &MessageType {
125 &self.msgtype
126 }
127
128 pub fn body(&self) -> &str {
132 self.msgtype.body()
133 }
134
135 pub fn in_reply_to(&self) -> Option<&InReplyToDetails> {
137 self.in_reply_to.as_ref()
138 }
139
140 pub fn is_threaded(&self) -> bool {
142 self.thread_root.is_some()
143 }
144
145 pub fn thread_root(&self) -> Option<&OwnedEventId> {
147 self.thread_root.as_ref()
148 }
149
150 pub fn is_edited(&self) -> bool {
153 self.edited
154 }
155
156 pub fn mentions(&self) -> Option<&Mentions> {
158 self.mentions.as_ref()
159 }
160
161 pub(in crate::timeline) fn to_content(&self) -> RoomMessageEventContent {
162 let relates_to = make_relates_to(
165 self.thread_root.clone(),
166 self.in_reply_to.as_ref().map(|details| details.event_id.clone()),
167 );
168 assign!(RoomMessageEventContent::new(self.msgtype.clone()), { relates_to })
169 }
170
171 pub(in crate::timeline) fn with_in_reply_to(&self, in_reply_to: InReplyToDetails) -> Self {
172 Self { in_reply_to: Some(in_reply_to), ..self.clone() }
173 }
174}
175
176impl From<Message> for RoomMessageEventContent {
177 fn from(msg: Message) -> Self {
178 let relates_to =
179 make_relates_to(msg.thread_root, msg.in_reply_to.map(|details| details.event_id));
180 assign!(Self::new(msg.msgtype), { relates_to })
181 }
182}
183
184pub(crate) fn extract_bundled_edit_event_json(
190 raw: &Raw<AnySyncTimelineEvent>,
191) -> Option<Raw<AnySyncTimelineEvent>> {
192 let raw_unsigned: Raw<serde_json::Value> = raw.get_field("unsigned").ok()??;
194 let raw_relations: Raw<serde_json::Value> = raw_unsigned.get_field("m.relations").ok()??;
195 raw_relations.get_field::<Raw<AnySyncTimelineEvent>>("m.replace").ok()?
196}
197
198pub(crate) fn extract_room_msg_edit_content(
201 relations: BundledMessageLikeRelations<AnySyncMessageLikeEvent>,
202) -> Option<RoomMessageEventContentWithoutRelation> {
203 match *relations.replace? {
204 AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Original(ev)) => match ev
205 .content
206 .relates_to
207 {
208 Some(Relation::Replacement(re)) => {
209 trace!("found a bundled edit event in a room message");
210 Some(re.new_content)
211 }
212 _ => {
213 error!("got m.room.message event with an edit without a valid m.replace relation");
214 None
215 }
216 },
217
218 AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Redacted(_)) => None,
219
220 _ => {
221 error!("got m.room.message event with an edit of a different event type");
222 None
223 }
224 }
225}
226
227pub(crate) fn extract_poll_edit_content(
230 relations: BundledMessageLikeRelations<AnySyncMessageLikeEvent>,
231) -> Option<NewUnstablePollStartEventContentWithoutRelation> {
232 match *relations.replace? {
233 AnySyncMessageLikeEvent::UnstablePollStart(SyncUnstablePollStartEvent::Original(ev)) => {
234 match ev.content {
235 UnstablePollStartEventContent::Replacement(re) => {
236 trace!("found a bundled edit event in a poll");
237 Some(re.relates_to.new_content)
238 }
239 _ => {
240 error!("got new poll start event in a bundled edit");
241 None
242 }
243 }
244 }
245
246 AnySyncMessageLikeEvent::UnstablePollStart(SyncUnstablePollStartEvent::Redacted(_)) => None,
247
248 _ => {
249 error!("got poll edit event with an edit of a different event type");
250 None
251 }
252 }
253}
254
255fn make_relates_to(
262 thread_root: Option<OwnedEventId>,
263 in_reply_to: Option<OwnedEventId>,
264) -> Option<Relation<RoomMessageEventContentWithoutRelation>> {
265 match (thread_root, in_reply_to) {
266 (Some(thread_root), Some(in_reply_to)) => {
267 Some(Relation::Thread(Thread::plain(thread_root, in_reply_to)))
268 }
269 (Some(thread_root), None) => Some(Relation::Thread(Thread::without_fallback(thread_root))),
270 (None, Some(in_reply_to)) => {
271 Some(Relation::Reply { in_reply_to: InReplyTo::new(in_reply_to) })
272 }
273 (None, None) => None,
274 }
275}
276
277#[cfg(not(tarpaulin_include))]
278impl fmt::Debug for Message {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 let Self { msgtype: _, in_reply_to, thread_root, edited, reactions: _, mentions: _ } = self;
281 f.debug_struct("Message")
284 .field("in_reply_to", in_reply_to)
285 .field("thread_root", thread_root)
286 .field("edited", edited)
287 .finish_non_exhaustive()
288 }
289}
290
291#[derive(Clone, Debug)]
293pub struct InReplyToDetails {
294 pub event_id: OwnedEventId,
296
297 pub event: TimelineDetails<Box<RepliedToEvent>>,
304}
305
306impl InReplyToDetails {
307 pub fn new(
308 event_id: OwnedEventId,
309 timeline_items: &Vector<Arc<TimelineItem>>,
310 ) -> InReplyToDetails {
311 let event = timeline_items
312 .iter()
313 .filter_map(|it| it.as_event())
314 .find(|it| it.event_id() == Some(&*event_id))
315 .map(|item| Box::new(RepliedToEvent::from_timeline_item(item)));
316
317 InReplyToDetails { event_id, event: TimelineDetails::from_initial_value(event) }
318 }
319}
320
321#[derive(Clone, Debug)]
323pub struct RepliedToEvent {
324 pub(in crate::timeline) content: TimelineItemContent,
325 pub(in crate::timeline) sender: OwnedUserId,
326 pub(in crate::timeline) sender_profile: TimelineDetails<Profile>,
327}
328
329impl RepliedToEvent {
330 pub fn content(&self) -> &TimelineItemContent {
332 &self.content
333 }
334
335 pub fn sender(&self) -> &UserId {
337 &self.sender
338 }
339
340 pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
342 &self.sender_profile
343 }
344
345 pub fn from_timeline_item(timeline_item: &EventTimelineItem) -> Self {
346 Self {
347 content: timeline_item.content.clone(),
348 sender: timeline_item.sender.clone(),
349 sender_profile: timeline_item.sender_profile.clone(),
350 }
351 }
352
353 pub async fn try_from_timeline_event_for_room(
356 timeline_event: TimelineEvent,
357 room_data_provider: &Room,
358 ) -> Result<Self, TimelineError> {
359 Self::try_from_timeline_event(timeline_event, room_data_provider).await
360 }
361
362 #[instrument(skip_all)]
363 pub(in crate::timeline) async fn try_from_timeline_event<P: RoomDataProvider>(
364 timeline_event: TimelineEvent,
365 room_data_provider: &P,
366 ) -> Result<Self, TimelineError> {
367 let event = match timeline_event.raw().deserialize() {
368 Ok(AnySyncTimelineEvent::MessageLike(event)) => event,
369 Ok(_) => {
370 warn!("can't get details, event isn't a message-like event");
371 return Err(TimelineError::UnsupportedEvent);
372 }
373 Err(err) => {
374 warn!("can't get details, event couldn't be deserialized: {err}");
375 return Err(TimelineError::UnsupportedEvent);
376 }
377 };
378
379 debug!(event_type = %event.event_type(), "got deserialized event");
380
381 let content = match event.original_content() {
382 Some(content) => match content {
383 AnyMessageLikeEventContent::RoomMessage(c) => {
384 let reactions = ReactionsByKeyBySender::default();
388
389 TimelineItemContent::Message(Message::from_event(
390 c,
391 extract_room_msg_edit_content(event.relations()),
392 &vector![],
393 reactions,
394 ))
395 }
396
397 AnyMessageLikeEventContent::Sticker(content) => {
398 let reactions = ReactionsByKeyBySender::default();
401 TimelineItemContent::Sticker(Sticker { content, reactions })
402 }
403
404 AnyMessageLikeEventContent::RoomEncrypted(content) => {
405 let utd_cause = match &timeline_event.kind {
406 TimelineEventKind::UnableToDecrypt { utd_info, .. } => UtdCause::determine(
407 timeline_event.raw(),
408 room_data_provider.crypto_context_info().await,
409 utd_info,
410 ),
411 _ => UtdCause::Unknown,
412 };
413
414 TimelineItemContent::UnableToDecrypt(EncryptedMessage::from_content(
415 content, utd_cause,
416 ))
417 }
418
419 AnyMessageLikeEventContent::UnstablePollStart(
420 UnstablePollStartEventContent::New(content),
421 ) => {
422 let reactions = ReactionsByKeyBySender::default();
425 let poll_state = PollState::new(content, None, reactions);
427 TimelineItemContent::Poll(poll_state)
428 }
429
430 _ => {
431 warn!("unsupported event type");
432 return Err(TimelineError::UnsupportedEvent);
433 }
434 },
435
436 None => {
437 TimelineItemContent::RedactedMessage
439 }
440 };
441
442 let sender = event.sender().to_owned();
443 let sender_profile = TimelineDetails::from_initial_value(
444 room_data_provider.profile_from_user_id(&sender).await,
445 );
446
447 Ok(Self { content, sender, sender_profile })
448 }
449}