Skip to main content

fractal/session/room/
mod.rs

1use std::{cell::RefCell, collections::HashSet};
2
3use futures_util::StreamExt;
4use gettextrs::gettext;
5use gtk::{
6    glib,
7    glib::{clone, closure_local},
8    prelude::*,
9    subclass::prelude::*,
10};
11use matrix_sdk::{
12    Result as MatrixResult, RoomDisplayName, RoomInfo, RoomMemberships, RoomState,
13    deserialized_responses::{AmbiguityChange, RawSyncOrStrippedState},
14    event_handler::EventHandlerDropGuard,
15    room::Room as MatrixRoom,
16    send_queue::RoomSendQueueUpdate,
17};
18use ruma::{
19    EventId, MatrixToUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
20    api::client::{
21        error::{ErrorKind, LimitExceededErrorData, RetryAfter},
22        receipt::create_receipt::v3::ReceiptType as ApiReceiptType,
23    },
24    events::room::{
25        guest_access::GuestAccess,
26        history_visibility::HistoryVisibility,
27        member::{MembershipState, RoomMemberEventContent, SyncRoomMemberEvent},
28    },
29    room_version_rules::RoomVersionRules,
30};
31use serde::Deserialize;
32use tokio_stream::wrappers::BroadcastStream;
33use tracing::{debug, error, warn};
34
35mod aliases;
36mod category;
37mod highlight_flags;
38mod join_rule;
39mod member;
40mod member_list;
41mod permissions;
42mod timeline;
43mod typing_list;
44
45pub(crate) use self::{
46    aliases::{AddAltAliasError, RegisterLocalAliasError, RoomAliases},
47    category::{RoomCategory, TargetRoomCategory},
48    highlight_flags::HighlightFlags,
49    join_rule::{JoinRule, JoinRuleValue},
50    member::{Member, Membership},
51    member_list::*,
52    permissions::*,
53    timeline::*,
54    typing_list::TypingList,
55};
56use super::{
57    IdentityVerification, Session, User, notifications::NotificationsRoomSetting,
58    room_list::RoomMetainfo,
59};
60use crate::{
61    components::{AtRoom, AvatarImage, AvatarUriSource, PillSource},
62    gettext_f,
63    prelude::*,
64    spawn, spawn_tokio,
65    utils::{BoundObjectWeakRef, string::linkify},
66};
67
68/// The default duration in seconds that we wait for before retrying failed
69/// sending requests.
70const DEFAULT_RETRY_AFTER: u32 = 30;
71
72mod imp {
73    use std::{
74        cell::{Cell, OnceCell},
75        marker::PhantomData,
76        sync::LazyLock,
77        time::SystemTime,
78    };
79
80    use glib::subclass::Signal;
81
82    use super::*;
83
84    #[derive(Default, glib::Properties)]
85    #[properties(wrapper_type = super::Room)]
86    pub struct Room {
87        /// The room API of the SDK.
88        matrix_room: OnceCell<MatrixRoom>,
89        /// The current session.
90        #[property(get, set = Self::set_session, construct_only)]
91        session: glib::WeakRef<Session>,
92        /// The ID of this room, as a string.
93        #[property(get = Self::room_id_string)]
94        room_id_string: PhantomData<String>,
95        /// The aliases of this room.
96        #[property(get)]
97        aliases: RoomAliases,
98        /// The name that is set for this room.
99        ///
100        /// This can be empty, the display name should be used instead in the
101        /// interface.
102        #[property(get)]
103        name: RefCell<Option<String>>,
104        /// Whether this room has an avatar explicitly set.
105        ///
106        /// This is `false` if there is no avatar or if the avatar is the one
107        /// from the other member.
108        #[property(get)]
109        has_avatar: Cell<bool>,
110        /// The topic of this room.
111        #[property(get)]
112        topic: RefCell<Option<String>>,
113        /// The linkified topic of this room.
114        ///
115        /// This is the string that should be used in the interface when markup
116        /// is allowed.
117        #[property(get)]
118        topic_linkified: RefCell<Option<String>>,
119        /// The category of this room.
120        #[property(get, builder(RoomCategory::default()))]
121        category: Cell<RoomCategory>,
122        /// Whether this room is a direct chat.
123        #[property(get)]
124        is_direct: Cell<bool>,
125        /// Whether this room has been upgraded.
126        #[property(get)]
127        is_tombstoned: Cell<bool>,
128        /// The ID of the room that was upgraded and that this one replaces.
129        pub(super) predecessor_id: OnceCell<OwnedRoomId>,
130        /// The ID of the room that was upgraded and that this one replaces, as
131        /// a string.
132        #[property(get = Self::predecessor_id_string)]
133        predecessor_id_string: PhantomData<Option<String>>,
134        /// The ID of the successor of this Room, if this room was upgraded.
135        pub(super) successor_id: OnceCell<OwnedRoomId>,
136        /// The ID of the successor of this Room, if this room was upgraded, as
137        /// a string.
138        #[property(get = Self::successor_id_string)]
139        successor_id_string: PhantomData<Option<String>>,
140        /// The successor of this Room, if this room was upgraded and the
141        /// successor was joined.
142        #[property(get)]
143        successor: glib::WeakRef<super::Room>,
144        /// The members of this room.
145        #[property(get)]
146        pub(super) members: glib::WeakRef<MemberList>,
147        members_drop_guard: OnceCell<EventHandlerDropGuard>,
148        /// The number of joined members in the room, according to the
149        /// homeserver.
150        #[property(get)]
151        joined_members_count: Cell<u64>,
152        /// The member corresponding to our own user.
153        #[property(get)]
154        own_member: OnceCell<Member>,
155        /// Whether this room is a current invite or an invite that was declined
156        /// or retracted.
157        #[property(get)]
158        is_invite: Cell<bool>,
159        /// The user who sent the invite to this room.
160        ///
161        /// This is only set when this room is an invitation.
162        #[property(get)]
163        inviter: RefCell<Option<Member>>,
164        /// The other member of the room, if this room is a direct chat and
165        /// there is only one other member.
166        #[property(get)]
167        direct_member: RefCell<Option<Member>>,
168        /// The live timeline of this room.
169        #[property(get)]
170        live_timeline: OnceCell<Timeline>,
171        /// The timestamp of the room's latest activity.
172        ///
173        /// This is the timestamp of the latest event that counts as possibly
174        /// unread.
175        ///
176        /// If it is not known, it will return `0`.
177        #[property(get)]
178        latest_activity: Cell<u64>,
179        /// Whether this room is marked as unread.
180        #[property(get)]
181        is_marked_unread: Cell<bool>,
182        /// Whether all messages of this room are read.
183        #[property(get)]
184        is_read: Cell<bool>,
185        /// The number of unread notifications of this room.
186        #[property(get)]
187        notification_count: Cell<u64>,
188        /// whether this room has unread notifications.
189        #[property(get)]
190        has_notifications: Cell<bool>,
191        /// The highlight state of the room.
192        #[property(get)]
193        highlight: Cell<HighlightFlags>,
194        /// Whether this room is encrypted.
195        #[property(get)]
196        is_encrypted: Cell<bool>,
197        /// The join rule of this room.
198        #[property(get)]
199        join_rule: JoinRule,
200        /// Whether guests are allowed.
201        #[property(get)]
202        guests_allowed: Cell<bool>,
203        /// The visibility of the history.
204        #[property(get, builder(HistoryVisibilityValue::default()))]
205        history_visibility: Cell<HistoryVisibilityValue>,
206        /// The version of this room.
207        #[property(get = Self::version)]
208        version: PhantomData<String>,
209        /// Whether this room is federated.
210        #[property(get = Self::federated)]
211        federated: PhantomData<bool>,
212        /// The list of members currently typing in this room.
213        #[property(get)]
214        typing_list: TypingList,
215        typing_drop_guard: OnceCell<EventHandlerDropGuard>,
216        /// The notifications settings for this room.
217        #[property(get, set = Self::set_notifications_setting, explicit_notify, builder(NotificationsRoomSetting::default()))]
218        notifications_setting: Cell<NotificationsRoomSetting>,
219        /// The permissions of our own user in this room
220        #[property(get)]
221        permissions: Permissions,
222        /// An ongoing identity verification in this room.
223        #[property(get, set = Self::set_verification, nullable, explicit_notify)]
224        verification: BoundObjectWeakRef<IdentityVerification>,
225        /// Whether the room info is initialized.
226        ///
227        /// Used to silence logs during initialization.
228        #[property(get)]
229        is_room_info_initialized: Cell<bool>,
230        /// Whether we already attempted an auto-join.
231        #[property(get)]
232        attempted_auto_join: Cell<bool>,
233        /// Whether this is a call room as defined by [MSC3417](https://github.com/matrix-org/matrix-spec-proposals/pull/3417)
234        #[property(get = Self::is_call)]
235        is_call: PhantomData<bool>,
236    }
237
238    #[glib::object_subclass]
239    impl ObjectSubclass for Room {
240        const NAME: &'static str = "Room";
241        type Type = super::Room;
242        type ParentType = PillSource;
243    }
244
245    #[glib::derived_properties]
246    impl ObjectImpl for Room {
247        fn signals() -> &'static [Signal] {
248            static SIGNALS: LazyLock<Vec<Signal>> =
249                LazyLock::new(|| vec![Signal::builder("room-forgotten").build()]);
250            SIGNALS.as_ref()
251        }
252    }
253
254    impl PillSourceImpl for Room {
255        fn identifier(&self) -> String {
256            self.aliases
257                .alias_string()
258                .unwrap_or_else(|| self.room_id_string())
259        }
260    }
261
262    impl Room {
263        /// Initialize this room.
264        pub(super) fn init(&self, matrix_room: MatrixRoom, metainfo: Option<RoomMetainfo>) {
265            let obj = self.obj();
266
267            self.matrix_room
268                .set(matrix_room)
269                .expect("matrix room is uninitialized");
270
271            self.init_live_timeline();
272            self.aliases.init(&obj);
273            self.load_predecessor();
274            self.watch_members();
275            self.join_rule.init(&obj);
276            self.set_up_typing();
277            self.watch_send_queue();
278
279            spawn!(
280                glib::Priority::DEFAULT_IDLE,
281                clone!(
282                    #[weak(rename_to = imp)]
283                    self,
284                    async move {
285                        imp.update_with_room_info(imp.matrix_room().clone_info())
286                            .await;
287                        imp.watch_room_info();
288
289                        imp.is_room_info_initialized.set(true);
290                        imp.obj().notify_is_room_info_initialized();
291
292                        // Only initialize the following after we have loaded the category of the
293                        // room since we only load them for some categories.
294
295                        // Preload the timeline of rooms that the user is likely to visit and for
296                        // which we offer to show the timeline.
297                        let preload = matches!(
298                            imp.category.get(),
299                            RoomCategory::Favorite
300                                | RoomCategory::Normal
301                                | RoomCategory::LowPriority
302                        );
303                        imp.live_timeline().set_preload(preload);
304
305                        imp.permissions.init(&imp.obj()).await;
306                    }
307                )
308            );
309
310            spawn!(
311                glib::Priority::DEFAULT_IDLE,
312                clone!(
313                    #[weak(rename_to = imp)]
314                    self,
315                    async move {
316                        imp.load_own_member().await;
317                    }
318                )
319            );
320
321            if let Some(RoomMetainfo {
322                latest_activity,
323                is_read,
324            }) = metainfo
325            {
326                self.set_latest_activity(latest_activity);
327                self.set_is_read(is_read);
328
329                self.update_highlight();
330            }
331        }
332
333        /// The room API of the SDK.
334        pub(super) fn matrix_room(&self) -> &MatrixRoom {
335            self.matrix_room.get().expect("matrix room was initialized")
336        }
337
338        /// Set the current session
339        fn set_session(&self, session: &Session) {
340            self.session.set(Some(session));
341
342            let own_member = Member::new(&self.obj(), session.user_id().clone());
343            self.own_member
344                .set(own_member)
345                .expect("own member was uninitialized");
346        }
347
348        /// The ID of this room.
349        pub(super) fn room_id(&self) -> &RoomId {
350            self.matrix_room().room_id()
351        }
352
353        /// The ID of this room, as a string.
354        fn room_id_string(&self) -> String {
355            self.matrix_room().room_id().to_string()
356        }
357
358        /// Update the name of this room.
359        fn update_name(&self) {
360            let name = self.matrix_room().name().into_clean_string();
361
362            if *self.name.borrow() == name {
363                return;
364            }
365
366            self.name.replace(name);
367            self.obj().notify_name();
368        }
369
370        /// Load the display name from the SDK.
371        async fn update_display_name(&self) {
372            let matrix_room = self.matrix_room().clone();
373            let handle = spawn_tokio!(async move { matrix_room.display_name().await });
374
375            let sdk_display_name = handle
376                .await
377                .expect("task was not aborted")
378                .inspect_err(|error| {
379                    error!("Could not compute display name: {error}");
380                })
381                .ok();
382
383            let mut display_name = if let Some(sdk_display_name) = sdk_display_name {
384                match sdk_display_name {
385                    RoomDisplayName::Named(s)
386                    | RoomDisplayName::Calculated(s)
387                    | RoomDisplayName::Aliased(s) => s,
388                    RoomDisplayName::EmptyWas(s) => {
389                        // Translators: This is the name of a room that is empty but had another
390                        // user before. Do NOT translate the content between
391                        // '{' and '}', this is a variable name.
392                        gettext_f("Empty Room (was {user})", &[("user", &s)])
393                    }
394                    // Translators: This is the name of a room without other users.
395                    RoomDisplayName::Empty => gettext("Empty Room"),
396                }
397            } else {
398                Default::default()
399            };
400
401            display_name.clean_string();
402
403            if display_name.is_empty() {
404                // Translators: This is displayed when the room name is unknown yet.
405                display_name = gettext("Unknown");
406            }
407
408            self.obj().set_display_name(display_name);
409        }
410
411        /// Set whether this room has an avatar explicitly set.
412        fn set_has_avatar(&self, has_avatar: bool) {
413            if self.has_avatar.get() == has_avatar {
414                return;
415            }
416
417            self.has_avatar.set(has_avatar);
418            self.obj().notify_has_avatar();
419        }
420
421        /// Update the avatar of the room.
422        fn update_avatar(&self) {
423            let Some(session) = self.session.upgrade() else {
424                return;
425            };
426
427            let obj = self.obj();
428            let avatar_data = obj.avatar_data();
429            let matrix_room = self.matrix_room();
430
431            let prev_avatar_url = avatar_data.image().and_then(|i| i.uri());
432            let room_avatar_url = matrix_room.avatar_url();
433
434            if prev_avatar_url.is_some() && prev_avatar_url == room_avatar_url {
435                // The avatar did not change.
436                return;
437            }
438
439            if let Some(avatar_url) = room_avatar_url {
440                // The avatar has changed, update it.
441                let avatar_info = matrix_room.avatar_info();
442
443                if let Some(avatar_image) = avatar_data
444                    .image()
445                    .filter(|i| i.uri_source() == AvatarUriSource::Room)
446                {
447                    avatar_image.set_uri_and_info(Some(avatar_url), avatar_info);
448                } else {
449                    let avatar_image = AvatarImage::new(
450                        &session,
451                        AvatarUriSource::Room,
452                        Some(avatar_url),
453                        avatar_info,
454                    );
455
456                    avatar_data.set_image(Some(avatar_image.clone()));
457                }
458
459                self.set_has_avatar(true);
460                return;
461            }
462
463            self.set_has_avatar(false);
464
465            // If we have a direct member, use their avatar.
466            if let Some(direct_member) = self.direct_member.borrow().as_ref() {
467                avatar_data.set_image(direct_member.avatar_data().image());
468            }
469
470            let avatar_image = avatar_data.image();
471
472            if let Some(avatar_image) = avatar_image
473                .as_ref()
474                .filter(|i| i.uri_source() == AvatarUriSource::Room)
475            {
476                // The room has no avatar, make sure we remove it.
477                avatar_image.set_uri_and_info(None, None);
478            } else if avatar_image.is_none() {
479                // We always need an avatar image, even if it is empty.
480                avatar_data.set_image(Some(AvatarImage::new(
481                    &session,
482                    AvatarUriSource::Room,
483                    None,
484                    None,
485                )));
486            }
487        }
488
489        /// Update the topic of this room.
490        fn update_topic(&self) {
491            let topic = self
492                .matrix_room()
493                .topic()
494                .map(|mut s| {
495                    s.strip_nul();
496                    s.truncate_end_whitespaces();
497                    s
498                })
499                .filter(|topic| !topic.is_empty());
500
501            if *self.topic.borrow() == topic {
502                return;
503            }
504
505            let topic_linkified = topic.as_ref().map(|t| {
506                // Detect links.
507                let mut s = linkify(t);
508                // Remove trailing spaces.
509                s.truncate_end_whitespaces();
510                s
511            });
512
513            self.topic.replace(topic);
514            self.topic_linkified.replace(topic_linkified);
515
516            let obj = self.obj();
517            obj.notify_topic();
518            obj.notify_topic_linkified();
519        }
520
521        /// Set the category of this room.
522        fn set_category(&self, category: RoomCategory) {
523            let old_category = self.category.get();
524
525            if old_category == RoomCategory::Outdated || old_category == category {
526                return;
527            }
528
529            self.category.set(category);
530            self.obj().notify_category();
531
532            // Check if the previous state was different.
533            let room_state = self.matrix_room().state();
534            if !old_category.is_state(room_state) {
535                if self.is_room_info_initialized.get() {
536                    debug!(room_id = %self.room_id(), ?room_state, "The state of the room changed");
537                }
538
539                match room_state {
540                    RoomState::Joined => {
541                        if let Some(members) = self.members.upgrade() {
542                            // If we where invited or left before, the list was likely not completed
543                            // or might have changed.
544                            members.reload();
545                        }
546
547                        self.set_up_typing();
548                    }
549                    RoomState::Left
550                    | RoomState::Knocked
551                    | RoomState::Banned
552                    | RoomState::Invited => {}
553                }
554            }
555        }
556
557        /// Update the category from the SDK.
558        pub(super) async fn update_category(&self) {
559            // Do not load the category if this room was upgraded.
560            if self.category.get() == RoomCategory::Outdated {
561                return;
562            }
563
564            self.update_is_invite().await;
565            self.update_inviter().await;
566
567            let matrix_room = self.matrix_room();
568            let state = matrix_room.state();
569
570            // The state changed, reset the attempted auto-join.
571            if state != RoomState::Invited {
572                self.attempted_auto_join.take();
573            }
574
575            let category = match state {
576                RoomState::Joined => {
577                    if matrix_room.is_space() {
578                        RoomCategory::Space
579                    } else if matrix_room.is_favourite() {
580                        RoomCategory::Favorite
581                    } else if matrix_room.is_low_priority() {
582                        RoomCategory::LowPriority
583                    } else {
584                        RoomCategory::Normal
585                    }
586                }
587                RoomState::Invited => {
588                    // Automatically accept invite that was after a knock.
589                    if !self.attempted_auto_join.get()
590                        && self.was_membership(&MembershipState::Knock).await
591                    {
592                        self.attempted_auto_join.set(true);
593
594                        if self
595                            .change_category(TargetRoomCategory::Normal)
596                            .await
597                            .is_ok()
598                        {
599                            // Wait for the next change to move automatically from knocked to
600                            // joined.
601                            return;
602                        }
603                    }
604
605                    if self
606                        .inviter
607                        .borrow()
608                        .as_ref()
609                        .is_some_and(Member::is_ignored)
610                    {
611                        RoomCategory::Ignored
612                    } else {
613                        RoomCategory::Invited
614                    }
615                }
616                RoomState::Knocked => RoomCategory::Knocked,
617                RoomState::Left | RoomState::Banned => RoomCategory::Left,
618            };
619
620            self.set_category(category);
621        }
622
623        /// Set whether this room is a direct chat.
624        async fn set_is_direct(&self, is_direct: bool) {
625            if self.is_direct.get() == is_direct {
626                return;
627            }
628
629            self.is_direct.set(is_direct);
630            self.obj().notify_is_direct();
631
632            self.update_direct_member().await;
633        }
634
635        /// Update whether the room is direct or not.
636        pub(super) async fn update_is_direct(&self) {
637            let matrix_room = self.matrix_room().clone();
638            let handle = spawn_tokio!(async move { matrix_room.is_direct().await });
639
640            match handle.await.expect("task was not aborted") {
641                Ok(is_direct) => self.set_is_direct(is_direct).await,
642                Err(error) => {
643                    error!(room_id = %self.room_id(), "Could not load whether room is direct: {error}");
644                }
645            }
646        }
647
648        /// Update the tombstone for this room.
649        fn update_tombstone(&self) {
650            let matrix_room = self.matrix_room();
651
652            if !matrix_room.is_tombstoned() || self.successor_id.get().is_some() {
653                return;
654            }
655            let obj = self.obj();
656
657            if let Some(successor_id) = matrix_room
658                .tombstone_content()
659                .and_then(|room_tombstone| room_tombstone.replacement_room)
660            {
661                self.successor_id
662                    .set(successor_id)
663                    .expect("successor ID should be uninitialized");
664                obj.notify_successor_id_string();
665            }
666
667            // Try to get the successor.
668            self.update_successor();
669
670            // If the successor was not found, watch for it in the room list.
671            if self.successor.upgrade().is_none()
672                && let Some(session) = self.session.upgrade()
673            {
674                session
675                    .room_list()
676                    .add_tombstoned_room(self.room_id().to_owned());
677            }
678
679            if !self.is_tombstoned.get() {
680                self.is_tombstoned.set(true);
681                obj.notify_is_tombstoned();
682            }
683        }
684
685        /// Update the successor of this room.
686        pub(super) fn update_successor(&self) {
687            if self.category.get() == RoomCategory::Outdated {
688                return;
689            }
690
691            let Some(session) = self.session.upgrade() else {
692                return;
693            };
694            let room_list = session.room_list();
695
696            if let Some(successor) = self
697                .successor_id
698                .get()
699                .and_then(|successor_id| room_list.get(successor_id))
700            {
701                // The Matrix spec says that we should use the "predecessor" field of the
702                // m.room.create event of the successor, not the "successor" field of the
703                // m.room.tombstone event, so check it just to be sure.
704                if successor
705                    .predecessor_id()
706                    .is_some_and(|predecessor_id| predecessor_id == self.room_id())
707                {
708                    self.set_successor(&successor);
709                    return;
710                }
711            }
712
713            // The tombstone event can be redacted and we lose the successor, so search in
714            // the room predecessors of other rooms.
715            for room in room_list.iter::<super::Room>() {
716                let Ok(room) = room else {
717                    break;
718                };
719
720                if room
721                    .predecessor_id()
722                    .is_some_and(|predecessor_id| predecessor_id == self.room_id())
723                {
724                    self.set_successor(&room);
725                    return;
726                }
727            }
728        }
729
730        /// The ID of the room that was upgraded and that this one replaces, as
731        /// a string.
732        fn predecessor_id_string(&self) -> Option<String> {
733            self.predecessor_id.get().map(ToString::to_string)
734        }
735
736        /// Load the predecessor of this room.
737        fn load_predecessor(&self) {
738            let Some(event) = self.matrix_room().create_content() else {
739                return;
740            };
741            let Some(predecessor) = event.predecessor else {
742                return;
743            };
744
745            self.predecessor_id
746                .set(predecessor.room_id)
747                .expect("predecessor ID is uninitialized");
748            self.obj().notify_predecessor_id_string();
749        }
750
751        /// The ID of the successor of this room, if this room was upgraded.
752        fn successor_id_string(&self) -> Option<String> {
753            self.successor_id.get().map(ToString::to_string)
754        }
755
756        /// Set the successor of this room.
757        fn set_successor(&self, successor: &super::Room) {
758            self.successor.set(Some(successor));
759            self.obj().notify_successor();
760
761            self.set_category(RoomCategory::Outdated);
762        }
763
764        /// Watch changes in the members list.
765        fn watch_members(&self) {
766            let matrix_room = self.matrix_room();
767
768            let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
769            let handle = matrix_room.add_event_handler(move |event: SyncRoomMemberEvent| {
770                let obj_weak = obj_weak.clone();
771                async move {
772                    let ctx = glib::MainContext::default();
773                    ctx.spawn(async move {
774                        spawn!(async move {
775                            if let Some(obj) = obj_weak.upgrade() {
776                                obj.imp().handle_member_event(&event);
777                            }
778                        });
779                    });
780                }
781            });
782
783            let drop_guard = matrix_room.client().event_handler_drop_guard(handle);
784            self.members_drop_guard.set(drop_guard).unwrap();
785        }
786
787        /// Handle a member event received via sync
788        fn handle_member_event(&self, event: &SyncRoomMemberEvent) {
789            let user_id = event.state_key();
790
791            if let Some(members) = self.members.upgrade() {
792                members.update_member(user_id.clone());
793            } else if user_id == self.own_member().user_id() {
794                self.own_member().update();
795            } else if let Some(member) = self
796                .direct_member
797                .borrow()
798                .as_ref()
799                .filter(|member| member.user_id() == user_id)
800            {
801                member.update();
802            }
803
804            // It might change the direct member if the number of members changed.
805            spawn!(clone!(
806                #[weak(rename_to = imp)]
807                self,
808                async move {
809                    imp.update_direct_member().await;
810                }
811            ));
812        }
813
814        /// Set the number of joined members in the room, according to the
815        /// homeserver.
816        fn set_joined_members_count(&self, count: u64) {
817            if self.joined_members_count.get() == count {
818                return;
819            }
820
821            self.joined_members_count.set(count);
822            self.obj().notify_joined_members_count();
823        }
824
825        /// The member corresponding to our own user.
826        pub(super) fn own_member(&self) -> &Member {
827            self.own_member.get().expect("Own member was initialized")
828        }
829
830        /// Load our own member from the store.
831        async fn load_own_member(&self) {
832            let own_member = self.own_member();
833            let user_id = own_member.user_id().clone();
834            let matrix_room = self.matrix_room().clone();
835
836            let handle =
837                spawn_tokio!(async move { matrix_room.get_member_no_sync(&user_id).await });
838
839            match handle.await.expect("task was not aborted") {
840                Ok(Some(matrix_member)) => own_member.update_from_room_member(&matrix_member),
841                Ok(None) => {}
842                Err(error) => error!(
843                    "Could not load own member for room {}: {error}",
844                    self.room_id()
845                ),
846            }
847        }
848
849        /// Update whether this room is a current invite or an invite that was
850        /// declined or retracted.
851        async fn update_is_invite(&self) {
852            let matrix_room = self.matrix_room();
853
854            let is_invite = match matrix_room.state() {
855                RoomState::Invited => true,
856                RoomState::Left | RoomState::Banned => {
857                    self.was_membership(&MembershipState::Invite).await
858                }
859                _ => false,
860            };
861
862            if self.is_invite.get() == is_invite {
863                return;
864            }
865
866            self.is_invite.set(is_invite);
867            self.obj().notify_is_invite();
868        }
869
870        /// Check whether the previous membership of our user in this room
871        /// matches the one that is given.
872        async fn was_membership(&self, membership: &MembershipState) -> bool {
873            let matrix_room = self.matrix_room();
874
875            // To know if this was an invite we need to check in the member event of our own
876            // user if the current membership is `invite`, or if the current membership is
877            // `leave` or `ban`, and the previous membership was `invite`.
878            let matrix_room_clone = matrix_room.clone();
879            let handle = spawn_tokio!(async move {
880                matrix_room_clone
881                    .get_state_event_static_for_key::<RoomMemberEventContent, _>(
882                        matrix_room_clone.own_user_id(),
883                    )
884                    .await
885            });
886
887            let raw_member_event = match handle.await.expect("task was not aborted") {
888                Ok(Some(raw_member_event)) => raw_member_event,
889                Ok(None) => {
890                    return false;
891                }
892                Err(error) => {
893                    error!("Could not get own member event: {error}");
894                    return false;
895                }
896            };
897
898            let member_event = match raw_member_event {
899                RawSyncOrStrippedState::Sync(raw) => {
900                    raw.deserialize_as_unchecked::<RoomMemberMembershipEvent>()
901                }
902                RawSyncOrStrippedState::Stripped(raw) => raw.deserialize_as_unchecked(),
903            };
904
905            let member_event = match member_event {
906                Ok(member_event) => member_event,
907                Err(error) => {
908                    warn!("Could not deserialize room member event: {error}");
909                    return false;
910                }
911            };
912
913            // Check the current membership event, in case we did not get a state update
914            // with the latest change.
915            if member_event.content.membership == *membership {
916                return true;
917            }
918
919            // Check the previous membership, in case we did get a state update with the
920            // latest change.
921            if let Some(prev_content) = member_event
922                .unsigned
923                .as_ref()
924                .and_then(|unsigned| unsigned.prev_content.as_ref())
925            {
926                return prev_content.membership == *membership;
927            }
928
929            // If we do not have the `prev_content`, we need to fetch the previous state
930            // event.
931            let Some(replaces_state) = member_event
932                .unsigned
933                .and_then(|unsigned| unsigned.replaces_state)
934            else {
935                return false;
936            };
937
938            let matrix_room = matrix_room.clone();
939            let handle = spawn_tokio!(async move {
940                matrix_room.load_or_fetch_event(&replaces_state, None).await
941            });
942
943            let raw_prev_member_event = match handle.await.expect("task was not aborted") {
944                Ok(event) => event,
945                Err(error) => {
946                    warn!("Could not fetch previous member event: {error}");
947                    return false;
948                }
949            };
950
951            match raw_prev_member_event
952                .kind
953                .raw()
954                .deserialize_as_unchecked::<RoomMemberMembershipEvent>()
955            {
956                Ok(prev_member_event) => prev_member_event.content.membership == *membership,
957                Err(error) => {
958                    warn!("Could not deserialize previous member event: {error}");
959                    false
960                }
961            }
962        }
963
964        /// Update the member that invited us to this room.
965        async fn update_inviter(&self) {
966            let matrix_room = self.matrix_room();
967
968            // We are only interested in the inviter for current invites.
969            if matrix_room.state() != RoomState::Invited {
970                if self.inviter.take().is_some() {
971                    self.obj().notify_inviter();
972                }
973
974                return;
975            }
976
977            let matrix_room = matrix_room.clone();
978            let handle = spawn_tokio!(async move { matrix_room.invite_details().await });
979
980            let invite = match handle.await.expect("task was not aborted") {
981                Ok(invite) => invite,
982                Err(error) => {
983                    error!("Could not get invite: {error}");
984                    return;
985                }
986            };
987
988            let Some(inviter_member) = invite.inviter else {
989                if self.inviter.take().is_some() {
990                    self.obj().notify_inviter();
991                }
992                return;
993            };
994
995            if let Some(inviter) = self
996                .inviter
997                .borrow()
998                .as_ref()
999                .filter(|inviter| inviter.user_id() == inviter_member.user_id())
1000            {
1001                // Just update the member.
1002                inviter.update_from_room_member(&inviter_member);
1003
1004                return;
1005            }
1006
1007            let inviter = Member::new(&self.obj(), inviter_member.user_id().to_owned());
1008            inviter.update_from_room_member(&inviter_member);
1009
1010            inviter
1011                .upcast_ref::<User>()
1012                .connect_is_ignored_notify(clone!(
1013                    #[weak(rename_to = imp)]
1014                    self,
1015                    move |_| {
1016                        spawn!(async move {
1017                            // When the user is ignored, this invite should be ignored too.
1018                            imp.update_category().await;
1019                        });
1020                    }
1021                ));
1022
1023            self.inviter.replace(Some(inviter));
1024
1025            self.obj().notify_inviter();
1026        }
1027
1028        /// Set the other member of the room, if this room is a direct chat and
1029        /// there is only one other member.
1030        fn set_direct_member(&self, member: Option<Member>) {
1031            if *self.direct_member.borrow() == member {
1032                return;
1033            }
1034
1035            self.direct_member.replace(member);
1036            self.obj().notify_direct_member();
1037            self.update_avatar();
1038        }
1039
1040        /// The ID of the other user, if this is a direct chat and there is only
1041        /// one other user.
1042        async fn direct_user_id(&self) -> Option<OwnedUserId> {
1043            let matrix_room = self.matrix_room();
1044
1045            // Check if the room is direct and if there is only one target.
1046            let mut direct_targets = matrix_room
1047                .direct_targets()
1048                .into_iter()
1049                .filter_map(|id| OwnedUserId::try_from(id).ok());
1050
1051            let Some(direct_target_user_id) = direct_targets.next() else {
1052                // It is not a direct chat.
1053                return None;
1054            };
1055
1056            if direct_targets.next().is_some() {
1057                // It is a direct chat with several users.
1058                return None;
1059            }
1060
1061            // Check that there are still at most 2 members.
1062            let members_count = matrix_room.active_members_count();
1063
1064            if members_count > 2 {
1065                // We only want a 1-to-1 room. The count might be 1 if the other user left, but
1066                // we can reinvite them.
1067                return None;
1068            }
1069
1070            // Check that the members count is correct. It might not be correct if the room
1071            // was just joined, or if it is in an invited state.
1072            let matrix_room_clone = matrix_room.clone();
1073            let handle =
1074                spawn_tokio!(
1075                    async move { matrix_room_clone.members(RoomMemberships::ACTIVE).await }
1076                );
1077
1078            let members = match handle.await.expect("task was not aborted") {
1079                Ok(m) => m,
1080                Err(error) => {
1081                    error!("Could not load room members: {error}");
1082                    vec![]
1083                }
1084            };
1085
1086            let members_count = members_count.max(members.len() as u64);
1087            if members_count > 2 {
1088                // Same as before.
1089                return None;
1090            }
1091
1092            let own_user_id = matrix_room.own_user_id();
1093            // Get the other member from the list.
1094            for member in members {
1095                let user_id = member.user_id();
1096
1097                if user_id != direct_target_user_id && user_id != own_user_id {
1098                    // There is a non-direct member.
1099                    return None;
1100                }
1101            }
1102
1103            Some(direct_target_user_id)
1104        }
1105
1106        /// Update the other member of the room, if this room is a direct chat
1107        /// and there is only one other member.
1108        async fn update_direct_member(&self) {
1109            let Some(direct_user_id) = self.direct_user_id().await else {
1110                self.set_direct_member(None);
1111                return;
1112            };
1113
1114            if self
1115                .direct_member
1116                .borrow()
1117                .as_ref()
1118                .is_some_and(|m| *m.user_id() == direct_user_id)
1119            {
1120                // Already up-to-date.
1121                return;
1122            }
1123
1124            let direct_member = if let Some(members) = self.members.upgrade() {
1125                members.get_or_create(direct_user_id.clone())
1126            } else {
1127                Member::new(&self.obj(), direct_user_id.clone())
1128            };
1129
1130            let matrix_room = self.matrix_room().clone();
1131            let handle =
1132                spawn_tokio!(async move { matrix_room.get_member_no_sync(&direct_user_id).await });
1133
1134            match handle.await.expect("task was not aborted") {
1135                Ok(Some(matrix_member)) => {
1136                    direct_member.update_from_room_member(&matrix_member);
1137                }
1138                Ok(None) => {}
1139                Err(error) => {
1140                    error!("Could not get direct member: {error}");
1141                }
1142            }
1143
1144            self.set_direct_member(Some(direct_member));
1145        }
1146
1147        /// Initialize the live timeline of this room.
1148        fn init_live_timeline(&self) {
1149            let timeline = self
1150                .live_timeline
1151                .get_or_init(|| Timeline::new(&self.obj()));
1152
1153            timeline.connect_read_change_trigger(clone!(
1154                #[weak(rename_to = imp)]
1155                self,
1156                move |_| {
1157                    spawn!(glib::Priority::DEFAULT_IDLE, async move {
1158                        imp.handle_read_change_trigger().await;
1159                    });
1160                }
1161            ));
1162        }
1163
1164        /// The live timeline of this room.
1165        fn live_timeline(&self) -> &Timeline {
1166            self.live_timeline
1167                .get()
1168                .expect("live timeline is initialized")
1169        }
1170
1171        /// Set the timestamp of the room's latest possibly unread event.
1172        pub(super) fn set_latest_activity(&self, latest_activity: u64) {
1173            if self.latest_activity.get() == latest_activity {
1174                return;
1175            }
1176
1177            self.latest_activity.set(latest_activity);
1178            self.obj().notify_latest_activity();
1179        }
1180
1181        /// Update whether this room is marked as unread.
1182        async fn update_is_marked_unread(&self) {
1183            let is_marked_unread = self.matrix_room().is_marked_unread();
1184
1185            if self.is_marked_unread.get() == is_marked_unread {
1186                return;
1187            }
1188
1189            self.is_marked_unread.set(is_marked_unread);
1190            self.handle_read_change_trigger().await;
1191            self.obj().notify_is_marked_unread();
1192        }
1193
1194        /// Set whether all messages of this room are read.
1195        fn set_is_read(&self, is_read: bool) {
1196            if self.is_read.get() == is_read {
1197                return;
1198            }
1199
1200            self.is_read.set(is_read);
1201            self.obj().notify_is_read();
1202        }
1203
1204        /// Handle the trigger emitted when a read change might have occurred.
1205        async fn handle_read_change_trigger(&self) {
1206            let timeline = self.live_timeline();
1207
1208            if self.is_marked_unread.get() {
1209                self.set_is_read(false);
1210            } else if let Some(has_unread) = timeline.has_unread_messages().await {
1211                self.set_is_read(!has_unread);
1212            }
1213
1214            self.update_highlight();
1215        }
1216
1217        /// Set how this room is highlighted.
1218        fn set_highlight(&self, highlight: HighlightFlags) {
1219            if self.highlight.get() == highlight {
1220                return;
1221            }
1222
1223            self.highlight.set(highlight);
1224            self.obj().notify_highlight();
1225        }
1226
1227        /// Update the highlight of the room from the current state.
1228        fn update_highlight(&self) {
1229            let mut highlight = HighlightFlags::empty();
1230
1231            if matches!(self.category.get(), RoomCategory::Left) {
1232                // Consider that all left rooms are read.
1233                self.set_highlight(highlight);
1234                self.set_notification_count(0);
1235                return;
1236            }
1237
1238            if self.is_read.get() {
1239                self.set_notification_count(0);
1240            } else {
1241                let counts = self.matrix_room().unread_notification_counts();
1242
1243                if counts.highlight_count > 0 {
1244                    highlight = HighlightFlags::all();
1245                } else {
1246                    highlight = HighlightFlags::BOLD;
1247                }
1248                self.set_notification_count(counts.notification_count);
1249            }
1250
1251            self.set_highlight(highlight);
1252        }
1253
1254        /// Set the number of unread notifications of this room.
1255        fn set_notification_count(&self, count: u64) {
1256            if self.notification_count.get() == count {
1257                return;
1258            }
1259
1260            self.notification_count.set(count);
1261            self.set_has_notifications(count > 0);
1262            self.obj().notify_notification_count();
1263        }
1264
1265        /// Set whether this room has unread notifications.
1266        fn set_has_notifications(&self, has_notifications: bool) {
1267            if self.has_notifications.get() == has_notifications {
1268                return;
1269            }
1270
1271            self.has_notifications.set(has_notifications);
1272            self.obj().notify_has_notifications();
1273        }
1274
1275        /// Update whether the room is encrypted from the SDK.
1276        async fn update_is_encrypted(&self) {
1277            let matrix_room = self.matrix_room();
1278            let matrix_room_clone = matrix_room.clone();
1279            let handle =
1280                spawn_tokio!(async move { matrix_room_clone.latest_encryption_state().await });
1281
1282            match handle.await.expect("task was not aborted") {
1283                Ok(state) => {
1284                    if state.is_encrypted() {
1285                        self.is_encrypted.set(true);
1286                        self.obj().notify_is_encrypted();
1287                    }
1288                }
1289                Err(error) => {
1290                    // It can be expected to not be allowed to access the encryption state if the
1291                    // user was never in the room, so do not add noise in the logs.
1292                    if matches!(matrix_room.state(), RoomState::Invited | RoomState::Knocked)
1293                        && error
1294                            .as_client_api_error()
1295                            .is_some_and(|e| e.status_code.is_client_error())
1296                    {
1297                        debug!("Could not load room encryption state: {error}");
1298                    } else {
1299                        error!("Could not load room encryption state: {error}");
1300                    }
1301                }
1302            }
1303        }
1304
1305        /// Update whether guests are allowed.
1306        fn update_guests_allowed(&self) {
1307            let matrix_room = self.matrix_room();
1308            let guests_allowed = matrix_room.guest_access() == GuestAccess::CanJoin;
1309
1310            if self.guests_allowed.get() == guests_allowed {
1311                return;
1312            }
1313
1314            self.guests_allowed.set(guests_allowed);
1315            self.obj().notify_guests_allowed();
1316        }
1317
1318        /// Update the visibility of the history.
1319        fn update_history_visibility(&self) {
1320            let matrix_room = self.matrix_room();
1321            let visibility = matrix_room.history_visibility_or_default().into();
1322
1323            if self.history_visibility.get() == visibility {
1324                return;
1325            }
1326
1327            self.history_visibility.set(visibility);
1328            self.obj().notify_history_visibility();
1329        }
1330
1331        /// The version of this room.
1332        fn version(&self) -> String {
1333            self.matrix_room()
1334                .create_content()
1335                .map(|c| c.room_version.to_string())
1336                .unwrap_or_default()
1337        }
1338
1339        /// If this is a Call room as defined by [MSC3417].
1340        ///
1341        /// [MSC3417]: <https://github.com/matrix-org/matrix-spec-proposals/pull/3417>
1342        fn is_call(&self) -> bool {
1343            self.matrix_room().is_call()
1344        }
1345
1346        /// The rules for the version of this room.
1347        pub(super) fn rules(&self) -> RoomVersionRules {
1348            self.matrix_room()
1349                .clone_info()
1350                .room_version_rules_or_default()
1351        }
1352
1353        /// Whether this room is federated.
1354        fn federated(&self) -> bool {
1355            self.matrix_room()
1356                .create_content()
1357                .is_some_and(|c| c.federate)
1358        }
1359
1360        /// Start listening to typing events.
1361        fn set_up_typing(&self) {
1362            if self.typing_drop_guard.get().is_some() {
1363                // The event handler is already set up.
1364                return;
1365            }
1366
1367            let matrix_room = self.matrix_room();
1368            if matrix_room.state() != RoomState::Joined {
1369                return;
1370            }
1371
1372            let (typing_drop_guard, receiver) = matrix_room.subscribe_to_typing_notifications();
1373            let stream = BroadcastStream::new(receiver);
1374
1375            let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
1376            let fut = stream.for_each(move |typing_user_ids| {
1377                let obj_weak = obj_weak.clone();
1378                async move {
1379                    let Ok(typing_user_ids) = typing_user_ids else {
1380                        return;
1381                    };
1382
1383                    let ctx = glib::MainContext::default();
1384                    ctx.spawn(async move {
1385                        spawn!(async move {
1386                            if let Some(obj) = obj_weak.upgrade() {
1387                                obj.imp().update_typing_list(typing_user_ids);
1388                            }
1389                        });
1390                    });
1391                }
1392            });
1393            spawn_tokio!(fut);
1394
1395            self.typing_drop_guard
1396                .set(typing_drop_guard)
1397                .expect("typing drop guard is uninitialized");
1398        }
1399
1400        /// Update the typing list with the given user IDs.
1401        fn update_typing_list(&self, typing_user_ids: Vec<OwnedUserId>) {
1402            let Some(session) = self.session.upgrade() else {
1403                return;
1404            };
1405
1406            let Some(members) = self.members.upgrade() else {
1407                // If we don't have a members list, the room is not shown so we don't need to
1408                // update the typing list.
1409                self.typing_list.update(vec![]);
1410                return;
1411            };
1412
1413            let own_user_id = session.user_id();
1414
1415            let members = typing_user_ids
1416                .into_iter()
1417                .filter(|user_id| user_id != own_user_id)
1418                .map(|user_id| members.get_or_create(user_id))
1419                .collect();
1420
1421            self.typing_list.update(members);
1422        }
1423
1424        /// Set the notifications setting for this room.
1425        fn set_notifications_setting(&self, setting: NotificationsRoomSetting) {
1426            if self.notifications_setting.get() == setting {
1427                return;
1428            }
1429
1430            self.notifications_setting.set(setting);
1431            self.obj().notify_notifications_setting();
1432        }
1433
1434        /// Set an ongoing verification in this room.
1435        fn set_verification(&self, verification: Option<IdentityVerification>) {
1436            if self.verification.obj().is_some() && verification.is_some() {
1437                // Just keep the same verification until it is dropped. Then we will look if
1438                // there is an ongoing verification in the room.
1439                return;
1440            }
1441
1442            self.verification.disconnect_signals();
1443
1444            let verification = verification.or_else(|| {
1445                // Look if there is an ongoing verification to replace it with.
1446                let room_id = self.matrix_room().room_id();
1447                self.session
1448                    .upgrade()
1449                    .map(|s| s.verification_list())
1450                    .and_then(|list| list.ongoing_room_verification(room_id))
1451            });
1452
1453            if let Some(verification) = &verification {
1454                let state_handler = verification.connect_is_finished_notify(clone!(
1455                    #[weak(rename_to = imp)]
1456                    self,
1457                    move |_| {
1458                        imp.set_verification(None);
1459                    }
1460                ));
1461
1462                let dismiss_handler = verification.connect_dismiss(clone!(
1463                    #[weak(rename_to = imp)]
1464                    self,
1465                    move |_| {
1466                        imp.set_verification(None);
1467                    }
1468                ));
1469
1470                self.verification
1471                    .set(verification, vec![state_handler, dismiss_handler]);
1472            }
1473
1474            self.obj().notify_verification();
1475        }
1476
1477        /// Watch the SDK's room info for changes to the room state.
1478        fn watch_room_info(&self) {
1479            let matrix_room = self.matrix_room();
1480            let subscriber = matrix_room.subscribe_info();
1481
1482            let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
1483            let fut = subscriber.for_each(move |room_info| {
1484                let obj_weak = obj_weak.clone();
1485                async move {
1486                    let ctx = glib::MainContext::default();
1487                    ctx.spawn(async move {
1488                        spawn!(async move {
1489                            if let Some(obj) = obj_weak.upgrade() {
1490                                obj.imp().update_with_room_info(room_info).await;
1491                            }
1492                        });
1493                    });
1494                }
1495            });
1496            spawn_tokio!(fut);
1497        }
1498
1499        /// Update this room with the given SDK room info.
1500        async fn update_with_room_info(&self, room_info: RoomInfo) {
1501            self.aliases.update();
1502            self.update_name();
1503            self.update_display_name().await;
1504            self.update_avatar();
1505            self.update_topic();
1506            self.update_category().await;
1507            self.update_is_direct().await;
1508            self.update_is_marked_unread().await;
1509            self.update_tombstone();
1510            self.set_joined_members_count(room_info.joined_members_count());
1511            self.update_is_encrypted().await;
1512            self.join_rule.update(room_info.join_rule());
1513            self.update_guests_allowed();
1514            self.update_history_visibility();
1515        }
1516
1517        /// Handle changes in the ambiguity of members display names.
1518        pub(super) fn handle_ambiguity_changes<'a>(
1519            &self,
1520            changes: impl Iterator<Item = &'a AmbiguityChange>,
1521        ) {
1522            // Use a set to make sure we update members only once.
1523            let user_ids = changes
1524                .flat_map(AmbiguityChange::user_ids)
1525                .collect::<HashSet<_>>();
1526
1527            if let Some(members) = self.members.upgrade() {
1528                for user_id in user_ids {
1529                    members.update_member(user_id.to_owned());
1530                }
1531            } else {
1532                let own_member = self.own_member();
1533                let own_user_id = own_member.user_id();
1534
1535                if user_ids.contains(&**own_user_id) {
1536                    own_member.update();
1537                }
1538            }
1539        }
1540
1541        /// Watch errors in the send queue to try to handle them.
1542        fn watch_send_queue(&self) {
1543            let matrix_room = self.matrix_room().clone();
1544
1545            let room_weak = glib::SendWeakRef::from(self.obj().downgrade());
1546            spawn_tokio!(async move {
1547                let send_queue = matrix_room.send_queue();
1548                let subscriber = match send_queue.subscribe().await {
1549                    Ok((_, subscriber)) => BroadcastStream::new(subscriber),
1550                    Err(error) => {
1551                        warn!("Failed to listen to room send queue: {error}");
1552                        return;
1553                    }
1554                };
1555
1556                subscriber
1557                    .for_each(move |update| {
1558                        let room_weak = room_weak.clone();
1559                        async move {
1560                            let Ok(RoomSendQueueUpdate::SendError {
1561                                error,
1562                                is_recoverable: true,
1563                                ..
1564                            }) = update
1565                            else {
1566                                return;
1567                            };
1568
1569                            let ctx = glib::MainContext::default();
1570                            ctx.spawn(async move {
1571                                spawn!(async move {
1572                                    let Some(obj) = room_weak.upgrade() else {
1573                                        return;
1574                                    };
1575                                    let Some(session) = obj.session() else {
1576                                        return;
1577                                    };
1578
1579                                    if session.is_offline() {
1580                                        // The queue will be restarted when the session is back
1581                                        // online.
1582                                        return;
1583                                    }
1584
1585                                    let duration = match error.client_api_error_kind() {
1586                                        Some(ErrorKind::LimitExceeded(
1587                                            LimitExceededErrorData {
1588                                                retry_after: Some(retry_after),
1589                                                ..
1590                                            },
1591                                        )) => match retry_after {
1592                                            RetryAfter::Delay(duration) => Some(*duration),
1593                                            RetryAfter::DateTime(time) => {
1594                                                time.duration_since(SystemTime::now()).ok()
1595                                            }
1596                                        },
1597                                        _ => None,
1598                                    };
1599                                    let retry_after = duration
1600                                        .and_then(|d| d.as_secs().try_into().ok())
1601                                        .unwrap_or(DEFAULT_RETRY_AFTER);
1602
1603                                    glib::timeout_add_seconds_local_once(retry_after, move || {
1604                                        let matrix_room = obj.matrix_room().clone();
1605                                        // Getting a room's send queue requires a tokio executor.
1606                                        spawn_tokio!(async move {
1607                                            matrix_room.send_queue().set_enabled(true);
1608                                        });
1609                                    });
1610                                });
1611                            });
1612                        }
1613                    })
1614                    .await;
1615            });
1616        }
1617
1618        /// Change the category of this room.
1619        ///
1620        /// This makes the necessary to propagate the category to the
1621        /// homeserver.
1622        ///
1623        /// This can be used to trigger actions like join or leave, as well as
1624        /// changing the category in the sidebar.
1625        ///
1626        /// Note that rooms cannot change category once they are upgraded.
1627        pub(super) async fn change_category(
1628            &self,
1629            category: TargetRoomCategory,
1630        ) -> MatrixResult<()> {
1631            let previous_category = self.category.get();
1632
1633            if previous_category == category {
1634                return Ok(());
1635            }
1636
1637            if previous_category == RoomCategory::Outdated {
1638                warn!("Cannot change the category of an upgraded room");
1639                return Ok(());
1640            }
1641
1642            self.set_category(category.into());
1643
1644            let matrix_room = self.matrix_room().clone();
1645            let handle = spawn_tokio!(async move {
1646                let room_state = matrix_room.state();
1647
1648                match category {
1649                    TargetRoomCategory::Favorite => {
1650                        if !matrix_room.is_favourite() {
1651                            // This method handles removing the low priority tag.
1652                            matrix_room.set_is_favourite(true, None).await?;
1653                        } else if matrix_room.is_low_priority() {
1654                            matrix_room.set_is_low_priority(false, None).await?;
1655                        }
1656
1657                        if matches!(room_state, RoomState::Invited | RoomState::Left) {
1658                            matrix_room.join().await?;
1659                        }
1660                    }
1661                    TargetRoomCategory::Normal => {
1662                        if matrix_room.is_favourite() {
1663                            matrix_room.set_is_favourite(false, None).await?;
1664                        }
1665                        if matrix_room.is_low_priority() {
1666                            matrix_room.set_is_low_priority(false, None).await?;
1667                        }
1668
1669                        if matches!(room_state, RoomState::Invited | RoomState::Left) {
1670                            matrix_room.join().await?;
1671                        }
1672                    }
1673                    TargetRoomCategory::LowPriority => {
1674                        if !matrix_room.is_low_priority() {
1675                            // This method handles removing the favourite tag.
1676                            matrix_room.set_is_low_priority(true, None).await?;
1677                        } else if matrix_room.is_favourite() {
1678                            matrix_room.set_is_favourite(false, None).await?;
1679                        }
1680
1681                        if matches!(room_state, RoomState::Invited | RoomState::Left) {
1682                            matrix_room.join().await?;
1683                        }
1684                    }
1685                    TargetRoomCategory::Left => {
1686                        if matches!(
1687                            room_state,
1688                            RoomState::Knocked | RoomState::Invited | RoomState::Joined
1689                        ) {
1690                            matrix_room.leave().await?;
1691                        }
1692                    }
1693                }
1694
1695                Result::<_, matrix_sdk::Error>::Ok(())
1696            });
1697
1698            match handle.await.expect("task was not aborted") {
1699                Ok(()) => Ok(()),
1700                Err(error) => {
1701                    error!("Could not set the room category: {error}");
1702
1703                    // Reset the category
1704                    Box::pin(self.update_category()).await;
1705
1706                    Err(error)
1707                }
1708            }
1709        }
1710    }
1711}
1712
1713glib::wrapper! {
1714    /// GObject representation of a Matrix room.
1715    ///
1716    /// Handles populating the Timeline.
1717    pub struct Room(ObjectSubclass<imp::Room>) @extends PillSource;
1718}
1719
1720impl Room {
1721    /// Create a new `Room` for the given session, with the given room API.
1722    pub fn new(session: &Session, matrix_room: MatrixRoom, metainfo: Option<RoomMetainfo>) -> Self {
1723        let this = glib::Object::builder::<Self>()
1724            .property("session", session)
1725            .build();
1726
1727        this.imp().init(matrix_room, metainfo);
1728        this
1729    }
1730
1731    /// The room API of the SDK.
1732    pub(crate) fn matrix_room(&self) -> &MatrixRoom {
1733        self.imp().matrix_room()
1734    }
1735
1736    /// The ID of this room.
1737    pub(crate) fn room_id(&self) -> &RoomId {
1738        self.imp().room_id()
1739    }
1740
1741    /// Get a human-readable ID for this `Room`.
1742    ///
1743    /// This shows the display name and room ID to identify the room easily in
1744    /// logs.
1745    pub fn human_readable_id(&self) -> String {
1746        format!("{} ({})", self.display_name(), self.room_id())
1747    }
1748
1749    /// The rules for the version of this room.
1750    pub(crate) fn rules(&self) -> RoomVersionRules {
1751        self.imp().rules()
1752    }
1753
1754    /// Whether this room is joined.
1755    pub(crate) fn is_joined(&self) -> bool {
1756        self.own_member().membership() == Membership::Join
1757    }
1758
1759    /// The ID of the predecessor of this room, if this room is an upgrade to a
1760    /// previous room.
1761    pub(crate) fn predecessor_id(&self) -> Option<&OwnedRoomId> {
1762        self.imp().predecessor_id.get()
1763    }
1764
1765    /// The ID of the successor of this Room, if this room was upgraded.
1766    pub(crate) fn successor_id(&self) -> Option<&OwnedRoomId> {
1767        self.imp().successor_id.get()
1768    }
1769
1770    /// The `matrix.to` URI representation for this room.
1771    pub(crate) async fn matrix_to_uri(&self) -> MatrixToUri {
1772        let matrix_room = self.matrix_room().clone();
1773
1774        let handle = spawn_tokio!(async move { matrix_room.matrix_to_permalink().await });
1775        match handle.await.expect("task was not aborted") {
1776            Ok(permalink) => {
1777                return permalink;
1778            }
1779            Err(error) => {
1780                error!("Could not get room event permalink: {error}");
1781            }
1782        }
1783
1784        // Fallback to using just the room ID, without routing.
1785        self.room_id().matrix_to_uri()
1786    }
1787
1788    /// The `matrix.to` URI representation for the given event in this room.
1789    pub(crate) async fn matrix_to_event_uri(&self, event_id: OwnedEventId) -> MatrixToUri {
1790        let matrix_room = self.matrix_room().clone();
1791
1792        let event_id_clone = event_id.clone();
1793        let handle =
1794            spawn_tokio!(
1795                async move { matrix_room.matrix_to_event_permalink(event_id_clone).await }
1796            );
1797        match handle.await.expect("task was not aborted") {
1798            Ok(permalink) => {
1799                return permalink;
1800            }
1801            Err(error) => {
1802                error!("Could not get room event permalink: {error}");
1803            }
1804        }
1805
1806        // Fallback to using just the room ID, without routing.
1807        self.room_id().matrix_to_event_uri(event_id)
1808    }
1809
1810    /// Constructs an `AtRoom` for this room.
1811    pub(crate) fn at_room(&self) -> AtRoom {
1812        AtRoom::new(self)
1813    }
1814
1815    /// Get or create the list of members of this room.
1816    ///
1817    /// This creates the [`MemberList`] if no strong reference to it exists.
1818    pub(crate) fn get_or_create_members(&self) -> MemberList {
1819        let members = &self.imp().members;
1820        if let Some(list) = members.upgrade() {
1821            list
1822        } else {
1823            let list = MemberList::new(self);
1824            members.set(Some(&list));
1825            self.notify_members();
1826            list
1827        }
1828    }
1829
1830    /// Change the category of this room.
1831    ///
1832    /// This makes the necessary to propagate the category to the homeserver.
1833    ///
1834    /// This can be used to trigger actions like join or leave, as well as
1835    /// changing the category in the sidebar.
1836    ///
1837    /// Note that rooms cannot change category once they are upgraded.
1838    pub(crate) async fn change_category(&self, category: TargetRoomCategory) -> MatrixResult<()> {
1839        self.imp().change_category(category).await
1840    }
1841
1842    /// Toggle the `key` reaction on the given related event in this room.
1843    pub(crate) async fn toggle_reaction(&self, key: String, event: &Event) -> Result<(), ()> {
1844        let matrix_timeline = self.live_timeline().matrix_timeline();
1845        let identifier = event.identifier();
1846
1847        let handle =
1848            spawn_tokio!(async move { matrix_timeline.toggle_reaction(&identifier, &key).await });
1849
1850        if let Err(error) = handle.await.expect("task was not aborted") {
1851            error!("Could not toggle reaction: {error}");
1852            return Err(());
1853        }
1854
1855        Ok(())
1856    }
1857
1858    /// Send the given receipt.
1859    ///
1860    /// This will also unmark the room as unread.
1861    pub(crate) async fn send_receipt(
1862        &self,
1863        receipt_type: ApiReceiptType,
1864        position: ReceiptPosition,
1865    ) {
1866        let Some(session) = self.session() else {
1867            return;
1868        };
1869        let send_public_receipt = session.settings().public_read_receipts_enabled();
1870
1871        let receipt_type = match receipt_type {
1872            ApiReceiptType::Read if !send_public_receipt => ApiReceiptType::ReadPrivate,
1873            t => t,
1874        };
1875
1876        let matrix_timeline = self.live_timeline().matrix_timeline();
1877        let handle = spawn_tokio!(async move {
1878            match position {
1879                ReceiptPosition::End => matrix_timeline.mark_as_read(receipt_type).await,
1880                ReceiptPosition::Event(event_id) => {
1881                    matrix_timeline
1882                        .send_single_receipt(receipt_type, event_id)
1883                        .await
1884                }
1885            }
1886        });
1887
1888        if let Err(error) = handle.await.expect("task was not aborted") {
1889            error!("Could not send read receipt: {error}");
1890        }
1891    }
1892
1893    /// Mark the room as unread.
1894    pub(crate) async fn mark_as_unread(&self) {
1895        let matrix_room = self.matrix_room().clone();
1896        let handle = spawn_tokio!(async move { matrix_room.set_unread_flag(true).await });
1897
1898        if let Err(error) = handle.await.expect("task was not aborted") {
1899            error!("Could not mark room as unread: {error}");
1900        }
1901    }
1902
1903    /// Send a typing notification for this room, with the given typing state.
1904    pub(crate) fn send_typing_notification(&self, is_typing: bool) {
1905        let matrix_room = self.matrix_room();
1906        if matrix_room.state() != RoomState::Joined {
1907            return;
1908        }
1909
1910        let matrix_room = matrix_room.clone();
1911        let handle = spawn_tokio!(async move { matrix_room.typing_notice(is_typing).await });
1912
1913        spawn!(glib::Priority::DEFAULT_IDLE, async move {
1914            match handle.await.expect("task was not aborted") {
1915                Ok(()) => {}
1916                Err(error) => error!("Could not send typing notification: {error}"),
1917            }
1918        });
1919    }
1920
1921    /// Redact the given events in this room because of the given reason.
1922    ///
1923    /// Returns `Ok(())` if all the redactions are successful, otherwise
1924    /// returns the list of events that could not be redacted.
1925    pub(crate) async fn redact<'a>(
1926        &self,
1927        events: &'a [OwnedEventId],
1928        reason: Option<String>,
1929    ) -> Result<(), Vec<&'a EventId>> {
1930        let matrix_room = self.matrix_room();
1931        if matrix_room.state() != RoomState::Joined {
1932            return Ok(());
1933        }
1934
1935        let events_clone = events.to_owned();
1936        let matrix_room = matrix_room.clone();
1937        let handle = spawn_tokio!(async move {
1938            let mut failed_redactions = Vec::new();
1939
1940            for (i, event_id) in events_clone.iter().enumerate() {
1941                match matrix_room.redact(event_id, reason.as_deref(), None).await {
1942                    Ok(_) => {}
1943                    Err(error) => {
1944                        error!("Could not redact event with ID {event_id}: {error}");
1945                        failed_redactions.push(i);
1946                    }
1947                }
1948            }
1949
1950            failed_redactions
1951        });
1952
1953        let failed_redactions = handle.await.expect("task was not aborted");
1954        let failed_redactions = failed_redactions
1955            .into_iter()
1956            .map(|i| &*events[i])
1957            .collect::<Vec<_>>();
1958
1959        if failed_redactions.is_empty() {
1960            Ok(())
1961        } else {
1962            Err(failed_redactions)
1963        }
1964    }
1965
1966    /// Report the given events in this room.
1967    ///
1968    /// The events are a list of `(event_id, reason)` tuples.
1969    ///
1970    /// Returns `Ok(())` if all the reports are sent successfully, otherwise
1971    /// returns the list of event IDs that could not be reported.
1972    pub(crate) async fn report_events<'a>(
1973        &self,
1974        events: &'a [(OwnedEventId, Option<String>)],
1975    ) -> Result<(), Vec<&'a EventId>> {
1976        let events_clone = events.to_owned();
1977        let matrix_room = self.matrix_room().clone();
1978        let handle = spawn_tokio!(async move {
1979            let futures = events_clone
1980                .into_iter()
1981                .map(|(event_id, reason)| matrix_room.report_content(event_id, reason));
1982            futures_util::future::join_all(futures).await
1983        });
1984
1985        let mut failed = Vec::new();
1986        for (index, result) in handle
1987            .await
1988            .expect("task was not aborted")
1989            .iter()
1990            .enumerate()
1991        {
1992            match result {
1993                Ok(_) => {}
1994                Err(error) => {
1995                    error!(
1996                        "Could not report content with event ID {}: {error}",
1997                        events[index].0,
1998                    );
1999                    failed.push(&*events[index].0);
2000                }
2001            }
2002        }
2003
2004        if failed.is_empty() {
2005            Ok(())
2006        } else {
2007            Err(failed)
2008        }
2009    }
2010
2011    /// Invite the given users to this room.
2012    ///
2013    /// Returns `Ok(())` if all the invites are sent successfully, otherwise
2014    /// returns the list of users who could not be invited.
2015    pub(crate) async fn invite<'a>(
2016        &self,
2017        user_ids: &'a [OwnedUserId],
2018    ) -> Result<(), Vec<&'a UserId>> {
2019        let matrix_room = self.matrix_room();
2020        if matrix_room.state() != RoomState::Joined {
2021            error!("Can’t invite users, because this room isn’t a joined room");
2022            return Ok(());
2023        }
2024
2025        let user_ids_clone = user_ids.to_owned();
2026        let matrix_room = matrix_room.clone();
2027        let handle = spawn_tokio!(async move {
2028            let invitations = user_ids_clone
2029                .iter()
2030                .map(|user_id| matrix_room.invite_user_by_id(user_id));
2031            futures_util::future::join_all(invitations).await
2032        });
2033
2034        let mut failed_invites = Vec::new();
2035        for (index, result) in handle
2036            .await
2037            .expect("task was not aborted")
2038            .iter()
2039            .enumerate()
2040        {
2041            match result {
2042                Ok(()) => {}
2043                Err(error) => {
2044                    error!("Could not invite user with ID {}: {error}", user_ids[index],);
2045                    failed_invites.push(&*user_ids[index]);
2046                }
2047            }
2048        }
2049
2050        if failed_invites.is_empty() {
2051            Ok(())
2052        } else {
2053            Err(failed_invites)
2054        }
2055    }
2056
2057    /// Kick the given users from this room.
2058    ///
2059    /// The users are a list of `(user_id, reason)` tuples.
2060    ///
2061    /// Returns `Ok(())` if all the kicks are sent successfully, otherwise
2062    /// returns the list of users who could not be kicked.
2063    pub(crate) async fn kick<'a>(
2064        &self,
2065        users: &'a [(OwnedUserId, Option<String>)],
2066    ) -> Result<(), Vec<&'a UserId>> {
2067        let users_clone = users.to_owned();
2068        let matrix_room = self.matrix_room().clone();
2069        let handle = spawn_tokio!(async move {
2070            let futures = users_clone
2071                .iter()
2072                .map(|(user_id, reason)| matrix_room.kick_user(user_id, reason.as_deref()));
2073            futures_util::future::join_all(futures).await
2074        });
2075
2076        let mut failed_kicks = Vec::new();
2077        for (index, result) in handle
2078            .await
2079            .expect("task was not aborted")
2080            .iter()
2081            .enumerate()
2082        {
2083            match result {
2084                Ok(()) => {}
2085                Err(error) => {
2086                    error!("Could not kick user with ID {}: {error}", users[index].0);
2087                    failed_kicks.push(&*users[index].0);
2088                }
2089            }
2090        }
2091
2092        if failed_kicks.is_empty() {
2093            Ok(())
2094        } else {
2095            Err(failed_kicks)
2096        }
2097    }
2098
2099    /// Ban the given users from this room.
2100    ///
2101    /// The users are a list of `(user_id, reason)` tuples.
2102    ///
2103    /// Returns `Ok(())` if all the bans are sent successfully, otherwise
2104    /// returns the list of users who could not be banned.
2105    pub(crate) async fn ban<'a>(
2106        &self,
2107        users: &'a [(OwnedUserId, Option<String>)],
2108    ) -> Result<(), Vec<&'a UserId>> {
2109        let users_clone = users.to_owned();
2110        let matrix_room = self.matrix_room().clone();
2111        let handle = spawn_tokio!(async move {
2112            let futures = users_clone
2113                .iter()
2114                .map(|(user_id, reason)| matrix_room.ban_user(user_id, reason.as_deref()));
2115            futures_util::future::join_all(futures).await
2116        });
2117
2118        let mut failed_bans = Vec::new();
2119        for (index, result) in handle
2120            .await
2121            .expect("task was not aborted")
2122            .iter()
2123            .enumerate()
2124        {
2125            match result {
2126                Ok(()) => {}
2127                Err(error) => {
2128                    error!("Could not ban user with ID {}: {error}", users[index].0);
2129                    failed_bans.push(&*users[index].0);
2130                }
2131            }
2132        }
2133
2134        if failed_bans.is_empty() {
2135            Ok(())
2136        } else {
2137            Err(failed_bans)
2138        }
2139    }
2140
2141    /// Unban the given users from this room.
2142    ///
2143    /// The users are a list of `(user_id, reason)` tuples.
2144    ///
2145    /// Returns `Ok(())` if all the unbans are sent successfully, otherwise
2146    /// returns the list of users who could not be unbanned.
2147    pub(crate) async fn unban<'a>(
2148        &self,
2149        users: &'a [(OwnedUserId, Option<String>)],
2150    ) -> Result<(), Vec<&'a UserId>> {
2151        let users_clone = users.to_owned();
2152        let matrix_room = self.matrix_room().clone();
2153        let handle = spawn_tokio!(async move {
2154            let futures = users_clone
2155                .iter()
2156                .map(|(user_id, reason)| matrix_room.unban_user(user_id, reason.as_deref()));
2157            futures_util::future::join_all(futures).await
2158        });
2159
2160        let mut failed_unbans = Vec::new();
2161        for (index, result) in handle
2162            .await
2163            .expect("task was not aborted")
2164            .iter()
2165            .enumerate()
2166        {
2167            match result {
2168                Ok(()) => {}
2169                Err(error) => {
2170                    error!("Could not unban user with ID {}: {error}", users[index].0);
2171                    failed_unbans.push(&*users[index].0);
2172                }
2173            }
2174        }
2175
2176        if failed_unbans.is_empty() {
2177            Ok(())
2178        } else {
2179            Err(failed_unbans)
2180        }
2181    }
2182
2183    /// Enable encryption for this room.
2184    pub(crate) async fn enable_encryption(&self) -> Result<(), ()> {
2185        if self.is_encrypted() {
2186            // Nothing to do.
2187            return Ok(());
2188        }
2189
2190        let matrix_room = self.matrix_room().clone();
2191        let handle = spawn_tokio!(async move { matrix_room.enable_encryption().await });
2192
2193        match handle.await.expect("task was not aborted") {
2194            Ok(()) => Ok(()),
2195            Err(error) => {
2196                error!("Could not enable room encryption: {error}");
2197                Err(())
2198            }
2199        }
2200    }
2201
2202    /// Forget a room that is left.
2203    pub(crate) async fn forget(&self) -> MatrixResult<()> {
2204        if self.category() != RoomCategory::Left {
2205            warn!("Cannot forget a room that is not left");
2206            return Ok(());
2207        }
2208
2209        let matrix_room = self.matrix_room().clone();
2210        let handle = spawn_tokio!(async move { matrix_room.forget().await });
2211
2212        match handle.await.expect("task was not aborted") {
2213            Ok(()) => {
2214                self.emit_by_name::<()>("room-forgotten", &[]);
2215                Ok(())
2216            }
2217            Err(error) => {
2218                error!("Could not forget the room: {error}");
2219                Err(error)
2220            }
2221        }
2222    }
2223
2224    /// Handle room member name ambiguity changes.
2225    pub(crate) fn handle_ambiguity_changes<'a>(
2226        &self,
2227        changes: impl Iterator<Item = &'a AmbiguityChange>,
2228    ) {
2229        self.imp().handle_ambiguity_changes(changes);
2230    }
2231
2232    /// Update the latest activity of the room with the given events.
2233    ///
2234    /// The events must be in reverse chronological order.
2235    fn update_latest_activity<'a>(&self, events: impl Iterator<Item = &'a Event>) {
2236        let own_user_id = self.imp().own_member().user_id();
2237        let mut latest_activity = self.latest_activity();
2238
2239        for event in events {
2240            if event.counts_as_activity(own_user_id) {
2241                latest_activity = latest_activity.max(event.origin_server_ts().get().into());
2242                break;
2243            }
2244        }
2245
2246        self.imp().set_latest_activity(latest_activity);
2247    }
2248
2249    /// Update the successor of this room.
2250    pub(crate) fn update_successor(&self) {
2251        self.imp().update_successor();
2252    }
2253
2254    /// Connect to the signal emitted when the room was forgotten.
2255    pub(crate) fn connect_room_forgotten<F: Fn(&Self) + 'static>(
2256        &self,
2257        f: F,
2258    ) -> glib::SignalHandlerId {
2259        self.connect_closure(
2260            "room-forgotten",
2261            true,
2262            closure_local!(move |obj: Self| {
2263                f(&obj);
2264            }),
2265        )
2266    }
2267}
2268
2269/// Supported values for the history visibility.
2270#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
2271#[enum_type(name = "HistoryVisibilityValue")]
2272pub enum HistoryVisibilityValue {
2273    /// Anyone can read.
2274    WorldReadable,
2275    /// Members, since this was selected.
2276    #[default]
2277    Shared,
2278    /// Members, since they were invited.
2279    Invited,
2280    /// Members, since they joined.
2281    Joined,
2282    /// Unsupported value.
2283    Unsupported,
2284}
2285
2286impl From<HistoryVisibility> for HistoryVisibilityValue {
2287    fn from(value: HistoryVisibility) -> Self {
2288        match value {
2289            HistoryVisibility::Invited => Self::Invited,
2290            HistoryVisibility::Joined => Self::Joined,
2291            HistoryVisibility::Shared => Self::Shared,
2292            HistoryVisibility::WorldReadable => Self::WorldReadable,
2293            _ => Self::Unsupported,
2294        }
2295    }
2296}
2297
2298impl From<HistoryVisibilityValue> for HistoryVisibility {
2299    fn from(value: HistoryVisibilityValue) -> Self {
2300        match value {
2301            HistoryVisibilityValue::Invited => Self::Invited,
2302            HistoryVisibilityValue::Joined => Self::Joined,
2303            HistoryVisibilityValue::Shared => Self::Shared,
2304            HistoryVisibilityValue::WorldReadable => Self::WorldReadable,
2305            HistoryVisibilityValue::Unsupported => unimplemented!(),
2306        }
2307    }
2308}
2309
2310/// The position of the receipt to send.
2311#[derive(Debug, Clone)]
2312pub(crate) enum ReceiptPosition {
2313    /// We are at the end of the timeline (bottom of the view).
2314    End,
2315    /// We are at the event with the given ID.
2316    Event(OwnedEventId),
2317}
2318
2319/// Helper type to extract the current and previous memberships from a raw
2320/// `m.room.member` event.
2321#[derive(Deserialize)]
2322struct RoomMemberMembershipEvent {
2323    content: RoomMemberMembershipContent,
2324    unsigned: Option<RoomMemberMembershipUnsigned>,
2325}
2326
2327/// Helper type to extract the membership of the `unsigned` object of an
2328/// `m.room.member` event.
2329#[derive(Deserialize)]
2330struct RoomMemberMembershipUnsigned {
2331    replaces_state: Option<OwnedEventId>,
2332    prev_content: Option<RoomMemberMembershipContent>,
2333}
2334
2335/// Helper type to extract the membership of the `content` object of an
2336/// `m.room.member` event.
2337#[derive(Deserialize)]
2338struct RoomMemberMembershipContent {
2339    membership: MembershipState,
2340}