use std::{cell::RefCell, collections::HashSet, io::Cursor};

use futures_util::StreamExt;
use gettextrs::gettext;
use gtk::{
    glib,
    glib::{clone, closure_local},
    prelude::*,
    subclass::prelude::*,
};
use matrix_sdk::{
    attachment::{generate_image_thumbnail, AttachmentConfig, AttachmentInfo, Thumbnail},
    deserialized_responses::{AmbiguityChange, MemberEvent, SyncTimelineEvent},
    event_handler::EventHandlerDropGuard,
    room::Room as MatrixRoom,
    sync::{JoinedRoomUpdate, LeftRoomUpdate},
    DisplayName, Result as MatrixResult, RoomInfo, RoomMemberships, RoomState,
};
use ruma::{
    events::{
        receipt::{ReceiptEventContent, ReceiptType},
        relation::Annotation,
        room::{
            encryption::SyncRoomEncryptionEvent, guest_access::GuestAccess,
            history_visibility::HistoryVisibility,
        },
        tag::{TagInfo, TagName},
        typing::TypingEventContent,
        AnyMessageLikeEventContent, AnyRoomAccountDataEvent, AnySyncStateEvent,
        AnySyncTimelineEvent, SyncEphemeralRoomEvent, SyncStateEvent,
    },
    EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedRoomAliasId, OwnedRoomId, OwnedUserId,
    RoomId, UserId,
};
use tracing::{debug, error, warn};

mod aliases;
mod event;
mod highlight_flags;
mod join_rule;
mod member;
mod member_list;
mod permissions;
mod room_type;
mod timeline;
mod typing_list;

pub use self::{
    aliases::{AddAltAliasError, RegisterLocalAliasError, RoomAliases},
    event::*,
    highlight_flags::HighlightFlags,
    join_rule::{JoinRule, JoinRuleValue},
    member::{Member, Membership},
    member_list::MemberList,
    permissions::*,
    room_type::RoomType,
    timeline::*,
    typing_list::TypingList,
};
use super::{
    notifications::NotificationsRoomSetting, room_list::RoomMetainfo, IdentityVerification,
    Session, User,
};
use crate::{
    components::{AvatarImage, AvatarUriSource, PillSource},
    gettext_f,
    prelude::*,
    spawn, spawn_tokio,
    utils::BoundObjectWeakRef,
};

mod imp {
    use std::{
        cell::{Cell, OnceCell},
        marker::PhantomData,
    };

    use glib::subclass::Signal;
    use once_cell::sync::Lazy;

    use super::*;

    #[derive(Default, glib::Properties)]
    #[properties(wrapper_type = super::Room)]
    pub struct Room {
        /// The room API of the SDK.
        pub matrix_room: OnceCell<MatrixRoom>,
        /// The current session.
        #[property(get, set = Self::set_session, construct_only)]
        pub session: glib::WeakRef<Session>,
        /// The ID of this room, as a string.
        #[property(get = Self::room_id_string)]
        pub room_id_string: PhantomData<String>,
        /// The aliases of this room.
        #[property(get)]
        pub aliases: RoomAliases,
        /// The version of this room.
        #[property(get = Self::version)]
        pub version: PhantomData<String>,
        /// Whether this room is federated.
        #[property(get = Self::federated)]
        pub federated: PhantomData<bool>,
        /// The name that is set for this room.
        ///
        /// This can be empty, the display name should be used instead in the
        /// interface.
        #[property(get = Self::name)]
        pub name: PhantomData<Option<String>>,
        /// Whether this room has an avatar explicitly set.
        ///
        /// This is `false` if there is no avatar or if the avatar is the one
        /// from the other member.
        #[property(get)]
        pub has_avatar: Cell<bool>,
        /// The category of this room.
        #[property(get, builder(RoomType::default()))]
        pub category: Cell<RoomType>,
        /// The timeline of this room.
        #[property(get)]
        pub timeline: OnceCell<Timeline>,
        /// The member corresponding to our own user.
        #[property(get)]
        pub own_member: OnceCell<Member>,
        /// The members of this room.
        #[property(get)]
        pub members: glib::WeakRef<MemberList>,
        /// The number of joined members in the room, according to the
        /// homeserver.
        #[property(get)]
        pub joined_members_count: Cell<u64>,
        /// The user who sent the invite to this room.
        ///
        /// This is only set when this room is an invitation.
        #[property(get)]
        pub inviter: RefCell<Option<Member>>,
        /// The permissions of our own user in this room
        #[property(get)]
        pub permissions: Permissions,
        /// The timestamp of the room's latest activity.
        ///
        /// This is the timestamp of the latest event that counts as possibly
        /// unread.
        ///
        /// If it is not known, it will return `0`.
        #[property(get)]
        pub latest_activity: Cell<u64>,
        /// Whether all messages of this room are read.
        #[property(get)]
        pub is_read: Cell<bool>,
        /// The highlight state of the room.
        #[property(get)]
        pub highlight: Cell<HighlightFlags>,
        /// The ID of the room that was upgraded and that this one replaces.
        pub predecessor_id: OnceCell<OwnedRoomId>,
        /// The ID of the room that was upgraded and that this one replaces, as
        /// a string.
        #[property(get = Self::predecessor_id_string)]
        pub predecessor_id_string: PhantomData<Option<String>>,
        /// The ID of the successor of this Room, if this room was upgraded.
        pub successor_id: OnceCell<OwnedRoomId>,
        /// The ID of the successor of this Room, if this room was upgraded, as
        /// a string.
        #[property(get = Self::successor_id_string)]
        pub successor_id_string: PhantomData<Option<String>>,
        /// The successor of this Room, if this room was upgraded and the
        /// successor was joined.
        #[property(get)]
        pub successor: glib::WeakRef<super::Room>,
        /// An ongoing identity verification in this room.
        #[property(get, set = Self::set_verification, nullable, explicit_notify)]
        pub verification: BoundObjectWeakRef<IdentityVerification>,
        /// Whether this room is encrypted.
        #[property(get)]
        pub is_encrypted: Cell<bool>,
        /// The list of members currently typing in this room.
        #[property(get)]
        pub typing_list: TypingList,
        /// Whether this room is a direct chat.
        #[property(get)]
        pub is_direct: Cell<bool>,
        /// The other member of the room, if this room is a direct chat and
        /// there is only one other member.
        #[property(get)]
        pub direct_member: RefCell<Option<Member>>,
        /// The number of unread notifications of this room.
        #[property(get = Self::notification_count)]
        pub notification_count: PhantomData<u64>,
        /// The topic of this room.
        #[property(get = Self::topic)]
        pub topic: PhantomData<Option<String>>,
        /// Whether this room has been upgraded.
        #[property(get = Self::is_tombstoned)]
        pub is_tombstoned: PhantomData<bool>,
        /// The notifications settings for this room.
        #[property(get, set = Self::set_notifications_setting, explicit_notify, builder(NotificationsRoomSetting::default()))]
        pub notifications_setting: Cell<NotificationsRoomSetting>,
        /// The join rule of this room.
        #[property(get)]
        pub join_rule: JoinRule,
        /// Whether guests are allowed.
        #[property(get)]
        pub guests_allowed: Cell<bool>,
        /// The visibility of the history.
        #[property(get, builder(HistoryVisibilityValue::default()))]
        pub history_visibility: Cell<HistoryVisibilityValue>,
        pub typing_drop_guard: OnceCell<EventHandlerDropGuard>,
        pub receipts_drop_guard: OnceCell<EventHandlerDropGuard>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for Room {
        const NAME: &'static str = "Room";
        type Type = super::Room;
        type ParentType = PillSource;
    }

    #[glib::derived_properties]
    impl ObjectImpl for Room {
        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> =
                Lazy::new(|| vec![Signal::builder("room-forgotten").build()]);
            SIGNALS.as_ref()
        }
    }

    impl PillSourceImpl for Room {
        fn identifier(&self) -> String {
            self.alias_string().unwrap_or_else(|| self.room_id_string())
        }
    }

    impl Room {
        /// Set the current session
        fn set_session(&self, session: Session) {
            self.session.set(Some(&session));

            let own_member = Member::new(&self.obj(), session.user_id().clone());
            self.own_member.set(own_member).unwrap();
        }

        /// The room API of the SDK.
        pub fn matrix_room(&self) -> &MatrixRoom {
            self.matrix_room.get().unwrap()
        }

        /// The room ID of this room, as a string.
        fn room_id_string(&self) -> String {
            self.matrix_room().room_id().to_string()
        }

        /// The alias of this room.
        pub(super) fn alias(&self) -> Option<OwnedRoomAliasId> {
            let matrix_room = self.matrix_room();
            matrix_room
                .canonical_alias()
                .or_else(|| matrix_room.alt_aliases().into_iter().next())
        }

        /// The alias of this room, as a string.
        fn alias_string(&self) -> Option<String> {
            self.alias().map(Into::into)
        }

        /// The version of this room.
        fn version(&self) -> String {
            self.matrix_room()
                .create_content()
                .map(|c| c.room_version.to_string())
                .unwrap_or_default()
        }

        /// Whether this room is federated.
        fn federated(&self) -> bool {
            self.matrix_room()
                .create_content()
                .map(|c| c.federate)
                .unwrap_or_default()
        }

        /// The name of this room.
        ///
        /// This can be empty, the display name should be used instead in the
        /// interface.
        fn name(&self) -> Option<String> {
            self.matrix_room().name()
        }

        /// Set whether this room has an avatar explicitly set.
        pub fn set_has_avatar(&self, has_avatar: bool) {
            if self.has_avatar.get() == has_avatar {
                return;
            }

            self.has_avatar.set(has_avatar);
            self.obj().notify_has_avatar();
        }

        /// The number of unread notifications of this room.
        fn notification_count(&self) -> u64 {
            self.matrix_room()
                .unread_notification_counts()
                .notification_count
        }

        /// The topic of this room.
        fn topic(&self) -> Option<String> {
            self.matrix_room().topic().filter(|topic| {
                !topic.is_empty() && topic.find(|c: char| !c.is_whitespace()).is_some()
            })
        }

        /// Whether this room was tombstoned.
        fn is_tombstoned(&self) -> bool {
            self.matrix_room().is_tombstoned()
        }

        /// The ID of the room that was upgraded and that this one replaces, as
        /// a string.
        fn predecessor_id_string(&self) -> Option<String> {
            self.predecessor_id.get().map(ToString::to_string)
        }

        /// The ID of the successor of this Room, if this room was upgraded.
        fn successor_id_string(&self) -> Option<String> {
            self.successor_id.get().map(ToString::to_string)
        }

        /// Set an ongoing verification in this room.
        fn set_verification(&self, verification: Option<IdentityVerification>) {
            if self.verification.obj().is_some() && verification.is_some() {
                // Just keep the same verification until it is dropped. Then we will look if
                // there is an ongoing verification in the room.
                return;
            }

            self.verification.disconnect_signals();

            let verification = verification.or_else(|| {
                // Look if there is an ongoing verification to replace it with.
                let room_id = self.matrix_room().room_id();
                self.session
                    .upgrade()
                    .map(|s| s.verification_list())
                    .and_then(|list| list.ongoing_room_verification(room_id))
            });

            if let Some(verification) = &verification {
                let state_handler =
                    verification.connect_is_finished_notify(clone!(@weak self as imp => move |_| {
                        imp.set_verification(None);
                    }));

                let dismiss_handler =
                    verification.connect_dismiss(clone!(@weak self as imp => move |_| {
                        imp.set_verification(None);
                    }));

                self.verification
                    .set(verification, vec![state_handler, dismiss_handler]);
            }

            self.obj().notify_verification();
        }

        /// Set the notifications setting for this room.
        fn set_notifications_setting(&self, setting: NotificationsRoomSetting) {
            if self.notifications_setting.get() == setting {
                return;
            }

            self.notifications_setting.set(setting);
            self.obj().notify_notifications_setting();
        }
    }
}

glib::wrapper! {
    /// GObject representation of a Matrix room.
    ///
    /// Handles populating the Timeline.
    pub struct Room(ObjectSubclass<imp::Room>) @extends PillSource;
}

impl Room {
    pub fn new(session: &Session, matrix_room: MatrixRoom, metainfo: Option<RoomMetainfo>) -> Self {
        let this = glib::Object::builder::<Self>()
            .property("session", session)
            .build();

        this.set_matrix_room(matrix_room);

        if let Some(RoomMetainfo {
            latest_activity,
            is_read,
        }) = metainfo
        {
            this.set_latest_activity(latest_activity);
            this.set_is_read(is_read);

            this.update_highlight();
        }

        this
    }

    /// The room API of the SDK.
    pub fn matrix_room(&self) -> &MatrixRoom {
        self.imp().matrix_room()
    }

    /// Set the room API of the SDK.
    fn set_matrix_room(&self, matrix_room: MatrixRoom) {
        let imp = self.imp();

        self.set_joined_members_count(matrix_room.joined_members_count());

        imp.matrix_room.set(matrix_room).unwrap();

        self.update_avatar();
        self.load_predecessor();
        self.load_tombstone();
        self.load_category();
        self.set_up_receipts();
        self.set_up_typing();
        self.init_timeline();
        self.set_up_is_encrypted();
        self.aliases().init(self);
        self.update_guests_allowed();
        self.update_history_visibility();

        spawn!(
            glib::Priority::DEFAULT_IDLE,
            clone!(@weak self as obj => async move {
                obj.load_display_name().await;
            })
        );

        spawn!(
            glib::Priority::DEFAULT_IDLE,
            clone!(@weak self as obj => async move {
                obj.load_own_member().await;
            })
        );

        spawn!(
            glib::Priority::DEFAULT_IDLE,
            clone!(@weak self as obj => async move {
                obj.load_is_direct().await;
            })
        );

        spawn!(
            glib::Priority::DEFAULT_IDLE,
            clone!(@weak self as obj => async move {
                obj.watch_room_info().await;
            })
        );

        spawn!(
            glib::Priority::DEFAULT_IDLE,
            clone!(@weak self as obj => async move {
                obj.load_inviter().await;
            })
        );

        spawn!(
            glib::Priority::DEFAULT_IDLE,
            clone!(@weak self as obj => async move {
                obj.imp().permissions.init(&obj).await;
            })
        );

        spawn!(
            glib::Priority::DEFAULT_IDLE,
            clone!(@weak self as obj => async move {
                obj.imp().join_rule.init(&obj).await;
            })
        );
    }

    fn init_timeline(&self) {
        let timeline = Timeline::new(self);
        self.imp().timeline.set(timeline.clone()).unwrap();

        timeline
            .sdk_items()
            .connect_items_changed(clone!(@weak self as obj => move |_, _, _, _| {
                spawn!(async move {
                    obj.update_is_read().await;
                });
            }));

        if !matches!(self.category(), RoomType::Left | RoomType::Outdated) {
            // Load the room history when idle.
            spawn!(
                glib::source::Priority::LOW,
                clone!(@weak self as obj => async move {
                    obj.timeline().load().await;
                })
            );
        }
    }

    /// The ID of this room.
    pub fn room_id(&self) -> &RoomId {
        self.matrix_room().room_id()
    }

    /// The alias of this room.
    pub fn alias(&self) -> Option<OwnedRoomAliasId> {
        self.imp().alias()
    }

    /// The state of the room.
    pub fn state(&self) -> RoomState {
        self.matrix_room().state()
    }

    /// Set whether this room is a direct chat.
    fn set_is_direct(&self, is_direct: bool) {
        if self.is_direct() == is_direct {
            return;
        }

        self.imp().is_direct.set(is_direct);
        self.notify_is_direct();

        spawn!(clone!(@weak self as obj => async move {
            obj.load_direct_member().await;
        }));
    }

    /// Load whether the room is direct or not.
    pub async fn load_is_direct(&self) {
        let matrix_room = self.matrix_room().clone();
        let handle = spawn_tokio!(async move { matrix_room.is_direct().await });

        match handle.await.unwrap() {
            Ok(is_direct) => self.set_is_direct(is_direct),
            Err(error) => {
                error!(room_id = %self.room_id(), "Could not load whether room is direct: {error}");
            }
        }
    }

    /// The ID of the other user, if this is a direct chat and there is only one
    /// other user.
    async fn direct_user_id(&self) -> Option<OwnedUserId> {
        let matrix_room = self.matrix_room();

        // Check if the room direct and if there only one target.
        let direct_targets = matrix_room.direct_targets();
        if direct_targets.len() != 1 {
            // It was a direct chat with several users.
            return None;
        }

        let direct_target_user_id = direct_targets.into_iter().next().unwrap();

        // Check that there are still at most 2 members.
        let members_count = matrix_room.active_members_count();

        if members_count > 2 {
            // We only want a 1-to-1 room. The count might be 1 if the other user left, but
            // we can reinvite them.
            return None;
        }

        // Check that the members count is correct. It might not be correct if the room
        // was just joined, or if it is in an invited state.
        let matrix_room_clone = matrix_room.clone();
        let handle =
            spawn_tokio!(async move { matrix_room_clone.members(RoomMemberships::ACTIVE).await });

        let members = match handle.await.unwrap() {
            Ok(m) => m,
            Err(error) => {
                error!("Could not load room members: {error}");
                vec![]
            }
        };

        let members_count = members_count.max(members.len() as u64);
        if members_count > 2 {
            // Same as before.
            return None;
        }

        let own_user_id = matrix_room.own_user_id();
        // Get the other member from the list.
        for member in members {
            let user_id = member.user_id();

            if user_id != direct_target_user_id && user_id != own_user_id {
                // There is a non-direct member.
                return None;
            }
        }

        Some(direct_target_user_id)
    }

    /// Set the other member of the room, if this room is a direct chat and
    /// there is only one other member..
    fn set_direct_member(&self, member: Option<Member>) {
        if self.direct_member() == member {
            return;
        }

        self.imp().direct_member.replace(member);
        self.notify_direct_member();
        self.update_avatar();
    }

    /// Load the other member of the room, if this room is a direct chat and
    /// there is only one other member.
    async fn load_direct_member(&self) {
        let Some(direct_user_id) = self.direct_user_id().await else {
            self.set_direct_member(None);
            return;
        };

        if self
            .direct_member()
            .is_some_and(|m| *m.user_id() == direct_user_id)
        {
            // Already up-to-date.
            return;
        }

        let direct_member = if let Some(members) = self.members() {
            members.get_or_create(direct_user_id.clone())
        } else {
            Member::new(self, direct_user_id.clone())
        };

        let matrix_room = self.matrix_room().clone();
        let handle =
            spawn_tokio!(async move { matrix_room.get_member_no_sync(&direct_user_id).await });

        match handle.await.unwrap() {
            Ok(Some(matrix_member)) => {
                direct_member.update_from_room_member(&matrix_member);
            }
            Ok(None) => {}
            Err(error) => {
                error!("Could not get direct member: {error}");
            }
        }

        self.set_direct_member(Some(direct_member));
    }

    /// Ensure the direct user of this room is an active member.
    ///
    /// If there is supposed to be a direct user in this room but they have left
    /// it, re-invite them.
    ///
    /// This is a noop if there is no supposed direct user or if the user is
    /// already an active member.
    pub async fn ensure_direct_user(&self) -> Result<(), ()> {
        let Some(member) = self.direct_member() else {
            warn!("Cannot ensure direct user in a room without direct target");
            return Ok(());
        };

        if self.matrix_room().active_members_count() == 2 {
            return Ok(());
        }

        self.invite(&[member.user_id().clone()])
            .await
            .map_err(|_| ())
    }

    /// Forget a room that is left.
    pub async fn forget(&self) -> MatrixResult<()> {
        if self.category() != RoomType::Left {
            warn!("Cannot forget a room that is not left");
            return Ok(());
        }

        let matrix_room = self.matrix_room().clone();
        let handle = spawn_tokio!(async move { matrix_room.forget().await });

        match handle.await.unwrap() {
            Ok(_) => {
                self.emit_by_name::<()>("room-forgotten", &[]);
                Ok(())
            }
            Err(error) => {
                error!("Could not forget the room: {error}");

                // Load the previous category
                self.load_category();

                Err(error)
            }
        }
    }

    /// Whether this room is joined.
    pub fn is_joined(&self) -> bool {
        self.own_member().membership() == Membership::Join
    }

    fn set_category_internal(&self, category: RoomType) {
        let old_category = self.category();

        if old_category == RoomType::Outdated || old_category == category {
            return;
        }

        self.imp().category.set(category);
        self.notify_category();
    }

    /// Set the category of this room.
    ///
    /// This makes the necessary to propagate the category to the homeserver.
    ///
    /// Note: Rooms can't be moved to the invite category and they can't be
    /// moved once they are upgraded.
    pub async fn set_category(&self, category: RoomType) -> MatrixResult<()> {
        let previous_category = self.category();

        if previous_category == category {
            return Ok(());
        }

        if previous_category == RoomType::Outdated {
            warn!("Can't set the category of an upgraded room");
            return Ok(());
        }

        match category {
            RoomType::Invited => {
                warn!("Rooms can’t be moved to the invite Category");
                return Ok(());
            }
            RoomType::Outdated => {
                // Outdated rooms don't need to propagate anything to the server
                self.set_category_internal(category);
                return Ok(());
            }
            _ => {}
        }

        self.set_category_internal(category);

        let matrix_room = self.matrix_room().clone();
        let handle = spawn_tokio!(async move {
            match matrix_room.state() {
                RoomState::Invited => match category {
                    RoomType::Invited => {}
                    RoomType::Favorite => {
                        if let Some(tags) = matrix_room.tags().await? {
                            if !tags.contains_key(&TagName::Favorite) {
                                matrix_room
                                    .set_tag(TagName::Favorite, TagInfo::new())
                                    .await?;
                            }
                            if tags.contains_key(&TagName::LowPriority) {
                                matrix_room.remove_tag(TagName::LowPriority).await?;
                            }
                        }
                        matrix_room.join().await?;
                    }
                    RoomType::Normal => {
                        if let Some(tags) = matrix_room.tags().await? {
                            if tags.contains_key(&TagName::Favorite) {
                                matrix_room.remove_tag(TagName::Favorite).await?;
                            }
                            if tags.contains_key(&TagName::LowPriority) {
                                matrix_room.remove_tag(TagName::LowPriority).await?;
                            }
                        }

                        if matrix_room.is_direct().await.unwrap_or_default() {
                            matrix_room.set_is_direct(false).await?;
                        }

                        matrix_room.join().await?;
                    }
                    RoomType::LowPriority => {
                        if let Some(tags) = matrix_room.tags().await? {
                            if tags.contains_key(&TagName::Favorite) {
                                matrix_room.remove_tag(TagName::Favorite).await?;
                            }
                            if !tags.contains_key(&TagName::LowPriority) {
                                matrix_room
                                    .set_tag(TagName::LowPriority, TagInfo::new())
                                    .await?;
                            }
                        }
                        matrix_room.join().await?;
                    }
                    RoomType::Left => {
                        matrix_room.leave().await?;
                    }
                    RoomType::Outdated | RoomType::Space | RoomType::Ignored => unimplemented!(),
                },
                RoomState::Joined => match category {
                    RoomType::Invited => {}
                    RoomType::Favorite => {
                        matrix_room
                            .set_tag(TagName::Favorite, TagInfo::new())
                            .await?;
                        if previous_category == RoomType::LowPriority {
                            matrix_room.remove_tag(TagName::LowPriority).await?;
                        }
                    }
                    RoomType::Normal => match previous_category {
                        RoomType::Favorite => {
                            matrix_room.remove_tag(TagName::Favorite).await?;
                        }
                        RoomType::LowPriority => {
                            matrix_room.remove_tag(TagName::LowPriority).await?;
                        }
                        _ => {}
                    },
                    RoomType::LowPriority => {
                        matrix_room
                            .set_tag(TagName::LowPriority, TagInfo::new())
                            .await?;
                        if previous_category == RoomType::Favorite {
                            matrix_room.remove_tag(TagName::Favorite).await?;
                        }
                    }
                    RoomType::Left => {
                        matrix_room.leave().await?;
                    }
                    RoomType::Outdated | RoomType::Space | RoomType::Ignored => unimplemented!(),
                },
                RoomState::Left => match category {
                    RoomType::Invited => {}
                    RoomType::Favorite => {
                        if let Some(tags) = matrix_room.tags().await? {
                            if !tags.contains_key(&TagName::Favorite) {
                                matrix_room
                                    .set_tag(TagName::Favorite, TagInfo::new())
                                    .await?;
                            }
                            if tags.contains_key(&TagName::LowPriority) {
                                matrix_room.remove_tag(TagName::LowPriority).await?;
                            }
                        }
                        matrix_room.join().await?;
                    }
                    RoomType::Normal => {
                        if let Some(tags) = matrix_room.tags().await? {
                            if tags.contains_key(&TagName::Favorite) {
                                matrix_room.remove_tag(TagName::Favorite).await?;
                            }
                            if tags.contains_key(&TagName::LowPriority) {
                                matrix_room.remove_tag(TagName::LowPriority).await?;
                            }
                        }
                        matrix_room.join().await?;
                    }
                    RoomType::LowPriority => {
                        if let Some(tags) = matrix_room.tags().await? {
                            if tags.contains_key(&TagName::Favorite) {
                                matrix_room.remove_tag(TagName::Favorite).await?;
                            }
                            if !tags.contains_key(&TagName::LowPriority) {
                                matrix_room
                                    .set_tag(TagName::LowPriority, TagInfo::new())
                                    .await?;
                            }
                        }
                        matrix_room.join().await?;
                    }
                    RoomType::Left => {}
                    RoomType::Outdated | RoomType::Space | RoomType::Ignored => unimplemented!(),
                },
            }

            Result::<_, matrix_sdk::Error>::Ok(())
        });

        match handle.await.unwrap() {
            Ok(_) => Ok(()),
            Err(error) => {
                error!("Could not set the room category: {error}");

                // Load the previous category
                self.load_category();

                Err(error)
            }
        }
    }

    /// Load the category from the SDK.
    fn load_category(&self) {
        // Don't load the category if this room was upgraded
        if self.category() == RoomType::Outdated {
            return;
        }

        if self.inviter().is_some_and(|i| i.is_ignored()) {
            self.set_category_internal(RoomType::Ignored);
        }

        let matrix_room = self.matrix_room();
        match matrix_room.state() {
            RoomState::Joined => {
                if matrix_room.is_space() {
                    self.set_category_internal(RoomType::Space);
                } else {
                    let matrix_room = matrix_room.clone();
                    let tags = spawn_tokio!(async move { matrix_room.tags().await });

                    spawn!(
                        glib::Priority::DEFAULT_IDLE,
                        clone!(@weak self as obj => async move {
                            let mut category = RoomType::Normal;

                            if let Ok(Some(tags)) = tags.await.unwrap() {
                                if tags.contains_key(&TagName::Favorite) {
                                    category = RoomType::Favorite;
                                } else if tags.contains_key(&TagName::LowPriority) {
                                    category = RoomType::LowPriority;
                                }
                            }

                            obj.set_category_internal(category);
                        })
                    );
                }
            }
            RoomState::Invited => self.set_category_internal(RoomType::Invited),
            RoomState::Left => self.set_category_internal(RoomType::Left),
        };
    }

    async fn watch_room_info(&self) {
        let matrix_room = self.matrix_room();
        let subscriber = matrix_room.subscribe_info();

        let room_weak = glib::SendWeakRef::from(self.downgrade());
        subscriber
            .for_each(move |room_info| {
                let room_weak = room_weak.clone();
                async move {
                    let ctx = glib::MainContext::default();
                    ctx.spawn(async move {
                        spawn!(async move {
                            if let Some(obj) = room_weak.upgrade() {
                                obj.update_room_info(room_info)
                            }
                        });
                    });
                }
            })
            .await;
    }

    fn update_room_info(&self, room_info: RoomInfo) {
        self.set_joined_members_count(room_info.joined_members_count());
    }

    /// Start listening to typing events.
    fn set_up_typing(&self) {
        let imp = self.imp();
        if imp.typing_drop_guard.get().is_some() {
            // The event handler is already set up.
            return;
        }

        let matrix_room = self.matrix_room();
        if matrix_room.state() != RoomState::Joined {
            return;
        };

        let room_weak = glib::SendWeakRef::from(self.downgrade());
        let handle = matrix_room.add_event_handler(
            move |event: SyncEphemeralRoomEvent<TypingEventContent>| {
                let room_weak = room_weak.clone();
                async move {
                    let ctx = glib::MainContext::default();
                    ctx.spawn(async move {
                        spawn!(async move {
                            if let Some(obj) = room_weak.upgrade() {
                                obj.handle_typing_event(event.content)
                            }
                        });
                    });
                }
            },
        );

        let drop_guard = matrix_room.client().event_handler_drop_guard(handle);
        imp.typing_drop_guard.set(drop_guard).unwrap();
    }

    /// Start listening to read receipts events.
    fn set_up_receipts(&self) {
        let imp = self.imp();
        if imp.receipts_drop_guard.get().is_some() {
            // The event handler is already set up.
            return;
        }

        // Listen to changes in the read receipts.
        let matrix_room = self.matrix_room();
        let room_weak = glib::SendWeakRef::from(self.downgrade());
        let handle = matrix_room.add_event_handler(
            move |event: SyncEphemeralRoomEvent<ReceiptEventContent>| {
                let room_weak = room_weak.clone();
                async move {
                    let ctx = glib::MainContext::default();
                    ctx.spawn(async move {
                        spawn!(async move {
                            if let Some(obj) = room_weak.upgrade() {
                                obj.handle_receipt_event(event.content)
                            }
                        });
                    });
                }
            },
        );

        let drop_guard = matrix_room.client().event_handler_drop_guard(handle);
        imp.receipts_drop_guard.set(drop_guard).unwrap();
    }

    fn handle_receipt_event(&self, content: ReceiptEventContent) {
        let Some(session) = self.session() else {
            return;
        };
        let own_user_id = session.user_id();

        for (_event_id, receipts) in content.iter() {
            if let Some(users) = receipts.get(&ReceiptType::Read) {
                if users.contains_key(own_user_id) {
                    spawn!(clone!(@weak self as obj => async move {
                        obj.update_is_read().await;
                    }));
                }
            }
        }
    }

    fn handle_typing_event(&self, content: TypingEventContent) {
        let Some(session) = self.session() else {
            return;
        };
        let typing_list = &self.imp().typing_list;

        let Some(members) = self.members() else {
            // If we don't have a members list, the room is not shown so we don't need to
            // update the typing list.
            typing_list.update(vec![]);
            return;
        };

        let own_user_id = session.user_id();

        let members = content
            .user_ids
            .into_iter()
            .filter_map(|user_id| (user_id != *own_user_id).then(|| members.get_or_create(user_id)))
            .collect();

        typing_list.update(members);
    }

    /// Create and load our own member from the store.
    async fn load_own_member(&self) {
        let own_member = self.own_member();
        let user_id = own_member.user_id().clone();
        let matrix_room = self.matrix_room().clone();

        let handle = spawn_tokio!(async move { matrix_room.get_member_no_sync(&user_id).await });

        match handle.await.unwrap() {
            Ok(Some(matrix_member)) => own_member.update_from_room_member(&matrix_member),
            Ok(None) => {}
            Err(error) => error!(
                "Could not load own member for room {}: {error}",
                self.room_id()
            ),
        }
    }

    /// The members of this room.
    ///
    /// This creates the [`MemberList`] if no strong reference to it exists.
    pub fn get_or_create_members(&self) -> MemberList {
        let members = &self.imp().members;
        if let Some(list) = members.upgrade() {
            list
        } else {
            let list = MemberList::new(self);
            members.set(Some(&list));
            self.notify_members();
            list
        }
    }

    /// Set the number of joined members in the room, according to the
    /// homeserver.
    fn set_joined_members_count(&self, count: u64) {
        if self.joined_members_count() == count {
            return;
        }

        self.imp().joined_members_count.set(count);
        self.notify_joined_members_count();
    }

    fn update_highlight(&self) {
        let mut highlight = HighlightFlags::empty();

        if matches!(self.category(), RoomType::Left) {
            // Consider that all left rooms are read.
            self.set_highlight(highlight);
            return;
        }

        let counts = self.matrix_room().unread_notification_counts();

        if counts.highlight_count > 0 {
            highlight = HighlightFlags::all();
        } else if counts.notification_count > 0 || !self.is_read() {
            highlight = HighlightFlags::BOLD;
        }

        self.set_highlight(highlight);
    }

    /// Set how this room is highlighted.
    fn set_highlight(&self, highlight: HighlightFlags) {
        if self.highlight() == highlight {
            return;
        }

        self.imp().highlight.set(highlight);
        self.notify_highlight();
    }

    async fn update_is_read(&self) {
        if let Some(has_unread) = self.timeline().has_unread_messages().await {
            self.set_is_read(!has_unread);
        }

        self.update_highlight();
    }

    /// Set whether all messages of this room are read.
    fn set_is_read(&self, is_read: bool) {
        if is_read == self.is_read() {
            return;
        }

        self.imp().is_read.set(is_read);
        self.notify_is_read();
    }

    /// Load the display name from the SDK.
    async fn load_display_name(&self) {
        let matrix_room = self.matrix_room().clone();
        let handle = spawn_tokio!(async move { matrix_room.display_name().await });

        // FIXME: We should retry if the request failed
        match handle.await.unwrap() {
            Ok(display_name) => {
                let name = match display_name {
                    DisplayName::Named(s)
                    | DisplayName::Calculated(s)
                    | DisplayName::Aliased(s) => s,
                    // Translators: This is the name of a room that is empty but had another user
                    // before. Do NOT translate the content between '{' and '}',
                    // this is a variable name.
                    DisplayName::EmptyWas(s) => {
                        gettext_f("Empty Room (was {user})", &[("user", &s)])
                    }
                    // Translators: This is the name of a room without other users.
                    DisplayName::Empty => gettext("Empty Room"),
                };

                self.set_display_name(name);
            }
            Err(error) => error!("Could not fetch display name: {error}"),
        };

        if self.display_name().is_empty() {
            // Translators: This is displayed when the room name is unknown yet.
            self.set_display_name(gettext("Unknown"));
        }
    }

    /// Load the member that invited us to this room, when applicable.
    async fn load_inviter(&self) {
        let Some(session) = self.session() else {
            return;
        };

        let matrix_room = self.matrix_room();

        if matrix_room.state() != RoomState::Invited {
            return;
        }

        let own_user_id = session.user_id().clone();
        let matrix_room_clone = matrix_room.clone();
        let handle =
            spawn_tokio!(async move { matrix_room_clone.get_member_no_sync(&own_user_id).await });

        let own_member = match handle.await.unwrap() {
            Ok(Some(member)) => member,
            Ok(None) => return,
            Err(error) => {
                error!("Could not get room member: {error}");
                return;
            }
        };

        let inviter_id = match &**own_member.event() {
            MemberEvent::Sync(_) => return,
            MemberEvent::Stripped(event) => event.sender.clone(),
        };

        let inviter_id_clone = inviter_id.clone();
        let matrix_room = matrix_room.clone();
        let handle =
            spawn_tokio!(async move { matrix_room.get_member_no_sync(&inviter_id_clone).await });

        let inviter_member = match handle.await.unwrap() {
            Ok(Some(member)) => member,
            Ok(None) => return,
            Err(error) => {
                error!("Could not get room member: {error}");
                return;
            }
        };

        let inviter = Member::new(self, inviter_id);
        inviter.update_from_room_member(&inviter_member);

        inviter.upcast_ref::<User>().connect_is_ignored_notify(
            clone!(@weak self as obj => move |_| {
                obj.load_category();
            }),
        );

        self.imp().inviter.replace(Some(inviter));

        self.notify_inviter();
        self.load_category();
    }

    /// Update the room state based on the new sync response
    /// FIXME: We should use the sdk's event handler to get updates
    pub fn update_for_events(&self, batch: Vec<SyncTimelineEvent>) {
        // FIXME: notify only when the count has changed
        self.notify_notification_count();

        let events: Vec<_> = batch
            .iter()
            .flat_map(|e| e.event.deserialize().ok())
            .collect();
        let own_member = self.own_member();
        let own_user_id = own_member.user_id();
        let direct_member = self.direct_member();
        let direct_member_id = direct_member.as_ref().map(|m| m.user_id());

        for event in events.iter() {
            if let AnySyncTimelineEvent::State(state_event) = event {
                match state_event {
                    AnySyncStateEvent::RoomMember(event) => {
                        let user_id = event.state_key();

                        if let Some(members) = self.members() {
                            members.update_member(user_id.clone());
                        } else if user_id == own_user_id {
                            own_member.update();
                        } else if direct_member_id.is_some_and(|id| id == user_id) {
                            if let Some(member) = &direct_member {
                                member.update();
                            }
                        }

                        // It might change the direct member.
                        spawn!(clone!(@weak self as obj => async move {
                            obj.load_direct_member().await;
                            obj.load_display_name().await;
                        }));
                    }
                    AnySyncStateEvent::RoomAvatar(SyncStateEvent::Original(_)) => {
                        self.update_avatar();
                    }
                    AnySyncStateEvent::RoomName(_) => {
                        self.notify_name();
                        spawn!(clone!(@weak self as obj => async move {
                            obj.load_display_name().await;
                        }));
                    }
                    AnySyncStateEvent::RoomTopic(_) => {
                        self.notify_topic();
                    }
                    AnySyncStateEvent::RoomTombstone(_) => {
                        self.load_tombstone();
                    }
                    AnySyncStateEvent::RoomGuestAccess(_) => {
                        self.update_guests_allowed();
                    }
                    AnySyncStateEvent::RoomHistoryVisibility(_) => {
                        self.update_history_visibility();
                    }
                    _ => {}
                }
            }
        }
    }

    /// Set the timestamp of the room's latest possibly unread event.
    fn set_latest_activity(&self, latest_activity: u64) {
        if latest_activity == self.latest_activity() {
            return;
        }

        self.imp().latest_activity.set(latest_activity);
        self.notify_latest_activity();
    }

    /// Send a message with the given `content` in this room.
    pub fn send_room_message_event(&self, content: impl Into<AnyMessageLikeEventContent>) {
        let timeline = self.timeline().matrix_timeline();
        let content = content.into();

        let handle = spawn_tokio!(async move { timeline.send(content).await });

        spawn!(
            glib::Priority::DEFAULT_IDLE,
            clone!(@weak self as obj => async move {
                handle.await.unwrap();
            })
        );
    }

    /// Toggle a `key` reaction for the `relates_to` event ID in this room.
    pub async fn toggle_reaction(&self, key: String, relates_to: OwnedEventId) -> Result<(), ()> {
        let timeline = self.timeline().matrix_timeline();
        let annotation = Annotation::new(relates_to, key);

        let handle = spawn_tokio!(async move { timeline.toggle_reaction(&annotation).await });

        if let Err(error) = handle.await.unwrap() {
            error!("Could not toggle reaction: {error}");
            return Err(());
        }

        Ok(())
    }

    /// Redact the given events in this room because of the given reason.
    ///
    /// Returns `Ok(())` if all the redactions are successful, otherwise
    /// returns the list of events that could not be redacted.
    pub async fn redact<'a>(
        &self,
        events: &'a [OwnedEventId],
        reason: Option<String>,
    ) -> Result<(), Vec<&'a EventId>> {
        let matrix_room = self.matrix_room();
        if matrix_room.state() != RoomState::Joined {
            return Ok(());
        };

        let events_clone = events.to_owned();
        let matrix_room = matrix_room.clone();
        let handle = spawn_tokio!(async move {
            let mut failed_redactions = Vec::new();

            for (i, event_id) in events_clone.iter().enumerate() {
                match matrix_room.redact(event_id, reason.as_deref(), None).await {
                    Ok(_) => {}
                    Err(error) => {
                        error!("Could not redact event with ID {event_id}: {error}");
                        failed_redactions.push(i);
                    }
                }
            }

            failed_redactions
        });

        let failed_redactions = handle.await.unwrap();
        let failed_redactions = failed_redactions
            .into_iter()
            .map(|i| &*events[i])
            .collect::<Vec<_>>();

        if failed_redactions.is_empty() {
            Ok(())
        } else {
            Err(failed_redactions)
        }
    }

    pub fn send_typing_notification(&self, is_typing: bool) {
        let matrix_room = self.matrix_room();
        if matrix_room.state() != RoomState::Joined {
            return;
        };

        let matrix_room = matrix_room.clone();
        let handle = spawn_tokio!(async move { matrix_room.typing_notice(is_typing).await });

        spawn!(
            glib::Priority::DEFAULT_IDLE,
            clone!(@weak self as obj => async move {
                match handle.await.unwrap() {
                    Ok(_) => {},
                    Err(error) => error!("Could not send typing notification: {error}"),
                };
            })
        );
    }

    pub async fn accept_invite(&self) -> MatrixResult<()> {
        let matrix_room = self.matrix_room();

        if matrix_room.state() != RoomState::Invited {
            error!("Can’t accept invite, because this room isn’t an invited room");
            return Ok(());
        }

        let matrix_room = matrix_room.clone();
        let handle = spawn_tokio!(async move { matrix_room.join().await });
        match handle.await.unwrap() {
            Ok(_) => Ok(()),
            Err(error) => {
                error!("Accepting invitation failed: {error}");
                Err(error)
            }
        }
    }

    pub async fn decline_invite(&self) -> MatrixResult<()> {
        let matrix_room = self.matrix_room();

        if matrix_room.state() != RoomState::Invited {
            error!("Cannot decline invite, because this room is not an invited room");
            return Ok(());
        }

        let matrix_room = matrix_room.clone();
        let handle = spawn_tokio!(async move { matrix_room.leave().await });
        match handle.await.unwrap() {
            Ok(_) => Ok(()),
            Err(error) => {
                error!("Declining invitation failed: {error}");

                Err(error)
            }
        }
    }

    /// Reload the room from the SDK when its state might have changed.
    pub fn update_room(&self) {
        let state = self.matrix_room().state();
        let category = self.category();

        // Check if the previous state was different.
        if category.is_state(state) {
            // Nothing needs to be reloaded.
            return;
        }

        debug!(room_id = %self.room_id(), ?state, "The state of `Room` changed");

        if state == RoomState::Joined {
            if let Some(members) = self.members() {
                // If we where invited or left before, the list was likely not completed or
                // might have changed.
                members.reload();
            }
        }

        self.load_category();
        spawn!(clone!(@weak self as obj => async move {
            obj.load_inviter().await;
        }));
    }

    pub fn handle_left_update(&self, update: LeftRoomUpdate) {
        self.update_for_events(update.timeline.events);
        self.handle_ambiguity_changes(update.ambiguity_changes.values());
    }

    pub fn handle_joined_update(&self, update: JoinedRoomUpdate) {
        if update
            .account_data
            .iter()
            .any(|e| matches!(e.deserialize(), Ok(AnyRoomAccountDataEvent::Tag(_))))
        {
            self.load_category();
        }

        self.update_for_events(update.timeline.events);
        self.handle_ambiguity_changes(update.ambiguity_changes.values());
    }

    /// Handle changes in the ambiguity of members display names.
    fn handle_ambiguity_changes<'a>(&self, changes: impl Iterator<Item = &'a AmbiguityChange>) {
        // Use a set to make sure we update members only once.
        let user_ids = changes.flat_map(|c| c.user_ids()).collect::<HashSet<_>>();

        if let Some(members) = self.members() {
            for user_id in user_ids {
                members.update_member(user_id.to_owned());
            }
        } else {
            let own_member = self.own_member();
            let own_user_id = own_member.user_id();

            for user_id in user_ids {
                if user_id == *own_user_id {
                    own_member.update();
                }
            }
        }
    }

    /// Connect to the signal emitted when the room was forgotten.
    pub fn connect_room_forgotten<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
        self.connect_closure(
            "room-forgotten",
            true,
            closure_local!(move |obj: Self| {
                f(&obj);
            }),
        )
    }

    /// The ID of the predecessor of this room, if this room is an upgrade to a
    /// previous room.
    pub fn predecessor_id(&self) -> Option<&OwnedRoomId> {
        self.imp().predecessor_id.get()
    }

    /// Load the predecessor of this room.
    fn load_predecessor(&self) {
        if self.predecessor_id().is_some() {
            return;
        }

        let Some(event) = self.matrix_room().create_content() else {
            return;
        };
        let Some(predecessor) = event.predecessor else {
            return;
        };

        self.imp().predecessor_id.set(predecessor.room_id).unwrap();
        self.notify_predecessor_id_string();
    }

    /// The ID of the successor of this Room, if this room was upgraded.
    pub fn successor_id(&self) -> Option<&RoomId> {
        self.imp().successor_id.get().map(std::ops::Deref::deref)
    }

    /// Set the successor of this Room.
    fn set_successor(&self, successor: &Room) {
        self.imp().successor.set(Some(successor));
        self.notify_successor();
    }

    /// Load the tombstone for this room.
    pub fn load_tombstone(&self) {
        let imp = self.imp();

        if !self.is_tombstoned() || self.successor_id().is_some() {
            return;
        }

        if let Some(room_tombstone) = self.matrix_room().tombstone() {
            imp.successor_id
                .set(room_tombstone.replacement_room)
                .unwrap();
            self.notify_successor_id_string();
        };

        if !self.update_outdated() {
            if let Some(session) = self.session() {
                session
                    .room_list()
                    .add_tombstoned_room(self.room_id().to_owned());
            }
        }

        self.notify_is_tombstoned();
    }

    /// Update whether this `Room` is outdated.
    ///
    /// A room is outdated when it was tombstoned and we joined its successor.
    ///
    /// Returns `true` if the `Room` was set as outdated, `false` otherwise.
    pub fn update_outdated(&self) -> bool {
        if self.category() == RoomType::Outdated {
            return true;
        }

        let Some(session) = self.session() else {
            return false;
        };
        let room_list = session.room_list();

        if let Some(successor_id) = self.successor_id() {
            if let Some(successor) = room_list.get(successor_id) {
                // The Matrix spec says that we should use the "predecessor" field of the
                // m.room.create event of the successor, not the "successor" field of the
                // m.room.tombstone event, so check it just to be sure.
                if let Some(predecessor_id) = successor.predecessor_id() {
                    if predecessor_id == self.room_id() {
                        self.set_successor(&successor);
                        self.set_category_internal(RoomType::Outdated);
                        return true;
                    }
                }
            }
        }

        // The tombstone event can be redacted and we lose the successor, so search in
        // the room predecessors of other rooms.
        for room in room_list.iter::<Room>() {
            let Ok(room) = room else {
                break;
            };

            if let Some(predecessor_id) = room.predecessor_id() {
                if predecessor_id == self.room_id() {
                    self.set_successor(&room);
                    self.set_category_internal(RoomType::Outdated);
                    return true;
                }
            }
        }

        false
    }

    pub fn send_attachment(
        &self,
        bytes: Vec<u8>,
        mime: mime::Mime,
        body: &str,
        info: AttachmentInfo,
    ) {
        let matrix_room = self.matrix_room();
        if matrix_room.state() != RoomState::Joined {
            return;
        };

        let matrix_room = matrix_room.clone();
        let body = body.to_string();
        spawn_tokio!(async move {
            // Needed to hold the thumbnail data until it is sent.
            let data_slot;

            // The method will filter compatible mime types so we don't need to
            // since we ignore errors.
            let thumbnail = match generate_image_thumbnail(&mime, Cursor::new(&bytes), None) {
                Ok((data, info)) => {
                    data_slot = data;
                    Some(Thumbnail {
                        data: data_slot,
                        content_type: mime::IMAGE_JPEG,
                        info: Some(info),
                    })
                }
                _ => None,
            };

            let config = if let Some(thumbnail) = thumbnail {
                AttachmentConfig::with_thumbnail(thumbnail)
            } else {
                AttachmentConfig::new()
            }
            .info(info);

            matrix_room
                // TODO This should be added to pending messages instead of
                // sending it directly.
                .send_attachment(&body, &mime, bytes, config)
                .await
                .unwrap();
        });
    }

    /// Invite the given users to this room.
    ///
    /// Returns `Ok(())` if all the invites are sent successfully, otherwise
    /// returns the list of users who could not be invited.
    pub async fn invite<'a>(&self, user_ids: &'a [OwnedUserId]) -> Result<(), Vec<&'a UserId>> {
        let matrix_room = self.matrix_room();
        if matrix_room.state() != RoomState::Joined {
            error!("Can’t invite users, because this room isn’t a joined room");
            return Ok(());
        }

        let user_ids_clone = user_ids.to_owned();
        let matrix_room = matrix_room.clone();
        let handle = spawn_tokio!(async move {
            let invitations = user_ids_clone
                .iter()
                .map(|user_id| matrix_room.invite_user_by_id(user_id));
            futures_util::future::join_all(invitations).await
        });

        let mut failed_invites = Vec::new();
        for (index, result) in handle.await.unwrap().iter().enumerate() {
            match result {
                Ok(_) => {}
                Err(error) => {
                    error!("Could not invite user with ID {}: {error}", user_ids[index],);
                    failed_invites.push(&*user_ids[index]);
                }
            }
        }

        if failed_invites.is_empty() {
            Ok(())
        } else {
            Err(failed_invites)
        }
    }

    /// Kick the given users from this room.
    ///
    /// The users are a list of `(user_id, reason)` tuples.
    ///
    /// Returns `Ok(())` if all the kicks are sent successfully, otherwise
    /// returns the list of users who could not be kicked.
    pub async fn kick<'a>(
        &self,
        users: &'a [(OwnedUserId, Option<String>)],
    ) -> Result<(), Vec<&'a UserId>> {
        let users_clone = users.to_owned();
        let matrix_room = self.matrix_room().clone();
        let handle = spawn_tokio!(async move {
            let futures = users_clone
                .iter()
                .map(|(user_id, reason)| matrix_room.kick_user(user_id, reason.as_deref()));
            futures_util::future::join_all(futures).await
        });

        let mut failed_kicks = Vec::new();
        for (index, result) in handle.await.unwrap().iter().enumerate() {
            match result {
                Ok(_) => {}
                Err(error) => {
                    error!("Could not kick user with ID {}: {error}", users[index].0);
                    failed_kicks.push(&*users[index].0);
                }
            }
        }

        if failed_kicks.is_empty() {
            Ok(())
        } else {
            Err(failed_kicks)
        }
    }

    /// Ban the given users from this room.
    ///
    /// The users are a list of `(user_id, reason)` tuples.
    ///
    /// Returns `Ok(())` if all the bans are sent successfully, otherwise
    /// returns the list of users who could not be banned.
    pub async fn ban<'a>(
        &self,
        users: &'a [(OwnedUserId, Option<String>)],
    ) -> Result<(), Vec<&'a UserId>> {
        let users_clone = users.to_owned();
        let matrix_room = self.matrix_room().clone();
        let handle = spawn_tokio!(async move {
            let futures = users_clone
                .iter()
                .map(|(user_id, reason)| matrix_room.ban_user(user_id, reason.as_deref()));
            futures_util::future::join_all(futures).await
        });

        let mut failed_bans = Vec::new();
        for (index, result) in handle.await.unwrap().iter().enumerate() {
            match result {
                Ok(_) => {}
                Err(error) => {
                    error!("Could not ban user with ID {}: {error}", users[index].0);
                    failed_bans.push(&*users[index].0);
                }
            }
        }

        if failed_bans.is_empty() {
            Ok(())
        } else {
            Err(failed_bans)
        }
    }

    /// Unban the given users from this room.
    ///
    /// The users are a list of `(user_id, reason)` tuples.
    ///
    /// Returns `Ok(())` if all the unbans are sent successfully, otherwise
    /// returns the list of users who could not be unbanned.
    pub async fn unban<'a>(
        &self,
        users: &'a [(OwnedUserId, Option<String>)],
    ) -> Result<(), Vec<&'a UserId>> {
        let users_clone = users.to_owned();
        let matrix_room = self.matrix_room().clone();
        let handle = spawn_tokio!(async move {
            let futures = users_clone
                .iter()
                .map(|(user_id, reason)| matrix_room.unban_user(user_id, reason.as_deref()));
            futures_util::future::join_all(futures).await
        });

        let mut failed_unbans = Vec::new();
        for (index, result) in handle.await.unwrap().iter().enumerate() {
            match result {
                Ok(_) => {}
                Err(error) => {
                    error!("Could not unban user with ID {}: {error}", users[index].0);
                    failed_unbans.push(&*users[index].0);
                }
            }
        }

        if failed_unbans.is_empty() {
            Ok(())
        } else {
            Err(failed_unbans)
        }
    }

    /// Update the latest activity of the room with the given events.
    ///
    /// The events must be in reverse chronological order.
    pub fn update_latest_activity<'a>(&self, events: impl IntoIterator<Item = &'a Event>) {
        let mut latest_activity = self.latest_activity();

        for event in events {
            if event.counts_as_unread() {
                latest_activity = latest_activity.max(event.origin_server_ts_u64());
                break;
            }
        }

        self.set_latest_activity(latest_activity);
    }

    /// Listen to changes in room encryption.
    fn set_up_is_encrypted(&self) {
        let matrix_room = self.matrix_room();

        let obj_weak = glib::SendWeakRef::from(self.downgrade());
        matrix_room.add_event_handler(move |_: SyncRoomEncryptionEvent| {
            let obj_weak = obj_weak.clone();
            async move {
                let ctx = glib::MainContext::default();
                ctx.spawn(async move {
                    spawn!(async move {
                        if let Some(obj) = obj_weak.upgrade() {
                            obj.load_is_encrypted().await;
                        }
                    });
                });
            }
        });

        spawn!(
            glib::Priority::DEFAULT_IDLE,
            clone!(@weak self as obj => async move {
                obj.load_is_encrypted().await;
            })
        );
    }

    /// Load whether the room is encrypted from the SDK.
    async fn load_is_encrypted(&self) {
        let matrix_room = self.matrix_room().clone();
        let handle = spawn_tokio!(async move { matrix_room.is_encrypted().await });

        match handle.await.unwrap() {
            Ok(true) => {
                self.imp().is_encrypted.set(true);
                self.notify_is_encrypted();
            }
            Ok(false) => {}
            Err(error) => {
                error!("Could not load room encryption state: {error}");
            }
        }
    }

    /// Enable encryption for this room.
    pub async fn enable_encryption(&self) -> Result<(), ()> {
        if self.is_encrypted() {
            // Nothing to do.
            return Ok(());
        }

        let matrix_room = self.matrix_room().clone();
        let handle = spawn_tokio!(async move { matrix_room.enable_encryption().await });

        match handle.await.unwrap() {
            Ok(_) => Ok(()),
            Err(error) => {
                error!("Could not enabled room encryption: {error}");
                Err(())
            }
        }
    }

    /// Get a human-readable ID for this `Room`.
    ///
    /// This is to identify the room easily in logs.
    pub fn human_readable_id(&self) -> String {
        format!("{} ({})", self.display_name(), self.room_id())
    }

    /// Update the avatar for the room.
    fn update_avatar(&self) {
        let Some(session) = self.session() else {
            return;
        };
        let imp = self.imp();
        let avatar_data = self.avatar_data();

        if let Some(avatar_url) = self.matrix_room().avatar_url() {
            imp.set_has_avatar(true);

            let avatar_image = if let Some(avatar_image) = avatar_data
                .image()
                .filter(|i| i.uri_source() == AvatarUriSource::Room)
            {
                avatar_image
            } else {
                let avatar_image =
                    AvatarImage::new(&session, Some(&avatar_url), AvatarUriSource::Room);
                avatar_data.set_image(Some(avatar_image.clone()));
                avatar_image
            };
            avatar_image.set_uri(Some(avatar_url.to_string()));

            return;
        }

        imp.set_has_avatar(false);

        if let Some(direct_member) = self.direct_member() {
            avatar_data.set_image(direct_member.avatar_data().image());
        }

        if avatar_data.image().is_none() {
            avatar_data.set_image(Some(AvatarImage::new(
                &session,
                None,
                AvatarUriSource::Room,
            )))
        }
    }

    /// The `matrix.to` URI representation for this room.
    pub async fn matrix_to_uri(&self) -> MatrixToUri {
        let matrix_room = self.matrix_room().clone();

        let handle = spawn_tokio!(async move { matrix_room.matrix_to_permalink().await });
        match handle.await.unwrap() {
            Ok(permalink) => {
                return permalink;
            }
            Err(error) => {
                error!("Could not get room event permalink: {error}");
            }
        }

        // Fallback to using just the room ID, without routing.
        self.room_id().matrix_to_uri()
    }

    /// The `matrix:` URI representation for this room.
    pub async fn matrix_uri(&self) -> MatrixUri {
        let matrix_room = self.matrix_room().clone();

        let handle = spawn_tokio!(async move { matrix_room.matrix_permalink(false).await });
        match handle.await.unwrap() {
            Ok(permalink) => {
                return permalink;
            }
            Err(error) => {
                error!("Could not get room event permalink: {error}");
            }
        }

        // Fallback to using just the room ID, without routing.
        self.room_id().matrix_uri(false)
    }

    /// The `matrix.to` URI representation for the given event in this room.
    pub async fn matrix_to_event_uri(&self, event_id: OwnedEventId) -> MatrixToUri {
        let matrix_room = self.matrix_room().clone();

        let event_id_clone = event_id.clone();
        let handle =
            spawn_tokio!(
                async move { matrix_room.matrix_to_event_permalink(event_id_clone).await }
            );
        match handle.await.unwrap() {
            Ok(permalink) => {
                return permalink;
            }
            Err(error) => {
                error!("Could not get room event permalink: {error}");
            }
        }

        // Fallback to using just the room ID, without routing.
        self.room_id().matrix_to_event_uri(event_id)
    }

    /// The `matrix:` URI representation for the given event in this room.
    pub async fn matrix_event_uri(&self, event_id: OwnedEventId) -> MatrixUri {
        let matrix_room = self.matrix_room().clone();

        let event_id_clone = event_id.clone();
        let handle =
            spawn_tokio!(async move { matrix_room.matrix_event_permalink(event_id_clone).await });
        match handle.await.unwrap() {
            Ok(permalink) => {
                return permalink;
            }
            Err(error) => {
                error!("Could not get room event permalink: {error}");
            }
        }

        // Fallback to using just the room ID, without routing.
        self.room_id().matrix_event_uri(event_id)
    }

    /// Report the given events in this room.
    ///
    /// The events are a list of `(event_id, reason)` tuples.
    ///
    /// Returns `Ok(())` if all the reports are sent successfully, otherwise
    /// returns the list of event IDs that could not be reported.
    pub async fn report_events<'a>(
        &self,
        events: &'a [(OwnedEventId, Option<String>)],
    ) -> Result<(), Vec<&'a EventId>> {
        let events_clone = events.to_owned();
        let matrix_room = self.matrix_room().clone();
        let handle = spawn_tokio!(async move {
            let futures = events_clone
                .into_iter()
                .map(|(event_id, reason)| matrix_room.report_content(event_id, None, reason));
            futures_util::future::join_all(futures).await
        });

        let mut failed = Vec::new();
        for (index, result) in handle.await.unwrap().iter().enumerate() {
            match result {
                Ok(_) => {}
                Err(error) => {
                    error!(
                        "Could not report content with event ID {}: {error}",
                        events[index].0,
                    );
                    failed.push(&*events[index].0);
                }
            }
        }

        if failed.is_empty() {
            Ok(())
        } else {
            Err(failed)
        }
    }

    /// Update whether guests are allowed.
    fn update_guests_allowed(&self) {
        let matrix_room = self.matrix_room();
        let guests_allowed = matrix_room.guest_access() == GuestAccess::CanJoin;

        if self.guests_allowed() == guests_allowed {
            return;
        }

        self.imp().guests_allowed.set(guests_allowed);
        self.notify_guests_allowed();
    }

    /// Update the visibility of the history.
    fn update_history_visibility(&self) {
        let matrix_room = self.matrix_room();
        let visibility = matrix_room.history_visibility().into();

        if self.history_visibility() == visibility {
            return;
        }

        self.imp().history_visibility.set(visibility);
        self.notify_history_visibility();
    }
}

/// Supported values for the history visibility.
#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[enum_type(name = "HistoryVisibilityValue")]
pub enum HistoryVisibilityValue {
    /// Anyone can read.
    WorldReadable,
    /// Members, since this was selected.
    #[default]
    Shared,
    /// Members, since they were invited.
    Invited,
    /// Members, since they joined.
    Joined,
    /// Unsupported value.
    Unsupported,
}

impl From<HistoryVisibility> for HistoryVisibilityValue {
    fn from(value: HistoryVisibility) -> Self {
        match value {
            HistoryVisibility::Invited => Self::Invited,
            HistoryVisibility::Joined => Self::Joined,
            HistoryVisibility::Shared => Self::Shared,
            HistoryVisibility::WorldReadable => Self::WorldReadable,
            _ => Self::Unsupported,
        }
    }
}

impl From<HistoryVisibilityValue> for HistoryVisibility {
    fn from(value: HistoryVisibilityValue) -> Self {
        match value {
            HistoryVisibilityValue::Invited => Self::Invited,
            HistoryVisibilityValue::Joined => Self::Joined,
            HistoryVisibilityValue::Shared => Self::Shared,
            HistoryVisibilityValue::WorldReadable => Self::WorldReadable,
            HistoryVisibilityValue::Unsupported => unimplemented!(),
        }
    }
}
