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,
12 poll::unstable_start::SyncUnstablePollStartEvent,
13 relation::RelationType,
14 room::{
15 member::{MembershipState, SyncRoomMemberEvent},
16 message::{MessageType, SyncRoomMessageEvent},
17 power_levels::RoomPowerLevels,
18 },
19 rtc::notification::SyncRtcNotificationEvent,
20 sticker::SyncStickerEvent,
21 },
22};
23use serde::{Deserialize, Serialize};
24
25use crate::{MinimalRoomMemberEvent, store::SerializableEventContent};
26
27#[derive(Debug, Default, Clone, Serialize, Deserialize)]
29pub enum LatestEventValue {
30 #[default]
32 None,
33
34 Remote(RemoteLatestEventValue),
36
37 LocalIsSending(LocalLatestEventValue),
39
40 LocalCannotBeSent(LocalLatestEventValue),
43}
44
45impl LatestEventValue {
46 pub fn timestamp(&self) -> Option<MilliSecondsSinceUnixEpoch> {
58 match self {
59 Self::None => None,
60 Self::Remote(remote_latest_event_value) => remote_latest_event_value.timestamp(),
61 Self::LocalIsSending(LocalLatestEventValue { timestamp, .. })
62 | Self::LocalCannotBeSent(LocalLatestEventValue { timestamp, .. }) => Some(*timestamp),
63 }
64 }
65
66 pub fn is_local(&self) -> bool {
72 match self {
73 Self::LocalIsSending(_) | Self::LocalCannotBeSent(_) => true,
74 Self::None | Self::Remote(_) => false,
75 }
76 }
77}
78
79pub type RemoteLatestEventValue = TimelineEvent;
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct LocalLatestEventValue {
86 pub timestamp: MilliSecondsSinceUnixEpoch,
88
89 pub content: SerializableEventContent,
91}
92
93#[cfg(test)]
94mod tests_latest_event_value {
95 use ruma::{
96 MilliSecondsSinceUnixEpoch,
97 events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
98 serde::Raw,
99 uint,
100 };
101 use serde_json::json;
102
103 use super::{LatestEventValue, LocalLatestEventValue, RemoteLatestEventValue};
104 use crate::store::SerializableEventContent;
105
106 #[test]
107 fn test_timestamp_with_none() {
108 let value = LatestEventValue::None;
109
110 assert_eq!(value.timestamp(), None);
111 }
112
113 #[test]
114 fn test_timestamp_with_remote() {
115 let value = LatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
116 Raw::from_json_string(
117 json!({
118 "content": RoomMessageEventContent::text_plain("raclette"),
119 "type": "m.room.message",
120 "event_id": "$ev0",
121 "room_id": "!r0",
122 "origin_server_ts": 42,
123 "sender": "@mnt_io:matrix.org",
124 })
125 .to_string(),
126 )
127 .unwrap(),
128 ));
129
130 assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
131 }
132
133 #[test]
134 fn test_timestamp_with_local_is_sending() {
135 let value = LatestEventValue::LocalIsSending(LocalLatestEventValue {
136 timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
137 content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
138 RoomMessageEventContent::text_plain("raclette"),
139 ))
140 .unwrap(),
141 });
142
143 assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
144 }
145
146 #[test]
147 fn test_timestamp_with_local_cannot_be_sent() {
148 let value = LatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
149 timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
150 content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
151 RoomMessageEventContent::text_plain("raclette"),
152 ))
153 .unwrap(),
154 });
155
156 assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
157 }
158}
159
160#[cfg(feature = "e2e-encryption")]
165#[derive(Debug)]
166pub enum PossibleLatestEvent<'a> {
167 YesRoomMessage(&'a SyncRoomMessageEvent),
169 YesSticker(&'a SyncStickerEvent),
171 YesPoll(&'a SyncUnstablePollStartEvent),
173
174 YesCallInvite(&'a SyncCallInviteEvent),
176
177 YesRtcNotification(&'a SyncRtcNotificationEvent),
179
180 YesKnockedStateEvent(&'a SyncRoomMemberEvent),
183
184 NoUnsupportedEventType,
188 NoUnsupportedMessageLikeType,
190 NoEncrypted,
192}
193
194#[cfg(feature = "e2e-encryption")]
197pub fn is_suitable_for_latest_event<'a>(
198 event: &'a AnySyncTimelineEvent,
199 power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>,
200) -> PossibleLatestEvent<'a> {
201 match event {
202 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
204 if let Some(original_message) = message.as_original() {
205 if let MessageType::VerificationRequest(_) = original_message.content.msgtype {
207 return PossibleLatestEvent::NoUnsupportedMessageLikeType;
208 }
209
210 let is_replacement =
212 original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
213 if let Some(relation_type) = relates_to.rel_type() {
214 relation_type == RelationType::Replacement
215 } else {
216 false
217 }
218 });
219
220 if is_replacement {
221 PossibleLatestEvent::NoUnsupportedMessageLikeType
222 } else {
223 PossibleLatestEvent::YesRoomMessage(message)
224 }
225 } else {
226 PossibleLatestEvent::YesRoomMessage(message)
227 }
228 }
229
230 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
231 PossibleLatestEvent::YesPoll(poll)
232 }
233
234 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(invite)) => {
235 PossibleLatestEvent::YesCallInvite(invite)
236 }
237
238 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RtcNotification(notify)) => {
239 PossibleLatestEvent::YesRtcNotification(notify)
240 }
241
242 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(sticker)) => {
243 PossibleLatestEvent::YesSticker(sticker)
244 }
245
246 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
248 PossibleLatestEvent::NoEncrypted
249 }
250
251 AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType,
257
258 AnySyncTimelineEvent::State(state) => {
260 if let AnySyncStateEvent::RoomMember(member) = state
263 && matches!(member.membership(), MembershipState::Knock)
264 {
265 let can_accept_or_decline_knocks = match power_levels_info {
266 Some((own_user_id, room_power_levels)) => {
267 room_power_levels.user_can_invite(own_user_id)
268 || room_power_levels.user_can_kick(own_user_id)
269 }
270 _ => false,
271 };
272
273 if can_accept_or_decline_knocks {
276 return PossibleLatestEvent::YesKnockedStateEvent(member);
277 }
278 }
279 PossibleLatestEvent::NoUnsupportedEventType
280 }
281 }
282}
283
284#[derive(Clone, Debug, Serialize)]
304pub struct LatestEvent {
305 event: TimelineEvent,
307
308 #[serde(skip_serializing_if = "Option::is_none")]
310 sender_profile: Option<MinimalRoomMemberEvent>,
311
312 #[serde(skip_serializing_if = "Option::is_none")]
314 sender_name_is_ambiguous: Option<bool>,
315}
316
317#[derive(Deserialize)]
318struct SerializedLatestEvent {
319 event: TimelineEvent,
321
322 #[serde(skip_serializing_if = "Option::is_none")]
324 sender_profile: Option<MinimalRoomMemberEvent>,
325
326 #[serde(skip_serializing_if = "Option::is_none")]
328 sender_name_is_ambiguous: Option<bool>,
329}
330
331impl<'de> Deserialize<'de> for LatestEvent {
334 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
335 where
336 D: serde::Deserializer<'de>,
337 {
338 let raw: Box<serde_json::value::RawValue> = Box::deserialize(deserializer)?;
339
340 let mut variant_errors = Vec::new();
341
342 match serde_json::from_str::<SerializedLatestEvent>(raw.get()) {
343 Ok(value) => {
344 return Ok(LatestEvent {
345 event: value.event,
346 sender_profile: value.sender_profile,
347 sender_name_is_ambiguous: value.sender_name_is_ambiguous,
348 });
349 }
350 Err(err) => variant_errors.push(err),
351 }
352
353 match serde_json::from_str::<TimelineEvent>(raw.get()) {
354 Ok(value) => {
355 return Ok(LatestEvent {
356 event: value,
357 sender_profile: None,
358 sender_name_is_ambiguous: None,
359 });
360 }
361 Err(err) => variant_errors.push(err),
362 }
363
364 Err(serde::de::Error::custom(format!(
365 "data did not match any variant of serialized LatestEvent (using serde_json). \
366 Observed errors: {variant_errors:?}"
367 )))
368 }
369}
370
371impl LatestEvent {
372 pub fn new(event: TimelineEvent) -> Self {
374 Self { event, sender_profile: None, sender_name_is_ambiguous: None }
375 }
376
377 pub fn new_with_sender_details(
379 event: TimelineEvent,
380 sender_profile: Option<MinimalRoomMemberEvent>,
381 sender_name_is_ambiguous: Option<bool>,
382 ) -> Self {
383 Self { event, sender_profile, sender_name_is_ambiguous }
384 }
385
386 pub fn into_event(self) -> TimelineEvent {
388 self.event
389 }
390
391 pub fn event(&self) -> &TimelineEvent {
393 &self.event
394 }
395
396 pub fn event_mut(&mut self) -> &mut TimelineEvent {
398 &mut self.event
399 }
400
401 pub fn event_id(&self) -> Option<OwnedEventId> {
403 self.event.event_id()
404 }
405
406 pub fn has_sender_profile(&self) -> bool {
408 self.sender_profile.is_some()
409 }
410
411 pub fn sender_display_name(&self) -> Option<&str> {
414 self.sender_profile.as_ref().and_then(|profile| {
415 profile.as_original().and_then(|event| event.content.displayname.as_deref())
416 })
417 }
418
419 pub fn sender_name_ambiguous(&self) -> Option<bool> {
423 self.sender_name_is_ambiguous
424 }
425
426 pub fn sender_avatar_url(&self) -> Option<&MxcUri> {
429 self.sender_profile.as_ref().and_then(|profile| {
430 profile.as_original().and_then(|event| event.content.avatar_url.as_deref())
431 })
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 #[cfg(feature = "e2e-encryption")]
438 use std::collections::BTreeMap;
439 use std::time::Duration;
440
441 #[cfg(feature = "e2e-encryption")]
442 use assert_matches::assert_matches;
443 #[cfg(feature = "e2e-encryption")]
444 use assert_matches2::assert_let;
445 use matrix_sdk_common::deserialized_responses::TimelineEvent;
446 #[cfg(feature = "e2e-encryption")]
447 use matrix_sdk_test::event_factory::EventFactory;
448 #[cfg(feature = "e2e-encryption")]
449 use ruma::{
450 MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
451 events::{
452 SyncMessageLikeEvent,
453 call::{SessionDescription, invite::CallInviteEventContent},
454 poll::{
455 unstable_response::UnstablePollResponseEventContent,
456 unstable_start::{
457 NewUnstablePollStartEventContent, UnstablePollAnswer,
458 UnstablePollStartContentBlock,
459 },
460 },
461 relation::Replacement,
462 room::{
463 ImageInfo, MediaSource,
464 encrypted::{
465 EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
466 },
467 message::{
468 ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
469 Relation, RoomMessageEventContent,
470 },
471 topic::RoomTopicEventContent,
472 },
473 sticker::{StickerEventContent, SyncStickerEvent},
474 },
475 owned_event_id, owned_mxc_uri, user_id,
476 };
477 use ruma::{
478 events::rtc::notification::{NotificationType, RtcNotificationEventContent},
479 serde::Raw,
480 };
481 use serde_json::json;
482
483 use super::LatestEvent;
484 #[cfg(feature = "e2e-encryption")]
485 use super::{PossibleLatestEvent, is_suitable_for_latest_event};
486
487 #[cfg(feature = "e2e-encryption")]
488 #[test]
489 fn test_room_messages_are_suitable() {
490 let event = EventFactory::new()
491 .sender(user_id!("@a:b.c"))
492 .event(RoomMessageEventContent::new(MessageType::Image(ImageMessageEventContent::new(
493 "".to_owned(),
494 MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")),
495 ))))
496 .into();
497 assert_let!(
498 PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
499 is_suitable_for_latest_event(&event, None)
500 );
501
502 assert_eq!(m.content.msgtype.msgtype(), "m.image");
503 }
504
505 #[cfg(feature = "e2e-encryption")]
506 #[test]
507 fn test_polls_are_suitable() {
508 let event = EventFactory::new()
509 .sender(user_id!("@a:b.c"))
510 .event(NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
511 "do you like rust?",
512 vec![UnstablePollAnswer::new("id", "yes")].try_into().unwrap(),
513 )))
514 .into();
515 assert_let!(
516 PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
517 is_suitable_for_latest_event(&event, None)
518 );
519
520 assert_eq!(m.content.poll_start().question.text, "do you like rust?");
521 }
522
523 #[cfg(feature = "e2e-encryption")]
524 #[test]
525 fn test_call_invites_are_suitable() {
526 let event = EventFactory::new()
527 .sender(user_id!("@a:b.c"))
528 .event(CallInviteEventContent::new(
529 "call_id".into(),
530 UInt::new(123).unwrap(),
531 SessionDescription::new("".into(), "".into()),
532 VoipVersionId::V1,
533 ))
534 .into();
535 assert_let!(
536 PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
537 is_suitable_for_latest_event(&event, None)
538 );
539 }
540
541 #[cfg(feature = "e2e-encryption")]
542 #[test]
543 fn test_call_notifications_are_suitable() {
544 let event = EventFactory::new()
545 .sender(user_id!("@a:b.c"))
546 .event(RtcNotificationEventContent::new(
547 MilliSecondsSinceUnixEpoch::now(),
548 Duration::new(30, 0),
549 NotificationType::Ring,
550 ))
551 .into();
552 assert_let!(
553 PossibleLatestEvent::YesRtcNotification(SyncMessageLikeEvent::Original(_)) =
554 is_suitable_for_latest_event(&event, None)
555 );
556 }
557
558 #[cfg(feature = "e2e-encryption")]
559 #[test]
560 fn test_stickers_are_suitable() {
561 let event = EventFactory::new()
562 .sender(user_id!("@a:b.c"))
563 .event(StickerEventContent::new(
564 "sticker!".to_owned(),
565 ImageInfo::new(),
566 owned_mxc_uri!("mxc://example.com/1"),
567 ))
568 .into();
569
570 assert_matches!(
571 is_suitable_for_latest_event(&event, None),
572 PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
573 );
574 }
575
576 #[cfg(feature = "e2e-encryption")]
577 #[test]
578 fn test_different_types_of_messagelike_are_unsuitable() {
579 let event = EventFactory::new()
580 .sender(user_id!("@a:b.c"))
581 .event(UnstablePollResponseEventContent::new(
582 vec![String::from("option1")],
583 owned_event_id!("$1"),
584 ))
585 .into();
586
587 assert_matches!(
588 is_suitable_for_latest_event(&event, None),
589 PossibleLatestEvent::NoUnsupportedMessageLikeType
590 );
591 }
592
593 #[cfg(feature = "e2e-encryption")]
594 #[test]
595 fn test_redacted_messages_are_suitable() {
596 let event = EventFactory::new()
597 .sender(user_id!("@a:b.c"))
598 .redacted(user_id!("@x:y.za"), RedactedRoomMessageEventContent::new())
599 .into();
600
601 assert_matches!(
602 is_suitable_for_latest_event(&event, None),
603 PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
604 );
605 }
606
607 #[cfg(feature = "e2e-encryption")]
608 #[test]
609 fn test_encrypted_messages_are_unsuitable() {
610 let event = EventFactory::new()
611 .sender(user_id!("@a:b.c"))
612 .event(RoomEncryptedEventContent::new(
613 EncryptedEventScheme::OlmV1Curve25519AesSha2(OlmV1Curve25519AesSha2Content::new(
614 BTreeMap::new(),
615 "".to_owned(),
616 )),
617 None,
618 ))
619 .into();
620
621 assert_matches!(
622 is_suitable_for_latest_event(&event, None),
623 PossibleLatestEvent::NoEncrypted
624 );
625 }
626
627 #[cfg(feature = "e2e-encryption")]
628 #[test]
629 fn test_state_events_are_unsuitable() {
630 let event = EventFactory::new()
631 .sender(user_id!("@a:b.c"))
632 .event(RoomTopicEventContent::new("".to_owned()))
633 .state_key("")
634 .into();
635
636 assert_matches!(
637 is_suitable_for_latest_event(&event, None),
638 PossibleLatestEvent::NoUnsupportedEventType
639 );
640 }
641
642 #[cfg(feature = "e2e-encryption")]
643 #[test]
644 fn test_replacement_events_are_unsuitable() {
645 let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
646 event_content.relates_to = Some(Relation::Replacement(Replacement::new(
647 owned_event_id!("$1"),
648 RoomMessageEventContent::text_plain("Hello, world!").into(),
649 )));
650
651 let event = EventFactory::new().sender(user_id!("@a:b.c")).event(event_content).into();
652
653 assert_matches!(
654 is_suitable_for_latest_event(&event, None),
655 PossibleLatestEvent::NoUnsupportedMessageLikeType
656 );
657 }
658
659 #[cfg(feature = "e2e-encryption")]
660 #[test]
661 fn test_verification_requests_are_unsuitable() {
662 use ruma::{device_id, events::room::message::KeyVerificationRequestEventContent, user_id};
663
664 let event = EventFactory::new()
665 .sender(user_id!("@a:b.c"))
666 .event(RoomMessageEventContent::new(MessageType::VerificationRequest(
667 KeyVerificationRequestEventContent::new(
668 "body".to_owned(),
669 vec![],
670 device_id!("device_id").to_owned(),
671 user_id!("@user_id:example.com").to_owned(),
672 ),
673 )))
674 .into();
675
676 assert_let!(
677 PossibleLatestEvent::NoUnsupportedMessageLikeType =
678 is_suitable_for_latest_event(&event, None)
679 );
680 }
681
682 #[test]
683 fn test_deserialize_latest_event() {
684 #[derive(Debug, serde::Serialize, serde::Deserialize)]
685 struct TestStruct {
686 latest_event: LatestEvent,
687 }
688
689 let event = TimelineEvent::from_plaintext(
690 Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
691 );
692
693 let initial = TestStruct {
694 latest_event: LatestEvent {
695 event: event.clone(),
696 sender_profile: None,
697 sender_name_is_ambiguous: None,
698 },
699 };
700
701 let serialized = serde_json::to_value(&initial).unwrap();
703 assert_eq!(
704 serialized,
705 json!({
706 "latest_event": {
707 "event": {
708 "kind": {
709 "PlainText": {
710 "event": {
711 "event_id": "$1"
712 }
713 }
714 },
715 "thread_summary": "None",
716 "timestamp": null,
717 }
718 }
719 })
720 );
721
722 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
724 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
725 assert!(deserialized.latest_event.sender_profile.is_none());
726 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
727
728 let serialized = json!({
730 "latest_event": {
731 "event": {
732 "encryption_info": null,
733 "event": {
734 "event_id": "$1"
735 }
736 },
737 }
738 });
739
740 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
741 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
742 assert!(deserialized.latest_event.sender_profile.is_none());
743 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
744
745 let serialized = json!({
747 "latest_event": event
748 });
749
750 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
751 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
752 assert!(deserialized.latest_event.sender_profile.is_none());
753 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
754 }
755}