matrix_sdk_ffi/timeline/
content.rs

1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{collections::HashMap, sync::Arc};
16
17use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
18use matrix_sdk_ui::timeline::{
19    MsgLikeContent, MsgLikeKind, PollResult, RoomPinnedEventsChange, TimelineDetails,
20};
21use ruma::events::{room::MediaSource as RumaMediaSource, EventContent, FullStateEventContent};
22
23use super::ProfileDetails;
24use crate::{
25    ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
26    utils::Timestamp,
27};
28
29impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
30    fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
31        use matrix_sdk_ui::timeline::TimelineItemContent as Content;
32
33        match value {
34            Content::MsgLike(MsgLikeContent {
35                kind: MsgLikeKind::Message(message),
36                thread_root,
37                in_reply_to,
38                ..
39            }) => {
40                let message_type_string = message.msgtype().msgtype().to_owned();
41
42                match TryInto::<MessageType>::try_into(message.msgtype().clone()) {
43                    Ok(message_type) => TimelineItemContent::Message {
44                        content: MessageContent {
45                            msg_type: message_type,
46                            body: message.body().to_owned(),
47                            in_reply_to: in_reply_to.map(|r| Arc::new(r.into())),
48                            is_edited: message.is_edited(),
49                            thread_root: thread_root.map(|id| id.to_string()),
50                            mentions: message.mentions().cloned().map(|m| m.into()),
51                        },
52                    },
53                    Err(error) => TimelineItemContent::FailedToParseMessageLike {
54                        event_type: message_type_string,
55                        error: error.to_string(),
56                    },
57                }
58            }
59
60            Content::MsgLike(MsgLikeContent { kind: MsgLikeKind::Redacted, .. }) => {
61                TimelineItemContent::RedactedMessage
62            }
63
64            Content::MsgLike(MsgLikeContent { kind: MsgLikeKind::Sticker(sticker), .. }) => {
65                let content = sticker.content();
66
67                let media_source = RumaMediaSource::from(content.source.clone());
68
69                if let Err(error) = media_source.verify() {
70                    return TimelineItemContent::FailedToParseMessageLike {
71                        event_type: sticker.content().event_type().to_string(),
72                        error: error.to_string(),
73                    };
74                }
75
76                match TryInto::<ImageInfo>::try_into(&content.info) {
77                    Ok(info) => TimelineItemContent::Sticker {
78                        body: content.body.clone(),
79                        info,
80                        source: Arc::new(MediaSource { media_source }),
81                    },
82                    Err(error) => TimelineItemContent::FailedToParseMessageLike {
83                        event_type: sticker.content().event_type().to_string(),
84                        error: error.to_string(),
85                    },
86                }
87            }
88
89            Content::MsgLike(MsgLikeContent { kind: MsgLikeKind::Poll(poll_state), .. }) => {
90                TimelineItemContent::from(poll_state.results())
91            }
92
93            Content::CallInvite => TimelineItemContent::CallInvite,
94
95            Content::CallNotify => TimelineItemContent::CallNotify,
96
97            Content::MsgLike(MsgLikeContent {
98                kind: MsgLikeKind::UnableToDecrypt(msg), ..
99            }) => TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(&msg) },
100
101            Content::MembershipChange(membership) => {
102                let reason = match membership.content() {
103                    FullStateEventContent::Original { content, .. } => content.reason.clone(),
104                    _ => None,
105                };
106                TimelineItemContent::RoomMembership {
107                    user_id: membership.user_id().to_string(),
108                    user_display_name: membership.display_name(),
109                    change: membership.change().map(Into::into),
110                    reason,
111                }
112            }
113
114            Content::ProfileChange(profile) => {
115                let (display_name, prev_display_name) = profile
116                    .displayname_change()
117                    .map(|change| (change.new.clone(), change.old.clone()))
118                    .unzip();
119                let (avatar_url, prev_avatar_url) = profile
120                    .avatar_url_change()
121                    .map(|change| {
122                        (
123                            change.new.as_ref().map(ToString::to_string),
124                            change.old.as_ref().map(ToString::to_string),
125                        )
126                    })
127                    .unzip();
128                TimelineItemContent::ProfileChange {
129                    display_name: display_name.flatten(),
130                    prev_display_name: prev_display_name.flatten(),
131                    avatar_url: avatar_url.flatten(),
132                    prev_avatar_url: prev_avatar_url.flatten(),
133                }
134            }
135
136            Content::OtherState(state) => TimelineItemContent::State {
137                state_key: state.state_key().to_owned(),
138                content: state.content().into(),
139            },
140
141            Content::FailedToParseMessageLike { event_type, error } => {
142                TimelineItemContent::FailedToParseMessageLike {
143                    event_type: event_type.to_string(),
144                    error: error.to_string(),
145                }
146            }
147
148            Content::FailedToParseState { event_type, state_key, error } => {
149                TimelineItemContent::FailedToParseState {
150                    event_type: event_type.to_string(),
151                    state_key,
152                    error: error.to_string(),
153                }
154            }
155        }
156    }
157}
158
159#[derive(Clone, uniffi::Record)]
160pub struct MessageContent {
161    pub msg_type: MessageType,
162    pub body: String,
163    pub in_reply_to: Option<Arc<InReplyToDetails>>,
164    pub thread_root: Option<String>,
165    pub is_edited: bool,
166    pub mentions: Option<Mentions>,
167}
168
169impl From<ruma::events::Mentions> for Mentions {
170    fn from(value: ruma::events::Mentions) -> Self {
171        Self {
172            user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(),
173            room: value.room,
174        }
175    }
176}
177
178#[derive(Clone, uniffi::Enum)]
179pub enum TimelineItemContent {
180    Message {
181        content: MessageContent,
182    },
183    RedactedMessage,
184    Sticker {
185        body: String,
186        info: ImageInfo,
187        source: Arc<MediaSource>,
188    },
189    Poll {
190        question: String,
191        kind: PollKind,
192        max_selections: u64,
193        answers: Vec<PollAnswer>,
194        votes: HashMap<String, Vec<String>>,
195        end_time: Option<Timestamp>,
196        has_been_edited: bool,
197    },
198    CallInvite,
199    CallNotify,
200    UnableToDecrypt {
201        msg: EncryptedMessage,
202    },
203    RoomMembership {
204        user_id: String,
205        user_display_name: Option<String>,
206        change: Option<MembershipChange>,
207        reason: Option<String>,
208    },
209    ProfileChange {
210        display_name: Option<String>,
211        prev_display_name: Option<String>,
212        avatar_url: Option<String>,
213        prev_avatar_url: Option<String>,
214    },
215    State {
216        state_key: String,
217        content: OtherState,
218    },
219    FailedToParseMessageLike {
220        event_type: String,
221        error: String,
222    },
223    FailedToParseState {
224        event_type: String,
225        state_key: String,
226        error: String,
227    },
228}
229
230#[derive(Clone, uniffi::Object)]
231pub struct InReplyToDetails {
232    event_id: String,
233    event: RepliedToEventDetails,
234}
235
236impl InReplyToDetails {
237    pub(crate) fn new(event_id: String, event: RepliedToEventDetails) -> Self {
238        Self { event_id, event }
239    }
240}
241
242#[matrix_sdk_ffi_macros::export]
243impl InReplyToDetails {
244    pub fn event_id(&self) -> String {
245        self.event_id.clone()
246    }
247
248    pub fn event(&self) -> RepliedToEventDetails {
249        self.event.clone()
250    }
251}
252
253impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
254    fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
255        let event_id = inner.event_id.to_string();
256        let event = match &inner.event {
257            TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
258            TimelineDetails::Pending => RepliedToEventDetails::Pending,
259            TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
260                content: event.content().clone().into(),
261                sender: event.sender().to_string(),
262                sender_profile: event.sender_profile().into(),
263            },
264            TimelineDetails::Error(err) => {
265                RepliedToEventDetails::Error { message: err.to_string() }
266            }
267        };
268
269        Self { event_id, event }
270    }
271}
272
273#[derive(Clone, uniffi::Enum)]
274pub enum RepliedToEventDetails {
275    Unavailable,
276    Pending,
277    Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
278    Error { message: String },
279}
280
281#[derive(Clone, uniffi::Enum)]
282pub enum EncryptedMessage {
283    OlmV1Curve25519AesSha2 {
284        /// The Curve25519 key of the sender.
285        sender_key: String,
286    },
287    // Other fields not included because UniFFI doesn't have the concept of
288    // deprecated fields right now.
289    MegolmV1AesSha2 {
290        /// The ID of the session used to encrypt the message.
291        session_id: String,
292
293        /// What we know about what caused this UTD. E.g. was this event sent
294        /// when we were not a member of this room?
295        cause: UtdCause,
296    },
297    Unknown,
298}
299
300impl EncryptedMessage {
301    fn new(msg: &matrix_sdk_ui::timeline::EncryptedMessage) -> Self {
302        use matrix_sdk_ui::timeline::EncryptedMessage as Message;
303
304        match msg {
305            Message::OlmV1Curve25519AesSha2 { sender_key } => {
306                let sender_key = sender_key.clone();
307                Self::OlmV1Curve25519AesSha2 { sender_key }
308            }
309            Message::MegolmV1AesSha2 { session_id, cause, .. } => {
310                let session_id = session_id.clone();
311                Self::MegolmV1AesSha2 { session_id, cause: *cause }
312            }
313            Message::Unknown => Self::Unknown,
314        }
315    }
316}
317
318#[derive(Clone, uniffi::Record)]
319pub struct Reaction {
320    pub key: String,
321    pub senders: Vec<ReactionSenderData>,
322}
323
324#[derive(Clone, uniffi::Record)]
325pub struct ReactionSenderData {
326    pub sender_id: String,
327    pub timestamp: Timestamp,
328}
329
330#[derive(Clone, uniffi::Enum)]
331pub enum MembershipChange {
332    None,
333    Error,
334    Joined,
335    Left,
336    Banned,
337    Unbanned,
338    Kicked,
339    Invited,
340    KickedAndBanned,
341    InvitationAccepted,
342    InvitationRejected,
343    InvitationRevoked,
344    Knocked,
345    KnockAccepted,
346    KnockRetracted,
347    KnockDenied,
348    NotImplemented,
349}
350
351impl From<matrix_sdk_ui::timeline::MembershipChange> for MembershipChange {
352    fn from(membership_change: matrix_sdk_ui::timeline::MembershipChange) -> Self {
353        use matrix_sdk_ui::timeline::MembershipChange as Change;
354        match membership_change {
355            Change::None => Self::None,
356            Change::Error => Self::Error,
357            Change::Joined => Self::Joined,
358            Change::Left => Self::Left,
359            Change::Banned => Self::Banned,
360            Change::Unbanned => Self::Unbanned,
361            Change::Kicked => Self::Kicked,
362            Change::Invited => Self::Invited,
363            Change::KickedAndBanned => Self::KickedAndBanned,
364            Change::InvitationAccepted => Self::InvitationAccepted,
365            Change::InvitationRejected => Self::InvitationRejected,
366            Change::InvitationRevoked => Self::InvitationRevoked,
367            Change::Knocked => Self::Knocked,
368            Change::KnockAccepted => Self::KnockAccepted,
369            Change::KnockRetracted => Self::KnockRetracted,
370            Change::KnockDenied => Self::KnockDenied,
371            Change::NotImplemented => Self::NotImplemented,
372        }
373    }
374}
375
376#[derive(Clone, uniffi::Enum)]
377pub enum OtherState {
378    PolicyRuleRoom,
379    PolicyRuleServer,
380    PolicyRuleUser,
381    RoomAliases,
382    RoomAvatar { url: Option<String> },
383    RoomCanonicalAlias,
384    RoomCreate,
385    RoomEncryption,
386    RoomGuestAccess,
387    RoomHistoryVisibility,
388    RoomJoinRules,
389    RoomName { name: Option<String> },
390    RoomPinnedEvents { change: RoomPinnedEventsChange },
391    RoomPowerLevels { users: HashMap<String, i64>, previous: Option<HashMap<String, i64>> },
392    RoomServerAcl,
393    RoomThirdPartyInvite { display_name: Option<String> },
394    RoomTombstone,
395    RoomTopic { topic: Option<String> },
396    SpaceChild,
397    SpaceParent,
398    Custom { event_type: String },
399}
400
401impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherState {
402    fn from(content: &matrix_sdk_ui::timeline::AnyOtherFullStateEventContent) -> Self {
403        use matrix_sdk::ruma::events::FullStateEventContent as FullContent;
404        use matrix_sdk_ui::timeline::AnyOtherFullStateEventContent as Content;
405
406        match content {
407            Content::PolicyRuleRoom(_) => Self::PolicyRuleRoom,
408            Content::PolicyRuleServer(_) => Self::PolicyRuleServer,
409            Content::PolicyRuleUser(_) => Self::PolicyRuleUser,
410            Content::RoomAliases(_) => Self::RoomAliases,
411            Content::RoomAvatar(c) => {
412                let url = match c {
413                    FullContent::Original { content, .. } => {
414                        content.url.as_ref().map(ToString::to_string)
415                    }
416                    FullContent::Redacted(_) => None,
417                };
418                Self::RoomAvatar { url }
419            }
420            Content::RoomCanonicalAlias(_) => Self::RoomCanonicalAlias,
421            Content::RoomCreate(_) => Self::RoomCreate,
422            Content::RoomEncryption(_) => Self::RoomEncryption,
423            Content::RoomGuestAccess(_) => Self::RoomGuestAccess,
424            Content::RoomHistoryVisibility(_) => Self::RoomHistoryVisibility,
425            Content::RoomJoinRules(_) => Self::RoomJoinRules,
426            Content::RoomName(c) => {
427                let name = match c {
428                    FullContent::Original { content, .. } => Some(content.name.clone()),
429                    FullContent::Redacted(_) => None,
430                };
431                Self::RoomName { name }
432            }
433            Content::RoomPinnedEvents(c) => Self::RoomPinnedEvents { change: c.into() },
434            Content::RoomPowerLevels(c) => match c {
435                FullContent::Original { content, prev_content } => Self::RoomPowerLevels {
436                    users: power_level_user_changes(content, prev_content)
437                        .iter()
438                        .map(|(k, v)| (k.to_string(), *v))
439                        .collect(),
440                    previous: prev_content.as_ref().map(|prev_content| {
441                        prev_content.users.iter().map(|(k, &v)| (k.to_string(), v.into())).collect()
442                    }),
443                },
444                FullContent::Redacted(_) => {
445                    Self::RoomPowerLevels { users: Default::default(), previous: None }
446                }
447            },
448            Content::RoomServerAcl(_) => Self::RoomServerAcl,
449            Content::RoomThirdPartyInvite(c) => {
450                let display_name = match c {
451                    FullContent::Original { content, .. } => Some(content.display_name.clone()),
452                    FullContent::Redacted(_) => None,
453                };
454                Self::RoomThirdPartyInvite { display_name }
455            }
456            Content::RoomTombstone(_) => Self::RoomTombstone,
457            Content::RoomTopic(c) => {
458                let topic = match c {
459                    FullContent::Original { content, .. } => Some(content.topic.clone()),
460                    FullContent::Redacted(_) => None,
461                };
462                Self::RoomTopic { topic }
463            }
464            Content::SpaceChild(_) => Self::SpaceChild,
465            Content::SpaceParent(_) => Self::SpaceParent,
466            Content::_Custom { event_type, .. } => Self::Custom { event_type: event_type.clone() },
467        }
468    }
469}
470
471#[derive(Clone, uniffi::Record)]
472pub struct PollAnswer {
473    pub id: String,
474    pub text: String,
475}
476
477impl From<PollResult> for TimelineItemContent {
478    fn from(value: PollResult) -> Self {
479        TimelineItemContent::Poll {
480            question: value.question,
481            kind: PollKind::from(value.kind),
482            max_selections: value.max_selections,
483            answers: value
484                .answers
485                .into_iter()
486                .map(|i| PollAnswer { id: i.id, text: i.text })
487                .collect(),
488            votes: value.votes,
489            end_time: value.end_time.map(|t| t.into()),
490            has_been_edited: value.has_been_edited,
491        }
492    }
493}