Browse Source

room-details: Allow to change join rule to knock

fractal-12
Kévin Commaille 8 months ago
parent
commit
b672f52bf8
No known key found for this signature in database
GPG Key ID: F26F4BE20A08255B
  1. 2
      data/resources/icons/scalable/status/info-symbolic.svg
  2. 1
      data/resources/resources.gresource.xml
  3. 2
      po/POTFILES.in
  4. 1
      src/components/rows/button_count_row.ui
  5. 30
      src/session/model/room/join_rule.rs
  6. 13
      src/session/model/room/mod.rs
  7. 56
      src/session/view/content/room_details/general_page.rs
  8. 15
      src/session/view/content/room_details/general_page.ui
  9. 284
      src/session/view/content/room_details/join_rule_subpage.rs
  10. 116
      src/session/view/content/room_details/join_rule_subpage.ui
  11. 7
      src/session/view/content/room_details/mod.rs
  12. 1
      src/session/view/content/room_details/permissions/permissions_subpage.rs
  13. 1
      src/ui-resources.gresource.xml

2
data/resources/icons/scalable/status/info-symbolic.svg

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 8 0 c -4.410156 0 -8 3.589844 -8 8 s 3.589844 8 8 8 s 8 -3.589844 8 -8 s -3.589844 -8 -8 -8 z m 0 2 c 3.332031 0 6 2.667969 6 6 s -2.667969 6 -6 6 s -6 -2.667969 -6 -6 s 2.667969 -6 6 -6 z m 0 1.875 c -0.621094 0 -1.125 0.503906 -1.125 1.125 s 0.503906 1.125 1.125 1.125 s 1.125 -0.503906 1.125 -1.125 s -0.503906 -1.125 -1.125 -1.125 z m -1.523438 3.125 c -0.265624 0.011719 -0.476562 0.230469 -0.476562 0.5 c 0 0.277344 0.222656 0.5 0.5 0.5 h 0.5 v 3 h -0.5 c -0.277344 0 -0.5 0.222656 -0.5 0.5 s 0.222656 0.5 0.5 0.5 h 3 c 0.277344 0 0.5 -0.222656 0.5 -0.5 s -0.222656 -0.5 -0.5 -0.5 h -0.5 v -4 h -2.5 c -0.007812 0 -0.015625 0 -0.023438 0 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 813 B

1
data/resources/resources.gresource.xml

@ -57,6 +57,7 @@
<file preprocess="xml-stripblanks">icons/scalable/status/explore-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/home-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/image-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/info-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/key-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/no-camera-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/notifications-symbolic.svg</file>

2
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

1
src/components/rows/button_count_row.ui

@ -13,6 +13,7 @@
</child>
<child type="suffix">
<object class="GtkImage">
<property name="visible" bind-source="ButtonCountRow" bind-property="activatable" bind-flags="sync-create" />
<property name="valign">center</property>
<property name="halign">center</property>
<property name="icon-name">go-next-symbolic</property>

30
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<MatrixJoinRule> {
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<MatrixJoinRule> {
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();

13
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

56
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<Option<CopyableRow>>,
alt_aliases_rows: RefCell<Vec<CopyableRow>>,
#[template_child]
join_rule: TemplateChild<ComboLoadingRow>,
join_rule: TemplateChild<ButtonCountRow>,
#[template_child]
guest_access: TemplateChild<SwitchLoadingRow>,
#[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.

15
src/session/view/content/room_details/general_page.ui

@ -240,24 +240,17 @@
</closure>
</binding>
<child>
<object class="ComboLoadingRow" id="join_rule">
<object class="ButtonCountRow" id="join_rule">
<property name="title" translatable="yes">Who Can Join</property>
<property name="string-model">
<object class="GtkStringList">
<items>
<item translatable="yes">Only Invited Users</item>
<item translatable="yes">Any Registered User</item>
</items>
</object>
</property>
<binding name="selected-string">
<binding name="subtitle">
<lookup name="display-name">
<lookup name="join-rule">
<lookup name="room">RoomDetailsGeneralPage</lookup>
</lookup>
</lookup>
</binding>
<signal name="notify::selected-string" handler="set_join_rule" swapped="yes"/>
<property name="action-name">details.show-subpage</property>
<property name="action-target">'join-rule'</property>
</object>
</child>
<child>

284
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<LoadingButton>,
#[template_child]
info_box: TemplateChild<gtk::Box>,
#[template_child]
info_image: TemplateChild<gtk::Image>,
#[template_child]
info_description: TemplateChild<gtk::Label>,
#[template_child]
knock_box: TemplateChild<gtk::ListBox>,
#[template_child]
knock_row: TemplateChild<adw::SwitchRow>,
/// The presented room.
#[property(get, set = Self::set_room, explicit_notify, nullable)]
room: glib::WeakRef<Room>,
/// The local value of the join rule.
#[property(get, set = Self::set_local_value, explicit_notify, builder(JoinRuleValue::default()))]
local_value: Cell<JoinRuleValue>,
/// Whether the join rule was changed by the user.
#[property(get)]
changed: Cell<bool>,
permissions_handler: RefCell<Option<glib::SignalHandlerId>>,
join_rule_handler: RefCell<Option<glib::SignalHandlerId>>,
}
#[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<Self>) {
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<imp::JoinRuleSubpage>)
@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()
}
}

116
src/session/view/content/room_details/join_rule_subpage.ui

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="RoomDetailsJoinRuleSubpage" parent="AdwNavigationPage">
<property name="title" translatable="yes">Who Can Join</property>
<style>
<class name="form-page"/>
</style>
<property name="child">
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<property name="show-back-button">False</property>
<child type="start">
<object class="GtkButton">
<property name="icon-name">go-previous-symbolic</property>
<property name="tooltip-text" translatable="yes">Back</property>
<signal name="clicked" handler="go_back" swapped="yes"/>
<style>
<class name="back"/>
</style>
</object>
</child>
<child type="end">
<object class="LoadingButton" id="save_button">
<property name="sensitive" bind-source="RoomDetailsJoinRuleSubpage" bind-property="changed" bind-flags="sync-create" />
<property name="content-label" translatable="yes">_Save</property>
<property name="use-underline">True</property>
<signal name="clicked" handler="save" swapped="yes"/>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>
</child>
<property name="content">
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="hscrollbar-policy">never</property>
<property name="propagate-natural-height">True</property>
<property name="child">
<object class="AdwClamp">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="info_box">
<property name="visible">False</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="spacing">12</property>
<style>
<class name="dimmed"/>
</style>
<child>
<object class="GtkImage" id="info_image">
<property name="pixel-size">24</property>
<property name="accessible-role">presentation</property>
</object>
</child>
<child>
<object class="GtkLabel" id="info_description">
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="xalign">0.0</property>
<style>
<class name="body"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBox">
<child>
<object class="CheckLoadingRow">
<property name="title" translatable="yes">Only Invited Users</property>
<property name="action-name">join-rule.set-value</property>
<property name="action-target">'invite'</property>
</object>
</child>
<child>
<object class="CheckLoadingRow">
<property name="title" translatable="yes">Any Registered User</property>
<property name="action-name">join-rule.set-value</property>
<property name="action-target">'public'</property>
</object>
</child>
<style>
<class name="boxed-list"/>
</style>
</object>
</child>
<child>
<object class="GtkListBox" id="knock_box">
<child>
<object class="AdwSwitchRow" id="knock_row">
<property name="selectable">False</property>
<property name="title" translatable="yes">Allow Invite Requests</property>
<signal name="notify::active" handler="update_changed" swapped="true"/>
</object>
</child>
<style>
<class name="boxed-list"/>
</style>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</property>
</object>
</property>
</template>
</interface>

7
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 {

1
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);
}
}

1
src/ui-resources.gresource.xml

@ -96,6 +96,7 @@
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/history_viewer/visual_media_item.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/invite_subpage/mod.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/invite_subpage/row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/join_rule_subpage.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/member_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/members_page/members_list_view/membership_subpage_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/members_page/members_list_view/mod.ui</file>

Loading…
Cancel
Save