matrix_sdk_base/
latest_event.rs

1//! Utilities for working with events to decide whether they are suitable for
2//! use as a [crate::Room::latest_event].
3
4use 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/// A latest event value!
27#[derive(Debug, Default, Clone, Serialize, Deserialize)]
28pub enum LatestEventValue {
29    /// No value has been computed yet, or no candidate value was found.
30    #[default]
31    None,
32
33    /// The latest event represents a remote event.
34    Remote(RemoteLatestEventValue),
35
36    /// The latest event represents a local event that is sending.
37    LocalIsSending(LocalLatestEventValue),
38
39    /// The latest event represents a local event that cannot be sent, either
40    /// because a previous local event, or this local event cannot be sent.
41    LocalCannotBeSent(LocalLatestEventValue),
42}
43
44/// Represents the value for [`LatestEventValue::Remote`].
45pub type RemoteLatestEventValue = TimelineEvent;
46
47/// Represents the value for [`LatestEventValue::LocalIsSending`] and
48/// [`LatestEventValue::LocalCannotBeSent`].
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct LocalLatestEventValue {
51    /// The time where the event has been created (by this module).
52    pub timestamp: MilliSecondsSinceUnixEpoch,
53
54    /// The content of the local event.
55    pub content: SerializableEventContent,
56}
57
58/// Represents a decision about whether an event could be stored as the latest
59/// event in a room. Variants starting with Yes indicate that this message could
60/// be stored, and provide the inner event information, and those starting with
61/// a No indicate that it could not, and give a reason.
62#[cfg(feature = "e2e-encryption")]
63#[derive(Debug)]
64pub enum PossibleLatestEvent<'a> {
65    /// This message is suitable - it is an m.room.message
66    YesRoomMessage(&'a SyncRoomMessageEvent),
67    /// This message is suitable - it is a sticker
68    YesSticker(&'a SyncStickerEvent),
69    /// This message is suitable - it is a poll
70    YesPoll(&'a SyncUnstablePollStartEvent),
71
72    /// This message is suitable - it is a call invite
73    YesCallInvite(&'a SyncCallInviteEvent),
74
75    /// This message is suitable - it's a call notification
76    YesCallNotify(&'a SyncCallNotifyEvent),
77
78    /// This state event is suitable - it's a knock membership change
79    /// that can be handled by the current user.
80    YesKnockedStateEvent(&'a SyncRoomMemberEvent),
81
82    // Later: YesState(),
83    // Later: YesReaction(),
84    /// Not suitable - it's a state event
85    NoUnsupportedEventType,
86    /// Not suitable - it's not a m.room.message or an edit/replacement
87    NoUnsupportedMessageLikeType,
88    /// Not suitable - it's encrypted
89    NoEncrypted,
90}
91
92/// Decide whether an event could be stored as the latest event in a room.
93/// Returns a LatestEvent representing our decision.
94#[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        // Suitable - we have an m.room.message that was not redacted or edited
101        AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
102            if let Some(original_message) = message.as_original() {
103                // Don't show incoming verification requests
104                if let MessageType::VerificationRequest(_) = original_message.content.msgtype {
105                    return PossibleLatestEvent::NoUnsupportedMessageLikeType;
106                }
107
108                // Check if this is a replacement for another message. If it is, ignore it
109                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        // Encrypted events are not suitable
145        AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
146            PossibleLatestEvent::NoEncrypted
147        }
148
149        // Later, if we support reactions:
150        // AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Reaction(_))
151
152        // MessageLike, but not one of the types we want to show in message previews, so not
153        // suitable
154        AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType,
155
156        // We don't currently support most state events
157        AnySyncTimelineEvent::State(state) => {
158            // But we make an exception for knocked state events *if* the current user
159            // can either accept or decline them
160            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                // The current user can act on the knock changes, so they should be
172                // displayed
173                if can_accept_or_decline_knocks {
174                    return PossibleLatestEvent::YesKnockedStateEvent(member);
175                }
176            }
177            PossibleLatestEvent::NoUnsupportedEventType
178        }
179    }
180}
181
182/// Represent all information required to represent a latest event in an
183/// efficient way.
184///
185/// ## Implementation details
186///
187/// Serialization and deserialization should be a breeze, but we introduced a
188/// change in the format without realizing, and without a migration. Ideally,
189/// this would be handled with a `serde(untagged)` enum that would be used to
190/// deserialize in either the older format, or to the new format. Unfortunately,
191/// untagged enums don't play nicely with `serde_json::value::RawValue`,
192/// so we did have to implement a custom `Deserialize` for `LatestEvent`, that
193/// first deserializes the thing as a raw JSON value, and then deserializes the
194/// JSON string as one variant or the other.
195///
196/// Because of that, `LatestEvent` should only be (de)serialized using
197/// serde_json.
198///
199/// Whenever you introduce new fields to `LatestEvent` make sure to add them to
200/// `SerializedLatestEvent` too.
201#[derive(Clone, Debug, Serialize)]
202pub struct LatestEvent {
203    /// The actual event.
204    event: TimelineEvent,
205
206    /// The member profile of the event' sender.
207    #[serde(skip_serializing_if = "Option::is_none")]
208    sender_profile: Option<MinimalRoomMemberEvent>,
209
210    /// The name of the event' sender is ambiguous.
211    #[serde(skip_serializing_if = "Option::is_none")]
212    sender_name_is_ambiguous: Option<bool>,
213}
214
215#[derive(Deserialize)]
216struct SerializedLatestEvent {
217    /// The actual event.
218    event: TimelineEvent,
219
220    /// The member profile of the event' sender.
221    #[serde(skip_serializing_if = "Option::is_none")]
222    sender_profile: Option<MinimalRoomMemberEvent>,
223
224    /// The name of the event' sender is ambiguous.
225    #[serde(skip_serializing_if = "Option::is_none")]
226    sender_name_is_ambiguous: Option<bool>,
227}
228
229// Note: this deserialize implementation for LatestEvent will *only* work with
230// serde_json.
231impl<'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    /// Create a new [`LatestEvent`] without the sender's profile.
271    pub fn new(event: TimelineEvent) -> Self {
272        Self { event, sender_profile: None, sender_name_is_ambiguous: None }
273    }
274
275    /// Create a new [`LatestEvent`] with maybe the sender's profile.
276    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    /// Transform [`Self`] into an event.
285    pub fn into_event(self) -> TimelineEvent {
286        self.event
287    }
288
289    /// Get a reference to the event.
290    pub fn event(&self) -> &TimelineEvent {
291        &self.event
292    }
293
294    /// Get a mutable reference to the event.
295    pub fn event_mut(&mut self) -> &mut TimelineEvent {
296        &mut self.event
297    }
298
299    /// Get the event ID.
300    pub fn event_id(&self) -> Option<OwnedEventId> {
301        self.event.event_id()
302    }
303
304    /// Check whether [`Self`] has a sender profile.
305    pub fn has_sender_profile(&self) -> bool {
306        self.sender_profile.is_some()
307    }
308
309    /// Return the sender's display name if it was known at the time [`Self`]
310    /// was built.
311    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    /// Return `Some(true)` if the sender's name is ambiguous, `Some(false)` if
318    /// it isn't, `None` if ambiguity detection wasn't possible at the time
319    /// [`Self`] was built.
320    pub fn sender_name_ambiguous(&self) -> Option<bool> {
321        self.sender_name_is_ambiguous
322    }
323
324    /// Return the sender's avatar URL if it was known at the time [`Self`] was
325    /// built.
326    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        // Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
535        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        // When serialized, LatestEvent always uses the new format.
677        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        // And it can be properly deserialized from the new format.
697        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        // The previous format can also be deserialized.
703        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        // The even older format can also be deserialized.
720        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}