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;
5#[cfg(feature = "e2e-encryption")]
6use ruma::{
7    events::{
8        call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
9        poll::unstable_start::SyncUnstablePollStartEvent,
10        relation::RelationType,
11        room::{
12            member::{MembershipState, SyncRoomMemberEvent},
13            message::SyncRoomMessageEvent,
14            power_levels::RoomPowerLevels,
15        },
16        sticker::SyncStickerEvent,
17        AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
18    },
19    UserId,
20};
21use ruma::{MxcUri, OwnedEventId};
22use serde::{Deserialize, Serialize};
23
24use crate::MinimalRoomMemberEvent;
25
26/// Represents a decision about whether an event could be stored as the latest
27/// event in a room. Variants starting with Yes indicate that this message could
28/// be stored, and provide the inner event information, and those starting with
29/// a No indicate that it could not, and give a reason.
30#[cfg(feature = "e2e-encryption")]
31#[derive(Debug)]
32pub enum PossibleLatestEvent<'a> {
33    /// This message is suitable - it is an m.room.message
34    YesRoomMessage(&'a SyncRoomMessageEvent),
35    /// This message is suitable - it is a sticker
36    YesSticker(&'a SyncStickerEvent),
37    /// This message is suitable - it is a poll
38    YesPoll(&'a SyncUnstablePollStartEvent),
39
40    /// This message is suitable - it is a call invite
41    YesCallInvite(&'a SyncCallInviteEvent),
42
43    /// This message is suitable - it's a call notification
44    YesCallNotify(&'a SyncCallNotifyEvent),
45
46    /// This state event is suitable - it's a knock membership change
47    /// that can be handled by the current user.
48    YesKnockedStateEvent(&'a SyncRoomMemberEvent),
49
50    // Later: YesState(),
51    // Later: YesReaction(),
52    /// Not suitable - it's a state event
53    NoUnsupportedEventType,
54    /// Not suitable - it's not a m.room.message or an edit/replacement
55    NoUnsupportedMessageLikeType,
56    /// Not suitable - it's encrypted
57    NoEncrypted,
58}
59
60/// Decide whether an event could be stored as the latest event in a room.
61/// Returns a LatestEvent representing our decision.
62#[cfg(feature = "e2e-encryption")]
63pub fn is_suitable_for_latest_event<'a>(
64    event: &'a AnySyncTimelineEvent,
65    power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>,
66) -> PossibleLatestEvent<'a> {
67    match event {
68        // Suitable - we have an m.room.message that was not redacted or edited
69        AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
70            // Check if this is a replacement for another message. If it is, ignore it
71            if let Some(original_message) = message.as_original() {
72                let is_replacement =
73                    original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
74                        if let Some(relation_type) = relates_to.rel_type() {
75                            relation_type == RelationType::Replacement
76                        } else {
77                            false
78                        }
79                    });
80
81                if is_replacement {
82                    PossibleLatestEvent::NoUnsupportedMessageLikeType
83                } else {
84                    PossibleLatestEvent::YesRoomMessage(message)
85                }
86            } else {
87                PossibleLatestEvent::YesRoomMessage(message)
88            }
89        }
90
91        AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
92            PossibleLatestEvent::YesPoll(poll)
93        }
94
95        AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(invite)) => {
96            PossibleLatestEvent::YesCallInvite(invite)
97        }
98
99        AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(notify)) => {
100            PossibleLatestEvent::YesCallNotify(notify)
101        }
102
103        AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(sticker)) => {
104            PossibleLatestEvent::YesSticker(sticker)
105        }
106
107        // Encrypted events are not suitable
108        AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
109            PossibleLatestEvent::NoEncrypted
110        }
111
112        // Later, if we support reactions:
113        // AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Reaction(_))
114
115        // MessageLike, but not one of the types we want to show in message previews, so not
116        // suitable
117        AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType,
118
119        // We don't currently support most state events
120        AnySyncTimelineEvent::State(state) => {
121            // But we make an exception for knocked state events *if* the current user
122            // can either accept or decline them
123            if let AnySyncStateEvent::RoomMember(member) = state {
124                if matches!(member.membership(), MembershipState::Knock) {
125                    let can_accept_or_decline_knocks = match power_levels_info {
126                        Some((own_user_id, room_power_levels)) => {
127                            room_power_levels.user_can_invite(own_user_id)
128                                || room_power_levels.user_can_kick(own_user_id)
129                        }
130                        _ => false,
131                    };
132
133                    // The current user can act on the knock changes, so they should be
134                    // displayed
135                    if can_accept_or_decline_knocks {
136                        return PossibleLatestEvent::YesKnockedStateEvent(member);
137                    }
138                }
139            }
140            PossibleLatestEvent::NoUnsupportedEventType
141        }
142    }
143}
144
145/// Represent all information required to represent a latest event in an
146/// efficient way.
147///
148/// ## Implementation details
149///
150/// Serialization and deserialization should be a breeze, but we introduced a
151/// change in the format without realizing, and without a migration. Ideally,
152/// this would be handled with a `serde(untagged)` enum that would be used to
153/// deserialize in either the older format, or to the new format. Unfortunately,
154/// untagged enums don't play nicely with `serde_json::value::RawValue`,
155/// so we did have to implement a custom `Deserialize` for `LatestEvent`, that
156/// first deserializes the thing as a raw JSON value, and then deserializes the
157/// JSON string as one variant or the other.
158///
159/// Because of that, `LatestEvent` should only be (de)serialized using
160/// serde_json.
161///
162/// Whenever you introduce new fields to `LatestEvent` make sure to add them to
163/// `SerializedLatestEvent` too.
164#[derive(Clone, Debug, Serialize)]
165pub struct LatestEvent {
166    /// The actual event.
167    event: TimelineEvent,
168
169    /// The member profile of the event' sender.
170    #[serde(skip_serializing_if = "Option::is_none")]
171    sender_profile: Option<MinimalRoomMemberEvent>,
172
173    /// The name of the event' sender is ambiguous.
174    #[serde(skip_serializing_if = "Option::is_none")]
175    sender_name_is_ambiguous: Option<bool>,
176}
177
178#[derive(Deserialize)]
179struct SerializedLatestEvent {
180    /// The actual event.
181    event: TimelineEvent,
182
183    /// The member profile of the event' sender.
184    #[serde(skip_serializing_if = "Option::is_none")]
185    sender_profile: Option<MinimalRoomMemberEvent>,
186
187    /// The name of the event' sender is ambiguous.
188    #[serde(skip_serializing_if = "Option::is_none")]
189    sender_name_is_ambiguous: Option<bool>,
190}
191
192// Note: this deserialize implementation for LatestEvent will *only* work with
193// serde_json.
194impl<'de> Deserialize<'de> for LatestEvent {
195    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
196    where
197        D: serde::Deserializer<'de>,
198    {
199        let raw: Box<serde_json::value::RawValue> = Box::deserialize(deserializer)?;
200
201        let mut variant_errors = Vec::new();
202
203        match serde_json::from_str::<SerializedLatestEvent>(raw.get()) {
204            Ok(value) => {
205                return Ok(LatestEvent {
206                    event: value.event,
207                    sender_profile: value.sender_profile,
208                    sender_name_is_ambiguous: value.sender_name_is_ambiguous,
209                });
210            }
211            Err(err) => variant_errors.push(err),
212        }
213
214        match serde_json::from_str::<TimelineEvent>(raw.get()) {
215            Ok(value) => {
216                return Ok(LatestEvent {
217                    event: value,
218                    sender_profile: None,
219                    sender_name_is_ambiguous: None,
220                });
221            }
222            Err(err) => variant_errors.push(err),
223        }
224
225        Err(serde::de::Error::custom(
226            format!("data did not match any variant of serialized LatestEvent (using serde_json). Observed errors: {variant_errors:?}")
227        ))
228    }
229}
230
231impl LatestEvent {
232    /// Create a new [`LatestEvent`] without the sender's profile.
233    pub fn new(event: TimelineEvent) -> Self {
234        Self { event, sender_profile: None, sender_name_is_ambiguous: None }
235    }
236
237    /// Create a new [`LatestEvent`] with maybe the sender's profile.
238    pub fn new_with_sender_details(
239        event: TimelineEvent,
240        sender_profile: Option<MinimalRoomMemberEvent>,
241        sender_name_is_ambiguous: Option<bool>,
242    ) -> Self {
243        Self { event, sender_profile, sender_name_is_ambiguous }
244    }
245
246    /// Transform [`Self`] into an event.
247    pub fn into_event(self) -> TimelineEvent {
248        self.event
249    }
250
251    /// Get a reference to the event.
252    pub fn event(&self) -> &TimelineEvent {
253        &self.event
254    }
255
256    /// Get a mutable reference to the event.
257    pub fn event_mut(&mut self) -> &mut TimelineEvent {
258        &mut self.event
259    }
260
261    /// Get the event ID.
262    pub fn event_id(&self) -> Option<OwnedEventId> {
263        self.event.event_id()
264    }
265
266    /// Check whether [`Self`] has a sender profile.
267    pub fn has_sender_profile(&self) -> bool {
268        self.sender_profile.is_some()
269    }
270
271    /// Return the sender's display name if it was known at the time [`Self`]
272    /// was built.
273    pub fn sender_display_name(&self) -> Option<&str> {
274        self.sender_profile.as_ref().and_then(|profile| {
275            profile.as_original().and_then(|event| event.content.displayname.as_deref())
276        })
277    }
278
279    /// Return `Some(true)` if the sender's name is ambiguous, `Some(false)` if
280    /// it isn't, `None` if ambiguity detection wasn't possible at the time
281    /// [`Self`] was built.
282    pub fn sender_name_ambiguous(&self) -> Option<bool> {
283        self.sender_name_is_ambiguous
284    }
285
286    /// Return the sender's avatar URL if it was known at the time [`Self`] was
287    /// built.
288    pub fn sender_avatar_url(&self) -> Option<&MxcUri> {
289        self.sender_profile.as_ref().and_then(|profile| {
290            profile.as_original().and_then(|event| event.content.avatar_url.as_deref())
291        })
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    #[cfg(feature = "e2e-encryption")]
298    use std::collections::BTreeMap;
299
300    #[cfg(feature = "e2e-encryption")]
301    use assert_matches::assert_matches;
302    #[cfg(feature = "e2e-encryption")]
303    use assert_matches2::assert_let;
304    use matrix_sdk_common::deserialized_responses::TimelineEvent;
305    use ruma::serde::Raw;
306    #[cfg(feature = "e2e-encryption")]
307    use ruma::{
308        events::{
309            call::{
310                invite::{CallInviteEventContent, SyncCallInviteEvent},
311                notify::{
312                    ApplicationType, CallNotifyEventContent, NotifyType, SyncCallNotifyEvent,
313                },
314                SessionDescription,
315            },
316            poll::{
317                unstable_response::{
318                    SyncUnstablePollResponseEvent, UnstablePollResponseEventContent,
319                },
320                unstable_start::{
321                    NewUnstablePollStartEventContent, SyncUnstablePollStartEvent,
322                    UnstablePollAnswer, UnstablePollStartContentBlock,
323                },
324            },
325            relation::Replacement,
326            room::{
327                encrypted::{
328                    EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
329                    SyncRoomEncryptedEvent,
330                },
331                message::{
332                    ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
333                    Relation, RoomMessageEventContent, SyncRoomMessageEvent,
334                },
335                topic::{RoomTopicEventContent, SyncRoomTopicEvent},
336                ImageInfo, MediaSource,
337            },
338            sticker::{StickerEventContent, SyncStickerEvent},
339            AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
340            Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
341            RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
342            UnsignedRoomRedactionEvent,
343        },
344        owned_event_id, owned_mxc_uri, owned_user_id, MilliSecondsSinceUnixEpoch, UInt,
345        VoipVersionId,
346    };
347    use serde_json::json;
348
349    use super::LatestEvent;
350    #[cfg(feature = "e2e-encryption")]
351    use super::{is_suitable_for_latest_event, PossibleLatestEvent};
352
353    #[cfg(feature = "e2e-encryption")]
354    #[test]
355    fn test_room_messages_are_suitable() {
356        let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
357            SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
358                content: RoomMessageEventContent::new(MessageType::Image(
359                    ImageMessageEventContent::new(
360                        "".to_owned(),
361                        MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")),
362                    ),
363                )),
364                event_id: owned_event_id!("$1"),
365                sender: owned_user_id!("@a:b.c"),
366                origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
367                unsigned: MessageLikeUnsigned::new(),
368            }),
369        ));
370        assert_let!(
371            PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
372                is_suitable_for_latest_event(&event, None)
373        );
374
375        assert_eq!(m.content.msgtype.msgtype(), "m.image");
376    }
377
378    #[cfg(feature = "e2e-encryption")]
379    #[test]
380    fn test_polls_are_suitable() {
381        let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(
382            SyncUnstablePollStartEvent::Original(OriginalSyncMessageLikeEvent {
383                content: NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
384                    "do you like rust?",
385                    vec![UnstablePollAnswer::new("id", "yes")].try_into().unwrap(),
386                ))
387                .into(),
388                event_id: owned_event_id!("$1"),
389                sender: owned_user_id!("@a:b.c"),
390                origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
391                unsigned: MessageLikeUnsigned::new(),
392            }),
393        ));
394        assert_let!(
395            PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
396                is_suitable_for_latest_event(&event, None)
397        );
398
399        assert_eq!(m.content.poll_start().question.text, "do you like rust?");
400    }
401
402    #[cfg(feature = "e2e-encryption")]
403    #[test]
404    fn test_call_invites_are_suitable() {
405        let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(
406            SyncCallInviteEvent::Original(OriginalSyncMessageLikeEvent {
407                content: CallInviteEventContent::new(
408                    "call_id".into(),
409                    UInt::new(123).unwrap(),
410                    SessionDescription::new("".into(), "".into()),
411                    VoipVersionId::V1,
412                ),
413                event_id: owned_event_id!("$1"),
414                sender: owned_user_id!("@a:b.c"),
415                origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
416                unsigned: MessageLikeUnsigned::new(),
417            }),
418        ));
419        assert_let!(
420            PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
421                is_suitable_for_latest_event(&event, None)
422        );
423    }
424
425    #[cfg(feature = "e2e-encryption")]
426    #[test]
427    fn test_call_notifications_are_suitable() {
428        let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(
429            SyncCallNotifyEvent::Original(OriginalSyncMessageLikeEvent {
430                content: CallNotifyEventContent::new(
431                    "call_id".into(),
432                    ApplicationType::Call,
433                    NotifyType::Ring,
434                    Mentions::new(),
435                ),
436                event_id: owned_event_id!("$1"),
437                sender: owned_user_id!("@a:b.c"),
438                origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
439                unsigned: MessageLikeUnsigned::new(),
440            }),
441        ));
442        assert_let!(
443            PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
444                is_suitable_for_latest_event(&event, None)
445        );
446    }
447
448    #[cfg(feature = "e2e-encryption")]
449    #[test]
450    fn test_stickers_are_suitable() {
451        let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
452            SyncStickerEvent::Original(OriginalSyncMessageLikeEvent {
453                content: StickerEventContent::new(
454                    "sticker!".to_owned(),
455                    ImageInfo::new(),
456                    owned_mxc_uri!("mxc://example.com/1"),
457                ),
458                event_id: owned_event_id!("$1"),
459                sender: owned_user_id!("@a:b.c"),
460                origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
461                unsigned: MessageLikeUnsigned::new(),
462            }),
463        ));
464
465        assert_matches!(
466            is_suitable_for_latest_event(&event, None),
467            PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
468        );
469    }
470
471    #[cfg(feature = "e2e-encryption")]
472    #[test]
473    fn test_different_types_of_messagelike_are_unsuitable() {
474        let event =
475            AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollResponse(
476                SyncUnstablePollResponseEvent::Original(OriginalSyncMessageLikeEvent {
477                    content: UnstablePollResponseEventContent::new(
478                        vec![String::from("option1")],
479                        owned_event_id!("$1"),
480                    ),
481                    event_id: owned_event_id!("$2"),
482                    sender: owned_user_id!("@a:b.c"),
483                    origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
484                    unsigned: MessageLikeUnsigned::new(),
485                }),
486            ));
487
488        assert_matches!(
489            is_suitable_for_latest_event(&event, None),
490            PossibleLatestEvent::NoUnsupportedMessageLikeType
491        );
492    }
493
494    #[cfg(feature = "e2e-encryption")]
495    #[test]
496    fn test_redacted_messages_are_suitable() {
497        // Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
498        let room_redaction_event: UnsignedRoomRedactionEvent = serde_json::from_value(json!({
499            "content": {},
500            "event_id": "$redaction",
501            "sender": "@x:y.za",
502            "origin_server_ts": 223543,
503            "unsigned": { "reason": "foo" }
504        }))
505        .unwrap();
506
507        let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
508            SyncRoomMessageEvent::Redacted(RedactedSyncMessageLikeEvent {
509                content: RedactedRoomMessageEventContent::new(),
510                event_id: owned_event_id!("$1"),
511                sender: owned_user_id!("@a:b.c"),
512                origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
513                unsigned: RedactedUnsigned::new(room_redaction_event),
514            }),
515        ));
516
517        assert_matches!(
518            is_suitable_for_latest_event(&event, None),
519            PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
520        );
521    }
522
523    #[cfg(feature = "e2e-encryption")]
524    #[test]
525    fn test_encrypted_messages_are_unsuitable() {
526        let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
527            SyncRoomEncryptedEvent::Original(OriginalSyncMessageLikeEvent {
528                content: RoomEncryptedEventContent::new(
529                    EncryptedEventScheme::OlmV1Curve25519AesSha2(
530                        OlmV1Curve25519AesSha2Content::new(BTreeMap::new(), "".to_owned()),
531                    ),
532                    None,
533                ),
534                event_id: owned_event_id!("$1"),
535                sender: owned_user_id!("@a:b.c"),
536                origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
537                unsigned: MessageLikeUnsigned::new(),
538            }),
539        ));
540
541        assert_matches!(
542            is_suitable_for_latest_event(&event, None),
543            PossibleLatestEvent::NoEncrypted
544        );
545    }
546
547    #[cfg(feature = "e2e-encryption")]
548    #[test]
549    fn test_state_events_are_unsuitable() {
550        let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
551            SyncRoomTopicEvent::Original(OriginalSyncStateEvent {
552                content: RoomTopicEventContent::new("".to_owned()),
553                event_id: owned_event_id!("$1"),
554                sender: owned_user_id!("@a:b.c"),
555                origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
556                unsigned: StateUnsigned::new(),
557                state_key: EmptyStateKey,
558            }),
559        ));
560
561        assert_matches!(
562            is_suitable_for_latest_event(&event, None),
563            PossibleLatestEvent::NoUnsupportedEventType
564        );
565    }
566
567    #[cfg(feature = "e2e-encryption")]
568    #[test]
569    fn test_replacement_events_are_unsuitable() {
570        let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
571        event_content.relates_to = Some(Relation::Replacement(Replacement::new(
572            owned_event_id!("$1"),
573            RoomMessageEventContent::text_plain("Hello, world!").into(),
574        )));
575
576        let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
577            SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
578                content: event_content,
579                event_id: owned_event_id!("$2"),
580                sender: owned_user_id!("@a:b.c"),
581                origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
582                unsigned: MessageLikeUnsigned::new(),
583            }),
584        ));
585
586        assert_matches!(
587            is_suitable_for_latest_event(&event, None),
588            PossibleLatestEvent::NoUnsupportedMessageLikeType
589        );
590    }
591
592    #[test]
593    fn test_deserialize_latest_event() {
594        #[derive(Debug, serde::Serialize, serde::Deserialize)]
595        struct TestStruct {
596            latest_event: LatestEvent,
597        }
598
599        let event = TimelineEvent::new(
600            Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
601        );
602
603        let initial = TestStruct {
604            latest_event: LatestEvent {
605                event: event.clone(),
606                sender_profile: None,
607                sender_name_is_ambiguous: None,
608            },
609        };
610
611        // When serialized, LatestEvent always uses the new format.
612        let serialized = serde_json::to_value(&initial).unwrap();
613        assert_eq!(
614            serialized,
615            json!({
616                "latest_event": {
617                    "event": {
618                        "kind": {
619                            "PlainText": {
620                                "event": {
621                                    "event_id": "$1"
622                                }
623                            }
624                        }
625                    },
626                }
627            })
628        );
629
630        // And it can be properly deserialized from the new format.
631        let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
632        assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
633        assert!(deserialized.latest_event.sender_profile.is_none());
634        assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
635
636        // The previous format can also be deserialized.
637        let serialized = json!({
638                "latest_event": {
639                    "event": {
640                        "encryption_info": null,
641                        "event": {
642                            "event_id": "$1"
643                        }
644                    },
645                }
646        });
647
648        let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
649        assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
650        assert!(deserialized.latest_event.sender_profile.is_none());
651        assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
652
653        // The even older format can also be deserialized.
654        let serialized = json!({
655            "latest_event": event
656        });
657
658        let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
659        assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
660        assert!(deserialized.latest_event.sender_profile.is_none());
661        assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
662    }
663}