1use 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 sender_key: String,
286 },
287 MegolmV1AesSha2 {
290 session_id: String,
292
293 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}