diff --git a/src/meson.build b/src/meson.build index 032fed6d..ee7f4d2a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -58,6 +58,7 @@ sources = files( 'session/room/item.rs', 'session/room/member.rs', 'session/room/mod.rs', + 'session/room/power_levels.rs', 'session/room/room.rs', 'session/room/room_type.rs', 'session/room_list.rs', diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs index fa8d4f82..c6920c6d 100644 --- a/src/session/room/mod.rs +++ b/src/session/room/mod.rs @@ -2,6 +2,7 @@ mod event; mod highlight_flags; mod item; mod member; +mod power_levels; mod room; mod room_type; mod timeline; @@ -11,6 +12,7 @@ pub use self::highlight_flags::HighlightFlags; pub use self::item::Item; pub use self::item::ItemType; pub use self::member::Member; +pub use self::power_levels::{PowerLevels, RoomAction}; pub use self::room::Room; pub use self::room_type::RoomType; pub use self::timeline::Timeline; diff --git a/src/session/room/power_levels.rs b/src/session/room/power_levels.rs new file mode 100644 index 00000000..4bd3869d --- /dev/null +++ b/src/session/room/power_levels.rs @@ -0,0 +1,141 @@ +use gtk::glib; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use matrix_sdk::ruma::events::room::power_levels::PowerLevelsEventContent; +use matrix_sdk::ruma::events::{EventType, SyncStateEvent}; + +use crate::session::room::Member; +use crate::utils::prop_expr; + +#[derive(Clone, Debug, Default, glib::GBoxed)] +#[gboxed(type_name = "BoxedPowerLevelsEventContent")] +pub struct BoxedPowerLevelsEventContent(PowerLevelsEventContent); + +mod imp { + use super::*; + use once_cell::sync::Lazy; + use std::cell::RefCell; + + #[derive(Debug, Default)] + pub struct PowerLevels { + pub content: RefCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for PowerLevels { + const NAME: &'static str = "PowerLevels"; + type Type = super::PowerLevels; + type ParentType = glib::Object; + } + + impl ObjectImpl for PowerLevels { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![glib::ParamSpec::new_boxed( + "power-levels", + "Power levels", + "Ruma struct containing all power level information of a room", + BoxedPowerLevelsEventContent::static_type(), + glib::ParamFlags::READABLE, + )] + }); + + PROPERTIES.as_ref() + } + + fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "power-levels" => obj.power_levels().to_value(), + _ => unimplemented!(), + } + } + } +} + +glib::wrapper! { + pub struct PowerLevels(ObjectSubclass); +} + +impl PowerLevels { + pub fn new() -> Self { + glib::Object::new(&[]).expect("Failed to create PowerLevels") + } + + pub fn power_levels(&self) -> BoxedPowerLevelsEventContent { + let priv_ = imp::PowerLevels::from_instance(self); + priv_.content.borrow().clone() + } + + /// Returns the power level minimally required to perform the given action. + pub fn min_level_for_room_action(&self, room_action: &RoomAction) -> u32 { + let priv_ = imp::PowerLevels::from_instance(self); + let content = priv_.content.borrow(); + min_level_for_room_action(&content.0, room_action) + } + + /// Creates an expression that is true when the user is allowed the given action. + pub fn new_allowed_expr(&self, member: &Member, room_action: RoomAction) -> gtk::Expression { + gtk::ClosureExpression::new( + move |args| { + let power_level: u32 = args[1].get().unwrap(); + let content = args[2].get::().unwrap().0; + power_level >= min_level_for_room_action(&content, &room_action) + }, + &[ + prop_expr(member, "power-level"), + prop_expr(self, "power-levels"), + ], + ) + .upcast() + } + + /// Updates the power levels from the given event. + pub fn update_from_event(&self, event: SyncStateEvent) { + let priv_ = imp::PowerLevels::from_instance(self); + let content = BoxedPowerLevelsEventContent(event.content); + priv_.content.replace(content); + self.notify("power-levels"); + } +} + +impl Default for PowerLevels { + fn default() -> Self { + Self::new() + } +} + +/// Returns the power level minimally required to perform the given action. +fn min_level_for_room_action(content: &PowerLevelsEventContent, room_action: &RoomAction) -> u32 { + let power_level = i64::from(match room_action { + RoomAction::Ban => content.ban, + RoomAction::Invite => content.invite, + RoomAction::Kick => content.kick, + RoomAction::Redact => content.redact, + RoomAction::RoomNotification => content.notifications.room, + RoomAction::StateEvent(event_type) => *content + .events + .get(event_type) + .unwrap_or(&content.state_default), + RoomAction::MessageEvent(event_type) => *content + .events + .get(event_type) + .unwrap_or(&content.events_default), + }); + + if (0..=100).contains(&power_level) { + power_level as u32 + } else { + 0 + } +} + +/// Actions that require different power levels to perform them. +pub enum RoomAction { + Ban, + Invite, + Kick, + Redact, + RoomNotification, + StateEvent(EventType), + MessageEvent(EventType), +} diff --git a/src/session/room/room.rs b/src/session/room/room.rs index 3538987e..346db85b 100644 --- a/src/session/room/room.rs +++ b/src/session/room/room.rs @@ -31,7 +31,9 @@ use std::convert::TryFrom; use crate::components::{LabelWithWidgets, Pill}; use crate::prelude::*; -use crate::session::room::{Event, HighlightFlags, Member, RoomType, Timeline}; +use crate::session::room::{ + Event, HighlightFlags, Member, PowerLevels, RoomAction, RoomType, Timeline, +}; use crate::session::{Avatar, Session}; use crate::utils::do_async; use crate::Error; @@ -56,6 +58,7 @@ mod imp { /// The user who sent the invite to this room. This is only set when this room is an invitiation. pub inviter: RefCell>, pub members_loaded: Cell, + pub power_levels: RefCell, } #[glib::object_subclass] @@ -486,6 +489,11 @@ impl Room { .filter(|topic| !topic.is_empty() && topic.find(|c: char| !c.is_whitespace()).is_some()) } + pub fn power_levels(&self) -> PowerLevels { + let priv_ = imp::Room::from_instance(self); + priv_.power_levels.borrow().clone() + } + pub fn inviter(&self) -> Option { let priv_ = imp::Room::from_instance(self); priv_.inviter.borrow().clone() @@ -563,6 +571,9 @@ impl Room { AnySyncRoomEvent::State(AnySyncStateEvent::RoomTopic(_)) => { self.notify("topic"); } + AnySyncRoomEvent::State(AnySyncStateEvent::RoomPowerLevels(event)) => { + self.power_levels().update_from_event(event); + } _ => {} } } @@ -695,6 +706,13 @@ impl Room { } } + /// Creates an expression that is true when the user is allowed the given action. + pub fn new_allowed_expr(&self, room_action: RoomAction) -> gtk::Expression { + let user_id = self.session().user().user_id(); + let member = self.member_by_id(user_id); + self.power_levels().new_allowed_expr(&member, room_action) + } + pub async fn accept_invite(&self) -> Result<(), Error> { let matrix_room = self.matrix_room(); diff --git a/src/utils.rs b/src/utils.rs index 6642b57e..7b5e8d3b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -33,7 +33,8 @@ macro_rules! event_from_sync_event { } use crate::RUNTIME; -use gtk::glib; +use gtk::gio::prelude::*; +use gtk::glib::{self, Object}; use std::future::Future; /// Execute a future on a tokio runtime and spawn a future on the local thread to handle the result pub fn do_async< @@ -54,3 +55,9 @@ pub fn do_async< RUNTIME.spawn(async move { sender.send(tokio_fut.await) }); } + +/// Returns an expression looking up the given property on `object`. +pub fn prop_expr>(object: &T, prop: &str) -> gtk::Expression { + let obj_expr = gtk::ConstantExpression::new(object).upcast(); + gtk::PropertyExpression::new(T::static_type(), Some(&obj_expr), prop).upcast() +}