15 changed files with 1577 additions and 67 deletions
@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<interface> |
||||
<template class="ChangePasswordSubpage" parent="GtkBox"> |
||||
<property name="orientation">vertical</property> |
||||
<child> |
||||
<object class="GtkHeaderBar"> |
||||
<property name="title-widget"> |
||||
<object class="GtkLabel"> |
||||
<property name="label" translatable="yes">Change Password</property> |
||||
<property name="single-line-mode">True</property> |
||||
<property name="ellipsize">end</property> |
||||
<property name="width-chars">5</property> |
||||
<style> |
||||
<class name="title"/> |
||||
</style> |
||||
</object> |
||||
</property> |
||||
<child type="start"> |
||||
<object class="GtkButton" id="back"> |
||||
<property name="icon-name">go-previous-symbolic</property> |
||||
<property name="action-name">win.close-subpage</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesPage"> |
||||
<style> |
||||
<class name="status-page"/> |
||||
</style> |
||||
<property name="vexpand">true</property> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="GtkImage"> |
||||
<style> |
||||
<class name="extra-large-icon"/> |
||||
<class name="error"/> |
||||
</style> |
||||
<property name="icon-name">dialog-warning-symbolic</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="GtkLabel"> |
||||
<style> |
||||
<class name="body"/> |
||||
</style> |
||||
<property name="label">Changing your password will log you out of your other sessions.</property> |
||||
<property name="wrap">True</property> |
||||
<property name="wrap-mode">word-char</property> |
||||
<property name="xalign">0.0</property> |
||||
<property name="margin-bottom">12</property> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkLabel"> |
||||
<style> |
||||
<class name="body"/> |
||||
</style> |
||||
<property name="label">Fractal's support for encryption is unstable so you might lose access to your encrypted message history. It is recommended to backup your encryption keys from another Matrix client before proceeding.</property> |
||||
<property name="wrap">True</property> |
||||
<property name="wrap-mode">word-char</property> |
||||
<property name="xalign">0.0</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="ComponentsPasswordEntryRow" id="password"> |
||||
<property name="title" translatable="yes">New Password</property> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="ComponentsPasswordEntryRow" id="confirm_password"> |
||||
<property name="title" translatable="yes">Confirm New Password</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="SpinnerButton" id="button"> |
||||
<style> |
||||
<class name="row"/> |
||||
<class name="destructive-action"/> |
||||
</style> |
||||
<property name="label" translatable="yes">Continue</property> |
||||
<property name="sensitive">false</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</template> |
||||
</interface> |
||||
@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<interface> |
||||
<template class="DeactivateAccountSubpage" parent="GtkBox"> |
||||
<property name="orientation">vertical</property> |
||||
<child> |
||||
<object class="GtkHeaderBar"> |
||||
<property name="title-widget"> |
||||
<object class="GtkLabel"> |
||||
<property name="label" translatable="yes">Deactivate Account</property> |
||||
<property name="single-line-mode">True</property> |
||||
<property name="ellipsize">end</property> |
||||
<property name="width-chars">5</property> |
||||
<style> |
||||
<class name="title"/> |
||||
</style> |
||||
</object> |
||||
</property> |
||||
<child type="start"> |
||||
<object class="GtkButton" id="back"> |
||||
<property name="icon-name">go-previous-symbolic</property> |
||||
<property name="action-name">win.close-subpage</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesPage"> |
||||
<style> |
||||
<class name="status-page"/> |
||||
</style> |
||||
<property name="vexpand">true</property> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="GtkImage"> |
||||
<style> |
||||
<class name="extra-large-icon"/> |
||||
<class name="error"/> |
||||
</style> |
||||
<property name="icon-name">dialog-warning-symbolic</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="GtkLabel"> |
||||
<style> |
||||
<class name="body"/> |
||||
</style> |
||||
<property name="label">Deactivating your account means you will lose access to all your messages, contacts, files, and more, forever.</property> |
||||
<property name="wrap">True</property> |
||||
<property name="wrap-mode">word-char</property> |
||||
<property name="xalign">0.0</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="GtkLabel"> |
||||
<style> |
||||
<class name="body"/> |
||||
</style> |
||||
<property name="label">To confirm that you really want to deactivate this account, type in your Matrix user ID:</property> |
||||
<property name="wrap">True</property> |
||||
<property name="wrap-mode">word-char</property> |
||||
<property name="xalign">0.0</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="ComponentsEntryRow" id="confirmation"> |
||||
<property name="title" translatable="yes">Matrix User ID</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="SpinnerButton" id="button"> |
||||
<style> |
||||
<class name="row"/> |
||||
<class name="destructive-action"/> |
||||
</style> |
||||
<property name="label" translatable="yes">Continue</property> |
||||
<property name="sensitive">false</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</template> |
||||
</interface> |
||||
@ -0,0 +1,129 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<interface> |
||||
<template class="UserPage" parent="AdwPreferencesPage"> |
||||
<property name="icon-name">preferences-system-symbolic</property> |
||||
<property name="title" translatable="yes">General</property> |
||||
<property name="name">general</property> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="ComponentsEditableAvatar" id="avatar"> |
||||
<binding name="avatar"> |
||||
<lookup name="avatar"> |
||||
<lookup name="user"> |
||||
<lookup name="session">UserPage</lookup> |
||||
</lookup> |
||||
</lookup> |
||||
</binding> |
||||
<property name="editable">true</property> |
||||
<binding name="removable"> |
||||
<closure type="gboolean" function="object_is_some"> |
||||
<lookup name="image"> |
||||
<lookup name="avatar"> |
||||
<lookup name="user"> |
||||
<lookup name="session">UserPage</lookup> |
||||
</lookup> |
||||
</lookup> |
||||
</lookup> |
||||
</closure> |
||||
</binding> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="ComponentsEntryRow" id="display_name"> |
||||
<property name="title" translatable="yes">Name</property> |
||||
<binding name="text"> |
||||
<lookup name="display-name"> |
||||
<lookup name="user"> |
||||
<lookup name="session">UserPage</lookup> |
||||
</lookup> |
||||
</lookup> |
||||
</binding> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesGroup" id="change_password_group"> |
||||
<child> |
||||
<object class="ComponentsButtonRow"> |
||||
<property name="title" translatable="yes">Change Password</property> |
||||
<property name="to-subpage">true</property> |
||||
<signal name="activated" handler="show_change_password" swapped="yes"/> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<property name="title" translatable="yes">Advanced Information</property> |
||||
<child> |
||||
<object class="AdwActionRow"> |
||||
<property name="title" translatable="yes">Homeserver</property> |
||||
<child> |
||||
<object class="GtkLabel" id="homeserver"> |
||||
<style> |
||||
<class name="dim-label"/> |
||||
</style> |
||||
<property name="ellipsize">end</property> |
||||
<property name="selectable">true</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwActionRow"> |
||||
<property name="title" translatable="yes">Matrix User ID</property> |
||||
<child> |
||||
<object class="GtkLabel" id="user_id"> |
||||
<style> |
||||
<class name="dim-label"/> |
||||
</style> |
||||
<property name="ellipsize">end</property> |
||||
<property name="selectable">true</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwActionRow"> |
||||
<property name="title" translatable="yes">Session ID</property> |
||||
<child> |
||||
<object class="GtkLabel" id="session_id"> |
||||
<style> |
||||
<class name="dim-label"/> |
||||
</style> |
||||
<property name="ellipsize">end</property> |
||||
<property name="selectable">true</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="ComponentsButtonRow"> |
||||
<style> |
||||
<class name="error"/> |
||||
</style> |
||||
<property name="title" translatable="yes">Deactivate Account</property> |
||||
<property name="to-subpage">true</property> |
||||
<signal name="activated" handler="show_deactivate_account" swapped="yes"/> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</template> |
||||
<object class="ChangePasswordSubpage" id="change_password_subpage"> |
||||
<property name="session" bind-source="UserPage" bind-property="session" bind-flags="sync-create"/> |
||||
</object> |
||||
<object class="DeactivateAccountSubpage" id="deactivate_account_subpage"> |
||||
<property name="session" bind-source="UserPage" bind-property="session" bind-flags="sync-create"/> |
||||
</object> |
||||
</interface> |
||||
@ -0,0 +1,344 @@
|
||||
use adw::{prelude::*, subclass::prelude::*}; |
||||
use gettextrs::gettext; |
||||
use gtk::{ |
||||
glib::{self, clone}, |
||||
subclass::prelude::*, |
||||
CompositeTemplate, |
||||
}; |
||||
use log::error; |
||||
use matrix_sdk::{ |
||||
ruma::{ |
||||
api::{ |
||||
client::r0::account::change_password, |
||||
error::{FromHttpResponseError, ServerError}, |
||||
}, |
||||
assign, |
||||
}, |
||||
Error as MatrixError, HttpError, |
||||
}; |
||||
|
||||
use crate::{ |
||||
components::{AuthDialog, AuthError, PasswordEntryRow, SpinnerButton}, |
||||
session::Session, |
||||
spawn, |
||||
utils::validate_password, |
||||
}; |
||||
|
||||
mod imp { |
||||
use glib::{subclass::InitializingObject, WeakRef}; |
||||
use once_cell::unsync::OnceCell; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)] |
||||
#[template(resource = "/org/gnome/FractalNext/account-settings-change-password-subpage.ui")] |
||||
pub struct ChangePasswordSubpage { |
||||
pub session: OnceCell<WeakRef<Session>>, |
||||
#[template_child] |
||||
pub password: TemplateChild<PasswordEntryRow>, |
||||
#[template_child] |
||||
pub confirm_password: TemplateChild<PasswordEntryRow>, |
||||
#[template_child] |
||||
pub button: TemplateChild<SpinnerButton>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for ChangePasswordSubpage { |
||||
const NAME: &'static str = "ChangePasswordSubpage"; |
||||
type Type = super::ChangePasswordSubpage; |
||||
type ParentType = gtk::Box; |
||||
|
||||
fn class_init(klass: &mut Self::Class) { |
||||
PasswordEntryRow::static_type(); |
||||
SpinnerButton::static_type(); |
||||
Self::bind_template(klass); |
||||
} |
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) { |
||||
obj.init_template(); |
||||
} |
||||
} |
||||
|
||||
impl ObjectImpl for ChangePasswordSubpage { |
||||
fn properties() -> &'static [glib::ParamSpec] { |
||||
use once_cell::sync::Lazy; |
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
||||
vec![glib::ParamSpecObject::new( |
||||
"session", |
||||
"Session", |
||||
"The session", |
||||
Session::static_type(), |
||||
glib::ParamFlags::READWRITE, |
||||
)] |
||||
}); |
||||
|
||||
PROPERTIES.as_ref() |
||||
} |
||||
|
||||
fn set_property( |
||||
&self, |
||||
obj: &Self::Type, |
||||
_id: usize, |
||||
value: &glib::Value, |
||||
pspec: &glib::ParamSpec, |
||||
) { |
||||
match pspec.name() { |
||||
"session" => obj.set_session(value.get().unwrap()), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
||||
match pspec.name() { |
||||
"session" => obj.session().to_value(), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn constructed(&self, obj: &Self::Type) { |
||||
self.parent_constructed(obj); |
||||
|
||||
self.password.define_progress_steps(&[ |
||||
>k::LEVEL_BAR_OFFSET_LOW, |
||||
"step2", |
||||
"step3", |
||||
>k::LEVEL_BAR_OFFSET_HIGH, |
||||
>k::LEVEL_BAR_OFFSET_FULL, |
||||
]); |
||||
self.password |
||||
.connect_focused(clone!(@weak obj => move |entry, focused| { |
||||
if focused { |
||||
entry.set_progress_visible(true); |
||||
obj.validate_password(); |
||||
} else { |
||||
entry.remove_css_class("warning"); |
||||
entry.remove_css_class("success"); |
||||
if entry.text().is_empty() { |
||||
entry.set_progress_visible(false); |
||||
} |
||||
} |
||||
})); |
||||
self.password |
||||
.connect_activated(clone!(@weak obj => move|_| { |
||||
spawn!( |
||||
clone!(@weak obj => async move { |
||||
obj.change_password().await; |
||||
}) |
||||
); |
||||
})); |
||||
self.password.connect_changed(clone!(@weak obj => move|_| { |
||||
obj.validate_password(); |
||||
})); |
||||
|
||||
self.confirm_password |
||||
.connect_focused(clone!(@weak obj => move |entry, focused| { |
||||
if focused { |
||||
obj.validate_password_confirmation(); |
||||
} else { |
||||
entry.remove_css_class("warning"); |
||||
entry.remove_css_class("success"); |
||||
} |
||||
})); |
||||
self.confirm_password |
||||
.connect_activated(clone!(@weak obj => move|_| { |
||||
spawn!( |
||||
clone!(@weak obj => async move { |
||||
obj.change_password().await; |
||||
}) |
||||
); |
||||
})); |
||||
self.confirm_password |
||||
.connect_changed(clone!(@weak obj => move|_| { |
||||
obj.validate_password_confirmation(); |
||||
})); |
||||
|
||||
self.button.connect_clicked(clone!(@weak obj => move|_| { |
||||
spawn!( |
||||
clone!(@weak obj => async move { |
||||
obj.change_password().await; |
||||
}) |
||||
); |
||||
})); |
||||
} |
||||
} |
||||
|
||||
impl WidgetImpl for ChangePasswordSubpage {} |
||||
impl BoxImpl for ChangePasswordSubpage {} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// Account settings page about the user and the session.
|
||||
pub struct ChangePasswordSubpage(ObjectSubclass<imp::ChangePasswordSubpage>) |
||||
@extends gtk::Widget, gtk::Box, @implements gtk::Accessible; |
||||
} |
||||
|
||||
impl ChangePasswordSubpage { |
||||
pub fn new(session: &Session) -> Self { |
||||
glib::Object::new(&[("session", session)]).expect("Failed to create ChangePasswordSubpage") |
||||
} |
||||
|
||||
pub fn session(&self) -> Option<Session> { |
||||
self.imp() |
||||
.session |
||||
.get() |
||||
.and_then(|session| session.upgrade()) |
||||
} |
||||
|
||||
pub fn set_session(&self, session: Option<Session>) { |
||||
if let Some(session) = session { |
||||
self.imp().session.set(session.downgrade()).unwrap(); |
||||
} |
||||
} |
||||
|
||||
fn validate_password(&self) { |
||||
let entry = &self.imp().password; |
||||
let password = entry.text(); |
||||
|
||||
if password.is_empty() { |
||||
entry.set_hint(""); |
||||
entry.remove_css_class("success"); |
||||
entry.remove_css_class("warning"); |
||||
entry.set_progress_value(0.0); |
||||
self.update_button(); |
||||
return; |
||||
} |
||||
|
||||
let validity = validate_password(&password); |
||||
|
||||
entry.set_progress_value(validity.progress as f64 / 20.0); |
||||
if validity.progress == 100 { |
||||
entry.set_hint(""); |
||||
entry.add_css_class("success"); |
||||
entry.remove_css_class("warning"); |
||||
} else { |
||||
entry.remove_css_class("success"); |
||||
entry.add_css_class("warning"); |
||||
if !validity.has_length { |
||||
entry.set_hint(&gettext("Password must be at least 8 characters long")); |
||||
} else if !validity.has_lowercase { |
||||
entry.set_hint(&gettext( |
||||
"Password must have at least one lower-case letter", |
||||
)); |
||||
} else if !validity.has_uppercase { |
||||
entry.set_hint(&gettext( |
||||
"Password must have at least one upper-case letter", |
||||
)); |
||||
} else if !validity.has_number { |
||||
entry.set_hint(&gettext("Password must have at least one digit")); |
||||
} else if !validity.has_symbol { |
||||
entry.set_hint(&gettext("Password must have at least one symbol")); |
||||
} |
||||
} |
||||
self.update_button(); |
||||
} |
||||
|
||||
fn validate_password_confirmation(&self) { |
||||
let priv_ = self.imp(); |
||||
let entry = &priv_.confirm_password; |
||||
let password = priv_.password.text(); |
||||
let confirmation = entry.text(); |
||||
|
||||
if confirmation.is_empty() { |
||||
entry.set_hint(""); |
||||
entry.remove_css_class("success"); |
||||
entry.remove_css_class("warning"); |
||||
return; |
||||
} |
||||
|
||||
if password == confirmation { |
||||
entry.set_hint(""); |
||||
entry.add_css_class("success"); |
||||
entry.remove_css_class("warning"); |
||||
} else { |
||||
entry.remove_css_class("success"); |
||||
entry.add_css_class("warning"); |
||||
entry.set_hint(&gettext("Passwords do not match")); |
||||
} |
||||
self.update_button(); |
||||
} |
||||
|
||||
fn update_button(&self) { |
||||
self.imp().button.set_sensitive(self.can_change_password()); |
||||
} |
||||
|
||||
fn can_change_password(&self) -> bool { |
||||
let priv_ = self.imp(); |
||||
let password = priv_.password.text(); |
||||
let confirmation = priv_.confirm_password.text(); |
||||
|
||||
validate_password(&password).progress == 100 && password == confirmation |
||||
} |
||||
|
||||
async fn change_password(&self) { |
||||
if !self.can_change_password() { |
||||
return; |
||||
} |
||||
|
||||
let priv_ = self.imp(); |
||||
let password = priv_.password.text(); |
||||
|
||||
priv_.button.set_loading(true); |
||||
priv_.password.set_entry_sensitive(false); |
||||
priv_.confirm_password.set_entry_sensitive(false); |
||||
|
||||
let session = self.session().unwrap(); |
||||
let dialog = AuthDialog::new( |
||||
self.root() |
||||
.as_ref() |
||||
.and_then(|root| root.downcast_ref::<gtk::Window>()), |
||||
&session, |
||||
); |
||||
|
||||
let result = dialog |
||||
.authenticate(move |client, auth_data| { |
||||
let password = password.clone(); |
||||
async move { |
||||
if let Some(auth) = auth_data { |
||||
let auth = Some(auth.as_matrix_auth_data()); |
||||
let request = assign!(change_password::Request::new(&password), { auth }); |
||||
client.send(request, None).await.map_err(Into::into) |
||||
} else { |
||||
let request = change_password::Request::new(&password); |
||||
client.send(request, None).await.map_err(Into::into) |
||||
} |
||||
} |
||||
}) |
||||
.await; |
||||
|
||||
match result { |
||||
Ok(_) => { |
||||
let _ = self.activate_action( |
||||
"win.add-toast", |
||||
Some(&gettext("Password changed successfully").to_variant()), |
||||
); |
||||
priv_.password.set_text(""); |
||||
priv_.confirm_password.set_text(""); |
||||
self.activate_action("win.close-subpage", None).unwrap(); |
||||
} |
||||
Err(err) => match err { |
||||
AuthError::UserCancelled => {} |
||||
AuthError::ServerResponse(error) |
||||
if matches!(error.as_ref(), MatrixError::Http(HttpError::ClientApi( |
||||
FromHttpResponseError::Http(ServerError::Known(error)), |
||||
)) if error.kind.as_ref() == "M_WEAK_PASSWORD") => |
||||
{ |
||||
error!("Weak password: {:?}", error); |
||||
let _ = self.activate_action( |
||||
"win.add-toast", |
||||
Some(&gettext("Password rejected for being too weak").to_variant()), |
||||
); |
||||
} |
||||
_ => { |
||||
error!("Failed to change the password: {:?}", err); |
||||
let _ = self.activate_action( |
||||
"win.add-toast", |
||||
Some(&gettext("Could not change password").to_variant()), |
||||
); |
||||
} |
||||
}, |
||||
} |
||||
priv_.button.set_loading(false); |
||||
priv_.password.set_entry_sensitive(true); |
||||
priv_.confirm_password.set_entry_sensitive(true); |
||||
} |
||||
} |
||||
@ -0,0 +1,217 @@
|
||||
use adw::{prelude::*, subclass::prelude::*}; |
||||
use gettextrs::gettext; |
||||
use gtk::{ |
||||
glib::{self, clone}, |
||||
subclass::prelude::*, |
||||
CompositeTemplate, |
||||
}; |
||||
use log::error; |
||||
use matrix_sdk::ruma::{api::client::r0::account::deactivate, assign}; |
||||
|
||||
use crate::{ |
||||
components::{AuthDialog, EntryRow, SpinnerButton, Toast}, |
||||
session::{Session, UserExt}, |
||||
spawn, |
||||
}; |
||||
|
||||
mod imp { |
||||
use glib::{subclass::InitializingObject, WeakRef}; |
||||
use once_cell::unsync::OnceCell; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)] |
||||
#[template(resource = "/org/gnome/FractalNext/account-settings-deactivate-account-subpage.ui")] |
||||
pub struct DeactivateAccountSubpage { |
||||
pub session: OnceCell<WeakRef<Session>>, |
||||
#[template_child] |
||||
pub confirmation: TemplateChild<EntryRow>, |
||||
#[template_child] |
||||
pub button: TemplateChild<SpinnerButton>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for DeactivateAccountSubpage { |
||||
const NAME: &'static str = "DeactivateAccountSubpage"; |
||||
type Type = super::DeactivateAccountSubpage; |
||||
type ParentType = gtk::Box; |
||||
|
||||
fn class_init(klass: &mut Self::Class) { |
||||
EntryRow::static_type(); |
||||
Self::bind_template(klass); |
||||
} |
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) { |
||||
obj.init_template(); |
||||
} |
||||
} |
||||
|
||||
impl ObjectImpl for DeactivateAccountSubpage { |
||||
fn properties() -> &'static [glib::ParamSpec] { |
||||
use once_cell::sync::Lazy; |
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
||||
vec![glib::ParamSpecObject::new( |
||||
"session", |
||||
"Session", |
||||
"The session", |
||||
Session::static_type(), |
||||
glib::ParamFlags::READWRITE, |
||||
)] |
||||
}); |
||||
|
||||
PROPERTIES.as_ref() |
||||
} |
||||
|
||||
fn set_property( |
||||
&self, |
||||
obj: &Self::Type, |
||||
_id: usize, |
||||
value: &glib::Value, |
||||
pspec: &glib::ParamSpec, |
||||
) { |
||||
match pspec.name() { |
||||
"session" => obj.set_session(value.get().unwrap()), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
||||
match pspec.name() { |
||||
"session" => obj.session().to_value(), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn constructed(&self, obj: &Self::Type) { |
||||
self.parent_constructed(obj); |
||||
|
||||
self.confirmation |
||||
.connect_activated(clone!(@weak obj => move|_| { |
||||
spawn!( |
||||
clone!(@weak obj => async move { |
||||
obj.deactivate_account().await; |
||||
}) |
||||
); |
||||
})); |
||||
self.confirmation |
||||
.connect_changed(clone!(@weak obj => move|_| { |
||||
obj.update_button(); |
||||
})); |
||||
|
||||
self.button.connect_clicked(clone!(@weak obj => move|_| { |
||||
spawn!( |
||||
clone!(@weak obj => async move { |
||||
obj.deactivate_account().await; |
||||
}) |
||||
); |
||||
})); |
||||
} |
||||
} |
||||
|
||||
impl WidgetImpl for DeactivateAccountSubpage {} |
||||
impl BoxImpl for DeactivateAccountSubpage {} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// Account settings page about the user and the session.
|
||||
pub struct DeactivateAccountSubpage(ObjectSubclass<imp::DeactivateAccountSubpage>) |
||||
@extends gtk::Widget, gtk::Box, @implements gtk::Accessible; |
||||
} |
||||
|
||||
impl DeactivateAccountSubpage { |
||||
pub fn new(session: &Session) -> Self { |
||||
glib::Object::new(&[("session", session)]) |
||||
.expect("Failed to create DeactivateAccountSubpage") |
||||
} |
||||
|
||||
pub fn session(&self) -> Option<Session> { |
||||
self.imp() |
||||
.session |
||||
.get() |
||||
.and_then(|session| session.upgrade()) |
||||
} |
||||
|
||||
pub fn set_session(&self, session: Option<Session>) { |
||||
if let Some(session) = session { |
||||
let priv_ = self.imp(); |
||||
priv_.session.set(session.downgrade()).unwrap(); |
||||
priv_ |
||||
.confirmation |
||||
.set_placeholder_text(Some(&self.user_id())); |
||||
} |
||||
} |
||||
|
||||
fn user_id(&self) -> String { |
||||
self.session() |
||||
.as_ref() |
||||
.and_then(|session| session.user()) |
||||
.unwrap() |
||||
.user_id() |
||||
.to_string() |
||||
} |
||||
|
||||
fn update_button(&self) { |
||||
self.imp() |
||||
.button |
||||
.set_sensitive(self.can_deactivate_account()); |
||||
} |
||||
|
||||
fn can_deactivate_account(&self) -> bool { |
||||
let confirmation = self.imp().confirmation.text(); |
||||
confirmation == self.user_id() |
||||
} |
||||
|
||||
async fn deactivate_account(&self) { |
||||
if !self.can_deactivate_account() { |
||||
return; |
||||
} |
||||
|
||||
let priv_ = self.imp(); |
||||
priv_.button.set_loading(true); |
||||
priv_.confirmation.set_sensitive(false); |
||||
|
||||
let session = self.session().unwrap(); |
||||
let dialog = AuthDialog::new( |
||||
self.root() |
||||
.as_ref() |
||||
.and_then(|root| root.downcast_ref::<gtk::Window>()), |
||||
&session, |
||||
); |
||||
|
||||
let result = dialog |
||||
.authenticate(move |client, auth_data| async move { |
||||
if let Some(auth) = auth_data { |
||||
let auth = Some(auth.as_matrix_auth_data()); |
||||
let request = assign!(deactivate::Request::new(), { auth }); |
||||
client.send(request, None).await.map_err(Into::into) |
||||
} else { |
||||
let request = deactivate::Request::new(); |
||||
client.send(request, None).await.map_err(Into::into) |
||||
} |
||||
}) |
||||
.await; |
||||
|
||||
match result { |
||||
Ok(_) => { |
||||
if let Some(session) = self.session() { |
||||
session |
||||
.parent_window() |
||||
.unwrap() |
||||
.add_toast(&Toast::new(&gettext("Account successfully deactivated"))); |
||||
session.handle_logged_out(); |
||||
} |
||||
self.activate_action("account-settings.close", None) |
||||
.unwrap(); |
||||
} |
||||
Err(err) => { |
||||
error!("Failed to deactivate account: {:?}", err); |
||||
let _ = self.activate_action( |
||||
"win.add-toast", |
||||
Some(&gettext("Could not deactivate account").to_variant()), |
||||
); |
||||
} |
||||
} |
||||
priv_.button.set_loading(false); |
||||
priv_.confirmation.set_sensitive(true); |
||||
} |
||||
} |
||||
@ -0,0 +1,482 @@
|
||||
use std::{fs::File, time::Duration}; |
||||
|
||||
use adw::{prelude::*, subclass::prelude::*}; |
||||
use gettextrs::gettext; |
||||
use gtk::{ |
||||
gio, |
||||
glib::{self, clone}, |
||||
subclass::prelude::*, |
||||
CompositeTemplate, |
||||
}; |
||||
use log::error; |
||||
use matrix_sdk::ruma::{api::client::r0::capabilities::get_capabilities, MxcUri}; |
||||
|
||||
mod change_password_subpage; |
||||
mod deactivate_account_subpage; |
||||
|
||||
use change_password_subpage::ChangePasswordSubpage; |
||||
use deactivate_account_subpage::DeactivateAccountSubpage; |
||||
|
||||
use crate::{ |
||||
components::{ActionState, ButtonRow, EditableAvatar, EntryRow}, |
||||
session::{Session, User, UserExt}, |
||||
spawn, spawn_tokio, |
||||
utils::TemplateCallbacks, |
||||
}; |
||||
|
||||
mod imp { |
||||
use std::cell::{Cell, RefCell}; |
||||
|
||||
use glib::{subclass::InitializingObject, WeakRef}; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)] |
||||
#[template(resource = "/org/gnome/FractalNext/account-settings-user-page.ui")] |
||||
pub struct UserPage { |
||||
pub session: RefCell<Option<WeakRef<Session>>>, |
||||
#[template_child] |
||||
pub avatar: TemplateChild<EditableAvatar>, |
||||
#[template_child] |
||||
pub display_name: TemplateChild<EntryRow>, |
||||
#[template_child] |
||||
pub change_password_group: TemplateChild<adw::PreferencesGroup>, |
||||
#[template_child] |
||||
pub change_password_subpage: TemplateChild<ChangePasswordSubpage>, |
||||
#[template_child] |
||||
pub homeserver: TemplateChild<gtk::Label>, |
||||
#[template_child] |
||||
pub user_id: TemplateChild<gtk::Label>, |
||||
#[template_child] |
||||
pub session_id: TemplateChild<gtk::Label>, |
||||
#[template_child] |
||||
pub deactivate_account_subpage: TemplateChild<DeactivateAccountSubpage>, |
||||
pub changing_avatar_to: RefCell<Option<Box<MxcUri>>>, |
||||
pub removing_avatar: Cell<bool>, |
||||
pub changing_display_name_to: RefCell<Option<String>>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for UserPage { |
||||
const NAME: &'static str = "UserPage"; |
||||
type Type = super::UserPage; |
||||
type ParentType = adw::PreferencesPage; |
||||
|
||||
fn class_init(klass: &mut Self::Class) { |
||||
EditableAvatar::static_type(); |
||||
EntryRow::static_type(); |
||||
ButtonRow::static_type(); |
||||
ChangePasswordSubpage::static_type(); |
||||
DeactivateAccountSubpage::static_type(); |
||||
Self::bind_template(klass); |
||||
Self::Type::bind_template_callbacks(klass); |
||||
TemplateCallbacks::bind_template_callbacks(klass); |
||||
} |
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) { |
||||
obj.init_template(); |
||||
} |
||||
} |
||||
|
||||
impl ObjectImpl for UserPage { |
||||
fn properties() -> &'static [glib::ParamSpec] { |
||||
use once_cell::sync::Lazy; |
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
||||
vec![glib::ParamSpecObject::new( |
||||
"session", |
||||
"Session", |
||||
"The session", |
||||
Session::static_type(), |
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY, |
||||
)] |
||||
}); |
||||
|
||||
PROPERTIES.as_ref() |
||||
} |
||||
|
||||
fn set_property( |
||||
&self, |
||||
obj: &Self::Type, |
||||
_id: usize, |
||||
value: &glib::Value, |
||||
pspec: &glib::ParamSpec, |
||||
) { |
||||
match pspec.name() { |
||||
"session" => obj.set_session(value.get().unwrap()), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
||||
match pspec.name() { |
||||
"session" => obj.session().to_value(), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn constructed(&self, obj: &Self::Type) { |
||||
self.parent_constructed(obj); |
||||
|
||||
obj.init_avatar(); |
||||
obj.init_display_name(); |
||||
obj.init_change_password(); |
||||
} |
||||
} |
||||
|
||||
impl WidgetImpl for UserPage {} |
||||
impl PreferencesPageImpl for UserPage {} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// Account settings page about the user and the session.
|
||||
pub struct UserPage(ObjectSubclass<imp::UserPage>) |
||||
@extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible; |
||||
} |
||||
|
||||
#[gtk::template_callbacks] |
||||
impl UserPage { |
||||
pub fn new(parent_window: &Option<gtk::Window>, session: &Session) -> Self { |
||||
glib::Object::new(&[("transient-for", parent_window), ("session", session)]) |
||||
.expect("Failed to create UserPage") |
||||
} |
||||
|
||||
pub fn session(&self) -> Option<Session> { |
||||
self.imp() |
||||
.session |
||||
.borrow() |
||||
.clone() |
||||
.and_then(|session| session.upgrade()) |
||||
} |
||||
|
||||
pub fn set_session(&self, session: Option<Session>) { |
||||
if self.session() == session { |
||||
return; |
||||
} |
||||
self.imp() |
||||
.session |
||||
.replace(session.map(|session| session.downgrade())); |
||||
self.notify("session"); |
||||
|
||||
self.user().avatar().connect_notify_local( |
||||
Some("url"), |
||||
clone!(@weak self as obj => move |avatar, _| { |
||||
obj.avatar_changed(avatar.url().as_deref()); |
||||
}), |
||||
); |
||||
self.user().connect_notify_local( |
||||
Some("display-name"), |
||||
clone!(@weak self as obj => move |user, _| { |
||||
obj.display_name_changed(&user.display_name()); |
||||
}), |
||||
); |
||||
|
||||
spawn!( |
||||
glib::PRIORITY_LOW, |
||||
clone!(@weak self as obj => async move { |
||||
let priv_ = obj.imp(); |
||||
let client = obj.session().unwrap().client(); |
||||
|
||||
let homeserver = client.homeserver().await; |
||||
priv_.homeserver.set_label(homeserver.as_ref()); |
||||
|
||||
let user_id = client.user_id().await.unwrap(); |
||||
priv_.user_id.set_label(user_id.as_ref()); |
||||
|
||||
let session_id = client.device_id().await.unwrap(); |
||||
priv_.session_id.set_label(session_id.as_ref()); |
||||
}) |
||||
); |
||||
} |
||||
|
||||
fn user(&self) -> User { |
||||
self.session() |
||||
.as_ref() |
||||
.and_then(|session| session.user()) |
||||
.unwrap() |
||||
.to_owned() |
||||
} |
||||
|
||||
fn init_avatar(&self) { |
||||
let avatar = &self.imp().avatar; |
||||
avatar.connect_edit_avatar(clone!(@weak self as obj => move |_, file| { |
||||
spawn!( |
||||
clone!(@weak obj => async move { |
||||
obj.change_avatar(file).await; |
||||
}) |
||||
); |
||||
})); |
||||
avatar.connect_remove_avatar(clone!(@weak self as obj => move |_| { |
||||
spawn!( |
||||
clone!(@weak obj => async move { |
||||
obj.remove_avatar().await; |
||||
}) |
||||
); |
||||
})); |
||||
} |
||||
|
||||
fn avatar_changed(&self, uri: Option<&MxcUri>) { |
||||
let priv_ = self.imp(); |
||||
let avatar = &*priv_.avatar; |
||||
if uri.is_none() && priv_.removing_avatar.get() { |
||||
priv_.removing_avatar.set(false); |
||||
avatar.show_temp_image(false); |
||||
avatar.set_remove_state(ActionState::Success); |
||||
avatar.set_edit_sensitive(true); |
||||
let _ = self.activate_action( |
||||
"win.add-toast", |
||||
Some(&gettext("Avatar removed successfully").to_variant()), |
||||
); |
||||
glib::timeout_add_local_once( |
||||
Duration::from_secs(2), |
||||
clone!(@weak avatar => move || { |
||||
avatar.set_remove_state(ActionState::Default); |
||||
}), |
||||
); |
||||
} else if uri.is_some() { |
||||
let to_uri = priv_.changing_avatar_to.borrow().clone(); |
||||
if to_uri.as_deref() == uri { |
||||
priv_.changing_avatar_to.take(); |
||||
avatar.set_edit_state(ActionState::Success); |
||||
avatar.show_temp_image(false); |
||||
avatar.set_temp_image_from_file(None); |
||||
avatar.set_remove_sensitive(true); |
||||
let _ = self.activate_action( |
||||
"win.add-toast", |
||||
Some(&gettext("Avatar changed successfully").to_variant()), |
||||
); |
||||
glib::timeout_add_local_once( |
||||
Duration::from_secs(2), |
||||
clone!(@weak avatar => move || { |
||||
avatar.set_edit_state(ActionState::Default); |
||||
}), |
||||
); |
||||
} |
||||
} |
||||
} |
||||
|
||||
async fn change_avatar(&self, file: gio::File) { |
||||
let priv_ = self.imp(); |
||||
let avatar = &priv_.avatar; |
||||
avatar.set_temp_image_from_file(Some(&file)); |
||||
avatar.show_temp_image(true); |
||||
avatar.set_edit_state(ActionState::Loading); |
||||
avatar.set_remove_sensitive(false); |
||||
|
||||
let client = self.session().unwrap().client(); |
||||
let mime = file |
||||
.query_info_future( |
||||
&gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, |
||||
gio::FileQueryInfoFlags::NONE, |
||||
glib::PRIORITY_LOW, |
||||
) |
||||
.await |
||||
.ok() |
||||
.and_then(|info| info.content_type()) |
||||
.and_then(|content_type| gio::content_type_get_mime_type(&content_type)) |
||||
.unwrap(); |
||||
let mut file = File::open(file.path().unwrap()).unwrap(); |
||||
|
||||
let client_clone = client.clone(); |
||||
let handle = |
||||
spawn_tokio!( |
||||
async move { client_clone.upload(&mime.parse().unwrap(), &mut file).await } |
||||
); |
||||
|
||||
let uri = match handle.await.unwrap() { |
||||
Ok(res) => res.content_uri, |
||||
Err(error) => { |
||||
error!("Could not upload user avatar: {}", error); |
||||
let _ = self.activate_action( |
||||
"win.add-toast", |
||||
Some(&gettext("Could not upload avatar").to_variant()), |
||||
); |
||||
avatar.show_temp_image(false); |
||||
avatar.set_temp_image_from_file(None); |
||||
avatar.set_edit_state(ActionState::Default); |
||||
avatar.set_remove_sensitive(true); |
||||
return; |
||||
} |
||||
}; |
||||
|
||||
priv_.changing_avatar_to.replace(Some(uri.clone())); |
||||
let handle = spawn_tokio!(async move { client.account().set_avatar_url(Some(&uri)).await }); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(_) => { |
||||
let to_uri = priv_.changing_avatar_to.borrow().clone(); |
||||
if let Some(avatar) = to_uri { |
||||
self.user().set_avatar_url(Some(avatar)) |
||||
} |
||||
} |
||||
Err(error) => { |
||||
if priv_.changing_avatar_to.take().is_some() { |
||||
error!("Could not change user avatar: {}", error); |
||||
let _ = self.activate_action( |
||||
"win.add-toast", |
||||
Some(&gettext("Could not change avatar").to_variant()), |
||||
); |
||||
avatar.show_temp_image(false); |
||||
avatar.set_temp_image_from_file(None); |
||||
avatar.set_edit_state(ActionState::Default); |
||||
avatar.set_remove_sensitive(true); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
async fn remove_avatar(&self) { |
||||
let priv_ = self.imp(); |
||||
let avatar = &*priv_.avatar; |
||||
avatar.show_temp_image(true); |
||||
avatar.set_remove_state(ActionState::Loading); |
||||
avatar.set_edit_sensitive(false); |
||||
|
||||
let client = self.session().unwrap().client(); |
||||
let handle = spawn_tokio!(async move { client.account().set_avatar_url(None).await }); |
||||
priv_.removing_avatar.set(true); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(_) => { |
||||
self.user().set_avatar_url(None); |
||||
} |
||||
Err(error) => { |
||||
if priv_.removing_avatar.get() { |
||||
priv_.removing_avatar.set(false); |
||||
error!("Couldn’t remove user avatar: {}", error); |
||||
let _ = self.activate_action( |
||||
"win.add-toast", |
||||
Some(&gettext("Could not remove avatar").to_variant()), |
||||
); |
||||
avatar.show_temp_image(false); |
||||
avatar.set_remove_state(ActionState::Default); |
||||
avatar.set_edit_sensitive(true); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn init_display_name(&self) { |
||||
let entry = &*self.imp().display_name; |
||||
entry.connect_focused(clone!(@weak self as obj => move|entry, focused| { |
||||
if entry.entry_sensitive() { |
||||
if focused { |
||||
entry.set_action_state(ActionState::Confirm); |
||||
} else if entry.text() == obj.user().display_name() { |
||||
entry.set_action_state(ActionState::Default); |
||||
} |
||||
} |
||||
})); |
||||
entry.connect_activated(clone!(@weak self as obj => move|_| { |
||||
spawn!( |
||||
clone!(@weak obj => async move { |
||||
obj.change_display_name().await; |
||||
}) |
||||
); |
||||
})); |
||||
entry.connect_cancel(clone!(@weak self as obj => move|entry| { |
||||
entry.set_text(&obj.user().display_name()); |
||||
})); |
||||
} |
||||
|
||||
fn display_name_changed(&self, name: &str) { |
||||
let priv_ = self.imp(); |
||||
let entry = &*priv_.display_name; |
||||
|
||||
let to_display_name = priv_ |
||||
.changing_display_name_to |
||||
.borrow() |
||||
.clone() |
||||
.unwrap_or_default(); |
||||
if to_display_name == name { |
||||
priv_.changing_display_name_to.take(); |
||||
entry.remove_css_class("error"); |
||||
entry.set_action_state(ActionState::Success); |
||||
entry.set_entry_sensitive(true); |
||||
let _ = self.activate_action( |
||||
"win.add-toast", |
||||
Some(&gettext("Name changed successfully").to_variant()), |
||||
); |
||||
glib::timeout_add_local_once( |
||||
Duration::from_secs(2), |
||||
clone!(@weak entry => move || { |
||||
entry.set_action_state(ActionState::Default); |
||||
}), |
||||
); |
||||
} |
||||
} |
||||
|
||||
async fn change_display_name(&self) { |
||||
let priv_ = self.imp(); |
||||
let entry = &*priv_.display_name; |
||||
entry.set_action_state(ActionState::Loading); |
||||
entry.set_entry_sensitive(false); |
||||
|
||||
let display_name = entry.text(); |
||||
priv_ |
||||
.changing_display_name_to |
||||
.replace(Some(display_name.to_string())); |
||||
|
||||
let client = self.session().unwrap().client(); |
||||
let handle = |
||||
spawn_tokio!( |
||||
async move { client.account().set_display_name(Some(&display_name)).await } |
||||
); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(_) => { |
||||
let to_display_name = priv_.changing_display_name_to.borrow().clone(); |
||||
if let Some(display_name) = to_display_name { |
||||
self.user().set_display_name(Some(display_name)); |
||||
} |
||||
} |
||||
Err(err) => { |
||||
error!("Couldn’t change user display name: {}", err); |
||||
let _ = self.activate_action( |
||||
"win.add-toast", |
||||
Some(&gettext("Could not change display name").to_variant()), |
||||
); |
||||
entry.set_action_state(ActionState::Retry); |
||||
entry.add_css_class("error"); |
||||
entry.set_entry_sensitive(true); |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn init_change_password(&self) { |
||||
spawn!( |
||||
glib::PRIORITY_LOW, |
||||
clone!(@weak self as obj => async move { |
||||
let client = obj.session().unwrap().client(); |
||||
|
||||
// Check whether the user can change their password.
|
||||
let handle = spawn_tokio!(async move { |
||||
client.send(get_capabilities::Request::new(), None).await |
||||
}); |
||||
match handle.await.unwrap() { |
||||
Ok(res) => { |
||||
obj.imp().change_password_group.set_visible(res.capabilities.change_password.enabled); |
||||
} |
||||
Err(error) => error!("Could not get server capabilities: {}", error), |
||||
} |
||||
}) |
||||
); |
||||
} |
||||
|
||||
#[template_callback] |
||||
fn show_change_password(&self) { |
||||
self.root() |
||||
.as_ref() |
||||
.and_then(|root| root.downcast_ref::<adw::PreferencesWindow>()) |
||||
.unwrap() |
||||
.present_subpage(&*self.imp().change_password_subpage); |
||||
} |
||||
|
||||
#[template_callback] |
||||
fn show_deactivate_account(&self) { |
||||
self.root() |
||||
.as_ref() |
||||
.and_then(|root| root.downcast_ref::<adw::PreferencesWindow>()) |
||||
.unwrap() |
||||
.present_subpage(&*self.imp().deactivate_account_subpage); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue