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