matrix_sdk_ui/timeline/event_item/content/
message.rs1use std::{fmt, sync::Arc};
18
19use imbl::{vector, Vector};
20use matrix_sdk::{deserialized_responses::TimelineEvent, Room};
21use ruma::{
22 assign,
23 events::{
24 poll::unstable_start::{
25 NewUnstablePollStartEventContentWithoutRelation, SyncUnstablePollStartEvent,
26 UnstablePollStartEventContent,
27 },
28 relation::{InReplyTo, Thread},
29 room::message::{
30 MessageType, Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
31 SyncRoomMessageEvent,
32 },
33 AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
34 BundledMessageLikeRelations, Mentions,
35 },
36 html::RemoveReplyFallback,
37 serde::Raw,
38 OwnedEventId, OwnedUserId, UserId,
39};
40use tracing::{error, trace};
41
42use super::TimelineItemContent;
43use crate::{
44 timeline::{
45 event_item::{EventTimelineItem, Profile, TimelineDetails},
46 traits::RoomDataProvider,
47 Error as TimelineError, TimelineItem,
48 },
49 DEFAULT_SANITIZER_MODE,
50};
51
52#[derive(Clone)]
54pub struct Message {
55 pub(in crate::timeline) msgtype: MessageType,
56 pub(in crate::timeline) in_reply_to: Option<InReplyToDetails>,
57 pub(in crate::timeline) thread_root: Option<OwnedEventId>,
59 pub(in crate::timeline) edited: bool,
60 pub(in crate::timeline) mentions: Option<Mentions>,
61}
62
63impl Message {
64 pub(in crate::timeline) fn from_event(
66 c: RoomMessageEventContent,
67 edit: Option<RoomMessageEventContentWithoutRelation>,
68 timeline_items: &Vector<Arc<TimelineItem>>,
69 ) -> Self {
70 let mut thread_root = None;
71 let in_reply_to = c.relates_to.and_then(|relation| match relation {
72 Relation::Reply { in_reply_to } => {
73 Some(InReplyToDetails::new(in_reply_to.event_id, timeline_items))
74 }
75 Relation::Thread(thread) => {
76 thread_root = Some(thread.event_id);
77 thread
78 .in_reply_to
79 .map(|in_reply_to| InReplyToDetails::new(in_reply_to.event_id, timeline_items))
80 }
81 _ => None,
82 });
83
84 let remove_reply_fallback =
85 if in_reply_to.is_some() { RemoveReplyFallback::Yes } else { RemoveReplyFallback::No };
86
87 let mut msgtype = c.msgtype;
88 msgtype.sanitize(DEFAULT_SANITIZER_MODE, remove_reply_fallback);
89
90 let mut ret =
91 Self { msgtype, in_reply_to, thread_root, edited: false, mentions: c.mentions };
92
93 if let Some(edit) = edit {
94 ret.apply_edit(edit);
95 }
96
97 ret
98 }
99
100 pub(crate) fn apply_edit(&mut self, mut new_content: RoomMessageEventContentWithoutRelation) {
102 trace!("applying edit to a Message");
103 new_content.msgtype.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::No);
105 self.msgtype = new_content.msgtype;
106 self.mentions = new_content.mentions;
107 self.edited = true;
108 }
109
110 pub fn msgtype(&self) -> &MessageType {
112 &self.msgtype
113 }
114
115 pub fn body(&self) -> &str {
119 self.msgtype.body()
120 }
121
122 pub fn in_reply_to(&self) -> Option<&InReplyToDetails> {
124 self.in_reply_to.as_ref()
125 }
126
127 pub fn is_threaded(&self) -> bool {
129 self.thread_root.is_some()
130 }
131
132 pub fn thread_root(&self) -> Option<&OwnedEventId> {
134 self.thread_root.as_ref()
135 }
136
137 pub fn is_edited(&self) -> bool {
140 self.edited
141 }
142
143 pub fn mentions(&self) -> Option<&Mentions> {
145 self.mentions.as_ref()
146 }
147
148 pub(in crate::timeline) fn to_content(&self) -> RoomMessageEventContent {
149 let relates_to = make_relates_to(
152 self.thread_root.clone(),
153 self.in_reply_to.as_ref().map(|details| details.event_id.clone()),
154 );
155 assign!(RoomMessageEventContent::new(self.msgtype.clone()), { relates_to })
156 }
157
158 pub(in crate::timeline) fn with_in_reply_to(&self, in_reply_to: InReplyToDetails) -> Self {
159 Self { in_reply_to: Some(in_reply_to), ..self.clone() }
160 }
161}
162
163impl From<Message> for RoomMessageEventContent {
164 fn from(msg: Message) -> Self {
165 let relates_to =
166 make_relates_to(msg.thread_root, msg.in_reply_to.map(|details| details.event_id));
167 assign!(Self::new(msg.msgtype), { relates_to })
168 }
169}
170
171pub(crate) fn extract_bundled_edit_event_json(
177 raw: &Raw<AnySyncTimelineEvent>,
178) -> Option<Raw<AnySyncTimelineEvent>> {
179 let raw_unsigned: Raw<serde_json::Value> = raw.get_field("unsigned").ok()??;
181 let raw_relations: Raw<serde_json::Value> = raw_unsigned.get_field("m.relations").ok()??;
182 raw_relations.get_field::<Raw<AnySyncTimelineEvent>>("m.replace").ok()?
183}
184
185pub(crate) fn extract_room_msg_edit_content(
188 relations: BundledMessageLikeRelations<AnySyncMessageLikeEvent>,
189) -> Option<RoomMessageEventContentWithoutRelation> {
190 match *relations.replace? {
191 AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Original(ev)) => match ev
192 .content
193 .relates_to
194 {
195 Some(Relation::Replacement(re)) => {
196 trace!("found a bundled edit event in a room message");
197 Some(re.new_content)
198 }
199 _ => {
200 error!("got m.room.message event with an edit without a valid m.replace relation");
201 None
202 }
203 },
204
205 AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Redacted(_)) => None,
206
207 _ => {
208 error!("got m.room.message event with an edit of a different event type");
209 None
210 }
211 }
212}
213
214pub(crate) fn extract_poll_edit_content(
217 relations: BundledMessageLikeRelations<AnySyncMessageLikeEvent>,
218) -> Option<NewUnstablePollStartEventContentWithoutRelation> {
219 match *relations.replace? {
220 AnySyncMessageLikeEvent::UnstablePollStart(SyncUnstablePollStartEvent::Original(ev)) => {
221 match ev.content {
222 UnstablePollStartEventContent::Replacement(re) => {
223 trace!("found a bundled edit event in a poll");
224 Some(re.relates_to.new_content)
225 }
226 _ => {
227 error!("got new poll start event in a bundled edit");
228 None
229 }
230 }
231 }
232
233 AnySyncMessageLikeEvent::UnstablePollStart(SyncUnstablePollStartEvent::Redacted(_)) => None,
234
235 _ => {
236 error!("got poll edit event with an edit of a different event type");
237 None
238 }
239 }
240}
241
242fn make_relates_to(
249 thread_root: Option<OwnedEventId>,
250 in_reply_to: Option<OwnedEventId>,
251) -> Option<Relation<RoomMessageEventContentWithoutRelation>> {
252 match (thread_root, in_reply_to) {
253 (Some(thread_root), Some(in_reply_to)) => {
254 Some(Relation::Thread(Thread::plain(thread_root, in_reply_to)))
255 }
256 (Some(thread_root), None) => Some(Relation::Thread(Thread::without_fallback(thread_root))),
257 (None, Some(in_reply_to)) => {
258 Some(Relation::Reply { in_reply_to: InReplyTo::new(in_reply_to) })
259 }
260 (None, None) => None,
261 }
262}
263
264#[cfg(not(tarpaulin_include))]
265impl fmt::Debug for Message {
266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267 let Self { msgtype: _, in_reply_to, thread_root, edited, mentions: _ } = self;
268 f.debug_struct("Message")
271 .field("in_reply_to", in_reply_to)
272 .field("thread_root", thread_root)
273 .field("edited", edited)
274 .finish_non_exhaustive()
275 }
276}
277
278#[derive(Clone, Debug)]
280pub struct InReplyToDetails {
281 pub event_id: OwnedEventId,
283
284 pub event: TimelineDetails<Box<RepliedToEvent>>,
291}
292
293impl InReplyToDetails {
294 pub fn new(
295 event_id: OwnedEventId,
296 timeline_items: &Vector<Arc<TimelineItem>>,
297 ) -> InReplyToDetails {
298 let event = timeline_items
299 .iter()
300 .filter_map(|it| it.as_event())
301 .find(|it| it.event_id() == Some(&*event_id))
302 .map(|item| Box::new(RepliedToEvent::from_timeline_item(item)));
303
304 InReplyToDetails { event_id, event: TimelineDetails::from_initial_value(event) }
305 }
306}
307
308#[derive(Clone, Debug)]
310pub struct RepliedToEvent {
311 pub(in crate::timeline) content: TimelineItemContent,
312 pub(in crate::timeline) sender: OwnedUserId,
313 pub(in crate::timeline) sender_profile: TimelineDetails<Profile>,
314}
315
316impl RepliedToEvent {
317 pub fn content(&self) -> &TimelineItemContent {
319 &self.content
320 }
321
322 pub fn sender(&self) -> &UserId {
324 &self.sender
325 }
326
327 pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
329 &self.sender_profile
330 }
331
332 pub fn from_timeline_item(timeline_item: &EventTimelineItem) -> Self {
333 Self {
334 content: timeline_item.content.clone(),
335 sender: timeline_item.sender.clone(),
336 sender_profile: timeline_item.sender_profile.clone(),
337 }
338 }
339
340 pub async fn try_from_timeline_event_for_room(
343 timeline_event: TimelineEvent,
344 room_data_provider: &Room,
345 ) -> Result<Self, TimelineError> {
346 Self::try_from_timeline_event(timeline_event, room_data_provider).await
347 }
348
349 pub(in crate::timeline) async fn try_from_timeline_event<P: RoomDataProvider>(
350 timeline_event: TimelineEvent,
351 room_data_provider: &P,
352 ) -> Result<Self, TimelineError> {
353 let event = match timeline_event.raw().deserialize() {
354 Ok(AnySyncTimelineEvent::MessageLike(event)) => event,
355 _ => {
356 return Err(TimelineError::UnsupportedEvent);
357 }
358 };
359
360 let Some(AnyMessageLikeEventContent::RoomMessage(c)) = event.original_content() else {
361 return Err(TimelineError::UnsupportedEvent);
362 };
363
364 let content = TimelineItemContent::Message(Message::from_event(
365 c,
366 extract_room_msg_edit_content(event.relations()),
367 &vector![],
368 ));
369 let sender = event.sender().to_owned();
370 let sender_profile = TimelineDetails::from_initial_value(
371 room_data_provider.profile_from_user_id(&sender).await,
372 );
373
374 Ok(Self { content, sender, sender_profile })
375 }
376}