fractal/session/room/
join_rule.rs1use gettextrs::gettext;
2use gtk::{
3 glib,
4 glib::{clone, closure_local},
5 prelude::*,
6 subclass::prelude::*,
7};
8use ruma::{
9 OwnedRoomId,
10 events::room::join_rules::{
11 AllowRule, JoinRule as MatrixJoinRule, Restricted, RoomJoinRulesEventContent,
12 },
13};
14use tracing::error;
15
16use super::{Membership, Room};
17use crate::{components::PillSource, gettext_f, spawn_tokio, utils::BoundObject};
18
19#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
21#[enum_type(name = "JoinRuleValue")]
22pub enum JoinRuleValue {
23 #[default]
25 Invite,
26 Public,
28 RoomMembership,
30 Unsupported,
32}
33
34impl JoinRuleValue {
35 pub(crate) fn can_be_edited(self) -> bool {
37 matches!(self, Self::Invite | Self::Public)
38 }
39}
40
41impl From<&MatrixJoinRule> for JoinRuleValue {
42 fn from(value: &MatrixJoinRule) -> Self {
43 match value {
44 MatrixJoinRule::Invite | MatrixJoinRule::Knock => Self::Invite,
45 MatrixJoinRule::Restricted(restricted)
46 | MatrixJoinRule::KnockRestricted(restricted) => {
47 if has_restricted_membership_room(restricted) {
48 Self::RoomMembership
49 } else {
50 Self::Unsupported
51 }
52 }
53 MatrixJoinRule::Public => Self::Public,
54 _ => Self::Unsupported,
55 }
56 }
57}
58
59mod imp {
60 use std::{
61 cell::{Cell, RefCell},
62 sync::LazyLock,
63 };
64
65 use glib::subclass::Signal;
66
67 use super::*;
68
69 #[derive(Debug, Default, glib::Properties)]
70 #[properties(wrapper_type = super::JoinRule)]
71 pub struct JoinRule {
72 #[property(get)]
74 room: glib::WeakRef<Room>,
75 matrix_join_rule: RefCell<Option<MatrixJoinRule>>,
77 #[property(get, builder(JoinRuleValue::default()))]
79 value: Cell<JoinRuleValue>,
80 #[property(get)]
82 can_knock: Cell<bool>,
83 #[property(get)]
87 display_name: RefCell<String>,
88 #[property(get)]
93 membership_room: BoundObject<PillSource>,
94 #[property(get)]
96 we_can_join: Cell<bool>,
97 #[property(get)]
99 anyone_can_join: Cell<bool>,
100 own_membership_handler: RefCell<Option<glib::SignalHandlerId>>,
101 }
102
103 #[glib::object_subclass]
104 impl ObjectSubclass for JoinRule {
105 const NAME: &'static str = "RoomJoinRule";
106 type Type = super::JoinRule;
107 }
108
109 #[glib::derived_properties]
110 impl ObjectImpl for JoinRule {
111 fn signals() -> &'static [Signal] {
112 static SIGNALS: LazyLock<Vec<Signal>> =
113 LazyLock::new(|| vec![Signal::builder("changed").build()]);
114 SIGNALS.as_ref()
115 }
116
117 fn dispose(&self) {
118 if let Some(room) = self.room.upgrade()
119 && let Some(handler) = self.own_membership_handler.take()
120 {
121 room.own_member().disconnect(handler);
122 }
123 }
124 }
125
126 impl JoinRule {
127 pub(super) fn set_room(&self, room: &Room) {
129 self.room.set(Some(room));
130
131 let own_membership_handler = room.own_member().connect_membership_notify(clone!(
132 #[weak(rename_to = imp)]
133 self,
134 move |_| {
135 imp.update_we_can_join();
136 }
137 ));
138 self.own_membership_handler
139 .replace(Some(own_membership_handler));
140 }
141
142 pub(super) fn matrix_join_rule(&self) -> Option<MatrixJoinRule> {
144 self.matrix_join_rule.borrow().clone()
145 }
146
147 pub(super) fn update_join_rule(&self, join_rule: Option<&MatrixJoinRule>) {
149 if self.matrix_join_rule.borrow().as_ref() == join_rule {
150 return;
151 }
152
153 self.matrix_join_rule.replace(join_rule.cloned());
154
155 self.update_value();
156 self.update_can_knock();
157 self.update_membership_room();
158 self.update_display_name();
159
160 self.update_we_can_join();
161 self.update_anyone_can_join();
162
163 self.obj().emit_by_name::<()>("changed", &[]);
164 }
165
166 fn update_value(&self) {
168 let value = self
169 .matrix_join_rule
170 .borrow()
171 .as_ref()
172 .map(Into::into)
173 .unwrap_or_default();
174
175 if self.value.get() == value {
176 return;
177 }
178
179 self.value.set(value);
180 self.obj().notify_value();
181 }
182
183 fn update_can_knock(&self) {
185 let can_knock = self.matrix_join_rule.borrow().as_ref().is_some_and(|r| {
186 matches!(
187 r,
188 MatrixJoinRule::Knock | MatrixJoinRule::KnockRestricted(_)
189 )
190 });
191
192 if self.can_knock.get() == can_knock {
193 return;
194 }
195
196 self.can_knock.set(can_knock);
197 self.obj().notify_can_knock();
198 }
199
200 fn update_membership_room(&self) {
202 let room_id = self
203 .matrix_join_rule
204 .borrow()
205 .as_ref()
206 .and_then(|r| match r {
207 MatrixJoinRule::Restricted(restricted)
208 | MatrixJoinRule::KnockRestricted(restricted) => {
209 restricted_membership_room(restricted)
210 }
211 _ => None,
212 });
213
214 if self
215 .membership_room
216 .obj()
217 .map(|d| d.identifier())
218 .as_deref()
219 == room_id.as_ref().map(|id| id.as_str())
220 {
221 return;
222 }
223
224 self.membership_room.disconnect_signals();
225
226 if let Some(room_id) = room_id {
227 let Some(session) = self.room.upgrade().and_then(|r| r.session()) else {
228 return;
229 };
230
231 let room: PillSource = if let Some(room) = session.room_list().get(&room_id) {
232 room.upcast()
233 } else {
234 session.remote_cache().room(room_id.into()).upcast()
235 };
236
237 let display_name_handler = room.connect_display_name_notify(clone!(
238 #[weak(rename_to = imp)]
239 self,
240 move |_| {
241 imp.update_display_name();
242 }
243 ));
244
245 self.membership_room.set(room, vec![display_name_handler]);
246 }
247
248 self.obj().notify_membership_room();
249 }
250
251 fn update_display_name(&self) {
253 let value = self.value.get();
254 let can_knock = self.can_knock.get();
255
256 let name = match value {
257 JoinRuleValue::Invite => {
258 if can_knock {
259 gettext("Only invited users, and users can request an invite")
260 } else {
261 gettext("Only invited users")
262 }
263 }
264 JoinRuleValue::RoomMembership => {
265 let room_name = self
266 .membership_room
267 .obj()
268 .map(|r| r.display_name())
269 .unwrap_or_default();
270
271 if can_knock {
272 gettext_f(
273 "Members of {room}, and users can request an invite",
276 &[("room", &format!("<b>{room_name}</b>"))],
277 )
278 } else {
279 gettext_f(
280 "Members of {room}",
283 &[("room", &format!("<b>{room_name}</b>"))],
284 )
285 }
286 }
287 JoinRuleValue::Public => gettext("Any registered user"),
288 JoinRuleValue::Unsupported => gettext("Unsupported rule"),
289 };
290
291 if *self.display_name.borrow() == name {
292 return;
293 }
294
295 self.display_name.replace(name);
296 self.obj().notify_display_name();
297 }
298
299 fn update_we_can_join(&self) {
301 let we_can_join = self.we_can_join();
302
303 if self.we_can_join.get() == we_can_join {
304 return;
305 }
306
307 self.we_can_join.set(we_can_join);
308 self.obj().notify_we_can_join();
309 }
310
311 fn we_can_join(&self) -> bool {
313 let Some(matrix_join_rule) = self.matrix_join_rule() else {
314 return false;
315 };
316 let Some(room) = self.room.upgrade() else {
317 return false;
318 };
319
320 if room.own_member().membership() == Membership::Ban {
321 return false;
322 }
323
324 match matrix_join_rule {
325 MatrixJoinRule::Public => true,
326 MatrixJoinRule::Restricted(rules) | MatrixJoinRule::KnockRestricted(rules) => rules
327 .allow
328 .into_iter()
329 .any(|rule| we_pass_restricted_allow_rule(&room, rule)),
330 _ => false,
331 }
332 }
333
334 fn update_anyone_can_join(&self) {
336 let anyone_can_join = self
337 .matrix_join_rule
338 .borrow()
339 .as_ref()
340 .is_some_and(|r| *r == MatrixJoinRule::Public);
341
342 if self.anyone_can_join.get() == anyone_can_join {
343 return;
344 }
345
346 self.anyone_can_join.set(anyone_can_join);
347 self.obj().notify_anyone_can_join();
348 }
349 }
350}
351
352glib::wrapper! {
353 pub struct JoinRule(ObjectSubclass<imp::JoinRule>);
355}
356
357impl JoinRule {
358 pub fn new() -> Self {
359 glib::Object::new()
360 }
361
362 pub(super) fn init(&self, room: &Room) {
364 self.imp().set_room(room);
365 }
366
367 pub(super) fn update(&self, join_rule: Option<&MatrixJoinRule>) {
369 self.imp().update_join_rule(join_rule);
370 }
371
372 pub(crate) fn matrix_join_rule(&self) -> Option<MatrixJoinRule> {
374 self.imp().matrix_join_rule()
375 }
376
377 pub(crate) async fn set_matrix_join_rule(&self, rule: MatrixJoinRule) -> Result<(), ()> {
379 let Some(room) = self.room() else {
380 return Err(());
381 };
382
383 let content = RoomJoinRulesEventContent::new(rule);
384
385 let matrix_room = room.matrix_room().clone();
386 let handle = spawn_tokio!(async move { matrix_room.send_state_event(content).await });
387
388 match handle.await.expect("task was not aborted") {
389 Ok(_) => Ok(()),
390 Err(error) => {
391 error!("Could not change join rule: {error}");
392 Err(())
393 }
394 }
395 }
396
397 pub(crate) fn connect_changed<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
399 self.connect_closure(
400 "changed",
401 true,
402 closure_local!(move |obj: Self| {
403 f(&obj);
404 }),
405 )
406 }
407}
408
409impl Default for JoinRule {
410 fn default() -> Self {
411 Self::new()
412 }
413}
414
415fn has_restricted_membership_room(restricted: &Restricted) -> bool {
417 restricted
418 .allow
419 .iter()
420 .any(|a| matches!(a, AllowRule::RoomMembership(_)))
421}
422
423fn restricted_membership_room(restricted: &Restricted) -> Option<OwnedRoomId> {
426 restricted.allow.iter().find_map(|a| match a {
427 AllowRule::RoomMembership(m) => Some(m.room_id.clone()),
428 _ => None,
429 })
430}
431
432fn we_pass_restricted_allow_rule(room: &Room, rule: AllowRule) -> bool {
434 match rule {
435 AllowRule::RoomMembership(room_membership) => room.session().is_some_and(|s| {
436 s.room_list()
437 .get_by_identifier((&*room_membership.room_id).into())
438 .is_some_and(|room| room.is_joined())
439 }),
440 _ => false,
441 }
442}