1use 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 Message { content: MessageContent },
36 Sticker { body: String, info: ImageInfo, source: Arc<MediaSource> },
38 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 Redacted,
51
52 UnableToDecrypt { msg: EncryptedMessage },
54}
55
56#[derive(Clone, uniffi::Record)]
60pub struct MsgLikeContent {
61 pub kind: MsgLikeKind,
62 pub reactions: Vec<Reaction>,
63 pub in_reply_to: Option<Arc<InReplyToDetails>>,
65 pub thread_root: Option<String>,
67 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 sender_key: String,
204 },
205 MegolmV1AesSha2 {
208 session_id: String,
210
211 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}