matrix_sdk_ffi/timeline/
msg_like.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;
18use matrix_sdk_ui::timeline::TimelineDetails;
19use ruma::events::{room::MediaSource as RumaMediaSource, EventContent};
20
21use super::{
22    content::{Reaction, TimelineItemContent},
23    reply::InReplyToDetails,
24};
25use crate::{
26    error::ClientError,
27    ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
28    timeline::{content::ReactionSenderData, ProfileDetails},
29    utils::Timestamp,
30};
31
32#[derive(Clone, uniffi::Enum)]
33pub enum MsgLikeKind {
34    /// An `m.room.message` event or extensible event, including edits.
35    Message { content: MessageContent },
36    /// An `m.sticker` event.
37    Sticker { body: String, info: ImageInfo, source: Arc<MediaSource> },
38    /// An `m.poll.start` event.
39    Poll {
40        question: String,
41        kind: PollKind,
42        max_selections: u64,
43        answers: Vec<PollAnswer>,
44        votes: HashMap<String, Vec<String>>,
45        end_time: Option<Timestamp>,
46        has_been_edited: bool,
47    },
48
49    /// A redacted message.
50    Redacted,
51
52    /// An `m.room.encrypted` event that could not be decrypted.
53    UnableToDecrypt { msg: EncryptedMessage },
54}
55
56/// A special kind of [`super::TimelineItemContent`] that groups together
57/// different room message types with their respective reactions and thread
58/// information.
59#[derive(Clone, uniffi::Record)]
60pub struct MsgLikeContent {
61    pub kind: MsgLikeKind,
62    pub reactions: Vec<Reaction>,
63    /// The event this message is replying to, if any.
64    pub in_reply_to: Option<Arc<InReplyToDetails>>,
65    /// Event ID of the thread root, if this is a message in a thread.
66    pub thread_root: Option<String>,
67    /// Details about the thread this message is the root of.
68    pub thread_summary: Option<Arc<ThreadSummary>>,
69}
70
71#[derive(Clone, uniffi::Record)]
72pub struct MessageContent {
73    pub msg_type: MessageType,
74    pub body: String,
75    pub is_edited: bool,
76    pub mentions: Option<Mentions>,
77}
78
79impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
80    type Error = (ClientError, String);
81
82    fn try_from(value: matrix_sdk_ui::timeline::MsgLikeContent) -> Result<Self, Self::Error> {
83        use matrix_sdk_ui::timeline::MsgLikeKind as Kind;
84
85        let reactions = value
86            .reactions
87            .iter()
88            .map(|(k, v)| Reaction {
89                key: k.to_owned(),
90                senders: v
91                    .into_iter()
92                    .map(|(sender_id, info)| ReactionSenderData {
93                        sender_id: sender_id.to_string(),
94                        timestamp: info.timestamp.into(),
95                    })
96                    .collect(),
97            })
98            .collect();
99
100        let in_reply_to = value.in_reply_to.map(|r| Arc::new(r.into()));
101
102        let thread_root = value.thread_root.map(|id| id.to_string());
103
104        let thread_summary = value.thread_summary.map(|t| Arc::new(t.into()));
105
106        Ok(match value.kind {
107            Kind::Message(message) => {
108                let msg_type = TryInto::<MessageType>::try_into(message.msgtype().clone())
109                    .map_err(|e| (e, message.msgtype().msgtype().to_owned()))?;
110
111                Self {
112                    kind: MsgLikeKind::Message {
113                        content: MessageContent {
114                            msg_type,
115                            body: message.body().to_owned(),
116                            is_edited: message.is_edited(),
117                            mentions: message.mentions().cloned().map(|m| m.into()),
118                        },
119                    },
120                    reactions,
121                    in_reply_to,
122                    thread_root,
123                    thread_summary,
124                }
125            }
126            Kind::Sticker(sticker) => {
127                let content = sticker.content();
128
129                let media_source = RumaMediaSource::from(content.source.clone());
130                media_source
131                    .verify()
132                    .map_err(|e| (e, sticker.content().event_type().to_string()))?;
133
134                let image_info = TryInto::<ImageInfo>::try_into(&content.info)
135                    .map_err(|e| (e, sticker.content().event_type().to_string()))?;
136
137                Self {
138                    kind: MsgLikeKind::Sticker {
139                        body: content.body.clone(),
140                        info: image_info,
141                        source: Arc::new(MediaSource { media_source }),
142                    },
143                    reactions,
144                    in_reply_to,
145                    thread_root,
146                    thread_summary,
147                }
148            }
149            Kind::Poll(poll_state) => {
150                let results = poll_state.results();
151
152                Self {
153                    kind: MsgLikeKind::Poll {
154                        question: results.question,
155                        kind: PollKind::from(results.kind),
156                        max_selections: results.max_selections,
157                        answers: results
158                            .answers
159                            .into_iter()
160                            .map(|i| PollAnswer { id: i.id, text: i.text })
161                            .collect(),
162                        votes: results.votes,
163                        end_time: results.end_time.map(|t| t.into()),
164                        has_been_edited: results.has_been_edited,
165                    },
166                    reactions,
167                    in_reply_to,
168                    thread_root,
169                    thread_summary,
170                }
171            }
172            Kind::Redacted => Self {
173                kind: MsgLikeKind::Redacted,
174                reactions,
175                in_reply_to,
176                thread_root,
177                thread_summary,
178            },
179            Kind::UnableToDecrypt(msg) => Self {
180                kind: MsgLikeKind::UnableToDecrypt { msg: EncryptedMessage::new(&msg) },
181                reactions,
182                in_reply_to,
183                thread_root,
184                thread_summary,
185            },
186        })
187    }
188}
189
190impl From<ruma::events::Mentions> for Mentions {
191    fn from(value: ruma::events::Mentions) -> Self {
192        Self {
193            user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(),
194            room: value.room,
195        }
196    }
197}
198
199#[derive(Clone, uniffi::Enum)]
200pub enum EncryptedMessage {
201    OlmV1Curve25519AesSha2 {
202        /// The Curve25519 key of the sender.
203        sender_key: String,
204    },
205    // Other fields not included because UniFFI doesn't have the concept of
206    // deprecated fields right now.
207    MegolmV1AesSha2 {
208        /// The ID of the session used to encrypt the message.
209        session_id: String,
210
211        /// What we know about what caused this UTD. E.g. was this event sent
212        /// when we were not a member of this room?
213        cause: UtdCause,
214    },
215    Unknown,
216}
217
218impl EncryptedMessage {
219    pub(crate) fn new(msg: &matrix_sdk_ui::timeline::EncryptedMessage) -> Self {
220        use matrix_sdk_ui::timeline::EncryptedMessage as Message;
221
222        match msg {
223            Message::OlmV1Curve25519AesSha2 { sender_key } => {
224                let sender_key = sender_key.clone();
225                Self::OlmV1Curve25519AesSha2 { sender_key }
226            }
227            Message::MegolmV1AesSha2 { session_id, cause, .. } => {
228                let session_id = session_id.clone();
229                Self::MegolmV1AesSha2 { session_id, cause: *cause }
230            }
231            Message::Unknown => Self::Unknown,
232        }
233    }
234}
235
236#[derive(Clone, uniffi::Record)]
237pub struct PollAnswer {
238    pub id: String,
239    pub text: String,
240}
241
242#[derive(Clone, uniffi::Object)]
243pub struct ThreadSummary {
244    pub latest_event: ThreadSummaryLatestEventDetails,
245}
246
247#[matrix_sdk_ffi_macros::export]
248impl ThreadSummary {
249    pub fn latest_event(&self) -> ThreadSummaryLatestEventDetails {
250        self.latest_event.clone()
251    }
252}
253
254#[derive(Clone, uniffi::Enum)]
255pub enum ThreadSummaryLatestEventDetails {
256    Unavailable,
257    Pending,
258    Ready { sender: String, sender_profile: ProfileDetails, content: TimelineItemContent },
259    Error { message: String },
260}
261
262impl From<matrix_sdk_ui::timeline::ThreadSummary> for ThreadSummary {
263    fn from(value: matrix_sdk_ui::timeline::ThreadSummary) -> Self {
264        let latest_event = match value.latest_event {
265            TimelineDetails::Unavailable => ThreadSummaryLatestEventDetails::Unavailable,
266            TimelineDetails::Pending => ThreadSummaryLatestEventDetails::Pending,
267            TimelineDetails::Ready(event) => ThreadSummaryLatestEventDetails::Ready {
268                content: event.content.into(),
269                sender: event.sender.to_string(),
270                sender_profile: (&event.sender_profile).into(),
271            },
272            TimelineDetails::Error(message) => {
273                ThreadSummaryLatestEventDetails::Error { message: message.to_string() }
274            }
275        };
276
277        Self { latest_event }
278    }
279}