1use matrix_sdk_common::deserialized_responses::TimelineEvent;
5use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId};
6#[cfg(feature = "e2e-encryption")]
7use ruma::{
8 UserId,
9 events::{
10 AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
11 call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
12 poll::unstable_start::SyncUnstablePollStartEvent,
13 relation::RelationType,
14 room::{
15 member::{MembershipState, SyncRoomMemberEvent},
16 message::{MessageType, SyncRoomMessageEvent},
17 power_levels::RoomPowerLevels,
18 },
19 sticker::SyncStickerEvent,
20 },
21};
22use serde::{Deserialize, Serialize};
23
24use crate::{MinimalRoomMemberEvent, store::SerializableEventContent};
25
26#[derive(Debug, Default, Clone, Serialize, Deserialize)]
28pub enum LatestEventValue {
29 #[default]
31 None,
32
33 Remote(RemoteLatestEventValue),
35
36 LocalIsSending(LocalLatestEventValue),
38
39 LocalCannotBeSent(LocalLatestEventValue),
42}
43
44pub type RemoteLatestEventValue = TimelineEvent;
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct LocalLatestEventValue {
51 pub timestamp: MilliSecondsSinceUnixEpoch,
53
54 pub content: SerializableEventContent,
56}
57
58#[cfg(feature = "e2e-encryption")]
63#[derive(Debug)]
64pub enum PossibleLatestEvent<'a> {
65 YesRoomMessage(&'a SyncRoomMessageEvent),
67 YesSticker(&'a SyncStickerEvent),
69 YesPoll(&'a SyncUnstablePollStartEvent),
71
72 YesCallInvite(&'a SyncCallInviteEvent),
74
75 YesCallNotify(&'a SyncCallNotifyEvent),
77
78 YesKnockedStateEvent(&'a SyncRoomMemberEvent),
81
82 NoUnsupportedEventType,
86 NoUnsupportedMessageLikeType,
88 NoEncrypted,
90}
91
92#[cfg(feature = "e2e-encryption")]
95pub fn is_suitable_for_latest_event<'a>(
96 event: &'a AnySyncTimelineEvent,
97 power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>,
98) -> PossibleLatestEvent<'a> {
99 match event {
100 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
102 if let Some(original_message) = message.as_original() {
103 if let MessageType::VerificationRequest(_) = original_message.content.msgtype {
105 return PossibleLatestEvent::NoUnsupportedMessageLikeType;
106 }
107
108 let is_replacement =
110 original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
111 if let Some(relation_type) = relates_to.rel_type() {
112 relation_type == RelationType::Replacement
113 } else {
114 false
115 }
116 });
117
118 if is_replacement {
119 PossibleLatestEvent::NoUnsupportedMessageLikeType
120 } else {
121 PossibleLatestEvent::YesRoomMessage(message)
122 }
123 } else {
124 PossibleLatestEvent::YesRoomMessage(message)
125 }
126 }
127
128 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
129 PossibleLatestEvent::YesPoll(poll)
130 }
131
132 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(invite)) => {
133 PossibleLatestEvent::YesCallInvite(invite)
134 }
135
136 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(notify)) => {
137 PossibleLatestEvent::YesCallNotify(notify)
138 }
139
140 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(sticker)) => {
141 PossibleLatestEvent::YesSticker(sticker)
142 }
143
144 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
146 PossibleLatestEvent::NoEncrypted
147 }
148
149 AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType,
155
156 AnySyncTimelineEvent::State(state) => {
158 if let AnySyncStateEvent::RoomMember(member) = state
161 && matches!(member.membership(), MembershipState::Knock)
162 {
163 let can_accept_or_decline_knocks = match power_levels_info {
164 Some((own_user_id, room_power_levels)) => {
165 room_power_levels.user_can_invite(own_user_id)
166 || room_power_levels.user_can_kick(own_user_id)
167 }
168 _ => false,
169 };
170
171 if can_accept_or_decline_knocks {
174 return PossibleLatestEvent::YesKnockedStateEvent(member);
175 }
176 }
177 PossibleLatestEvent::NoUnsupportedEventType
178 }
179 }
180}
181
182#[derive(Clone, Debug, Serialize)]
202pub struct LatestEvent {
203 event: TimelineEvent,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 sender_profile: Option<MinimalRoomMemberEvent>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
212 sender_name_is_ambiguous: Option<bool>,
213}
214
215#[derive(Deserialize)]
216struct SerializedLatestEvent {
217 event: TimelineEvent,
219
220 #[serde(skip_serializing_if = "Option::is_none")]
222 sender_profile: Option<MinimalRoomMemberEvent>,
223
224 #[serde(skip_serializing_if = "Option::is_none")]
226 sender_name_is_ambiguous: Option<bool>,
227}
228
229impl<'de> Deserialize<'de> for LatestEvent {
232 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
233 where
234 D: serde::Deserializer<'de>,
235 {
236 let raw: Box<serde_json::value::RawValue> = Box::deserialize(deserializer)?;
237
238 let mut variant_errors = Vec::new();
239
240 match serde_json::from_str::<SerializedLatestEvent>(raw.get()) {
241 Ok(value) => {
242 return Ok(LatestEvent {
243 event: value.event,
244 sender_profile: value.sender_profile,
245 sender_name_is_ambiguous: value.sender_name_is_ambiguous,
246 });
247 }
248 Err(err) => variant_errors.push(err),
249 }
250
251 match serde_json::from_str::<TimelineEvent>(raw.get()) {
252 Ok(value) => {
253 return Ok(LatestEvent {
254 event: value,
255 sender_profile: None,
256 sender_name_is_ambiguous: None,
257 });
258 }
259 Err(err) => variant_errors.push(err),
260 }
261
262 Err(serde::de::Error::custom(format!(
263 "data did not match any variant of serialized LatestEvent (using serde_json). \
264 Observed errors: {variant_errors:?}"
265 )))
266 }
267}
268
269impl LatestEvent {
270 pub fn new(event: TimelineEvent) -> Self {
272 Self { event, sender_profile: None, sender_name_is_ambiguous: None }
273 }
274
275 pub fn new_with_sender_details(
277 event: TimelineEvent,
278 sender_profile: Option<MinimalRoomMemberEvent>,
279 sender_name_is_ambiguous: Option<bool>,
280 ) -> Self {
281 Self { event, sender_profile, sender_name_is_ambiguous }
282 }
283
284 pub fn into_event(self) -> TimelineEvent {
286 self.event
287 }
288
289 pub fn event(&self) -> &TimelineEvent {
291 &self.event
292 }
293
294 pub fn event_mut(&mut self) -> &mut TimelineEvent {
296 &mut self.event
297 }
298
299 pub fn event_id(&self) -> Option<OwnedEventId> {
301 self.event.event_id()
302 }
303
304 pub fn has_sender_profile(&self) -> bool {
306 self.sender_profile.is_some()
307 }
308
309 pub fn sender_display_name(&self) -> Option<&str> {
312 self.sender_profile.as_ref().and_then(|profile| {
313 profile.as_original().and_then(|event| event.content.displayname.as_deref())
314 })
315 }
316
317 pub fn sender_name_ambiguous(&self) -> Option<bool> {
321 self.sender_name_is_ambiguous
322 }
323
324 pub fn sender_avatar_url(&self) -> Option<&MxcUri> {
327 self.sender_profile.as_ref().and_then(|profile| {
328 profile.as_original().and_then(|event| event.content.avatar_url.as_deref())
329 })
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 #[cfg(feature = "e2e-encryption")]
336 use std::collections::BTreeMap;
337
338 #[cfg(feature = "e2e-encryption")]
339 use assert_matches::assert_matches;
340 #[cfg(feature = "e2e-encryption")]
341 use assert_matches2::assert_let;
342 use matrix_sdk_common::deserialized_responses::TimelineEvent;
343 use ruma::serde::Raw;
344 #[cfg(feature = "e2e-encryption")]
345 use ruma::{
346 MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
347 events::{
348 AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
349 Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
350 RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
351 call::{
352 SessionDescription,
353 invite::{CallInviteEventContent, SyncCallInviteEvent},
354 notify::{
355 ApplicationType, CallNotifyEventContent, NotifyType, SyncCallNotifyEvent,
356 },
357 },
358 poll::{
359 unstable_response::{
360 SyncUnstablePollResponseEvent, UnstablePollResponseEventContent,
361 },
362 unstable_start::{
363 NewUnstablePollStartEventContent, SyncUnstablePollStartEvent,
364 UnstablePollAnswer, UnstablePollStartContentBlock,
365 },
366 },
367 relation::Replacement,
368 room::{
369 ImageInfo, MediaSource,
370 encrypted::{
371 EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
372 SyncRoomEncryptedEvent,
373 },
374 message::{
375 ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
376 Relation, RoomMessageEventContent, SyncRoomMessageEvent,
377 },
378 topic::{RoomTopicEventContent, SyncRoomTopicEvent},
379 },
380 sticker::{StickerEventContent, SyncStickerEvent},
381 },
382 owned_event_id, owned_mxc_uri, owned_user_id,
383 };
384 use serde_json::json;
385
386 use super::LatestEvent;
387 #[cfg(feature = "e2e-encryption")]
388 use super::{PossibleLatestEvent, is_suitable_for_latest_event};
389
390 #[cfg(feature = "e2e-encryption")]
391 #[test]
392 fn test_room_messages_are_suitable() {
393 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
394 SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
395 content: RoomMessageEventContent::new(MessageType::Image(
396 ImageMessageEventContent::new(
397 "".to_owned(),
398 MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")),
399 ),
400 )),
401 event_id: owned_event_id!("$1"),
402 sender: owned_user_id!("@a:b.c"),
403 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
404 unsigned: MessageLikeUnsigned::new(),
405 }),
406 ));
407 assert_let!(
408 PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
409 is_suitable_for_latest_event(&event, None)
410 );
411
412 assert_eq!(m.content.msgtype.msgtype(), "m.image");
413 }
414
415 #[cfg(feature = "e2e-encryption")]
416 #[test]
417 fn test_polls_are_suitable() {
418 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(
419 SyncUnstablePollStartEvent::Original(OriginalSyncMessageLikeEvent {
420 content: NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
421 "do you like rust?",
422 vec![UnstablePollAnswer::new("id", "yes")].try_into().unwrap(),
423 ))
424 .into(),
425 event_id: owned_event_id!("$1"),
426 sender: owned_user_id!("@a:b.c"),
427 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
428 unsigned: MessageLikeUnsigned::new(),
429 }),
430 ));
431 assert_let!(
432 PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
433 is_suitable_for_latest_event(&event, None)
434 );
435
436 assert_eq!(m.content.poll_start().question.text, "do you like rust?");
437 }
438
439 #[cfg(feature = "e2e-encryption")]
440 #[test]
441 fn test_call_invites_are_suitable() {
442 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(
443 SyncCallInviteEvent::Original(OriginalSyncMessageLikeEvent {
444 content: CallInviteEventContent::new(
445 "call_id".into(),
446 UInt::new(123).unwrap(),
447 SessionDescription::new("".into(), "".into()),
448 VoipVersionId::V1,
449 ),
450 event_id: owned_event_id!("$1"),
451 sender: owned_user_id!("@a:b.c"),
452 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
453 unsigned: MessageLikeUnsigned::new(),
454 }),
455 ));
456 assert_let!(
457 PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
458 is_suitable_for_latest_event(&event, None)
459 );
460 }
461
462 #[cfg(feature = "e2e-encryption")]
463 #[test]
464 fn test_call_notifications_are_suitable() {
465 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(
466 SyncCallNotifyEvent::Original(OriginalSyncMessageLikeEvent {
467 content: CallNotifyEventContent::new(
468 "call_id".into(),
469 ApplicationType::Call,
470 NotifyType::Ring,
471 Mentions::new(),
472 ),
473 event_id: owned_event_id!("$1"),
474 sender: owned_user_id!("@a:b.c"),
475 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
476 unsigned: MessageLikeUnsigned::new(),
477 }),
478 ));
479 assert_let!(
480 PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
481 is_suitable_for_latest_event(&event, None)
482 );
483 }
484
485 #[cfg(feature = "e2e-encryption")]
486 #[test]
487 fn test_stickers_are_suitable() {
488 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
489 SyncStickerEvent::Original(OriginalSyncMessageLikeEvent {
490 content: StickerEventContent::new(
491 "sticker!".to_owned(),
492 ImageInfo::new(),
493 owned_mxc_uri!("mxc://example.com/1"),
494 ),
495 event_id: owned_event_id!("$1"),
496 sender: owned_user_id!("@a:b.c"),
497 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
498 unsigned: MessageLikeUnsigned::new(),
499 }),
500 ));
501
502 assert_matches!(
503 is_suitable_for_latest_event(&event, None),
504 PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
505 );
506 }
507
508 #[cfg(feature = "e2e-encryption")]
509 #[test]
510 fn test_different_types_of_messagelike_are_unsuitable() {
511 let event =
512 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollResponse(
513 SyncUnstablePollResponseEvent::Original(OriginalSyncMessageLikeEvent {
514 content: UnstablePollResponseEventContent::new(
515 vec![String::from("option1")],
516 owned_event_id!("$1"),
517 ),
518 event_id: owned_event_id!("$2"),
519 sender: owned_user_id!("@a:b.c"),
520 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
521 unsigned: MessageLikeUnsigned::new(),
522 }),
523 ));
524
525 assert_matches!(
526 is_suitable_for_latest_event(&event, None),
527 PossibleLatestEvent::NoUnsupportedMessageLikeType
528 );
529 }
530
531 #[cfg(feature = "e2e-encryption")]
532 #[test]
533 fn test_redacted_messages_are_suitable() {
534 let room_redaction_event = serde_json::from_value(json!({
536 "content": {},
537 "event_id": "$redaction",
538 "sender": "@x:y.za",
539 "origin_server_ts": 223543,
540 "unsigned": { "reason": "foo" }
541 }))
542 .unwrap();
543
544 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
545 SyncRoomMessageEvent::Redacted(RedactedSyncMessageLikeEvent {
546 content: RedactedRoomMessageEventContent::new(),
547 event_id: owned_event_id!("$1"),
548 sender: owned_user_id!("@a:b.c"),
549 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
550 unsigned: RedactedUnsigned::new(room_redaction_event),
551 }),
552 ));
553
554 assert_matches!(
555 is_suitable_for_latest_event(&event, None),
556 PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
557 );
558 }
559
560 #[cfg(feature = "e2e-encryption")]
561 #[test]
562 fn test_encrypted_messages_are_unsuitable() {
563 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
564 SyncRoomEncryptedEvent::Original(OriginalSyncMessageLikeEvent {
565 content: RoomEncryptedEventContent::new(
566 EncryptedEventScheme::OlmV1Curve25519AesSha2(
567 OlmV1Curve25519AesSha2Content::new(BTreeMap::new(), "".to_owned()),
568 ),
569 None,
570 ),
571 event_id: owned_event_id!("$1"),
572 sender: owned_user_id!("@a:b.c"),
573 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
574 unsigned: MessageLikeUnsigned::new(),
575 }),
576 ));
577
578 assert_matches!(
579 is_suitable_for_latest_event(&event, None),
580 PossibleLatestEvent::NoEncrypted
581 );
582 }
583
584 #[cfg(feature = "e2e-encryption")]
585 #[test]
586 fn test_state_events_are_unsuitable() {
587 let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
588 SyncRoomTopicEvent::Original(OriginalSyncStateEvent {
589 content: RoomTopicEventContent::new("".to_owned()),
590 event_id: owned_event_id!("$1"),
591 sender: owned_user_id!("@a:b.c"),
592 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
593 unsigned: StateUnsigned::new(),
594 state_key: EmptyStateKey,
595 }),
596 ));
597
598 assert_matches!(
599 is_suitable_for_latest_event(&event, None),
600 PossibleLatestEvent::NoUnsupportedEventType
601 );
602 }
603
604 #[cfg(feature = "e2e-encryption")]
605 #[test]
606 fn test_replacement_events_are_unsuitable() {
607 let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
608 event_content.relates_to = Some(Relation::Replacement(Replacement::new(
609 owned_event_id!("$1"),
610 RoomMessageEventContent::text_plain("Hello, world!").into(),
611 )));
612
613 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
614 SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
615 content: event_content,
616 event_id: owned_event_id!("$2"),
617 sender: owned_user_id!("@a:b.c"),
618 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
619 unsigned: MessageLikeUnsigned::new(),
620 }),
621 ));
622
623 assert_matches!(
624 is_suitable_for_latest_event(&event, None),
625 PossibleLatestEvent::NoUnsupportedMessageLikeType
626 );
627 }
628
629 #[cfg(feature = "e2e-encryption")]
630 #[test]
631 fn test_verification_requests_are_unsuitable() {
632 use ruma::{device_id, events::room::message::KeyVerificationRequestEventContent, user_id};
633
634 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
635 SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
636 content: RoomMessageEventContent::new(MessageType::VerificationRequest(
637 KeyVerificationRequestEventContent::new(
638 "body".to_owned(),
639 vec![],
640 device_id!("device_id").to_owned(),
641 user_id!("@user_id:example.com").to_owned(),
642 ),
643 )),
644 event_id: owned_event_id!("$1"),
645 sender: owned_user_id!("@a:b.c"),
646 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(123).unwrap()),
647 unsigned: MessageLikeUnsigned::new(),
648 }),
649 ));
650
651 assert_let!(
652 PossibleLatestEvent::NoUnsupportedMessageLikeType =
653 is_suitable_for_latest_event(&event, None)
654 );
655 }
656
657 #[test]
658 fn test_deserialize_latest_event() {
659 #[derive(Debug, serde::Serialize, serde::Deserialize)]
660 struct TestStruct {
661 latest_event: LatestEvent,
662 }
663
664 let event = TimelineEvent::from_plaintext(
665 Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
666 );
667
668 let initial = TestStruct {
669 latest_event: LatestEvent {
670 event: event.clone(),
671 sender_profile: None,
672 sender_name_is_ambiguous: None,
673 },
674 };
675
676 let serialized = serde_json::to_value(&initial).unwrap();
678 assert_eq!(
679 serialized,
680 json!({
681 "latest_event": {
682 "event": {
683 "kind": {
684 "PlainText": {
685 "event": {
686 "event_id": "$1"
687 }
688 }
689 },
690 "thread_summary": "None",
691 }
692 }
693 })
694 );
695
696 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
698 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
699 assert!(deserialized.latest_event.sender_profile.is_none());
700 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
701
702 let serialized = json!({
704 "latest_event": {
705 "event": {
706 "encryption_info": null,
707 "event": {
708 "event_id": "$1"
709 }
710 },
711 }
712 });
713
714 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
715 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
716 assert!(deserialized.latest_event.sender_profile.is_none());
717 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
718
719 let serialized = json!({
721 "latest_event": event
722 });
723
724 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
725 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
726 assert!(deserialized.latest_event.sender_profile.is_none());
727 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
728 }
729}