diff --git a/src/session/view/content/room_details/addresses_subpage/completion_popover.rs b/src/session/view/content/room_details/addresses_subpage/completion_popover.rs index d59915c4..7425edbe 100644 --- a/src/session/view/content/room_details/addresses_subpage/completion_popover.rs +++ b/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, + list: TemplateChild, /// The parent entry to autocomplete. #[property(get, set = Self::set_entry, explicit_notify, nullable)] - pub entry: BoundObjectWeakRef, + entry: BoundObjectWeakRef, /// The key controller added to the parent entry. entry_controller: RefCell>, entry_binding: RefCell>, @@ -29,13 +29,13 @@ mod imp { /// /// Only supports `GtkStringObject` items. #[property(get, set = Self::set_model, explicit_notify, nullable)] - pub model: RefCell>, + model: RefCell>, /// 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) { @@ -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<>k::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) - @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::() + { + 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::() - { - if item.string() == entry.text() { - if self.is_visible() { - self.popdown(); - } + /// The index of the selected row. + fn selected_row_index(&self) -> Option { + 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) { + 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 { - let imp = self.imp(); + /// The text of the selected row, if any. + fn selected_text(&self) -> Option { + Some( + self.list + .selected_row()? + .child()? + .downcast_ref::()? + .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::().position(|o| { - o.ok() - .and_downcast::() - .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) { - 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::<>k::ListBoxRow>); - } - } + let obj = self.obj(); - /// The text of the selected row, if any. - pub fn selected_text(&self) -> Option { - Some( - self.imp() - .list - .selected_row()? - .child()? - .downcast_ref::()? - .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: >k::ListBoxRow) { + let Some(label) = row.child().and_downcast::() 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: >k::ListBoxRow) { - let Some(label) = row.child().and_downcast::() 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) + @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() } } diff --git a/src/session/view/content/room_details/addresses_subpage/mod.rs b/src/session/view/content/room_details/addresses_subpage/mod.rs index 89f22d19..40a57cf2 100644 --- a/src/session/view/content/room_details/addresses_subpage/mod.rs +++ b/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, + public_addresses_list: TemplateChild, #[template_child] - pub public_addresses_error_revealer: TemplateChild, + public_addresses_error_revealer: TemplateChild, #[template_child] - pub public_addresses_error: TemplateChild, + public_addresses_error: TemplateChild, #[template_child] - pub local_addresses_group: TemplateChild, + local_addresses_group: TemplateChild, #[template_child] - pub local_addresses_list: TemplateChild, + local_addresses_list: TemplateChild, #[template_child] - pub local_addresses_error_revealer: TemplateChild, + local_addresses_error_revealer: TemplateChild, #[template_child] - pub local_addresses_error: TemplateChild, + local_addresses_error: TemplateChild, #[template_child] - pub public_addresses_add_row: TemplateChild, + public_addresses_add_row: TemplateChild, #[template_child] - pub local_addresses_add_row: TemplateChild, + local_addresses_add_row: TemplateChild, /// The room users will be invited to. #[property(get, set = Self::set_room, construct_only)] - pub room: glib::WeakRef, + room: glib::WeakRef, /// The full list of public addresses. - pub public_addresses: OnceCell, + public_addresses: OnceCell, /// The full list of local addresses. - pub local_addresses: gtk::StringList, + local_addresses: gtk::StringList, aliases_changed_handler: RefCell>, - 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) { @@ -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::(); - extra_items.append(&DummyObject::new("add")); + let add_item = SingleItemListModel::new(&DummyObject::new("add")); // Public addresses. let public_items = gio::ListStore::new::(); 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::(); 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::) } @@ -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) - @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::() 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::() { 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::()) { - 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::()) { + 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::() 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::() 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::() { - 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::() { + 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::() 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::() { 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 { - 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 { + 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::() { - let Some(local_address) = local_address.ok().and_downcast::() 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::() { + let Some(local_address) = local_address.ok().and_downcast::() + 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) + @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() } } diff --git a/src/session/view/content/room_details/addresses_subpage/public_address.rs b/src/session/view/content/room_details/addresses_subpage/public_address.rs index 88b2f1e3..541a85d9 100644 --- a/src/session/view/content/room_details/addresses_subpage/public_address.rs +++ b/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, + alias: OnceCell, /// Whether this is the main address. #[property(get, set = Self::set_is_main, explicit_notify)] - pub is_main: Cell, + is_main: Cell, } #[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::() .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() } } diff --git a/src/session/view/content/room_details/edit_details_subpage.rs b/src/session/view/content/room_details/edit_details_subpage.rs index 8fcb5b1e..c74086e5 100644 --- a/src/session/view/content/room_details/edit_details_subpage.rs +++ b/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; diff --git a/src/session/view/content/room_details/general_page.rs b/src/session/view/content/room_details/general_page.rs index 88a3cb0f..34d7a35b 100644 --- a/src/session/view/content/room_details/general_page.rs +++ b/src/session/view/content/room_details/general_page.rs @@ -34,9 +34,7 @@ use crate::{ HistoryVisibilityValue, JoinRuleValue, Member, NotificationsRoomSetting, Room, RoomCategory, }, spawn, spawn_tokio, toast, - utils::{ - expression, matrix::MatrixIdUri, BoundObjectWeakRef, OngoingAsyncAction, TemplateCallbacks, - }, + utils::{expression, matrix::MatrixIdUri, BoundObjectWeakRef, TemplateCallbacks}, Window, }; @@ -57,51 +55,51 @@ mod imp { #[properties(wrapper_type = super::GeneralPage)] pub struct GeneralPage { #[template_child] - pub room_topic: TemplateChild, + room_topic: TemplateChild, #[template_child] - pub edit_details_btn: TemplateChild, + edit_details_btn: TemplateChild, #[template_child] - pub direct_members_group: TemplateChild, + direct_members_group: TemplateChild, #[template_child] - pub direct_members_list: TemplateChild, + direct_members_list: TemplateChild, #[template_child] - pub no_direct_members_label: TemplateChild, + no_direct_members_label: TemplateChild, #[template_child] - pub members_row_group: TemplateChild, + members_row_group: TemplateChild, #[template_child] - pub members_row: TemplateChild, + members_row: TemplateChild, #[template_child] - pub notifications: TemplateChild, + notifications: TemplateChild, #[template_child] - pub notifications_global_row: TemplateChild, + notifications_global_row: TemplateChild, #[template_child] - pub notifications_all_row: TemplateChild, + notifications_all_row: TemplateChild, #[template_child] - pub notifications_mentions_row: TemplateChild, + notifications_mentions_row: TemplateChild, #[template_child] - pub notifications_mute_row: TemplateChild, + notifications_mute_row: TemplateChild, #[template_child] - pub addresses_group: TemplateChild, + addresses_group: TemplateChild, #[template_child] - pub edit_addresses_button: TemplateChild, + edit_addresses_button: TemplateChild, #[template_child] - pub no_addresses_label: TemplateChild, - pub canonical_alias_row: RefCell>, - pub alt_aliases_rows: RefCell>, + no_addresses_label: TemplateChild, + canonical_alias_row: RefCell>, + alt_aliases_rows: RefCell>, #[template_child] - pub join_rule: TemplateChild, + join_rule: TemplateChild, #[template_child] - pub guest_access: TemplateChild, + guest_access: TemplateChild, #[template_child] - pub publish: TemplateChild, + publish: TemplateChild, #[template_child] - pub history_visibility: TemplateChild, + history_visibility: TemplateChild, #[template_child] - pub encryption: TemplateChild, + encryption: TemplateChild, #[template_child] - pub upgrade_button: TemplateChild, + upgrade_button: TemplateChild, #[template_child] - pub room_federated: TemplateChild, + room_federated: TemplateChild, /// The presented room. #[property(get, set = Self::set_room, construct_only)] room: BoundObjectWeakRef, @@ -110,24 +108,21 @@ mod imp { membership_lists: glib::WeakRef, /// The notifications setting for the room. #[property(get = Self::notifications_setting, set = Self::set_notifications_setting, explicit_notify, builder(NotificationsRoomSetting::default()))] - pub notifications_setting: PhantomData, + notifications_setting: PhantomData, /// Whether the notifications section is busy. #[property(get)] - pub notifications_loading: Cell, + notifications_loading: Cell, /// Whether the room is published in the directory. #[property(get)] - pub is_published: Cell, - pub changing_avatar: RefCell>>, - pub changing_name: RefCell>>, - pub changing_topic: RefCell>>, - pub expr_watch: RefCell>, - pub notifications_settings_handlers: RefCell>, - pub membership_handler: RefCell>, - pub permissions_handler: RefCell>, - pub canonical_alias_handler: RefCell>, - pub alt_aliases_handler: RefCell>, - pub join_rule_handler: RefCell>, - pub capabilities: RefCell, + is_published: Cell, + expr_watch: RefCell>, + notifications_settings_handlers: RefCell>, + membership_handler: RefCell>, + permissions_handler: RefCell>, + canonical_alias_handler: RefCell>, + alt_aliases_handler: RefCell>, + join_rule_handler: RefCell>, + capabilities: RefCell, direct_members_list_has_bound_model: Cell, } @@ -141,7 +136,7 @@ mod imp { CopyableRow::ensure_type(); Self::bind_template(klass); - Self::Type::bind_template_callbacks(klass); + Self::bind_template_callbacks(klass); TemplateCallbacks::bind_template_callbacks(klass); klass @@ -195,6 +190,7 @@ mod imp { impl WidgetImpl for GeneralPage {} impl PreferencesPageImpl for GeneralPage {} + #[gtk::template_callbacks] impl GeneralPage { /// Set the presented room. #[allow(clippy::too_many_lines)] @@ -202,27 +198,27 @@ mod imp { let obj = self.obj(); let membership_handler = room.own_member().connect_membership_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_notifications(); + imp.update_notifications(); } )); self.membership_handler.replace(Some(membership_handler)); let permissions_handler = room.permissions().connect_changed(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_upgrade_button(); - obj.update_edit_addresses_button(); - obj.update_join_rule(); - obj.update_guest_access(); - obj.update_history_visibility(); - obj.update_encryption(); + imp.update_upgrade_button(); + imp.update_edit_addresses_button(); + imp.update_join_rule(); + imp.update_guest_access(); + imp.update_history_visibility(); + imp.update_encryption(); spawn!(async move { - obj.update_publish().await; + imp.update_publish().await; }); } )); @@ -230,29 +226,29 @@ mod imp { let aliases = room.aliases(); let canonical_alias_handler = aliases.connect_canonical_alias_string_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_addresses(); + imp.update_addresses(); } )); self.canonical_alias_handler .replace(Some(canonical_alias_handler)); let alt_aliases_handler = aliases.alt_aliases_model().connect_items_changed(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_, _, _, _| { - obj.update_addresses(); + imp.update_addresses(); } )); self.alt_aliases_handler.replace(Some(alt_aliases_handler)); let join_rule_handler = room.join_rule().connect_changed(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_join_rule(); + imp.update_join_rule(); } )); self.join_rule_handler.replace(Some(join_rule_handler)); @@ -280,38 +276,38 @@ mod imp { } )), room.connect_notifications_setting_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_notifications(); + imp.update_notifications(); } )), room.connect_is_tombstoned_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_upgrade_button(); + imp.update_upgrade_button(); } )), room.connect_guests_allowed_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_guest_access(); + imp.update_guest_access(); } )), room.connect_history_visibility_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_history_visibility(); + imp.update_history_visibility(); } )), room.connect_is_encrypted_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_encryption(); + imp.update_encryption(); } )), ]; @@ -323,17 +319,17 @@ mod imp { let settings = session.notifications().settings(); let notifications_settings_handlers = vec![ settings.connect_account_enabled_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_notifications(); + imp.update_notifications(); } )), settings.connect_session_enabled_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_notifications(); + imp.update_notifications(); } )), ]; @@ -344,22 +340,22 @@ mod imp { self.init_edit_details(); self.update_members(); - obj.update_notifications(); - obj.update_edit_addresses_button(); - obj.update_addresses(); - obj.update_federated(); - obj.update_join_rule(); - obj.update_guest_access(); - obj.update_publish_title(); - obj.update_history_visibility(); - obj.update_encryption(); - obj.update_upgrade_button(); + self.update_notifications(); + self.update_edit_addresses_button(); + self.update_addresses(); + self.update_federated(); + self.update_join_rule(); + self.update_guest_access(); + self.update_publish_title(); + self.update_history_visibility(); + self.update_encryption(); + self.update_upgrade_button(); spawn!(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, async move { - obj.update_publish().await; + imp.update_publish().await; } )); @@ -386,7 +382,7 @@ mod imp { return; } - self.obj().notifications_setting_changed(setting); + self.notifications_setting_changed(setting); } /// Fetch the capabilities of the homeserver. @@ -560,638 +556,645 @@ mod imp { watch.unwatch(); } } - } -} -glib::wrapper! { - /// Preference Window to display and update room details. - pub struct GeneralPage(ObjectSubclass) - @extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible; -} - -#[gtk::template_callbacks] -impl GeneralPage { - pub fn new(room: &Room, membership_lists: &MembershipLists) -> Self { - glib::Object::builder() - .property("room", room) - .property("membership-lists", membership_lists) - .build() - } - - /// Unselect the topic of the room. - /// - /// This is to circumvent the default GTK behavior to select all the text - /// when opening the details. - pub fn unselect_topic(&self) { - let imp = self.imp(); + /// Update the section about notifications. + fn update_notifications(&self) { + let Some(room) = self.room.obj() else { + return; + }; - glib::idle_add_local_once(clone!( - #[weak] - imp, - move || { - // Put the cursor at the beginning of the title instead of having the title - // selected, if it is visible. - if imp.room_topic.is_visible() { - imp.room_topic.select_region(0, 0); - } + if !room.is_joined() { + self.notifications.set_visible(false); + return; } - )); - } - - /// Update the section about notifications. - fn update_notifications(&self) { - let Some(room) = self.room() else { - return; - }; - let imp = self.imp(); - - if !room.is_joined() { - imp.notifications.set_visible(false); - return; - } - let Some(session) = room.session() else { - return; - }; - - // Updates the active radio button. - self.notify_notifications_setting(); - - let settings = session.notifications().settings(); - let sensitive = settings.account_enabled() - && settings.session_enabled() - && !self.notifications_loading(); - imp.notifications.set_sensitive(sensitive); - imp.notifications.set_visible(true); - } - - /// Update the loading state in the notifications section. - fn set_notifications_loading(&self, loading: bool, setting: NotificationsRoomSetting) { - let imp = self.imp(); + let Some(session) = room.session() else { + return; + }; - // Only show the spinner on the selected one. - imp.notifications_global_row - .set_is_loading(loading && setting == NotificationsRoomSetting::Global); - imp.notifications_all_row - .set_is_loading(loading && setting == NotificationsRoomSetting::All); - imp.notifications_mentions_row - .set_is_loading(loading && setting == NotificationsRoomSetting::MentionsOnly); - imp.notifications_mute_row - .set_is_loading(loading && setting == NotificationsRoomSetting::Mute); - - self.imp().notifications_loading.set(loading); - self.notify_notifications_loading(); - } + // Updates the active radio button. + self.obj().notify_notifications_setting(); - /// Handle a change of the notifications setting. - fn notifications_setting_changed(&self, setting: NotificationsRoomSetting) { - let Some(room) = self.room() else { - return; - }; - let Some(session) = room.session() else { - return; - }; - let imp = self.imp(); + let settings = session.notifications().settings(); + let sensitive = settings.account_enabled() + && settings.session_enabled() + && !self.notifications_loading.get(); + self.notifications.set_sensitive(sensitive); + self.notifications.set_visible(true); + } - if setting == room.notifications_setting() { - // Nothing to do. - return; + /// Update the loading state in the notifications section. + fn set_notifications_loading(&self, loading: bool, setting: NotificationsRoomSetting) { + // Only show the spinner on the selected one. + self.notifications_global_row + .set_is_loading(loading && setting == NotificationsRoomSetting::Global); + self.notifications_all_row + .set_is_loading(loading && setting == NotificationsRoomSetting::All); + self.notifications_mentions_row + .set_is_loading(loading && setting == NotificationsRoomSetting::MentionsOnly); + self.notifications_mute_row + .set_is_loading(loading && setting == NotificationsRoomSetting::Mute); + + self.notifications_loading.set(loading); + self.obj().notify_notifications_loading(); } - imp.notifications.set_sensitive(false); - self.set_notifications_loading(true, setting); - - let settings = session.notifications().settings(); - spawn!(clone!( - #[weak(rename_to = obj)] - self, - async move { - if settings - .set_per_room_setting(room.room_id().to_owned(), setting) - .await - .is_err() - { - toast!(obj, gettext("Could not change notifications setting")); - } + /// Handle a change of the notifications setting. + fn notifications_setting_changed(&self, setting: NotificationsRoomSetting) { + let Some(room) = self.room.obj() else { + return; + }; + let Some(session) = room.session() else { + return; + }; - obj.set_notifications_loading(false, setting); - obj.update_notifications(); + if setting == room.notifications_setting() { + // Nothing to do. + return; } - )); - } - - /// Update the button to edit addresses. - fn update_edit_addresses_button(&self) { - let Some(room) = self.room() else { - return; - }; - let can_edit = room.is_joined() - && room - .permissions() - .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomPowerLevels)); - self.imp().edit_addresses_button.set_visible(can_edit); - } + self.notifications.set_sensitive(false); + self.set_notifications_loading(true, setting); - /// Update the addresses group. - fn update_addresses(&self) { - let Some(room) = self.room() else { - return; - }; - let imp = self.imp(); - let aliases = room.aliases(); + let settings = session.notifications().settings(); + spawn!(clone!( + #[weak(rename_to = imp)] + self, + async move { + if settings + .set_per_room_setting(room.room_id().to_owned(), setting) + .await + .is_err() + { + let obj = imp.obj(); + toast!(obj, gettext("Could not change notifications setting")); + } - let canonical_alias_string = aliases.canonical_alias_string(); - let has_canonical_alias = canonical_alias_string.is_some(); + imp.set_notifications_loading(false, setting); + imp.update_notifications(); + } + )); + } - if let Some(canonical_alias_string) = canonical_alias_string { - let mut row_borrow = imp.canonical_alias_row.borrow_mut(); - let row = row_borrow.get_or_insert_with(|| { - // We want the main alias always at the top but cannot add a row at the top so - // we have to remove the other rows first. - self.remove_alt_aliases_rows(); + /// Update the button to edit addresses. + fn update_edit_addresses_button(&self) { + let Some(room) = self.room.obj() else { + return; + }; - let row = CopyableRow::new(); - row.set_copy_button_tooltip_text(Some(gettext("Copy address"))); - row.set_toast_text(Some(gettext("Address copied to clipboard"))); - - // Mark the main alias with a tag. - let label = gtk::Label::builder() - .label(gettext("Main Address")) - .ellipsize(pango::EllipsizeMode::End) - .css_classes(["public-address-tag"]) - .valign(gtk::Align::Center) - .build(); - row.update_relation(&[gtk::accessible::Relation::DescribedBy(&[ - label.upcast_ref() - ])]); - row.set_extra_suffix(Some(label)); - - imp.addresses_group.add(&row); - - row - }); - - row.set_title(&canonical_alias_string); - } else if let Some(row) = imp.canonical_alias_row.take() { - imp.addresses_group.remove(&row); + let can_edit = room.is_joined() + && room + .permissions() + .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomPowerLevels)); + self.edit_addresses_button.set_visible(can_edit); } - let alt_aliases = aliases.alt_aliases_model(); - let alt_aliases_count = alt_aliases.n_items() as usize; - if alt_aliases_count == 0 { - self.remove_alt_aliases_rows(); - } else { - let mut rows = imp.alt_aliases_rows.borrow_mut(); + /// Update the addresses group. + fn update_addresses(&self) { + let Some(room) = self.room.obj() else { + return; + }; + let aliases = room.aliases(); - for (pos, alt_alias) in alt_aliases.iter::().enumerate() { - let Some(alt_alias) = alt_alias.ok().and_downcast::() else { - break; - }; + let canonical_alias_string = aliases.canonical_alias_string(); + let has_canonical_alias = canonical_alias_string.is_some(); + + if let Some(canonical_alias_string) = canonical_alias_string { + let mut row_borrow = self.canonical_alias_row.borrow_mut(); + let row = row_borrow.get_or_insert_with(|| { + // We want the main alias always at the top but cannot add a row at the top so + // we have to remove the other rows first. + self.remove_alt_aliases_rows(); - let row = rows.get(pos).cloned().unwrap_or_else(|| { let row = CopyableRow::new(); row.set_copy_button_tooltip_text(Some(gettext("Copy address"))); row.set_toast_text(Some(gettext("Address copied to clipboard"))); - imp.addresses_group.add(&row); - rows.push(row.clone()); + // Mark the main alias with a tag. + let label = gtk::Label::builder() + .label(gettext("Main Address")) + .ellipsize(pango::EllipsizeMode::End) + .css_classes(["public-address-tag"]) + .valign(gtk::Align::Center) + .build(); + row.update_relation(&[gtk::accessible::Relation::DescribedBy(&[ + label.upcast_ref() + ])]); + row.set_extra_suffix(Some(label)); + + self.addresses_group.add(&row); row }); - row.set_title(&alt_alias.string()); + row.set_title(&canonical_alias_string); + } else if let Some(row) = self.canonical_alias_row.take() { + self.addresses_group.remove(&row); } - let rows_count = rows.len(); - if alt_aliases_count < rows_count { - for _ in alt_aliases_count..rows_count { - if let Some(row) = rows.pop() { - imp.addresses_group.remove(&row); + let alt_aliases = aliases.alt_aliases_model(); + let alt_aliases_count = alt_aliases.n_items() as usize; + if alt_aliases_count == 0 { + self.remove_alt_aliases_rows(); + } else { + let mut rows = self.alt_aliases_rows.borrow_mut(); + + for (pos, alt_alias) in alt_aliases.iter::().enumerate() { + let Some(alt_alias) = alt_alias.ok().and_downcast::() else { + break; + }; + + let row = rows.get(pos).cloned().unwrap_or_else(|| { + let row = CopyableRow::new(); + row.set_copy_button_tooltip_text(Some(gettext("Copy address"))); + row.set_toast_text(Some(gettext("Address copied to clipboard"))); + + self.addresses_group.add(&row); + rows.push(row.clone()); + + row + }); + + row.set_title(&alt_alias.string()); + } + + let rows_count = rows.len(); + if alt_aliases_count < rows_count { + for _ in alt_aliases_count..rows_count { + if let Some(row) = rows.pop() { + self.addresses_group.remove(&row); + } } } } + + self.no_addresses_label + .set_visible(!has_canonical_alias && alt_aliases_count == 0); } - imp.no_addresses_label - .set_visible(!has_canonical_alias && alt_aliases_count == 0); - } + fn remove_alt_aliases_rows(&self) { + for row in self.alt_aliases_rows.take() { + self.addresses_group.remove(&row); + } + } - fn remove_alt_aliases_rows(&self) { - let imp = self.imp(); + /// Copy the room's permalink to the clipboard. + #[template_callback] + async fn copy_permalink(&self) { + let Some(room) = self.room.obj() else { + return; + }; - for row in imp.alt_aliases_rows.take() { - imp.addresses_group.remove(&row); - } - } + let permalink = room.matrix_to_uri().await; - /// Copy the room's permalink to the clipboard. - #[template_callback] - async fn copy_permalink(&self) { - let Some(room) = self.room() else { - return; - }; + let obj = self.obj(); + obj.clipboard().set_text(&permalink.to_string()); + toast!(obj, gettext("Room link copied to clipboard")); + } - let permalink = room.matrix_to_uri().await; - self.clipboard().set_text(&permalink.to_string()); - toast!(self, gettext("Room link copied to clipboard")); - } + /// Update the join rule row. + fn update_join_rule(&self) { + let Some(room) = self.room.obj() else { + return; + }; - /// Update the join rule row. - fn update_join_rule(&self) { - let Some(room) = self.room() else { - return; - }; + let row = &self.join_rule; + row.set_is_loading(false); - let row = &self.imp().join_rule; - row.set_is_loading(false); + let permissions = room.permissions(); + let join_rule = room.join_rule(); - let permissions = room.permissions(); - let join_rule = room.join_rule(); + let is_supported_join_rule = matches!( + join_rule.value(), + JoinRuleValue::Public | JoinRuleValue::Invite + ) && !join_rule.can_knock(); + let can_change = permissions + .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomJoinRules)); - let is_supported_join_rule = matches!( - join_rule.value(), - JoinRuleValue::Public | JoinRuleValue::Invite - ) && !join_rule.can_knock(); - let can_change = - permissions.is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomJoinRules)); + row.set_read_only(!is_supported_join_rule || !can_change); + row.set_selected_string(Some(join_rule.display_name())); + } - row.set_read_only(!is_supported_join_rule || !can_change); - row.set_selected_string(Some(join_rule.display_name())); - } + /// Set the join rule of the room. + #[template_callback] + async fn set_join_rule(&self) { + let Some(room) = self.room.obj() else { + return; + }; + let join_rule = room.join_rule(); - /// Set the join rule of the room. - #[template_callback] - async fn set_join_rule(&self) { - let Some(room) = self.room() else { - return; - }; - let join_rule = room.join_rule(); + let row = &self.join_rule; - let row = &self.imp().join_rule; + let value = match row.selected() { + 0 => JoinRuleValue::Invite, + 1 => JoinRuleValue::Public, + _ => { + return; + } + }; - let value = match row.selected() { - 0 => JoinRuleValue::Invite, - 1 => JoinRuleValue::Public, - _ => { + if join_rule.value() == value { + // Nothing to do. return; } - }; - if join_rule.value() == value { - // Nothing to do. - return; + row.set_is_loading(true); + row.set_read_only(true); + + if join_rule.set_value(value).await.is_err() { + let obj = self.obj(); + toast!(obj, gettext("Could not change who can join")); + self.update_join_rule(); + } } - row.set_is_loading(true); - row.set_read_only(true); + /// Update the guest access row. + fn update_guest_access(&self) { + let Some(room) = self.room.obj() else { + return; + }; - if join_rule.set_value(value).await.is_err() { - toast!(self, gettext("Could not change who can join")); - self.update_join_rule(); + let row = &self.guest_access; + row.set_is_active(room.guests_allowed()); + row.set_is_loading(false); + + let can_change = room + .permissions() + .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomGuestAccess)); + row.set_read_only(!can_change); } - } - /// Update the guest access row. - fn update_guest_access(&self) { - let Some(room) = self.room() else { - return; - }; + /// Toggle the guest access. + #[template_callback] + async fn toggle_guest_access(&self) { + let Some(room) = self.room.obj() else { return }; - let row = &self.imp().guest_access; - row.set_is_active(room.guests_allowed()); - row.set_is_loading(false); + let row = &self.guest_access; + let guests_allowed = row.is_active(); - let can_change = room - .permissions() - .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomGuestAccess)); - row.set_read_only(!can_change); - } + if room.guests_allowed() == guests_allowed { + return; + } - /// Toggle the guest access. - #[template_callback] - async fn toggle_guest_access(&self) { - let Some(room) = self.room() else { return }; + row.set_is_loading(true); + row.set_read_only(true); - let row = &self.imp().guest_access; - let guests_allowed = row.is_active(); + let guest_access = if guests_allowed { + GuestAccess::CanJoin + } else { + GuestAccess::Forbidden + }; + let content = RoomGuestAccessEventContent::new(guest_access); - if room.guests_allowed() == guests_allowed { - return; - } + let matrix_room = room.matrix_room().clone(); + let handle = spawn_tokio!(async move { matrix_room.send_state_event(content).await }); - row.set_is_loading(true); - row.set_read_only(true); + if let Err(error) = handle.await.unwrap() { + error!("Could not change guest access: {error}"); + let obj = self.obj(); + toast!(obj, gettext("Could not change guest access")); + self.update_guest_access(); + } + } - let guest_access = if guests_allowed { - GuestAccess::CanJoin - } else { - GuestAccess::Forbidden - }; - let content = RoomGuestAccessEventContent::new(guest_access); + /// Update the title of the publish row. + fn update_publish_title(&self) { + let Some(room) = self.room.obj() else { + return; + }; - let matrix_room = room.matrix_room().clone(); - let handle = spawn_tokio!(async move { matrix_room.send_state_event(content).await }); + let own_member = room.own_member(); + let server_name = own_member.user_id().server_name(); - if let Err(error) = handle.await.unwrap() { - error!("Could not change guest access: {error}"); - toast!(self, gettext("Could not change guest access")); - self.update_guest_access(); + let title = gettext_f( + // Translators: Do NOT translate the content between '{' and '}', + // this is a variable name. + "Publish in the {homeserver} directory", + &[("homeserver", server_name.as_str())], + ); + self.publish.set_title(&title); } - } - /// Update the title of the publish row. - fn update_publish_title(&self) { - let Some(room) = self.room() else { - return; - }; - - let own_member = room.own_member(); - let server_name = own_member.user_id().server_name(); - - let title = gettext_f( - // Translators: Do NOT translate the content between '{' and '}', - // this is a variable name. - "Publish in the {homeserver} directory", - &[("homeserver", server_name.as_str())], - ); - self.imp().publish.set_title(&title); - } + /// Update the publish row. + async fn update_publish(&self) { + let Some(room) = self.room.obj() else { + return; + }; - /// Update the publish row. - async fn update_publish(&self) { - let Some(room) = self.room() else { - return; - }; + let row = &self.publish; - let imp = self.imp(); - let row = &imp.publish; - - // There is no clear definition of who is allowed to publish a room to the - // directory in the Matrix spec. Let's assume it doesn't make sense unless the - // user can change the public addresses. - let can_change = room - .permissions() - .is_allowed_to(PowerLevelAction::SendState( - StateEventType::RoomCanonicalAlias, - )); - row.set_read_only(!can_change); + // There is no clear definition of who is allowed to publish a room to the + // directory in the Matrix spec. Let's assume it doesn't make sense unless the + // user can change the public addresses. + let can_change = room + .permissions() + .is_allowed_to(PowerLevelAction::SendState( + StateEventType::RoomCanonicalAlias, + )); + row.set_read_only(!can_change); + + let matrix_room = room.matrix_room(); + let client = matrix_room.client(); + let request = get_room_visibility::v3::Request::new(matrix_room.room_id().to_owned()); + + let handle = spawn_tokio!(async move { client.send(request).await }); + + match handle.await.unwrap() { + Ok(response) => { + let is_published = response.visibility == Visibility::Public; + self.is_published.set(is_published); + row.set_is_active(is_published); + } + Err(error) => { + error!("Could not get directory visibility of room: {error}"); + } + } - let matrix_room = room.matrix_room(); - let client = matrix_room.client(); - let request = get_room_visibility::v3::Request::new(matrix_room.room_id().to_owned()); + row.set_is_loading(false); + } - let handle = spawn_tokio!(async move { client.send(request).await }); + /// Toggle whether the room is published in the room directory. + #[template_callback] + async fn toggle_publish(&self) { + let Some(room) = self.room.obj() else { return }; - match handle.await.unwrap() { - Ok(response) => { - let is_published = response.visibility == Visibility::Public; - imp.is_published.set(is_published); - row.set_is_active(is_published); + let row = &self.publish; + let publish = row.is_active(); + + if self.is_published.get() == publish { + return; } - Err(error) => { - error!("Could not get directory visibility of room: {error}"); + + row.set_is_loading(true); + row.set_read_only(true); + + let visibility = if publish { + Visibility::Public + } else { + Visibility::Private + }; + + let matrix_room = room.matrix_room(); + let client = matrix_room.client(); + let request = + set_room_visibility::v3::Request::new(matrix_room.room_id().to_owned(), visibility); + + let handle = spawn_tokio!(async move { client.send(request).await }); + + if let Err(error) = handle.await.unwrap() { + error!("Could not change directory visibility of room: {error}"); + let text = if publish { + gettext("Could not publish room in directory") + } else { + gettext("Could not unpublish room from directory") + }; + let obj = self.obj(); + toast!(obj, text); } + + self.update_publish().await; } - row.set_is_loading(false); - } + /// Update the history visibility edit button. + fn update_history_visibility(&self) { + let Some(room) = self.room.obj() else { + return; + }; - /// Toggle whether the room is published in the room directory. - #[template_callback] - async fn toggle_publish(&self) { - let Some(room) = self.room() else { return }; + let row = &self.history_visibility; + row.set_is_loading(false); - let imp = self.imp(); - let row = &imp.publish; - let publish = row.is_active(); + let visibility = room.history_visibility(); - if imp.is_published.get() == publish { - return; + let text = match visibility { + HistoryVisibilityValue::WorldReadable => { + gettext("Anyone, even if they are not in the room") + } + HistoryVisibilityValue::Shared => { + gettext("Members only, since this option was selected") + } + HistoryVisibilityValue::Invited => gettext("Members only, since they were invited"), + HistoryVisibilityValue::Joined => { + gettext("Members only, since they joined the room") + } + HistoryVisibilityValue::Unsupported => gettext("Unsupported rule"), + }; + row.set_selected_string(Some(text)); + + let is_supported = visibility != HistoryVisibilityValue::Unsupported; + let can_change = room + .permissions() + .is_allowed_to(PowerLevelAction::SendState( + StateEventType::RoomHistoryVisibility, + )); + + row.set_read_only(!is_supported || !can_change); } - row.set_is_loading(true); - row.set_read_only(true); + /// Set the history_visibility of the room. + #[template_callback] + async fn set_history_visibility(&self) { + let Some(room) = self.room.obj() else { + return; + }; + let row = &self.history_visibility; + + let visibility = match row.selected() { + 0 => HistoryVisibilityValue::WorldReadable, + 1 => HistoryVisibilityValue::Shared, + 2 => HistoryVisibilityValue::Joined, + 3 => HistoryVisibilityValue::Invited, + _ => { + return; + } + }; - let visibility = if publish { - Visibility::Public - } else { - Visibility::Private - }; + if room.history_visibility() == visibility { + // Nothing to do. + return; + } - let matrix_room = room.matrix_room(); - let client = matrix_room.client(); - let request = - set_room_visibility::v3::Request::new(matrix_room.room_id().to_owned(), visibility); + row.set_is_loading(true); + row.set_read_only(true); - let handle = spawn_tokio!(async move { client.send(request).await }); + let content = RoomHistoryVisibilityEventContent::new(visibility.into()); - if let Err(error) = handle.await.unwrap() { - error!("Could not change directory visibility of room: {error}"); - let text = if publish { - gettext("Could not publish room in directory") - } else { - gettext("Could not unpublish room from directory") - }; - toast!(self, text); + let matrix_room = room.matrix_room().clone(); + let handle = spawn_tokio!(async move { matrix_room.send_state_event(content).await }); + + if let Err(error) = handle.await.unwrap() { + error!("Could not change room history visibility: {error}"); + let obj = self.obj(); + toast!(obj, gettext("Could not change who can read history")); + + self.update_history_visibility(); + } } - self.update_publish().await; - } + /// Update the encryption row. + fn update_encryption(&self) { + let Some(room) = self.room.obj() else { + return; + }; - /// Update the history visibility edit button. - fn update_history_visibility(&self) { - let Some(room) = self.room() else { - return; - }; + let row = &self.encryption; + row.set_is_loading(false); - let row = &self.imp().history_visibility; - row.set_is_loading(false); + let is_encrypted = room.is_encrypted(); + row.set_is_active(is_encrypted); - let visibility = room.history_visibility(); + let can_change = !is_encrypted + && room + .permissions() + .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomEncryption)); + row.set_read_only(!can_change); + } - let text = match visibility { - HistoryVisibilityValue::WorldReadable => { - gettext("Anyone, even if they are not in the room") - } - HistoryVisibilityValue::Shared => { - gettext("Members only, since this option was selected") + /// Enable encryption in the room. + #[template_callback] + async fn enable_encryption(&self) { + let Some(room) = self.room.obj() else { return }; + + let row = &self.encryption; + + if room.is_encrypted() || !row.is_active() { + // Nothing to do. + return; } - HistoryVisibilityValue::Invited => gettext("Members only, since they were invited"), - HistoryVisibilityValue::Joined => gettext("Members only, since they joined the room"), - HistoryVisibilityValue::Unsupported => gettext("Unsupported rule"), - }; - row.set_selected_string(Some(text)); - - let is_supported = visibility != HistoryVisibilityValue::Unsupported; - let can_change = room - .permissions() - .is_allowed_to(PowerLevelAction::SendState( - StateEventType::RoomHistoryVisibility, - )); - row.set_read_only(!is_supported || !can_change); - } + row.set_is_loading(true); + row.set_read_only(true); + + // Ask for confirmation. + let dialog = adw::AlertDialog::builder() + .heading(gettext("Enable Encryption?")) + .body(gettext("Enabling encryption will prevent new members to read the history before they arrived. This cannot be disabled later.")) + .default_response("cancel") + .build(); + dialog.add_responses(&[ + ("cancel", &gettext("Cancel")), + ("enable", &gettext("Enable")), + ]); + dialog.set_response_appearance("enable", adw::ResponseAppearance::Destructive); - /// Set the history_visibility of the room. - #[template_callback] - async fn set_history_visibility(&self) { - let Some(room) = self.room() else { - return; - }; - let row = &self.imp().history_visibility; - - let visibility = match row.selected() { - 0 => HistoryVisibilityValue::WorldReadable, - 1 => HistoryVisibilityValue::Shared, - 2 => HistoryVisibilityValue::Joined, - 3 => HistoryVisibilityValue::Invited, - _ => { + let obj = self.obj(); + if dialog.choose_future(&*obj).await != "enable" { + self.update_encryption(); return; } - }; - if room.history_visibility() == visibility { - // Nothing to do. - return; + if room.enable_encryption().await.is_err() { + toast!(obj, gettext("Could not enable encryption")); + self.update_encryption(); + } } - row.set_is_loading(true); - row.set_read_only(true); + /// Update the room upgrade button. + fn update_upgrade_button(&self) { + let Some(room) = self.room.obj() else { + return; + }; - let content = RoomHistoryVisibilityEventContent::new(visibility.into()); + let can_upgrade = !room.is_tombstoned() + && room + .permissions() + .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomTombstone)); + self.upgrade_button.set_visible(can_upgrade); + } - let matrix_room = room.matrix_room().clone(); - let handle = spawn_tokio!(async move { matrix_room.send_state_event(content).await }); + /// Update the room federation row. + fn update_federated(&self) { + let Some(room) = self.room.obj() else { + return; + }; - if let Err(error) = handle.await.unwrap() { - error!("Could not change room history visibility: {error}"); - toast!(self, gettext("Could not change who can read history")); + let subtitle = if room.federated() { + // Translators: As in, 'Room federated'. + gettext("Federated") + } else { + // Translators: As in, 'Room not federated'. + gettext("Not federated") + }; - self.update_history_visibility(); + self.room_federated.set_subtitle(&subtitle); } - } - - /// Update the encryption row. - fn update_encryption(&self) { - let Some(room) = self.room() else { - return; - }; - - let imp = self.imp(); - let row = &imp.encryption; - row.set_is_loading(false); - let is_encrypted = room.is_encrypted(); - row.set_is_active(is_encrypted); + /// Upgrade the room to a new version. + #[template_callback] + async fn upgrade(&self) { + let Some(room) = self.room.obj() else { + return; + }; - let can_change = !is_encrypted - && room - .permissions() - .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomEncryption)); - row.set_read_only(!can_change); - } + let obj = self.obj(); + // TODO: Hide upgrade button if room already upgraded? + self.upgrade_button.set_is_loading(true); + let room_versions_capability = self.capabilities.borrow().room_versions.clone(); - /// Enable encryption in the room. - #[template_callback] - async fn enable_encryption(&self) { - let Some(room) = self.room() else { return }; + let Some(new_version) = confirm_room_upgrade(room_versions_capability, &*obj).await + else { + self.upgrade_button.set_is_loading(false); + return; + }; - let imp = self.imp(); - let row = &imp.encryption; + let client = room.matrix_room().client(); + let request = upgrade_room::v3::Request::new(room.room_id().to_owned(), new_version); - if room.is_encrypted() || !row.is_active() { - // Nothing to do. - return; - } + let handle = spawn_tokio!(async move { client.send(request).await }); - row.set_is_loading(true); - row.set_read_only(true); - - // Ask for confirmation. - let dialog = adw::AlertDialog::builder() - .heading(gettext("Enable Encryption?")) - .body(gettext("Enabling encryption will prevent new members to read the history before they arrived. This cannot be disabled later.")) - .default_response("cancel") - .build(); - dialog.add_responses(&[ - ("cancel", &gettext("Cancel")), - ("enable", &gettext("Enable")), - ]); - dialog.set_response_appearance("enable", adw::ResponseAppearance::Destructive); - - if dialog.choose_future(self).await != "enable" { - self.update_encryption(); - return; + match handle.await.unwrap() { + Ok(_) => { + toast!(obj, gettext("Room upgraded successfully")); + } + Err(error) => { + error!("Could not upgrade room: {error}"); + toast!(obj, gettext("Could not upgrade room")); + self.upgrade_button.set_is_loading(false); + } + } } - if room.enable_encryption().await.is_err() { - toast!(self, gettext("Could not enable encryption")); - self.update_encryption(); + /// Unselect the topic of the room. + /// + /// This is to circumvent the default GTK behavior to select all the + /// text when opening the details. + pub(super) fn unselect_topic(&self) { + // Put the cursor at the beginning of the title instead of having the title + // selected, if it is visible. + if self.room_topic.is_visible() { + self.room_topic.select_region(0, 0); + } } } +} - /// Update the room upgrade button. - fn update_upgrade_button(&self) { - let Some(room) = self.room() else { - return; - }; - - let can_upgrade = !room.is_tombstoned() - && room - .permissions() - .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomTombstone)); - self.imp().upgrade_button.set_visible(can_upgrade); - } +glib::wrapper! { + /// Preference Window to display and update room details. + pub struct GeneralPage(ObjectSubclass) + @extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible; +} - /// Update the room federation row. - fn update_federated(&self) { - let Some(room) = self.room() else { - return; - }; - - let subtitle = if room.federated() { - // Translators: As in, 'Room federated'. - gettext("Federated") - } else { - // Translators: As in, 'Room not federated'. - gettext("Not federated") - }; - - self.imp().room_federated.set_subtitle(&subtitle); +impl GeneralPage { + pub fn new(room: &Room, membership_lists: &MembershipLists) -> Self { + glib::Object::builder() + .property("room", room) + .property("membership-lists", membership_lists) + .build() } - /// Upgrade the room to a new version. - #[template_callback] - async fn upgrade(&self) { - let Some(room) = self.room() else { - return; - }; + /// Unselect the topic of the room. + /// + /// This is to circumvent the default GTK behavior to select all the text + /// when opening the details. + pub(crate) fn unselect_topic(&self) { let imp = self.imp(); - // TODO: Hide upgrade button if room already upgraded? - imp.upgrade_button.set_is_loading(true); - let room_versions_capability = imp.capabilities.borrow().room_versions.clone(); - - let Some(new_version) = confirm_room_upgrade(room_versions_capability, self).await else { - imp.upgrade_button.set_is_loading(false); - return; - }; - - let client = room.matrix_room().client(); - let request = upgrade_room::v3::Request::new(room.room_id().to_owned(), new_version); - - let handle = spawn_tokio!(async move { client.send(request).await }); - - match handle.await.unwrap() { - Ok(_) => { - toast!(self, gettext("Room upgraded successfully")); - } - Err(error) => { - error!("Could not upgrade room: {error}"); - toast!(self, gettext("Could not upgrade room")); - imp.upgrade_button.set_is_loading(false); + glib::idle_add_local_once(clone!( + #[weak] + imp, + move || { + imp.unselect_topic(); } - } + )); } } diff --git a/src/session/view/content/room_details/history_viewer/audio.rs b/src/session/view/content/room_details/history_viewer/audio.rs index 3314bf89..58a01156 100644 --- a/src/session/view/content/room_details/history_viewer/audio.rs +++ b/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, #[template_child] - pub stack: TemplateChild, + stack: TemplateChild, #[template_child] - pub list_view: TemplateChild, + list_view: TemplateChild, + /// The timeline containing the audio events. + #[property(get, set = Self::set_timeline, construct_only)] + timeline: BoundConstructOnlyObject, } #[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; - } } diff --git a/src/session/view/content/room_details/history_viewer/audio_row.rs b/src/session/view/content/room_details/history_viewer/audio_row.rs index 400b287a..c900e69b 100644 --- a/src/session/view/content/room_details/history_viewer/audio_row.rs +++ b/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, + #[template_child] + title_label: TemplateChild, + #[template_child] + duration_label: TemplateChild, /// The audio event. #[property(get, set = Self::set_event, explicit_notify, nullable)] - pub event: RefCell>, + event: RefCell>, /// The media file. file: RefCell>, /// The API for the media file. - pub media_file: RefCell>, - #[template_child] - pub play_button: TemplateChild, - #[template_child] - pub title_label: TemplateChild, - #[template_child] - pub duration_label: TemplateChild, + media_file: RefCell>, } #[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) { @@ -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) { @@ -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"); - } - } - } } diff --git a/src/session/view/content/room_details/history_viewer/event.rs b/src/session/view/content/room_details/history_viewer/event.rs index 9f81e4a7..2eef3987 100644 --- a/src/session/view/content/room_details/history_viewer/event.rs +++ b/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: glib::WeakRef, /// The Matrix event. - #[property(construct_only)] - pub matrix_event: OnceCell, + matrix_event: OnceCell, /// The type of the event. #[property(get, construct_only, builder(HistoryViewerEventType::default()))] - pub event_type: Cell, + event_type: Cell, } #[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::() .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, matrix_sdk::Error> { + pub(crate) async fn get_file_content(&self) -> Result, matrix_sdk::Error> { let Some(room) = self.room() else { return Err(matrix_sdk::Error::UnknownError( "Could not upgrade Room".into(), diff --git a/src/session/view/content/room_details/history_viewer/file.rs b/src/session/view/content/room_details/history_viewer/file.rs index 4d05924a..18e1d230 100644 --- a/src/session/view/content/room_details/history_viewer/file.rs +++ b/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, #[template_child] - pub stack: TemplateChild, + stack: TemplateChild, #[template_child] - pub list_view: TemplateChild, + list_view: TemplateChild, + /// The timeline containing the file events. + #[property(get, set = Self::set_timeline, construct_only)] + timeline: BoundConstructOnlyObject, } #[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; - } } diff --git a/src/session/view/content/room_details/history_viewer/file_row.rs b/src/session/view/content/room_details/history_viewer/file_row.rs index f42870a2..b9e0eadc 100644 --- a/src/session/view/content/room_details/history_viewer/file_row.rs +++ b/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>, - pub file: RefCell>, #[template_child] - pub button: TemplateChild, + button: TemplateChild, #[template_child] - pub title_label: TemplateChild, + title_label: TemplateChild, #[template_child] - pub size_label: TemplateChild, + size_label: TemplateChild, + /// The file event. + #[property(get, set = Self::set_event, explicit_notify, nullable)] + event: RefCell>, + file: RefCell>, } #[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) { @@ -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) { @@ -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::(); + 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::().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(); - } - } } diff --git a/src/session/view/content/room_details/history_viewer/mod.rs b/src/session/view/content/room_details/history_viewer/mod.rs index b1145299..ec3110c2 100644 --- a/src/session/view/content/room_details/history_viewer/mod.rs +++ b/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, }; diff --git a/src/session/view/content/room_details/history_viewer/timeline.rs b/src/session/view/content/room_details/history_viewer/timeline.rs index 1631cc6b..86dda85a 100644 --- a/src/session/view/content/room_details/history_viewer/timeline.rs +++ b/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: OnceCell, /// The loading state of this timeline. #[property(get, builder(LoadingState::default()))] - pub state: Cell, + state: Cell, /// Whether we have reached the start of the timeline. #[property(get)] has_reached_start: Cell, - pub list: RefCell>, - pub last_token: Arc>, + list: RefCell>, + last_token: RefCell>, /// 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(&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(&self, continue_fn: F) + pub(crate) async fn load(&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() } } diff --git a/src/session/view/content/room_details/history_viewer/visual_media.rs b/src/session/view/content/room_details/history_viewer/visual_media.rs index bca4960a..710c33fd 100644 --- a/src/session/view/content/room_details/history_viewer/visual_media.rs +++ b/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, #[template_child] - pub media_viewer: TemplateChild, + media_viewer: TemplateChild, #[template_child] - pub stack: TemplateChild, + stack: TemplateChild, #[template_child] - pub grid_view: TemplateChild, + grid_view: TemplateChild, + /// The timeline containing the media events. + #[property(get, set = Self::set_timeline, construct_only)] + timeline: BoundConstructOnlyObject, } #[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); } } diff --git a/src/session/view/content/room_details/history_viewer/visual_media_item.rs b/src/session/view/content/room_details/history_viewer/visual_media_item.rs index 1d986884..51bd0ca2 100644 --- a/src/session/view/content/room_details/history_viewer/visual_media_item.rs +++ b/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>, - pub overlay_icon: RefCell>, #[template_child] - pub overlay: TemplateChild, + overlay: TemplateChild, #[template_child] - pub picture: TemplateChild, + picture: TemplateChild, + /// The file event. + #[property(get, set = Self::set_event, explicit_notify, nullable)] + event: RefCell>, + overlay_icon: RefCell>, } #[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) { @@ -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::() + 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::() - .unwrap(); - media_history_viewer.show_media(self); - } } diff --git a/src/session/view/content/room_details/invite_subpage/item.rs b/src/session/view/content/room_details/invite_subpage/item.rs index fc037df8..75de745a 100644 --- a/src/session/view/content/room_details/invite_subpage/item.rs +++ b/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: OnceCell, /// Whether the user is invited. #[property(get, set = Self::set_is_invitee, explicit_notify)] - pub is_invitee: Cell, + is_invitee: Cell, /// Whether the user can be invited. #[property(get = Self::can_invite)] - pub can_invite: PhantomData, + can_invite: PhantomData, /// The reason why the user cannot be invited, when applicable. #[property(get, set = Self::set_invite_exception, explicit_notify, nullable)] - pub invite_exception: RefCell>, + invite_exception: RefCell>, } #[glib::object_subclass] diff --git a/src/session/view/content/room_details/invite_subpage/list.rs b/src/session/view/content/room_details/invite_subpage/list.rs index b4f6ef6d..b7db1376 100644 --- a/src/session/view/content/room_details/invite_subpage/list.rs +++ b/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>, + list: RefCell>, /// The room this invitee list refers to. #[property(get, construct_only)] - pub room: OnceCell, + room: OnceCell, /// The state of the list. #[property(get, builder(InviteListState::default()))] - pub state: Cell, + state: Cell, /// The search term. #[property(get, set = Self::set_search_term, explicit_notify)] - pub search_term: RefCell>, - pub invitee_list: RefCell>, - pub abort_handle: RefCell>, + search_term: RefCell>, + pub(super) invitee_list: RefCell>, + abort_handle: RefCell>, /// Whether some users are invited. #[property(get = Self::has_invitees)] - pub has_invitees: PhantomData, + has_invitees: PhantomData, } #[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) { 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) - @implements gio::ListModel; -} + /// Replace this list with the given items. + fn replace_list(&self, items: Vec) { + 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) { - 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) { + 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::() + .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) { - 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::() - .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, + invite_exception: Option, + ) -> 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, invite_exception: Option) -> 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 { - 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) + @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 { + pub(crate) fn first_invitee(&self) -> Option { 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 { + pub(crate) fn invitees_ids(&self) -> Vec { 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. diff --git a/src/session/view/content/room_details/invite_subpage/mod.rs b/src/session/view/content/room_details/invite_subpage/mod.rs index 22357a38..72e77ee4 100644 --- a/src/session/view/content/room_details/invite_subpage/mod.rs +++ b/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, + search_entry: TemplateChild, #[template_child] - pub list_view: TemplateChild, + list_view: TemplateChild, #[template_child] - pub invite_button: TemplateChild, + invite_button: TemplateChild, #[template_child] - pub cancel_button: TemplateChild, + cancel_button: TemplateChild, #[template_child] - pub stack: TemplateChild, + stack: TemplateChild, #[template_child] - pub matching_page: TemplateChild, + matching_page: TemplateChild, #[template_child] - pub no_matching_page: TemplateChild, + no_matching_page: TemplateChild, #[template_child] - pub no_search_page: TemplateChild, + no_search_page: TemplateChild, #[template_child] - pub error_page: TemplateChild, + error_page: TemplateChild, /// The room users will be invited to. #[property(get, set = Self::set_room, construct_only)] - pub room: glib::WeakRef, + room: glib::WeakRef, /// The list managing the invited users. #[property(get)] - pub invite_list: OnceCell, + invite_list: OnceCell, } #[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) - @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::() else { + return; + }; - /// Close this subpage. - #[template_callback] - fn close(&self) { - let window = self - .root() - .and_downcast::() - .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::() 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::() 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::() { - 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::() { + 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) + @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() } } diff --git a/src/session/view/content/room_details/invite_subpage/row.rs b/src/session/view/content/room_details/invite_subpage/row.rs index b0a30a20..b93ff9b0 100644 --- a/src/session/view/content/room_details/invite_subpage/row.rs +++ b/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, /// The item displayed by this row. #[property(get, set = Self::set_item, explicit_notify, nullable)] - pub item: RefCell>, - pub binding: RefCell>, - #[template_child] - pub check_button: TemplateChild, + item: RefCell>, + binding: RefCell>, } #[glib::object_subclass] diff --git a/src/session/view/content/room_details/mod.rs b/src/session/view/content/room_details/mod.rs index 12b1d25b..f9f53bbd 100644 --- a/src/session/view/content/room_details/mod.rs +++ b/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); } } diff --git a/src/session/view/content/room_details/permissions/add_members_subpage.rs b/src/session/view/content/room_details/permissions/add_members_subpage.rs index 1a0c5672..ace805cd 100644 --- a/src/session/view/content/room_details/permissions/add_members_subpage.rs +++ b/src/session/view/content/room_details/permissions/add_members_subpage.rs @@ -29,25 +29,25 @@ mod imp { #[properties(wrapper_type = super::PermissionsAddMembersSubpage)] pub struct PermissionsAddMembersSubpage { #[template_child] - pub search_entry: TemplateChild, + search_entry: TemplateChild, #[template_child] - pub power_level_combo: TemplateChild, + power_level_combo: TemplateChild, #[template_child] - pub list_view: TemplateChild, + list_view: TemplateChild, #[template_child] - pub add_button: TemplateChild, + add_button: TemplateChild, #[template_child] - pub stack: TemplateChild, + stack: TemplateChild, /// The permissions of the room. #[property(get, set = Self::set_permissions, explicit_notify, nullable)] - pub permissions: glib::WeakRef, + permissions: glib::WeakRef, 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, + privileged_members: glib::WeakRef, /// The selected members in the list. - pub selected_members: RefCell>, + selected_members: RefCell>, } #[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) { @@ -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(&self, f: F) -> glib::SignalHandlerId { self.connect_closure( diff --git a/src/session/view/content/room_details/permissions/member_power_level.rs b/src/session/view/content/room_details/permissions/member_power_level.rs index e2a157bf..9d430267 100644 --- a/src/session/view/content/room_details/permissions/member_power_level.rs +++ b/src/session/view/content/room_details/permissions/member_power_level.rs @@ -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: BoundObjectWeakRef, /// The room member or remote user. #[property(get, construct_only)] - pub user: OnceCell, + user: OnceCell, /// 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, + power_level: Cell, /// The wanted role of the member. #[property(get, builder(MemberRole::default()))] - pub role: Cell, + role: Cell, /// Whether this member's power level can be edited. #[property(get)] - pub editable: Cell, + editable: Cell, } #[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(); diff --git a/src/session/view/content/room_details/permissions/mod.rs b/src/session/view/content/room_details/permissions/mod.rs index 6c4c056a..f4646aa1 100644 --- a/src/session/view/content/room_details/permissions/mod.rs +++ b/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, diff --git a/src/session/view/content/room_details/permissions/permissions_subpage.rs b/src/session/view/content/room_details/permissions/permissions_subpage.rs index 9dfe5b47..84ad1a13 100644 --- a/src/session/view/content/room_details/permissions/permissions_subpage.rs +++ b/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, + save_button: TemplateChild, #[template_child] - pub messages_row: TemplateChild, + messages_row: TemplateChild, #[template_child] - pub redact_own_row: TemplateChild, + redact_own_row: TemplateChild, #[template_child] - pub redact_others_row: TemplateChild, + redact_others_row: TemplateChild, #[template_child] - pub notify_room_row: TemplateChild, + notify_room_row: TemplateChild, #[template_child] - pub state_row: TemplateChild, + state_row: TemplateChild, #[template_child] - pub name_row: TemplateChild, + name_row: TemplateChild, #[template_child] - pub topic_row: TemplateChild, + topic_row: TemplateChild, #[template_child] - pub avatar_row: TemplateChild, + avatar_row: TemplateChild, #[template_child] - pub aliases_row: TemplateChild, + aliases_row: TemplateChild, #[template_child] - pub history_visibility_row: TemplateChild, + history_visibility_row: TemplateChild, #[template_child] - pub encryption_row: TemplateChild, + encryption_row: TemplateChild, #[template_child] - pub power_levels_row: TemplateChild, + power_levels_row: TemplateChild, #[template_child] - pub server_acl_row: TemplateChild, + server_acl_row: TemplateChild, #[template_child] - pub upgrade_row: TemplateChild, + upgrade_row: TemplateChild, #[template_child] - pub invite_row: TemplateChild, + invite_row: TemplateChild, #[template_child] - pub kick_row: TemplateChild, + kick_row: TemplateChild, #[template_child] - pub ban_row: TemplateChild, + ban_row: TemplateChild, #[template_child] - pub members_default_spin_row: TemplateChild, + members_default_spin_row: TemplateChild, #[template_child] - pub members_default_adjustment: TemplateChild, + members_default_adjustment: TemplateChild, #[template_child] - pub members_default_text_row: TemplateChild, + members_default_text_row: TemplateChild, #[template_child] - pub members_default_label: TemplateChild, + members_default_label: TemplateChild, #[template_child] - pub members_privileged_button: TemplateChild, + members_privileged_button: TemplateChild, /// The subpage to view and edit members with custom power levels. #[template_child] - pub members_subpage: TemplateChild, + members_subpage: TemplateChild, /// The subpage to add members with custom power levels. #[template_child] - pub add_members_subpage: TemplateChild, + add_members_subpage: TemplateChild, /// The permissions to watch. #[property(get, set = Self::set_permissions, construct_only)] - pub permissions: BoundObjectWeakRef, + permissions: BoundObjectWeakRef, /// Whether our own user can change the power levels in this room. #[property(get)] - pub editable: Cell, + editable: Cell, /// Whether the permissions were changed by the user. #[property(get)] - pub changed: Cell, + changed: Cell, /// The list of members with custom power levels. #[property(get)] - pub privileged_members: OnceCell, + privileged_members: OnceCell, /// 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, + update_in_progress: Cell, } #[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) { @@ -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) - @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 { + 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 { - 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) + @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() } } diff --git a/src/session/view/content/room_details/permissions/privileged_members.rs b/src/session/view/content/room_details/permissions/privileged_members.rs index 008002c4..27f5f3b7 100644 --- a/src/session/view/content/room_details/permissions/privileged_members.rs +++ b/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>, + pub(super) list: RefCell>, /// The permissions to watch. #[property(get, set = Self::set_permissions, construct_only)] - pub permissions: BoundObjectWeakRef, + permissions: BoundObjectWeakRef, /// Whether this list has changed. #[property(get)] - pub changed: Cell, + changed: Cell, } #[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, ) { @@ -244,7 +244,7 @@ impl PrivilegedMembers { } /// Collect the list of members. - pub fn collect(&self) -> BTreeMap { + pub(crate) fn collect(&self) -> BTreeMap { self.imp() .list .borrow() diff --git a/src/session/view/content/room_details/permissions/select_member_row.rs b/src/session/view/content/room_details/permissions/select_member_row.rs index d4afcb92..def71a6e 100644 --- a/src/session/view/content/room_details/permissions/select_member_row.rs +++ b/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>, + member: RefCell>, /// Whether this row is selected. #[property(get, set = Self::set_selected, explicit_notify)] - pub selected: Cell, + selected: Cell, } #[glib::object_subclass]