23 changed files with 2703 additions and 25 deletions
@ -0,0 +1,458 @@
|
||||
use adw::{prelude::*, subclass::prelude::*}; |
||||
use gtk::{ |
||||
glib, |
||||
glib::{clone, closure_local}, |
||||
pango, CompositeTemplate, |
||||
}; |
||||
|
||||
use super::SpinnerButton; |
||||
|
||||
mod imp { |
||||
use std::{ |
||||
cell::{Cell, RefCell}, |
||||
marker::PhantomData, |
||||
}; |
||||
|
||||
use glib::subclass::{InitializingObject, Signal}; |
||||
use once_cell::sync::Lazy; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, CompositeTemplate, glib::Properties)] |
||||
#[template(resource = "/org/gnome/Fractal/ui/components/substring_entry_row.ui")] |
||||
#[properties(wrapper_type = super::SubstringEntryRow)] |
||||
pub struct SubstringEntryRow { |
||||
#[template_child] |
||||
pub header: TemplateChild<gtk::Box>, |
||||
#[template_child] |
||||
pub main_content: TemplateChild<gtk::Box>, |
||||
#[template_child] |
||||
pub entry_box: TemplateChild<gtk::Box>, |
||||
#[template_child] |
||||
pub text: TemplateChild<gtk::Text>, |
||||
#[template_child] |
||||
pub title: TemplateChild<gtk::Label>, |
||||
#[template_child] |
||||
pub edit_icon: TemplateChild<gtk::Image>, |
||||
#[template_child] |
||||
pub entry_prefix_label: TemplateChild<gtk::Label>, |
||||
#[template_child] |
||||
pub entry_suffix_label: TemplateChild<gtk::Label>, |
||||
#[template_child] |
||||
pub add_button: TemplateChild<SpinnerButton>, |
||||
/// The input hints of the entry.
|
||||
#[property(get = Self::input_hints, set = Self::set_input_hints, explicit_notify)] |
||||
pub input_hints: PhantomData<gtk::InputHints>, |
||||
/// The input purpose of the entry.
|
||||
#[property(get = Self::input_purpose, set = Self::set_input_purpose, explicit_notify, builder(gtk::InputPurpose::FreeForm))] |
||||
pub input_purpose: PhantomData<gtk::InputPurpose>, |
||||
/// A list of Pango attributes to apply to the text of the entry.
|
||||
#[property(get = Self::attributes, set = Self::set_attributes, explicit_notify, nullable)] |
||||
pub attributes: PhantomData<Option<pango::AttrList>>, |
||||
/// The placeholder text of the entry.
|
||||
#[property(get = Self::placeholder_text, set = Self::set_placeholder_text, explicit_notify, nullable)] |
||||
pub placeholder_text: PhantomData<Option<glib::GString>>, |
||||
/// The length of the text of the entry.
|
||||
#[property(get = Self::text_length)] |
||||
pub text_length: PhantomData<u32>, |
||||
/// The prefix text of the entry.
|
||||
#[property(get = Self::prefix_text, set = Self::set_prefix_text, explicit_notify)] |
||||
pub prefix_text: PhantomData<glib::GString>, |
||||
/// The suffix text of the entry.
|
||||
#[property(get = Self::suffix_text, set = Self::set_suffix_text, explicit_notify)] |
||||
pub suffix_text: PhantomData<glib::GString>, |
||||
/// Set the accessible description of the entry.
|
||||
///
|
||||
/// If it is not set, the placeholder text will be used.
|
||||
#[property(get, set = Self::set_accessible_description, explicit_notify, nullable)] |
||||
pub accessible_description: RefCell<Option<String>>, |
||||
/// The tooltip text of the add button.
|
||||
#[property(get = Self::add_button_tooltip_text, set = Self::set_add_button_tooltip_text, explicit_notify, nullable)] |
||||
pub add_button_tooltip_text: PhantomData<Option<glib::GString>>, |
||||
/// The accessible label of the add button.
|
||||
#[property(get, set = Self::set_add_button_accessible_label, explicit_notify, nullable)] |
||||
pub add_button_accessible_label: RefCell<Option<String>>, |
||||
/// Whether to prevent the add button from being activated.
|
||||
#[property(get, set = Self::set_inhibit_add, explicit_notify)] |
||||
pub inhibit_add: Cell<bool>, |
||||
/// Whether this row is loading.
|
||||
#[property(get = Self::is_loading, set = Self::set_is_loading, explicit_notify)] |
||||
pub is_loading: PhantomData<bool>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for SubstringEntryRow { |
||||
const NAME: &'static str = "SubstringEntryRow"; |
||||
type Type = super::SubstringEntryRow; |
||||
type ParentType = adw::PreferencesRow; |
||||
type Interfaces = (gtk::Editable,); |
||||
|
||||
fn class_init(klass: &mut Self::Class) { |
||||
Self::bind_template(klass); |
||||
Self::Type::bind_template_callbacks(klass); |
||||
} |
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) { |
||||
obj.init_template(); |
||||
} |
||||
} |
||||
|
||||
impl ObjectImpl for SubstringEntryRow { |
||||
fn signals() -> &'static [Signal] { |
||||
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| vec![Signal::builder("add").build()]); |
||||
SIGNALS.as_ref() |
||||
} |
||||
|
||||
fn properties() -> &'static [glib::ParamSpec] { |
||||
Self::derived_properties() |
||||
} |
||||
|
||||
fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { |
||||
// In case this is a property that's automatically added for Editable
|
||||
// implementations.
|
||||
if !self.delegate_set_property(id, value, pspec) { |
||||
self.derived_set_property(id, value, pspec) |
||||
} |
||||
} |
||||
|
||||
fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
||||
// In case this is a property that's automatically added for Editable
|
||||
// implementations.
|
||||
if let Some(value) = self.delegate_get_property(id, pspec) { |
||||
value |
||||
} else { |
||||
self.derived_property(id, pspec) |
||||
} |
||||
} |
||||
|
||||
fn constructed(&self) { |
||||
self.parent_constructed(); |
||||
let obj = self.obj(); |
||||
|
||||
obj.init_delegate(); |
||||
|
||||
self.text |
||||
.buffer() |
||||
.connect_length_notify(clone!(@weak obj => move |_| { |
||||
obj.notify_text_length(); |
||||
})); |
||||
} |
||||
|
||||
fn dispose(&self) { |
||||
self.obj().finish_delegate(); |
||||
} |
||||
} |
||||
|
||||
impl WidgetImpl for SubstringEntryRow { |
||||
fn grab_focus(&self) -> bool { |
||||
self.text.grab_focus() |
||||
} |
||||
} |
||||
|
||||
impl ListBoxRowImpl for SubstringEntryRow {} |
||||
impl PreferencesRowImpl for SubstringEntryRow {} |
||||
|
||||
impl EditableImpl for SubstringEntryRow { |
||||
fn delegate(&self) -> Option<gtk::Editable> { |
||||
Some(self.text.clone().upcast()) |
||||
} |
||||
} |
||||
|
||||
impl SubstringEntryRow { |
||||
/// The input hints of the entry.
|
||||
fn input_hints(&self) -> gtk::InputHints { |
||||
self.text.input_hints() |
||||
} |
||||
|
||||
/// Set the input hints of the entry.
|
||||
fn set_input_hints(&self, input_hints: gtk::InputHints) { |
||||
if self.input_hints() == input_hints { |
||||
return; |
||||
} |
||||
|
||||
self.text.set_input_hints(input_hints); |
||||
self.obj().notify_input_hints(); |
||||
} |
||||
|
||||
/// The input purpose of the entry.
|
||||
fn input_purpose(&self) -> gtk::InputPurpose { |
||||
self.text.input_purpose() |
||||
} |
||||
|
||||
/// Set the input purpose of the entry.
|
||||
fn set_input_purpose(&self, input_purpose: gtk::InputPurpose) { |
||||
if self.input_purpose() == input_purpose { |
||||
return; |
||||
} |
||||
|
||||
self.text.set_input_purpose(input_purpose); |
||||
self.obj().notify_input_purpose(); |
||||
} |
||||
|
||||
/// A list of Pango attributes to apply to the text of the entry.
|
||||
fn attributes(&self) -> Option<pango::AttrList> { |
||||
self.text.attributes() |
||||
} |
||||
|
||||
/// Set the list of Pango attributes to apply to the text of the entry.
|
||||
fn set_attributes(&self, attributes: Option<&pango::AttrList>) { |
||||
if self.attributes().as_ref() == attributes { |
||||
return; |
||||
} |
||||
|
||||
self.text.set_attributes(attributes); |
||||
self.obj().notify_attributes(); |
||||
} |
||||
|
||||
/// The placeholder text of the entry.
|
||||
fn placeholder_text(&self) -> Option<glib::GString> { |
||||
self.text.placeholder_text() |
||||
} |
||||
|
||||
/// Set the placeholder text of the entry.
|
||||
fn set_placeholder_text(&self, text: Option<glib::GString>) { |
||||
if self.placeholder_text() == text { |
||||
return; |
||||
} |
||||
|
||||
self.text.set_placeholder_text(text.as_deref()); |
||||
|
||||
self.update_accessible_description(); |
||||
self.obj().notify_placeholder_text(); |
||||
} |
||||
|
||||
/// The length of the text of the entry.
|
||||
fn text_length(&self) -> u32 { |
||||
self.text.text_length().into() |
||||
} |
||||
|
||||
/// The prefix text of the entry.
|
||||
fn prefix_text(&self) -> glib::GString { |
||||
self.entry_prefix_label.label() |
||||
} |
||||
|
||||
/// Set the prefix text of the entry.
|
||||
fn set_prefix_text(&self, text: glib::GString) { |
||||
if self.prefix_text() == text { |
||||
return; |
||||
} |
||||
|
||||
self.entry_prefix_label.set_label(&text); |
||||
self.obj().notify_prefix_text(); |
||||
} |
||||
|
||||
/// The suffix text of the entry.
|
||||
fn suffix_text(&self) -> glib::GString { |
||||
self.entry_suffix_label.label() |
||||
} |
||||
|
||||
/// Set the suffix text of the entry.
|
||||
fn set_suffix_text(&self, text: glib::GString) { |
||||
if self.suffix_text() == text { |
||||
return; |
||||
} |
||||
|
||||
self.entry_suffix_label.set_label(&text); |
||||
self.obj().notify_suffix_text(); |
||||
} |
||||
|
||||
/// Set the accessible description of the entry.
|
||||
fn set_accessible_description(&self, description: Option<String>) { |
||||
if *self.accessible_description.borrow() == description { |
||||
return; |
||||
} |
||||
|
||||
self.accessible_description.replace(description); |
||||
|
||||
self.update_accessible_description(); |
||||
self.obj().notify_accessible_description(); |
||||
} |
||||
|
||||
/// The tooltip text of the add button.
|
||||
fn add_button_tooltip_text(&self) -> Option<glib::GString> { |
||||
self.add_button.tooltip_text() |
||||
} |
||||
|
||||
/// Set the tooltip text of the add button.
|
||||
fn set_add_button_tooltip_text(&self, tooltip_text: Option<glib::GString>) { |
||||
if self.add_button_tooltip_text() == tooltip_text { |
||||
return; |
||||
} |
||||
|
||||
self.add_button.set_tooltip_text(tooltip_text.as_deref()); |
||||
self.obj().notify_add_button_tooltip_text(); |
||||
} |
||||
|
||||
/// Set the accessible label of the add button.
|
||||
fn set_add_button_accessible_label(&self, label: Option<String>) { |
||||
if *self.add_button_accessible_label.borrow() == label { |
||||
return; |
||||
} |
||||
|
||||
if let Some(label) = &label { |
||||
self.add_button |
||||
.update_property(&[gtk::accessible::Property::Label(label)]); |
||||
} else { |
||||
self.add_button |
||||
.reset_property(gtk::AccessibleProperty::Label); |
||||
} |
||||
|
||||
self.add_button_accessible_label.replace(label); |
||||
self.obj().notify_add_button_accessible_label(); |
||||
} |
||||
|
||||
/// Set whether to prevent the add button from being activated.
|
||||
fn set_inhibit_add(&self, inhibit: bool) { |
||||
if self.inhibit_add.get() == inhibit { |
||||
return; |
||||
} |
||||
|
||||
self.inhibit_add.set(inhibit); |
||||
|
||||
let obj = self.obj(); |
||||
obj.update_add_button(); |
||||
obj.notify_inhibit_add(); |
||||
} |
||||
|
||||
/// Whether this row is loading.
|
||||
fn is_loading(&self) -> bool { |
||||
self.add_button.loading() |
||||
} |
||||
|
||||
/// Set whether this row is loading.
|
||||
fn set_is_loading(&self, is_loading: bool) { |
||||
if self.is_loading() == is_loading { |
||||
return; |
||||
} |
||||
|
||||
self.add_button.set_loading(is_loading); |
||||
|
||||
let obj = self.obj(); |
||||
obj.set_sensitive(!is_loading); |
||||
obj.notify_is_loading(); |
||||
} |
||||
|
||||
/// Update the accessible_description.
|
||||
fn update_accessible_description(&self) { |
||||
let description = self |
||||
.accessible_description |
||||
.borrow() |
||||
.clone() |
||||
.or(self.placeholder_text().map(Into::into)); |
||||
|
||||
if let Some(description) = description { |
||||
self.text |
||||
.update_property(&[gtk::accessible::Property::Description(&description)]); |
||||
} else { |
||||
self.text |
||||
.reset_property(gtk::AccessibleProperty::Description); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// A `AdwPreferencesRow` with an embedded text entry, and a fixed text suffix and prefix.
|
||||
///
|
||||
/// It also has a built-in add button, making it an almost drop-in replacement to `AddEntryRow`.
|
||||
///
|
||||
/// Inspired from `AdwEntryRow`.
|
||||
pub struct SubstringEntryRow(ObjectSubclass<imp::SubstringEntryRow>) |
||||
@extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, |
||||
@implements gtk::Editable, gtk::Accessible; |
||||
} |
||||
|
||||
#[gtk::template_callbacks] |
||||
impl SubstringEntryRow { |
||||
pub fn new() -> Self { |
||||
glib::Object::new() |
||||
} |
||||
|
||||
/// Whether the GtkText is focused.
|
||||
fn is_text_focused(&self) -> bool { |
||||
let flags = self.imp().text.state_flags(); |
||||
flags.contains(gtk::StateFlags::FOCUS_WITHIN) |
||||
} |
||||
|
||||
/// Update this row when the GtkText flags changed.
|
||||
#[template_callback] |
||||
fn text_state_flags_changed_cb(&self) { |
||||
let editing = self.is_text_focused(); |
||||
|
||||
if editing { |
||||
self.add_css_class("focused"); |
||||
} else { |
||||
self.remove_css_class("focused"); |
||||
} |
||||
|
||||
self.imp().edit_icon.set_visible(!editing); |
||||
} |
||||
|
||||
/// Handle when the key navigation in the GtkText failed.
|
||||
#[template_callback] |
||||
fn text_keynav_failed_cb(&self, direction: gtk::DirectionType) -> bool { |
||||
if matches!( |
||||
direction, |
||||
gtk::DirectionType::Left | gtk::DirectionType::Right |
||||
) { |
||||
return self.child_focus(direction); |
||||
} |
||||
|
||||
// gdk::EVENT_PROPAGATE == 0;
|
||||
false |
||||
} |
||||
|
||||
/// Handle when this row is pressed.
|
||||
#[template_callback] |
||||
fn pressed_cb(&self, _n_press: i32, x: f64, y: f64, gesture: >k::Gesture) { |
||||
let imp = self.imp(); |
||||
let picked = self.pick(x, y, gtk::PickFlags::DEFAULT); |
||||
|
||||
if picked.is_some_and(|w| { |
||||
&w != self.upcast_ref::<gtk::Widget>() |
||||
|| &w != imp.header.upcast_ref::<gtk::Widget>() |
||||
|| &w != imp.main_content.upcast_ref::<gtk::Widget>() |
||||
|| &w != imp.entry_box.upcast_ref::<gtk::Widget>() |
||||
}) { |
||||
gesture.set_state(gtk::EventSequenceState::Denied); |
||||
|
||||
return; |
||||
} |
||||
|
||||
imp.text.grab_focus_without_selecting(); |
||||
|
||||
gesture.set_state(gtk::EventSequenceState::Claimed); |
||||
} |
||||
|
||||
/// Whether we can activate the add button.
|
||||
fn can_add(&self) -> bool { |
||||
!self.inhibit_add() && !self.text().is_empty() |
||||
} |
||||
|
||||
/// Update the state of the add button.
|
||||
#[template_callback] |
||||
fn update_add_button(&self) { |
||||
self.imp().add_button.set_sensitive(self.can_add()); |
||||
} |
||||
|
||||
/// Emit the `add` signal.
|
||||
#[template_callback] |
||||
fn add(&self) { |
||||
if !self.can_add() { |
||||
return; |
||||
} |
||||
|
||||
self.emit_by_name::<()>("add", &[]); |
||||
} |
||||
|
||||
/// Connect to the `add` signal.
|
||||
pub fn connect_add<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId { |
||||
self.connect_closure( |
||||
"add", |
||||
true, |
||||
closure_local!(move |obj: Self| { |
||||
f(&obj); |
||||
}), |
||||
) |
||||
} |
||||
} |
||||
@ -0,0 +1,104 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<interface> |
||||
<template class="SubstringEntryRow" parent="AdwPreferencesRow"> |
||||
<property name="activatable">False</property> |
||||
<property name="selectable">False</property> |
||||
<style> |
||||
<class name="substring-entry-row"/> |
||||
</style> |
||||
<child> |
||||
<object class="GtkGestureClick"> |
||||
<signal name="pressed" handler="pressed_cb" swapped="yes"/> |
||||
</object> |
||||
</child> |
||||
<property name="child"> |
||||
<object class="GtkBox" id="header"> |
||||
<property name="valign">center</property> |
||||
<style> |
||||
<class name="header"/> |
||||
</style> |
||||
<child> |
||||
<object class="GtkBox" id="main_content"> |
||||
<property name="orientation">vertical</property> |
||||
<child> |
||||
<object class="GtkLabel" id="title"> |
||||
<property name="ellipsize">end</property> |
||||
<property name="halign">start</property> |
||||
<property name="xalign">0</property> |
||||
<property name="label" bind-source="SubstringEntryRow" bind-property="title" bind-flags="sync-create"/> |
||||
<property name="can-target">False</property> |
||||
<style> |
||||
<class name="subtitle"/> |
||||
</style> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkBox" id="entry_box"> |
||||
<property name="spacing">6</property> |
||||
<child> |
||||
<object class="GtkLabel" id="entry_prefix_label"> |
||||
<property name="label">#</property> |
||||
<style> |
||||
<class name="dim-label"/> |
||||
</style> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkText" id="text"> |
||||
<property name="enable-undo">True</property> |
||||
<property name="hexpand">True</property> |
||||
<property name="vexpand">True</property> |
||||
<property name="max-length">0</property> |
||||
<property name="valign">baseline-fill</property> |
||||
<property name="accessible-role">text-box</property> |
||||
<accessibility> |
||||
<relation name="labelled-by">title</relation> |
||||
</accessibility> |
||||
<signal name="activate" handler="add" swapped="yes"/> |
||||
<signal name="state-flags-changed" handler="text_state_flags_changed_cb" swapped="yes"/> |
||||
<signal name="keynav-failed" handler="text_keynav_failed_cb" swapped="yes"/> |
||||
<signal name="changed" handler="update_add_button" swapped="yes"/> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkImage" id="edit_icon"> |
||||
<property name="valign">center</property> |
||||
<property name="can-target">False</property> |
||||
<property name="icon-name">document-edit-symbolic</property> |
||||
<property name="accessible-role">presentation</property> |
||||
<style> |
||||
<class name="edit-icon"/> |
||||
</style> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkLabel" id="entry_suffix_label"> |
||||
<property name="label">:matrix.org</property> |
||||
<style> |
||||
<class name="dim-label"/> |
||||
</style> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="SpinnerButton" id="add_button"> |
||||
<property name="sensitive">False</property> |
||||
<property name="content-icon-name">add-symbolic</property> |
||||
<property name="valign">center</property> |
||||
<property name="halign">center</property> |
||||
<signal name="clicked" handler="add" swapped="true"/> |
||||
<style> |
||||
<class name="flat"/> |
||||
</style> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</property> |
||||
<style> |
||||
<class name="entry"/> |
||||
</style> |
||||
</template> |
||||
</interface> |
||||
@ -0,0 +1,522 @@
|
||||
use gtk::{glib, glib::closure_local, prelude::*, subclass::prelude::*}; |
||||
use matrix_sdk::{deserialized_responses::RawSyncOrStrippedState, reqwest::StatusCode}; |
||||
use ruma::{ |
||||
api::client::{ |
||||
alias::{create_alias, delete_alias}, |
||||
room, |
||||
}, |
||||
events::{room::canonical_alias::RoomCanonicalAliasEventContent, SyncStateEvent}, |
||||
OwnedRoomAliasId, |
||||
}; |
||||
use tracing::error; |
||||
|
||||
use super::Room; |
||||
use crate::spawn_tokio; |
||||
|
||||
mod imp { |
||||
use std::{cell::RefCell, marker::PhantomData}; |
||||
|
||||
use glib::subclass::Signal; |
||||
use once_cell::sync::Lazy; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, glib::Properties)] |
||||
#[properties(wrapper_type = super::RoomAliases)] |
||||
pub struct RoomAliases { |
||||
/// The room these aliases belong to.
|
||||
#[property(get)] |
||||
pub room: glib::WeakRef<Room>, |
||||
/// The canonical alias.
|
||||
pub canonical_alias: RefCell<Option<OwnedRoomAliasId>>, |
||||
/// The canonical alias, as a string.
|
||||
#[property(get = Self::canonical_alias_string)] |
||||
canonical_alias_string: PhantomData<Option<String>>, |
||||
/// The other aliases.
|
||||
pub alt_aliases: RefCell<Vec<OwnedRoomAliasId>>, |
||||
/// The other aliases, as a `GtkStringList`.
|
||||
#[property(get)] |
||||
pub alt_aliases_model: gtk::StringList, |
||||
/// The alias, as a string.
|
||||
///
|
||||
/// If the canonical alias is not set, it can be an alt alias.
|
||||
#[property(get = Self::alias_string)] |
||||
alias_string: PhantomData<Option<String>>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for RoomAliases { |
||||
const NAME: &'static str = "RoomAliases"; |
||||
type Type = super::RoomAliases; |
||||
} |
||||
|
||||
#[glib::derived_properties] |
||||
impl ObjectImpl for RoomAliases { |
||||
fn signals() -> &'static [Signal] { |
||||
static SIGNALS: Lazy<Vec<Signal>> = |
||||
Lazy::new(|| vec![Signal::builder("changed").build()]); |
||||
SIGNALS.as_ref() |
||||
} |
||||
} |
||||
|
||||
impl RoomAliases { |
||||
/// Set the canonical alias.
|
||||
pub(super) fn set_canonical_alias(&self, canonical_alias: Option<OwnedRoomAliasId>) { |
||||
if *self.canonical_alias.borrow() == canonical_alias { |
||||
return; |
||||
} |
||||
|
||||
self.canonical_alias.replace(canonical_alias); |
||||
|
||||
let obj = self.obj(); |
||||
obj.notify_canonical_alias_string(); |
||||
obj.notify_alias_string(); |
||||
} |
||||
|
||||
/// The canonical alias, as a string.
|
||||
fn canonical_alias_string(&self) -> Option<String> { |
||||
self.canonical_alias |
||||
.borrow() |
||||
.as_ref() |
||||
.map(ToString::to_string) |
||||
} |
||||
|
||||
/// Set the alt aliases.
|
||||
pub(super) fn set_alt_aliases(&self, alt_aliases: Vec<OwnedRoomAliasId>) { |
||||
let (pos, removed) = { |
||||
let old_aliases = &*self.alt_aliases.borrow(); |
||||
let mut pos = None; |
||||
|
||||
// Check if aliases were changed in the current list.
|
||||
for (i, old_alias) in old_aliases.iter().enumerate() { |
||||
if !alt_aliases.get(i).is_some_and(|alias| alias == old_alias) { |
||||
pos = Some(i); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
// Check if aliases were added.
|
||||
let old_len = old_aliases.len(); |
||||
if pos.is_none() { |
||||
let new_len = alt_aliases.len(); |
||||
|
||||
if old_len < new_len { |
||||
pos = Some(old_len); |
||||
} |
||||
} |
||||
|
||||
let Some(pos) = pos else { |
||||
return; |
||||
}; |
||||
|
||||
let removed = old_len.saturating_sub(pos); |
||||
|
||||
(pos, removed) |
||||
}; |
||||
|
||||
let additions = alt_aliases.get(pos..).unwrap_or_default().to_owned(); |
||||
let additions_str = additions |
||||
.iter() |
||||
.map(|alias| alias.as_str()) |
||||
.collect::<Vec<_>>(); |
||||
|
||||
let Ok(pos) = u32::try_from(pos) else { |
||||
return; |
||||
}; |
||||
let Ok(removed) = u32::try_from(removed) else { |
||||
return; |
||||
}; |
||||
|
||||
self.alt_aliases.replace(alt_aliases); |
||||
self.alt_aliases_model.splice(pos, removed, &additions_str); |
||||
|
||||
self.obj().notify_alias_string(); |
||||
} |
||||
|
||||
/// The alias, as a string.
|
||||
fn alias_string(&self) -> Option<String> { |
||||
self.canonical_alias_string() |
||||
.or_else(|| self.alt_aliases_model.string(0).map(Into::into)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// Aliases of a room.
|
||||
pub struct RoomAliases(ObjectSubclass<imp::RoomAliases>); |
||||
} |
||||
|
||||
impl RoomAliases { |
||||
pub fn new() -> Self { |
||||
glib::Object::new() |
||||
} |
||||
|
||||
/// Initialize these aliases with the given room.
|
||||
pub fn init(&self, room: &Room) { |
||||
let imp = self.imp(); |
||||
self.imp().room.set(Some(room)); |
||||
|
||||
let matrix_room = room.matrix_room(); |
||||
imp.set_canonical_alias(matrix_room.canonical_alias()); |
||||
imp.set_alt_aliases(matrix_room.alt_aliases()); |
||||
|
||||
let obj_weak = glib::SendWeakRef::from(self.downgrade()); |
||||
matrix_room.add_event_handler(move |_: SyncStateEvent<RoomCanonicalAliasEventContent>| { |
||||
let obj_weak = obj_weak.clone(); |
||||
async move { |
||||
let ctx = glib::MainContext::default(); |
||||
ctx.spawn(async move { |
||||
if let Some(obj) = obj_weak.upgrade() { |
||||
let Some(room) = obj.room() else { |
||||
return; |
||||
}; |
||||
let imp = obj.imp(); |
||||
|
||||
let matrix_room = room.matrix_room(); |
||||
imp.set_canonical_alias(matrix_room.canonical_alias()); |
||||
imp.set_alt_aliases(matrix_room.alt_aliases()); |
||||
|
||||
obj.emit_by_name::<()>("changed", &[]); |
||||
} |
||||
}); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/// Get the content of the canonical alias event from the store.
|
||||
async fn canonical_alias_event_content( |
||||
&self, |
||||
) -> Result<Option<RoomCanonicalAliasEventContent>, ()> { |
||||
let Some(room) = self.room() else { |
||||
return Err(()); |
||||
}; |
||||
|
||||
let matrix_room = room.matrix_room().clone(); |
||||
let handle = spawn_tokio!(async move { |
||||
matrix_room |
||||
.get_state_event_static::<RoomCanonicalAliasEventContent>() |
||||
.await |
||||
}); |
||||
|
||||
let raw_event = match handle.await.unwrap() { |
||||
Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => raw_event, |
||||
// We shouldn't need to load this is an invited room.
|
||||
Ok(_) => return Ok(None), |
||||
Err(error) => { |
||||
error!("Failed to get canonical alias event: {error}"); |
||||
return Err(()); |
||||
} |
||||
}; |
||||
|
||||
match raw_event.deserialize() { |
||||
Ok(SyncStateEvent::Original(event)) => Ok(Some(event.content)), |
||||
// The redacted event doesn't have a content.
|
||||
Ok(_) => Ok(None), |
||||
Err(error) => { |
||||
error!("Failed to deserialize canonical alias event: {error}"); |
||||
Err(()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// The canonical alias.
|
||||
pub fn canonical_alias(&self) -> Option<OwnedRoomAliasId> { |
||||
self.imp().canonical_alias.borrow().clone() |
||||
} |
||||
|
||||
/// Remove the given canonical alias.
|
||||
///
|
||||
/// Checks that the canonical alias is the correct one before proceeding.
|
||||
pub async fn remove_canonical_alias(&self, alias: &OwnedRoomAliasId) -> Result<(), ()> { |
||||
let mut event_content = self |
||||
.canonical_alias_event_content() |
||||
.await? |
||||
.unwrap_or_default(); |
||||
|
||||
// Remove the canonical alias, if it is there.
|
||||
if !event_content.alias.take().is_some_and(|a| a == *alias) { |
||||
// Nothing to do.
|
||||
return Err(()); |
||||
} |
||||
|
||||
let Some(room) = self.room() else { |
||||
return Err(()); |
||||
}; |
||||
|
||||
let matrix_room = room.matrix_room().clone(); |
||||
let handle = spawn_tokio!(async move { matrix_room.send_state_event(event_content).await }); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(_) => Ok(()), |
||||
Err(error) => { |
||||
error!("Failed to remove canonical alias: {error}"); |
||||
Err(()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Set the given alias to be the canonical alias.
|
||||
///
|
||||
/// Removes the given alias from the alt aliases if it is in the list.
|
||||
pub async fn set_canonical_alias(&self, alias: OwnedRoomAliasId) -> Result<(), ()> { |
||||
let mut event_content = self |
||||
.canonical_alias_event_content() |
||||
.await? |
||||
.unwrap_or_default(); |
||||
|
||||
if event_content.alias.as_ref().is_some_and(|a| *a == alias) { |
||||
// Nothing to do.
|
||||
return Err(()); |
||||
} |
||||
|
||||
let Some(room) = self.room() else { |
||||
return Err(()); |
||||
}; |
||||
|
||||
// Remove from the alt aliases, if it is there.
|
||||
let alt_alias_pos = event_content.alt_aliases.iter().position(|a| *a == alias); |
||||
if let Some(pos) = alt_alias_pos { |
||||
event_content.alt_aliases.remove(pos); |
||||
} |
||||
|
||||
// Set as canonical alias.
|
||||
if let Some(old_canonical) = event_content.alias.replace(alias) { |
||||
// Move the old canonical alias to the alt aliases, if it is not there already.
|
||||
let has_old_canonical = event_content.alt_aliases.contains(&old_canonical); |
||||
|
||||
if !has_old_canonical { |
||||
event_content.alt_aliases.push(old_canonical); |
||||
} |
||||
} |
||||
|
||||
let matrix_room = room.matrix_room().clone(); |
||||
let handle = spawn_tokio!(async move { matrix_room.send_state_event(event_content).await }); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(_) => Ok(()), |
||||
Err(error) => { |
||||
error!("Failed to set canonical alias: {error}"); |
||||
Err(()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// The other public aliases.
|
||||
pub fn alt_aliases(&self) -> Vec<OwnedRoomAliasId> { |
||||
self.imp().alt_aliases.borrow().clone() |
||||
} |
||||
|
||||
/// Remove the given alt alias.
|
||||
///
|
||||
/// Checks that is in the list of alt aliases before proceeding.
|
||||
pub async fn remove_alt_alias(&self, alias: &OwnedRoomAliasId) -> Result<(), ()> { |
||||
let mut event_content = self |
||||
.canonical_alias_event_content() |
||||
.await? |
||||
.unwrap_or_default(); |
||||
|
||||
// Remove from the alt aliases, if it is there.
|
||||
let alt_alias_pos = event_content.alt_aliases.iter().position(|a| a == alias); |
||||
if let Some(pos) = alt_alias_pos { |
||||
event_content.alt_aliases.remove(pos); |
||||
} else { |
||||
// Nothing to do.
|
||||
return Err(()); |
||||
} |
||||
|
||||
let Some(room) = self.room() else { |
||||
return Err(()); |
||||
}; |
||||
|
||||
let matrix_room = room.matrix_room().clone(); |
||||
let handle = spawn_tokio!(async move { matrix_room.send_state_event(event_content).await }); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(_) => Ok(()), |
||||
Err(error) => { |
||||
error!("Failed to remove alt alias: {error}"); |
||||
Err(()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Set the given alias to be an alt alias.
|
||||
///
|
||||
/// Removes the given alias from the alt aliases if it is in the list.
|
||||
pub async fn add_alt_alias(&self, alias: OwnedRoomAliasId) -> Result<(), AddAltAliasError> { |
||||
let Ok(event_content) = self.canonical_alias_event_content().await else { |
||||
return Err(AddAltAliasError::Other); |
||||
}; |
||||
|
||||
let mut event_content = event_content.unwrap_or_default(); |
||||
|
||||
// Do nothing if it is already present.
|
||||
if event_content.alias.as_ref().is_some_and(|a| *a == alias) |
||||
|| event_content.alt_aliases.contains(&alias) |
||||
{ |
||||
error!("Cannot add alias already listed"); |
||||
return Err(AddAltAliasError::Other); |
||||
} |
||||
|
||||
let Some(room) = self.room() else { |
||||
return Err(AddAltAliasError::Other); |
||||
}; |
||||
|
||||
let matrix_room = room.matrix_room().clone(); |
||||
|
||||
// Check that the alias exists and points to the proper room.
|
||||
let client = matrix_room.client(); |
||||
let alias_clone = alias.clone(); |
||||
let handle = spawn_tokio!(async move { client.resolve_room_alias(&alias_clone).await }); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(response) => { |
||||
if response.room_id != matrix_room.room_id() { |
||||
error!("Cannot add alias that points to other room"); |
||||
return Err(AddAltAliasError::InvalidRoomId); |
||||
} |
||||
} |
||||
Err(error) => { |
||||
error!("Failed to check room alias: {error}"); |
||||
if error |
||||
.as_client_api_error() |
||||
.is_some_and(|e| e.status_code == StatusCode::NOT_FOUND) |
||||
{ |
||||
return Err(AddAltAliasError::NotRegistered); |
||||
} else { |
||||
return Err(AddAltAliasError::Other); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Add as alt alias.
|
||||
event_content.alt_aliases.push(alias); |
||||
let handle = spawn_tokio!(async move { matrix_room.send_state_event(event_content).await }); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(_) => Ok(()), |
||||
Err(error) => { |
||||
error!("Failed to add alt alias: {error}"); |
||||
Err(AddAltAliasError::Other) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Get the local aliases registered on the homeserver.
|
||||
pub async fn local_aliases(&self) -> Result<Vec<OwnedRoomAliasId>, ()> { |
||||
let Some(room) = self.room() else { |
||||
return Err(()); |
||||
}; |
||||
|
||||
let matrix_room = room.matrix_room(); |
||||
let client = matrix_room.client(); |
||||
let room_id = matrix_room.room_id().to_owned(); |
||||
|
||||
let handle = spawn_tokio!(async move { |
||||
client |
||||
.send(room::aliases::v3::Request::new(room_id), None) |
||||
.await |
||||
}); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(response) => Ok(response.aliases), |
||||
Err(error) => { |
||||
error!("Failed to fetch local room aliases: {error}"); |
||||
Err(()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Unregister the given local alias.
|
||||
pub async fn unregister_local_alias(&self, alias: OwnedRoomAliasId) -> Result<(), ()> { |
||||
let Some(room) = self.room() else { |
||||
return Err(()); |
||||
}; |
||||
|
||||
// Check that the alias exists and points to the proper room.
|
||||
let matrix_room = room.matrix_room(); |
||||
let client = matrix_room.client(); |
||||
|
||||
let request = delete_alias::v3::Request::new(alias); |
||||
let handle = spawn_tokio!(async move { client.send(request, None).await }); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(_) => Ok(()), |
||||
Err(error) => { |
||||
error!("Failed to unregister local alias: {error}"); |
||||
Err(()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Register the given local alias.
|
||||
pub async fn register_local_alias( |
||||
&self, |
||||
alias: OwnedRoomAliasId, |
||||
) -> Result<(), RegisterLocalAliasError> { |
||||
let Some(room) = self.room() else { |
||||
return Err(RegisterLocalAliasError::Other); |
||||
}; |
||||
|
||||
// Check that the alias exists and points to the proper room.
|
||||
let matrix_room = room.matrix_room(); |
||||
let client = matrix_room.client(); |
||||
let room_id = matrix_room.room_id().to_owned(); |
||||
|
||||
let request = create_alias::v3::Request::new(alias, room_id); |
||||
let handle = spawn_tokio!(async move { client.send(request, None).await }); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(_) => Ok(()), |
||||
Err(error) => { |
||||
error!("Failed to register local alias: {error}"); |
||||
|
||||
if error |
||||
.as_client_api_error() |
||||
.is_some_and(|e| e.status_code == StatusCode::CONFLICT) |
||||
{ |
||||
Err(RegisterLocalAliasError::AlreadyInUse) |
||||
} else { |
||||
Err(RegisterLocalAliasError::Other) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Connect to the signal emitted when the aliases changed.
|
||||
pub fn connect_changed<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId { |
||||
self.connect_closure( |
||||
"changed", |
||||
true, |
||||
closure_local!(move |obj: Self| { |
||||
f(&obj); |
||||
}), |
||||
) |
||||
} |
||||
} |
||||
|
||||
impl Default for RoomAliases { |
||||
fn default() -> Self { |
||||
Self::new() |
||||
} |
||||
} |
||||
|
||||
/// All high-level errors that can happen when trying to add an alt alias.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
||||
pub enum AddAltAliasError { |
||||
/// The alias is not registered.
|
||||
NotRegistered, |
||||
/// The alias is not registered to this room.
|
||||
InvalidRoomId, |
||||
/// An other error occurred.
|
||||
Other, |
||||
} |
||||
|
||||
/// All high-level errors that can happen when trying to register a local alias.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
||||
pub enum RegisterLocalAliasError { |
||||
/// The alias is already registered.
|
||||
AlreadyInUse, |
||||
/// An other error occurred.
|
||||
Other, |
||||
} |
||||
@ -0,0 +1,357 @@
|
||||
use adw::prelude::*; |
||||
use gtk::{gdk, gio, glib, glib::clone, pango, subclass::prelude::*, CompositeTemplate}; |
||||
use tracing::error; |
||||
|
||||
use crate::utils::BoundObjectWeakRef; |
||||
|
||||
mod imp { |
||||
use std::cell::RefCell; |
||||
|
||||
use glib::subclass::InitializingObject; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, CompositeTemplate, glib::Properties)] |
||||
#[template(
|
||||
resource = "/org/gnome/Fractal/ui/session/view/content/room_details/addresses_subpage/completion_popover.ui" |
||||
)] |
||||
#[properties(wrapper_type = super::CompletionPopover)] |
||||
pub struct CompletionPopover { |
||||
#[template_child] |
||||
pub list: TemplateChild<gtk::ListBox>, |
||||
/// The parent entry to autocomplete.
|
||||
#[property(get, set = Self::set_entry, explicit_notify, nullable)] |
||||
pub entry: BoundObjectWeakRef<gtk::Editable>, |
||||
/// The key controller added to the parent entry.
|
||||
entry_controller: RefCell<Option<gtk::EventControllerKey>>, |
||||
entry_binding: RefCell<Option<glib::Binding>>, |
||||
/// The list model to use for completion.
|
||||
///
|
||||
/// Only supports `GtkStringObject` items.
|
||||
#[property(get, set = Self::set_model, explicit_notify, nullable)] |
||||
pub model: RefCell<Option<gio::ListModel>>, |
||||
/// The string filter.
|
||||
#[property(get)] |
||||
pub filter: gtk::StringFilter, |
||||
/// The filtered list model.
|
||||
#[property(get)] |
||||
pub filtered_list: gtk::FilterListModel, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for CompletionPopover { |
||||
const NAME: &'static str = "RoomDetailsAddressesSubpageCompletionPopover"; |
||||
type Type = super::CompletionPopover; |
||||
type ParentType = gtk::Popover; |
||||
|
||||
fn class_init(klass: &mut Self::Class) { |
||||
Self::bind_template(klass); |
||||
Self::Type::bind_template_callbacks(klass); |
||||
} |
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) { |
||||
obj.init_template(); |
||||
} |
||||
} |
||||
|
||||
#[glib::derived_properties] |
||||
impl ObjectImpl for 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 => move |_,_,_,_| { |
||||
obj.update_completion(); |
||||
})); |
||||
|
||||
self.list.bind_model(Some(&self.filtered_list), |item| { |
||||
let Some(item) = item.downcast_ref::<gtk::StringObject>() else { |
||||
error!("Completion has item that is not a GtkStringObject"); |
||||
return adw::Bin::new().upcast(); |
||||
}; |
||||
|
||||
let label = gtk::Label::builder() |
||||
.label(item.string()) |
||||
.ellipsize(pango::EllipsizeMode::End) |
||||
.halign(gtk::Align::Start) |
||||
.build(); |
||||
|
||||
gtk::ListBoxRow::builder().child(&label).build().upcast() |
||||
}); |
||||
} |
||||
|
||||
fn dispose(&self) { |
||||
if let Some(entry) = self.entry.obj() { |
||||
if let Some(controller) = self.entry_controller.take() { |
||||
entry.remove_controller(&controller) |
||||
} |
||||
} |
||||
|
||||
if let Some(binding) = self.entry_binding.take() { |
||||
binding.unbind(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl WidgetImpl for CompletionPopover {} |
||||
impl PopoverImpl for CompletionPopover {} |
||||
|
||||
impl CompletionPopover { |
||||
/// Set the parent entry to autocomplete.
|
||||
fn set_entry(&self, entry: Option<>k::Editable>) { |
||||
let prev_entry = self.entry.obj(); |
||||
|
||||
if prev_entry.as_ref() == entry { |
||||
return; |
||||
} |
||||
let obj = self.obj(); |
||||
|
||||
if let Some(entry) = prev_entry { |
||||
if let Some(controller) = self.entry_controller.take() { |
||||
entry.remove_controller(&controller) |
||||
} |
||||
|
||||
obj.unparent(); |
||||
} |
||||
if let Some(binding) = self.entry_binding.take() { |
||||
binding.unbind(); |
||||
} |
||||
self.entry.disconnect_signals(); |
||||
|
||||
if let Some(entry) = entry { |
||||
let key_events = gtk::EventControllerKey::new(); |
||||
key_events.connect_key_pressed(clone!(@weak obj => @default-return 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 |
||||
})); |
||||
|
||||
entry.add_controller(key_events.clone()); |
||||
self.entry_controller.replace(Some(key_events)); |
||||
|
||||
let search_binding = entry |
||||
.bind_property("text", &self.filter, "search") |
||||
.sync_create() |
||||
.build(); |
||||
self.entry_binding.replace(Some(search_binding)); |
||||
|
||||
let changed_handler = entry.connect_changed(clone!(@weak obj => move |_| { |
||||
obj.update_completion(); |
||||
})); |
||||
|
||||
let state_flags_handler = |
||||
entry.connect_state_flags_changed(clone!(@weak obj => move |_, _| { |
||||
obj.update_completion(); |
||||
})); |
||||
|
||||
obj.set_parent(entry); |
||||
self.entry |
||||
.set(entry, vec![changed_handler, state_flags_handler]); |
||||
} |
||||
|
||||
self.obj().notify_entry(); |
||||
} |
||||
|
||||
/// Set the list model to use for completion.
|
||||
fn set_model(&self, model: Option<gio::ListModel>) { |
||||
if *self.model.borrow() == model { |
||||
return; |
||||
} |
||||
|
||||
self.filtered_list.set_model(model.as_ref()); |
||||
|
||||
self.model.replace(model); |
||||
self.obj().notify_model(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// A popover to auto-complete strings for a `gtk::Editable`.
|
||||
pub struct CompletionPopover(ObjectSubclass<imp::CompletionPopover>) |
||||
@extends gtk::Widget, gtk::Popover, @implements gtk::Accessible; |
||||
} |
||||
|
||||
#[gtk::template_callbacks] |
||||
impl CompletionPopover { |
||||
pub fn new() -> Self { |
||||
glib::Object::new() |
||||
} |
||||
|
||||
/// Update completion.
|
||||
fn update_completion(&self) { |
||||
let Some(entry) = self.entry() else { |
||||
return; |
||||
}; |
||||
|
||||
let imp = self.imp(); |
||||
let n_items = imp.filtered_list.n_items(); |
||||
|
||||
// Always hide the popover if it's empty.
|
||||
if n_items == 0 { |
||||
if self.is_visible() { |
||||
self.popdown(); |
||||
} |
||||
|
||||
return; |
||||
} |
||||
|
||||
// Always hide the popover if it has a single item that is exactly the text of
|
||||
// the entry.
|
||||
if n_items == 1 { |
||||
if let Some(item) = imp |
||||
.filtered_list |
||||
.item(0) |
||||
.and_downcast::<gtk::StringObject>() |
||||
{ |
||||
if item.string() == entry.text() { |
||||
if self.is_visible() { |
||||
self.popdown(); |
||||
} |
||||
|
||||
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 !self.is_visible() { |
||||
self.popup(); |
||||
} |
||||
} else if self.is_visible() { |
||||
self.popdown(); |
||||
} |
||||
} |
||||
|
||||
fn selected_row_index(&self) -> Option<usize> { |
||||
let imp = self.imp(); |
||||
|
||||
let selected_text = self.selected_text()?; |
||||
|
||||
imp.filtered_list.iter::<glib::Object>().position(|o| { |
||||
o.ok() |
||||
.and_downcast::<gtk::StringObject>() |
||||
.is_some_and(|o| o.string() == selected_text) |
||||
}) |
||||
} |
||||
|
||||
fn select_row_at_index(&self, idx: Option<usize>) { |
||||
let imp = self.imp(); |
||||
|
||||
if self.selected_row_index() == idx || idx >= Some(imp.filtered_list.n_items() as usize) { |
||||
return; |
||||
} |
||||
|
||||
let imp = self.imp(); |
||||
|
||||
if let Some(row) = idx.and_then(|idx| imp.list.row_at_index(idx as i32)) { |
||||
imp.list.select_row(Some(&row)); |
||||
} else { |
||||
imp.list.select_row(None::<>k::ListBoxRow>); |
||||
} |
||||
} |
||||
|
||||
/// The text of the selected row, if any.
|
||||
pub fn selected_text(&self) -> Option<glib::GString> { |
||||
Some( |
||||
self.imp() |
||||
.list |
||||
.selected_row()? |
||||
.child()? |
||||
.downcast_ref::<gtk::Label>()? |
||||
.label(), |
||||
) |
||||
} |
||||
|
||||
/// 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; |
||||
}; |
||||
|
||||
let Some(selected_text) = self.selected_text() else { |
||||
return false; |
||||
}; |
||||
|
||||
if selected_text == entry.text() { |
||||
// Activating the row would have no effect.
|
||||
return false; |
||||
} |
||||
|
||||
let Some(row) = self.imp().list.selected_row() else { |
||||
return false; |
||||
}; |
||||
|
||||
row.activate(); |
||||
true |
||||
} |
||||
|
||||
/// Handle a row being activated.
|
||||
#[template_callback] |
||||
fn row_activated(&self, row: >k::ListBoxRow) { |
||||
let Some(label) = row.child().and_downcast::<gtk::Label>() else { |
||||
return; |
||||
}; |
||||
let Some(entry) = self.entry() else { |
||||
return; |
||||
}; |
||||
|
||||
entry.set_text(&label.label()); |
||||
|
||||
self.popdown(); |
||||
entry.grab_focus(); |
||||
} |
||||
} |
||||
|
||||
impl Default for CompletionPopover { |
||||
fn default() -> Self { |
||||
Self::new() |
||||
} |
||||
} |
||||
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<interface> |
||||
<template class="RoomDetailsAddressesSubpageCompletionPopover" parent="GtkPopover"> |
||||
<accessibility> |
||||
<property name="label" translatable="yes">Address auto-completion</property> |
||||
</accessibility> |
||||
<style> |
||||
<class name="string-row-list"/> |
||||
</style> |
||||
<property name="autohide">false</property> |
||||
<property name="has-arrow">false</property> |
||||
<property name="position">bottom</property> |
||||
<property name="halign">start</property> |
||||
<property name="valign">center</property> |
||||
<property name="width-request">260</property> |
||||
<property name="child"> |
||||
<object class="GtkScrolledWindow" id="scrolled_window"> |
||||
<property name="propagate-natural-height">true</property> |
||||
<property name="hscrollbar-policy">never</property> |
||||
<property name="max-content-height">280</property> |
||||
<property name="child"> |
||||
<object class="GtkListBox" id="list"> |
||||
<signal name="row-activated" handler="row_activated" swapped="yes"/> |
||||
</object> |
||||
</property> |
||||
</object> |
||||
</property> |
||||
</template> |
||||
</interface> |
||||
@ -0,0 +1,776 @@
|
||||
use adw::{prelude::*, subclass::prelude::*}; |
||||
use gettextrs::gettext; |
||||
use gtk::{gio, glib, glib::clone, pango, CompositeTemplate}; |
||||
use ruma::RoomAliasId; |
||||
use tracing::error; |
||||
|
||||
mod completion_popover; |
||||
mod public_address; |
||||
|
||||
use self::{completion_popover::CompletionPopover, public_address::PublicAddress}; |
||||
use crate::{ |
||||
components::{EntryAddRow, RemovableRow, SpinnerButton, SubstringEntryRow}, |
||||
gettext_f, |
||||
prelude::*, |
||||
session::model::{AddAltAliasError, RegisterLocalAliasError, Room}, |
||||
spawn, toast, |
||||
utils::DummyObject, |
||||
}; |
||||
|
||||
mod imp { |
||||
use std::{ |
||||
cell::{OnceCell, RefCell}, |
||||
collections::HashSet, |
||||
}; |
||||
|
||||
use glib::subclass::InitializingObject; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, CompositeTemplate, glib::Properties)] |
||||
#[template(
|
||||
resource = "/org/gnome/Fractal/ui/session/view/content/room_details/addresses_subpage/mod.ui" |
||||
)] |
||||
#[properties(wrapper_type = super::AddressesSubpage)] |
||||
pub struct AddressesSubpage { |
||||
#[template_child] |
||||
pub public_addresses_list: TemplateChild<gtk::ListBox>, |
||||
#[template_child] |
||||
pub public_addresses_error_revealer: TemplateChild<gtk::Revealer>, |
||||
#[template_child] |
||||
pub public_addresses_error: TemplateChild<gtk::Label>, |
||||
#[template_child] |
||||
pub local_addresses_group: TemplateChild<adw::PreferencesGroup>, |
||||
#[template_child] |
||||
pub local_addresses_list: TemplateChild<gtk::ListBox>, |
||||
#[template_child] |
||||
pub local_addresses_error_revealer: TemplateChild<gtk::Revealer>, |
||||
#[template_child] |
||||
pub local_addresses_error: TemplateChild<gtk::Label>, |
||||
#[template_child] |
||||
pub public_addresses_add_row: TemplateChild<EntryAddRow>, |
||||
#[template_child] |
||||
pub local_addresses_add_row: TemplateChild<SubstringEntryRow>, |
||||
/// The room users will be invited to.
|
||||
#[property(get, set = Self::set_room, construct_only)] |
||||
pub room: glib::WeakRef<Room>, |
||||
/// The full list of public addresses.
|
||||
pub public_addresses: OnceCell<gio::ListStore>, |
||||
/// The full list of local addresses.
|
||||
pub local_addresses: gtk::StringList, |
||||
aliases_changed_handler: RefCell<Option<glib::SignalHandlerId>>, |
||||
pub public_addresses_completion: CompletionPopover, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for AddressesSubpage { |
||||
const NAME: &'static str = "RoomDetailsAddressesSubpage"; |
||||
type Type = super::AddressesSubpage; |
||||
type ParentType = adw::NavigationPage; |
||||
|
||||
fn class_init(klass: &mut Self::Class) { |
||||
Self::bind_template(klass); |
||||
Self::Type::bind_template_callbacks(klass); |
||||
} |
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) { |
||||
obj.init_template(); |
||||
} |
||||
} |
||||
|
||||
#[glib::derived_properties] |
||||
impl ObjectImpl for AddressesSubpage { |
||||
fn constructed(&self) { |
||||
self.parent_constructed(); |
||||
let obj = self.obj(); |
||||
|
||||
let extra_items = gio::ListStore::new::<glib::Object>(); |
||||
extra_items.append(&DummyObject::new("add")); |
||||
|
||||
// Public addresses.
|
||||
let public_items = gio::ListStore::new::<glib::Object>(); |
||||
public_items.append(self.public_addresses()); |
||||
public_items.append(&extra_items); |
||||
|
||||
let flattened_public_list = gtk::FlattenListModel::new(Some(public_items)); |
||||
self.public_addresses_list.bind_model( |
||||
Some(&flattened_public_list), |
||||
clone!( |
||||
@weak obj => @default-return { adw::ActionRow::new().upcast() }, |
||||
move |item| obj.create_public_address_row(item) |
||||
), |
||||
); |
||||
|
||||
self.public_addresses_add_row |
||||
.connect_changed(clone!(@weak obj => move |_| { |
||||
obj.update_public_addresses_add_row(); |
||||
})); |
||||
|
||||
// Filter addresses already in the list.
|
||||
let new_addresses_filter = gtk::CustomFilter::new( |
||||
clone!(@weak self as imp => @default-return false, move |item: &glib::Object| { |
||||
let Some(item) = item.downcast_ref::<gtk::StringObject>() else { |
||||
return false; |
||||
}; |
||||
|
||||
let address = item.string(); |
||||
|
||||
for public_address in imp.public_addresses().iter::<PublicAddress>() { |
||||
let Ok(public_address) = public_address else { |
||||
// The iterator is broken.
|
||||
break; |
||||
}; |
||||
|
||||
if public_address.alias().as_str() == address { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
true |
||||
}), |
||||
); |
||||
|
||||
// Update the filtered list everytime an item changes.
|
||||
self.public_addresses().connect_items_changed( |
||||
clone!(@weak new_addresses_filter => move |_,_,_,_| { |
||||
new_addresses_filter.changed(gtk::FilterChange::Different); |
||||
}), |
||||
); |
||||
|
||||
let new_local_addresses = gtk::FilterListModel::new( |
||||
Some(self.local_addresses.clone()), |
||||
Some(new_addresses_filter), |
||||
); |
||||
|
||||
self.public_addresses_completion |
||||
.set_model(Some(new_local_addresses)); |
||||
self.public_addresses_completion.set_entry(Some( |
||||
self.public_addresses_add_row.upcast_ref::<gtk::Editable>(), |
||||
)); |
||||
|
||||
// Local addresses.
|
||||
let local_items = gio::ListStore::new::<glib::Object>(); |
||||
local_items.append(&self.local_addresses); |
||||
local_items.append(&extra_items); |
||||
|
||||
let flattened_local_list = gtk::FlattenListModel::new(Some(local_items)); |
||||
self.local_addresses_list.bind_model( |
||||
Some(&flattened_local_list), |
||||
clone!( |
||||
@weak obj => @default-return { adw::ActionRow::new().upcast() }, |
||||
move |item| obj.create_local_address_row(item) |
||||
), |
||||
); |
||||
|
||||
self.local_addresses_add_row |
||||
.connect_changed(clone!(@weak obj => move |_| { |
||||
obj.update_local_addresses_add_row(); |
||||
})); |
||||
} |
||||
|
||||
fn dispose(&self) { |
||||
if let Some(room) = self.room.upgrade() { |
||||
if let Some(handler) = self.aliases_changed_handler.take() { |
||||
room.aliases().disconnect(handler); |
||||
} |
||||
} |
||||
|
||||
self.public_addresses_completion.unparent(); |
||||
} |
||||
} |
||||
|
||||
impl WidgetImpl for AddressesSubpage {} |
||||
impl NavigationPageImpl for AddressesSubpage {} |
||||
|
||||
impl AddressesSubpage { |
||||
pub(super) fn public_addresses(&self) -> &gio::ListStore { |
||||
self.public_addresses |
||||
.get_or_init(gio::ListStore::new::<PublicAddress>) |
||||
} |
||||
|
||||
/// Set the room users will be invited to.
|
||||
fn set_room(&self, room: Room) { |
||||
let aliases = room.aliases(); |
||||
|
||||
let aliases_changed_handler = |
||||
aliases.connect_changed(clone!(@weak self as imp => move |_| { |
||||
imp.update_public_addresses(); |
||||
})); |
||||
self.aliases_changed_handler |
||||
.replace(Some(aliases_changed_handler)); |
||||
|
||||
self.room.set(Some(&room)); |
||||
|
||||
self.obj().notify_room(); |
||||
self.update_public_addresses(); |
||||
self.update_local_addresses_server(); |
||||
|
||||
spawn!(clone!(@weak self as imp => async move { |
||||
imp.update_local_addresses().await; |
||||
})); |
||||
} |
||||
|
||||
/// Update the list of public addresses.
|
||||
fn update_public_addresses(&self) { |
||||
let Some(room) = self.room.upgrade() else { |
||||
return; |
||||
}; |
||||
|
||||
let aliases = room.aliases(); |
||||
let canonical_alias = aliases.canonical_alias(); |
||||
let alt_aliases = aliases.alt_aliases(); |
||||
|
||||
// Map of `(alias, is_main)`.
|
||||
let mut public_aliases = canonical_alias |
||||
.into_iter() |
||||
.map(|a| (a, true)) |
||||
.chain(alt_aliases.into_iter().map(|a| (a, false))) |
||||
.collect::<Vec<_>>(); |
||||
|
||||
let public_addresses = self.public_addresses(); |
||||
|
||||
// Remove aliases that are not in the list anymore and update the main alias.
|
||||
let mut i = 0; |
||||
while i < public_addresses.n_items() { |
||||
let Some(item) = public_addresses.item(i).and_downcast::<PublicAddress>() else { |
||||
break; |
||||
}; |
||||
|
||||
let position = public_aliases |
||||
.iter() |
||||
.position(|(alias, _)| item.alias() == alias); |
||||
|
||||
if let Some(position) = position { |
||||
// It is in the list, update whether it is the main alias.
|
||||
let (_, is_main) = public_aliases.remove(position); |
||||
item.set_is_main(is_main); |
||||
|
||||
i += 1; |
||||
} else { |
||||
// It is not in the list, remove.
|
||||
public_addresses.remove(i); |
||||
} |
||||
} |
||||
|
||||
// If there are new aliases in the list, append them.
|
||||
if !public_aliases.is_empty() { |
||||
let new_aliases = public_aliases |
||||
.into_iter() |
||||
.map(|(alias, is_main)| PublicAddress::new(alias, is_main)) |
||||
.collect::<Vec<_>>(); |
||||
public_addresses.splice(public_addresses.n_items(), 0, &new_aliases); |
||||
} |
||||
|
||||
self.reset_public_addresses_state(); |
||||
} |
||||
|
||||
/// Reset the public addresses section UI state.
|
||||
fn reset_public_addresses_state(&self) { |
||||
// Reset the list.
|
||||
self.public_addresses_list.set_sensitive(true); |
||||
|
||||
// Reset the rows loading state.
|
||||
for i in 0..self.public_addresses().n_items() { |
||||
let Some(row) = self |
||||
.public_addresses_list |
||||
.row_at_index(i as i32) |
||||
.and_downcast::<RemovableRow>() |
||||
else { |
||||
break; |
||||
}; |
||||
|
||||
row.set_is_loading(false); |
||||
|
||||
if let Some(button) = row.extra_suffix().and_downcast::<SpinnerButton>() { |
||||
button.set_loading(false); |
||||
} |
||||
} |
||||
|
||||
self.public_addresses_add_row.set_is_loading(false); |
||||
} |
||||
|
||||
/// Update the server of the local addresses.
|
||||
fn update_local_addresses_server(&self) { |
||||
let Some(room) = self.room.upgrade() else { |
||||
return; |
||||
}; |
||||
let own_member = room.own_member(); |
||||
let server_name = own_member.user_id().server_name(); |
||||
|
||||
self.local_addresses_group.set_title(&gettext_f( |
||||
// Translators: Do NOT translate the content between '{' and '}',
|
||||
// this is a variable name.
|
||||
"Local Addresses on {homeserver}", |
||||
&[("homeserver", server_name.as_str())], |
||||
)); |
||||
self.local_addresses_add_row |
||||
.set_suffix_text(format!(":{server_name}")); |
||||
} |
||||
|
||||
/// Update the list of local addresses.
|
||||
pub(super) async fn update_local_addresses(&self) { |
||||
let Some(room) = self.room.upgrade() else { |
||||
return; |
||||
}; |
||||
|
||||
let aliases = room.aliases(); |
||||
|
||||
let Ok(local_aliases) = aliases.local_aliases().await else { |
||||
return; |
||||
}; |
||||
|
||||
let mut local_aliases = local_aliases |
||||
.into_iter() |
||||
.map(String::from) |
||||
.collect::<HashSet<_>>(); |
||||
|
||||
// Remove aliases that are not in the list anymore.
|
||||
let mut i = 0; |
||||
while i < self.local_addresses.n_items() { |
||||
let Some(item) = self |
||||
.local_addresses |
||||
.item(i) |
||||
.and_downcast::<gtk::StringObject>() |
||||
else { |
||||
break; |
||||
}; |
||||
|
||||
let address = String::from(item.string()); |
||||
|
||||
if !local_aliases.remove(&address) { |
||||
self.local_addresses.remove(i); |
||||
} else { |
||||
i += 1; |
||||
} |
||||
} |
||||
|
||||
// If there are new aliases in the list, append them.
|
||||
if !local_aliases.is_empty() { |
||||
let new_aliases = local_aliases.iter().map(|s| s.as_str()).collect::<Vec<_>>(); |
||||
self.local_addresses |
||||
.splice(self.local_addresses.n_items(), 0, &new_aliases); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// Subpage to invite new members to a room.
|
||||
pub struct AddressesSubpage(ObjectSubclass<imp::AddressesSubpage>) |
||||
@extends gtk::Widget, gtk::Window, adw::NavigationPage, @implements gtk::Accessible; |
||||
} |
||||
|
||||
#[gtk::template_callbacks] |
||||
impl AddressesSubpage { |
||||
pub fn new(room: &Room) -> Self { |
||||
glib::Object::builder().property("room", room).build() |
||||
} |
||||
|
||||
/// Create a row for the given item in the public addresses section.
|
||||
fn create_public_address_row(&self, item: &glib::Object) -> gtk::Widget { |
||||
let imp = self.imp(); |
||||
|
||||
if let Some(address) = item.downcast_ref::<PublicAddress>() { |
||||
let alias = address.alias(); |
||||
let row = RemovableRow::new(); |
||||
row.set_title(alias.as_str()); |
||||
row.set_remove_button_tooltip_text(Some(gettext("Remove address"))); |
||||
row.set_remove_button_accessible_label(Some(gettext_f( |
||||
// Translators: Do NOT translate the content between '{' and '}',
|
||||
// this is a variable name.
|
||||
"Remove “{address}”", |
||||
&[("address", alias.as_str())], |
||||
))); |
||||
|
||||
address.connect_is_main_notify(clone!(@weak self as obj, @weak row => move |address| { |
||||
obj.update_public_row_is_main(&row, address.is_main()); |
||||
})); |
||||
self.update_public_row_is_main(&row, address.is_main()); |
||||
|
||||
row.connect_remove(clone!(@weak self as obj => move |row| { |
||||
spawn!(clone!(@weak row => async move { |
||||
obj.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::<SpinnerButton>()) { |
||||
let button = SpinnerButton::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 self as obj, @weak row => move |_| { |
||||
spawn!(async move { |
||||
obj.set_main_public_address(&row).await; |
||||
}); |
||||
})); |
||||
|
||||
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); |
||||
} |
||||
} |
||||
|
||||
/// 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::<SpinnerButton>() 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_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_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()); |
||||
} |
||||
|
||||
/// Activate the auto-completion of the public addresses add row.
|
||||
#[template_callback] |
||||
fn handle_public_addresses_add_row_activated(&self) { |
||||
if !self |
||||
.imp() |
||||
.public_addresses_completion |
||||
.activate_selected_row() |
||||
{ |
||||
self.add_public_address(); |
||||
} |
||||
} |
||||
|
||||
/// Add a an address to the public list.
|
||||
#[template_callback] |
||||
fn add_public_address(&self) { |
||||
let Some(room) = self.room() else { |
||||
return; |
||||
}; |
||||
let imp = self.imp(); |
||||
|
||||
if !self.can_add_public_address() { |
||||
return; |
||||
} |
||||
|
||||
let row = &imp.public_addresses_add_row; |
||||
|
||||
let Ok(alias) = RoomAliasId::parse(row.text()) else { |
||||
error!("Cannot add public address with invalid alias"); |
||||
return; |
||||
}; |
||||
|
||||
imp.public_addresses_list.set_sensitive(false); |
||||
row.set_is_loading(true); |
||||
imp.public_addresses_error_revealer.set_reveal_child(false); |
||||
|
||||
let aliases = room.aliases(); |
||||
|
||||
spawn!(clone!(@weak self as obj => async move { |
||||
let imp = obj.imp(); |
||||
let row = &imp.public_addresses_add_row; |
||||
|
||||
match aliases.add_alt_alias(alias).await { |
||||
Ok(()) => { |
||||
row.set_text(""); |
||||
} |
||||
Err(error) => { |
||||
toast!(obj, gettext("Could not add public address")); |
||||
|
||||
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, |
||||
}; |
||||
|
||||
if let Some(label) = label { |
||||
imp.public_addresses_error.set_label(&label); |
||||
imp.public_addresses_error_revealer.set_reveal_child(true); |
||||
} |
||||
|
||||
imp.public_addresses_list.set_sensitive(true); |
||||
row.set_is_loading(false); |
||||
} |
||||
} |
||||
})); |
||||
} |
||||
|
||||
/// 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(); |
||||
|
||||
// Cannot add an empty address.
|
||||
if new_address.is_empty() { |
||||
return false; |
||||
} |
||||
|
||||
// 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 imp.public_addresses().iter::<PublicAddress>() { |
||||
let Ok(public_address) = public_address else { |
||||
// The iterator is broken.
|
||||
return false; |
||||
}; |
||||
|
||||
if *public_address.alias() == new_alias { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
true |
||||
} |
||||
|
||||
/// 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(); |
||||
|
||||
if let Some(string_obj) = item.downcast_ref::<gtk::StringObject>() { |
||||
let alias = string_obj.string(); |
||||
let row = RemovableRow::new(); |
||||
row.set_title(&alias); |
||||
row.set_remove_button_tooltip_text(Some(gettext("Unregister local address"))); |
||||
row.set_remove_button_accessible_label(Some(gettext_f( |
||||
// Translators: Do NOT translate the content between '{' and '}',
|
||||
// this is a variable name.
|
||||
"Unregister “{address}”", |
||||
&[("address", &alias)], |
||||
))); |
||||
|
||||
row.connect_remove(clone!(@weak self as obj => move |row| { |
||||
spawn!(clone!(@weak row => async move { |
||||
obj.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); |
||||
|
||||
if aliases.unregister_local_alias(alias).await.is_err() { |
||||
toast!(self, gettext("Could not unregister local address")); |
||||
} |
||||
|
||||
self.imp().update_local_addresses().await; |
||||
|
||||
row.set_is_loading(false); |
||||
} |
||||
|
||||
/// The full new address in the public addresses add row.
|
||||
///
|
||||
/// Returns `None` if the localpart is empty.
|
||||
fn new_local_address(&self) -> Option<String> { |
||||
let row = &self.imp().local_addresses_add_row; |
||||
let localpart = row.text(); |
||||
|
||||
if localpart.is_empty() { |
||||
return None; |
||||
} |
||||
|
||||
let server_name = row.suffix_text(); |
||||
Some(format!("#{localpart}{server_name}")) |
||||
} |
||||
|
||||
/// 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()); |
||||
|
||||
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); |
||||
} |
||||
|
||||
/// Register a local address.
|
||||
#[template_callback] |
||||
fn register_local_address(&self) { |
||||
let Some(room) = self.room() else { |
||||
return; |
||||
}; |
||||
|
||||
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 imp = self.imp(); |
||||
imp.local_addresses_add_row.set_is_loading(true); |
||||
imp.local_addresses_error_revealer.set_reveal_child(false); |
||||
|
||||
let aliases = room.aliases(); |
||||
|
||||
spawn!(clone!(@weak self as obj => async move { |
||||
let imp = obj.imp(); |
||||
let row = &imp.local_addresses_add_row; |
||||
|
||||
match aliases.register_local_alias(alias).await { |
||||
Ok(()) => { |
||||
row.set_text(""); |
||||
} |
||||
Err(error) => { |
||||
toast!(obj, gettext("Could not register local address")); |
||||
|
||||
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); |
||||
} |
||||
} |
||||
} |
||||
|
||||
imp.update_local_addresses().await; |
||||
|
||||
row.set_is_loading(false); |
||||
})); |
||||
} |
||||
|
||||
/// Whether the user can add the current address to the local list.
|
||||
fn can_register_local_address(&self) -> bool { |
||||
let imp = self.imp(); |
||||
|
||||
// Cannot add an empty address.
|
||||
let Some(new_address) = self.new_local_address() else { |
||||
return false; |
||||
}; |
||||
|
||||
// Cannot add an invalid alias.
|
||||
let Ok(new_alias) = RoomAliasId::parse(new_address) else { |
||||
return false; |
||||
}; |
||||
|
||||
// Cannot add a duplicate address.
|
||||
for local_address in imp.public_addresses().iter::<glib::Object>() { |
||||
let Some(local_address) = local_address.ok().and_downcast::<gtk::StringObject>() else { |
||||
// The iterator is broken.
|
||||
return true; |
||||
}; |
||||
|
||||
if local_address.string() == new_alias.as_str() { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
true |
||||
} |
||||
} |
||||
|
||||
/// Whether the given public row contains the main address.
|
||||
fn public_row_is_main(row: &RemovableRow) -> bool { |
||||
row.extra_suffix().is_some_and(|w| w.is::<gtk::Box>()) |
||||
} |
||||
@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<interface> |
||||
<template class="RoomDetailsAddressesSubpage" parent="AdwNavigationPage"> |
||||
<property name="title" translatable="yes">Edit Room Addresses</property> |
||||
<property name="child"> |
||||
<object class="AdwToolbarView"> |
||||
<child type="top"> |
||||
<object class="AdwHeaderBar"/> |
||||
</child> |
||||
<property name="content"> |
||||
<object class="AdwPreferencesPage"> |
||||
D<child> |
||||
<object class="AdwPreferencesGroup" id="public_addresses_group"> |
||||
<property name="title" translatable="yes">Public Addresses</property> |
||||
<property name="description" translatable="yes">Public addresses are advertised for all users in the room and the main address is used to identify a room publicly. Before adding an address to this list, it must be registered as a local address.</property> |
||||
<child> |
||||
<object class="GtkListBox" id="public_addresses_list"> |
||||
<style> |
||||
<class name="boxed-list"/> |
||||
</style> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkRevealer" id="public_addresses_error_revealer"> |
||||
<style> |
||||
<class name="entry-row-error-revealer"/> |
||||
</style> |
||||
<property name="child"> |
||||
<object class="GtkLabel" id="public_addresses_error"> |
||||
<property name="accessible-role">status</property> |
||||
<style> |
||||
<class name="caption"/> |
||||
<class name="error"/> |
||||
</style> |
||||
<property name="wrap">True</property> |
||||
<property name="wrap-mode">word-char</property> |
||||
<property name="xalign">0.0</property> |
||||
</object> |
||||
</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesGroup" id="local_addresses_group"> |
||||
<property name="description" translatable="yes">Local addresses can only be registered with your own homeserver. If they are not made public, only people on your homeserver can discover them.</property> |
||||
<child> |
||||
<object class="GtkListBox" id="local_addresses_list"> |
||||
<style> |
||||
<class name="boxed-list"/> |
||||
</style> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkRevealer" id="local_addresses_error_revealer"> |
||||
<style> |
||||
<class name="entry-row-error-revealer"/> |
||||
</style> |
||||
<property name="child"> |
||||
<object class="GtkLabel" id="local_addresses_error"> |
||||
<property name="accessible-role">status</property> |
||||
<style> |
||||
<class name="caption"/> |
||||
<class name="error"/> |
||||
</style> |
||||
<property name="wrap">True</property> |
||||
<property name="wrap-mode">word-char</property> |
||||
<property name="xalign">0.0</property> |
||||
</object> |
||||
</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</property> |
||||
</object> |
||||
</property> |
||||
</template> |
||||
<object class="EntryAddRow" id="public_addresses_add_row"> |
||||
<property name="title" translatable="yes">Add Public Address…</property> |
||||
<property name="add-button-tooltip-text" translatable="yes">Add Public Address</property> |
||||
<signal name="add" handler="add_public_address" swapped="yes"/> |
||||
<signal name="entry-activated" handler="handle_public_addresses_add_row_activated" swapped="yes"/> |
||||
</object> |
||||
<object class="SubstringEntryRow" id="local_addresses_add_row"> |
||||
<property name="title" translatable="yes">Register Local Address</property> |
||||
<property name="placeholder-text" translatable="yes">my-room</property> |
||||
<property name="accessible-description" translatable="yes">First part of the address, for example “my-room”</property> |
||||
<property name="prefix-text">#</property> |
||||
<property name="add-button-tooltip-text" translatable="yes">Register Local Address</property> |
||||
<signal name="add" handler="register_local_address" swapped="yes"/> |
||||
</object> |
||||
</interface> |
||||
@ -0,0 +1,63 @@
|
||||
use adw::subclass::prelude::*; |
||||
use gtk::{glib, prelude::*}; |
||||
use ruma::OwnedRoomAliasId; |
||||
|
||||
mod imp { |
||||
use std::cell::{Cell, OnceCell}; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, glib::Properties)] |
||||
#[properties(wrapper_type = super::PublicAddress)] |
||||
pub struct PublicAddress { |
||||
/// The room alias.
|
||||
pub alias: OnceCell<OwnedRoomAliasId>, |
||||
/// Whether this is the main address.
|
||||
#[property(get, set = Self::set_is_main, explicit_notify)] |
||||
pub is_main: Cell<bool>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for PublicAddress { |
||||
const NAME: &'static str = "RoomDetailsAddressesSubpagePublicAddress"; |
||||
type Type = super::PublicAddress; |
||||
} |
||||
|
||||
#[glib::derived_properties] |
||||
impl ObjectImpl for PublicAddress {} |
||||
|
||||
impl PublicAddress { |
||||
/// Set whether this is the main address.
|
||||
fn set_is_main(&self, is_main: bool) { |
||||
if self.is_main.get() == is_main { |
||||
return; |
||||
} |
||||
|
||||
self.is_main.set(is_main); |
||||
self.obj().notify_is_main(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// A public address.
|
||||
pub struct PublicAddress(ObjectSubclass<imp::PublicAddress>); |
||||
} |
||||
|
||||
impl PublicAddress { |
||||
/// Constructs a new `PublicAddress`.
|
||||
pub fn new(alias: OwnedRoomAliasId, is_main: bool) -> Self { |
||||
let obj = glib::Object::builder::<Self>() |
||||
.property("is-main", is_main) |
||||
.build(); |
||||
|
||||
obj.imp().alias.set(alias).unwrap(); |
||||
|
||||
obj |
||||
} |
||||
|
||||
/// The room alias.
|
||||
pub fn alias(&self) -> &OwnedRoomAliasId { |
||||
self.imp().alias.get().unwrap() |
||||
} |
||||
} |
||||
Loading…
Reference in new issue