fractal/session/view/content/room_details/addresses_subpage/
mod.rsuse 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, LoadingButton, RemovableRow, 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>,
#[property(get, set = Self::set_room, construct_only)]
pub room: glib::WeakRef<Room>,
pub public_addresses: OnceCell<gio::ListStore>,
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"));
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,
#[upgrade_or_else]
|| { 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();
}
));
let new_addresses_filter = gtk::CustomFilter::new(clone!(
#[weak(rename_to = imp)]
self,
#[upgrade_or]
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 {
break;
};
if public_address.alias().as_str() == address {
return false;
}
}
true
}
));
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>(),
));
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,
#[upgrade_or_else]
|| { 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>)
}
fn set_room(&self, room: &Room) {
let aliases = room.aliases();
let aliases_changed_handler = aliases.connect_changed(clone!(
#[weak(rename_to = imp)]
self,
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(rename_to = imp)]
self,
async move {
imp.update_local_addresses().await;
}
));
}
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();
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();
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 {
let (_, is_main) = public_aliases.remove(position);
item.set_is_main(is_main);
i += 1;
} else {
public_addresses.remove(i);
}
}
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();
}
fn reset_public_addresses_state(&self) {
self.public_addresses_list.set_sensitive(true);
let n_items = i32::try_from(self.public_addresses().n_items()).unwrap_or(i32::MAX);
for i in 0..n_items {
let Some(row) = self
.public_addresses_list
.row_at_index(i)
.and_downcast::<RemovableRow>()
else {
break;
};
row.set_is_loading(false);
if let Some(button) = row.extra_suffix().and_downcast::<LoadingButton>() {
button.set_is_loading(false);
}
}
self.public_addresses_add_row.set_is_loading(false);
}
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(
"Local Addresses on {homeserver}",
&[("homeserver", server_name.as_str())],
));
self.local_addresses_add_row
.set_suffix_text(format!(":{server_name}"));
}
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<_>>();
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) {
i += 1;
} else {
self.local_addresses.remove(i);
}
}
if !local_aliases.is_empty() {
let new_aliases = local_aliases.iter().map(String::as_str).collect::<Vec<_>>();
self.local_addresses
.splice(self.local_addresses.n_items(), 0, &new_aliases);
}
}
}
}
glib::wrapper! {
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()
}
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(
"Remove “{address}”",
&[("address", alias.as_str())],
)));
address.connect_is_main_notify(clone!(
#[weak(rename_to = obj)]
self,
#[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(rename_to = obj)]
self,
move |row| {
spawn!(clone!(
#[weak]
row,
async move {
obj.remove_public_address(&row).await;
}
));
}
));
row.upcast()
} else {
imp.public_addresses_add_row.clone().upcast()
}
}
fn update_public_row_is_main(&self, row: &RemovableRow, is_main: bool) {
if is_main && !public_row_is_main(row) {
let label = gtk::Label::builder()
.label(gettext("Main Address"))
.ellipsize(pango::EllipsizeMode::End)
.build();
let image = gtk::Image::builder()
.icon_name("checkmark-symbolic")
.accessible_role(gtk::AccessibleRole::Presentation)
.build();
let main_box = gtk::Box::builder()
.spacing(6)
.css_classes(["public-address-tag"])
.valign(gtk::Align::Center)
.build();
main_box.append(&image);
main_box.append(&label);
row.update_relation(&[gtk::accessible::Relation::DescribedBy(
&[label.upcast_ref()],
)]);
row.set_extra_suffix(Some(main_box));
} else if !is_main && !row.extra_suffix().is_some_and(|w| w.is::<LoadingButton>()) {
let button = LoadingButton::new();
button.set_content_icon_name("checkmark-symbolic");
button.add_css_class("flat");
button.set_tooltip_text(Some(&gettext("Set as main address")));
button.set_valign(gtk::Align::Center);
let accessible_label = gettext_f(
"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;
});
}
));
row.set_extra_suffix(Some(button));
}
}
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);
}
}
async fn set_main_public_address(&self, row: &RemovableRow) {
let Some(room) = self.room() else {
return;
};
let Some(button) = row.extra_suffix().and_downcast::<LoadingButton>() else {
return;
};
let Ok(alias) = RoomAliasId::parse(row.title()) else {
error!("Cannot set main public address with invalid alias");
return;
};
let imp = self.imp();
let aliases = room.aliases();
imp.public_addresses_list.set_sensitive(false);
button.set_is_loading(true);
if aliases.set_canonical_alias(alias).await.is_err() {
toast!(self, gettext("Could not set main public address"));
imp.public_addresses_list.set_sensitive(true);
button.set_is_loading(false);
}
}
fn update_public_addresses_add_row(&self) {
self.imp()
.public_addresses_add_row
.set_inhibit_add(!self.can_add_public_address());
}
#[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;
}
}
#[template_callback]
async fn add_public_address(&self) {
if !self.can_add_public_address() {
return;
}
let Some(room) = self.room() else {
return;
};
let imp = self.imp();
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();
match aliases.add_alt_alias(alias).await {
Ok(()) => {
row.set_text("");
}
Err(error) => {
toast!(self, 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);
}
}
}
fn can_add_public_address(&self) -> bool {
let imp = self.imp();
let new_address = imp.public_addresses_add_row.text();
if new_address.is_empty() {
return false;
}
let Ok(new_alias) = RoomAliasId::parse(new_address) else {
return false;
};
for public_address in imp.public_addresses().iter::<PublicAddress>() {
let Ok(public_address) = public_address else {
return false;
};
if *public_address.alias() == new_alias {
return false;
}
}
true
}
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(
"Unregister “{address}”",
&[("address", &alias)],
)));
row.connect_remove(clone!(
#[weak(rename_to = obj)]
self,
move |row| {
spawn!(clone!(
#[weak]
row,
async move {
obj.unregister_local_address(&row).await;
}
));
}
));
row.upcast()
} else {
imp.local_addresses_add_row.clone().upcast()
}
}
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);
}
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}"))
}
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(
"Register “{address}”",
&[("address", &address)],
)
});
row.set_add_button_accessible_label(accessible_label);
}
#[template_callback]
async fn register_local_address(&self) {
if !self.can_register_local_address() {
return;
}
let Some(room) = self.room() else {
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();
let row = &imp.local_addresses_add_row;
row.set_is_loading(true);
imp.local_addresses_error_revealer.set_reveal_child(false);
let aliases = room.aliases();
match aliases.register_local_alias(alias).await {
Ok(()) => {
row.set_text("");
}
Err(error) => {
toast!(self, 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);
}
fn can_register_local_address(&self) -> bool {
let imp = self.imp();
let Some(new_address) = self.new_local_address() else {
return false;
};
let Ok(new_alias) = RoomAliasId::parse(new_address) else {
return false;
};
for local_address in imp.public_addresses().iter::<glib::Object>() {
let Some(local_address) = local_address.ok().and_downcast::<gtk::StringObject>() else {
return true;
};
if local_address.string() == new_alias.as_str() {
return false;
}
}
true
}
}
fn public_row_is_main(row: &RemovableRow) -> bool {
row.extra_suffix().is_some_and(|w| w.is::<gtk::Box>())
}