From b672f52bf82d800d03d7455ab41314c808b1ef2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 16 Jul 2025 09:55:50 +0200 Subject: [PATCH] room-details: Allow to change join rule to knock --- .../icons/scalable/status/info-symbolic.svg | 2 + data/resources/resources.gresource.xml | 1 + po/POTFILES.in | 2 + src/components/rows/button_count_row.ui | 1 + src/session/model/room/join_rule.rs | 30 +- src/session/model/room/mod.rs | 13 + .../view/content/room_details/general_page.rs | 56 +--- .../view/content/room_details/general_page.ui | 15 +- .../content/room_details/join_rule_subpage.rs | 284 ++++++++++++++++++ .../content/room_details/join_rule_subpage.ui | 116 +++++++ src/session/view/content/room_details/mod.rs | 7 +- .../permissions/permissions_subpage.rs | 1 - src/ui-resources.gresource.xml | 1 + 13 files changed, 459 insertions(+), 70 deletions(-) create mode 100644 data/resources/icons/scalable/status/info-symbolic.svg create mode 100644 src/session/view/content/room_details/join_rule_subpage.rs create mode 100644 src/session/view/content/room_details/join_rule_subpage.ui diff --git a/data/resources/icons/scalable/status/info-symbolic.svg b/data/resources/icons/scalable/status/info-symbolic.svg new file mode 100644 index 00000000..254e7e83 --- /dev/null +++ b/data/resources/icons/scalable/status/info-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index 2dcaca7c..df76a59a 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -57,6 +57,7 @@ icons/scalable/status/explore-symbolic.svg icons/scalable/status/home-symbolic.svg icons/scalable/status/image-symbolic.svg + icons/scalable/status/info-symbolic.svg icons/scalable/status/key-symbolic.svg icons/scalable/status/no-camera-symbolic.svg icons/scalable/status/notifications-symbolic.svg diff --git a/po/POTFILES.in b/po/POTFILES.in index 65e0824b..f8825122 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -131,6 +131,8 @@ src/session/view/content/room_details/history_viewer/visual_media.ui src/session/view/content/room_details/invite_subpage/list.rs src/session/view/content/room_details/invite_subpage/mod.rs src/session/view/content/room_details/invite_subpage/mod.ui +src/session/view/content/room_details/join_rule_subpage.rs +src/session/view/content/room_details/join_rule_subpage.ui src/session/view/content/room_details/member_row.ui src/session/view/content/room_details/members_page/members_list_view/membership_subpage_row.rs src/session/view/content/room_details/members_page/members_list_view/mod.rs diff --git a/src/components/rows/button_count_row.ui b/src/components/rows/button_count_row.ui index 411a5c9e..33819d66 100644 --- a/src/components/rows/button_count_row.ui +++ b/src/components/rows/button_count_row.ui @@ -13,6 +13,7 @@ + center center go-next-symbolic diff --git a/src/session/model/room/join_rule.rs b/src/session/model/room/join_rule.rs index c73acb7f..da4298db 100644 --- a/src/session/model/room/join_rule.rs +++ b/src/session/model/room/join_rule.rs @@ -16,7 +16,7 @@ use tracing::error; use super::{Membership, Room}; use crate::{components::PillSource, gettext_f, spawn_tokio, utils::BoundObject}; -/// Supported values for the join rule. +/// Simplified join rules. #[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)] #[enum_type(name = "JoinRuleValue")] pub enum JoinRuleValue { @@ -31,6 +31,13 @@ pub enum JoinRuleValue { Unsupported, } +impl JoinRuleValue { + /// Whether we support editing this join rule. + pub(crate) fn can_be_edited(self) -> bool { + matches!(self, Self::Invite | Self::Public) + } +} + impl From<&MatrixJoinRule> for JoinRuleValue { fn from(value: &MatrixJoinRule) -> Self { match value { @@ -132,6 +139,11 @@ mod imp { .replace(Some(own_membership_handler)); } + /// The current join rule from the SDK. + pub(super) fn matrix_join_rule(&self) -> Option { + self.matrix_join_rule.borrow().clone() + } + /// Update the join rule. pub(super) fn update_join_rule(&self, join_rule: Option<&MatrixJoinRule>) { if self.matrix_join_rule.borrow().as_ref() == join_rule { @@ -298,7 +310,7 @@ mod imp { /// Whether our own user can join this room on their own. fn we_can_join(&self) -> bool { - let Some(matrix_join_rule) = self.matrix_join_rule.borrow().clone() else { + let Some(matrix_join_rule) = self.matrix_join_rule() else { return false; }; let Some(room) = self.room.upgrade() else { @@ -357,17 +369,17 @@ impl JoinRule { self.imp().update_join_rule(join_rule); } - /// Change the value of the join rule. - pub(crate) async fn set_value(&self, value: JoinRuleValue) -> Result<(), ()> { + /// Get the current join rule from the SDK. + pub(crate) fn matrix_join_rule(&self) -> Option { + self.imp().matrix_join_rule() + } + + /// Change the join rule. + pub(crate) async fn set_matrix_join_rule(&self, rule: MatrixJoinRule) -> Result<(), ()> { let Some(room) = self.room() else { return Err(()); }; - let rule = match value { - JoinRuleValue::Invite => MatrixJoinRule::Invite, - JoinRuleValue::Public => MatrixJoinRule::Public, - _ => unimplemented!(), - }; let content = RoomJoinRulesEventContent::new(rule); let matrix_room = room.matrix_room().clone(); diff --git a/src/session/model/room/mod.rs b/src/session/model/room/mod.rs index b0dc8c8e..1df2eb87 100644 --- a/src/session/model/room/mod.rs +++ b/src/session/model/room/mod.rs @@ -26,6 +26,7 @@ use ruma::{ history_visibility::HistoryVisibility, member::{MembershipState, RoomMemberEventContent, SyncRoomMemberEvent}, }, + room_version_rules::RoomVersionRules, }; use serde::Deserialize; use tokio_stream::wrappers::BroadcastStream; @@ -1318,6 +1319,13 @@ mod imp { .unwrap_or_default() } + /// The rules for the version of this room. + pub(super) fn rules(&self) -> RoomVersionRules { + self.matrix_room() + .clone_info() + .room_version_rules_or_default() + } + /// Whether this room is federated. fn federated(&self) -> bool { self.matrix_room() @@ -1618,6 +1626,11 @@ impl Room { format!("{} ({})", self.display_name(), self.room_id()) } + /// The rules for the version of this room. + pub(crate) fn rules(&self) -> RoomVersionRules { + self.imp().rules() + } + /// Whether this room is joined. pub(crate) fn is_joined(&self) -> bool { self.own_member().membership() == Membership::Join diff --git a/src/session/view/content/room_details/general_page.rs b/src/session/view/content/room_details/general_page.rs index 87f014a7..fa543ebf 100644 --- a/src/session/view/content/room_details/general_page.rs +++ b/src/session/view/content/room_details/general_page.rs @@ -32,8 +32,8 @@ use crate::{ gettext_f, prelude::*, session::model::{ - HistoryVisibilityValue, JoinRuleValue, Member, MemberList, MembershipListKind, - NotificationsRoomSetting, Room, RoomCategory, + HistoryVisibilityValue, Member, MemberList, MembershipListKind, NotificationsRoomSetting, + Room, RoomCategory, }, spawn, spawn_tokio, toast, utils::{BoundObjectWeakRef, TemplateCallbacks, expression, matrix::MatrixIdUri}, @@ -90,7 +90,7 @@ mod imp { canonical_alias_row: RefCell>, alt_aliases_rows: RefCell>, #[template_child] - join_rule: TemplateChild, + join_rule: TemplateChild, #[template_child] guest_access: TemplateChild, #[template_child] @@ -760,53 +760,13 @@ mod imp { return; }; - let row = &self.join_rule; - row.set_is_loading(false); - - let permissions = room.permissions(); - let join_rule = room.join_rule(); - - let is_supported_join_rule = matches!( - join_rule.value(), - JoinRuleValue::Public | JoinRuleValue::Invite - ) && !join_rule.can_knock(); - let can_change = permissions + let join_rule_can_be_edited = room.join_rule().value().can_be_edited(); + let can_change = room + .permissions() .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomJoinRules)); - row.set_read_only(!is_supported_join_rule || !can_change); - row.set_selected_string(Some(join_rule.display_name())); - } - - /// Set the join rule of the room. - #[template_callback] - async fn set_join_rule(&self) { - let Some(room) = self.room.obj() else { - return; - }; - let join_rule = room.join_rule(); - - let row = &self.join_rule; - - let value = match row.selected() { - 0 => JoinRuleValue::Invite, - 1 => JoinRuleValue::Public, - _ => { - return; - } - }; - - if join_rule.value() == value { - // Nothing to do. - return; - } - - row.set_is_loading(true); - row.set_read_only(true); - - if join_rule.set_value(value).await.is_err() { - toast!(self.obj(), gettext("Could not change who can join")); - self.update_join_rule(); - } + self.join_rule + .set_activatable(join_rule_can_be_edited && can_change); } /// Update the guest access row. diff --git a/src/session/view/content/room_details/general_page.ui b/src/session/view/content/room_details/general_page.ui index 2646cffa..734f197c 100644 --- a/src/session/view/content/room_details/general_page.ui +++ b/src/session/view/content/room_details/general_page.ui @@ -240,24 +240,17 @@ - + Who Can Join - - - - Only Invited Users - Any Registered User - - - - + RoomDetailsGeneralPage - + details.show-subpage + 'join-rule' diff --git a/src/session/view/content/room_details/join_rule_subpage.rs b/src/session/view/content/room_details/join_rule_subpage.rs new file mode 100644 index 00000000..089c680a --- /dev/null +++ b/src/session/view/content/room_details/join_rule_subpage.rs @@ -0,0 +1,284 @@ +use adw::{prelude::*, subclass::prelude::*}; +use gettextrs::gettext; +use gtk::{CompositeTemplate, glib, glib::clone}; +use ruma::events::room::join_rules::JoinRule as MatrixJoinRule; + +use crate::{ + components::{CheckLoadingRow, LoadingButton, UnsavedChangesResponse, unsaved_changes_dialog}, + session::model::{JoinRuleValue, Room}, + toast, +}; + +mod imp { + use std::cell::{Cell, RefCell}; + + use glib::subclass::InitializingObject; + use ruma::events::{StateEventType, room::power_levels::PowerLevelAction}; + + use super::*; + + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] + #[template( + resource = "/org/gnome/Fractal/ui/session/view/content/room_details/join_rule_subpage.ui" + )] + #[properties(wrapper_type = super::JoinRuleSubpage)] + pub struct JoinRuleSubpage { + #[template_child] + save_button: TemplateChild, + #[template_child] + info_box: TemplateChild, + #[template_child] + info_image: TemplateChild, + #[template_child] + info_description: TemplateChild, + #[template_child] + knock_box: TemplateChild, + #[template_child] + knock_row: TemplateChild, + /// The presented room. + #[property(get, set = Self::set_room, explicit_notify, nullable)] + room: glib::WeakRef, + /// The local value of the join rule. + #[property(get, set = Self::set_local_value, explicit_notify, builder(JoinRuleValue::default()))] + local_value: Cell, + /// Whether the join rule was changed by the user. + #[property(get)] + changed: Cell, + permissions_handler: RefCell>, + join_rule_handler: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for JoinRuleSubpage { + const NAME: &'static str = "RoomDetailsJoinRuleSubpage"; + type Type = super::JoinRuleSubpage; + type ParentType = adw::NavigationPage; + + fn class_init(klass: &mut Self::Class) { + CheckLoadingRow::ensure_type(); + + Self::bind_template(klass); + Self::bind_template_callbacks(klass); + + klass.install_property_action("join-rule.set-value", "local-value"); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for JoinRuleSubpage { + fn dispose(&self) { + self.disconnect_signals(); + } + } + + impl WidgetImpl for JoinRuleSubpage {} + impl NavigationPageImpl for JoinRuleSubpage {} + + #[gtk::template_callbacks] + impl JoinRuleSubpage { + /// Set the presented room. + fn set_room(&self, room: Option<&Room>) { + let Some(room) = room else { + // Just ignore when room is missing. + return; + }; + + self.disconnect_signals(); + + let permissions_handler = room.permissions().connect_changed(clone!( + #[weak(rename_to = imp)] + self, + move |_| { + imp.update(); + } + )); + self.permissions_handler.replace(Some(permissions_handler)); + + let join_rule_handler = room.join_rule().connect_changed(clone!( + #[weak(rename_to = imp)] + self, + move |_| { + imp.update(); + } + )); + self.join_rule_handler.replace(Some(join_rule_handler)); + + let supports_knocking = room.rules().authorization.knocking; + if !supports_knocking { + self.info_description.set_label(&gettext("The version of this room does not support all possibilities. Upgrade this room to the latest version to see more options.")); + self.info_image.set_icon_name(Some("info-symbolic")); + } + + self.info_box.set_visible(!supports_knocking); + self.knock_box.set_visible(supports_knocking); + + self.room.set(Some(room)); + + self.update(); + self.obj().notify_room(); + } + + /// Update the subpage. + fn update(&self) { + let Some(room) = self.room.upgrade() else { + return; + }; + + let join_rule = room.join_rule(); + self.set_local_value(join_rule.value()); + self.knock_row.set_active(join_rule.can_knock()); + + self.save_button.set_is_loading(false); + self.update_changed(); + } + + /// Set the local value of the join rule. + fn set_local_value(&self, value: JoinRuleValue) { + if self.local_value.get() == value { + return; + } + + self.local_value.set(value); + + let can_knock = matches!(value, JoinRuleValue::Invite | JoinRuleValue::RoomMembership); + self.knock_box.set_sensitive(can_knock); + + self.update_changed(); + self.obj().notify_local_value(); + } + + /// Whether we can change the join rule. + fn can_change(&self) -> bool { + let Some(room) = self.room.upgrade() else { + return false; + }; + + if !room.join_rule().value().can_be_edited() { + return false; + } + + room.permissions() + .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomJoinRules)) + } + + /// Whether users can request invites. + fn can_knock(&self) -> bool { + self.knock_box.is_visible() + && self.knock_box.is_sensitive() + && self.knock_row.is_active() + } + + /// Compute the new join rule from the current state. + fn new_join_rule(&self) -> MatrixJoinRule { + match self.local_value.get() { + JoinRuleValue::Invite => { + if self.can_knock() { + MatrixJoinRule::Knock + } else { + MatrixJoinRule::Invite + } + } + JoinRuleValue::Public => MatrixJoinRule::Public, + _ => unimplemented!(), + } + } + + /// Update whether the join rule was changed by the user. + #[template_callback] + fn update_changed(&self) { + let Some(room) = self.room.upgrade() else { + return; + }; + + let changed = if self.can_change() { + let current_join_rule = room + .join_rule() + .matrix_join_rule() + .unwrap_or(MatrixJoinRule::Invite); + let new_join_rule = self.new_join_rule(); + + current_join_rule != new_join_rule + } else { + false + }; + + self.changed.set(changed); + self.obj().notify_changed(); + } + + /// Save the changes of this page. + #[template_callback] + async fn save(&self) { + if !self.changed.get() { + // Nothing to do. + return; + } + + let Some(room) = self.room.upgrade() else { + return; + }; + + self.save_button.set_is_loading(true); + + let rule = self.new_join_rule(); + + if room.join_rule().set_matrix_join_rule(rule).await.is_err() { + toast!(self.obj(), gettext("Could not change who can join")); + self.save_button.set_is_loading(false); + } + } + + /// Go back to the previous page in the room details. + /// + /// If there are changes in the page, ask the user to confirm. + #[template_callback] + async fn go_back(&self) { + let obj = self.obj(); + let mut reset_after = false; + + if self.changed.get() { + match unsaved_changes_dialog(&*obj).await { + UnsavedChangesResponse::Save => self.save().await, + UnsavedChangesResponse::Discard => reset_after = true, + UnsavedChangesResponse::Cancel => return, + } + } + + let _ = obj.activate_action("navigation.pop", None); + + if reset_after { + self.update(); + } + } + + /// Disconnect all the signal handlers. + fn disconnect_signals(&self) { + if let Some(room) = self.room.upgrade() { + if let Some(handler) = self.permissions_handler.take() { + room.permissions().disconnect(handler); + } + + if let Some(handler) = self.join_rule_handler.take() { + room.join_rule().disconnect(handler); + } + } + } + } +} + +glib::wrapper! { + /// Subpage to select the join rule of a room. + pub struct JoinRuleSubpage(ObjectSubclass) + @extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible; +} + +impl JoinRuleSubpage { + /// Construct a new `JoinRuleSubpage` for the given room. + pub fn new(room: &Room) -> Self { + glib::Object::builder().property("room", room).build() + } +} diff --git a/src/session/view/content/room_details/join_rule_subpage.ui b/src/session/view/content/room_details/join_rule_subpage.ui new file mode 100644 index 00000000..63e9f22b --- /dev/null +++ b/src/session/view/content/room_details/join_rule_subpage.ui @@ -0,0 +1,116 @@ + + + + diff --git a/src/session/view/content/room_details/mod.rs b/src/session/view/content/room_details/mod.rs index 1b02947d..c30d9524 100644 --- a/src/session/view/content/room_details/mod.rs +++ b/src/session/view/content/room_details/mod.rs @@ -12,6 +12,7 @@ mod edit_details_subpage; mod general_page; mod history_viewer; mod invite_subpage; +mod join_rule_subpage; mod member_row; mod members_page; mod membership_subpage_item; @@ -26,6 +27,7 @@ use self::{ AudioHistoryViewer, FileHistoryViewer, HistoryViewerTimeline, VisualMediaHistoryViewer, }, invite_subpage::InviteSubpage, + join_rule_subpage::JoinRuleSubpage, member_row::MemberRow, members_page::MembersPage, membership_subpage_item::MembershipSubpageItem, @@ -56,6 +58,8 @@ pub(crate) enum SubpageName { Addresses, /// The page to edit the permissions of the room. Permissions, + /// The page to edit the join rule of the room. + JoinRule, } mod imp { @@ -163,7 +167,7 @@ mod imp { self.general_page .get() - .expect("general page is initialized") + .expect("general page should be initialized") .unselect_topic(); } } @@ -220,6 +224,7 @@ mod imp { SubpageName::AudioHistory => AudioHistoryViewer::new(self.timeline()).upcast(), SubpageName::Addresses => AddressesSubpage::new(room).upcast(), SubpageName::Permissions => PermissionsSubpage::new(&room.permissions()).upcast(), + SubpageName::JoinRule => JoinRuleSubpage::new(room).upcast(), }); if is_initial { diff --git a/src/session/view/content/room_details/permissions/permissions_subpage.rs b/src/session/view/content/room_details/permissions/permissions_subpage.rs index 7a45d33d..0fd7ceec 100644 --- a/src/session/view/content/room_details/permissions/permissions_subpage.rs +++ b/src/session/view/content/room_details/permissions/permissions_subpage.rs @@ -573,7 +573,6 @@ mod imp { if permissions.set_power_levels(power_levels).await.is_err() { toast!(self.obj(), gettext("Could not save permissions")); - self.save_button.set_is_loading(false); } } diff --git a/src/ui-resources.gresource.xml b/src/ui-resources.gresource.xml index 5d6624d7..30256495 100644 --- a/src/ui-resources.gresource.xml +++ b/src/ui-resources.gresource.xml @@ -96,6 +96,7 @@ session/view/content/room_details/history_viewer/visual_media_item.ui session/view/content/room_details/invite_subpage/mod.ui session/view/content/room_details/invite_subpage/row.ui + session/view/content/room_details/join_rule_subpage.ui session/view/content/room_details/member_row.ui session/view/content/room_details/members_page/members_list_view/membership_subpage_row.ui session/view/content/room_details/members_page/members_list_view/mod.ui