fractal/session/model/room/
member.rs

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