Skip to main content

matrix_ui_serializable/room/frontend_events/
events_dto.rs

1use bitflags::bitflags;
2use std::sync::Arc;
3
4use matrix_sdk::ruma::{OwnedEventId, UInt, event_id, events::room::message::MessageType};
5use matrix_sdk_ui::timeline::{
6    EventTimelineItem, MsgLikeKind, TimelineEventItemId, TimelineItem, TimelineItemContent,
7    TimelineItemKind, VirtualTimelineItem,
8};
9use serde::{Serialize, Serializer};
10
11use crate::{
12    events::timeline::TimelineKind,
13    room::frontend_events::{
14        msg_like::{FrontendStickerEventContent, SerializableReactions},
15        state_event::{
16            FrontendAnyOtherStateEventContentChange, FrontendMemberProfileChange,
17            FrontendRoomMembershipChange, FrontendStateEvent,
18        },
19        thread_summary::get_frontend_thread_summary,
20        timeline_item_id::FrontendTimelineEventItemId,
21    },
22    user::user_power_level::UserPowerLevels,
23    utils::get_or_fetch_event_sender,
24};
25
26use super::{
27    msg_like::{FrontendMsgLikeContent, FrontendMsgLikeKind},
28    virtual_event::FrontendVirtualTimelineItem,
29};
30
31#[derive(Debug, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct FrontendTimelineItem {
34    unique_id: String,
35    event_id: Option<OwnedEventId>,
36    #[serde(flatten)]
37    timeline_item_id: FrontendTimelineEventItemId,
38    #[serde(flatten)]
39    data: FrontendTimelineItemData,
40    timestamp: Option<UInt>, // We keep the timestamp at root to sort events
41    is_own: bool,
42    is_local: bool,
43    abilities: MessageAbilities,
44}
45
46#[allow(clippy::large_enum_variant)]
47#[derive(Debug, Serialize)]
48#[serde(
49    rename_all = "camelCase",
50    rename_all_fields = "camelCase",
51    tag = "kind",
52    content = "data"
53)]
54pub enum FrontendTimelineItemData {
55    MsgLike(FrontendMsgLikeContent),
56    Virtual(FrontendVirtualTimelineItem),
57    StateChange(FrontendStateEvent),
58    Error(FrontendTimelineErrorItem),
59    Call,
60}
61
62#[derive(Debug, Clone, Serialize)]
63#[serde(rename_all = "camelCase")]
64pub struct FrontendTimelineErrorItem {
65    error: String,
66}
67
68pub fn to_frontend_timeline_item(
69    item: &Arc<TimelineItem>,
70    timeline_kind: &TimelineKind,
71    user_power_levels: &UserPowerLevels,
72) -> Option<FrontendTimelineItem> {
73    let unique_id = item.unique_id().0.clone();
74    match item.kind() {
75        TimelineItemKind::Event(event_tl_item) => {
76            map_event_timeline_item(unique_id, event_tl_item, timeline_kind, user_power_levels)
77        }
78        TimelineItemKind::Virtual(event) => match event {
79            VirtualTimelineItem::DateDivider(timestamp) => Some(FrontendTimelineItem {
80                unique_id,
81                event_id: None,
82                timeline_item_id: TimelineEventItemId::EventId(
83                    event_id!("$no_ids_for_virtual").to_owned(),
84                )
85                .into(),
86                data: FrontendTimelineItemData::Virtual(FrontendVirtualTimelineItem::DateDivider),
87                is_local: true,
88                is_own: true,
89                timestamp: Some(timestamp.0),
90                abilities: MessageAbilities::empty(),
91            }),
92            VirtualTimelineItem::ReadMarker => Some(FrontendTimelineItem {
93                unique_id,
94                event_id: None,
95                timeline_item_id: TimelineEventItemId::EventId(
96                    event_id!("$no_ids_for_virtual").to_owned(),
97                )
98                .into(),
99                data: FrontendTimelineItemData::Virtual(FrontendVirtualTimelineItem::ReadMarker),
100                is_local: true,
101                is_own: true,
102                timestamp: None,
103                abilities: MessageAbilities::empty(),
104            }),
105            VirtualTimelineItem::TimelineStart => Some(FrontendTimelineItem {
106                unique_id,
107                event_id: None,
108                timeline_item_id: TimelineEventItemId::EventId(
109                    event_id!("$no_ids_for_virtual").to_owned(),
110                )
111                .into(),
112                data: FrontendTimelineItemData::Virtual(FrontendVirtualTimelineItem::TimelineStart),
113                is_local: true,
114                is_own: true,
115                timestamp: None,
116                abilities: MessageAbilities::empty(),
117            }),
118        },
119    }
120}
121
122fn map_msg_event_content(content: MessageType) -> FrontendMsgLikeKind {
123    match content {
124        MessageType::Audio(c) => FrontendMsgLikeKind::Audio(c),
125        MessageType::File(c) => FrontendMsgLikeKind::File(c),
126        MessageType::Image(c) => FrontendMsgLikeKind::Image(c),
127        MessageType::Text(c) => FrontendMsgLikeKind::Text(c),
128        MessageType::Video(c) => FrontendMsgLikeKind::Video(c),
129        MessageType::Emote(c) => FrontendMsgLikeKind::Emote(c),
130        MessageType::Location(c) => FrontendMsgLikeKind::Location(c),
131        MessageType::Notice(c) => FrontendMsgLikeKind::Notice(c),
132        MessageType::ServerNotice(c) => FrontendMsgLikeKind::ServerNotice(c),
133        MessageType::VerificationRequest(c) => FrontendMsgLikeKind::VerificationRequest(c),
134        _ => FrontendMsgLikeKind::Unknown,
135    }
136}
137
138bitflags! {
139    /// Possible actions that the user can perform on a message.
140    ///
141    /// This is used to determine which buttons to show in the message context menu.
142    #[derive(Copy, Clone, Debug)]
143    pub struct MessageAbilities: u8 {
144        /// Whether the user can react to this message.
145        const CanReact = 1 << 0;
146        /// Whether the user can reply to this message.
147        const CanReplyTo = 1 << 1;
148        /// Whether the user can edit this message.
149        const CanEdit = 1 << 2;
150        /// Whether the user can pin this message.
151        const CanPin = 1 << 3;
152        /// Whether the user can unpin this message.
153        const CanUnpin = 1 << 4;
154        /// Whether the user can delete/redact this message.
155        const CanDelete = 1 << 5;
156    }
157}
158impl MessageAbilities {
159    pub fn from_user_power_and_event(
160        user_power_levels: &UserPowerLevels,
161        event_tl_item: &EventTimelineItem,
162    ) -> Self {
163        let mut abilities = Self::empty();
164        abilities.set(Self::CanEdit, event_tl_item.is_editable());
165        // Currently we only support deleting one's own messages.
166        if event_tl_item.is_own() {
167            abilities.set(Self::CanDelete, user_power_levels._can_redact_own());
168        }
169        abilities.set(Self::CanReplyTo, event_tl_item.can_be_replied_to());
170        abilities.set(Self::CanPin, user_power_levels._can_pin());
171        // TODO: currently we don't differentiate between pin and unpin,
172        //       but we should first check whether the given message is already pinned
173        //       before deciding which ability to set.
174        // abilities.set(Self::CanUnPin, user_power_levels.can_pin_unpin());
175        abilities.set(Self::CanReact, user_power_levels._can_send_reaction());
176        abilities
177    }
178}
179
180impl Serialize for MessageAbilities {
181    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
182    where
183        S: Serializer,
184    {
185        use serde::ser::SerializeSeq;
186
187        let mut seq = serializer.serialize_seq(None)?;
188
189        if self.contains(MessageAbilities::CanReact) {
190            seq.serialize_element("canReact")?;
191        }
192        if self.contains(MessageAbilities::CanReplyTo) {
193            seq.serialize_element("canReplyTo")?;
194        }
195        if self.contains(MessageAbilities::CanEdit) {
196            seq.serialize_element("canEdit")?;
197        }
198        if self.contains(MessageAbilities::CanPin) {
199            seq.serialize_element("canPin")?;
200        }
201        if self.contains(MessageAbilities::CanUnpin) {
202            seq.serialize_element("canUnpin")?;
203        }
204        if self.contains(MessageAbilities::CanDelete) {
205            seq.serialize_element("canDelete")?;
206        }
207
208        seq.end()
209    }
210}
211
212pub(crate) fn map_event_timeline_item(
213    unique_id: String,
214    event_tl_item: &EventTimelineItem,
215    kind: &TimelineKind,
216    user_power_levels: &UserPowerLevels,
217) -> Option<FrontendTimelineItem> {
218    let timeline_item_id: FrontendTimelineEventItemId = event_tl_item.identifier().into();
219    let is_own = event_tl_item.is_own();
220    let is_local = event_tl_item.is_local_echo();
221    let timestamp = Some(event_tl_item.timestamp().get());
222    let sender = Some(get_or_fetch_event_sender(event_tl_item, Some(kind.clone())));
223    let sender_id = event_tl_item.sender().to_string();
224    let abilities = MessageAbilities::from_user_power_and_event(user_power_levels, event_tl_item);
225    let event_id = event_tl_item.event_id().map(|id| id.to_owned());
226    map_timeline_event_item_content(
227        event_tl_item.content(),
228        unique_id,
229        timeline_item_id,
230        is_own,
231        is_local,
232        timestamp,
233        sender,
234        sender_id,
235        abilities,
236        event_id,
237    )
238}
239
240#[allow(clippy::too_many_arguments)]
241pub(super) fn map_timeline_event_item_content(
242    timeline_item_content: &TimelineItemContent,
243    unique_id: String,
244    timeline_item_id: FrontendTimelineEventItemId,
245    is_own: bool,
246    is_local: bool,
247    timestamp: Option<UInt>,
248    sender: Option<String>,
249    sender_id: String,
250    abilities: MessageAbilities,
251    event_id: Option<OwnedEventId>,
252) -> Option<FrontendTimelineItem> {
253    match timeline_item_content {
254        TimelineItemContent::MsgLike(msg_like) => {
255            let in_reply_to_id = msg_like.in_reply_to.clone().map(|r| r.event_id);
256            let thread_root = msg_like.thread_root.clone();
257            let thread_summary = msg_like
258                .thread_summary
259                .clone()
260                .and_then(get_frontend_thread_summary);
261            match msg_like.kind.clone() {
262                MsgLikeKind::Message(message) => Some(FrontendTimelineItem {
263                    unique_id,
264                    event_id,
265                    timeline_item_id,
266                    is_local,
267                    is_own,
268                    timestamp,
269                    abilities,
270                    data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
271                        edited: message.is_edited(),
272                        reactions: SerializableReactions(msg_like.reactions.clone()),
273                        sender_id,
274                        sender,
275                        thread_root,
276                        thread_summary,
277                        in_reply_to_id,
278                        kind: map_msg_event_content(message.msgtype().clone()),
279                    }),
280                }),
281                MsgLikeKind::Sticker(sticker) => Some(FrontendTimelineItem {
282                    unique_id,
283                    event_id,
284                    timeline_item_id,
285                    is_local,
286                    is_own,
287                    timestamp,
288                    abilities,
289                    data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
290                        edited: false,
291                        reactions: SerializableReactions(msg_like.reactions.clone()),
292                        sender_id,
293                        sender,
294                        thread_root,
295                        thread_summary,
296                        in_reply_to_id,
297                        kind: FrontendMsgLikeKind::Sticker(Box::new(
298                            FrontendStickerEventContent::from(sticker.content().clone()),
299                        )),
300                    }),
301                }),
302                MsgLikeKind::Redacted => Some(FrontendTimelineItem {
303                    unique_id,
304                    event_id,
305                    timeline_item_id,
306                    is_local,
307                    is_own,
308                    timestamp,
309                    abilities,
310                    data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
311                        edited: true,
312                        reactions: SerializableReactions(msg_like.reactions.clone()),
313                        sender_id,
314                        sender,
315                        thread_root,
316                        thread_summary,
317                        in_reply_to_id,
318                        kind: FrontendMsgLikeKind::Redacted,
319                    }),
320                }),
321                MsgLikeKind::UnableToDecrypt(_) => Some(FrontendTimelineItem {
322                    unique_id,
323                    event_id,
324                    timeline_item_id,
325                    is_local,
326                    is_own,
327                    timestamp,
328                    abilities,
329                    data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
330                        edited: false,
331                        reactions: SerializableReactions(msg_like.reactions.clone()),
332                        sender_id,
333                        sender,
334                        thread_root,
335                        thread_summary,
336                        in_reply_to_id,
337                        kind: FrontendMsgLikeKind::UnableToDecrypt,
338                    }),
339                }),
340                // TODO: map locations
341                MsgLikeKind::LiveLocation(_) => None,
342
343                MsgLikeKind::Poll(_) => Some(FrontendTimelineItem {
344                    unique_id,
345                    event_id,
346                    timeline_item_id,
347                    is_local,
348                    is_own,
349                    timestamp,
350                    abilities,
351                    data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
352                        edited: false,
353                        reactions: SerializableReactions(msg_like.reactions.clone()),
354                        sender_id,
355                        sender,
356                        thread_root,
357                        thread_summary,
358                        in_reply_to_id,
359                        kind: FrontendMsgLikeKind::Poll,
360                    }),
361                }),
362
363                MsgLikeKind::Other(_) => Some(FrontendTimelineItem {
364                    unique_id,
365                    event_id,
366                    timeline_item_id,
367                    is_local,
368                    is_own,
369                    timestamp,
370                    abilities,
371                    data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
372                        edited: false,
373                        reactions: SerializableReactions(msg_like.reactions.clone()),
374                        sender_id,
375                        sender,
376                        thread_root,
377                        thread_summary,
378                        in_reply_to_id,
379                        kind: FrontendMsgLikeKind::Unknown,
380                    }),
381                }),
382            }
383        }
384        TimelineItemContent::OtherState(state) => Some(FrontendTimelineItem {
385            unique_id,
386            event_id,
387            timeline_item_id,
388            is_local,
389            is_own,
390            timestamp,
391            abilities,
392            data: FrontendTimelineItemData::StateChange(FrontendStateEvent::OtherState(
393                FrontendAnyOtherStateEventContentChange::from(state.content().clone()),
394            )),
395        }),
396        TimelineItemContent::MembershipChange(change) => Some(FrontendTimelineItem {
397            unique_id,
398            event_id,
399            timeline_item_id,
400            is_local,
401            is_own,
402            timestamp,
403            abilities,
404            data: FrontendTimelineItemData::StateChange(FrontendStateEvent::MembershipChange(
405                FrontendRoomMembershipChange::from(change.clone()),
406            )),
407        }),
408
409        TimelineItemContent::ProfileChange(change) => Some(FrontendTimelineItem {
410            unique_id,
411            event_id,
412            timeline_item_id,
413            is_local,
414            is_own,
415            timestamp,
416            abilities,
417            data: FrontendTimelineItemData::StateChange(FrontendStateEvent::ProfileChange(
418                FrontendMemberProfileChange::from(change.clone()),
419            )),
420        }),
421
422        TimelineItemContent::RtcNotification { .. } | TimelineItemContent::CallInvite => {
423            Some(FrontendTimelineItem {
424                unique_id,
425                event_id,
426                timeline_item_id,
427                is_local,
428                is_own,
429                timestamp,
430                abilities,
431                data: FrontendTimelineItemData::Call,
432            })
433        }
434
435        TimelineItemContent::FailedToParseMessageLike {
436            event_type: _,
437            error,
438        } => Some(FrontendTimelineItem {
439            unique_id,
440            event_id: None,
441            timeline_item_id,
442            data: FrontendTimelineItemData::Error(FrontendTimelineErrorItem {
443                error: error.to_string(),
444            }),
445            is_local: true,
446            is_own: true,
447            timestamp: None,
448            abilities,
449        }),
450
451        TimelineItemContent::FailedToParseState {
452            state_key: _,
453            event_type: _,
454            error,
455        } => Some(FrontendTimelineItem {
456            unique_id,
457            event_id: None,
458            timeline_item_id,
459            data: FrontendTimelineItemData::Error(FrontendTimelineErrorItem {
460                error: error.to_string(),
461            }),
462            is_local: true,
463            is_own: true,
464            timestamp: None,
465            abilities,
466        }),
467    }
468}