use adw::{prelude::*, subclass::prelude::*}; use gtk::{ glib, glib::{clone, closure_local}, }; use matrix_sdk::authentication::oauth::{ AccountManagementUrlBuilder, OAuthError, error::OAuthDiscoveryError, }; use tracing::{error, warn}; mod encryption_page; mod general_page; mod notifications_page; mod safety_page; mod user_session; use self::{ encryption_page::{EncryptionPage, ImportExportKeysSubpage, ImportExportKeysSubpageMode}, general_page::{ChangePasswordSubpage, DeactivateAccountSubpage, GeneralPage, LogOutSubpage}, notifications_page::NotificationsPage, safety_page::{IgnoredUsersSubpage, SafetyPage}, user_session::{UserSessionListSubpage, UserSessionSubpage}, }; use crate::{ components::crypto::{CryptoIdentitySetupView, CryptoRecoverySetupView}, session::Session, spawn, spawn_tokio, utils::BoundObjectWeakRef, }; /// A subpage of the account settings. #[derive(Debug, Clone, Copy, Eq, PartialEq, glib::Variant)] pub(crate) enum AccountSettingsSubpage { /// A form to change the account's password. ChangePassword, /// A page to view the list of account's sessions. UserSessionList, /// A page to confirm the logout. LogOut, /// A page to confirm the deactivation of the password. DeactivateAccount, /// The list of ignored users. IgnoredUsers, /// A form to import encryption keys. ImportKeys, /// A form to export encryption keys. ExportKeys, /// The crypto identity setup view. CryptoIdentitySetup, /// The recovery setup view. RecoverySetup, } mod imp { use std::{cell::RefCell, sync::LazyLock}; use glib::subclass::{InitializingObject, Signal}; use super::*; #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)] #[template(resource = "/org/gnome/Fractal/ui/account_settings/mod.ui")] #[properties(wrapper_type = super::AccountSettings)] pub struct AccountSettings { /// The current session. #[property(get, set = Self::set_session, nullable)] session: BoundObjectWeakRef, /// The builder for the account management URL of the OAuth 2.0 /// authorization server, if any. account_management_url_builder: RefCell>, } #[glib::object_subclass] impl ObjectSubclass for AccountSettings { const NAME: &'static str = "AccountSettings"; type Type = super::AccountSettings; type ParentType = adw::PreferencesDialog; fn class_init(klass: &mut Self::Class) { GeneralPage::ensure_type(); NotificationsPage::ensure_type(); SafetyPage::ensure_type(); EncryptionPage::ensure_type(); Self::bind_template(klass); klass.install_action( "account-settings.show-subpage", Some(&AccountSettingsSubpage::static_variant_type()), |obj, _, param| { let subpage = param .and_then(glib::Variant::get::) .expect("The parameter should be a valid subpage name"); obj.show_subpage(subpage); }, ); klass.install_action( "account-settings.show-session-subpage", Some(&String::static_variant_type()), |obj, _, param| { obj.show_session_subpage( ¶m .and_then(glib::Variant::get::) .expect("The parameter should be a string"), ); }, ); klass.install_action_async( "account-settings.reload-user-sessions", None, |obj, _, _| async move { obj.imp().reload_user_sessions().await; }, ); klass.install_action("account-settings.close", None, |obj, _, _| { obj.close(); }); klass.install_action("account-settings.close-subpage", None, |obj, _, _| { obj.pop_subpage(); }); } fn instance_init(obj: &InitializingObject) { obj.init_template(); } } #[glib::derived_properties] impl ObjectImpl for AccountSettings { fn signals() -> &'static [Signal] { static SIGNALS: LazyLock> = LazyLock::new(|| { vec![Signal::builder("account-management-url-builder-changed").build()] }); SIGNALS.as_ref() } } impl WidgetImpl for AccountSettings {} impl AdwDialogImpl for AccountSettings {} impl PreferencesDialogImpl for AccountSettings {} impl AccountSettings { /// Set the current session. fn set_session(&self, session: Option) { if self.session.obj() == session { return; } let obj = self.obj(); self.session.disconnect_signals(); self.set_account_management_url_builder(None); if let Some(session) = session { let logged_out_handler = session.connect_logged_out(clone!( #[weak] obj, move |_| { obj.close(); } )); self.session.set(&session, vec![logged_out_handler]); // Refresh the list of sessions. spawn!(clone!( #[weak(rename_to = imp)] self, async move { imp.reload_user_sessions().await; } )); // Load the account management URL. spawn!(clone!( #[weak(rename_to = imp)] self, async move { imp.load_account_management_url_builder().await; } )); } obj.notify_session(); } /// Load the builder for the account management URL of the OAuth 2.0 /// authorization server. async fn load_account_management_url_builder(&self) { let Some(session) = self.session.obj() else { return; }; let oauth = session.client().oauth(); let handle = spawn_tokio!(async move { oauth.account_management_url().await }); let url_builder = match handle.await.expect("task was not aborted") { Ok(url_builder) => url_builder, Err(error) => { // Ignore the error that says that OAuth 2.0 is not supported, it can happen. if !matches!( error, OAuthError::Discovery(OAuthDiscoveryError::NotSupported) ) { warn!("Could not fetch OAuth 2.0 account management URL: {error}"); } None } }; self.set_account_management_url_builder(url_builder); } /// Set the builder for the account management URL of the OAuth 2.0 /// authorization server. fn set_account_management_url_builder( &self, url_builder: Option, ) { self.account_management_url_builder.replace(url_builder); self.obj() .emit_by_name::<()>("account-management-url-builder-changed", &[]); } /// The builder for the account management URL of the OAuth 2.0 /// authorization server, if any. pub(super) fn account_management_url_builder(&self) -> Option { self.account_management_url_builder.borrow().clone() } /// Reload the sessions from the server. async fn reload_user_sessions(&self) { let Some(session) = self.session.obj() else { return; }; session.user_sessions().load().await; } } } glib::wrapper! { /// Preference window to display and update account settings. pub struct AccountSettings(ObjectSubclass) @extends gtk::Widget, adw::Dialog, adw::PreferencesDialog, @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::ShortcutManager; } impl AccountSettings { /// Construct new `AccountSettings` for the given session. pub fn new(session: &Session) -> Self { glib::Object::builder().property("session", session).build() } /// The builder for the account management URL of the OAuth 2.0 /// authorization server, if any. fn account_management_url_builder(&self) -> Option { self.imp().account_management_url_builder() } /// Show the "Encryption" tab. pub(crate) fn show_encryption_tab(&self) { self.set_visible_page_name("encryption"); } /// Show the given subpage. pub(crate) fn show_subpage(&self, subpage: AccountSettingsSubpage) { let Some(session) = self.session() else { return; }; let page: adw::NavigationPage = match subpage { AccountSettingsSubpage::ChangePassword => ChangePasswordSubpage::new(&session).upcast(), AccountSettingsSubpage::UserSessionList => { UserSessionListSubpage::new(&session).upcast() } AccountSettingsSubpage::LogOut => LogOutSubpage::new(&session).upcast(), AccountSettingsSubpage::DeactivateAccount => { DeactivateAccountSubpage::new(&session, self).upcast() } AccountSettingsSubpage::IgnoredUsers => IgnoredUsersSubpage::new(&session).upcast(), AccountSettingsSubpage::ImportKeys => { ImportExportKeysSubpage::new(&session, ImportExportKeysSubpageMode::Import).upcast() } AccountSettingsSubpage::ExportKeys => { ImportExportKeysSubpage::new(&session, ImportExportKeysSubpageMode::Export).upcast() } AccountSettingsSubpage::CryptoIdentitySetup => { let view = CryptoIdentitySetupView::new(&session); view.connect_completed(clone!( #[weak(rename_to = obj)] self, move |_, _| { obj.pop_subpage(); } )); let page = adw::NavigationPage::builder() .tag("crypto-identity-setup") .child(&view) .build(); page.connect_shown(clone!( #[weak] view, move |_| { view.grab_focus(); } )); page } AccountSettingsSubpage::RecoverySetup => { let view = CryptoRecoverySetupView::new(&session); view.connect_completed(clone!( #[weak(rename_to = obj)] self, move |_| { obj.pop_subpage(); } )); let page = adw::NavigationPage::builder() .tag("crypto-recovery-setup") .child(&view) .build(); page.connect_shown(clone!( #[weak] view, move |_| { view.grab_focus(); } )); page } }; self.push_subpage(&page); } /// Show a subpage with the session details of the given session ID. pub(crate) fn show_session_subpage(&self, device_id: &str) { let Some(session) = self.session() else { return; }; let user_session = session.user_sessions().get(&device_id.into()); let Some(user_session) = user_session else { error!("ID {device_id} is not associated to any device"); return; }; let page = UserSessionSubpage::new(&user_session, self); self.push_subpage(&page); } /// Connect to the signal emitted when the builder for the OAuth 2.0 account /// management URL changed. pub fn connect_account_management_url_builder_changed( &self, f: F, ) -> glib::SignalHandlerId { self.connect_closure( "account-management-url-builder-changed", true, closure_local!(move |obj: Self| { f(&obj); }), ) } }