1use bitflags::bitflags;
2use std::sync::Arc;
3
4use matrix_sdk::ruma::{OwnedEventId, UInt, event_id, events::room::message::MessageType};
5use matrix_sdk_ui::timeline::{
6 EventTimelineItem, MsgLikeKind, TimelineEventItemId, TimelineItem, TimelineItemContent,
7 TimelineItemKind, VirtualTimelineItem,
8};
9use serde::{Serialize, Serializer};
10
11use crate::{
12 events::timeline::TimelineKind,
13 room::frontend_events::{
14 msg_like::{FrontendStickerEventContent, SerializableReactions},
15 state_event::{
16 FrontendAnyOtherStateEventContentChange, FrontendMemberProfileChange,
17 FrontendRoomMembershipChange, FrontendStateEvent,
18 },
19 thread_summary::get_frontend_thread_summary,
20 timeline_item_id::FrontendTimelineEventItemId,
21 },
22 user::user_power_level::UserPowerLevels,
23 utils::get_or_fetch_event_sender,
24};
25
26use super::{
27 msg_like::{FrontendMsgLikeContent, FrontendMsgLikeKind},
28 virtual_event::FrontendVirtualTimelineItem,
29};
30
31#[derive(Debug, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct FrontendTimelineItem {
34 unique_id: String,
35 event_id: Option<OwnedEventId>,
36 #[serde(flatten)]
37 timeline_item_id: FrontendTimelineEventItemId,
38 #[serde(flatten)]
39 data: FrontendTimelineItemData,
40 timestamp: Option<UInt>, is_own: bool,
42 is_local: bool,
43 abilities: MessageAbilities,
44}
45
46#[allow(clippy::large_enum_variant)]
47#[derive(Debug, Serialize)]
48#[serde(
49 rename_all = "camelCase",
50 rename_all_fields = "camelCase",
51 tag = "kind",
52 content = "data"
53)]
54pub enum FrontendTimelineItemData {
55 MsgLike(FrontendMsgLikeContent),
56 Virtual(FrontendVirtualTimelineItem),
57 StateChange(FrontendStateEvent),
58 Error(FrontendTimelineErrorItem),
59 Call,
60}
61
62#[derive(Debug, Clone, Serialize)]
63#[serde(rename_all = "camelCase")]
64pub struct FrontendTimelineErrorItem {
65 error: String,
66}
67
68pub fn to_frontend_timeline_item(
69 item: &Arc<TimelineItem>,
70 timeline_kind: &TimelineKind,
71 user_power_levels: &UserPowerLevels,
72) -> Option<FrontendTimelineItem> {
73 let unique_id = item.unique_id().0.clone();
74 match item.kind() {
75 TimelineItemKind::Event(event_tl_item) => {
76 map_event_timeline_item(unique_id, event_tl_item, timeline_kind, user_power_levels)
77 }
78 TimelineItemKind::Virtual(event) => match event {
79 VirtualTimelineItem::DateDivider(timestamp) => Some(FrontendTimelineItem {
80 unique_id,
81 event_id: None,
82 timeline_item_id: TimelineEventItemId::EventId(
83 event_id!("$no_ids_for_virtual").to_owned(),
84 )
85 .into(),
86 data: FrontendTimelineItemData::Virtual(FrontendVirtualTimelineItem::DateDivider),
87 is_local: true,
88 is_own: true,
89 timestamp: Some(timestamp.0),
90 abilities: MessageAbilities::empty(),
91 }),
92 VirtualTimelineItem::ReadMarker => Some(FrontendTimelineItem {
93 unique_id,
94 event_id: None,
95 timeline_item_id: TimelineEventItemId::EventId(
96 event_id!("$no_ids_for_virtual").to_owned(),
97 )
98 .into(),
99 data: FrontendTimelineItemData::Virtual(FrontendVirtualTimelineItem::ReadMarker),
100 is_local: true,
101 is_own: true,
102 timestamp: None,
103 abilities: MessageAbilities::empty(),
104 }),
105 VirtualTimelineItem::TimelineStart => Some(FrontendTimelineItem {
106 unique_id,
107 event_id: None,
108 timeline_item_id: TimelineEventItemId::EventId(
109 event_id!("$no_ids_for_virtual").to_owned(),
110 )
111 .into(),
112 data: FrontendTimelineItemData::Virtual(FrontendVirtualTimelineItem::TimelineStart),
113 is_local: true,
114 is_own: true,
115 timestamp: None,
116 abilities: MessageAbilities::empty(),
117 }),
118 },
119 }
120}
121
122fn map_msg_event_content(content: MessageType) -> FrontendMsgLikeKind {
123 match content {
124 MessageType::Audio(c) => FrontendMsgLikeKind::Audio(c),
125 MessageType::File(c) => FrontendMsgLikeKind::File(c),
126 MessageType::Image(c) => FrontendMsgLikeKind::Image(c),
127 MessageType::Text(c) => FrontendMsgLikeKind::Text(c),
128 MessageType::Video(c) => FrontendMsgLikeKind::Video(c),
129 MessageType::Emote(c) => FrontendMsgLikeKind::Emote(c),
130 MessageType::Location(c) => FrontendMsgLikeKind::Location(c),
131 MessageType::Notice(c) => FrontendMsgLikeKind::Notice(c),
132 MessageType::ServerNotice(c) => FrontendMsgLikeKind::ServerNotice(c),
133 MessageType::VerificationRequest(c) => FrontendMsgLikeKind::VerificationRequest(c),
134 _ => FrontendMsgLikeKind::Unknown,
135 }
136}
137
138bitflags! {
139 #[derive(Copy, Clone, Debug)]
143 pub struct MessageAbilities: u8 {
144 const CanReact = 1 << 0;
146 const CanReplyTo = 1 << 1;
148 const CanEdit = 1 << 2;
150 const CanPin = 1 << 3;
152 const CanUnpin = 1 << 4;
154 const CanDelete = 1 << 5;
156 }
157}
158impl MessageAbilities {
159 pub fn from_user_power_and_event(
160 user_power_levels: &UserPowerLevels,
161 event_tl_item: &EventTimelineItem,
162 ) -> Self {
163 let mut abilities = Self::empty();
164 abilities.set(Self::CanEdit, event_tl_item.is_editable());
165 if event_tl_item.is_own() {
167 abilities.set(Self::CanDelete, user_power_levels._can_redact_own());
168 }
169 abilities.set(Self::CanReplyTo, event_tl_item.can_be_replied_to());
170 abilities.set(Self::CanPin, user_power_levels._can_pin());
171 abilities.set(Self::CanReact, user_power_levels._can_send_reaction());
176 abilities
177 }
178}
179
180impl Serialize for MessageAbilities {
181 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
182 where
183 S: Serializer,
184 {
185 use serde::ser::SerializeSeq;
186
187 let mut seq = serializer.serialize_seq(None)?;
188
189 if self.contains(MessageAbilities::CanReact) {
190 seq.serialize_element("canReact")?;
191 }
192 if self.contains(MessageAbilities::CanReplyTo) {
193 seq.serialize_element("canReplyTo")?;
194 }
195 if self.contains(MessageAbilities::CanEdit) {
196 seq.serialize_element("canEdit")?;
197 }
198 if self.contains(MessageAbilities::CanPin) {
199 seq.serialize_element("canPin")?;
200 }
201 if self.contains(MessageAbilities::CanUnpin) {
202 seq.serialize_element("canUnpin")?;
203 }
204 if self.contains(MessageAbilities::CanDelete) {
205 seq.serialize_element("canDelete")?;
206 }
207
208 seq.end()
209 }
210}
211
212pub(crate) fn map_event_timeline_item(
213 unique_id: String,
214 event_tl_item: &EventTimelineItem,
215 kind: &TimelineKind,
216 user_power_levels: &UserPowerLevels,
217) -> Option<FrontendTimelineItem> {
218 let timeline_item_id: FrontendTimelineEventItemId = event_tl_item.identifier().into();
219 let is_own = event_tl_item.is_own();
220 let is_local = event_tl_item.is_local_echo();
221 let timestamp = Some(event_tl_item.timestamp().get());
222 let sender = Some(get_or_fetch_event_sender(event_tl_item, Some(kind.clone())));
223 let sender_id = event_tl_item.sender().to_string();
224 let abilities = MessageAbilities::from_user_power_and_event(user_power_levels, event_tl_item);
225 let event_id = event_tl_item.event_id().map(|id| id.to_owned());
226 map_timeline_event_item_content(
227 event_tl_item.content(),
228 unique_id,
229 timeline_item_id,
230 is_own,
231 is_local,
232 timestamp,
233 sender,
234 sender_id,
235 abilities,
236 event_id,
237 )
238}
239
240#[allow(clippy::too_many_arguments)]
241pub(super) fn map_timeline_event_item_content(
242 timeline_item_content: &TimelineItemContent,
243 unique_id: String,
244 timeline_item_id: FrontendTimelineEventItemId,
245 is_own: bool,
246 is_local: bool,
247 timestamp: Option<UInt>,
248 sender: Option<String>,
249 sender_id: String,
250 abilities: MessageAbilities,
251 event_id: Option<OwnedEventId>,
252) -> Option<FrontendTimelineItem> {
253 match timeline_item_content {
254 TimelineItemContent::MsgLike(msg_like) => {
255 let in_reply_to_id = msg_like.in_reply_to.clone().map(|r| r.event_id);
256 let thread_root = msg_like.thread_root.clone();
257 let thread_summary = msg_like
258 .thread_summary
259 .clone()
260 .and_then(get_frontend_thread_summary);
261 match msg_like.kind.clone() {
262 MsgLikeKind::Message(message) => Some(FrontendTimelineItem {
263 unique_id,
264 event_id,
265 timeline_item_id,
266 is_local,
267 is_own,
268 timestamp,
269 abilities,
270 data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
271 edited: message.is_edited(),
272 reactions: SerializableReactions(msg_like.reactions.clone()),
273 sender_id,
274 sender,
275 thread_root,
276 thread_summary,
277 in_reply_to_id,
278 kind: map_msg_event_content(message.msgtype().clone()),
279 }),
280 }),
281 MsgLikeKind::Sticker(sticker) => Some(FrontendTimelineItem {
282 unique_id,
283 event_id,
284 timeline_item_id,
285 is_local,
286 is_own,
287 timestamp,
288 abilities,
289 data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
290 edited: false,
291 reactions: SerializableReactions(msg_like.reactions.clone()),
292 sender_id,
293 sender,
294 thread_root,
295 thread_summary,
296 in_reply_to_id,
297 kind: FrontendMsgLikeKind::Sticker(Box::new(
298 FrontendStickerEventContent::from(sticker.content().clone()),
299 )),
300 }),
301 }),
302 MsgLikeKind::Redacted => Some(FrontendTimelineItem {
303 unique_id,
304 event_id,
305 timeline_item_id,
306 is_local,
307 is_own,
308 timestamp,
309 abilities,
310 data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
311 edited: true,
312 reactions: SerializableReactions(msg_like.reactions.clone()),
313 sender_id,
314 sender,
315 thread_root,
316 thread_summary,
317 in_reply_to_id,
318 kind: FrontendMsgLikeKind::Redacted,
319 }),
320 }),
321 MsgLikeKind::UnableToDecrypt(_) => Some(FrontendTimelineItem {
322 unique_id,
323 event_id,
324 timeline_item_id,
325 is_local,
326 is_own,
327 timestamp,
328 abilities,
329 data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
330 edited: false,
331 reactions: SerializableReactions(msg_like.reactions.clone()),
332 sender_id,
333 sender,
334 thread_root,
335 thread_summary,
336 in_reply_to_id,
337 kind: FrontendMsgLikeKind::UnableToDecrypt,
338 }),
339 }),
340 MsgLikeKind::LiveLocation(_) => None,
342
343 MsgLikeKind::Poll(_) => Some(FrontendTimelineItem {
344 unique_id,
345 event_id,
346 timeline_item_id,
347 is_local,
348 is_own,
349 timestamp,
350 abilities,
351 data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
352 edited: false,
353 reactions: SerializableReactions(msg_like.reactions.clone()),
354 sender_id,
355 sender,
356 thread_root,
357 thread_summary,
358 in_reply_to_id,
359 kind: FrontendMsgLikeKind::Poll,
360 }),
361 }),
362
363 MsgLikeKind::Other(_) => Some(FrontendTimelineItem {
364 unique_id,
365 event_id,
366 timeline_item_id,
367 is_local,
368 is_own,
369 timestamp,
370 abilities,
371 data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
372 edited: false,
373 reactions: SerializableReactions(msg_like.reactions.clone()),
374 sender_id,
375 sender,
376 thread_root,
377 thread_summary,
378 in_reply_to_id,
379 kind: FrontendMsgLikeKind::Unknown,
380 }),
381 }),
382 }
383 }
384 TimelineItemContent::OtherState(state) => Some(FrontendTimelineItem {
385 unique_id,
386 event_id,
387 timeline_item_id,
388 is_local,
389 is_own,
390 timestamp,
391 abilities,
392 data: FrontendTimelineItemData::StateChange(FrontendStateEvent::OtherState(
393 FrontendAnyOtherStateEventContentChange::from(state.content().clone()),
394 )),
395 }),
396 TimelineItemContent::MembershipChange(change) => Some(FrontendTimelineItem {
397 unique_id,
398 event_id,
399 timeline_item_id,
400 is_local,
401 is_own,
402 timestamp,
403 abilities,
404 data: FrontendTimelineItemData::StateChange(FrontendStateEvent::MembershipChange(
405 FrontendRoomMembershipChange::from(change.clone()),
406 )),
407 }),
408
409 TimelineItemContent::ProfileChange(change) => Some(FrontendTimelineItem {
410 unique_id,
411 event_id,
412 timeline_item_id,
413 is_local,
414 is_own,
415 timestamp,
416 abilities,
417 data: FrontendTimelineItemData::StateChange(FrontendStateEvent::ProfileChange(
418 FrontendMemberProfileChange::from(change.clone()),
419 )),
420 }),
421
422 TimelineItemContent::RtcNotification { .. } | TimelineItemContent::CallInvite => {
423 Some(FrontendTimelineItem {
424 unique_id,
425 event_id,
426 timeline_item_id,
427 is_local,
428 is_own,
429 timestamp,
430 abilities,
431 data: FrontendTimelineItemData::Call,
432 })
433 }
434
435 TimelineItemContent::FailedToParseMessageLike {
436 event_type: _,
437 error,
438 } => Some(FrontendTimelineItem {
439 unique_id,
440 event_id: None,
441 timeline_item_id,
442 data: FrontendTimelineItemData::Error(FrontendTimelineErrorItem {
443 error: error.to_string(),
444 }),
445 is_local: true,
446 is_own: true,
447 timestamp: None,
448 abilities,
449 }),
450
451 TimelineItemContent::FailedToParseState {
452 state_key: _,
453 event_type: _,
454 error,
455 } => Some(FrontendTimelineItem {
456 unique_id,
457 event_id: None,
458 timeline_item_id,
459 data: FrontendTimelineItemData::Error(FrontendTimelineErrorItem {
460 error: error.to_string(),
461 }),
462 is_local: true,
463 is_own: true,
464 timestamp: None,
465 abilities,
466 }),
467 }
468}