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