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#[derive(Debug, Default, Eq, PartialEq, Clone, Copy, glib::Enum)]
23#[enum_type(name = "Membership")]
24pub enum Membership {
25 #[default]
27 Leave,
28 Join,
30 Invite,
32 Ban,
34 Knock,
36 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 #[property(get, set = Self::set_room, construct_only)]
75 room: OnceCell<Room>,
76 pub(super) power_level: Cell<UserPowerLevel>,
78 #[property(get = Self::power_level_i64)]
85 power_level_i64: PhantomData<i64>,
86 #[property(get, builder(MemberRole::default()))]
88 role: Cell<MemberRole>,
89 #[property(get, builder(Membership::default()))]
91 membership: Cell<Membership>,
92 #[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 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 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 fn power_level_i64(&self) -> i64 {
183 if let UserPowerLevel::Int(power_level) = self.power_level.get() {
184 power_level.into()
185 } else {
186 i64::MAX
189 }
190 }
191
192 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 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 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 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 pub(crate) fn power_level(&self) -> UserPowerLevel {
250 self.imp().power_level.get()
251 }
252
253 pub(super) fn set_power_level(&self, power_level: UserPowerLevel) {
255 self.imp().set_power_level(power_level);
256 }
257
258 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 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 pub(crate) fn redactable_events(&self) -> Vec<OwnedEventId> {
306 self.room()
307 .live_timeline()
308 .redactable_events_for(self.user_id())
309 }
310
311 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 pub(crate) fn search_string(&self) -> String {
321 format!("{} {} {}", self.display_name(), self.user_id(), self.role())
322 }
323
324 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}