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::{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 sender_key: String,
282 },
283 MegolmV1AesSha2 {
286 session_id: String,
288
289 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}