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