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::from_raw(
138 Raw::new(&AnyMessageLikeEventContent::RoomMessage(
139 RoomMessageEventContent::text_plain("raclette"),
140 ))
141 .unwrap(),
142 "m.room.message".to_owned(),
143 ),
144 });
145
146 assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
147 }
148
149 #[test]
150 fn test_timestamp_with_local_cannot_be_sent() {
151 let value = LatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
152 timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
153 content: SerializableEventContent::from_raw(
154 Raw::new(&AnyMessageLikeEventContent::RoomMessage(
155 RoomMessageEventContent::text_plain("raclette"),
156 ))
157 .unwrap(),
158 "m.room.message".to_owned(),
159 ),
160 });
161
162 assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
163 }
164}
165
166#[cfg(feature = "e2e-encryption")]
171#[derive(Debug)]
172pub enum PossibleLatestEvent<'a> {
173 YesRoomMessage(&'a SyncRoomMessageEvent),
175 YesSticker(&'a SyncStickerEvent),
177 YesPoll(&'a SyncUnstablePollStartEvent),
179
180 YesCallInvite(&'a SyncCallInviteEvent),
182
183 YesRtcNotification(&'a SyncRtcNotificationEvent),
185
186 YesKnockedStateEvent(&'a SyncRoomMemberEvent),
189
190 NoUnsupportedEventType,
194 NoUnsupportedMessageLikeType,
196 NoEncrypted,
198}
199
200#[cfg(feature = "e2e-encryption")]
203pub fn is_suitable_for_latest_event<'a>(
204 event: &'a AnySyncTimelineEvent,
205 power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>,
206) -> PossibleLatestEvent<'a> {
207 match event {
208 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
210 if let Some(original_message) = message.as_original() {
211 if let MessageType::VerificationRequest(_) = original_message.content.msgtype {
213 return PossibleLatestEvent::NoUnsupportedMessageLikeType;
214 }
215
216 let is_replacement =
218 original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
219 if let Some(relation_type) = relates_to.rel_type() {
220 relation_type == RelationType::Replacement
221 } else {
222 false
223 }
224 });
225
226 if is_replacement {
227 PossibleLatestEvent::NoUnsupportedMessageLikeType
228 } else {
229 PossibleLatestEvent::YesRoomMessage(message)
230 }
231 } else {
232 PossibleLatestEvent::YesRoomMessage(message)
233 }
234 }
235
236 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
237 PossibleLatestEvent::YesPoll(poll)
238 }
239
240 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(invite)) => {
241 PossibleLatestEvent::YesCallInvite(invite)
242 }
243
244 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RtcNotification(notify)) => {
245 PossibleLatestEvent::YesRtcNotification(notify)
246 }
247
248 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(sticker)) => {
249 PossibleLatestEvent::YesSticker(sticker)
250 }
251
252 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
254 PossibleLatestEvent::NoEncrypted
255 }
256
257 AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType,
263
264 AnySyncTimelineEvent::State(state) => {
266 if let AnySyncStateEvent::RoomMember(member) = state
269 && matches!(member.membership(), MembershipState::Knock)
270 {
271 let can_accept_or_decline_knocks = match power_levels_info {
272 Some((own_user_id, room_power_levels)) => {
273 room_power_levels.user_can_invite(own_user_id)
274 || room_power_levels.user_can_kick(own_user_id)
275 }
276 _ => false,
277 };
278
279 if can_accept_or_decline_knocks {
282 return PossibleLatestEvent::YesKnockedStateEvent(member);
283 }
284 }
285 PossibleLatestEvent::NoUnsupportedEventType
286 }
287 }
288}
289
290#[derive(Clone, Debug, Serialize)]
310pub struct LatestEvent {
311 event: TimelineEvent,
313
314 #[serde(skip_serializing_if = "Option::is_none")]
316 sender_profile: Option<MinimalRoomMemberEvent>,
317
318 #[serde(skip_serializing_if = "Option::is_none")]
320 sender_name_is_ambiguous: Option<bool>,
321}
322
323#[derive(Deserialize)]
324struct SerializedLatestEvent {
325 event: TimelineEvent,
327
328 #[serde(skip_serializing_if = "Option::is_none")]
330 sender_profile: Option<MinimalRoomMemberEvent>,
331
332 #[serde(skip_serializing_if = "Option::is_none")]
334 sender_name_is_ambiguous: Option<bool>,
335}
336
337impl<'de> Deserialize<'de> for LatestEvent {
340 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
341 where
342 D: serde::Deserializer<'de>,
343 {
344 let raw: Box<serde_json::value::RawValue> = Box::deserialize(deserializer)?;
345
346 let mut variant_errors = Vec::new();
347
348 match serde_json::from_str::<SerializedLatestEvent>(raw.get()) {
349 Ok(value) => {
350 return Ok(LatestEvent {
351 event: value.event,
352 sender_profile: value.sender_profile,
353 sender_name_is_ambiguous: value.sender_name_is_ambiguous,
354 });
355 }
356 Err(err) => variant_errors.push(err),
357 }
358
359 match serde_json::from_str::<TimelineEvent>(raw.get()) {
360 Ok(value) => {
361 return Ok(LatestEvent {
362 event: value,
363 sender_profile: None,
364 sender_name_is_ambiguous: None,
365 });
366 }
367 Err(err) => variant_errors.push(err),
368 }
369
370 Err(serde::de::Error::custom(format!(
371 "data did not match any variant of serialized LatestEvent (using serde_json). \
372 Observed errors: {variant_errors:?}"
373 )))
374 }
375}
376
377impl LatestEvent {
378 pub fn new(event: TimelineEvent) -> Self {
380 Self { event, sender_profile: None, sender_name_is_ambiguous: None }
381 }
382
383 pub fn new_with_sender_details(
385 event: TimelineEvent,
386 sender_profile: Option<MinimalRoomMemberEvent>,
387 sender_name_is_ambiguous: Option<bool>,
388 ) -> Self {
389 Self { event, sender_profile, sender_name_is_ambiguous }
390 }
391
392 pub fn into_event(self) -> TimelineEvent {
394 self.event
395 }
396
397 pub fn event(&self) -> &TimelineEvent {
399 &self.event
400 }
401
402 pub fn event_mut(&mut self) -> &mut TimelineEvent {
404 &mut self.event
405 }
406
407 pub fn event_id(&self) -> Option<OwnedEventId> {
409 self.event.event_id()
410 }
411
412 pub fn has_sender_profile(&self) -> bool {
414 self.sender_profile.is_some()
415 }
416
417 pub fn sender_display_name(&self) -> Option<&str> {
420 self.sender_profile.as_ref().and_then(|profile| {
421 profile.as_original().and_then(|event| event.content.displayname.as_deref())
422 })
423 }
424
425 pub fn sender_name_ambiguous(&self) -> Option<bool> {
429 self.sender_name_is_ambiguous
430 }
431
432 pub fn sender_avatar_url(&self) -> Option<&MxcUri> {
435 self.sender_profile.as_ref().and_then(|profile| {
436 profile.as_original().and_then(|event| event.content.avatar_url.as_deref())
437 })
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 #[cfg(feature = "e2e-encryption")]
444 use std::collections::BTreeMap;
445 use std::time::Duration;
446
447 #[cfg(feature = "e2e-encryption")]
448 use assert_matches::assert_matches;
449 #[cfg(feature = "e2e-encryption")]
450 use assert_matches2::assert_let;
451 use matrix_sdk_common::deserialized_responses::TimelineEvent;
452 #[cfg(feature = "e2e-encryption")]
453 use matrix_sdk_test::event_factory::EventFactory;
454 #[cfg(feature = "e2e-encryption")]
455 use ruma::{
456 MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
457 events::{
458 SyncMessageLikeEvent,
459 call::{SessionDescription, invite::CallInviteEventContent},
460 poll::{
461 unstable_response::UnstablePollResponseEventContent,
462 unstable_start::{
463 NewUnstablePollStartEventContent, UnstablePollAnswer,
464 UnstablePollStartContentBlock,
465 },
466 },
467 relation::Replacement,
468 room::{
469 ImageInfo, MediaSource,
470 encrypted::{
471 EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
472 },
473 message::{
474 ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
475 Relation, RoomMessageEventContent,
476 },
477 topic::RoomTopicEventContent,
478 },
479 sticker::{StickerEventContent, SyncStickerEvent},
480 },
481 owned_event_id, owned_mxc_uri, user_id,
482 };
483 use ruma::{
484 events::rtc::notification::{NotificationType, RtcNotificationEventContent},
485 serde::Raw,
486 };
487 use serde_json::json;
488
489 use super::LatestEvent;
490 #[cfg(feature = "e2e-encryption")]
491 use super::{PossibleLatestEvent, is_suitable_for_latest_event};
492
493 #[cfg(feature = "e2e-encryption")]
494 #[test]
495 fn test_room_messages_are_suitable() {
496 let event = EventFactory::new()
497 .sender(user_id!("@a:b.c"))
498 .event(RoomMessageEventContent::new(MessageType::Image(ImageMessageEventContent::new(
499 "".to_owned(),
500 MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")),
501 ))))
502 .into();
503 assert_let!(
504 PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
505 is_suitable_for_latest_event(&event, None)
506 );
507
508 assert_eq!(m.content.msgtype.msgtype(), "m.image");
509 }
510
511 #[cfg(feature = "e2e-encryption")]
512 #[test]
513 fn test_polls_are_suitable() {
514 let event = EventFactory::new()
515 .sender(user_id!("@a:b.c"))
516 .event(NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
517 "do you like rust?",
518 vec![UnstablePollAnswer::new("id", "yes")].try_into().unwrap(),
519 )))
520 .into();
521 assert_let!(
522 PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
523 is_suitable_for_latest_event(&event, None)
524 );
525
526 assert_eq!(m.content.poll_start().question.text, "do you like rust?");
527 }
528
529 #[cfg(feature = "e2e-encryption")]
530 #[test]
531 fn test_call_invites_are_suitable() {
532 let event = EventFactory::new()
533 .sender(user_id!("@a:b.c"))
534 .event(CallInviteEventContent::new(
535 "call_id".into(),
536 UInt::new(123).unwrap(),
537 SessionDescription::new("".into(), "".into()),
538 VoipVersionId::V1,
539 ))
540 .into();
541 assert_let!(
542 PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
543 is_suitable_for_latest_event(&event, None)
544 );
545 }
546
547 #[cfg(feature = "e2e-encryption")]
548 #[test]
549 fn test_call_notifications_are_suitable() {
550 let event = EventFactory::new()
551 .sender(user_id!("@a:b.c"))
552 .event(RtcNotificationEventContent::new(
553 MilliSecondsSinceUnixEpoch::now(),
554 Duration::new(30, 0),
555 NotificationType::Ring,
556 ))
557 .into();
558 assert_let!(
559 PossibleLatestEvent::YesRtcNotification(SyncMessageLikeEvent::Original(_)) =
560 is_suitable_for_latest_event(&event, None)
561 );
562 }
563
564 #[cfg(feature = "e2e-encryption")]
565 #[test]
566 fn test_stickers_are_suitable() {
567 let event = EventFactory::new()
568 .sender(user_id!("@a:b.c"))
569 .event(StickerEventContent::new(
570 "sticker!".to_owned(),
571 ImageInfo::new(),
572 owned_mxc_uri!("mxc://example.com/1"),
573 ))
574 .into();
575
576 assert_matches!(
577 is_suitable_for_latest_event(&event, None),
578 PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
579 );
580 }
581
582 #[cfg(feature = "e2e-encryption")]
583 #[test]
584 fn test_different_types_of_messagelike_are_unsuitable() {
585 let event = EventFactory::new()
586 .sender(user_id!("@a:b.c"))
587 .event(UnstablePollResponseEventContent::new(
588 vec![String::from("option1")],
589 owned_event_id!("$1"),
590 ))
591 .into();
592
593 assert_matches!(
594 is_suitable_for_latest_event(&event, None),
595 PossibleLatestEvent::NoUnsupportedMessageLikeType
596 );
597 }
598
599 #[cfg(feature = "e2e-encryption")]
600 #[test]
601 fn test_redacted_messages_are_suitable() {
602 let event = EventFactory::new()
603 .sender(user_id!("@a:b.c"))
604 .redacted(user_id!("@x:y.za"), RedactedRoomMessageEventContent::new())
605 .into();
606
607 assert_matches!(
608 is_suitable_for_latest_event(&event, None),
609 PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
610 );
611 }
612
613 #[cfg(feature = "e2e-encryption")]
614 #[test]
615 fn test_encrypted_messages_are_unsuitable() {
616 let event = EventFactory::new()
617 .sender(user_id!("@a:b.c"))
618 .event(RoomEncryptedEventContent::new(
619 EncryptedEventScheme::OlmV1Curve25519AesSha2(OlmV1Curve25519AesSha2Content::new(
620 BTreeMap::new(),
621 "".to_owned(),
622 )),
623 None,
624 ))
625 .into();
626
627 assert_matches!(
628 is_suitable_for_latest_event(&event, None),
629 PossibleLatestEvent::NoEncrypted
630 );
631 }
632
633 #[cfg(feature = "e2e-encryption")]
634 #[test]
635 fn test_state_events_are_unsuitable() {
636 let event = EventFactory::new()
637 .sender(user_id!("@a:b.c"))
638 .event(RoomTopicEventContent::new("".to_owned()))
639 .state_key("")
640 .into();
641
642 assert_matches!(
643 is_suitable_for_latest_event(&event, None),
644 PossibleLatestEvent::NoUnsupportedEventType
645 );
646 }
647
648 #[cfg(feature = "e2e-encryption")]
649 #[test]
650 fn test_replacement_events_are_unsuitable() {
651 let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
652 event_content.relates_to = Some(Relation::Replacement(Replacement::new(
653 owned_event_id!("$1"),
654 RoomMessageEventContent::text_plain("Hello, world!").into(),
655 )));
656
657 let event = EventFactory::new().sender(user_id!("@a:b.c")).event(event_content).into();
658
659 assert_matches!(
660 is_suitable_for_latest_event(&event, None),
661 PossibleLatestEvent::NoUnsupportedMessageLikeType
662 );
663 }
664
665 #[cfg(feature = "e2e-encryption")]
666 #[test]
667 fn test_verification_requests_are_unsuitable() {
668 use ruma::{device_id, events::room::message::KeyVerificationRequestEventContent, user_id};
669
670 let event = EventFactory::new()
671 .sender(user_id!("@a:b.c"))
672 .event(RoomMessageEventContent::new(MessageType::VerificationRequest(
673 KeyVerificationRequestEventContent::new(
674 "body".to_owned(),
675 vec![],
676 device_id!("device_id").to_owned(),
677 user_id!("@user_id:example.com").to_owned(),
678 ),
679 )))
680 .into();
681
682 assert_let!(
683 PossibleLatestEvent::NoUnsupportedMessageLikeType =
684 is_suitable_for_latest_event(&event, None)
685 );
686 }
687
688 #[test]
689 fn test_deserialize_latest_event() {
690 #[derive(Debug, serde::Serialize, serde::Deserialize)]
691 struct TestStruct {
692 latest_event: LatestEvent,
693 }
694
695 let event = TimelineEvent::from_plaintext(
696 Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
697 );
698
699 let initial = TestStruct {
700 latest_event: LatestEvent {
701 event: event.clone(),
702 sender_profile: None,
703 sender_name_is_ambiguous: None,
704 },
705 };
706
707 let serialized = serde_json::to_value(&initial).unwrap();
709 assert_eq!(
710 serialized,
711 json!({
712 "latest_event": {
713 "event": {
714 "kind": {
715 "PlainText": {
716 "event": {
717 "event_id": "$1"
718 }
719 }
720 },
721 "thread_summary": "None",
722 "timestamp": null,
723 }
724 }
725 })
726 );
727
728 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
730 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
731 assert!(deserialized.latest_event.sender_profile.is_none());
732 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
733
734 let serialized = json!({
736 "latest_event": {
737 "event": {
738 "encryption_info": null,
739 "event": {
740 "event_id": "$1"
741 }
742 },
743 }
744 });
745
746 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
747 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
748 assert!(deserialized.latest_event.sender_profile.is_none());
749 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
750
751 let serialized = json!({
753 "latest_event": event
754 });
755
756 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
757 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
758 assert!(deserialized.latest_event.sender_profile.is_none());
759 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
760 }
761}