Skip to main content

fractal/session/room/
member.rs

1use gtk::{
2    glib,
3    glib::{clone, closure_local},
4    prelude::*,
5    subclass::prelude::*,
6};
7use matrix_sdk::room::RoomMember;
8use ruma::{
9    OwnedEventId, OwnedUserId,
10    events::room::{
11        member::MembershipState,
12        power_levels::{NotificationPowerLevelType, PowerLevelAction, UserPowerLevel},
13    },
14    int,
15};
16use tracing::{debug, error};
17
18use super::{MemberRole, Room};
19use crate::{components::PillSource, prelude::*, session::User, spawn, spawn_tokio};
20
21/// The possible states of membership of a user in a room.
22#[derive(Debug, Default, Eq, PartialEq, Clone, Copy, glib::Enum)]
23#[enum_type(name = "Membership")]
24pub enum Membership {
25    /// The user left the room, or was never in the room.
26    #[default]
27    Leave,
28    /// The user is currently in the room.
29    Join,
30    /// The user was invited to the room.
31    Invite,
32    /// The user was banned from the room.
33    Ban,
34    /// The user knocked on the room.
35    Knock,
36    /// The user is in an unsupported membership state.
37    Unsupported,
38}
39
40impl From<&MembershipState> for Membership {
41    fn from(state: &MembershipState) -> Self {
42        match state {
43            MembershipState::Leave => Membership::Leave,
44            MembershipState::Join => Membership::Join,
45            MembershipState::Invite => Membership::Invite,
46            MembershipState::Ban => Membership::Ban,
47            MembershipState::Knock => Membership::Knock,
48            _ => Membership::Unsupported,
49        }
50    }
51}
52
53impl From<MembershipState> for Membership {
54    fn from(state: MembershipState) -> Self {
55        Membership::from(&state)
56    }
57}
58
59mod imp {
60    use std::{
61        cell::{Cell, OnceCell, RefCell},
62        marker::PhantomData,
63        sync::LazyLock,
64    };
65
66    use glib::subclass::Signal;
67
68    use super::*;
69
70    #[derive(Debug, glib::Properties)]
71    #[properties(wrapper_type = super::Member)]
72    pub struct Member {
73        /// The room of the member.
74        #[property(get, set = Self::set_room, construct_only)]
75        room: OnceCell<Room>,
76        /// The power level of the member.
77        pub(super) power_level: Cell<UserPowerLevel>,
78        /// The power level of the member, as an `i64`.
79        ///
80        /// Should only be used for sorting.
81        ///
82        /// `i64::MAX` is used to represent an infinite power level, since it
83        /// cannot be reached with the Matrix specification.
84        #[property(get = Self::power_level_i64)]
85        power_level_i64: PhantomData<i64>,
86        /// The role of the member.
87        #[property(get, builder(MemberRole::default()))]
88        role: Cell<MemberRole>,
89        /// This membership state of the member.
90        #[property(get, builder(Membership::default()))]
91        membership: Cell<Membership>,
92        /// The timestamp of the latest activity of this member.
93        #[property(get, set = Self::set_latest_activity, explicit_notify)]
94        latest_activity: Cell<u64>,
95        power_level_handlers: RefCell<Vec<glib::SignalHandlerId>>,
96    }
97
98    impl Default for Member {
99        fn default() -> Self {
100            Self {
101                room: Default::default(),
102                power_level: Cell::new(UserPowerLevel::Int(int!(0))),
103                power_level_i64: Default::default(),
104                role: Default::default(),
105                membership: Default::default(),
106                latest_activity: Default::default(),
107                power_level_handlers: Default::default(),
108            }
109        }
110    }
111
112    #[glib::object_subclass]
113    impl ObjectSubclass for Member {
114        const NAME: &'static str = "Member";
115        type Type = super::Member;
116        type ParentType = User;
117    }
118
119    #[glib::derived_properties]
120    impl ObjectImpl for Member {
121        fn signals() -> &'static [Signal] {
122            static SIGNALS: LazyLock<Vec<Signal>> =
123                LazyLock::new(|| vec![Signal::builder("power-level-changed").build()]);
124            SIGNALS.as_ref()
125        }
126
127        fn dispose(&self) {
128            if let Some(room) = self.room.get() {
129                for handler in self.power_level_handlers.take() {
130                    room.permissions().disconnect(handler);
131                }
132            }
133        }
134    }
135
136    impl PillSourceImpl for Member {
137        fn identifier(&self) -> String {
138            self.obj().upcast_ref::<User>().user_id_string()
139        }
140    }
141
142    impl Member {
143        /// Set the room of the member.
144        fn set_room(&self, room: Room) {
145            let room = self.room.get_or_init(|| room);
146
147            let default_pl_handler = room
148                .permissions()
149                .connect_default_power_level_notify(clone!(
150                    #[weak(rename_to = imp)]
151                    self,
152                    move |_| {
153                        imp.update_role();
154                    }
155                ));
156            let mute_pl_handler = room.permissions().connect_mute_power_level_notify(clone!(
157                #[weak(rename_to = imp)]
158                self,
159                move |_| {
160                    imp.update_role();
161                }
162            ));
163            self.power_level_handlers
164                .replace(vec![default_pl_handler, mute_pl_handler]);
165        }
166
167        /// Set the power level of the member.
168        pub(super) fn set_power_level(&self, power_level: UserPowerLevel) {
169            if self.power_level.get() == power_level {
170                return;
171            }
172
173            self.power_level.set(power_level);
174            self.update_role();
175
176            let obj = self.obj();
177            obj.emit_by_name::<()>("power-level-changed", &[]);
178            obj.notify_power_level_i64();
179        }
180
181        /// The power level of the member, as an `i64`.
182        fn power_level_i64(&self) -> i64 {
183            if let UserPowerLevel::Int(power_level) = self.power_level.get() {
184                power_level.into()
185            } else {
186                // Represent the infinite power level with a value out of range for a power
187                // level.
188                i64::MAX
189            }
190        }
191
192        /// Update the role of the member.
193        fn update_role(&self) {
194            let role = self
195                .room
196                .get()
197                .expect("room is initialized")
198                .permissions()
199                .role(self.power_level.get());
200
201            if self.role.get() == role {
202                return;
203            }
204
205            self.role.set(role);
206            self.obj().notify_role();
207        }
208
209        /// Set this membership state of the member.
210        pub(super) fn set_membership(&self, membership: Membership) {
211            if self.membership.get() == membership {
212                return;
213            }
214
215            self.membership.replace(membership);
216            self.obj().notify_membership();
217        }
218
219        /// Set the timestamp of the latest activity of this member.
220        fn set_latest_activity(&self, activity: u64) {
221            if self.latest_activity.get() >= activity {
222                return;
223            }
224
225            self.latest_activity.set(activity);
226            self.obj().notify_latest_activity();
227        }
228    }
229}
230
231glib::wrapper! {
232    /// A `User` in the context of a given room.
233    pub struct Member(ObjectSubclass<imp::Member>) @extends PillSource, User;
234}
235
236impl Member {
237    pub fn new(room: &Room, user_id: OwnedUserId) -> Self {
238        let session = room.session();
239        let obj = glib::Object::builder::<Self>()
240            .property("session", &session)
241            .property("room", room)
242            .build();
243
244        obj.upcast_ref::<User>().imp().set_user_id(user_id);
245        obj
246    }
247
248    /// The power level of the member.
249    pub(crate) fn power_level(&self) -> UserPowerLevel {
250        self.imp().power_level.get()
251    }
252
253    /// Set the power level of the member.
254    pub(super) fn set_power_level(&self, power_level: UserPowerLevel) {
255        self.imp().set_power_level(power_level);
256    }
257
258    /// Update this member with the data from the given SDK's member.
259    pub(crate) fn update_from_room_member(&self, member: &RoomMember) {
260        if member.user_id() != self.user_id() {
261            error!("Tried Member update from RoomMember with wrong user ID.");
262            return;
263        }
264
265        self.set_name(member.display_name().map(ToOwned::to_owned));
266        self.set_is_name_ambiguous(member.name_ambiguous());
267        self.avatar_data()
268            .image()
269            .expect("image is set")
270            .set_uri_and_info(member.avatar_url().map(ToOwned::to_owned), None);
271        self.set_power_level(member.power_level());
272        self.imp().set_membership(member.membership().into());
273    }
274
275    /// Update this member with data from the SDK.
276    pub(crate) fn update(&self) {
277        spawn!(clone!(
278            #[weak(rename_to = obj)]
279            self,
280            async move {
281                obj.update_inner().await;
282            }
283        ));
284    }
285
286    async fn update_inner(&self) {
287        let room = self.room();
288
289        let matrix_room = room.matrix_room().clone();
290        let user_id = self.user_id().clone();
291        let handle = spawn_tokio!(async move { matrix_room.get_member_no_sync(&user_id).await });
292
293        match handle.await.expect("task was not aborted") {
294            Ok(Some(member)) => self.update_from_room_member(&member),
295            Ok(None) => {
296                debug!("Room member {} not found", self.user_id());
297            }
298            Err(error) => {
299                error!("Could not load room member {}: {error}", self.user_id());
300            }
301        }
302    }
303
304    /// The IDs of the events sent by this member that can be redacted.
305    pub(crate) fn redactable_events(&self) -> Vec<OwnedEventId> {
306        self.room()
307            .live_timeline()
308            .redactable_events_for(self.user_id())
309    }
310
311    /// Whether this room member can notify the whole room.
312    pub(crate) fn can_notify_room(&self) -> bool {
313        self.room().permissions().user_is_allowed_to(
314            self.user_id(),
315            PowerLevelAction::TriggerNotification(NotificationPowerLevelType::Room),
316        )
317    }
318
319    /// The string to use to search for this member.
320    pub(crate) fn search_string(&self) -> String {
321        format!("{} {} {}", self.display_name(), self.user_id(), self.role())
322    }
323
324    /// Connect to the signal emitted when the power level of the member
325    /// changed.
326    pub(crate) fn connect_power_level_changed<F: Fn(&Self) + 'static>(
327        &self,
328        f: F,
329    ) -> glib::SignalHandlerId {
330        self.connect_closure(
331            "power-level-changed",
332            true,
333            closure_local!(move |obj: Self| {
334                f(&obj);
335            }),
336        )
337    }
338}