diff --git a/Cargo.lock b/Cargo.lock index 33ef6545..73e1068a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3028,7 +3028,7 @@ dependencies = [ [[package]] name = "matrix-sdk" version = "0.13.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=93e25496adac5dfcd15e938438511e87fe63fcd3#93e25496adac5dfcd15e938438511e87fe63fcd3" +source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=ada68e11144507afc9d178f4264452aae1ff9e27#ada68e11144507afc9d178f4264452aae1ff9e27" dependencies = [ "anymap2", "aquamarine", @@ -3085,7 +3085,7 @@ dependencies = [ [[package]] name = "matrix-sdk-base" version = "0.13.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=93e25496adac5dfcd15e938438511e87fe63fcd3#93e25496adac5dfcd15e938438511e87fe63fcd3" +source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=ada68e11144507afc9d178f4264452aae1ff9e27#ada68e11144507afc9d178f4264452aae1ff9e27" dependencies = [ "as_variant", "async-trait", @@ -3112,7 +3112,7 @@ dependencies = [ [[package]] name = "matrix-sdk-common" version = "0.13.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=93e25496adac5dfcd15e938438511e87fe63fcd3#93e25496adac5dfcd15e938438511e87fe63fcd3" +source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=ada68e11144507afc9d178f4264452aae1ff9e27#ada68e11144507afc9d178f4264452aae1ff9e27" dependencies = [ "eyeball-im", "futures-core", @@ -3135,7 +3135,7 @@ dependencies = [ [[package]] name = "matrix-sdk-crypto" version = "0.13.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=93e25496adac5dfcd15e938438511e87fe63fcd3#93e25496adac5dfcd15e938438511e87fe63fcd3" +source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=ada68e11144507afc9d178f4264452aae1ff9e27#ada68e11144507afc9d178f4264452aae1ff9e27" dependencies = [ "aes", "aquamarine", @@ -3176,7 +3176,7 @@ dependencies = [ [[package]] name = "matrix-sdk-indexeddb" version = "0.13.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=93e25496adac5dfcd15e938438511e87fe63fcd3#93e25496adac5dfcd15e938438511e87fe63fcd3" +source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=ada68e11144507afc9d178f4264452aae1ff9e27#ada68e11144507afc9d178f4264452aae1ff9e27" dependencies = [ "anyhow", "async-trait", @@ -3204,7 +3204,7 @@ dependencies = [ [[package]] name = "matrix-sdk-qrcode" version = "0.13.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=93e25496adac5dfcd15e938438511e87fe63fcd3#93e25496adac5dfcd15e938438511e87fe63fcd3" +source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=ada68e11144507afc9d178f4264452aae1ff9e27#ada68e11144507afc9d178f4264452aae1ff9e27" dependencies = [ "byteorder", "qrcode", @@ -3216,7 +3216,7 @@ dependencies = [ [[package]] name = "matrix-sdk-sqlite" version = "0.13.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=93e25496adac5dfcd15e938438511e87fe63fcd3#93e25496adac5dfcd15e938438511e87fe63fcd3" +source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=ada68e11144507afc9d178f4264452aae1ff9e27#ada68e11144507afc9d178f4264452aae1ff9e27" dependencies = [ "as_variant", "async-trait", @@ -3241,7 +3241,7 @@ dependencies = [ [[package]] name = "matrix-sdk-store-encryption" version = "0.13.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=93e25496adac5dfcd15e938438511e87fe63fcd3#93e25496adac5dfcd15e938438511e87fe63fcd3" +source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=ada68e11144507afc9d178f4264452aae1ff9e27#ada68e11144507afc9d178f4264452aae1ff9e27" dependencies = [ "base64", "blake3", @@ -3260,7 +3260,7 @@ dependencies = [ [[package]] name = "matrix-sdk-ui" version = "0.13.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=93e25496adac5dfcd15e938438511e87fe63fcd3#93e25496adac5dfcd15e938438511e87fe63fcd3" +source = "git+https://github.com/matrix-org/matrix-rust-sdk.git?rev=ada68e11144507afc9d178f4264452aae1ff9e27#ada68e11144507afc9d178f4264452aae1ff9e27" dependencies = [ "as_variant", "async-rx", @@ -4230,7 +4230,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.12.5" -source = "git+https://github.com/ruma/ruma.git?rev=a3663c04511f79f99376924d739f84d839600de6#a3663c04511f79f99376924d739f84d839600de6" +source = "git+https://github.com/ruma/ruma.git?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "assign", "js_int", @@ -4246,7 +4246,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.20.4" -source = "git+https://github.com/ruma/ruma.git?rev=a3663c04511f79f99376924d739f84d839600de6#a3663c04511f79f99376924d739f84d839600de6" +source = "git+https://github.com/ruma/ruma.git?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "as_variant", "assign", @@ -4269,7 +4269,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.15.4" -source = "git+https://github.com/ruma/ruma.git?rev=a3663c04511f79f99376924d739f84d839600de6#a3663c04511f79f99376924d739f84d839600de6" +source = "git+https://github.com/ruma/ruma.git?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "as_variant", "base64", @@ -4302,7 +4302,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.30.4" -source = "git+https://github.com/ruma/ruma.git?rev=a3663c04511f79f99376924d739f84d839600de6#a3663c04511f79f99376924d739f84d839600de6" +source = "git+https://github.com/ruma/ruma.git?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "as_variant", "indexmap", @@ -4328,7 +4328,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.11.2" -source = "git+https://github.com/ruma/ruma.git?rev=a3663c04511f79f99376924d739f84d839600de6#a3663c04511f79f99376924d739f84d839600de6" +source = "git+https://github.com/ruma/ruma.git?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "headers", "http", @@ -4346,7 +4346,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.4.1" -source = "git+https://github.com/ruma/ruma.git?rev=a3663c04511f79f99376924d739f84d839600de6#a3663c04511f79f99376924d739f84d839600de6" +source = "git+https://github.com/ruma/ruma.git?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "as_variant", "html5ever", @@ -4358,7 +4358,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.10.1" -source = "git+https://github.com/ruma/ruma.git?rev=a3663c04511f79f99376924d739f84d839600de6#a3663c04511f79f99376924d739f84d839600de6" +source = "git+https://github.com/ruma/ruma.git?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "js_int", "thiserror 2.0.12", @@ -4367,7 +4367,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.15.2" -source = "git+https://github.com/ruma/ruma.git?rev=a3663c04511f79f99376924d739f84d839600de6#a3663c04511f79f99376924d739f84d839600de6" +source = "git+https://github.com/ruma/ruma.git?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "cfg-if", "proc-macro-crate", diff --git a/Cargo.toml b/Cargo.toml index bbdc8cec..2cc2d40a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,23 +73,23 @@ sourceview = { package = "sourceview5", version = "0.9" } [dependencies.matrix-sdk] # version = "0.13" git = "https://github.com/matrix-org/matrix-rust-sdk.git" -rev = "93e25496adac5dfcd15e938438511e87fe63fcd3" +rev = "ada68e11144507afc9d178f4264452aae1ff9e27" features = ["socks", "sso-login", "markdown", "qrcode"] [dependencies.matrix-sdk-store-encryption] # version = "0.13" git = "https://github.com/matrix-org/matrix-rust-sdk.git" -rev = "93e25496adac5dfcd15e938438511e87fe63fcd3" +rev = "ada68e11144507afc9d178f4264452aae1ff9e27" [dependencies.matrix-sdk-ui] # version = "0.13" git = "https://github.com/matrix-org/matrix-rust-sdk.git" -rev = "93e25496adac5dfcd15e938438511e87fe63fcd3" +rev = "ada68e11144507afc9d178f4264452aae1ff9e27" [dependencies.ruma] # version = "0.12.5" git = "https://github.com/ruma/ruma.git" -rev = "a3663c04511f79f99376924d739f84d839600de6" +rev = "de19ebaf71af620eb17abaefd92e43153f9d041d" features = [ "client-api-c", "markdown", @@ -102,7 +102,6 @@ features = [ "compat-unset-avatar", "compat-lax-room-create-deser", "compat-lax-room-topic-deser", - "unstable-msc3824", ] # Linux-only dependencies. diff --git a/data/resources/stylesheet/_components.scss b/data/resources/stylesheet/_components.scss index 91963ba1..320f37e0 100644 --- a/data/resources/stylesheet/_components.scss +++ b/data/resources/stylesheet/_components.scss @@ -6,7 +6,7 @@ inline-pill { border-radius: 9999px; background-color: vendor.$button_color; - + @include vendor.focus-ring(); &.activatable { @@ -46,14 +46,19 @@ role-badge { padding: 0.1em 0.5em; font-size: 0.8em; + &.creator { + color: var(--accent-fg-color); + background-color: var(--accent-purple); + } + &.admin { - color: var(--error-fg-color); - background-color: var(--error-bg-color); + color: var(--accent-fg-color); + background-color: var(--accent-red); } &.mod { - color: var(--warning-fg-color); - background-color: var(--warning-bg-color); + color: var(--accent-fg-color); + background-color: var(--accent-yellow); } &.muted { @@ -160,4 +165,4 @@ crop-circle > .mask { user-page scrolledwindow > viewport > clamp > box { margin: 12px; border-spacing: 24px; -} \ No newline at end of file +} diff --git a/src/components/power_level_selection/combo_box.rs b/src/components/power_level_selection/combo_box.rs index fb579db7..30f5cb14 100644 --- a/src/components/power_level_selection/combo_box.rs +++ b/src/components/power_level_selection/combo_box.rs @@ -1,11 +1,9 @@ use adw::{prelude::*, subclass::prelude::*}; use gtk::{CompositeTemplate, gdk, glib}; +use ruma::{Int, events::room::power_levels::UserPowerLevel}; use super::PowerLevelSelectionPopover; -use crate::{ - components::RoleBadge, - session::model::{Permissions, PowerLevel}, -}; +use crate::{components::RoleBadge, session::model::Permissions}; mod imp { use std::cell::{Cell, RefCell}; @@ -29,7 +27,7 @@ mod imp { permissions: RefCell>, /// The selected power level. #[property(get, set = Self::set_selected_power_level, explicit_notify)] - selected_power_level: Cell, + selected_power_level: Cell, } #[glib::object_subclass] @@ -75,7 +73,7 @@ mod imp { }; let power_level = self.selected_power_level.get(); - let role = permissions.role(power_level); + let role = permissions.role(UserPowerLevel::Int(Int::new_saturating(power_level))); self.selected_role_badge.set_role(role); self.selected_level_label @@ -87,7 +85,7 @@ mod imp { } /// Set the selected power level. - fn set_selected_power_level(&self, power_level: PowerLevel) { + fn set_selected_power_level(&self, power_level: i64) { if self.selected_power_level.get() == power_level { return; } diff --git a/src/components/power_level_selection/popover.rs b/src/components/power_level_selection/popover.rs index 66c12e21..8279b8c1 100644 --- a/src/components/power_level_selection/popover.rs +++ b/src/components/power_level_selection/popover.rs @@ -1,8 +1,9 @@ use adw::{prelude::*, subclass::prelude::*}; use gtk::{CompositeTemplate, glib, glib::clone}; +use ruma::events::room::power_levels::UserPowerLevel; use crate::{ - session::model::{POWER_LEVEL_ADMIN, POWER_LEVEL_MOD, Permissions, PowerLevel}, + session::model::{POWER_LEVEL_ADMIN, POWER_LEVEL_MAX, POWER_LEVEL_MOD, Permissions}, utils::BoundObject, }; @@ -48,7 +49,7 @@ mod imp { permissions: BoundObject, /// The selected power level. #[property(get, set = Self::set_selected_power_level, explicit_notify)] - selected_power_level: Cell, + selected_power_level: Cell, } #[glib::object_subclass] @@ -84,7 +85,7 @@ mod imp { self.permissions.disconnect_signals(); if let Some(permissions) = permissions { - let own_pl_handler = permissions.connect_own_power_level_notify(clone!( + let own_pl_handler = permissions.connect_own_power_level_changed(clone!( #[weak(rename_to = imp)] self, move |_| { @@ -120,7 +121,7 @@ mod imp { } /// Set the selected power level. - fn set_selected_power_level(&self, power_level: PowerLevel) { + fn set_selected_power_level(&self, power_level: i64) { if self.selected_power_level.get() == power_level { return; } @@ -148,15 +149,10 @@ mod imp { return; }; - let can_change_to_admin = permissions.own_power_level() >= POWER_LEVEL_ADMIN; + let can_change_to_admin = permissions.can_set_user_power_level_to(POWER_LEVEL_ADMIN); - if can_change_to_admin { - self.admin_row.set_sensitive(true); - self.admin_row.set_activatable(true); - } else { - self.admin_row.set_sensitive(false); - self.admin_row.set_activatable(false); - } + self.admin_row.set_sensitive(can_change_to_admin); + self.admin_row.set_activatable(can_change_to_admin); } /// Update the moderator row. @@ -165,15 +161,10 @@ mod imp { return; }; - let can_change_to_mod = permissions.own_power_level() >= POWER_LEVEL_MOD; + let can_change_to_mod = permissions.can_set_user_power_level_to(POWER_LEVEL_MOD); - if can_change_to_mod { - self.mod_row.set_sensitive(true); - self.mod_row.set_activatable(true); - } else { - self.mod_row.set_sensitive(false); - self.mod_row.set_activatable(false); - } + self.mod_row.set_sensitive(can_change_to_mod); + self.mod_row.set_activatable(can_change_to_mod); } /// Update the default row. @@ -185,15 +176,10 @@ mod imp { let default = permissions.default_power_level(); self.default_pl_label.set_label(&default.to_string()); - let can_change_to_default = permissions.own_power_level() >= default; + let can_change_to_default = permissions.can_set_user_power_level_to(default); - if can_change_to_default { - self.default_row.set_sensitive(true); - self.default_row.set_activatable(true); - } else { - self.default_row.set_sensitive(false); - self.default_row.set_activatable(false); - } + self.default_row.set_sensitive(can_change_to_default); + self.default_row.set_activatable(can_change_to_default); } /// Update the muted row. @@ -214,15 +200,10 @@ mod imp { self.muted_pl_label.set_label(&mute.to_string()); - let can_change_to_muted = permissions.own_power_level() >= mute; + let can_change_to_muted = permissions.can_set_user_power_level_to(mute); - if can_change_to_muted { - self.muted_row.set_sensitive(true); - self.muted_row.set_activatable(true); - } else { - self.muted_row.set_sensitive(false); - self.muted_row.set_activatable(false); - } + self.muted_row.set_sensitive(can_change_to_muted); + self.muted_row.set_activatable(can_change_to_muted); self.muted_row.set_visible(true); } @@ -233,8 +214,13 @@ mod imp { return; }; - self.custom_adjustment - .set_upper(permissions.own_power_level() as f64); + let max = if let UserPowerLevel::Int(value) = permissions.own_power_level() { + i64::from(value) + } else { + POWER_LEVEL_MAX + }; + self.custom_adjustment.set_upper(max as f64); + self.custom_adjustment .set_value(self.selected_power_level.get() as f64); } @@ -260,7 +246,7 @@ mod imp { /// The custom value changed. #[template_callback] fn custom_value_changed(&self) { - let power_level = self.custom_adjustment.value() as PowerLevel; + let power_level = self.custom_adjustment.value() as i64; let can_confirm = power_level != self.selected_power_level.get(); self.custom_confirm.set_sensitive(can_confirm); @@ -269,7 +255,7 @@ mod imp { /// The custom value was confirmed. #[template_callback] fn custom_value_confirmed(&self) { - let power_level = self.custom_adjustment.value() as PowerLevel; + let power_level = self.custom_adjustment.value() as i64; self.obj().popdown(); self.set_selected_power_level(power_level); diff --git a/src/components/power_level_selection/popover.ui b/src/components/power_level_selection/popover.ui index 7e5dbcf1..0f8487af 100644 --- a/src/components/power_level_selection/popover.ui +++ b/src/components/power_level_selection/popover.ui @@ -181,4 +181,4 @@ - \ No newline at end of file + diff --git a/src/components/power_level_selection/row.rs b/src/components/power_level_selection/row.rs index 66c8c55a..233ab500 100644 --- a/src/components/power_level_selection/row.rs +++ b/src/components/power_level_selection/row.rs @@ -1,23 +1,25 @@ use adw::{prelude::*, subclass::prelude::*}; -use gtk::{CompositeTemplate, glib}; +use gtk::{CompositeTemplate, glib, glib::closure_local}; +use ruma::{Int, events::room::power_levels::UserPowerLevel, int}; use super::PowerLevelSelectionPopover; use crate::{ components::{LoadingBin, RoleBadge}, - session::model::{Permissions, PowerLevel}, + session::model::Permissions, }; mod imp { use std::{ cell::{Cell, RefCell}, marker::PhantomData, + sync::LazyLock, }; - use glib::subclass::InitializingObject; + use glib::subclass::{InitializingObject, Signal}; use super::*; - #[derive(Debug, Default, CompositeTemplate, glib::Properties)] + #[derive(Debug, CompositeTemplate, glib::Properties)] #[template(resource = "/org/gnome/Fractal/ui/components/power_level_selection/row.ui")] #[properties(wrapper_type = super::PowerLevelSelectionRow)] pub struct PowerLevelSelectionRow { @@ -41,8 +43,7 @@ mod imp { #[property(get, set = Self::set_permissions, explicit_notify, nullable)] permissions: RefCell>, /// The selected power level. - #[property(get, set = Self::set_selected_power_level, explicit_notify)] - selected_power_level: Cell, + pub(super) selected_power_level: Cell, /// Whether the selected power level should be displayed in the /// subtitle, rather than next to the combo arrow. #[property(get, set = Self::set_use_subtitle, explicit_notify)] @@ -55,6 +56,26 @@ mod imp { read_only: Cell, } + impl Default for PowerLevelSelectionRow { + fn default() -> Self { + Self { + subtitle_bin: Default::default(), + combo_selection_bin: Default::default(), + arrow_box: Default::default(), + loading_bin: Default::default(), + popover: Default::default(), + selected_box: Default::default(), + selected_level_label: Default::default(), + selected_role_badge: Default::default(), + permissions: Default::default(), + selected_power_level: Cell::new(UserPowerLevel::Int(int!(0))), + use_subtitle: Default::default(), + is_loading: PhantomData, + read_only: Default::default(), + } + } + } + #[glib::object_subclass] impl ObjectSubclass for PowerLevelSelectionRow { const NAME: &'static str = "PowerLevelSelectionRow"; @@ -81,6 +102,12 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for PowerLevelSelectionRow { + fn signals() -> &'static [Signal] { + static SIGNALS: LazyLock> = + LazyLock::new(|| vec![Signal::builder("selected-power-level-changed").build()]); + SIGNALS.as_ref() + } + fn constructed(&self) { self.parent_constructed(); @@ -110,21 +137,28 @@ mod imp { let Some(permissions) = self.permissions.borrow().clone() else { return; }; - let obj = self.obj(); let power_level = self.selected_power_level.get(); let role = permissions.role(power_level); self.selected_role_badge.set_role(role); - self.selected_level_label - .set_label(&power_level.to_string()); - let role_string = format!("{power_level} {role}"); - obj.update_property(&[gtk::accessible::Property::Description(&role_string)]); + let (level_visible, accessible_desc) = if let UserPowerLevel::Int(value) = power_level { + self.selected_level_label.set_label(&value.to_string()); + self.popover.set_selected_power_level(i64::from(value)); + (true, format!("{value} {role}")) + } else { + (false, role.to_string()) + }; + + self.selected_level_label.set_visible(level_visible); + + self.obj() + .update_property(&[gtk::accessible::Property::Description(&accessible_desc)]); } /// Set the selected power level. - fn set_selected_power_level(&self, power_level: PowerLevel) { + pub(super) fn set_selected_power_level(&self, power_level: UserPowerLevel) { if self.selected_power_level.get() == power_level { return; } @@ -132,7 +166,8 @@ mod imp { self.selected_power_level.set(power_level); self.update_selected_label(); - self.obj().notify_selected_power_level(); + self.obj() + .emit_by_name::<()>("selected-power-level-changed", &[]); } /// Set whether the selected power level should be displayed in the @@ -216,6 +251,14 @@ mod imp { obj.remove_css_class("has-open-popup"); } } + + /// The selected power level changed. + #[template_callback] + fn power_level_changed(&self) { + self.set_selected_power_level(UserPowerLevel::Int(Int::new_saturating( + self.popover.selected_power_level(), + ))); + } } } @@ -230,4 +273,28 @@ impl PowerLevelSelectionRow { pub fn new() -> Self { glib::Object::new() } + + /// The selected power level. + pub(crate) fn selected_power_level(&self) -> UserPowerLevel { + self.imp().selected_power_level.get() + } + + /// Set the selected power level. + pub(crate) fn set_selected_power_level(&self, power_level: UserPowerLevel) { + self.imp().set_selected_power_level(power_level); + } + + /// Connect to the signal emitted when the selected power level changed. + pub fn connect_power_level_changed( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_closure( + "selected-power-level-changed", + true, + closure_local!(move |obj: Self| { + f(&obj); + }), + ) + } } diff --git a/src/components/power_level_selection/row.ui b/src/components/power_level_selection/row.ui index dfcca125..cb10749a 100644 --- a/src/components/power_level_selection/row.ui +++ b/src/components/power_level_selection/row.ui @@ -77,8 +77,8 @@ + - @@ -101,4 +101,4 @@ - \ No newline at end of file + diff --git a/src/components/role_badge.rs b/src/components/role_badge.rs index b6227988..73e7dd2f 100644 --- a/src/components/role_badge.rs +++ b/src/components/role_badge.rs @@ -77,6 +77,12 @@ mod imp { self.label.set_text(&role.to_string()); + if role == MemberRole::Creator { + obj.add_css_class("creator"); + } else { + obj.remove_css_class("creator"); + } + if role == MemberRole::Administrator { obj.add_css_class("admin"); } else { diff --git a/src/components/user_page.rs b/src/components/user_page.rs index f9ff90b2..9e26ac88 100644 --- a/src/components/user_page.rs +++ b/src/components/user_page.rs @@ -6,7 +6,10 @@ use gtk::{ CompositeTemplate, glib, glib::{clone, closure_local}, }; -use ruma::{OwnedEventId, events::room::power_levels::PowerLevelUserAction}; +use ruma::{ + OwnedEventId, + events::room::power_levels::{PowerLevelUserAction, UserPowerLevel}, +}; use super::{Avatar, LoadingButton, LoadingButtonRow, PowerLevelSelectionRow}; use crate::{ @@ -194,7 +197,7 @@ mod imp { } } )); - let power_level_handler = member.connect_power_level_notify(clone!( + let power_level_handler = member.connect_power_level_changed(clone!( #[weak(rename_to = imp)] self, move |_| { @@ -433,8 +436,15 @@ mod imp { }; let row = &self.power_level_row; - let power_level = row.selected_power_level(); - let old_power_level = member.power_level(); + let UserPowerLevel::Int(power_level) = row.selected_power_level() else { + // We cannot set the power level to infinite. + return; + }; + + let UserPowerLevel::Int(old_power_level) = member.power_level() else { + // We cannot change the power level if it is currently infinite. + return; + }; if old_power_level == power_level { // Nothing to do. @@ -456,8 +466,8 @@ mod imp { } else { // Warn if user is muted but was not before. let mute_power_level = permissions.mute_power_level(); - let is_muted = - power_level <= mute_power_level && old_power_level > mute_power_level; + let is_muted = i64::from(power_level) <= mute_power_level + && i64::from(old_power_level) > mute_power_level; if is_muted && !confirm_mute_room_member_dialog(slice::from_ref(&member), &*obj).await { diff --git a/src/components/user_page.ui b/src/components/user_page.ui index 3379d1b7..5b18539e 100644 --- a/src/components/user_page.ui +++ b/src/components/user_page.ui @@ -130,7 +130,7 @@ Power Level - + diff --git a/src/login/mod.rs b/src/login/mod.rs index b1dad300..57d880dc 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -288,7 +288,7 @@ mod imp { let oauth = client.oauth(); let handle = spawn_tokio!(async move { oauth - .login(redirect_uri, None, Some(client_registration_data())) + .login(redirect_uri, None, Some(client_registration_data()), None) .build() .await }); diff --git a/src/session/model/room/member.rs b/src/session/model/room/member.rs index d7c57d9e..cbc4a99a 100644 --- a/src/session/model/room/member.rs +++ b/src/session/model/room/member.rs @@ -1,18 +1,21 @@ -use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*}; +use gtk::{ + glib, + glib::{clone, closure_local}, + prelude::*, + subclass::prelude::*, +}; use matrix_sdk::room::RoomMember; use ruma::{ OwnedEventId, OwnedUserId, events::room::{ member::MembershipState, - power_levels::{NotificationPowerLevelType, PowerLevelAction}, + power_levels::{NotificationPowerLevelType, PowerLevelAction, UserPowerLevel}, }, + int, }; use tracing::{debug, error}; -use super::{ - MemberRole, Room, - permissions::{POWER_LEVEL_MAX, POWER_LEVEL_MIN, PowerLevel}, -}; +use super::{MemberRole, Room}; use crate::{components::PillSource, prelude::*, session::model::User, spawn, spawn_tokio}; /// The possible states of membership of a user in a room. @@ -54,19 +57,32 @@ impl From for Membership { } mod imp { - use std::cell::{Cell, OnceCell, RefCell}; + use std::{ + cell::{Cell, OnceCell, RefCell}, + marker::PhantomData, + sync::LazyLock, + }; + + use glib::subclass::Signal; use super::*; - #[derive(Debug, Default, glib::Properties)] + #[derive(Debug, glib::Properties)] #[properties(wrapper_type = super::Member)] pub struct Member { /// The room of the member. #[property(get, set = Self::set_room, construct_only)] room: OnceCell, /// The power level of the member. - #[property(get, minimum = POWER_LEVEL_MIN, maximum = POWER_LEVEL_MAX)] - power_level: Cell, + pub(super) power_level: Cell, + /// The power level of the member, as an `i64`. + /// + /// Should only be used for sorting. + /// + /// `i64::MAX` is used to represent an infinite power level, since it + /// cannot be reached with the Matrix specification. + #[property(get = Self::power_level_i64)] + power_level_i64: PhantomData, /// The role of the member. #[property(get, builder(MemberRole::default()))] role: Cell, @@ -79,6 +95,20 @@ mod imp { power_level_handlers: RefCell>, } + impl Default for Member { + fn default() -> Self { + Self { + room: Default::default(), + power_level: Cell::new(UserPowerLevel::Int(int!(0))), + power_level_i64: Default::default(), + role: Default::default(), + membership: Default::default(), + latest_activity: Default::default(), + power_level_handlers: Default::default(), + } + } + } + #[glib::object_subclass] impl ObjectSubclass for Member { const NAME: &'static str = "Member"; @@ -88,6 +118,12 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for Member { + fn signals() -> &'static [Signal] { + static SIGNALS: LazyLock> = + LazyLock::new(|| vec![Signal::builder("power-level-changed").build()]); + SIGNALS.as_ref() + } + fn dispose(&self) { if let Some(room) = self.room.get() { for handler in self.power_level_handlers.take() { @@ -129,14 +165,28 @@ mod imp { } /// Set the power level of the member. - pub(super) fn set_power_level(&self, power_level: PowerLevel) { + pub(super) fn set_power_level(&self, power_level: UserPowerLevel) { if self.power_level.get() == power_level { return; } self.power_level.set(power_level); self.update_role(); - self.obj().notify_power_level(); + + let obj = self.obj(); + obj.emit_by_name::<()>("power-level-changed", &[]); + obj.notify_power_level_i64(); + } + + /// The power level of the member, as an `i64`. + fn power_level_i64(&self) -> i64 { + if let UserPowerLevel::Int(power_level) = self.power_level.get() { + power_level.into() + } else { + // Represent the infinite power level with a value out of range for a power + // level. + i64::MAX + } } /// Update the role of the member. @@ -195,8 +245,13 @@ impl Member { obj } + /// The power level of the member. + pub(crate) fn power_level(&self) -> UserPowerLevel { + self.imp().power_level.get() + } + /// Set the power level of the member. - pub(super) fn set_power_level(&self, power_level: PowerLevel) { + pub(super) fn set_power_level(&self, power_level: UserPowerLevel) { self.imp().set_power_level(power_level); } @@ -268,12 +323,21 @@ impl Member { /// The string to use to search for this member. pub(crate) fn search_string(&self) -> String { - format!( - "{} {} {} {}", - self.display_name(), - self.user_id(), - self.role(), - self.power_level(), + format!("{} {} {}", self.display_name(), self.user_id(), self.role()) + } + + /// Connect to the signal emitted when the power level of the member + /// changed. + pub(crate) fn connect_power_level_changed( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_closure( + "power-level-changed", + true, + closure_local!(move |obj: Self| { + f(&obj); + }), ) } } diff --git a/src/session/model/room/member_list.rs b/src/session/model/room/member_list.rs index 454b0de3..59bf9c1e 100644 --- a/src/session/model/room/member_list.rs +++ b/src/session/model/room/member_list.rs @@ -318,7 +318,7 @@ impl MemberList { // We need to go through the whole list because we don't know who was // added/removed. for (user_id, member) in &*self.imp().members.borrow() { - member.set_power_level(power_levels.for_user(user_id).into()); + member.set_power_level(power_levels.for_user(user_id)); } } } diff --git a/src/session/model/room/mod.rs b/src/session/model/room/mod.rs index 334b9062..3c3155dd 100644 --- a/src/session/model/room/mod.rs +++ b/src/session/model/room/mod.rs @@ -867,9 +867,9 @@ mod imp { let member_event = match raw_member_event { RawSyncOrStrippedState::Sync(raw) => { - raw.deserialize_as::() + raw.deserialize_as_unchecked::() } - RawSyncOrStrippedState::Stripped(raw) => raw.deserialize_as(), + RawSyncOrStrippedState::Stripped(raw) => raw.deserialize_as_unchecked(), }; let member_event = match member_event { @@ -927,7 +927,7 @@ mod imp { match raw_prev_member_event .kind .raw() - .deserialize_as::() + .deserialize_as_unchecked::() { Ok(prev_member_event) => { prev_member_event.content.membership == MembershipState::Invite diff --git a/src/session/model/room/permissions.rs b/src/session/model/room/permissions.rs index c9558d19..062d6609 100644 --- a/src/session/model/room/permissions.rs +++ b/src/session/model/room/permissions.rs @@ -14,36 +14,28 @@ use ruma::{ MessageLikeEventType, StateEventType, SyncStateEvent, room::power_levels::{ NotificationPowerLevelType, PowerLevelAction, PowerLevelUserAction, RoomPowerLevels, - RoomPowerLevelsEventContent, + RoomPowerLevelsEventContent, RoomPowerLevelsSource, UserPowerLevel, }, }, + int, + room_version_rules::AuthorizationRules, }; use tracing::error; use super::{Member, Membership, Room}; use crate::{prelude::*, spawn, spawn_tokio}; -/// Power level of a user. -/// -/// Is usually in the range (0..=100), but can be any JS integer. -pub type PowerLevel = i64; - /// The maximum power level that can be set, according to the Matrix /// specification. /// /// This is the same value as `MAX_SAFE_INT` from the `js_int` crate. -pub const POWER_LEVEL_MAX: PowerLevel = 0x001F_FFFF_FFFF_FFFF; -/// The minimum power level that can be set, according to the Matrix -/// specification. -/// -/// This is the same value as `MIN_SAFE_INT` from the `js_int` crate. -pub const POWER_LEVEL_MIN: PowerLevel = -POWER_LEVEL_MAX; +pub const POWER_LEVEL_MAX: i64 = 0x001F_FFFF_FFFF_FFFF; /// The minimum power level to have the role of Administrator, according to the /// Matrix specification. -pub const POWER_LEVEL_ADMIN: PowerLevel = 100; +pub const POWER_LEVEL_ADMIN: i64 = 100; /// The minimum power level to have the role of Moderator, according to the /// Matrix specification. -pub const POWER_LEVEL_MOD: PowerLevel = 50; +pub const POWER_LEVEL_MOD: i64 = 50; /// Role of a room member, like admin or moderator. #[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)] @@ -59,6 +51,8 @@ pub enum MemberRole { Moderator, /// An administrator. Administrator, + /// A creator. + Creator, /// A room member that cannot send messages. Muted, } @@ -72,6 +66,7 @@ impl fmt::Display for MemberRole { Self::Custom => write!(f, "{}", gettext("Custom")), Self::Moderator => write!(f, "{}", gettext("Moderator")), Self::Administrator => write!(f, "{}", gettext("Admin")), + Self::Creator => write!(f, "{}", gettext("Creator")), // Translators: As in 'Muted room member', a member that cannot send messages. Self::Muted => write!(f, "{}", gettext("Muted")), } @@ -101,14 +96,13 @@ mod imp { #[property(get)] is_joined: Cell, /// The power level of our own member. - #[property(get)] - own_power_level: Cell, + pub(super) own_power_level: Cell, /// The default power level for members. #[property(get)] - default_power_level: Cell, + default_power_level: Cell, /// The power level to mute members. #[property(get)] - mute_power_level: Cell, + mute_power_level: Cell, /// Whether our own member can change the room's avatar. #[property(get)] can_change_avatar: Cell, @@ -142,10 +136,14 @@ mod imp { fn default() -> Self { Self { room: Default::default(), - power_levels: RefCell::new(RoomPowerLevelsEventContent::default().into()), + power_levels: RefCell::new(RoomPowerLevels::new( + RoomPowerLevelsSource::None, + &AuthorizationRules::V1, + None, + )), power_levels_drop_guard: Default::default(), is_joined: Default::default(), - own_power_level: Default::default(), + own_power_level: Cell::new(UserPowerLevel::Int(int!(0))), default_power_level: Default::default(), mute_power_level: Default::default(), can_change_avatar: Default::default(), @@ -170,8 +168,12 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for Permissions { fn signals() -> &'static [Signal] { - static SIGNALS: LazyLock> = - LazyLock::new(|| vec![Signal::builder("changed").build()]); + static SIGNALS: LazyLock> = LazyLock::new(|| { + vec![ + Signal::builder("changed").build(), + Signal::builder("own-power-level-changed").build(), + ] + }); SIGNALS.as_ref() } } @@ -206,27 +208,19 @@ mod imp { // We will probably not be able to load the power levels if we were never in the // room, so skip this. We should get the power levels when we join the room. if !matches!(matrix_room.state(), RoomState::Invited | RoomState::Knocked) { - let matrix_room_clone = matrix_room.clone(); - let handle = spawn_tokio!(async move { matrix_room_clone.power_levels().await }); - - match handle.await.expect("task was not aborted") { - Ok(power_levels) => self.update_power_levels(&power_levels), - Err(error) => { - error!("Could not load room power levels: {error}"); - } - } + self.update_power_levels().await; } let obj_weak = glib::SendWeakRef::from(self.obj().downgrade()); let handle = matrix_room.add_event_handler( - move |event: SyncStateEvent| { + move |_event: SyncStateEvent| { let obj_weak = obj_weak.clone(); async move { let ctx = glib::MainContext::default(); ctx.spawn(async move { spawn!(async move { if let Some(obj) = obj_weak.upgrade() { - obj.imp().update_power_levels(&event.power_levels()); + obj.imp().update_power_levels().await; } }); }); @@ -256,19 +250,32 @@ mod imp { self.permissions_changed(); } - /// Update the power levels with the given data. - fn update_power_levels(&self, power_levels: &RoomPowerLevels) { + /// Update the power levels with the data from the SDK's room. + async fn update_power_levels(&self) { + let Some(room) = self.room.upgrade() else { + return; + }; + + let matrix_room = room.matrix_room().clone(); + let handle = spawn_tokio!(async move { matrix_room.power_levels().await }); + + let power_levels = match handle.await.expect("task was not aborted") { + Ok(power_levels) => power_levels, + Err(error) => { + error!("Could not load room power levels: {error}"); + return; + } + }; + self.power_levels.replace(power_levels.clone()); self.permissions_changed(); - if let Some(room) = self.room.upgrade() { - if let Some(members) = room.members() { - members.update_power_levels(power_levels); - } else { - let own_member = room.own_member(); - let own_user_id = own_member.user_id(); - own_member.set_power_level(power_levels.for_user(own_user_id).into()); - } + if let Some(members) = room.members() { + members.update_power_levels(&power_levels); + } else { + let own_member = room.own_member(); + let own_user_id = own_member.user_id(); + own_member.set_power_level(power_levels.for_user(own_user_id)); } } @@ -296,18 +303,15 @@ mod imp { }; let own_member = room.own_member(); - let power_level = self - .power_levels - .borrow() - .for_user(own_member.user_id()) - .into(); + let power_level = self.power_levels.borrow().for_user(own_member.user_id()); if self.own_power_level.get() == power_level { return; } self.own_power_level.set(power_level); - self.obj().notify_own_power_level(); + self.obj() + .emit_by_name::<()>("own-power-level-changed", &[]); } /// Update the default power level for members. @@ -500,13 +504,24 @@ impl Permissions { self.imp().power_levels.borrow().clone() } + /// The power level of our own member. + pub(crate) fn own_power_level(&self) -> UserPowerLevel { + self.imp().own_power_level.get() + } + /// The power level for the user with the given ID. - pub(crate) fn user_power_level(&self, user_id: &UserId) -> PowerLevel { - self.imp().power_levels.borrow().for_user(user_id).into() + pub(crate) fn user_power_level(&self, user_id: &UserId) -> UserPowerLevel { + self.imp().power_levels.borrow().for_user(user_id) } /// The current [`MemberRole`] for the given power level. - pub(crate) fn role(&self, power_level: PowerLevel) -> MemberRole { + pub(crate) fn role(&self, power_level: UserPowerLevel) -> MemberRole { + let UserPowerLevel::Int(power_level) = power_level else { + return MemberRole::Creator; + }; + + let power_level = i64::from(power_level); + if power_level >= POWER_LEVEL_ADMIN { MemberRole::Administrator } else if power_level >= POWER_LEVEL_MOD { @@ -546,19 +561,26 @@ impl Permissions { let power_levels = imp.power_levels.borrow(); if own_user_id == user_id { - // The only action we can do for our own user is change the power level. + // The only action we can do for our own user is change the power level, if it's + // not a creator. return action == PowerLevelUserAction::ChangePowerLevel - && power_levels.user_can_send_state(own_user_id, StateEventType::RoomPowerLevels); + && power_levels.user_can_change_user_power_level(own_user_id, own_user_id); } power_levels.user_can_do_to_user(own_user_id, user_id, action) } + /// Whether our user can set the given power level for another user. + pub(crate) fn can_set_user_power_level_to(&self, power_level: i64) -> bool { + self.is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomPowerLevels)) + && self.own_power_level() >= Int::new_saturating(power_level) + } + /// Set the power level of the room member with the given user ID. pub(crate) async fn set_user_power_level( &self, user_id: OwnedUserId, - power_level: PowerLevel, + power_level: Int, ) -> Result<(), ()> { let Some(room) = self.room() else { return Err(()); @@ -566,7 +588,6 @@ impl Permissions { let matrix_room = room.matrix_room().clone(); let handle = spawn_tokio!(async move { - let power_level = Int::new_saturating(power_level); matrix_room .update_power_levels(vec![(&user_id, power_level)]) .await @@ -587,16 +608,17 @@ impl Permissions { return Err(()); }; + let event = RoomPowerLevelsEventContent::try_from(power_levels).map_err(|error| { + error!("Could not set power levels: {error}"); + })?; + let matrix_room = room.matrix_room().clone(); - let handle = spawn_tokio!(async move { - let event = RoomPowerLevelsEventContent::from(power_levels); - matrix_room.send_state_event(event).await - }); + let handle = spawn_tokio!(async move { matrix_room.send_state_event(event).await }); match handle.await.expect("task was not aborted") { Ok(_) => Ok(()), Err(error) => { - error!("Failed to set power levels: {error}"); + error!("Could not set power levels: {error}"); Err(()) } } @@ -624,6 +646,21 @@ impl Permissions { }), ) } + + /// Connect to the signal emitted when the power level of our own member + /// changed. + pub(crate) fn connect_own_power_level_changed( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_closure( + "own-power-level-changed", + true, + closure_local!(move |obj: Self| { + f(&obj); + }), + ) + } } impl Default for Permissions { diff --git a/src/session/view/content/room_details/members_page/members_list_view/mod.rs b/src/session/view/content/room_details/members_page/members_list_view/mod.rs index 2ac0f7c6..2252f0de 100644 --- a/src/session/view/content/room_details/members_page/members_list_view/mod.rs +++ b/src/session/view/content/room_details/members_page/members_list_view/mod.rs @@ -216,7 +216,7 @@ mod imp { .insert(kind, items_changed_handler); // Sort the members list by power level, then display name. - let power_level_expr = Member::this_expression("power-level"); + let power_level_expr = Member::this_expression("power-level-i64"); let sorter = gtk::MultiSorter::new(); sorter.append( gtk::NumericSorter::builder() diff --git a/src/session/view/content/room_details/permissions/add_members_subpage.rs b/src/session/view/content/room_details/permissions/add_members_subpage.rs index 84deb269..48745534 100644 --- a/src/session/view/content/room_details/permissions/add_members_subpage.rs +++ b/src/session/view/content/room_details/permissions/add_members_subpage.rs @@ -3,7 +3,7 @@ use gtk::{ CompositeTemplate, glib, glib::{clone, closure, closure_local}, }; -use ruma::OwnedUserId; +use ruma::{Int, OwnedUserId, events::room::power_levels::UserPowerLevel}; use tracing::error; use super::{MemberPowerLevel, PermissionsSelectMemberRow, PrivilegedMembers}; @@ -125,7 +125,12 @@ mod imp { // Since this is a view to add custom power levels, filter out members with // a custom power level already. - member.power_level() == permissions.default_power_level() + if let UserPowerLevel::Int(power_level) = member.power_level() { + i64::from(power_level) == permissions.default_power_level() + } else { + // There should not be members with infinite power level. + false + } } )); } @@ -245,6 +250,8 @@ mod imp { return; } + let power_level = Int::new_saturating(power_level); + // Warn if power level is set at same level as own power level. let is_own_power_level = power_level == permissions.own_power_level(); if is_own_power_level @@ -259,7 +266,7 @@ mod imp { .into_iter() .map(|(user_id, member)| { let member = MemberPowerLevel::new(&member, &permissions); - member.set_power_level(power_level); + member.set_power_level(power_level.into()); (user_id, member) }); diff --git a/src/session/view/content/room_details/permissions/member_power_level.rs b/src/session/view/content/room_details/permissions/member_power_level.rs index ba6ad56c..be590dad 100644 --- a/src/session/view/content/room_details/permissions/member_power_level.rs +++ b/src/session/view/content/room_details/permissions/member_power_level.rs @@ -1,19 +1,34 @@ use adw::subclass::prelude::*; -use gtk::{glib, glib::clone, prelude::*}; -use ruma::{Int, OwnedUserId, events::room::power_levels::PowerLevelUserAction}; +use gtk::{ + glib, + glib::{clone, closure_local}, + prelude::*, +}; +use ruma::{ + Int, OwnedUserId, + events::room::power_levels::{PowerLevelUserAction, UserPowerLevel}, + int, +}; +use tracing::error; use crate::{ prelude::*, - session::model::{MemberRole, POWER_LEVEL_MAX, POWER_LEVEL_MIN, Permissions, PowerLevel, User}, + session::model::{MemberRole, Permissions, User}, utils::BoundObjectWeakRef, }; mod imp { - use std::cell::{Cell, OnceCell}; + use std::{ + cell::{Cell, OnceCell}, + marker::PhantomData, + sync::LazyLock, + }; + + use glib::subclass::Signal; use super::*; - #[derive(Debug, Default, glib::Properties)] + #[derive(Debug, glib::Properties)] #[properties(wrapper_type = super::MemberPowerLevel)] pub struct MemberPowerLevel { /// The permissions to watch. @@ -26,8 +41,15 @@ mod imp { /// /// Initially, it should be the same as the member's, but can change /// independently. - #[property(get, set = Self::set_power_level, explicit_notify, minimum = POWER_LEVEL_MIN, maximum = POWER_LEVEL_MAX)] - power_level: Cell, + pub(super) power_level: Cell, + /// The wanted power level of the member, as an `i64`. + /// + /// Should only be used for sorting. + /// + /// `i64::MAX` is used to represent an infinite power level, since it + /// cannot be reached with the Matrix specification. + #[property(get = Self::power_level_i64)] + power_level_i64: PhantomData, /// The wanted role of the member. #[property(get, builder(MemberRole::default()))] role: Cell, @@ -36,6 +58,19 @@ mod imp { editable: Cell, } + impl Default for MemberPowerLevel { + fn default() -> Self { + Self { + permissions: Default::default(), + user: Default::default(), + power_level: Cell::new(UserPowerLevel::Int(int!(0))), + power_level_i64: Default::default(), + role: Default::default(), + editable: Default::default(), + } + } + } + #[glib::object_subclass] impl ObjectSubclass for MemberPowerLevel { const NAME: &'static str = "RoomDetailsPermissionsMemberPowerLevel"; @@ -44,6 +79,12 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for MemberPowerLevel { + fn signals() -> &'static [Signal] { + static SIGNALS: LazyLock> = + LazyLock::new(|| vec![Signal::builder("power-level-changed").build()]); + SIGNALS.as_ref() + } + fn constructed(&self) { self.parent_constructed(); @@ -81,14 +122,28 @@ mod imp { } /// Set the wanted power level of the member. - fn set_power_level(&self, power_level: PowerLevel) { + pub(super) fn set_power_level(&self, power_level: UserPowerLevel) { if self.power_level.get() == power_level { return; } self.power_level.set(power_level); self.update_role(); - self.obj().notify_power_level(); + + let obj = self.obj(); + obj.emit_by_name::<()>("power-level-changed", &[]); + obj.notify_power_level_i64(); + } + + /// The wanted power level of the member, as an `i64`. + fn power_level_i64(&self) -> i64 { + if let UserPowerLevel::Int(power_level) = self.power_level.get() { + power_level.into() + } else { + // Represent the infinite power level with a value out of range for a power + // level. + i64::MAX + } } /// Update the wanted role of the member. @@ -143,6 +198,16 @@ impl MemberPowerLevel { .build() } + /// The wanted power level of the member. + pub(crate) fn power_level(&self) -> UserPowerLevel { + self.imp().power_level.get() + } + + /// Set the wanted power level of the member. + pub(crate) fn set_power_level(&self, power_level: UserPowerLevel) { + self.imp().set_power_level(power_level); + } + /// Get the parts of this member, to use in the power levels event. /// /// Returns `None` if the permissions could not be upgraded, or if the power @@ -150,25 +215,38 @@ impl MemberPowerLevel { pub(crate) fn to_parts(&self) -> Option<(OwnedUserId, Int)> { let permissions = self.permissions()?; + let UserPowerLevel::Int(power_level) = self.power_level() else { + error!("Cannot set user power level to infinite"); + return None; + }; + let users_default = permissions.default_power_level(); - let pl = self.power_level(); - if pl == users_default { + if i64::from(power_level) == users_default { return None; } - Some((self.user().user_id().clone(), Int::new_saturating(pl))) + Some((self.user().user_id().clone(), power_level)) } /// The string to use to search for this member. pub(crate) fn search_string(&self) -> String { let user = self.user(); - format!( - "{} {} {} {}", - user.display_name(), - user.user_id(), - self.role(), - self.power_level(), + format!("{} {} {}", user.display_name(), user.user_id(), self.role()) + } + + /// Connect to the signal emitted when the power level of the member + /// changed. + pub(crate) fn connect_power_level_changed( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_closure( + "power-level-changed", + true, + closure_local!(move |obj: Self| { + f(&obj); + }), ) } } diff --git a/src/session/view/content/room_details/permissions/member_row.rs b/src/session/view/content/room_details/permissions/member_row.rs index 1029d6d2..b4020667 100644 --- a/src/session/view/content/room_details/permissions/member_row.rs +++ b/src/session/view/content/room_details/permissions/member_row.rs @@ -1,6 +1,7 @@ use std::slice; use gtk::{CompositeTemplate, glib, glib::clone, prelude::*, subclass::prelude::*}; +use ruma::{Int, events::room::power_levels::UserPowerLevel}; use super::MemberPowerLevel; use crate::{ @@ -103,7 +104,7 @@ mod imp { self.member.disconnect_signals(); if let Some(member) = member { - let power_level_handler = member.connect_power_level_notify(clone!( + let power_level_handler = member.connect_power_level_changed(clone!( #[weak(rename_to = imp)] self, move |_| { @@ -127,14 +128,21 @@ mod imp { self.obj().notify_member(); } - /// Update the power level label. + /// Update the power level. fn update_power_level(&self) { let Some(member) = self.member.obj() else { return; }; + // We should only show power levels with a value. + let UserPowerLevel::Int(power_level) = member.power_level() else { + return; + }; + self.selected_level_label - .set_label(&member.power_level().to_string()); + .set_label(&power_level.to_string()); + self.popover + .set_selected_power_level(i64::from(power_level)); } /// Update the accessible role of this row. @@ -187,8 +195,12 @@ mod imp { return; }; - let power_level = self.popover.selected_power_level(); - let old_power_level = member.power_level(); + let power_level = Int::new_saturating(self.popover.selected_power_level()); + + let UserPowerLevel::Int(old_power_level) = member.power_level() else { + // We should only edit power levels with a value. + return; + }; if power_level == old_power_level { // Nothing changed. @@ -204,7 +216,7 @@ mod imp { if room_power_level == power_level { // The power level was reset to the one in the room, nothing to check. - member.set_power_level(power_level); + member.set_power_level(power_level.into()); return; } @@ -214,18 +226,20 @@ mod imp { // Warn that demoting oneself is irreversible. if !confirm_own_demotion_dialog(&*obj).await { // Reset the value in the popover. - self.popover.set_selected_power_level(old_power_level); + self.popover + .set_selected_power_level(i64::from(old_power_level)); return; } } else { // Warn if user is muted but was not before. let mute_power_level = permissions.mute_power_level(); - let is_muted = - power_level <= mute_power_level && old_power_level > mute_power_level; + let is_muted = i64::from(power_level) <= mute_power_level + && i64::from(old_power_level) > mute_power_level; if is_muted && !confirm_mute_room_member_dialog(slice::from_ref(&user), &*obj).await { // Reset the value in the popover. - self.popover.set_selected_power_level(old_power_level); + self.popover + .set_selected_power_level(i64::from(old_power_level)); return; } @@ -239,12 +253,13 @@ mod imp { .await { // Reset the value in the popover. - self.popover.set_selected_power_level(old_power_level); + self.popover + .set_selected_power_level(i64::from(old_power_level)); return; } } - member.set_power_level(power_level); + member.set_power_level(power_level.into()); } } } diff --git a/src/session/view/content/room_details/permissions/member_row.ui b/src/session/view/content/room_details/permissions/member_row.ui index a970db6f..888da575 100644 --- a/src/session/view/content/room_details/permissions/member_row.ui +++ b/src/session/view/content/room_details/permissions/member_row.ui @@ -92,11 +92,6 @@ - - - RoomDetailsPermissionsMemberRow - - diff --git a/src/session/view/content/room_details/permissions/members_subpage.rs b/src/session/view/content/room_details/permissions/members_subpage.rs index ba679159..2156cddc 100644 --- a/src/session/view/content/room_details/permissions/members_subpage.rs +++ b/src/session/view/content/room_details/permissions/members_subpage.rs @@ -104,7 +104,7 @@ mod imp { self.update_visible_page(); // Sort members by power level, then display name, then user ID. - let power_level_expr = MemberPowerLevel::this_expression("power-level"); + let power_level_expr = MemberPowerLevel::this_expression("power-level-i64"); let power_level_sorter = gtk::NumericSorter::builder() .expression(power_level_expr) .sort_order(gtk::SortType::Descending) 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 0fd7ceec..32ec6cce 100644 --- a/src/session/view/content/room_details/permissions/permissions_subpage.rs +++ b/src/session/view/content/room_details/permissions/permissions_subpage.rs @@ -5,9 +5,10 @@ use ruma::{ Int, events::{ StateEventType, TimelineEventType, - room::power_levels::{PowerLevelAction, RoomPowerLevels}, + room::power_levels::{PowerLevelAction, RoomPowerLevels, UserPowerLevel}, }, }; +use tracing::error; use super::{PermissionsAddMembersSubpage, PermissionsMembersSubpage, PrivilegedMembers}; use crate::{ @@ -15,7 +16,7 @@ use crate::{ ButtonCountRow, LoadingButton, PowerLevelSelectionRow, UnsavedChangesResponse, unsaved_changes_dialog, }, - session::model::{Permissions, PowerLevel}, + session::model::Permissions, toast, utils::BoundObjectWeakRef, }; @@ -26,6 +27,7 @@ mod imp { use glib::subclass::InitializingObject; use super::*; + use crate::session::model::POWER_LEVEL_MAX; #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( @@ -235,7 +237,7 @@ mod imp { }; let power_levels = permissions.power_levels(); - let events_default = PowerLevel::from(power_levels.events_default); + let events_default = UserPowerLevel::from(power_levels.events_default); if self.messages_row.selected_power_level() != events_default { return true; } @@ -254,12 +256,12 @@ mod imp { return true; } - let notify_room = PowerLevel::from(power_levels.notifications.room); + let notify_room = power_levels.notifications.room; if self.notify_room_row.selected_power_level() != notify_room { return true; } - let state_default = PowerLevel::from(power_levels.state_default); + let state_default = UserPowerLevel::from(power_levels.state_default); if self.state_row.selected_power_level() != state_default { return true; } @@ -336,23 +338,23 @@ mod imp { return true; } - let invite = PowerLevel::from(power_levels.invite); + let invite = power_levels.invite; if self.invite_row.selected_power_level() != invite { return true; } - let kick = PowerLevel::from(power_levels.kick); + let kick = power_levels.kick; if self.kick_row.selected_power_level() != kick { return true; } - let ban = PowerLevel::from(power_levels.ban); + let ban = power_levels.ban; if self.ban_row.selected_power_level() != ban { return true; } - let default_pl = PowerLevel::from(power_levels.users_default); - self.members_default_adjustment.value() as PowerLevel != default_pl + let default_pl = i64::from(power_levels.users_default); + self.members_default_adjustment.value() as i64 != default_pl } /// Update the room actions section. @@ -365,15 +367,16 @@ mod imp { let power_levels = permissions.power_levels(); let own_pl = permissions.own_power_level(); - let events_default = PowerLevel::from(power_levels.events_default); - self.messages_row.set_selected_power_level(events_default); + let events_default = power_levels.events_default; + self.messages_row + .set_selected_power_level(events_default.into()); self.messages_row .set_read_only(!editable || own_pl < events_default); let redact_own = event_power_level( &power_levels, &TimelineEventType::RoomRedaction, - events_default, + events_default.into(), ); self.redact_own_row.set_selected_power_level(redact_own); self.redact_own_row @@ -385,12 +388,12 @@ mod imp { self.redact_others_row .set_read_only(!editable || own_pl < redact_others); - let notify_room = PowerLevel::from(power_levels.notifications.room); + let notify_room = power_levels.notifications.room.into(); self.notify_room_row.set_selected_power_level(notify_room); self.notify_room_row .set_read_only(!editable || own_pl < notify_room); - let state_default = PowerLevel::from(power_levels.state_default); + let state_default = power_levels.state_default.into(); self.state_row.set_selected_power_level(state_default); self.state_row .set_read_only(!editable || own_pl < state_default); @@ -490,15 +493,15 @@ mod imp { let power_levels = permissions.power_levels(); let own_pl = permissions.own_power_level(); - let invite = PowerLevel::from(power_levels.invite); + let invite = power_levels.invite.into(); self.invite_row.set_selected_power_level(invite); self.invite_row.set_read_only(!editable || own_pl < invite); - let kick = PowerLevel::from(power_levels.kick); + let kick = power_levels.kick.into(); self.kick_row.set_selected_power_level(kick); self.kick_row.set_read_only(!editable || own_pl < kick); - let ban = PowerLevel::from(power_levels.ban); + let ban = power_levels.ban.into(); self.ban_row.set_selected_power_level(ban); self.ban_row.set_read_only(!editable || own_pl < ban); } @@ -510,14 +513,21 @@ mod imp { }; let power_levels = permissions.power_levels(); - let default_pl = PowerLevel::from(power_levels.users_default); - self.members_default_adjustment.set_value(default_pl as f64); + let default_pl = power_levels.users_default; + self.members_default_adjustment + .set_value(i64::from(default_pl) as f64); self.members_default_label .set_label(&default_pl.to_string()); - // We cannot change any required power level to something higher than ours. let own_pl = permissions.own_power_level(); - let max = default_pl.max(own_pl); + let own_max = if let UserPowerLevel::Int(pl) = own_pl { + // We cannot change any power level to something higher than ours. + i64::from(pl) + } else { + // We can change the power level to any valid value. + POWER_LEVEL_MAX + }; + let max = i64::from(default_pl).max(own_max); self.members_default_adjustment.set_upper(max as f64); let editable = self.editable.get(); @@ -579,13 +589,27 @@ mod imp { /// Collect the current power levels. /// /// Returns `None` if the permissions could not be upgraded. + #[allow(clippy::too_many_lines)] fn collect_power_levels(&self) -> Option { + macro_rules! set_power_level { + ($power_levels:ident, $field:ident, $value:ident) => { + set_power_level_inner(&mut $power_levels.$field, stringify!($field), $value); + }; + ($power_levels:ident, $field:ident.$nested:ident , $value:ident) => { + set_power_level_inner( + &mut $power_levels.$field.$nested, + stringify!($field.$nested), + $value, + ); + }; + } + let permissions = self.permissions.obj()?; let mut power_levels = permissions.power_levels(); let events_default = self.messages_row.selected_power_level(); - power_levels.events_default = Int::new_saturating(events_default); + set_power_level!(power_levels, events_default, events_default); let mut redact_own = self.redact_own_row.selected_power_level(); let redact_others = self.redact_others_row.selected_power_level(); @@ -600,13 +624,13 @@ mod imp { events_default, ); - power_levels.redact = Int::new_saturating(redact_others); + set_power_level!(power_levels, redact, redact_others); let notify_room = self.notify_room_row.selected_power_level(); - power_levels.notifications.room = Int::new_saturating(notify_room); + set_power_level!(power_levels, notifications.room, notify_room); let state_default = self.state_row.selected_power_level(); - power_levels.state_default = Int::new_saturating(state_default); + set_power_level!(power_levels, state_default, state_default); let name = self.name_row.selected_power_level(); set_event_power_level( @@ -681,15 +705,15 @@ mod imp { ); let invite = self.invite_row.selected_power_level(); - power_levels.invite = Int::new_saturating(invite); + set_power_level!(power_levels, invite, invite); let kick = self.kick_row.selected_power_level(); - power_levels.kick = Int::new_saturating(kick); + set_power_level!(power_levels, kick, kick); let ban = self.ban_row.selected_power_level(); - power_levels.ban = Int::new_saturating(ban); + set_power_level!(power_levels, ban, ban); - let default_pl = self.members_default_adjustment.value() as PowerLevel; + let default_pl = self.members_default_adjustment.value() as i64; power_levels.users_default = Int::new_saturating(default_pl); let privileged_members = self.privileged_members(); @@ -789,19 +813,32 @@ impl PermissionsSubpage { } } +/// Set the power level for the given field. +fn set_power_level_inner(current_value: &mut Int, field_name: &str, new_value: UserPowerLevel) { + let UserPowerLevel::Int(new_value) = new_value else { + error!("Cannot set power level for field `{field_name}` to infinite"); + return; + }; + + *current_value = new_value; +} + /// Set the power level for the given event type in the given power levels. fn set_event_power_level( power_levels: &mut RoomPowerLevels, event_type: TimelineEventType, - value: PowerLevel, - default: PowerLevel, + value: UserPowerLevel, + default: UserPowerLevel, ) { if value == default { power_levels.events.remove(&event_type); } else { - power_levels - .events - .insert(event_type, Int::new_saturating(value)); + let UserPowerLevel::Int(value) = value else { + error!("Cannot set power level for event `{event_type}` to infinite"); + return; + }; + + power_levels.events.insert(event_type, value); } } @@ -810,8 +847,8 @@ fn set_event_power_level( fn event_power_level( power_levels: &RoomPowerLevels, event_type: &TimelineEventType, - default: i64, -) -> i64 { + default: UserPowerLevel, +) -> UserPowerLevel { power_levels .events .get(event_type) diff --git a/src/session/view/content/room_details/permissions/permissions_subpage.ui b/src/session/view/content/room_details/permissions/permissions_subpage.ui index 7148399e..dd761ded 100644 --- a/src/session/view/content/room_details/permissions/permissions_subpage.ui +++ b/src/session/view/content/room_details/permissions/permissions_subpage.ui @@ -48,7 +48,7 @@ Send Messages True - + @@ -59,7 +59,7 @@ Remove Own Messages True - + @@ -70,7 +70,7 @@ Remove Messages of Other Members True - + @@ -81,7 +81,7 @@ Notify Entire Room True - + @@ -92,7 +92,7 @@ Change Room Settings True - + @@ -103,7 +103,7 @@ Change Room Name True - + @@ -114,7 +114,7 @@ Change Room Description True - + @@ -125,7 +125,7 @@ Change Room Avatar True - + @@ -136,7 +136,7 @@ Change Addresses True - + @@ -147,7 +147,7 @@ Change Who Can Read History True - + @@ -158,7 +158,7 @@ Enable Encryption True - + @@ -169,7 +169,7 @@ Change Permissions True - + @@ -180,7 +180,7 @@ Change Server Access Control List True - + @@ -191,7 +191,7 @@ Upgrade Room True - + @@ -209,7 +209,7 @@ Invite True - + @@ -221,7 +221,7 @@ Kick True - + @@ -233,7 +233,7 @@ Ban True - + diff --git a/src/session/view/content/room_details/permissions/privileged_members.rs b/src/session/view/content/room_details/permissions/privileged_members.rs index c0924805..45f874a7 100644 --- a/src/session/view/content/room_details/permissions/privileged_members.rs +++ b/src/session/view/content/room_details/permissions/privileged_members.rs @@ -7,7 +7,7 @@ use ruma::{Int, OwnedUserId}; use super::MemberPowerLevel; use crate::{ - session::model::{Permissions, PowerLevel, User}, + session::model::{Permissions, User}, utils::BoundObjectWeakRef, }; @@ -115,7 +115,7 @@ mod imp { }); let member = MemberPowerLevel::new(&user, &permissions); - let handler = member.connect_power_level_notify(clone!( + let handler = member.connect_power_level_changed(clone!( #[weak(rename_to = imp)] self, move |_| { @@ -184,7 +184,7 @@ mod imp { return true; }; - if member.power_level() != PowerLevel::from(*pl) { + if member.power_level() != *pl { return true; } } diff --git a/src/session/view/content/room_history/message_toolbar/mod.rs b/src/session/view/content/room_history/message_toolbar/mod.rs index b4ae5bad..1ceda34d 100644 --- a/src/session/view/content/room_history/message_toolbar/mod.rs +++ b/src/session/view/content/room_history/message_toolbar/mod.rs @@ -8,13 +8,12 @@ use gtk::{ glib::{self, clone}, }; use matrix_sdk::{ - attachment::{AttachmentConfig, AttachmentInfo, BaseFileInfo, Thumbnail}, - room::{ - edit::EditedContent, - reply::{EnforceThread, Reply}, - }, + attachment::{AttachmentInfo, BaseFileInfo, Thumbnail}, + room::edit::EditedContent, +}; +use matrix_sdk_ui::timeline::{ + AttachmentConfig, AttachmentSource, TimelineEventItemId, TimelineItemContent, }; -use matrix_sdk_ui::timeline::{AttachmentSource, TimelineEventItemId, TimelineItemContent}; use ruma::{ OwnedRoomId, events::{ @@ -693,14 +692,11 @@ mod imp { // Send event depending on relation. match composer_state.related_to() { Some(RelationInfo::Reply(message_event)) => { - let reply = Reply { - event_id: message_event.event_id(), - enforce_thread: EnforceThread::MaybeThreaded, - }; + let event_id = message_event.event_id(); let handle = spawn_tokio!( - async move { matrix_timeline.send_reply(content, reply).await } + async move { matrix_timeline.send_reply(content, event_id).await } ); if let Err(error) = handle.await.expect("task was not aborted") { @@ -888,7 +884,11 @@ mod imp { return; }; - let config = AttachmentConfig::new().thumbnail(thumbnail).info(info); + let config = AttachmentConfig { + info: Some(info), + thumbnail, + ..Default::default() + }; let matrix_timeline = timeline.matrix_timeline(); diff --git a/src/session/view/content/room_history/sender_avatar/mod.rs b/src/session/view/content/room_history/sender_avatar/mod.rs index d5b1818a..1aea0ce1 100644 --- a/src/session/view/content/room_history/sender_avatar/mod.rs +++ b/src/session/view/content/room_history/sender_avatar/mod.rs @@ -3,7 +3,10 @@ use std::slice; use adw::{prelude::*, subclass::prelude::*}; use gettextrs::{gettext, ngettext}; use gtk::{CompositeTemplate, gdk, glib, glib::clone}; -use ruma::{OwnedEventId, events::room::power_levels::PowerLevelUserAction}; +use ruma::{ + Int, OwnedEventId, + events::room::power_levels::{PowerLevelUserAction, UserPowerLevel}, +}; use crate::{ Window, @@ -247,7 +250,7 @@ mod imp { } )); - let power_level_handler = sender.connect_power_level_notify(clone!( + let power_level_handler = sender.connect_power_level_changed(clone!( #[weak(rename_to = imp)] self, move |_| { @@ -610,9 +613,14 @@ mod imp { let Some(sender) = self.sender.obj() else { return; }; - let obj = self.obj(); - let old_power_level = sender.power_level(); + let UserPowerLevel::Int(old_power_level) = sender.power_level() else { + // We cannot mute someone with an infinite power level. + return; + }; + + let old_power_level = i64::from(old_power_level); + let obj = self.obj(); let permissions = sender.room().permissions(); // Warn if user is muted but was not before. @@ -635,7 +643,7 @@ mod imp { toast!(obj, text); let text = if permissions - .set_user_power_level(user_id, new_power_level) + .set_user_power_level(user_id, Int::new_saturating(new_power_level)) .await .is_ok() {