Browse Source

room-details: Refactor and clean up

af/unable-to-decryt-styling
Kévin Commaille 12 months ago
parent
commit
da8fb3b112
No known key found for this signature in database
GPG Key ID: C971D9DBC9D678D
  1. 367
      src/session/view/content/room_details/addresses_subpage/completion_popover.rs
  2. 691
      src/session/view/content/room_details/addresses_subpage/mod.rs
  3. 26
      src/session/view/content/room_details/addresses_subpage/public_address.rs
  4. 6
      src/session/view/content/room_details/edit_details_subpage.rs
  5. 1225
      src/session/view/content/room_details/general_page.rs
  6. 23
      src/session/view/content/room_details/history_viewer/audio.rs
  7. 54
      src/session/view/content/room_details/history_viewer/audio_row.rs
  8. 54
      src/session/view/content/room_details/history_viewer/event.rs
  9. 23
      src/session/view/content/room_details/history_viewer/file.rs
  10. 137
      src/session/view/content/room_details/history_viewer/file_row.rs
  11. 2
      src/session/view/content/room_details/history_viewer/mod.rs
  12. 193
      src/session/view/content/room_details/history_viewer/timeline.rs
  13. 59
      src/session/view/content/room_details/history_viewer/visual_media.rs
  14. 41
      src/session/view/content/room_details/history_viewer/visual_media_item.rs
  15. 8
      src/session/view/content/room_details/invite_subpage/item.rs
  16. 520
      src/session/view/content/room_details/invite_subpage/list.rs
  17. 204
      src/session/view/content/room_details/invite_subpage/mod.rs
  18. 8
      src/session/view/content/room_details/invite_subpage/row.rs
  19. 4
      src/session/view/content/room_details/mod.rs
  20. 84
      src/session/view/content/room_details/permissions/add_members_subpage.rs
  21. 12
      src/session/view/content/room_details/permissions/member_power_level.rs
  22. 2
      src/session/view/content/room_details/permissions/mod.rs
  23. 575
      src/session/view/content/room_details/permissions/permissions_subpage.rs
  24. 10
      src/session/view/content/room_details/permissions/privileged_members.rs
  25. 4
      src/session/view/content/room_details/permissions/select_member_row.rs

367
src/session/view/content/room_details/addresses_subpage/completion_popover.rs

@ -1,5 +1,5 @@
use adw::prelude::*;
use gtk::{gdk, gio, glib, glib::clone, pango, subclass::prelude::*, CompositeTemplate};
use adw::{prelude::*, subclass::prelude::*};
use gtk::{gdk, gio, glib, glib::clone, pango, CompositeTemplate};
use tracing::error;
use crate::utils::BoundObjectWeakRef;
@ -18,10 +18,10 @@ mod imp {
#[properties(wrapper_type = super::CompletionPopover)]
pub struct CompletionPopover {
#[template_child]
pub list: TemplateChild<gtk::ListBox>,
list: TemplateChild<gtk::ListBox>,
/// The parent entry to autocomplete.
#[property(get, set = Self::set_entry, explicit_notify, nullable)]
pub entry: BoundObjectWeakRef<gtk::Editable>,
entry: BoundObjectWeakRef<gtk::Editable>,
/// The key controller added to the parent entry.
entry_controller: RefCell<Option<gtk::EventControllerKey>>,
entry_binding: RefCell<Option<glib::Binding>>,
@ -29,13 +29,13 @@ mod imp {
///
/// Only supports `GtkStringObject` items.
#[property(get, set = Self::set_model, explicit_notify, nullable)]
pub model: RefCell<Option<gio::ListModel>>,
model: RefCell<Option<gio::ListModel>>,
/// The string filter.
#[property(get)]
pub filter: gtk::StringFilter,
filter: gtk::StringFilter,
/// The filtered list model.
#[property(get)]
pub filtered_list: gtk::FilterListModel,
filtered_list: gtk::FilterListModel,
}
#[glib::object_subclass]
@ -46,7 +46,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
Self::bind_template_callbacks(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
@ -58,17 +58,16 @@ mod imp {
impl ObjectImpl for CompletionPopover {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
self.filter
.set_expression(Some(gtk::StringObject::this_expression("string")));
self.filtered_list.set_filter(Some(&self.filter));
self.filtered_list.connect_items_changed(clone!(
#[weak]
obj,
#[weak(rename_to = imp)]
self,
move |_, _, _, _| {
obj.update_completion();
imp.update_completion();
}
));
@ -104,6 +103,7 @@ mod imp {
impl WidgetImpl for CompletionPopover {}
impl PopoverImpl for CompletionPopover {}
#[gtk::template_callbacks]
impl CompletionPopover {
/// Set the parent entry to autocomplete.
fn set_entry(&self, entry: Option<&gtk::Editable>) {
@ -129,53 +129,11 @@ mod imp {
if let Some(entry) = entry {
let key_events = gtk::EventControllerKey::new();
key_events.connect_key_pressed(clone!(
#[weak]
obj,
#[weak(rename_to = imp)]
self,
#[upgrade_or]
glib::Propagation::Proceed,
move |_, key, _, modifier| {
if modifier.is_empty() {
if obj.is_visible() {
let imp = obj.imp();
if matches!(
key,
gdk::Key::Return | gdk::Key::KP_Enter | gdk::Key::ISO_Enter
) {
// Activate completion.
obj.activate_selected_row();
return glib::Propagation::Stop;
} else if matches!(key, gdk::Key::Up | gdk::Key::KP_Up) {
// Move up, if possible.
let idx = obj.selected_row_index().unwrap_or_default();
if idx > 0 {
obj.select_row_at_index(Some(idx - 1));
}
return glib::Propagation::Stop;
} else if matches!(key, gdk::Key::Down | gdk::Key::KP_Down) {
// Move down, if possible.
let new_idx = if let Some(idx) = obj.selected_row_index() {
idx + 1
} else {
0
};
let max = imp.filtered_list.n_items() as usize;
if new_idx < max {
obj.select_row_at_index(Some(new_idx));
}
return glib::Propagation::Stop;
} else if matches!(key, gdk::Key::Escape) {
// Close.
obj.popdown();
return glib::Propagation::Stop;
}
} else if matches!(key, gdk::Key::Tab) {
obj.update_completion();
return glib::Propagation::Stop;
}
}
glib::Propagation::Proceed
}
move |_, key, _, modifier| imp.key_pressed(key, modifier)
));
entry.add_controller(key_events.clone());
@ -188,18 +146,18 @@ mod imp {
self.entry_binding.replace(Some(search_binding));
let changed_handler = entry.connect_changed(clone!(
#[weak]
obj,
#[weak(rename_to = imp)]
self,
move |_| {
obj.update_completion();
imp.update_completion();
}
));
let state_flags_handler = entry.connect_state_flags_changed(clone!(
#[weak]
obj,
#[weak(rename_to = imp)]
self,
move |_, _| {
obj.update_completion();
imp.update_completion();
}
));
@ -208,7 +166,7 @@ mod imp {
.set(entry, vec![changed_handler, state_flags_handler]);
}
self.obj().notify_entry();
obj.notify_entry();
}
/// Set the list model to use for completion.
@ -222,152 +180,211 @@ mod imp {
self.model.replace(model);
self.obj().notify_model();
}
}
}
glib::wrapper! {
/// A popover to auto-complete strings for a `gtk::Editable`.
pub struct CompletionPopover(ObjectSubclass<imp::CompletionPopover>)
@extends gtk::Widget, gtk::Popover, @implements gtk::Accessible;
}
/// Update completion.
fn update_completion(&self) {
let Some(entry) = self.entry.obj() else {
return;
};
let obj = self.obj();
#[gtk::template_callbacks]
impl CompletionPopover {
pub fn new() -> Self {
glib::Object::new()
}
let n_items = self.filtered_list.n_items();
/// Update completion.
fn update_completion(&self) {
let Some(entry) = self.entry() else {
return;
};
// Always hide the popover if it's empty.
if n_items == 0 {
if obj.is_visible() {
obj.popdown();
}
let imp = self.imp();
let n_items = imp.filtered_list.n_items();
return;
}
// Always hide the popover if it's empty.
if n_items == 0 {
if self.is_visible() {
self.popdown();
// Always hide the popover if it has a single item that is exactly the text of
// the entry.
if n_items == 1 {
if let Some(item) = self
.filtered_list
.item(0)
.and_downcast::<gtk::StringObject>()
{
if item.string() == entry.text() {
if obj.is_visible() {
obj.popdown();
}
return;
}
}
}
return;
// Only show the popover if the entry is focused.
let entry_has_focus = entry.state_flags().contains(gtk::StateFlags::FOCUS_WITHIN);
if entry_has_focus {
if !obj.is_visible() {
obj.popup();
}
} else if obj.is_visible() {
obj.popdown();
}
}
// Always hide the popover if it has a single item that is exactly the text of
// the entry.
if n_items == 1 {
if let Some(item) = imp
.filtered_list
.item(0)
.and_downcast::<gtk::StringObject>()
{
if item.string() == entry.text() {
if self.is_visible() {
self.popdown();
}
/// The index of the selected row.
fn selected_row_index(&self) -> Option<usize> {
let selected_row = self.list.selected_row()?;
let n_rows = i32::try_from(self.filtered_list.n_items()).unwrap_or(i32::MAX);
for idx in 0..n_rows {
let Some(row) = self.list.row_at_index(idx) else {
break;
};
return;
if row == selected_row {
return Some(idx.try_into().unwrap_or_default());
}
}
None
}
// Only show the popover if the entry is focused.
let entry_has_focus = entry.state_flags().contains(gtk::StateFlags::FOCUS_WITHIN);
if entry_has_focus {
if !self.is_visible() {
self.popup();
/// Select the row at the given index.
fn select_row_at_index(&self, idx: Option<usize>) {
if self.selected_row_index() == idx
|| idx >= Some(self.filtered_list.n_items() as usize)
{
return;
}
} else if self.is_visible() {
self.popdown();
let row =
idx.and_then(|idx| self.list.row_at_index(idx.try_into().unwrap_or(i32::MAX)));
self.list.select_row(row.as_ref());
}
}
fn selected_row_index(&self) -> Option<usize> {
let imp = self.imp();
/// The text of the selected row, if any.
fn selected_text(&self) -> Option<glib::GString> {
Some(
self.list
.selected_row()?
.child()?
.downcast_ref::<gtk::Label>()?
.label(),
)
}
let selected_text = self.selected_text()?;
/// Activate the selected row.
///
/// Returns `true` if the row was activated.
pub(super) fn activate_selected_row(&self) -> bool {
if !self.obj().is_visible() {
return false;
}
let Some(entry) = self.entry.obj() else {
return false;
};
imp.filtered_list.iter::<glib::Object>().position(|o| {
o.ok()
.and_downcast::<gtk::StringObject>()
.is_some_and(|o| o.string() == selected_text)
})
}
let Some(selected_text) = self.selected_text() else {
return false;
};
if selected_text == entry.text() {
// Activating the row would have no effect.
return false;
}
fn select_row_at_index(&self, idx: Option<usize>) {
let imp = self.imp();
let Some(row) = self.list.selected_row() else {
return false;
};
if self.selected_row_index() == idx || idx >= Some(imp.filtered_list.n_items() as usize) {
return;
row.activate();
true
}
let imp = self.imp();
/// Handle a key being pressed in the entry.
fn key_pressed(&self, key: gdk::Key, modifier: gdk::ModifierType) -> glib::Propagation {
if !modifier.is_empty() {
return glib::Propagation::Proceed;
}
if let Some(row) =
idx.and_then(|idx| imp.list.row_at_index(idx.try_into().unwrap_or(i32::MAX)))
{
imp.list.select_row(Some(&row));
} else {
imp.list.select_row(None::<&gtk::ListBoxRow>);
}
}
let obj = self.obj();
/// The text of the selected row, if any.
pub fn selected_text(&self) -> Option<glib::GString> {
Some(
self.imp()
.list
.selected_row()?
.child()?
.downcast_ref::<gtk::Label>()?
.label(),
)
}
if obj.is_visible() {
if matches!(key, gdk::Key::Tab) {
self.update_completion();
return glib::Propagation::Stop;
}
/// Activate the selected row.
///
/// Returns `true` if the row was activated.
pub fn activate_selected_row(&self) -> bool {
if !self.is_visible() {
return false;
}
let Some(entry) = self.entry() else {
return false;
};
return glib::Propagation::Proceed;
}
let Some(selected_text) = self.selected_text() else {
return false;
};
if matches!(
key,
gdk::Key::Return | gdk::Key::KP_Enter | gdk::Key::ISO_Enter
) {
// Activate completion.
self.activate_selected_row();
return glib::Propagation::Stop;
} else if matches!(key, gdk::Key::Up | gdk::Key::KP_Up) {
// Move up, if possible.
let idx = self.selected_row_index().unwrap_or_default();
if idx > 0 {
self.select_row_at_index(Some(idx - 1));
}
return glib::Propagation::Stop;
} else if matches!(key, gdk::Key::Down | gdk::Key::KP_Down) {
// Move down, if possible.
let new_idx = if let Some(idx) = self.selected_row_index() {
idx + 1
} else {
0
};
let max = self.filtered_list.n_items() as usize;
if new_idx < max {
self.select_row_at_index(Some(new_idx));
}
return glib::Propagation::Stop;
} else if matches!(key, gdk::Key::Escape) {
// Close.
obj.popdown();
return glib::Propagation::Stop;
}
if selected_text == entry.text() {
// Activating the row would have no effect.
return false;
glib::Propagation::Proceed
}
let Some(row) = self.imp().list.selected_row() else {
return false;
};
/// Handle a row being activated.
#[template_callback]
fn row_activated(&self, row: &gtk::ListBoxRow) {
let Some(label) = row.child().and_downcast::<gtk::Label>() else {
return;
};
let Some(entry) = self.entry.obj() else {
return;
};
row.activate();
true
entry.set_text(&label.label());
self.obj().popdown();
entry.grab_focus();
}
}
}
/// Handle a row being activated.
#[template_callback]
fn row_activated(&self, row: &gtk::ListBoxRow) {
let Some(label) = row.child().and_downcast::<gtk::Label>() else {
return;
};
let Some(entry) = self.entry() else {
return;
};
glib::wrapper! {
/// A popover to auto-complete strings for a `gtk::Editable`.
pub struct CompletionPopover(ObjectSubclass<imp::CompletionPopover>)
@extends gtk::Widget, gtk::Popover, @implements gtk::Accessible;
}
entry.set_text(&label.label());
impl CompletionPopover {
pub fn new() -> Self {
glib::Object::new()
}
self.popdown();
entry.grab_focus();
/// Activate the selected row.
///
/// Returns `true` if the row was activated.
pub(crate) fn activate_selected_row(&self) -> bool {
self.imp().activate_selected_row()
}
}

691
src/session/view/content/room_details/addresses_subpage/mod.rs

@ -14,7 +14,7 @@ use crate::{
prelude::*,
session::model::{AddAltAliasError, RegisterLocalAliasError, Room},
spawn, toast,
utils::DummyObject,
utils::{DummyObject, SingleItemListModel},
};
mod imp {
@ -34,32 +34,32 @@ mod imp {
#[properties(wrapper_type = super::AddressesSubpage)]
pub struct AddressesSubpage {
#[template_child]
pub public_addresses_list: TemplateChild<gtk::ListBox>,
public_addresses_list: TemplateChild<gtk::ListBox>,
#[template_child]
pub public_addresses_error_revealer: TemplateChild<gtk::Revealer>,
public_addresses_error_revealer: TemplateChild<gtk::Revealer>,
#[template_child]
pub public_addresses_error: TemplateChild<gtk::Label>,
public_addresses_error: TemplateChild<gtk::Label>,
#[template_child]
pub local_addresses_group: TemplateChild<adw::PreferencesGroup>,
local_addresses_group: TemplateChild<adw::PreferencesGroup>,
#[template_child]
pub local_addresses_list: TemplateChild<gtk::ListBox>,
local_addresses_list: TemplateChild<gtk::ListBox>,
#[template_child]
pub local_addresses_error_revealer: TemplateChild<gtk::Revealer>,
local_addresses_error_revealer: TemplateChild<gtk::Revealer>,
#[template_child]
pub local_addresses_error: TemplateChild<gtk::Label>,
local_addresses_error: TemplateChild<gtk::Label>,
#[template_child]
pub public_addresses_add_row: TemplateChild<EntryAddRow>,
public_addresses_add_row: TemplateChild<EntryAddRow>,
#[template_child]
pub local_addresses_add_row: TemplateChild<SubstringEntryRow>,
local_addresses_add_row: TemplateChild<SubstringEntryRow>,
/// The room users will be invited to.
#[property(get, set = Self::set_room, construct_only)]
pub room: glib::WeakRef<Room>,
room: glib::WeakRef<Room>,
/// The full list of public addresses.
pub public_addresses: OnceCell<gio::ListStore>,
public_addresses: OnceCell<gio::ListStore>,
/// The full list of local addresses.
pub local_addresses: gtk::StringList,
local_addresses: gtk::StringList,
aliases_changed_handler: RefCell<Option<glib::SignalHandlerId>>,
pub public_addresses_completion: CompletionPopover,
public_addresses_completion: CompletionPopover,
}
#[glib::object_subclass]
@ -70,7 +70,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
Self::bind_template_callbacks(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
@ -82,33 +82,31 @@ mod imp {
impl ObjectImpl for AddressesSubpage {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
let extra_items = gio::ListStore::new::<glib::Object>();
extra_items.append(&DummyObject::new("add"));
let add_item = SingleItemListModel::new(&DummyObject::new("add"));
// Public addresses.
let public_items = gio::ListStore::new::<glib::Object>();
public_items.append(self.public_addresses());
public_items.append(&extra_items);
public_items.append(&add_item);
let flattened_public_list = gtk::FlattenListModel::new(Some(public_items));
self.public_addresses_list.bind_model(
Some(&flattened_public_list),
clone!(
#[weak]
obj,
#[weak(rename_to = imp)]
self,
#[upgrade_or_else]
|| { adw::ActionRow::new().upcast() },
move |item| obj.create_public_address_row(item)
move |item| imp.create_public_address_row(item)
),
);
self.public_addresses_add_row.connect_changed(clone!(
#[weak]
obj,
#[weak(rename_to = imp)]
self,
move |_| {
obj.update_public_addresses_add_row();
imp.update_public_addresses_add_row();
}
));
@ -163,25 +161,25 @@ mod imp {
// Local addresses.
let local_items = gio::ListStore::new::<glib::Object>();
local_items.append(&self.local_addresses);
local_items.append(&extra_items);
local_items.append(&add_item);
let flattened_local_list = gtk::FlattenListModel::new(Some(local_items));
self.local_addresses_list.bind_model(
Some(&flattened_local_list),
clone!(
#[weak]
obj,
#[weak(rename_to = imp)]
self,
#[upgrade_or_else]
|| { adw::ActionRow::new().upcast() },
move |item| obj.create_local_address_row(item)
move |item| imp.create_local_address_row(item)
),
);
self.local_addresses_add_row.connect_changed(clone!(
#[weak]
obj,
#[weak(rename_to = imp)]
self,
move |_| {
obj.update_local_addresses_add_row();
imp.update_local_addresses_add_row();
}
));
}
@ -200,8 +198,9 @@ mod imp {
impl WidgetImpl for AddressesSubpage {}
impl NavigationPageImpl for AddressesSubpage {}
#[gtk::template_callbacks]
impl AddressesSubpage {
pub(super) fn public_addresses(&self) -> &gio::ListStore {
fn public_addresses(&self) -> &gio::ListStore {
self.public_addresses
.get_or_init(gio::ListStore::new::<PublicAddress>)
}
@ -334,7 +333,7 @@ mod imp {
}
/// Update the list of local addresses.
pub(super) async fn update_local_addresses(&self) {
async fn update_local_addresses(&self) {
let Some(room) = self.room.upgrade() else {
return;
};
@ -377,26 +376,14 @@ mod imp {
.splice(self.local_addresses.n_items(), 0, &new_aliases);
}
}
}
}
glib::wrapper! {
/// Subpage to invite new members to a room.
pub struct AddressesSubpage(ObjectSubclass<imp::AddressesSubpage>)
@extends gtk::Widget, gtk::Window, adw::NavigationPage, @implements gtk::Accessible;
}
#[gtk::template_callbacks]
impl AddressesSubpage {
pub fn new(room: &Room) -> Self {
glib::Object::builder().property("room", room).build()
}
/// Create a row for the given item in the public addresses section.
fn create_public_address_row(&self, item: &glib::Object) -> gtk::Widget {
let imp = self.imp();
/// Create a row for the given item in the public addresses section.
fn create_public_address_row(&self, item: &glib::Object) -> gtk::Widget {
let Some(address) = item.downcast_ref::<PublicAddress>() else {
// It can only be the dummy item to add a new alias.
return self.public_addresses_add_row.clone().upcast();
};
if let Some(address) = item.downcast_ref::<PublicAddress>() {
let alias = address.alias();
let row = RemovableRow::new();
row.set_title(alias.as_str());
@ -409,254 +396,246 @@ impl AddressesSubpage {
)));
address.connect_is_main_notify(clone!(
#[weak(rename_to = obj)]
#[weak(rename_to = imp)]
self,
#[weak]
row,
move |address| {
obj.update_public_row_is_main(&row, address.is_main());
imp.update_public_row_is_main(&row, address.is_main());
}
));
self.update_public_row_is_main(&row, address.is_main());
row.connect_remove(clone!(
#[weak(rename_to = obj)]
#[weak(rename_to = imp)]
self,
move |row| {
spawn!(clone!(
#[weak]
row,
async move {
obj.remove_public_address(&row).await;
imp.remove_public_address(&row).await;
}
));
}
));
row.upcast()
} else {
// It can only be the dummy item to add a new alias.
imp.public_addresses_add_row.clone().upcast()
}
}
/// Update the given row for whether the address it presents is the main
/// address or not.
fn update_public_row_is_main(&self, row: &RemovableRow, is_main: bool) {
if is_main && !public_row_is_main(row) {
let label = gtk::Label::builder()
.label(gettext("Main Address"))
.ellipsize(pango::EllipsizeMode::End)
.build();
let image = gtk::Image::builder()
.icon_name("checkmark-symbolic")
.accessible_role(gtk::AccessibleRole::Presentation)
.build();
let main_box = gtk::Box::builder()
.spacing(6)
.css_classes(["public-address-tag"])
.valign(gtk::Align::Center)
.build();
main_box.append(&image);
main_box.append(&label);
row.update_relation(&[gtk::accessible::Relation::DescribedBy(
&[label.upcast_ref()],
)]);
row.set_extra_suffix(Some(main_box));
} else if !is_main && !row.extra_suffix().is_some_and(|w| w.is::<LoadingButton>()) {
let button = LoadingButton::new();
button.set_content_icon_name("checkmark-symbolic");
button.add_css_class("flat");
button.set_tooltip_text(Some(&gettext("Set as main address")));
button.set_valign(gtk::Align::Center);
let accessible_label = gettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Set “{address}” as main address",
&[("address", &row.title())],
);
button.update_property(&[gtk::accessible::Property::Label(&accessible_label)]);
button.connect_clicked(clone!(
#[weak(rename_to = obj)]
self,
#[weak]
row,
move |_| {
spawn!(async move {
obj.set_main_public_address(&row).await;
});
}
));
/// Update the given row for whether the address it presents is the main
/// address or not.
fn update_public_row_is_main(&self, row: &RemovableRow, is_main: bool) {
if is_main && !public_row_is_main(row) {
let label = gtk::Label::builder()
.label(gettext("Main Address"))
.ellipsize(pango::EllipsizeMode::End)
.build();
let image = gtk::Image::builder()
.icon_name("checkmark-symbolic")
.accessible_role(gtk::AccessibleRole::Presentation)
.build();
let main_box = gtk::Box::builder()
.spacing(6)
.css_classes(["public-address-tag"])
.valign(gtk::Align::Center)
.build();
main_box.append(&image);
main_box.append(&label);
row.update_relation(&[gtk::accessible::Relation::DescribedBy(&[
label.upcast_ref()
])]);
row.set_extra_suffix(Some(main_box));
} else if !is_main && !row.extra_suffix().is_some_and(|w| w.is::<LoadingButton>()) {
let button = LoadingButton::new();
button.set_content_icon_name("checkmark-symbolic");
button.add_css_class("flat");
button.set_tooltip_text(Some(&gettext("Set as main address")));
button.set_valign(gtk::Align::Center);
let accessible_label = gettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Set “{address}” as main address",
&[("address", &row.title())],
);
button.update_property(&[gtk::accessible::Property::Label(&accessible_label)]);
button.connect_clicked(clone!(
#[weak(rename_to = imp)]
self,
#[weak]
row,
move |_| {
spawn!(async move {
imp.set_main_public_address(&row).await;
});
}
));
row.set_extra_suffix(Some(button));
row.set_extra_suffix(Some(button));
}
}
}
/// Remove the public address from the given row.
async fn remove_public_address(&self, row: &RemovableRow) {
let Some(room) = self.room() else {
return;
};
let Ok(alias) = RoomAliasId::parse(row.title()) else {
error!("Cannot remove address with invalid alias");
return;
};
let imp = self.imp();
let aliases = room.aliases();
imp.public_addresses_list.set_sensitive(false);
row.set_is_loading(true);
let result = if public_row_is_main(row) {
aliases.remove_canonical_alias(&alias).await
} else {
aliases.remove_alt_alias(&alias).await
};
if result.is_err() {
toast!(self, gettext("Could not remove public address"));
imp.public_addresses_list.set_sensitive(true);
row.set_is_loading(false);
}
}
/// Remove the public address from the given row.
async fn remove_public_address(&self, row: &RemovableRow) {
let Some(room) = self.room.upgrade() else {
return;
};
let Ok(alias) = RoomAliasId::parse(row.title()) else {
error!("Cannot remove address with invalid alias");
return;
};
let aliases = room.aliases();
self.public_addresses_list.set_sensitive(false);
row.set_is_loading(true);
let result = if public_row_is_main(row) {
aliases.remove_canonical_alias(&alias).await
} else {
aliases.remove_alt_alias(&alias).await
};
/// Set the address from the given row as the main public address.
async fn set_main_public_address(&self, row: &RemovableRow) {
let Some(room) = self.room() else {
return;
};
let Some(button) = row.extra_suffix().and_downcast::<LoadingButton>() else {
return;
};
let Ok(alias) = RoomAliasId::parse(row.title()) else {
error!("Cannot set main public address with invalid alias");
return;
};
let imp = self.imp();
let aliases = room.aliases();
imp.public_addresses_list.set_sensitive(false);
button.set_is_loading(true);
if aliases.set_canonical_alias(alias).await.is_err() {
toast!(self, gettext("Could not set main public address"));
imp.public_addresses_list.set_sensitive(true);
button.set_is_loading(false);
if result.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not remove public address"));
self.public_addresses_list.set_sensitive(true);
row.set_is_loading(false);
}
}
}
/// Update the public addresses add row for the current state.
fn update_public_addresses_add_row(&self) {
self.imp()
.public_addresses_add_row
.set_inhibit_add(!self.can_add_public_address());
}
/// Set the address from the given row as the main public address.
async fn set_main_public_address(&self, row: &RemovableRow) {
let Some(room) = self.room.upgrade() else {
return;
};
let Some(button) = row.extra_suffix().and_downcast::<LoadingButton>() else {
return;
};
let Ok(alias) = RoomAliasId::parse(row.title()) else {
error!("Cannot set main public address with invalid alias");
return;
};
let aliases = room.aliases();
self.public_addresses_list.set_sensitive(false);
button.set_is_loading(true);
/// Activate the auto-completion of the public addresses add row.
#[template_callback]
async fn handle_public_addresses_add_row_activated(&self) {
if !self
.imp()
.public_addresses_completion
.activate_selected_row()
{
self.add_public_address().await;
if aliases.set_canonical_alias(alias).await.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not set main public address"));
self.public_addresses_list.set_sensitive(true);
button.set_is_loading(false);
}
}
}
/// Add a an address to the public list.
#[template_callback]
async fn add_public_address(&self) {
if !self.can_add_public_address() {
return;
/// Update the public addresses add row for the current state.
fn update_public_addresses_add_row(&self) {
self.public_addresses_add_row
.set_inhibit_add(!self.can_add_public_address());
}
let Some(room) = self.room() else {
return;
};
/// Activate the auto-completion of the public addresses add row.
#[template_callback]
async fn handle_public_addresses_add_row_activated(&self) {
if !self.public_addresses_completion.activate_selected_row() {
self.add_public_address().await;
}
}
let imp = self.imp();
let row = &imp.public_addresses_add_row;
/// Add a an address to the public list.
#[template_callback]
async fn add_public_address(&self) {
if !self.can_add_public_address() {
return;
}
let Ok(alias) = RoomAliasId::parse(row.text()) else {
error!("Cannot add public address with invalid alias");
return;
};
let Some(room) = self.room.upgrade() else {
return;
};
imp.public_addresses_list.set_sensitive(false);
row.set_is_loading(true);
imp.public_addresses_error_revealer.set_reveal_child(false);
let row = &self.public_addresses_add_row;
let aliases = room.aliases();
match aliases.add_alt_alias(alias).await {
Ok(()) => {
row.set_text("");
}
Err(error) => {
toast!(self, gettext("Could not add public address"));
let Ok(alias) = RoomAliasId::parse(row.text()) else {
error!("Cannot add public address with invalid alias");
return;
};
let label = match error {
AddAltAliasError::NotRegistered => {
Some(gettext("This address is not registered as a local address"))
}
AddAltAliasError::InvalidRoomId => {
Some(gettext("This address does not belong to this room"))
}
AddAltAliasError::Other => None,
};
self.public_addresses_list.set_sensitive(false);
row.set_is_loading(true);
self.public_addresses_error_revealer.set_reveal_child(false);
if let Some(label) = label {
imp.public_addresses_error.set_label(&label);
imp.public_addresses_error_revealer.set_reveal_child(true);
let aliases = room.aliases();
match aliases.add_alt_alias(alias).await {
Ok(()) => {
row.set_text("");
}
Err(error) => {
let obj = self.obj();
toast!(obj, gettext("Could not add public address"));
imp.public_addresses_list.set_sensitive(true);
row.set_is_loading(false);
}
}
}
let label = match error {
AddAltAliasError::NotRegistered => {
Some(gettext("This address is not registered as a local address"))
}
AddAltAliasError::InvalidRoomId => {
Some(gettext("This address does not belong to this room"))
}
AddAltAliasError::Other => None,
};
/// Whether the user can add the current address to the public list.
fn can_add_public_address(&self) -> bool {
let imp = self.imp();
let new_address = imp.public_addresses_add_row.text();
if let Some(label) = label {
self.public_addresses_error.set_label(&label);
self.public_addresses_error_revealer.set_reveal_child(true);
}
// Cannot add an empty address.
if new_address.is_empty() {
return false;
self.public_addresses_list.set_sensitive(true);
row.set_is_loading(false);
}
}
}
// Cannot add an invalid alias.
let Ok(new_alias) = RoomAliasId::parse(new_address) else {
return false;
};
/// Whether the user can add the current address to the public list.
fn can_add_public_address(&self) -> bool {
let new_address = self.public_addresses_add_row.text();
// Cannot add a duplicate address.
for public_address in imp.public_addresses().iter::<PublicAddress>() {
let Ok(public_address) = public_address else {
// The iterator is broken.
// Cannot add an empty address.
if new_address.is_empty() {
return false;
};
}
if *public_address.alias() == new_alias {
// Cannot add an invalid alias.
let Ok(new_alias) = RoomAliasId::parse(new_address) else {
return false;
};
// Cannot add a duplicate address.
for public_address in self.public_addresses().iter::<PublicAddress>() {
let Ok(public_address) = public_address else {
// The iterator is broken.
return false;
};
if *public_address.alias() == new_alias {
return false;
}
}
}
true
}
true
}
/// Create a row for the given item in the public addresses section.
fn create_local_address_row(&self, item: &glib::Object) -> gtk::Widget {
let imp = self.imp();
/// Create a row for the given item in the public addresses section.
fn create_local_address_row(&self, item: &glib::Object) -> gtk::Widget {
let Some(string_obj) = item.downcast_ref::<gtk::StringObject>() else {
// It can only be the dummy item to add a new alias.
return self.local_addresses_add_row.clone().upcast();
};
if let Some(string_obj) = item.downcast_ref::<gtk::StringObject>() {
let alias = string_obj.string();
let row = RemovableRow::new();
row.set_title(&alias);
@ -669,153 +648,163 @@ impl AddressesSubpage {
)));
row.connect_remove(clone!(
#[weak(rename_to = obj)]
#[weak(rename_to = imp)]
self,
move |row| {
spawn!(clone!(
#[weak]
row,
async move {
obj.unregister_local_address(&row).await;
imp.unregister_local_address(&row).await;
}
));
}
));
row.upcast()
} else {
imp.local_addresses_add_row.clone().upcast()
}
}
/// Unregister the local address from the given row.
async fn unregister_local_address(&self, row: &RemovableRow) {
let Some(room) = self.room() else {
return;
};
let Ok(alias) = RoomAliasId::parse(row.title()) else {
error!("Cannot unregister local address with invalid alias");
return;
};
let aliases = room.aliases();
row.set_is_loading(true);
/// Unregister the local address from the given row.
async fn unregister_local_address(&self, row: &RemovableRow) {
let Some(room) = self.room.upgrade() else {
return;
};
let Ok(alias) = RoomAliasId::parse(row.title()) else {
error!("Cannot unregister local address with invalid alias");
return;
};
if aliases.unregister_local_alias(alias).await.is_err() {
toast!(self, gettext("Could not unregister local address"));
}
let aliases = room.aliases();
self.imp().update_local_addresses().await;
row.set_is_loading(true);
row.set_is_loading(false);
}
if aliases.unregister_local_alias(alias).await.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not unregister local address"));
}
/// The full new address in the public addresses add row.
///
/// Returns `None` if the localpart is empty.
fn new_local_address(&self) -> Option<String> {
let row = &self.imp().local_addresses_add_row;
let localpart = row.text();
self.update_local_addresses().await;
if localpart.is_empty() {
return None;
row.set_is_loading(false);
}
let server_name = row.suffix_text();
Some(format!("#{localpart}{server_name}"))
}
/// The full new address in the public addresses add row.
///
/// Returns `None` if the localpart is empty.
fn new_local_address(&self) -> Option<String> {
let row = &self.local_addresses_add_row;
let localpart = row.text();
/// Update the public addresses add row for the current state.
fn update_local_addresses_add_row(&self) {
let row = &self.imp().local_addresses_add_row;
row.set_inhibit_add(!self.can_register_local_address());
if localpart.is_empty() {
return None;
}
let accessible_label = self.new_local_address().map(|address| {
gettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Register “{address}”",
&[("address", &address)],
)
});
row.set_add_button_accessible_label(accessible_label);
}
let server_name = row.suffix_text();
Some(format!("#{localpart}{server_name}"))
}
/// Register a local address.
#[template_callback]
async fn register_local_address(&self) {
if !self.can_register_local_address() {
return;
/// Update the public addresses add row for the current state.
fn update_local_addresses_add_row(&self) {
let row = &self.local_addresses_add_row;
row.set_inhibit_add(!self.can_register_local_address());
let accessible_label = self.new_local_address().map(|address| {
gettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Register “{address}”",
&[("address", &address)],
)
});
row.set_add_button_accessible_label(accessible_label);
}
let Some(room) = self.room() else {
return;
};
/// Register a local address.
#[template_callback]
async fn register_local_address(&self) {
if !self.can_register_local_address() {
return;
}
let Some(new_address) = self.new_local_address() else {
return;
};
let Ok(alias) = RoomAliasId::parse(new_address) else {
error!("Cannot register local address with invalid alias");
return;
};
let Some(room) = self.room.upgrade() else {
return;
};
let imp = self.imp();
let row = &imp.local_addresses_add_row;
row.set_is_loading(true);
imp.local_addresses_error_revealer.set_reveal_child(false);
let Some(new_address) = self.new_local_address() else {
return;
};
let Ok(alias) = RoomAliasId::parse(new_address) else {
error!("Cannot register local address with invalid alias");
return;
};
let aliases = room.aliases();
let row = &self.local_addresses_add_row;
row.set_is_loading(true);
self.local_addresses_error_revealer.set_reveal_child(false);
match aliases.register_local_alias(alias).await {
Ok(()) => {
row.set_text("");
}
Err(error) => {
toast!(self, gettext("Could not register local address"));
let aliases = room.aliases();
if let RegisterLocalAliasError::AlreadyInUse = error {
imp.local_addresses_error
.set_label(&gettext("This address is already registered"));
imp.local_addresses_error_revealer.set_reveal_child(true);
match aliases.register_local_alias(alias).await {
Ok(()) => {
row.set_text("");
}
Err(error) => {
let obj = self.obj();
toast!(obj, gettext("Could not register local address"));
if let RegisterLocalAliasError::AlreadyInUse = error {
self.local_addresses_error
.set_label(&gettext("This address is already registered"));
self.local_addresses_error_revealer.set_reveal_child(true);
}
}
}
}
imp.update_local_addresses().await;
self.update_local_addresses().await;
row.set_is_loading(false);
}
row.set_is_loading(false);
}
/// Whether the user can add the current address to the local list.
fn can_register_local_address(&self) -> bool {
let imp = self.imp();
// Cannot add an empty address.
let Some(new_address) = self.new_local_address() else {
return false;
};
// Cannot add an invalid alias.
let Ok(new_alias) = RoomAliasId::parse(new_address) else {
return false;
};
// Cannot add a duplicate address.
for local_address in imp.public_addresses().iter::<glib::Object>() {
let Some(local_address) = local_address.ok().and_downcast::<gtk::StringObject>() else {
// The iterator is broken.
return true;
/// Whether the user can add the current address to the local list.
fn can_register_local_address(&self) -> bool {
// Cannot add an empty address.
let Some(new_address) = self.new_local_address() else {
return false;
};
if local_address.string() == new_alias.as_str() {
// Cannot add an invalid alias.
let Ok(new_alias) = RoomAliasId::parse(new_address) else {
return false;
};
// Cannot add a duplicate address.
for local_address in self.public_addresses().iter::<glib::Object>() {
let Some(local_address) = local_address.ok().and_downcast::<gtk::StringObject>()
else {
// The iterator is broken.
return true;
};
if local_address.string() == new_alias.as_str() {
return false;
}
}
true
}
}
}
true
glib::wrapper! {
/// Subpage to manage the public addresses of a room.
pub struct AddressesSubpage(ObjectSubclass<imp::AddressesSubpage>)
@extends gtk::Widget, gtk::Window, adw::NavigationPage, @implements gtk::Accessible;
}
impl AddressesSubpage {
pub fn new(room: &Room) -> Self {
glib::Object::builder().property("room", room).build()
}
}

26
src/session/view/content/room_details/addresses_subpage/public_address.rs

@ -1,5 +1,5 @@
use adw::subclass::prelude::*;
use gtk::{glib, prelude::*};
use adw::{prelude::*, subclass::prelude::*};
use gtk::glib;
use ruma::OwnedRoomAliasId;
mod imp {
@ -11,10 +11,10 @@ mod imp {
#[properties(wrapper_type = super::PublicAddress)]
pub struct PublicAddress {
/// The room alias.
pub alias: OnceCell<OwnedRoomAliasId>,
alias: OnceCell<OwnedRoomAliasId>,
/// Whether this is the main address.
#[property(get, set = Self::set_is_main, explicit_notify)]
pub is_main: Cell<bool>,
is_main: Cell<bool>,
}
#[glib::object_subclass]
@ -27,6 +27,16 @@ mod imp {
impl ObjectImpl for PublicAddress {}
impl PublicAddress {
/// Set the room alias.
pub(super) fn set_alias(&self, alias: OwnedRoomAliasId) {
self.alias.set(alias).expect("alias is uninitialized");
}
/// The room alias.
pub(super) fn alias(&self) -> &OwnedRoomAliasId {
self.alias.get().expect("alias is initialized")
}
/// Set whether this is the main address.
fn set_is_main(&self, is_main: bool) {
if self.is_main.get() == is_main {
@ -50,14 +60,12 @@ impl PublicAddress {
let obj = glib::Object::builder::<Self>()
.property("is-main", is_main)
.build();
obj.imp().alias.set(alias).unwrap();
obj.imp().set_alias(alias);
obj
}
/// The room alias.
pub fn alias(&self) -> &OwnedRoomAliasId {
self.imp().alias.get().unwrap()
pub(crate) fn alias(&self) -> &OwnedRoomAliasId {
self.imp().alias()
}
}

6
src/session/view/content/room_details/edit_details_subpage.rs

@ -1,10 +1,6 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{
gio,
glib::{self, clone},
CompositeTemplate,
};
use gtk::{gio, glib, glib::clone, CompositeTemplate};
use matrix_sdk::RoomState;
use ruma::{assign, events::room::avatar::ImageInfo, OwnedMxcUri};
use tracing::error;

1225
src/session/view/content/room_details/general_page.rs

File diff suppressed because it is too large Load Diff

23
src/session/view/content/room_details/history_viewer/audio.rs

@ -25,13 +25,13 @@ mod imp {
)]
#[properties(wrapper_type = super::AudioHistoryViewer)]
pub struct AudioHistoryViewer {
/// The timeline containing the audio events.
#[property(get, set = Self::set_timeline, construct_only)]
pub timeline: BoundConstructOnlyObject<HistoryViewerTimeline>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
stack: TemplateChild<gtk::Stack>,
#[template_child]
pub list_view: TemplateChild<gtk::ListView>,
list_view: TemplateChild<gtk::ListView>,
/// The timeline containing the audio events.
#[property(get, set = Self::set_timeline, construct_only)]
timeline: BoundConstructOnlyObject<HistoryViewerTimeline>,
}
#[glib::object_subclass]
@ -42,7 +42,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
Self::bind_template_callbacks(klass);
klass.set_css_name("audio-history-viewer");
}
@ -107,6 +107,7 @@ mod imp {
impl WidgetImpl for AudioHistoryViewer {}
impl NavigationPageImpl for AudioHistoryViewer {}
#[gtk::template_callbacks]
impl AudioHistoryViewer {
/// Set the timeline containing the audio events.
fn set_timeline(&self, timeline: HistoryViewerTimeline) {
@ -171,7 +172,8 @@ mod imp {
}
/// Load more items in this viewer.
pub(super) async fn load_more_items(&self) {
#[template_callback]
async fn load_more_items(&self) {
self.timeline
.obj()
.load(clone!(
@ -242,17 +244,10 @@ glib::wrapper! {
@extends gtk::Widget, adw::NavigationPage;
}
#[gtk::template_callbacks]
impl AudioHistoryViewer {
pub fn new(timeline: &HistoryViewerTimeline) -> Self {
glib::Object::builder()
.property("timeline", timeline)
.build()
}
/// Load more items in this viewer.
#[template_callback]
async fn load_more_items(&self) {
self.imp().load_more_items().await;
}
}

54
src/session/view/content/room_details/history_viewer/audio_row.rs

@ -23,19 +23,19 @@ mod imp {
)]
#[properties(wrapper_type = super::AudioRow)]
pub struct AudioRow {
#[template_child]
play_button: TemplateChild<gtk::Button>,
#[template_child]
title_label: TemplateChild<gtk::Label>,
#[template_child]
duration_label: TemplateChild<gtk::Label>,
/// The audio event.
#[property(get, set = Self::set_event, explicit_notify, nullable)]
pub event: RefCell<Option<HistoryViewerEvent>>,
event: RefCell<Option<HistoryViewerEvent>>,
/// The media file.
file: RefCell<Option<File>>,
/// The API for the media file.
pub media_file: RefCell<Option<gtk::MediaFile>>,
#[template_child]
pub play_button: TemplateChild<gtk::Button>,
#[template_child]
pub title_label: TemplateChild<gtk::Label>,
#[template_child]
pub duration_label: TemplateChild<gtk::Label>,
media_file: RefCell<Option<gtk::MediaFile>>,
}
#[glib::object_subclass]
@ -46,7 +46,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
Self::bind_template_callbacks(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
@ -60,6 +60,7 @@ mod imp {
impl WidgetImpl for AudioRow {}
impl BinImpl for AudioRow {}
#[gtk::template_callbacks]
impl AudioRow {
/// Set the audio event.
fn set_event(&self, event: Option<HistoryViewerEvent>) {
@ -159,6 +160,22 @@ mod imp {
self.file.replace(Some(file));
self.media_file.replace(Some(media_file));
}
/// Toggle the audio player playing state.
#[template_callback]
fn toggle_play(&self) {
if let Some(media_file) = self.media_file.borrow().as_ref() {
if media_file.is_playing() {
media_file.pause();
self.play_button
.set_icon_name("media-playback-start-symbolic");
} else {
media_file.play();
self.play_button
.set_icon_name("media-playback-pause-symbolic");
}
}
}
}
}
@ -168,28 +185,9 @@ glib::wrapper! {
@extends gtk::Widget, adw::Bin;
}
#[gtk::template_callbacks]
impl AudioRow {
/// Construct an empty `AudioRow`.
pub fn new() -> Self {
glib::Object::new()
}
/// Toggle the audio player playing state.
#[template_callback]
fn toggle_play(&self) {
let imp = self.imp();
if let Some(media_file) = self.imp().media_file.borrow().as_ref() {
if media_file.is_playing() {
media_file.pause();
imp.play_button
.set_icon_name("media-playback-start-symbolic");
} else {
media_file.play();
imp.play_button
.set_icon_name("media-playback-pause-symbolic");
}
}
}
}

54
src/session/view/content/room_details/history_viewer/event.rs

@ -1,5 +1,3 @@
use std::ops::Deref;
use gtk::{glib, prelude::*, subclass::prelude::*};
use matrix_sdk::deserialized_responses::TimelineEvent;
use ruma::{
@ -15,13 +13,16 @@ use crate::{
utils::matrix::{MediaMessage, VisualMediaMessage},
};
/// The types of events that can be displayer in the history viewers.
/// The types of events that can be displayed in the history viewers.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, glib::Enum)]
#[enum_type(name = "HistoryViewerEventType")]
pub enum HistoryViewerEventType {
/// A file.
#[default]
File,
/// An image or a video.
Media,
/// An audio file.
Audio,
}
@ -38,18 +39,6 @@ impl HistoryViewerEventType {
}
}
#[derive(Clone, Debug, glib::Boxed)]
#[boxed_type(name = "BoxedSyncRoomMessageEvent")]
pub struct BoxedSyncRoomMessageEvent(pub OriginalSyncRoomMessageEvent);
impl Deref for BoxedSyncRoomMessageEvent {
type Target = OriginalSyncRoomMessageEvent;
fn deref(&self) -> &Self::Target {
&self.0
}
}
mod imp {
use std::cell::{Cell, OnceCell};
@ -60,13 +49,12 @@ mod imp {
pub struct HistoryViewerEvent {
/// The room containing this event.
#[property(get, construct_only)]
pub room: glib::WeakRef<Room>,
room: glib::WeakRef<Room>,
/// The Matrix event.
#[property(construct_only)]
pub matrix_event: OnceCell<BoxedSyncRoomMessageEvent>,
matrix_event: OnceCell<OriginalSyncRoomMessageEvent>,
/// The type of the event.
#[property(get, construct_only, builder(HistoryViewerEventType::default()))]
pub event_type: Cell<HistoryViewerEventType>,
event_type: Cell<HistoryViewerEventType>,
}
#[glib::object_subclass]
@ -77,6 +65,22 @@ mod imp {
#[glib::derived_properties]
impl ObjectImpl for HistoryViewerEvent {}
impl HistoryViewerEvent {
/// Set the Matrix event.
pub(super) fn set_matrix_event(&self, event: OriginalSyncRoomMessageEvent) {
self.matrix_event
.set(event)
.expect("Matrix event should be uninitialized");
}
/// The Matrix event.
pub(super) fn matrix_event(&self) -> &OriginalSyncRoomMessageEvent {
self.matrix_event
.get()
.expect("Matrix event should be initialized")
}
}
}
glib::wrapper! {
@ -118,22 +122,22 @@ impl HistoryViewerEvent {
let event_type = HistoryViewerEventType::with_msgtype(&message_event.content.msgtype)?;
let obj: Self = glib::Object::builder()
let obj = glib::Object::builder::<Self>()
.property("room", room)
.property("matrix-event", BoxedSyncRoomMessageEvent(message_event))
.property("event-type", event_type)
.build();
obj.imp().set_matrix_event(message_event);
Some(obj)
}
/// The Matrix event.
fn matrix_event(&self) -> &OriginalSyncRoomMessageEvent {
self.imp().matrix_event.get().unwrap()
pub(crate) fn matrix_event(&self) -> &OriginalSyncRoomMessageEvent {
self.imp().matrix_event()
}
/// The event ID of the inner event.
pub fn event_id(&self) -> OwnedEventId {
pub(crate) fn event_id(&self) -> OwnedEventId {
self.matrix_event().event_id.clone()
}
@ -149,7 +153,7 @@ impl HistoryViewerEvent {
}
/// Get the binary content of this event.
pub async fn get_file_content(&self) -> Result<Vec<u8>, matrix_sdk::Error> {
pub(crate) async fn get_file_content(&self) -> Result<Vec<u8>, matrix_sdk::Error> {
let Some(room) = self.room() else {
return Err(matrix_sdk::Error::UnknownError(
"Could not upgrade Room".into(),

23
src/session/view/content/room_details/history_viewer/file.rs

@ -25,13 +25,13 @@ mod imp {
)]
#[properties(wrapper_type = super::FileHistoryViewer)]
pub struct FileHistoryViewer {
/// The timeline containing the file events.
#[property(get, set = Self::set_timeline, construct_only)]
pub timeline: BoundConstructOnlyObject<HistoryViewerTimeline>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
stack: TemplateChild<gtk::Stack>,
#[template_child]
pub list_view: TemplateChild<gtk::ListView>,
list_view: TemplateChild<gtk::ListView>,
/// The timeline containing the file events.
#[property(get, set = Self::set_timeline, construct_only)]
timeline: BoundConstructOnlyObject<HistoryViewerTimeline>,
}
#[glib::object_subclass]
@ -42,7 +42,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
Self::bind_template_callbacks(klass);
klass.set_css_name("file-history-viewer");
}
@ -107,6 +107,7 @@ mod imp {
impl WidgetImpl for FileHistoryViewer {}
impl NavigationPageImpl for FileHistoryViewer {}
#[gtk::template_callbacks]
impl FileHistoryViewer {
/// Set the timeline containing the media events.
fn set_timeline(&self, timeline: HistoryViewerTimeline) {
@ -171,7 +172,8 @@ mod imp {
}
/// Load more items in this viewer.
pub(super) async fn load_more_items(&self) {
#[template_callback]
async fn load_more_items(&self) {
self.timeline
.obj()
.load(clone!(
@ -242,17 +244,10 @@ glib::wrapper! {
@extends gtk::Widget, adw::NavigationPage;
}
#[gtk::template_callbacks]
impl FileHistoryViewer {
pub fn new(timeline: &HistoryViewerTimeline) -> Self {
glib::Object::builder()
.property("timeline", timeline)
.build()
}
/// Load more items in this viewer.
#[template_callback]
async fn load_more_items(&self) {
self.imp().load_more_items().await;
}
}

137
src/session/view/content/room_details/history_viewer/file_row.rs

@ -19,16 +19,16 @@ mod imp {
)]
#[properties(wrapper_type = super::FileRow)]
pub struct FileRow {
/// The file event.
#[property(get, set = Self::set_event, explicit_notify, nullable)]
pub event: RefCell<Option<HistoryViewerEvent>>,
pub file: RefCell<Option<gio::File>>,
#[template_child]
pub button: TemplateChild<gtk::Button>,
button: TemplateChild<gtk::Button>,
#[template_child]
pub title_label: TemplateChild<gtk::Label>,
title_label: TemplateChild<gtk::Label>,
#[template_child]
pub size_label: TemplateChild<gtk::Label>,
size_label: TemplateChild<gtk::Label>,
/// The file event.
#[property(get, set = Self::set_event, explicit_notify, nullable)]
event: RefCell<Option<HistoryViewerEvent>>,
file: RefCell<Option<gio::File>>,
}
#[glib::object_subclass]
@ -39,7 +39,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
Self::bind_template_callbacks(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
@ -53,6 +53,7 @@ mod imp {
impl WidgetImpl for FileRow {}
impl BinImpl for FileRow {}
#[gtk::template_callbacks]
impl FileRow {
/// Set the file event.
fn set_event(&self, event: Option<HistoryViewerEvent>) {
@ -100,6 +101,67 @@ mod imp {
self.button.set_tooltip_text(Some(&gettext("Save File")));
}
}
/// Handle when the row's button was clicked.
#[template_callback]
async fn button_clicked(&self) {
let file = self.file.borrow().clone();
// If there is a file, open it.
if let Some(file) = file {
if let Err(error) =
gio::AppInfo::launch_default_for_uri(&file.uri(), gio::AppLaunchContext::NONE)
{
error!("Could not open file: {error}");
}
} else {
// Otherwise save the file.
self.save_file().await;
}
}
/// Save the file of this row.
async fn save_file(&self) {
let Some(event) = self.event.borrow().clone() else {
return;
};
let obj = self.obj();
let data = match event.get_file_content().await {
Ok(res) => res,
Err(error) => {
error!("Could not get file: {error}");
toast!(obj, error.to_user_facing());
return;
}
};
let filename = event.media_message().filename();
let parent_window = obj.root().and_downcast::<gtk::Window>();
let dialog = gtk::FileDialog::builder()
.title(gettext("Save File"))
.accept_label(gettext("Save"))
.initial_name(filename)
.build();
if let Ok(file) = dialog.save_future(parent_window.as_ref()).await {
if let Err(error) = file.replace_contents(
&data,
None,
false,
gio::FileCreateFlags::REPLACE_DESTINATION,
gio::Cancellable::NONE,
) {
error!("Could not write file content: {error}");
toast!(obj, gettext("Could not save file"));
return;
}
self.file.replace(Some(file));
self.update_button();
}
}
}
}
@ -109,68 +171,9 @@ glib::wrapper! {
@extends gtk::Widget, adw::Bin;
}
#[gtk::template_callbacks]
impl FileRow {
/// Construct an empty `FileRow`.
pub fn new() -> Self {
glib::Object::new()
}
/// Handle when the row's button was clicked.
#[template_callback]
async fn button_clicked(&self) {
let file = self.imp().file.borrow().clone();
// If there is a file, open it.
if let Some(file) = file {
if let Err(error) =
gio::AppInfo::launch_default_for_uri(&file.uri(), gio::AppLaunchContext::NONE)
{
error!("Could not open file: {error}");
}
} else {
// Otherwise save the file.
self.save_file().await;
}
}
/// Save the file of this row.
async fn save_file(&self) {
let Some(event) = self.event() else {
return;
};
let data = match event.get_file_content().await {
Ok(res) => res,
Err(error) => {
error!("Could not get file: {error}");
toast!(self, error.to_user_facing());
return;
}
};
let filename = event.media_message().filename();
let parent_window = self.root().and_downcast::<gtk::Window>().unwrap();
let dialog = gtk::FileDialog::builder()
.title(gettext("Save File"))
.accept_label(gettext("Save"))
.initial_name(filename)
.build();
if let Ok(file) = dialog.save_future(Some(&parent_window)).await {
file.replace_contents(
&data,
None,
false,
gio::FileCreateFlags::REPLACE_DESTINATION,
gio::Cancellable::NONE,
)
.unwrap();
let imp = self.imp();
imp.file.replace(Some(file));
imp.update_button();
}
}
}

2
src/session/view/content/room_details/history_viewer/mod.rs

@ -7,7 +7,7 @@ mod timeline;
mod visual_media;
mod visual_media_item;
pub use self::{
pub(crate) use self::{
audio::AudioHistoryViewer, file::FileHistoryViewer, timeline::HistoryViewerTimeline,
visual_media::VisualMediaHistoryViewer,
};

193
src/session/view/content/room_details/history_viewer/timeline.rs

@ -16,12 +16,7 @@ use super::HistoryViewerEvent;
use crate::{components::LoadingRow, session::model::Room, spawn_tokio, utils::LoadingState};
mod imp {
use std::{
cell::{Cell, OnceCell, RefCell},
sync::Arc,
};
use futures_util::lock::Mutex;
use std::cell::{Cell, OnceCell, RefCell};
use super::*;
@ -30,15 +25,15 @@ mod imp {
pub struct HistoryViewerTimeline {
/// The room that this timeline belongs to.
#[property(get, construct_only)]
pub room: OnceCell<Room>,
room: OnceCell<Room>,
/// The loading state of this timeline.
#[property(get, builder(LoadingState::default()))]
pub state: Cell<LoadingState>,
state: Cell<LoadingState>,
/// Whether we have reached the start of the timeline.
#[property(get)]
has_reached_start: Cell<bool>,
pub list: RefCell<Vec<HistoryViewerEvent>>,
pub last_token: Arc<Mutex<String>>,
list: RefCell<Vec<HistoryViewerEvent>>,
last_token: RefCell<Option<String>>,
/// A wrapper model with an extra loading item at the end when
/// applicable.
///
@ -77,8 +72,13 @@ mod imp {
}
impl HistoryViewerTimeline {
/// The room that this timeline belongs to.
fn room(&self) -> &Room {
self.room.get().expect("room should be initialized")
}
/// Set the loading state of the timeline.
pub(super) fn set_state(&self, state: LoadingState) {
fn set_state(&self, state: LoadingState) {
if state == self.state.get() {
return;
}
@ -141,6 +141,91 @@ mod imp {
gtk::FlattenListModel::new(Some(wrapper_model))
})
}
/// Load more events in the timeline until the given function tells us
/// to stop.
pub(super) async fn load<F>(&self, continue_fn: F)
where
F: Fn() -> ControlFlow<()>,
{
if self.state.get() == LoadingState::Loading || self.has_reached_start.get() {
return;
}
self.set_state(LoadingState::Loading);
loop {
if !self.load_inner().await {
return;
}
if continue_fn().is_break() {
self.set_state(LoadingState::Ready);
return;
}
}
}
/// Load more events in the timeline.
///
/// Returns `true` if more events can be loaded.
async fn load_inner(&self) -> bool {
let room = self.room();
let matrix_room = room.matrix_room().clone();
let last_token = self.last_token.borrow().clone();
let is_encrypted = room.is_encrypted();
let handle = spawn_tokio!(async move {
// If the room is encrypted, the messages content cannot be filtered with URLs
let filter = if is_encrypted {
let filter_types = vec![
MessageLikeEventType::RoomEncrypted.to_string(),
MessageLikeEventType::RoomMessage.to_string(),
];
assign!(RoomEventFilter::default(), {
types: Some(filter_types),
})
} else {
let filter_types = vec![MessageLikeEventType::RoomMessage.to_string()];
assign!(RoomEventFilter::default(), {
types: Some(filter_types),
url_filter: Some(UrlFilter::EventsWithUrl),
})
};
let options = assign!(MessagesOptions::backward().from(last_token.as_deref()), {
limit: uint!(20),
filter,
});
matrix_room.messages(options).await
});
match handle.await.expect("task was not aborted") {
Ok(events) => {
if let Some(end_token) = events.end {
self.last_token.replace(Some(end_token));
let events = events
.chunk
.into_iter()
.filter_map(|event| HistoryViewerEvent::try_new(room, &event))
.collect();
self.append(events);
true
} else {
self.set_has_reached_start(true);
self.set_state(LoadingState::Ready);
false
}
}
Err(error) => {
error!("Could not load history viewer timeline events: {error}");
self.set_state(LoadingState::Error);
false
}
}
}
}
}
@ -157,98 +242,18 @@ impl HistoryViewerTimeline {
/// Load more events in the timeline until the given function tells us to
/// stop.
pub async fn load<F>(&self, continue_fn: F)
pub(crate) async fn load<F>(&self, continue_fn: F)
where
F: Fn() -> ControlFlow<()>,
{
if self.state() == LoadingState::Loading || self.has_reached_start() {
return;
}
let imp = self.imp();
imp.set_state(LoadingState::Loading);
loop {
if !self.load_inner().await {
return;
}
if continue_fn().is_break() {
imp.set_state(LoadingState::Ready);
return;
}
}
}
/// Load more events in the timeline.
///
/// Returns `true` if more events can be loaded.
async fn load_inner(&self) -> bool {
let imp = self.imp();
let room = self.room();
let matrix_room = room.matrix_room().clone();
let last_token = imp.last_token.clone();
let is_encrypted = room.is_encrypted();
let handle = spawn_tokio!(async move {
let last_token = last_token.lock().await;
// If the room is encrypted, the messages content cannot be filtered with URLs
let filter = if is_encrypted {
let filter_types = vec![
MessageLikeEventType::RoomEncrypted.to_string(),
MessageLikeEventType::RoomMessage.to_string(),
];
assign!(RoomEventFilter::default(), {
types: Some(filter_types),
})
} else {
let filter_types = vec![MessageLikeEventType::RoomMessage.to_string()];
assign!(RoomEventFilter::default(), {
types: Some(filter_types),
url_filter: Some(UrlFilter::EventsWithUrl),
})
};
let options = assign!(MessagesOptions::backward().from(&**last_token), {
limit: uint!(20),
filter,
});
matrix_room.messages(options).await
});
match handle.await.expect("task was not aborted") {
Ok(events) => {
if let Some(end_token) = events.end {
*imp.last_token.lock().await = end_token;
let events = events
.chunk
.into_iter()
.filter_map(|event| HistoryViewerEvent::try_new(&room, &event))
.collect();
imp.append(events);
true
} else {
imp.set_has_reached_start(true);
imp.set_state(LoadingState::Ready);
false
}
}
Err(error) => {
error!("Could not load history viewer timeline events: {error}");
imp.set_state(LoadingState::Error);
false
}
}
self.imp().load(continue_fn).await;
}
/// This model with an extra loading item at the end when applicable.
///
/// The loading item is a [`LoadingRow`], all other items are
/// [`HistoryViewerEvent`]s.
pub fn with_loading_item(&self) -> &gio::ListModel {
pub(crate) fn with_loading_item(&self) -> &gio::ListModel {
self.imp().model_with_loading_item().upcast_ref()
}
}

59
src/session/view/content/room_details/history_viewer/visual_media.rs

@ -28,15 +28,15 @@ mod imp {
)]
#[properties(wrapper_type = super::VisualMediaHistoryViewer)]
pub struct VisualMediaHistoryViewer {
/// The timeline containing the media events.
#[property(get, set = Self::set_timeline, construct_only)]
pub timeline: BoundConstructOnlyObject<HistoryViewerTimeline>,
#[template_child]
pub media_viewer: TemplateChild<MediaViewer>,
media_viewer: TemplateChild<MediaViewer>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
stack: TemplateChild<gtk::Stack>,
#[template_child]
pub grid_view: TemplateChild<gtk::GridView>,
grid_view: TemplateChild<gtk::GridView>,
/// The timeline containing the media events.
#[property(get, set = Self::set_timeline, construct_only)]
timeline: BoundConstructOnlyObject<HistoryViewerTimeline>,
}
#[glib::object_subclass]
@ -47,7 +47,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
Self::bind_template_callbacks(klass);
klass.set_css_name("visual-media-history-viewer");
}
@ -116,6 +116,7 @@ mod imp {
impl WidgetImpl for VisualMediaHistoryViewer {}
impl NavigationPageImpl for VisualMediaHistoryViewer {}
#[gtk::template_callbacks]
impl VisualMediaHistoryViewer {
/// Set the timeline containing the media events.
fn set_timeline(&self, timeline: HistoryViewerTimeline) {
@ -178,7 +179,8 @@ mod imp {
}
/// Load more items in this viewer.
pub(super) async fn load_more_items(&self) {
#[template_callback]
async fn load_more_items(&self) {
self.timeline
.obj()
.load(clone!(
@ -240,6 +242,23 @@ mod imp {
};
self.stack.set_visible_child_name(visible_child_name);
}
/// Show the given media item.
pub(super) fn show_media(&self, item: &VisualMediaItem) {
let Some(event) = item.event() else {
return;
};
let Some(room) = event.room() else {
return;
};
let media_message = event
.visual_media_message()
.expect("visual media items should contain only visual message content");
self.media_viewer
.set_message(&room, event.event_id(), media_message);
self.media_viewer.reveal(item);
}
}
}
@ -249,7 +268,6 @@ glib::wrapper! {
@extends gtk::Widget, adw::NavigationPage;
}
#[gtk::template_callbacks]
impl VisualMediaHistoryViewer {
pub fn new(timeline: &HistoryViewerTimeline) -> Self {
glib::Object::builder()
@ -258,26 +276,7 @@ impl VisualMediaHistoryViewer {
}
/// Show the given media item.
pub fn show_media(&self, item: &VisualMediaItem) {
let Some(event) = item.event() else {
return;
};
let Some(room) = event.room() else {
return;
};
let imp = self.imp();
let media_message = event
.visual_media_message()
.expect("Visual media items contain only visual message content");
imp.media_viewer
.set_message(&room, event.event_id(), media_message);
imp.media_viewer.reveal(item);
}
/// Load more items in this viewery.
#[template_callback]
async fn load_more_items(&self) {
self.imp().load_more_items().await;
pub(crate) fn show_media(&self, item: &VisualMediaItem) {
self.imp().show_media(item);
}
}

41
src/session/view/content/room_details/history_viewer/visual_media_item.rs

@ -30,14 +30,14 @@ mod imp {
)]
#[properties(wrapper_type = super::VisualMediaItem)]
pub struct VisualMediaItem {
/// The file event.
#[property(get, set = Self::set_event, explicit_notify, nullable)]
pub event: RefCell<Option<HistoryViewerEvent>>,
pub overlay_icon: RefCell<Option<gtk::Image>>,
#[template_child]
pub overlay: TemplateChild<gtk::Overlay>,
overlay: TemplateChild<gtk::Overlay>,
#[template_child]
pub picture: TemplateChild<gtk::Picture>,
picture: TemplateChild<gtk::Picture>,
/// The file event.
#[property(get, set = Self::set_event, explicit_notify, nullable)]
event: RefCell<Option<HistoryViewerEvent>>,
overlay_icon: RefCell<Option<gtk::Image>>,
}
#[glib::object_subclass]
@ -48,7 +48,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
Self::bind_template_callbacks(klass);
klass.set_css_name("visual-media-history-viewer-item");
@ -87,6 +87,7 @@ mod imp {
}
}
#[gtk::template_callbacks]
impl VisualMediaItem {
/// Set the media event.
fn set_event(&self, event: Option<HistoryViewerEvent>) {
@ -184,6 +185,21 @@ mod imp {
.set_paintable(Some(&gdk::Paintable::from(image)));
}
}
/// The item was activated.
#[template_callback]
fn activate(&self) {
let obj = self.obj();
let Some(media_history_viewer) = obj
.ancestor(VisualMediaHistoryViewer::static_type())
.and_downcast::<VisualMediaHistoryViewer>()
else {
return;
};
media_history_viewer.show_media(&obj);
}
}
}
@ -193,20 +209,9 @@ glib::wrapper! {
@extends gtk::Widget, @implements gtk::Accessible;
}
#[gtk::template_callbacks]
impl VisualMediaItem {
/// Construct a new empty `VisualMediaItem`.
pub fn new() -> Self {
glib::Object::new()
}
/// The item was activated.
#[template_callback]
fn activate(&self) {
let media_history_viewer = self
.ancestor(VisualMediaHistoryViewer::static_type())
.and_downcast::<VisualMediaHistoryViewer>()
.unwrap();
media_history_viewer.show_media(self);
}
}

8
src/session/view/content/room_details/invite_subpage/item.rs

@ -15,16 +15,16 @@ mod imp {
pub struct InviteItem {
/// The user data of the item.
#[property(get, construct_only)]
pub user: OnceCell<User>,
user: OnceCell<User>,
/// Whether the user is invited.
#[property(get, set = Self::set_is_invitee, explicit_notify)]
pub is_invitee: Cell<bool>,
is_invitee: Cell<bool>,
/// Whether the user can be invited.
#[property(get = Self::can_invite)]
pub can_invite: PhantomData<bool>,
can_invite: PhantomData<bool>,
/// The reason why the user cannot be invited, when applicable.
#[property(get, set = Self::set_invite_exception, explicit_notify, nullable)]
pub invite_exception: RefCell<Option<String>>,
invite_exception: RefCell<Option<String>>,
}
#[glib::object_subclass]

520
src/session/view/content/room_details/invite_subpage/list.rs

@ -18,15 +18,14 @@ use crate::{
};
#[derive(Debug, Default, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[repr(u32)]
#[enum_type(name = "RoomDetailsInviteListState")]
pub enum InviteListState {
#[default]
Initial = 0,
Loading = 1,
NoMatching = 2,
Matching = 3,
Error = 4,
Initial,
Loading,
NoMatching,
Matching,
Error,
}
mod imp {
@ -44,21 +43,21 @@ mod imp {
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::InviteList)]
pub struct InviteList {
pub list: RefCell<Vec<InviteItem>>,
list: RefCell<Vec<InviteItem>>,
/// The room this invitee list refers to.
#[property(get, construct_only)]
pub room: OnceCell<Room>,
room: OnceCell<Room>,
/// The state of the list.
#[property(get, builder(InviteListState::default()))]
pub state: Cell<InviteListState>,
state: Cell<InviteListState>,
/// The search term.
#[property(get, set = Self::set_search_term, explicit_notify)]
pub search_term: RefCell<Option<String>>,
pub invitee_list: RefCell<HashMap<OwnedUserId, InviteItem>>,
pub abort_handle: RefCell<Option<tokio::task::AbortHandle>>,
search_term: RefCell<Option<String>>,
pub(super) invitee_list: RefCell<HashMap<OwnedUserId, InviteItem>>,
abort_handle: RefCell<Option<tokio::task::AbortHandle>>,
/// Whether some users are invited.
#[property(get = Self::has_invitees)]
pub has_invitees: PhantomData<bool>,
has_invitees: PhantomData<bool>,
}
#[glib::object_subclass]
@ -104,6 +103,11 @@ mod imp {
}
impl InviteList {
/// The room this invitee list refers to.
fn room(&self) -> &Room {
self.room.get().expect("room should be initialized")
}
/// Set the search term.
fn set_search_term(&self, search_term: Option<String>) {
let search_term = search_term.filter(|s| !s.is_empty());
@ -111,19 +115,18 @@ mod imp {
if search_term == *self.search_term.borrow() {
return;
}
let obj = self.obj();
self.search_term.replace(search_term);
spawn!(clone!(
#[weak]
obj,
#[weak(rename_to = imp)]
self,
async move {
obj.search_users().await;
imp.search_users().await;
}
));
obj.notify_search_term();
self.obj().notify_search_term();
}
/// Whether some users are invited.
@ -132,7 +135,7 @@ mod imp {
}
/// Set the state of the list.
pub(super) fn set_state(&self, state: InviteListState) {
pub fn set_state(&self, state: InviteListState) {
if state == self.state.get() {
return;
}
@ -140,305 +143,320 @@ mod imp {
self.state.set(state);
self.obj().notify_state();
}
}
}
glib::wrapper! {
/// List of users after a search in the user directory.
///
/// This also manages invitees.
pub struct InviteList(ObjectSubclass<imp::InviteList>)
@implements gio::ListModel;
}
/// Replace this list with the given items.
fn replace_list(&self, items: Vec<InviteItem>) {
let added = items.len();
impl InviteList {
pub fn new(room: &Room) -> Self {
glib::Object::builder().property("room", room).build()
}
/// Replace this list with the given items.
fn replace_list(&self, items: Vec<InviteItem>) {
let added = items.len();
let prev_items = self.list.replace(items);
let prev_items = self.imp().list.replace(items);
self.obj()
.items_changed(0, prev_items.len() as u32, added as u32);
}
self.items_changed(0, prev_items.len() as u32, added as u32);
}
/// Clear this list.
fn clear_list(&self) {
self.replace_list(Vec::new());
}
/// Clear this list.
fn clear_list(&self) {
self.replace_list(Vec::new());
}
/// Search for the current search term in the user directory.
async fn search_users(&self) {
let Some(session) = self.room().session() else {
return;
};
/// Search for the current search term in the user directory.
async fn search_users(&self) {
let Some(session) = self.room().session() else {
return;
};
let Some(search_term) = self.search_term.borrow().clone() else {
// Do nothing for no search term, but reset state when currently loading.
if self.state.get() == InviteListState::Loading {
self.set_state(InviteListState::Initial);
}
if let Some(abort_handle) = self.abort_handle.take() {
abort_handle.abort();
}
let imp = self.imp();
return;
};
let Some(search_term) = self.search_term() else {
// Do nothing for no search term, but reset state when currently loading.
if self.state() == InviteListState::Loading {
imp.set_state(InviteListState::Initial);
}
if let Some(abort_handle) = imp.abort_handle.take() {
abort_handle.abort();
}
self.set_state(InviteListState::Loading);
self.clear_list();
return;
};
let client = session.client();
let search_term_clone = search_term.clone();
let handle =
spawn_tokio!(async move { client.search_users(&search_term_clone, 10).await });
imp.set_state(InviteListState::Loading);
self.clear_list();
let abort_handle = handle.abort_handle();
let client = session.client();
let search_term_clone = search_term.clone();
let handle = spawn_tokio!(async move { client.search_users(&search_term_clone, 10).await });
// Keep the abort handle so we can abort the request if the user changes the
// search term.
if let Some(prev_abort_handle) = self.abort_handle.replace(Some(abort_handle)) {
// Abort the previous request.
prev_abort_handle.abort();
}
let abort_handle = handle.abort_handle();
match handle.await {
Ok(Ok(response)) => {
// The request succeeded.
if self
.search_term
.borrow()
.as_ref()
.is_some_and(|s| *s == search_term)
{
self.update_from_search_results(response.results);
}
}
Ok(Err(error)) => {
// The request failed.
error!("Could not search user directory: {error}");
self.set_state(InviteListState::Error);
self.clear_list();
}
Err(_) => {
// The request was aborted.
}
}
// Keep the abort handle so we can abort the request if the user changes the
// search term.
if let Some(prev_abort_handle) = imp.abort_handle.replace(Some(abort_handle)) {
// Abort the previous request.
prev_abort_handle.abort();
self.abort_handle.take();
}
match handle.await {
Ok(Ok(response)) => {
// The request succeeded.
if self.search_term().is_some_and(|s| s == search_term) {
self.update_from_search_results(response.results);
}
}
Ok(Err(error)) => {
// The request failed.
error!("Could not search user directory: {error}");
imp.set_state(InviteListState::Error);
/// Update this list from the given search results.
fn update_from_search_results(&self, results: Vec<SearchUser>) {
let Some(session) = self.room().session() else {
return;
};
let Some(search_term) = self.search_term.borrow().clone() else {
return;
};
// We should have a strong reference to the list in the main page so we can use
// `get_or_create_members()`.
let member_list = self.room().get_or_create_members();
// If the search term looks like a user ID and it is not already in the
// response, we will insert it in the list.
let search_term_user_id = UserId::parse(search_term)
.ok()
.filter(|user_id| !results.iter().any(|item| item.user_id == *user_id));
let search_term_user = search_term_user_id.clone().map(SearchUser::new);
let new_len = results
.len()
.saturating_add(search_term_user.is_some().into());
if new_len == 0 {
self.set_state(InviteListState::NoMatching);
self.clear_list();
return;
}
Err(_) => {
// The request was aborted.
}
}
imp.abort_handle.take();
}
let mut list = Vec::with_capacity(new_len);
let results = search_term_user.into_iter().chain(results);
for result in results {
let member = member_list.get(&result.user_id);
// 'Disable' users that can't be invited.
let invite_exception = member.as_ref().and_then(|m| match m.membership() {
Membership::Join => Some(gettext("Member")),
Membership::Ban => Some(gettext("Banned")),
Membership::Invite => Some(gettext("Invited")),
_ => None,
});
// If it's an invitee, reuse the item.
let invitee = self.invitee_list.borrow().get(&result.user_id).cloned();
if let Some(item) = invitee {
let user = item.user();
// The profile data may have changed in the meantime, but don't overwrite a
// joined member's data.
if !user
.downcast_ref::<Member>()
.is_some_and(|m| m.membership() == Membership::Join)
{
user.set_avatar_url(result.avatar_url);
user.set_name(result.display_name);
}
// The membership state may have changed in the meantime.
item.set_invite_exception(invite_exception);
list.push(item);
continue;
}
/// Update this list from the given search results.
fn update_from_search_results(&self, results: Vec<SearchUser>) {
let Some(session) = self.room().session() else {
return;
};
let Some(search_term) = self.search_term() else {
return;
};
let imp = self.imp();
// We should have a strong reference to the list in the main page so we can use
// `get_or_create_members()`.
let member_list = self.room().get_or_create_members();
// If the search term looks like a user ID and it is not already in the
// response, we will insert it in the list.
let search_term_user_id = UserId::parse(search_term)
.ok()
.filter(|user_id| !results.iter().any(|item| item.user_id == *user_id));
let search_term_user = search_term_user_id.clone().map(SearchUser::new);
let new_len = results
.len()
.saturating_add(search_term_user.is_some().into());
if new_len == 0 {
imp.set_state(InviteListState::NoMatching);
self.clear_list();
return;
}
// If it's a joined room member, reuse the user.
if let Some(member) = member.filter(|m| m.membership() == Membership::Join) {
let item = self.create_item(&member, invite_exception);
list.push(item);
let mut list = Vec::with_capacity(new_len);
let results = search_term_user.into_iter().chain(results);
continue;
}
for result in results {
let member = member_list.get(&result.user_id);
// If it's the dummy result for the search term user ID, use a RemoteUser to
// fetch its profile.
if search_term_user_id
.as_ref()
.is_some_and(|user_id| *user_id == result.user_id)
{
let user = RemoteUser::new(&session, result.user_id);
// 'Disable' users that can't be invited.
let invite_exception = member.as_ref().and_then(|m| match m.membership() {
Membership::Join => Some(gettext("Member")),
Membership::Ban => Some(gettext("Banned")),
Membership::Invite => Some(gettext("Invited")),
_ => None,
});
let item = self.create_item(&user, invite_exception);
list.push(item);
// If it's an invitee, reuse the item.
if let Some(item) = self.invitee(&result.user_id) {
let user = item.user();
spawn!(async move { user.load_profile().await });
// The profile data may have changed in the meantime, but don't overwrite a
// joined member's data.
if !user
.downcast_ref::<Member>()
.is_some_and(|m| m.membership() == Membership::Join)
{
user.set_avatar_url(result.avatar_url);
user.set_name(result.display_name);
continue;
}
// The membership state may have changed in the meantime.
item.set_invite_exception(invite_exception);
// As a last resort, we just use the data of the result.
let user = User::new(&session, result.user_id);
user.set_avatar_url(result.avatar_url);
user.set_name(result.display_name);
let item = self.create_item(&user, invite_exception);
list.push(item);
continue;
}
// If it's a joined room member, reuse the user.
if let Some(member) = member.filter(|m| m.membership() == Membership::Join) {
let item = self.create_item(&member, invite_exception);
list.push(item);
self.replace_list(list);
self.set_state(InviteListState::Matching);
}
continue;
}
/// Create an item for the given user and invite exception.
fn create_item(
&self,
user: &impl IsA<User>,
invite_exception: Option<String>,
) -> InviteItem {
let item = InviteItem::new(user);
item.set_invite_exception(invite_exception);
item.connect_is_invitee_notify(clone!(
#[weak(rename_to = imp)]
self,
move |item| {
imp.update_invitees_for_item(item);
}
));
item.connect_can_invite_notify(clone!(
#[weak(rename_to = imp)]
self,
move |item| {
imp.update_invitees_for_item(item);
}
));
// If it's the dummy result for the search term user ID, use a RemoteUser to
// fetch its profile.
if search_term_user_id
.as_ref()
.is_some_and(|user_id| *user_id == result.user_id)
{
let user = RemoteUser::new(&session, result.user_id);
item
}
let item = self.create_item(&user, invite_exception);
list.push(item);
/// Update the list of invitees for the current state of the item.
fn update_invitees_for_item(&self, item: &InviteItem) {
if item.is_invitee() && item.can_invite() {
self.add_invitee(item);
} else {
self.remove_invitee(item.user().user_id());
}
}
spawn!(async move { user.load_profile().await });
/// Add the given item as an invitee.
fn add_invitee(&self, item: &InviteItem) {
let had_invitees = self.has_invitees();
continue;
}
item.set_is_invitee(true);
self.invitee_list
.borrow_mut()
.insert(item.user().user_id().clone(), item.clone());
// As a last resort, we just use the data of the result.
let user = User::new(&session, result.user_id);
user.set_avatar_url(result.avatar_url);
user.set_name(result.display_name);
let obj = self.obj();
obj.emit_by_name::<()>("invitee-added", &[&item]);
let item = self.create_item(&user, invite_exception);
list.push(item);
if !had_invitees {
obj.notify_has_invitees();
}
}
self.replace_list(list);
imp.set_state(InviteListState::Matching);
}
/// Update the list of invitees so only the invitees with the given user
/// IDs remain.
pub(super) fn retain_invitees(&self, invitees_ids: &[&UserId]) {
if !self.has_invitees() {
// Nothing to do.
return;
}
/// Create an item for the given user and invite exception.
fn create_item(&self, user: &impl IsA<User>, invite_exception: Option<String>) -> InviteItem {
let item = InviteItem::new(user);
item.set_invite_exception(invite_exception);
let invitee_list = self.invitee_list.take();
item.connect_is_invitee_notify(clone!(
#[weak(rename_to = obj)]
self,
move |item| {
obj.update_invitees_for_item(item);
let (invitee_list, removed_invitees) = invitee_list
.into_iter()
.partition(|(key, _)| invitees_ids.contains(&key.as_ref()));
self.invitee_list.replace(invitee_list);
for item in removed_invitees.values() {
self.handle_removed_invitee(item);
}
));
item.connect_can_invite_notify(clone!(
#[weak(rename_to = obj)]
self,
move |item| {
obj.update_invitees_for_item(item);
if !self.has_invitees() {
self.obj().notify_has_invitees();
}
));
}
item
}
/// Remove the invitee with the given user ID from the list.
pub(super) fn remove_invitee(&self, user_id: &UserId) {
let Some(item) = self.invitee_list.borrow_mut().remove(user_id) else {
return;
};
self.handle_removed_invitee(&item);
/// Update the list of invitees for the current state of the item.
fn update_invitees_for_item(&self, item: &InviteItem) {
if item.is_invitee() && item.can_invite() {
self.add_invitee(item);
} else {
self.remove_invitee(item.user().user_id());
if !self.has_invitees() {
self.obj().notify_has_invitees();
}
}
/// Handle when the given item was removed from the list of invitees.
fn handle_removed_invitee(&self, item: &InviteItem) {
item.set_is_invitee(false);
self.obj().emit_by_name::<()>("invitee-removed", &[&item]);
}
}
}
/// Return the invitee with the given user ID, if any.
pub fn invitee(&self, user_id: &UserId) -> Option<InviteItem> {
self.imp().invitee_list.borrow().get(user_id).cloned()
glib::wrapper! {
/// List of users after a search in the user directory.
///
/// This also manages invitees.
pub struct InviteList(ObjectSubclass<imp::InviteList>)
@implements gio::ListModel;
}
impl InviteList {
pub fn new(room: &Room) -> Self {
glib::Object::builder().property("room", room).build()
}
/// Return the first invitee in the list, if any.
pub fn first_invitee(&self) -> Option<InviteItem> {
pub(crate) fn first_invitee(&self) -> Option<InviteItem> {
self.imp().invitee_list.borrow().values().next().cloned()
}
/// Add the given item as an invitee.
fn add_invitee(&self, item: &InviteItem) {
let had_invitees = self.has_invitees();
item.set_is_invitee(true);
self.imp()
.invitee_list
.borrow_mut()
.insert(item.user().user_id().clone(), item.clone());
self.emit_by_name::<()>("invitee-added", &[&item]);
if !had_invitees {
self.notify_has_invitees();
}
}
/// Get the number of invitees.
pub fn n_invitees(&self) -> usize {
pub(crate) fn n_invitees(&self) -> usize {
self.imp().invitee_list.borrow().len()
}
/// Get the list of user IDs of the invitees.
pub fn invitees_ids(&self) -> Vec<OwnedUserId> {
pub(crate) fn invitees_ids(&self) -> Vec<OwnedUserId> {
self.imp().invitee_list.borrow().keys().cloned().collect()
}
/// Update the list of invitees so only the invitees with the given user IDs
/// remain.
pub fn retain_invitees(&self, invitees_ids: &[&UserId]) {
if !self.has_invitees() {
// Nothing to do.
return;
}
let invitee_list = self.imp().invitee_list.take();
let (invitee_list, removed_invitees) = invitee_list
.into_iter()
.partition(|(key, _)| invitees_ids.contains(&key.as_ref()));
self.imp().invitee_list.replace(invitee_list);
for item in removed_invitees.values() {
self.handle_removed_invitee(item);
}
if !self.has_invitees() {
self.notify_has_invitees();
}
pub(crate) fn retain_invitees(&self, invitees_ids: &[&UserId]) {
self.imp().retain_invitees(invitees_ids);
}
/// Remove the invitee with the given user ID from the list.
pub fn remove_invitee(&self, user_id: &UserId) {
let Some(item) = self.imp().invitee_list.borrow_mut().remove(user_id) else {
return;
};
self.handle_removed_invitee(&item);
if !self.has_invitees() {
self.notify_has_invitees();
}
}
/// Handle when the given item was removed from the list of invitees.
fn handle_removed_invitee(&self, item: &InviteItem) {
item.set_is_invitee(false);
self.emit_by_name::<()>("invitee-removed", &[&item]);
pub(crate) fn remove_invitee(&self, user_id: &UserId) {
self.imp().remove_invitee(user_id);
}
/// Connect to the signal emitted when an invitee is added.

204
src/session/view/content/room_details/invite_subpage/mod.rs

@ -33,29 +33,29 @@ mod imp {
#[properties(wrapper_type = super::InviteSubpage)]
pub struct InviteSubpage {
#[template_child]
pub search_entry: TemplateChild<PillSearchEntry>,
search_entry: TemplateChild<PillSearchEntry>,
#[template_child]
pub list_view: TemplateChild<gtk::ListView>,
list_view: TemplateChild<gtk::ListView>,
#[template_child]
pub invite_button: TemplateChild<LoadingButton>,
invite_button: TemplateChild<LoadingButton>,
#[template_child]
pub cancel_button: TemplateChild<gtk::Button>,
cancel_button: TemplateChild<gtk::Button>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
stack: TemplateChild<gtk::Stack>,
#[template_child]
pub matching_page: TemplateChild<gtk::ScrolledWindow>,
matching_page: TemplateChild<gtk::ScrolledWindow>,
#[template_child]
pub no_matching_page: TemplateChild<adw::StatusPage>,
no_matching_page: TemplateChild<adw::StatusPage>,
#[template_child]
pub no_search_page: TemplateChild<adw::StatusPage>,
no_search_page: TemplateChild<adw::StatusPage>,
#[template_child]
pub error_page: TemplateChild<adw::StatusPage>,
error_page: TemplateChild<adw::StatusPage>,
/// The room users will be invited to.
#[property(get, set = Self::set_room, construct_only)]
pub room: glib::WeakRef<Room>,
room: glib::WeakRef<Room>,
/// The list managing the invited users.
#[property(get)]
pub invite_list: OnceCell<InviteList>,
invite_list: OnceCell<InviteList>,
}
#[glib::object_subclass]
@ -68,10 +68,10 @@ mod imp {
InviteRow::ensure_type();
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
Self::bind_template_callbacks(klass);
klass.add_binding(gdk::Key::Escape, gdk::ModifierType::empty(), |obj| {
obj.close();
obj.imp().close();
glib::Propagation::Stop
});
}
@ -87,6 +87,7 @@ mod imp {
impl WidgetImpl for InviteSubpage {}
impl NavigationPageImpl for InviteSubpage {}
#[gtk::template_callbacks]
impl InviteSubpage {
/// Set the room users will be invited to.
fn set_room(&self, room: &Room) {
@ -132,13 +133,16 @@ mod imp {
self.obj().notify_room();
}
/// The list managing the invited users.
fn invite_list(&self) -> &InviteList {
self.invite_list
.get()
.expect("invite list should be initialized")
}
/// Update the view for the current state of the list.
fn update_view(&self) {
let state = self
.invite_list
.get()
.expect("Can't update view without an InviteeList")
.state();
let state = self.invite_list().state();
let page = match state {
InviteListState::Initial => "no-search",
@ -150,102 +154,104 @@ mod imp {
self.stack.set_visible_child_name(page);
}
}
}
glib::wrapper! {
/// Subpage to invite new members to a room.
pub struct InviteSubpage(ObjectSubclass<imp::InviteSubpage>)
@extends gtk::Widget, gtk::Window, adw::NavigationPage, @implements gtk::Accessible;
}
#[gtk::template_callbacks]
impl InviteSubpage {
/// Construct a new `InviteSubpage` with the given room.
pub fn new(room: &Room) -> Self {
glib::Object::builder().property("room", room).build()
}
/// Close this subpage.
#[template_callback]
fn close(&self) {
let obj = self.obj();
let Some(window) = obj.root().and_downcast::<adw::PreferencesWindow>() else {
return;
};
/// Close this subpage.
#[template_callback]
fn close(&self) {
let window = self
.root()
.and_downcast::<adw::PreferencesWindow>()
.unwrap();
if self.can_pop() {
window.pop_subpage();
} else {
window.close();
if obj.can_pop() {
window.pop_subpage();
} else {
window.close();
}
}
}
/// Toggle the invited state of the item at the given index.
#[template_callback]
fn toggle_item_is_invitee(&self, index: u32) {
let Some(item) = self.invite_list().item(index).and_downcast::<InviteItem>() else {
return;
};
item.set_is_invitee(!item.is_invitee());
}
/// Toggle the invited state of the item at the given index.
#[template_callback]
fn toggle_item_is_invitee(&self, index: u32) {
let Some(item) = self.invite_list().item(index).and_downcast::<InviteItem>() else {
return;
};
/// Uninvite the user from the given pill source.
#[template_callback]
fn remove_pill_invitee(&self, source: PillSource) {
if let Ok(user) = source.downcast::<User>() {
self.invite_list().remove_invitee(user.user_id());
item.set_is_invitee(!item.is_invitee());
}
}
/// Invite the selected users to the room.
#[template_callback]
async fn invite(&self) {
let Some(room) = self.room() else {
return;
};
/// Uninvite the user from the given pill source.
#[template_callback]
fn remove_pill_invitee(&self, source: PillSource) {
if let Ok(user) = source.downcast::<User>() {
self.invite_list().remove_invitee(user.user_id());
}
}
self.imp().invite_button.set_is_loading(true);
/// Invite the selected users to the room.
#[template_callback]
async fn invite(&self) {
let Some(room) = self.room.upgrade() else {
return;
};
let invite_list = self.invite_list();
let invitees = invite_list.invitees_ids();
self.invite_button.set_is_loading(true);
match room.invite(&invitees).await {
Ok(()) => {
self.close();
}
Err(failed_users) => {
invite_list.retain_invitees(&failed_users);
let n_failed = failed_users.len();
let n = invite_list.n_invitees();
if n != n_failed {
// This should not be possible.
error!("The number of failed users does not match the number of remaining invitees: expected {n_failed}, got {n}");
}
let invite_list = self.invite_list();
let invitees = invite_list.invitees_ids();
if n == 0 {
match room.invite(&invitees).await {
Ok(()) => {
self.close();
} else {
let first_failed = invite_list.first_invitee().map(|item| item.user()).unwrap();
toast!(
self,
ngettext(
// Translators: Do NOT translate the content between '{' and '}', these
// are variable names.
"Could not invite {user} to {room}",
"Could not invite {n} users to {room}",
n as u32,
),
@user = first_failed,
@room,
n = n.to_string(),
);
}
Err(failed_users) => {
invite_list.retain_invitees(&failed_users);
let n_failed = failed_users.len();
let n = invite_list.n_invitees();
if n != n_failed {
// This should not be possible.
error!("The number of failed users does not match the number of remaining invitees: expected {n_failed}, got {n}");
}
if n == 0 {
self.close();
} else {
let first_failed =
invite_list.first_invitee().map(|item| item.user()).unwrap();
let obj = self.obj();
toast!(
obj,
ngettext(
// Translators: Do NOT translate the content between '{' and '}', these
// are variable names.
"Could not invite {user} to {room}",
"Could not invite {n} users to {room}",
n as u32,
),
@user = first_failed,
@room,
n = n.to_string(),
);
}
}
}
self.invite_button.set_is_loading(false);
}
}
}
self.imp().invite_button.set_is_loading(false);
glib::wrapper! {
/// Subpage to invite new members to a room.
pub struct InviteSubpage(ObjectSubclass<imp::InviteSubpage>)
@extends gtk::Widget, gtk::Window, adw::NavigationPage, @implements gtk::Accessible;
}
impl InviteSubpage {
/// Construct a new `InviteSubpage` with the given room.
pub fn new(room: &Room) -> Self {
glib::Object::builder().property("room", room).build()
}
}

8
src/session/view/content/room_details/invite_subpage/row.rs

@ -17,12 +17,12 @@ mod imp {
)]
#[properties(wrapper_type = super::InviteRow)]
pub struct InviteRow {
#[template_child]
check_button: TemplateChild<gtk::CheckButton>,
/// The item displayed by this row.
#[property(get, set = Self::set_item, explicit_notify, nullable)]
pub item: RefCell<Option<InviteItem>>,
pub binding: RefCell<Option<glib::Binding>>,
#[template_child]
pub check_button: TemplateChild<gtk::CheckButton>,
item: RefCell<Option<InviteItem>>,
binding: RefCell<Option<glib::Binding>>,
}
#[glib::object_subclass]

4
src/session/view/content/room_details/mod.rs

@ -37,7 +37,7 @@ use crate::{components::UserPage, session::model::Room, toast};
/// The possible subpages of the room details.
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Variant)]
pub enum SubpageName {
pub(crate) enum SubpageName {
/// The page to edit the name, topic and avatar of the room.
EditDetails,
/// The list of members of the room.
@ -241,7 +241,7 @@ impl RoomDetails {
}
/// Show the given subpage as the initial page.
pub fn show_initial_subpage(&self, name: SubpageName) {
pub(crate) fn show_initial_subpage(&self, name: SubpageName) {
self.imp().show_subpage(name, true);
}
}

84
src/session/view/content/room_details/permissions/add_members_subpage.rs

@ -29,25 +29,25 @@ mod imp {
#[properties(wrapper_type = super::PermissionsAddMembersSubpage)]
pub struct PermissionsAddMembersSubpage {
#[template_child]
pub search_entry: TemplateChild<PillSearchEntry>,
search_entry: TemplateChild<PillSearchEntry>,
#[template_child]
pub power_level_combo: TemplateChild<PowerLevelSelectionComboBox>,
power_level_combo: TemplateChild<PowerLevelSelectionComboBox>,
#[template_child]
pub list_view: TemplateChild<gtk::ListView>,
list_view: TemplateChild<gtk::ListView>,
#[template_child]
pub add_button: TemplateChild<gtk::Button>,
add_button: TemplateChild<gtk::Button>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
stack: TemplateChild<gtk::Stack>,
/// The permissions of the room.
#[property(get, set = Self::set_permissions, explicit_notify, nullable)]
pub permissions: glib::WeakRef<Permissions>,
permissions: glib::WeakRef<Permissions>,
power_level_filter: gtk::CustomFilter,
filtered_model: gtk::FilterListModel,
/// The list of members with custom power levels.
#[property(get, set = Self::set_privileged_members, explicit_notify, nullable)]
pub privileged_members: glib::WeakRef<PrivilegedMembers>,
privileged_members: glib::WeakRef<PrivilegedMembers>,
/// The selected members in the list.
pub selected_members: RefCell<HashMap<OwnedUserId, Member>>,
selected_members: RefCell<HashMap<OwnedUserId, Member>>,
}
#[glib::object_subclass]
@ -58,7 +58,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
Self::bind_template_callbacks(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
@ -197,6 +197,7 @@ mod imp {
impl WidgetImpl for PermissionsAddMembersSubpage {}
impl NavigationPageImpl for PermissionsAddMembersSubpage {}
#[gtk::template_callbacks]
impl PermissionsAddMembersSubpage {
/// Set the permissions of the room.
fn set_permissions(&self, permissions: Option<&Permissions>) {
@ -297,6 +298,38 @@ mod imp {
self.obj().emit_by_name::<()>("selection-changed", &[]);
}
}
/// Add the selected members to the list of members with custom power
/// levels.
#[template_callback]
fn add_members(&self) {
let Some(permissions) = self.permissions.upgrade() else {
return;
};
let Some(privileged_members) = self.privileged_members.upgrade() else {
return;
};
let power_level = self.power_level_combo.selected_power_level();
let members = self
.selected_members
.take()
.into_iter()
.map(|(user_id, member)| {
let member = MemberPowerLevel::new(&member, &permissions);
member.set_power_level(power_level);
(user_id, member)
});
privileged_members.add_members(members);
let obj = self.obj();
let _ = obj.activate_action("navigation.pop", None);
self.search_entry.clear();
self.add_button.set_sensitive(false);
obj.emit_by_name::<()>("selection-changed", &[]);
}
}
}
@ -306,44 +339,11 @@ glib::wrapper! {
@extends gtk::Widget, gtk::Window, adw::NavigationPage, @implements gtk::Accessible;
}
#[gtk::template_callbacks]
impl PermissionsAddMembersSubpage {
pub fn new() -> Self {
glib::Object::new()
}
/// Add the selected members to the list of members with custom power
/// levels.
#[template_callback]
fn add_members(&self) {
let Some(permissions) = self.permissions() else {
return;
};
let Some(privileged_members) = self.privileged_members() else {
return;
};
let imp = self.imp();
let power_level = imp.power_level_combo.selected_power_level();
let members = imp
.selected_members
.take()
.into_iter()
.map(|(user_id, member)| {
let member = MemberPowerLevel::new(&member, &permissions);
member.set_power_level(power_level);
(user_id, member)
});
privileged_members.add_members(members);
self.activate_action("navigation.pop", None).unwrap();
imp.search_entry.clear();
imp.add_button.set_sensitive(false);
self.emit_by_name::<()>("selection-changed", &[]);
}
/// Connect to the signal emitted when the selection changes.
pub fn connect_selection_changed<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_closure(

12
src/session/view/content/room_details/permissions/member_power_level.rs

@ -18,22 +18,22 @@ mod imp {
pub struct MemberPowerLevel {
/// The permissions to watch.
#[property(get, set = Self::set_permissions, construct_only)]
pub permissions: BoundObjectWeakRef<Permissions>,
permissions: BoundObjectWeakRef<Permissions>,
/// The room member or remote user.
#[property(get, construct_only)]
pub user: OnceCell<User>,
user: OnceCell<User>,
/// The wanted power level of the member.
///
/// 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)]
pub power_level: Cell<PowerLevel>,
power_level: Cell<PowerLevel>,
/// The wanted role of the member.
#[property(get, builder(MemberRole::default()))]
pub role: Cell<MemberRole>,
role: Cell<MemberRole>,
/// Whether this member's power level can be edited.
#[property(get)]
pub editable: Cell<bool>,
editable: Cell<bool>,
}
#[glib::object_subclass]
@ -150,7 +150,7 @@ impl MemberPowerLevel {
///
/// Returns `None` if the permissions could not be upgraded, or if the power
/// level is the users default.
pub fn to_parts(&self) -> Option<(OwnedUserId, Int)> {
pub(crate) fn to_parts(&self) -> Option<(OwnedUserId, Int)> {
let permissions = self.permissions()?;
let users_default = permissions.default_power_level();

2
src/session/view/content/room_details/permissions/mod.rs

@ -6,7 +6,7 @@ mod permissions_subpage;
mod privileged_members;
mod select_member_row;
pub use self::{
pub(crate) use self::{
add_members_subpage::PermissionsAddMembersSubpage, member_power_level::MemberPowerLevel,
member_row::PermissionsMemberRow, members_subpage::PermissionsMembersSubpage,
permissions_subpage::PermissionsSubpage, privileged_members::PrivilegedMembers,

575
src/session/view/content/room_details/permissions/permissions_subpage.rs

@ -31,74 +31,74 @@ mod imp {
#[properties(wrapper_type = super::PermissionsSubpage)]
pub struct PermissionsSubpage {
#[template_child]
pub save_button: TemplateChild<LoadingButton>,
save_button: TemplateChild<LoadingButton>,
#[template_child]
pub messages_row: TemplateChild<PowerLevelSelectionRow>,
messages_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub redact_own_row: TemplateChild<PowerLevelSelectionRow>,
redact_own_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub redact_others_row: TemplateChild<PowerLevelSelectionRow>,
redact_others_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub notify_room_row: TemplateChild<PowerLevelSelectionRow>,
notify_room_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub state_row: TemplateChild<PowerLevelSelectionRow>,
state_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub name_row: TemplateChild<PowerLevelSelectionRow>,
name_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub topic_row: TemplateChild<PowerLevelSelectionRow>,
topic_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub avatar_row: TemplateChild<PowerLevelSelectionRow>,
avatar_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub aliases_row: TemplateChild<PowerLevelSelectionRow>,
aliases_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub history_visibility_row: TemplateChild<PowerLevelSelectionRow>,
history_visibility_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub encryption_row: TemplateChild<PowerLevelSelectionRow>,
encryption_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub power_levels_row: TemplateChild<PowerLevelSelectionRow>,
power_levels_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub server_acl_row: TemplateChild<PowerLevelSelectionRow>,
server_acl_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub upgrade_row: TemplateChild<PowerLevelSelectionRow>,
upgrade_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub invite_row: TemplateChild<PowerLevelSelectionRow>,
invite_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub kick_row: TemplateChild<PowerLevelSelectionRow>,
kick_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub ban_row: TemplateChild<PowerLevelSelectionRow>,
ban_row: TemplateChild<PowerLevelSelectionRow>,
#[template_child]
pub members_default_spin_row: TemplateChild<adw::SpinRow>,
members_default_spin_row: TemplateChild<adw::SpinRow>,
#[template_child]
pub members_default_adjustment: TemplateChild<gtk::Adjustment>,
members_default_adjustment: TemplateChild<gtk::Adjustment>,
#[template_child]
pub members_default_text_row: TemplateChild<adw::ActionRow>,
members_default_text_row: TemplateChild<adw::ActionRow>,
#[template_child]
pub members_default_label: TemplateChild<gtk::Label>,
members_default_label: TemplateChild<gtk::Label>,
#[template_child]
pub members_privileged_button: TemplateChild<ButtonCountRow>,
members_privileged_button: TemplateChild<ButtonCountRow>,
/// The subpage to view and edit members with custom power levels.
#[template_child]
pub members_subpage: TemplateChild<PermissionsMembersSubpage>,
members_subpage: TemplateChild<PermissionsMembersSubpage>,
/// The subpage to add members with custom power levels.
#[template_child]
pub add_members_subpage: TemplateChild<PermissionsAddMembersSubpage>,
add_members_subpage: TemplateChild<PermissionsAddMembersSubpage>,
/// The permissions to watch.
#[property(get, set = Self::set_permissions, construct_only)]
pub permissions: BoundObjectWeakRef<Permissions>,
permissions: BoundObjectWeakRef<Permissions>,
/// Whether our own user can change the power levels in this room.
#[property(get)]
pub editable: Cell<bool>,
editable: Cell<bool>,
/// Whether the permissions were changed by the user.
#[property(get)]
pub changed: Cell<bool>,
changed: Cell<bool>,
/// The list of members with custom power levels.
#[property(get)]
pub privileged_members: OnceCell<PrivilegedMembers>,
privileged_members: OnceCell<PrivilegedMembers>,
/// Whether an update is in progress.
///
/// Avoids to call `Self::update_changed()` too often when several rows
/// might be changed at once.
pub update_in_progress: Cell<bool>,
update_in_progress: Cell<bool>,
}
#[glib::object_subclass]
@ -109,7 +109,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
Self::bind_template_callbacks(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
@ -123,6 +123,7 @@ mod imp {
impl WidgetImpl for PermissionsSubpage {}
impl NavigationPageImpl for PermissionsSubpage {}
#[gtk::template_callbacks]
impl PermissionsSubpage {
/// Set the permissions to watch.
fn set_permissions(&self, permissions: &Permissions) {
@ -159,8 +160,15 @@ mod imp {
self.update();
}
/// The list of members with custom power levels.
fn privileged_members(&self) -> &PrivilegedMembers {
self.privileged_members
.get()
.expect("privileged members should be initialized")
}
/// Update all the permissions.
pub(super) fn update(&self) {
fn update(&self) {
let Some(permissions) = self.permissions.obj() else {
return;
};
@ -192,7 +200,7 @@ mod imp {
}
/// Update whether the permissions were changed by the user.
pub(super) fn update_changed(&self) {
fn update_changed(&self) {
if self.update_in_progress.get() {
// Do not update, it will be called when all updates are done.
return;
@ -210,7 +218,7 @@ mod imp {
/// Compute whether the user changed the permissions.
#[allow(clippy::too_many_lines)]
pub(super) fn compute_changed(&self) -> bool {
fn compute_changed(&self) -> bool {
let Some(privileged_members) = self.privileged_members.get() else {
return false;
};
@ -388,7 +396,7 @@ mod imp {
}
/// Update the rows about state events, except the default one.
pub(super) fn update_state_rows(&self) {
fn update_state_rows(&self) {
let Some(permissions) = self.permissions.obj() else {
return;
};
@ -519,293 +527,288 @@ mod imp {
self.members_privileged_button
.set_count(power_levels.users.len().to_string());
}
}
}
glib::wrapper! {
/// Subpage to view and change the permissions of a room.
pub struct PermissionsSubpage(ObjectSubclass<imp::PermissionsSubpage>)
@extends gtk::Widget, gtk::Window, adw::NavigationPage, @implements gtk::Accessible;
}
/// Go back to the previous page in the room details.
///
/// If there are changes in the page, ask the user to confirm.
#[template_callback]
async fn go_back(&self) {
let obj = self.obj();
let mut reset_after = false;
if self.changed.get() {
let title = gettext("Save Changes?");
let description = gettext(
"This page contains unsaved changes. Changes which are not saved will be lost.",
);
let dialog = adw::AlertDialog::builder()
.title(title)
.body(description)
.default_response("cancel")
.build();
dialog.add_responses(&[
("cancel", &gettext("Cancel")),
("discard", &gettext("Discard")),
("save", &gettext("Save")),
]);
dialog.set_response_appearance("discard", adw::ResponseAppearance::Destructive);
dialog.set_response_appearance("save", adw::ResponseAppearance::Suggested);
match dialog.choose_future(&*obj).await.as_str() {
"discard" => {
reset_after = true;
}
"save" => {
self.save().await;
}
_ => {
return;
}
}
}
#[gtk::template_callbacks]
impl PermissionsSubpage {
pub fn new(permissions: &Permissions) -> Self {
glib::Object::builder()
.property("permissions", permissions)
.build()
}
let _ = obj.activate_action("navigation.pop", None);
/// Go back to the previous page in the room details.
///
/// If there are changes in the page, ask the user to confirm.
#[template_callback]
async fn go_back(&self) {
let mut reset_after = false;
if self.changed() {
let title = gettext("Save Changes?");
let description = gettext(
"This page contains unsaved changes. Changes which are not saved will be lost.",
);
let dialog = adw::AlertDialog::builder()
.title(title)
.body(description)
.default_response("cancel")
.build();
dialog.add_responses(&[
("cancel", &gettext("Cancel")),
("discard", &gettext("Discard")),
("save", &gettext("Save")),
]);
dialog.set_response_appearance("discard", adw::ResponseAppearance::Destructive);
dialog.set_response_appearance("save", adw::ResponseAppearance::Suggested);
match dialog.choose_future(self).await.as_str() {
"discard" => {
reset_after = true;
}
"save" => {
self.save().await;
}
_ => {
return;
}
if reset_after {
self.update();
}
}
self.activate_action("navigation.pop", None).unwrap();
/// Save the changes of this page.
#[template_callback]
async fn save(&self) {
if !self.compute_changed() {
return;
}
if reset_after {
self.imp().update();
}
}
let Some(permissions) = self.permissions.obj() else {
return;
};
self.save_button.set_is_loading(true);
let Some(power_levels) = self.collect_power_levels() else {
return;
};
/// Save the changes of this page.
#[template_callback]
async fn save(&self) {
if !self.imp().compute_changed() {
return;
if permissions.set_power_levels(power_levels).await.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not save permissions"));
self.save_button.set_is_loading(false);
}
}
let Some(permissions) = self.permissions() else {
return;
};
let imp = self.imp();
/// Collect the current power levels.
///
/// Returns `None` if the permissions could not be upgraded.
fn collect_power_levels(&self) -> Option<RoomPowerLevels> {
let permissions = self.permissions.obj()?;
imp.save_button.set_is_loading(true);
let mut power_levels = permissions.power_levels();
let Some(power_levels) = self.collect_power_levels() else {
return;
};
let events_default = self.messages_row.selected_power_level();
power_levels.events_default = Int::new_saturating(events_default);
if permissions.set_power_levels(power_levels).await.is_err() {
toast!(self, gettext("Could not save permissions"));
imp.save_button.set_is_loading(false);
}
}
let mut redact_own = self.redact_own_row.selected_power_level();
let redact_others = self.redact_others_row.selected_power_level();
/// Collect the current power levels.
///
/// Returns `None` if the permissions could not be upgraded.
fn collect_power_levels(&self) -> Option<RoomPowerLevels> {
let permissions = self.permissions()?;
let imp = self.imp();
let mut power_levels = permissions.power_levels();
let events_default = imp.messages_row.selected_power_level();
power_levels.events_default = Int::new_saturating(events_default);
let mut redact_own = imp.redact_own_row.selected_power_level();
let redact_others = imp.redact_others_row.selected_power_level();
// redact_own cannot be higher than redact_others because redact_others depends
// also on redact_own.
redact_own = redact_own.min(redact_others);
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomRedaction,
redact_own,
events_default,
);
power_levels.redact = Int::new_saturating(redact_others);
let notify_room = imp.notify_room_row.selected_power_level();
power_levels.notifications.room = Int::new_saturating(notify_room);
let state_default = imp.state_row.selected_power_level();
power_levels.state_default = Int::new_saturating(state_default);
let name = imp.name_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomName,
name,
state_default,
);
let topic = imp.topic_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomTopic,
topic,
state_default,
);
let avatar = imp.avatar_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomAvatar,
avatar,
state_default,
);
let aliases = imp.aliases_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomCanonicalAlias,
aliases,
state_default,
);
let history_visibility = imp.history_visibility_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomHistoryVisibility,
history_visibility,
state_default,
);
let encryption = imp.encryption_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomEncryption,
encryption,
state_default,
);
let pl = imp.power_levels_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomPowerLevels,
pl,
state_default,
);
let server_acl = imp.server_acl_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomServerAcl,
server_acl,
state_default,
);
let upgrade = imp.upgrade_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomTombstone,
upgrade,
state_default,
);
let invite = imp.invite_row.selected_power_level();
power_levels.invite = Int::new_saturating(invite);
let kick = imp.kick_row.selected_power_level();
power_levels.kick = Int::new_saturating(kick);
let ban = imp.ban_row.selected_power_level();
power_levels.ban = Int::new_saturating(ban);
let default_pl = imp.members_default_adjustment.value() as PowerLevel;
power_levels.users_default = Int::new_saturating(default_pl);
let privileged_members = self.privileged_members();
power_levels.users = privileged_members.collect();
Some(power_levels)
}
// redact_own cannot be higher than redact_others because redact_others depends
// also on redact_own.
redact_own = redact_own.min(redact_others);
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomRedaction,
redact_own,
events_default,
);
/// Handle when a value in the page has changed.
#[template_callback]
fn value_changed(&self) {
let imp = self.imp();
if imp.update_in_progress.get() {
// No need to run checks.
return;
}
power_levels.redact = Int::new_saturating(redact_others);
imp.update_changed();
}
let notify_room = self.notify_room_row.selected_power_level();
power_levels.notifications.room = Int::new_saturating(notify_room);
/// Handle when the redact_own row has changed.
#[template_callback]
fn redact_own_changed(&self) {
let imp = self.imp();
if imp.update_in_progress.get() {
// No need to run checks.
return;
}
let state_default = self.state_row.selected_power_level();
power_levels.state_default = Int::new_saturating(state_default);
let name = self.name_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomName,
name,
state_default,
);
let topic = self.topic_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomTopic,
topic,
state_default,
);
let avatar = self.avatar_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomAvatar,
avatar,
state_default,
);
let aliases = self.aliases_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomCanonicalAlias,
aliases,
state_default,
);
let history_visibility = self.history_visibility_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomHistoryVisibility,
history_visibility,
state_default,
);
let encryption = self.encryption_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomEncryption,
encryption,
state_default,
);
let pl = self.power_levels_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomPowerLevels,
pl,
state_default,
);
let server_acl = self.server_acl_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomServerAcl,
server_acl,
state_default,
);
let upgrade = self.upgrade_row.selected_power_level();
set_event_power_level(
&mut power_levels,
TimelineEventType::RoomTombstone,
upgrade,
state_default,
);
let invite = self.invite_row.selected_power_level();
power_levels.invite = Int::new_saturating(invite);
let kick = self.kick_row.selected_power_level();
power_levels.kick = Int::new_saturating(kick);
let redact_own = imp.redact_own_row.selected_power_level();
let redact_others = imp.redact_others_row.selected_power_level();
let ban = self.ban_row.selected_power_level();
power_levels.ban = Int::new_saturating(ban);
// redact_own cannot be higher than redact_others because redact_others depends
// also on redact_own.
if redact_others < redact_own {
imp.update_in_progress.set(true);
let default_pl = self.members_default_adjustment.value() as PowerLevel;
power_levels.users_default = Int::new_saturating(default_pl);
imp.redact_others_row.set_selected_power_level(redact_own);
let privileged_members = self.privileged_members();
power_levels.users = privileged_members.collect();
imp.update_in_progress.set(false);
Some(power_levels)
}
imp.update_changed();
}
/// Handle when a value in the page has changed.
#[template_callback]
fn value_changed(&self) {
if self.update_in_progress.get() {
// No need to run checks.
return;
}
/// Handle when the redact_others row has changed.
#[template_callback]
fn redact_others_changed(&self) {
let imp = self.imp();
if imp.update_in_progress.get() {
// No need to run checks.
return;
self.update_changed();
}
let redact_own = imp.redact_own_row.selected_power_level();
let redact_others = imp.redact_others_row.selected_power_level();
/// Handle when the redact_own row has changed.
#[template_callback]
fn redact_own_changed(&self) {
if self.update_in_progress.get() {
// No need to run checks.
return;
}
// redact_own cannot be higher than redact_others because redact_others depends
// also on redact_own.
if redact_others < redact_own {
imp.update_in_progress.set(true);
let redact_own = self.redact_own_row.selected_power_level();
let redact_others = self.redact_others_row.selected_power_level();
imp.redact_own_row.set_selected_power_level(redact_others);
// redact_own cannot be higher than redact_others because redact_others depends
// also on redact_own.
if redact_others < redact_own {
self.update_in_progress.set(true);
imp.update_in_progress.set(false);
self.redact_others_row.set_selected_power_level(redact_own);
self.update_in_progress.set(false);
}
self.update_changed();
}
imp.update_changed();
}
/// Handle when the redact_others row has changed.
#[template_callback]
fn redact_others_changed(&self) {
if self.update_in_progress.get() {
// No need to run checks.
return;
}
let redact_own = self.redact_own_row.selected_power_level();
let redact_others = self.redact_others_row.selected_power_level();
// redact_own cannot be higher than redact_others because redact_others depends
// also on redact_own.
if redact_others < redact_own {
self.update_in_progress.set(true);
self.redact_own_row.set_selected_power_level(redact_others);
/// Handle when the state default has changed.
#[template_callback]
fn state_default_changed(&self) {
let imp = self.imp();
if imp.update_in_progress.get() {
// No need to run checks.
return;
self.update_in_progress.set(false);
}
self.update_changed();
}
imp.update_in_progress.set(true);
/// Handle when the state default has changed.
#[template_callback]
fn state_default_changed(&self) {
if self.update_in_progress.get() {
// No need to run checks.
return;
}
self.update_in_progress.set(true);
self.update_state_rows();
self.update_in_progress.set(false);
self.update_changed();
}
}
}
imp.update_state_rows();
glib::wrapper! {
/// Subpage to view and change the permissions of a room.
pub struct PermissionsSubpage(ObjectSubclass<imp::PermissionsSubpage>)
@extends gtk::Widget, gtk::Window, adw::NavigationPage, @implements gtk::Accessible;
}
imp.update_in_progress.set(false);
imp.update_changed();
impl PermissionsSubpage {
pub fn new(permissions: &Permissions) -> Self {
glib::Object::builder()
.property("permissions", permissions)
.build()
}
}

10
src/session/view/content/room_details/permissions/privileged_members.rs

@ -21,13 +21,13 @@ mod imp {
#[properties(wrapper_type = super::PrivilegedMembers)]
pub struct PrivilegedMembers {
/// The list of members.
pub list: RefCell<IndexMap<OwnedUserId, MemberPowerLevel>>,
pub(super) list: RefCell<IndexMap<OwnedUserId, MemberPowerLevel>>,
/// The permissions to watch.
#[property(get, set = Self::set_permissions, construct_only)]
pub permissions: BoundObjectWeakRef<Permissions>,
permissions: BoundObjectWeakRef<Permissions>,
/// Whether this list has changed.
#[property(get)]
pub changed: Cell<bool>,
changed: Cell<bool>,
}
#[glib::object_subclass]
@ -219,7 +219,7 @@ impl PrivilegedMembers {
}
/// Add the given members to the list.
pub fn add_members(
pub(crate) fn add_members(
&self,
members: impl ExactSizeIterator<Item = (OwnedUserId, MemberPowerLevel)>,
) {
@ -244,7 +244,7 @@ impl PrivilegedMembers {
}
/// Collect the list of members.
pub fn collect(&self) -> BTreeMap<OwnedUserId, Int> {
pub(crate) fn collect(&self) -> BTreeMap<OwnedUserId, Int> {
self.imp()
.list
.borrow()

4
src/session/view/content/room_details/permissions/select_member_row.rs

@ -18,10 +18,10 @@ mod imp {
pub struct PermissionsSelectMemberRow {
/// The room member displayed by this row.
#[property(get, set = Self::set_member, explicit_notify, nullable)]
pub member: RefCell<Option<Member>>,
member: RefCell<Option<Member>>,
/// Whether this row is selected.
#[property(get, set = Self::set_selected, explicit_notify)]
pub selected: Cell<bool>,
selected: Cell<bool>,
}
#[glib::object_subclass]

Loading…
Cancel
Save