You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

375 lines
13 KiB

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<Session>,
/// The builder for the account management URL of the OAuth 2.0
/// authorization server, if any.
account_management_url_builder: RefCell<Option<AccountManagementUrlBuilder>>,
}
#[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::<AccountSettingsSubpage>)
.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(
&param
.and_then(glib::Variant::get::<String>)
.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<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for AccountSettings {
fn signals() -> &'static [Signal] {
static SIGNALS: LazyLock<Vec<Signal>> = 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<Session>) {
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<AccountManagementUrlBuilder>,
) {
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<AccountManagementUrlBuilder> {
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<imp::AccountSettings>)
@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<AccountManagementUrlBuilder> {
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<F: Fn(&Self) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_closure(
"account-management-url-builder-changed",
true,
closure_local!(move |obj: Self| {
f(&obj);
}),
)
}
}