Browse Source

user-page: Allow to change the role of a room member

merge-requests/1461/merge
Kévin Commaille 2 years ago
parent
commit
b05bdfcc7e
No known key found for this signature in database
GPG Key ID: 29A48C1F03620416
  1. 45
      data/resources/style.css
  2. 3
      po/POTFILES.in
  3. 4
      src/components/mod.rs
  4. 94
      src/components/power_level_badge.rs
  5. 112
      src/components/role_badge.rs
  6. 4
      src/components/rows/mod.rs
  7. 385
      src/components/rows/power_level_selection_row.rs
  8. 228
      src/components/rows/power_level_selection_row.ui
  9. 154
      src/components/user_page.rs
  10. 24
      src/components/user_page.ui
  11. 63
      src/session/model/room/member.rs
  12. 55
      src/session/model/room/member_role.rs
  13. 4
      src/session/model/room/mod.rs
  14. 170
      src/session/model/room/permissions.rs
  15. 4
      src/session/view/content/room_details/members_page/members_list_view/member_row.rs
  16. 9
      src/session/view/content/room_details/members_page/members_list_view/member_row.ui
  17. 1
      src/ui-resources.gresource.xml
  18. 65
      src/utils/message_dialog.rs

45
data/resources/style.css

@ -182,25 +182,29 @@ button.overlaid {
background-color: @accent_bg_color;
}
power-level-badge {
role-badge {
color: @dark_5;
background-color: @light_3;
border-radius: 0.4em;
padding: 0.1em 0.5em;
font-size: 0.8em;
margin-left: 0.5em;
}
power-level-badge.admin {
role-badge.admin {
color: @error_fg_color;
background-color: @error_bg_color;
}
power-level-badge.mod {
role-badge.mod {
color: @warning_fg_color;
background-color: @warning_bg_color;
}
role-badge.muted {
color: @light_1;
background-color: @dark_2;
}
media-viewer toolbarview headerbar {
background: black;
color: white;
@ -238,6 +242,39 @@ spinner-wrapper.large spinner {
opacity: 0.55;
}
.role-selection-row popover viewport > box {
padding: 6px;
}
.role-selection-row popover list row.spin {
padding: 0;
}
.role-selection-row popover row.spin > box {
min-height: 30px;
}
.role-selection-row popover row.spin spinbutton > button {
margin-top: 6px;
margin-bottom: 6px;
}
.role-selection-row popover row.spin button.spin-confirm {
min-height: 22px;
min-width: 22px;
padding: 0;
margin-left: 2px;
}
.role-selection-row popover row.spin button.spin-confirm:dir(rtl) {
margin-left: 0;
margin-right: 2px;
}
.role-selection-row popover row.spin button.spin-confirm image {
padding: 7px;
}
/* Login */

3
po/POTFILES.in

@ -20,6 +20,7 @@ src/components/location_viewer.rs
src/components/media_content_viewer.rs
src/components/reaction_chooser.ui
src/components/rows/loading_row.ui
src/components/rows/power_level_selection_row.ui
src/components/spinner.rs
src/components/user_page.rs
src/components/user_page.ui
@ -41,8 +42,8 @@ src/login/sso_page.ui
src/secret/linux.rs
src/session/model/session.rs
src/session/model/room/join_rule.rs
src/session/model/room/member_role.rs
src/session/model/room/mod.rs
src/session/model/room/permissions.rs
src/session/model/room_list/mod.rs
src/session/model/sidebar_data/category/category_type.rs
src/session/model/sidebar_data/icon_item.rs

4
src/components/mod.rs

@ -14,8 +14,8 @@ mod location_viewer;
mod media_content_viewer;
mod overlapping_avatars;
mod pill;
mod power_level_badge;
mod reaction_chooser;
mod role_badge;
mod room_title;
mod rows;
mod scale_revealer;
@ -44,8 +44,8 @@ pub use self::{
media_content_viewer::{ContentType, MediaContentViewer},
overlapping_avatars::OverlappingAvatars,
pill::*,
power_level_badge::PowerLevelBadge,
reaction_chooser::ReactionChooser,
role_badge::RoleBadge,
room_title::RoomTitle,
rows::*,
scale_revealer::ScaleRevealer,

94
src/components/power_level_badge.rs

@ -1,94 +0,0 @@
use adw::{prelude::*, subclass::prelude::*};
use gtk::glib;
use crate::session::model::{MemberRole, PowerLevel, POWER_LEVEL_MAX, POWER_LEVEL_MIN};
mod imp {
use std::cell::Cell;
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::PowerLevelBadge)]
pub struct PowerLevelBadge {
pub label: gtk::Label,
/// The power level displayed by this badge.
#[property(get, set = Self::set_power_level, explicit_notify, minimum = POWER_LEVEL_MIN, maximum = POWER_LEVEL_MAX)]
pub power_level: Cell<PowerLevel>,
}
#[glib::object_subclass]
impl ObjectSubclass for PowerLevelBadge {
const NAME: &'static str = "PowerLevelBadge";
type Type = super::PowerLevelBadge;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
klass.set_css_name("power-level-badge");
}
}
#[glib::derived_properties]
impl ObjectImpl for PowerLevelBadge {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.set_child(Some(&self.label));
}
}
impl WidgetImpl for PowerLevelBadge {}
impl BinImpl for PowerLevelBadge {}
impl PowerLevelBadge {
/// Set the power level this badge displays.
fn set_power_level(&self, power_level: PowerLevel) {
let obj = self.obj();
obj.update_badge(power_level);
self.power_level.set(power_level);
obj.notify_power_level();
}
}
}
glib::wrapper! {
/// Inline widget displaying a badge with a power level.
///
/// The badge displays admin for a power level of 100 and mod for levels
/// over or equal to 50.
pub struct PowerLevelBadge(ObjectSubclass<imp::PowerLevelBadge>)
@extends gtk::Widget, adw::Bin;
}
impl PowerLevelBadge {
pub fn new() -> Self {
glib::Object::new()
}
/// Update the badge for the given power level.
fn update_badge(&self, power_level: PowerLevel) {
let label = &self.imp().label;
let role = MemberRole::from(power_level);
match role {
MemberRole::Admin => {
label.set_text(&format!("{role} {power_level}"));
self.add_css_class("admin");
self.remove_css_class("mod");
}
MemberRole::Mod => {
label.set_text(&format!("{role} {power_level}"));
self.add_css_class("mod");
self.remove_css_class("admin");
}
MemberRole::Peasant => {
label.set_text(&power_level.to_string());
self.remove_css_class("admin");
self.remove_css_class("mod");
}
};
self.set_visible(power_level != 0);
}
}

112
src/components/role_badge.rs

@ -0,0 +1,112 @@
use adw::{prelude::*, subclass::prelude::*};
use gtk::glib;
use crate::session::model::MemberRole;
mod imp {
use std::cell::Cell;
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::RoleBadge)]
pub struct RoleBadge {
pub label: gtk::Label,
/// The role displayed by this badge.
#[property(get, set = Self::set_role, explicit_notify, builder(MemberRole::default()))]
pub role: Cell<MemberRole>,
/// Whether the role displayed by this badge is the default role.
#[property(get)]
pub is_default_role: Cell<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for RoleBadge {
const NAME: &'static str = "RoleBadge";
type Type = super::RoleBadge;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
klass.set_css_name("role-badge");
}
}
#[glib::derived_properties]
impl ObjectImpl for RoleBadge {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.set_child(Some(&self.label));
self.update_badge();
self.update_is_default_role();
}
}
impl WidgetImpl for RoleBadge {}
impl BinImpl for RoleBadge {}
impl RoleBadge {
/// Set the role displayed by this badge.
fn set_role(&self, role: MemberRole) {
if self.role.get() == role {
return;
}
self.role.set(role);
self.update_badge();
self.update_is_default_role();
self.obj().notify_role();
}
/// Update whether the role displayed by this badge is the default role.
fn update_is_default_role(&self) {
let is_default = self.role.get() == MemberRole::Default;
if self.is_default_role.get() == is_default {
return;
}
self.is_default_role.set(is_default);
self.obj().notify_is_default_role();
}
/// Update the badge for the current state.
fn update_badge(&self) {
let obj = self.obj();
let role = self.role.get();
self.label.set_text(&role.to_string());
if role == MemberRole::Administrator {
obj.add_css_class("admin");
} else {
obj.remove_css_class("admin");
}
if role == MemberRole::Moderator {
obj.add_css_class("mod");
} else {
obj.remove_css_class("mod");
}
if role == MemberRole::Muted {
obj.add_css_class("muted");
} else {
obj.remove_css_class("muted");
}
}
}
}
glib::wrapper! {
/// Inline widget displaying a badge with the role of a room member.
pub struct RoleBadge(ObjectSubclass<imp::RoleBadge>)
@extends gtk::Widget, adw::Bin;
}
impl RoleBadge {
pub fn new() -> Self {
glib::Object::new()
}
}

4
src/components/rows/mod.rs

@ -7,6 +7,7 @@ mod combo_loading_row;
mod copyable_row;
mod entry_add_row;
mod loading_row;
mod power_level_selection_row;
mod removable_row;
mod substring_entry_row;
mod switch_loading_row;
@ -14,6 +15,7 @@ mod switch_loading_row;
pub use self::{
button_count_row::ButtonCountRow, button_row::ButtonRow, check_loading_row::CheckLoadingRow,
combo_loading_row::ComboLoadingRow, copyable_row::CopyableRow, entry_add_row::EntryAddRow,
loading_row::LoadingRow, removable_row::RemovableRow, substring_entry_row::SubstringEntryRow,
loading_row::LoadingRow, power_level_selection_row::PowerLevelSelectionRow,
removable_row::RemovableRow, substring_entry_row::SubstringEntryRow,
switch_loading_row::SwitchLoadingRow,
};

385
src/components/rows/power_level_selection_row.rs

@ -0,0 +1,385 @@
use adw::{prelude::*, subclass::prelude::*};
use gtk::{glib, glib::clone, CompositeTemplate};
use crate::{
components::{LoadingBin, RoleBadge},
session::model::{Permissions, PowerLevel, POWER_LEVEL_ADMIN, POWER_LEVEL_MOD},
utils::BoundObject,
};
mod imp {
use std::{cell::Cell, marker::PhantomData};
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/components/rows/power_level_selection_row.ui")]
#[properties(wrapper_type = super::PowerLevelSelectionRow)]
pub struct PowerLevelSelectionRow {
#[template_child]
pub selected_box: TemplateChild<gtk::Box>,
#[template_child]
pub selected_level_label: TemplateChild<gtk::Label>,
#[template_child]
pub selected_role_badge: TemplateChild<RoleBadge>,
#[template_child]
pub loading_bin: TemplateChild<LoadingBin>,
#[template_child]
pub popover: TemplateChild<gtk::Popover>,
#[template_child]
pub admin_row: TemplateChild<gtk::ListBoxRow>,
#[template_child]
pub admin_selected: TemplateChild<gtk::Image>,
#[template_child]
pub mod_row: TemplateChild<gtk::ListBoxRow>,
#[template_child]
pub mod_selected: TemplateChild<gtk::Image>,
#[template_child]
pub default_row: TemplateChild<gtk::ListBoxRow>,
#[template_child]
pub default_pl_label: TemplateChild<gtk::Label>,
#[template_child]
pub default_selected: TemplateChild<gtk::Image>,
#[template_child]
pub muted_row: TemplateChild<gtk::ListBoxRow>,
#[template_child]
pub muted_pl_label: TemplateChild<gtk::Label>,
#[template_child]
pub muted_selected: TemplateChild<gtk::Image>,
#[template_child]
pub custom_row: TemplateChild<adw::SpinRow>,
#[template_child]
pub custom_adjustment: TemplateChild<gtk::Adjustment>,
#[template_child]
pub custom_confirm: TemplateChild<gtk::Button>,
/// The permissions to watch.
#[property(get, set = Self::set_permissions, explicit_notify, nullable)]
pub permissions: BoundObject<Permissions>,
/// The selected power level.
#[property(get, set = Self::set_selected_power_level, explicit_notify)]
pub selected_power_level: Cell<PowerLevel>,
/// Whether the row is loading.
#[property(get = Self::is_loading, set = Self::set_is_loading)]
pub is_loading: PhantomData<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for PowerLevelSelectionRow {
const NAME: &'static str = "PowerLevelSelectionRow";
type Type = super::PowerLevelSelectionRow;
type ParentType = adw::ActionRow;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for PowerLevelSelectionRow {
fn constructed(&self) {
self.parent_constructed();
self.obj()
.reset_relation(gtk::AccessibleRelation::DescribedBy);
}
}
impl WidgetImpl for PowerLevelSelectionRow {}
impl ListBoxRowImpl for PowerLevelSelectionRow {}
impl PreferencesRowImpl for PowerLevelSelectionRow {}
impl ActionRowImpl for PowerLevelSelectionRow {
fn activate(&self) {
if !self.is_loading() {
self.popover.popup();
}
}
}
impl PowerLevelSelectionRow {
/// Set the permissions to watch.
fn set_permissions(&self, permissions: Option<Permissions>) {
if self.permissions.obj() == permissions {
return;
}
let obj = self.obj();
self.permissions.disconnect_signals();
if let Some(permissions) = permissions {
let own_pl_handler = permissions.connect_own_power_level_notify(
clone!(@weak self as imp => move |_| {
imp.update();
}),
);
let default_pl_handler = permissions.connect_default_power_level_notify(
clone!(@weak self as imp => move |_| {
imp.update_default();
imp.update_muted();
imp.update_selection();
}),
);
let muted_pl_handler = permissions.connect_mute_power_level_notify(
clone!(@weak self as imp => move |_| {
imp.update_muted();
imp.update_selection();
}),
);
self.permissions.set(
permissions,
vec![own_pl_handler, default_pl_handler, muted_pl_handler],
);
}
self.update();
self.update_selected_label();
obj.notify_permissions();
}
/// Update the label of the selected power level.
fn update_selected_label(&self) {
let Some(permissions) = self.permissions.obj() 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)]);
}
/// Set the selected power level.
fn set_selected_power_level(&self, power_level: PowerLevel) {
if self.selected_power_level.get() == power_level {
return;
}
self.selected_power_level.set(power_level);
self.update_selected_label();
self.update_selection();
self.update_custom();
self.obj().notify_selected_power_level();
}
/// Whether the row is loading.
fn is_loading(&self) -> bool {
self.loading_bin.is_loading()
}
/// Set whether the row is loading.
fn set_is_loading(&self, loading: bool) {
if self.is_loading() == loading {
return;
}
self.loading_bin.set_is_loading(loading);
self.obj().notify_is_loading();
}
/// Update the rows.
fn update(&self) {
self.update_admin();
self.update_mod();
self.update_default();
self.update_muted();
self.update_custom();
self.update_selection();
}
/// Update the admin row.
fn update_admin(&self) {
let Some(permissions) = self.permissions.obj() else {
return;
};
let can_change_to_admin = permissions.own_power_level() >= 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);
}
}
/// Update the moderator row.
fn update_mod(&self) {
let Some(permissions) = self.permissions.obj() else {
return;
};
let can_change_to_mod = permissions.own_power_level() >= 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);
}
}
/// Update the default row.
fn update_default(&self) {
let Some(permissions) = self.permissions.obj() else {
return;
};
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;
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);
}
}
/// Update the muted row.
fn update_muted(&self) {
let Some(permissions) = self.permissions.obj() else {
return;
};
let mute = permissions.mute_power_level();
let default = permissions.default_power_level();
if mute >= default {
// There is no point in having the muted row since all users are muted by
// default.
self.muted_row.set_visible(false);
return;
}
self.muted_pl_label.set_label(&mute.to_string());
let can_change_to_muted = permissions.own_power_level() >= 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_visible(true);
}
/// Update the custom row.
fn update_custom(&self) {
let Some(permissions) = self.permissions.obj() else {
return;
};
self.custom_adjustment
.set_upper(permissions.own_power_level() as f64);
self.custom_adjustment
.set_value(self.selected_power_level.get() as f64);
}
/// Update the selected row.
fn update_selection(&self) {
let Some(permissions) = self.permissions.obj() else {
return;
};
let power_level = self.selected_power_level.get();
self.admin_selected
.set_opacity((power_level == POWER_LEVEL_ADMIN).into());
self.mod_selected
.set_opacity((power_level == POWER_LEVEL_MOD).into());
self.default_selected
.set_opacity((power_level == permissions.default_power_level()).into());
self.muted_selected
.set_opacity((power_level == permissions.mute_power_level()).into());
}
}
}
glib::wrapper! {
/// An `AdwActionRow` behaving like a combo box to select a room member's power level.
pub struct PowerLevelSelectionRow(ObjectSubclass<imp::PowerLevelSelectionRow>)
@extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow,
@implements gtk::Actionable, gtk::Accessible;
}
#[gtk::template_callbacks]
impl PowerLevelSelectionRow {
pub fn new() -> Self {
glib::Object::new()
}
/// The custom value changed.
#[template_callback]
fn custom_value_changed(&self) {
let imp = self.imp();
let power_level = imp.custom_adjustment.value() as PowerLevel;
let can_confirm = power_level != self.selected_power_level();
imp.custom_confirm.set_sensitive(can_confirm);
}
/// The custom value was confirmed.
#[template_callback]
fn custom_value_confirmed(&self) {
let imp = self.imp();
let power_level = imp.custom_adjustment.value() as PowerLevel;
imp.popover.popdown();
self.set_selected_power_level(power_level);
}
/// A row was activated.
#[template_callback]
fn row_activated(&self, row: &gtk::ListBoxRow) {
let Some(permissions) = self.permissions() else {
return;
};
let imp = self.imp();
let power_level = match row.index() {
0 => POWER_LEVEL_ADMIN,
1 => POWER_LEVEL_MOD,
2 => permissions.default_power_level(),
3 => permissions.mute_power_level(),
_ => return,
};
imp.popover.popdown();
self.set_selected_power_level(power_level);
}
/// The popover's visibility changed.
#[template_callback]
fn popover_visible(&self) {
let is_visible = self.imp().popover.is_visible();
if is_visible {
self.add_css_class("has-open-popup");
} else {
self.remove_css_class("has-open-popup");
}
}
}

228
src/components/rows/power_level_selection_row.ui

@ -0,0 +1,228 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="PowerLevelSelectionRow" parent="AdwActionRow">
<property name="selectable">False</property>
<property name="activatable">True</property>
<property name="accessible-role">combo-box</property>
<style>
<class name="combo" />
<class name="role-selection-row" />
</style>
<child>
<object class="GtkBox" id="selected_box">
<property name="accessible-role">group</property>
<property name="spacing">12</property>
<property name="margin-end">6</property>
<child>
<object class="GtkLabel" id="selected_level_label" />
</child>
<child>
<object class="RoleBadge" id="selected_role_badge">
<property name="valign">center</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="arrow_box">
<property name="valign">center</property>
<child>
<object class="LoadingBin" id="loading_bin">
<property name="child">
<object class="GtkImage">
<property name="icon_name">pan-down-symbolic</property>
<property name="accessible-role">presentation</property>
<style>
<class name="dropdown-arrow" />
</style>
</object>
</property>
</object>
</child>
<child>
<object class="GtkPopover" id="popover">
<signal name="notify::visible" handler="popover_visible" swapped="true" />
<style>
<class name="menu" />
</style>
<property name="child">
<object class="GtkScrolledWindow">
<property name="hscrollbar_policy">never</property>
<property name="max-content-height">400</property>
<property name="propagate-natural-width">True</property>
<property name="propagate-natural-height">True</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkListBox">
<signal name="row-activated" handler="row_activated" swapped="true" />
<child>
<object class="GtkListBoxRow" id="admin_row">
<property name="selectable">False</property>
<property name="child">
<object class="GtkBox">
<property name="accessible-role">group</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label">100</property>
<property name="width-chars">3</property>
<property name="xalign">1.0</property>
</object>
</child>
<child>
<object class="RoleBadge">
<property name="role">administrator</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkImage" id="admin_selected">
<property name="icon-name">object-select-symbolic</property>
<property name="accessible-role">presentation</property>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkListBoxRow" id="mod_row">
<property name="selectable">False</property>
<property name="child">
<object class="GtkBox">
<property name="accessible-role">group</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label">50</property>
<property name="width-chars">3</property>
<property name="xalign">1.0</property>
</object>
</child>
<child>
<object class="RoleBadge">
<property name="role">moderator</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkImage" id="mod_selected">
<property name="icon-name">object-select-symbolic</property>
<property name="accessible-role">presentation</property>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkListBoxRow" id="default_row">
<property name="selectable">False</property>
<property name="child">
<object class="GtkBox">
<property name="accessible-role">group</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="default_pl_label">
<property name="width-chars">3</property>
<property name="xalign">1.0</property>
</object>
</child>
<child>
<object class="RoleBadge">
<property name="role">default</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkImage" id="default_selected">
<property name="icon-name">object-select-symbolic</property>
<property name="accessible-role">presentation</property>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkListBoxRow" id="muted_row">
<property name="selectable">False</property>
<property name="child">
<object class="GtkBox">
<property name="accessible-role">group</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="muted_pl_label">
<property name="width-chars">3</property>
<property name="xalign">1.0</property>
</object>
</child>
<child>
<object class="RoleBadge">
<property name="role">muted</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkImage" id="muted_selected">
<property name="icon-name">object-select-symbolic</property>
<property name="accessible-role">presentation</property>
</object>
</child>
</object>
</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkSeparator" />
</child>
<child>
<object class="GtkListBox">
<child>
<object class="AdwSpinRow" id="custom_row">
<property name="selectable">False</property>
<!-- Translators: As in 'Custom role'. -->
<property name="title" translatable="yes">Custom</property>
<property name="numeric">True</property>
<property name="adjustment">
<object class="GtkAdjustment" id="custom_adjustment">
<!-- js_int::MIN_SAFE_INT -->
<property name="lower">-9007199254740991</property>
<property name="upper">100</property>
<property name="page-increment">10</property>
<property name="step-increment">1</property>
<signal name="notify::value" handler="custom_value_changed" swapped="yes" />
</object>
</property>
<child>
<object class="GtkButton" id="custom_confirm">
<property name="icon-name">checkmark-symbolic</property>
<property name="tooltip-text" translatable="yes">Confirm Custom Role</property>
<signal name="clicked" handler="custom_value_confirmed" swapped="yes" />
<property name="valign">center</property>
<property name="sensitive">False</property>
<style>
<class name="suggested-action" />
<class name="circular" />
<class name="spin-confirm" />
</style>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</child>
</object>
</child>
</template>
</interface>

154
src/components/user_page.rs

@ -7,15 +7,18 @@ use gtk::{
};
use ruma::{events::room::power_levels::PowerLevelUserAction, OwnedEventId};
use super::{Avatar, SpinnerButton};
use super::{Avatar, PowerLevelSelectionRow, SpinnerButton};
use crate::{
i18n::gettext_f,
ngettext_f,
prelude::*,
session::model::{Member, MemberRole, Membership, Room, User},
session::model::{Member, Membership, Permissions, Room, User},
spawn, toast,
utils::{
message_dialog::{confirm_room_member_destructive_action, RoomMemberDestructiveAction},
message_dialog::{
confirm_mute_room_member, confirm_room_member_destructive_action,
confirm_set_room_member_power_level_same_as_own, RoomMemberDestructiveAction,
},
BoundObject,
},
Window,
@ -48,13 +51,11 @@ mod imp {
#[template_child]
pub room_title: TemplateChild<gtk::Label>,
#[template_child]
pub role_row: TemplateChild<adw::ActionRow>,
pub membership_row: TemplateChild<adw::ActionRow>,
#[template_child]
pub role_group: TemplateChild<gtk::Box>,
pub membership_label: TemplateChild<gtk::Label>,
#[template_child]
pub role_label: TemplateChild<gtk::Label>,
#[template_child]
pub power_level_label: TemplateChild<gtk::Label>,
pub power_level_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub invite_button: TemplateChild<SpinnerButton>,
#[template_child]
@ -158,6 +159,7 @@ mod imp {
binding.unbind();
}
self.user.disconnect_signals();
self.power_level_row.set_permissions(None::<Permissions>);
if let Some(user) = user {
let title_binding = user
@ -168,28 +170,28 @@ mod imp {
.bind_property("avatar-data", &*self.avatar, "data")
.sync_create()
.build();
self.bindings.replace(vec![title_binding, avatar_binding]);
let bindings = vec![title_binding, avatar_binding];
let mut handlers = Vec::new();
handlers.push(user.connect_verified_notify(clone!(@weak obj => move |_| {
let verified_handler = user.connect_verified_notify(clone!(@weak obj => move |_| {
obj.update_verified();
})));
handlers.push(
}));
let ignored_handler =
user.connect_is_ignored_notify(clone!(@weak obj => move |_| {
obj.update_direct_chat();
obj.update_ignored();
})),
);
}));
let mut handlers = vec![verified_handler, ignored_handler];
if let Some(member) = user.downcast_ref::<Member>() {
let room = member.room();
let permissions = room.permissions();
let permissions_handler =
room.permissions()
.connect_changed(clone!(@weak obj => move |_| {
obj.update_room();
}));
permissions.connect_changed(clone!(@weak obj => move |_| {
obj.update_room();
}));
self.permissions_handler.replace(Some(permissions_handler));
self.power_level_row.set_permissions(Some(permissions));
let room_display_name_handler =
room.connect_display_name_notify(clone!(@weak obj => move |_| {
@ -198,20 +200,19 @@ mod imp {
self.room_display_name_handler
.replace(Some(room_display_name_handler));
handlers.push(member.connect_membership_notify(
clone!(@weak obj => move |member| {
let membership_handler =
member.connect_membership_notify(clone!(@weak obj => move |member| {
if member.membership() == Membership::Leave {
obj.emit_by_name::<()>("close", &[]);
} else {
obj.update_room();
}
}),
));
handlers.push(member.connect_power_level_notify(
clone!(@weak obj => move |_| {
}));
let power_level_handler =
member.connect_power_level_notify(clone!(@weak obj => move |_| {
obj.update_room();
}),
));
}));
handlers.extend([membership_handler, power_level_handler]);
}
// We don't need to listen to changes of the property, it never changes after
@ -220,6 +221,7 @@ mod imp {
self.ignored_row.set_visible(!is_own_user);
self.user.set(user, handlers);
self.bindings.replace(bindings);
}
obj.load_direct_chat();
@ -340,45 +342,45 @@ impl UserPage {
match membership {
Membership::Leave => unreachable!(),
Membership::Join => {
let power_level = member.power_level();
let role = MemberRole::from(power_level).to_string();
imp.role_label.set_label(&role);
imp.power_level_label.set_label(&power_level.to_string());
imp.role_group
.update_property(&[gtk::accessible::Property::Label(&format!(
"{role} {power_level}"
))]);
// Nothing to update, it should show the role row.
}
Membership::Invite => {
// Translators: As in, 'The room member was invited'.
imp.role_label.set_label(&pgettext("member", "Invited"));
imp.membership_label
// Translators: As in, 'The room member was invited'.
.set_label(&pgettext("member", "Invited"));
}
Membership::Ban => {
// Translators: As in, 'The room member was banned'.
imp.role_label.set_label(&pgettext("member", "Banned"));
imp.membership_label
// Translators: As in, 'The room member was banned'.
.set_label(&pgettext("member", "Banned"));
}
Membership::Knock => {
// Translators: As in, 'The room member knocked to request access to the room'.
imp.role_label.set_label(&pgettext("member", "Knocked"));
imp.membership_label
// Translators: As in, 'The room member knocked to request access to the room'.
.set_label(&pgettext("member", "Knocked"));
}
Membership::Custom => {
// Translators: As in, 'The room member has an unknown role'.
imp.role_label.set_label(&pgettext("member", "Unknown"));
imp.membership_label
// Translators: As in, 'The room member has an unknown role'.
.set_label(&pgettext("member", "Unknown"));
}
}
if matches!(membership, Membership::Ban) {
imp.role_label.add_css_class("error");
} else {
imp.role_label.remove_css_class("error");
}
imp.power_level_label
.set_visible(matches!(membership, Membership::Join));
let is_role = membership == Membership::Join;
imp.membership_row.set_visible(!is_role);
imp.power_level_row.set_visible(is_role);
let permissions = room.permissions();
let user_id = member.user_id();
imp.power_level_row.set_is_loading(false);
imp.power_level_row
.set_selected_power_level(member.power_level());
let can_change_power_level = !member.is_own_user()
&& permissions.can_do_to_user(user_id, PowerLevelUserAction::ChangePowerLevel);
imp.power_level_row.set_sensitive(can_change_power_level);
let can_invite = matches!(membership, Membership::Knock) && permissions.can_invite();
if can_invite {
imp.invite_button.set_content_label(gettext("Allow Access"));
@ -438,6 +440,56 @@ impl UserPage {
imp.remove_messages_button.set_sensitive(true);
}
/// Set the power level of the user.
#[template_callback]
fn set_power_level(&self) {
let Some(member) = self.user().and_downcast::<Member>() else {
return;
};
let row = &self.imp().power_level_row;
let power_level = row.selected_power_level();
let old_power_level = member.power_level();
if old_power_level == power_level {
// Nothing to do.
return;
}
row.set_is_loading(true);
row.set_sensitive(false);
spawn!(clone!(@weak self as obj => async move {
let permissions = member.room().permissions();
// 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;
if is_muted && !confirm_mute_room_member(&member, &obj).await {
obj.update_room();
return;
}
// 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 && !confirm_set_room_member_power_level_same_as_own(&member, &obj).await {
obj.update_room();
return;
}
let user_id = member.user_id().clone();
if permissions
.set_user_power_level(user_id, power_level)
.await
.is_err()
{
toast!(obj, gettext("Failed to change the role"));
obj.update_room();
}
}));
}
/// Invite the user to the room.
#[template_callback]
fn invite_user(&self) {

24
src/components/user_page.ui

@ -108,29 +108,25 @@
<class name="boxed-list"/>
</style>
<child>
<object class="AdwActionRow" id="role_row">
<object class="AdwActionRow" id="membership_row">
<property name="selectable">False</property>
<property name="title" translatable="yes">Role</property>
<accessibility>
<relation name="described-by">role_group</relation>
<relation name="described-by">membership_label</relation>
</accessibility>
<child>
<object class="GtkBox" id="role_group">
<property name="accessible-role">group</property>
<property name="focusable">True</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="role_label">
<property name="ellipsize">end</property>
</object>
</child>
<child>
<object class="GtkLabel" id="power_level_label"/>
</child>
<object class="GtkLabel" id="membership_label">
<property name="ellipsize">end</property>
</object>
</child>
</object>
</child>
<child>
<object class="PowerLevelSelectionRow" id="power_level_row">
<property name="title" translatable="yes">Power Level</property>
<signal name="notify::selected-power-level" handler="set_power_level" swapped="yes" />
</object>
</child>
</object>
</child>
<child>

63
src/session/model/room/member.rs

@ -43,7 +43,7 @@ impl From<MembershipState> for Membership {
}
mod imp {
use std::cell::{Cell, OnceCell};
use std::cell::{Cell, OnceCell, RefCell};
use super::*;
@ -51,17 +51,21 @@ mod imp {
#[properties(wrapper_type = super::Member)]
pub struct Member {
/// The room of the member.
#[property(get, construct_only)]
#[property(get, set = Self::set_room, construct_only)]
pub room: OnceCell<Room>,
/// The power level of the member.
#[property(get, minimum = POWER_LEVEL_MIN, maximum = POWER_LEVEL_MAX)]
pub power_level: Cell<PowerLevel>,
/// The role of the member.
#[property(get, builder(MemberRole::default()))]
pub role: Cell<MemberRole>,
/// This member's membership state.
#[property(get, builder(Membership::default()))]
pub membership: Cell<Membership>,
/// The timestamp of the latest activity of this member.
#[property(get, set = Self::set_latest_activity, explicit_notify)]
pub latest_activity: Cell<u64>,
power_level_handlers: RefCell<Vec<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
@ -72,7 +76,15 @@ mod imp {
}
#[glib::derived_properties]
impl ObjectImpl for Member {}
impl ObjectImpl for Member {
fn dispose(&self) {
if let Some(room) = self.room.get() {
for handler in self.power_level_handlers.take() {
room.permissions().disconnect(handler);
}
}
}
}
impl PillSourceImpl for Member {
fn identifier(&self) -> String {
@ -81,6 +93,26 @@ mod imp {
}
impl Member {
/// Set the room of the member.
fn set_room(&self, room: Room) {
let obj = self.obj();
let default_pl_handler = room.permissions().connect_default_power_level_notify(
clone!(@weak obj => move |_| {
obj.update_role();
}),
);
let mute_pl_handler =
room.permissions()
.connect_mute_power_level_notify(clone!(@weak obj => move |_| {
obj.update_role();
}));
self.power_level_handlers
.replace(vec![default_pl_handler, mute_pl_handler]);
self.room.set(room).unwrap();
}
/// Set the timestamp of the latest activity of this member.
fn set_latest_activity(&self, activity: u64) {
if self.latest_activity.get() >= activity {
@ -115,24 +147,22 @@ impl Member {
if self.power_level() == power_level {
return;
}
self.imp().power_level.replace(power_level);
self.notify_power_level();
}
pub fn role(&self) -> MemberRole {
self.power_level().into()
self.imp().power_level.set(power_level);
self.update_role();
self.notify_power_level();
}
pub fn is_admin(&self) -> bool {
self.role().is_admin()
}
/// Update the role of the member.
fn update_role(&self) {
let role = self.room().permissions().role(self.power_level());
pub fn is_mod(&self) -> bool {
self.role().is_mod()
}
if self.role() == role {
return;
}
pub fn is_peasant(&self) -> bool {
self.role().is_peasant()
self.imp().role.set(role);
self.notify_role();
}
/// Set this member's membership state.
@ -140,6 +170,7 @@ impl Member {
if self.membership() == membership {
return;
}
let imp = self.imp();
imp.membership.replace(membership);
self.notify_membership();

55
src/session/model/room/member_role.rs

@ -1,55 +0,0 @@
use std::fmt;
use gettextrs::gettext;
use gtk::glib;
use super::PowerLevel;
/// Role of a room member, like admin or moderator.
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[repr(u32)]
#[enum_type(name = "MemberRole")]
pub enum MemberRole {
/// An administrator.
Admin = 1,
/// A moderator.
Mod = 2,
/// A regular room member.
Peasant = 0,
}
impl MemberRole {
pub fn is_admin(&self) -> bool {
matches!(*self, Self::Admin)
}
pub fn is_mod(&self) -> bool {
matches!(*self, Self::Mod)
}
pub fn is_peasant(&self) -> bool {
matches!(*self, Self::Peasant)
}
}
impl From<PowerLevel> for MemberRole {
fn from(power_level: PowerLevel) -> Self {
if (100..).contains(&power_level) {
Self::Admin
} else if (50..100).contains(&power_level) {
Self::Mod
} else {
Self::Peasant
}
}
}
impl fmt::Display for MemberRole {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Admin => write!(f, "{}", gettext("Admin")),
Self::Mod => write!(f, "{}", gettext("Moderator")),
_ => write!(f, "{}", gettext("Normal user")),
}
}
}

4
src/session/model/room/mod.rs

@ -40,7 +40,6 @@ mod highlight_flags;
mod join_rule;
mod member;
mod member_list;
mod member_role;
mod permissions;
mod room_type;
mod timeline;
@ -53,8 +52,7 @@ pub use self::{
join_rule::{JoinRule, JoinRuleValue},
member::{Member, Membership},
member_list::MemberList,
member_role::MemberRole,
permissions::{Permissions, PowerLevel, POWER_LEVEL_MAX, POWER_LEVEL_MIN},
permissions::*,
room_type::RoomType,
timeline::*,
typing_list::TypingList,

170
src/session/model/room/permissions.rs

@ -1,3 +1,6 @@
use std::fmt;
use gettextrs::gettext;
use gtk::{
glib,
glib::{clone, closure_local},
@ -14,7 +17,7 @@ use ruma::{
},
MessageLikeEventType, StateEventType, SyncStateEvent,
},
UserId,
Int, OwnedUserId, UserId,
};
use tracing::error;
@ -25,9 +28,56 @@ use crate::{prelude::*, spawn, spawn_tokio};
///
/// Is usually in the range (0..=100), but can be any JS integer.
pub type PowerLevel = i64;
// Same value as MAX_SAFE_INT from js_int.
pub const POWER_LEVEL_MAX: i64 = 0x001F_FFFF_FFFF_FFFF;
pub const POWER_LEVEL_MIN: i64 = -POWER_LEVEL_MAX;
/// 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;
/// The minimum power level to have the role of Administrator, according to the
/// Matrix specification.
pub const POWER_LEVEL_ADMIN: PowerLevel = 100;
/// The minimum power level to have the role of Moderator, according to the
/// Matrix specification.
pub const POWER_LEVEL_MOD: PowerLevel = 50;
/// Role of a room member, like admin or moderator.
#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[enum_type(name = "MemberRole")]
pub enum MemberRole {
/// A room member with the default power level.
#[default]
Default,
/// A room member with a non-default power level, but lower than and a
/// moderator.
Custom,
/// A moderator.
Moderator,
/// An administrator.
Administrator,
/// A room member that cannot send messages.
Muted,
}
impl fmt::Display for MemberRole {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
// Translators: As in 'Default power level'.
Self::Default => write!(f, "{}", gettext("Default")),
// Translators: As in, 'Custom power level'.
Self::Custom => write!(f, "{}", gettext("Custom")),
Self::Moderator => write!(f, "{}", gettext("Moderator")),
Self::Administrator => write!(f, "{}", gettext("Admin")),
// Translators: As in 'Muted room member', a member that cannot send messages.
Self::Muted => write!(f, "{}", gettext("Muted")),
}
}
}
mod imp {
use std::cell::{Cell, OnceCell, RefCell};
@ -48,6 +98,15 @@ mod imp {
power_levels_drop_guard: OnceCell<EventHandlerDropGuard>,
/// Whether our own member is joined.
pub is_joined: Cell<bool>,
/// The power level of our own member.
#[property(get)]
pub own_power_level: Cell<PowerLevel>,
/// The default power level for members.
#[property(get)]
pub default_power_level: Cell<PowerLevel>,
/// The power level to mute members.
#[property(get)]
pub mute_power_level: Cell<PowerLevel>,
/// Whether our own member can change the room's avatar.
#[property(get)]
pub can_change_avatar: Cell<bool>,
@ -81,6 +140,9 @@ mod imp {
power_levels: RefCell::new(RoomPowerLevelsEventContent::default().into()),
power_levels_drop_guard: Default::default(),
is_joined: Default::default(),
own_power_level: Default::default(),
default_power_level: Default::default(),
mute_power_level: Default::default(),
can_change_avatar: Default::default(),
can_change_name: Default::default(),
can_change_topic: Default::default(),
@ -213,6 +275,9 @@ mod imp {
/// Trigger updates when the permissions changed.
fn permissions_changed(&self) {
self.update_own_power_level();
self.update_default_power_level();
self.update_mute_power_level();
self.update_can_change_avatar();
self.update_can_change_name();
self.update_can_change_topic();
@ -224,6 +289,58 @@ mod imp {
self.obj().emit_by_name::<()>("changed", &[]);
}
/// Update the power level of our own member.
fn update_own_power_level(&self) {
let Some(room) = self.room.upgrade() else {
return;
};
let own_member = room.own_member();
let power_level = self
.power_levels
.borrow()
.for_user(own_member.user_id())
.into();
if self.own_power_level.get() == power_level {
return;
}
self.own_power_level.set(power_level);
self.obj().notify_own_power_level();
}
/// Update the default power level for members.
fn update_default_power_level(&self) {
let power_level = self.power_levels.borrow().users_default.into();
if self.default_power_level.get() == power_level {
return;
}
self.default_power_level.set(power_level);
self.obj().notify_default_power_level();
}
/// Update the power level to mute members.
fn update_mute_power_level(&self) {
// To mute user they must not have enough power to send messages.
let power_levels = self.power_levels.borrow();
let message_power_level = power_levels
.events
.get(&MessageLikeEventType::RoomMessage.into())
.copied()
.unwrap_or(power_levels.events_default);
let power_level = (-1).min(message_power_level.into());
if self.mute_power_level.get() == power_level {
return;
}
self.mute_power_level.set(power_level);
self.obj().notify_mute_power_level();
}
/// Whether our own member is allowed to do the given action.
pub(super) fn is_allowed_to(&self, room_action: PowerLevelAction) -> bool {
if !self.is_joined.get() {
@ -364,6 +481,24 @@ impl Permissions {
imp.init_power_levels().await;
}
/// The current [`MemberRole`] for the given power level.
pub fn role(&self, power_level: PowerLevel) -> MemberRole {
if power_level >= POWER_LEVEL_ADMIN {
MemberRole::Administrator
} else if power_level >= POWER_LEVEL_MOD {
MemberRole::Moderator
} else if power_level == self.default_power_level() {
MemberRole::Default
} else if power_level < self.default_power_level() && power_level <= self.mute_power_level()
{
// Only set role as muted for members below default, to avoid visual noise in
// rooms where muted is the default.
MemberRole::Muted
} else {
MemberRole::Custom
}
}
/// Whether our own member is allowed to do the given action.
pub fn is_allowed_to(&self, room_action: PowerLevelAction) -> bool {
self.imp().is_allowed_to(room_action)
@ -395,6 +530,33 @@ impl Permissions {
power_levels.user_can_do_to_user(own_user_id, user_id, action)
}
/// Set the power level of the room member with the given user ID.
pub async fn set_user_power_level(
&self,
user_id: OwnedUserId,
power_level: PowerLevel,
) -> Result<(), ()> {
let Some(room) = self.room() else {
return Err(());
};
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
});
match handle.await.unwrap() {
Ok(_) => Ok(()),
Err(error) => {
error!("Failed to set user power level: {error}");
Err(())
}
}
}
/// Connect to the signal emitted when the permissions changed.
pub fn connect_changed<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_closure(

4
src/session/view/content/room_details/members_page/members_list_view/member_row.rs

@ -1,7 +1,7 @@
use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
use crate::{
components::{Avatar, PowerLevelBadge},
components::{Avatar, RoleBadge},
session::model::Member,
};
@ -31,7 +31,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
Avatar::static_type();
PowerLevelBadge::static_type();
RoleBadge::static_type();
Self::bind_template(klass);
}

9
src/session/view/content/room_details/members_page/members_list_view/member_row.ui

@ -24,7 +24,7 @@
</style>
<child>
<object class="GtkBox">
<property name="spacing">3</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="title">
<property name="halign">start</property>
@ -54,9 +54,10 @@
</object>
</child>
<child>
<object class="PowerLevelBadge">
<binding name="power-level">
<lookup name="power-level" type="Member">
<object class="RoleBadge" id="role_badge">
<property name="visible" bind-source="role_badge" bind-property="is-default-role" bind-flags="sync-create | invert-boolean"/>
<binding name="role">
<lookup name="role" type="Member">
<lookup name="member">ContentMemberRow</lookup>
</lookup>
</binding>

1
src/ui-resources.gresource.xml

@ -28,6 +28,7 @@
<file compressed="true" preprocess="xml-stripblanks">components/rows/copyable_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/rows/entry_add_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/rows/loading_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/rows/power_level_selection_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/rows/removable_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/rows/substring_entry_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/rows/switch_loading_row.ui</file>

65
src/utils/message_dialog.rs

@ -291,3 +291,68 @@ pub struct ConfirmRoomMemberDestructiveActionResponse {
/// Whether we can remove the events.
pub remove_events: bool,
}
/// Show a dialog to confirm muting a room member.
pub async fn confirm_mute_room_member(member: &Member, parent: &impl IsA<gtk::Widget>) -> bool {
let heading = gettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Mute {user}?",
&[("user", &member.display_name())],
);
let body = gettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Are you sure you want to mute {user_id}? They will not be able to send new messages.",
&[("user_id", member.user_id().as_str())],
);
// Ask for confirmation.
let confirm_dialog = adw::AlertDialog::builder()
.default_response("cancel")
.heading(heading)
.body(body)
.build();
confirm_dialog.add_responses(&[
("cancel", &gettext("Cancel")),
// Translators: In this string, 'Mute' is a verb, as in 'Mute room member'.
("mute", &gettext("Mute")),
]);
confirm_dialog.set_response_appearance("mute", adw::ResponseAppearance::Destructive);
confirm_dialog.choose_future(parent).await == "mute"
}
/// Show a dialog to confirm setting the power level of a room member with the
/// same value as our own.
pub async fn confirm_set_room_member_power_level_same_as_own(
member: &Member,
parent: &impl IsA<gtk::Widget>,
) -> bool {
let heading = gettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Promote {user}?",
&[("user", &member.display_name())],
);
let body = gettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"If you promote {user_id} to the same level as yours, you will not be able to demote them in the future.",
&[("user_id", member.user_id().as_str())],
);
// Ask for confirmation.
let confirm_dialog = adw::AlertDialog::builder()
.default_response("cancel")
.heading(heading)
.body(body)
.build();
confirm_dialog.add_responses(&[
("cancel", &gettext("Cancel")),
("promote", &gettext("Promote")),
]);
confirm_dialog.set_response_appearance("promote", adw::ResponseAppearance::Destructive);
confirm_dialog.choose_future(parent).await == "promote"
}

Loading…
Cancel
Save