fractal/session/view/account_settings/
mod.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{
3    glib,
4    glib::{clone, closure_local},
5    CompositeTemplate,
6};
7use matrix_sdk::authentication::oauth::{
8    error::OAuthDiscoveryError, AccountManagementUrlBuilder, OAuthError,
9};
10use tracing::{error, warn};
11
12mod encryption_page;
13mod general_page;
14mod notifications_page;
15mod safety_page;
16mod user_session;
17
18use self::{
19    encryption_page::{EncryptionPage, ImportExportKeysSubpage, ImportExportKeysSubpageMode},
20    general_page::{ChangePasswordSubpage, DeactivateAccountSubpage, GeneralPage, LogOutSubpage},
21    notifications_page::NotificationsPage,
22    safety_page::{IgnoredUsersSubpage, SafetyPage},
23    user_session::{UserSessionListSubpage, UserSessionSubpage},
24};
25use crate::{
26    components::crypto::{CryptoIdentitySetupView, CryptoRecoverySetupView},
27    session::model::Session,
28    spawn, spawn_tokio,
29    utils::BoundObjectWeakRef,
30};
31
32/// A subpage of the account settings.
33#[derive(Debug, Clone, Copy, Eq, PartialEq, glib::Variant, strum::AsRefStr)]
34pub(crate) enum AccountSettingsSubpage {
35    /// A form to change the account's password.
36    ChangePassword,
37    /// A page to view the list of account's sessions.
38    UserSessionList,
39    /// A page to confirm the logout.
40    LogOut,
41    /// A page to confirm the deactivation of the password.
42    DeactivateAccount,
43    /// The list of ignored users.
44    IgnoredUsers,
45    /// A form to import encryption keys.
46    ImportKeys,
47    /// A form to export encryption keys.
48    ExportKeys,
49    /// The crypto identity setup view.
50    CryptoIdentitySetup,
51    /// The recovery setup view.
52    RecoverySetup,
53}
54
55mod imp {
56    use std::{cell::RefCell, sync::LazyLock};
57
58    use glib::subclass::{InitializingObject, Signal};
59
60    use super::*;
61
62    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
63    #[template(resource = "/org/gnome/Fractal/ui/session/view/account_settings/mod.ui")]
64    #[properties(wrapper_type = super::AccountSettings)]
65    pub struct AccountSettings {
66        /// The current session.
67        #[property(get, set = Self::set_session, nullable)]
68        session: BoundObjectWeakRef<Session>,
69        /// The builder for the account management URL of the OAuth 2.0
70        /// authorization server, if any.
71        account_management_url_builder: RefCell<Option<AccountManagementUrlBuilder>>,
72    }
73
74    #[glib::object_subclass]
75    impl ObjectSubclass for AccountSettings {
76        const NAME: &'static str = "AccountSettings";
77        type Type = super::AccountSettings;
78        type ParentType = adw::PreferencesDialog;
79
80        fn class_init(klass: &mut Self::Class) {
81            GeneralPage::ensure_type();
82            NotificationsPage::ensure_type();
83            SafetyPage::ensure_type();
84            EncryptionPage::ensure_type();
85
86            Self::bind_template(klass);
87
88            klass.install_action(
89                "account-settings.show-subpage",
90                Some(&AccountSettingsSubpage::static_variant_type()),
91                |obj, _, param| {
92                    let subpage = param
93                        .and_then(glib::Variant::get::<AccountSettingsSubpage>)
94                        .expect("The parameter should be a valid subpage name");
95
96                    obj.show_subpage(subpage);
97                },
98            );
99
100            klass.install_action(
101                "account-settings.show-session-subpage",
102                Some(&String::static_variant_type()),
103                |obj, _, param| {
104                    obj.show_session_subpage(
105                        &param
106                            .and_then(glib::Variant::get::<String>)
107                            .expect("The parameter should be a string"),
108                    );
109                },
110            );
111
112            klass.install_action_async(
113                "account-settings.reload-user-sessions",
114                None,
115                |obj, _, _| async move {
116                    obj.imp().reload_user_sessions().await;
117                },
118            );
119
120            klass.install_action("account-settings.close", None, |obj, _, _| {
121                obj.close();
122            });
123
124            klass.install_action("account-settings.close-subpage", None, |obj, _, _| {
125                obj.pop_subpage();
126            });
127        }
128
129        fn instance_init(obj: &InitializingObject<Self>) {
130            obj.init_template();
131        }
132    }
133
134    #[glib::derived_properties]
135    impl ObjectImpl for AccountSettings {
136        fn signals() -> &'static [Signal] {
137            static SIGNALS: LazyLock<Vec<Signal>> = LazyLock::new(|| {
138                vec![Signal::builder("account-management-url-builder-changed").build()]
139            });
140            SIGNALS.as_ref()
141        }
142    }
143
144    impl WidgetImpl for AccountSettings {}
145    impl AdwDialogImpl for AccountSettings {}
146    impl PreferencesDialogImpl for AccountSettings {}
147
148    impl AccountSettings {
149        /// Set the current session.
150        fn set_session(&self, session: Option<Session>) {
151            if self.session.obj() == session {
152                return;
153            }
154            let obj = self.obj();
155
156            self.session.disconnect_signals();
157            self.set_account_management_url_builder(None);
158
159            if let Some(session) = session {
160                let logged_out_handler = session.connect_logged_out(clone!(
161                    #[weak]
162                    obj,
163                    move |_| {
164                        obj.close();
165                    }
166                ));
167                self.session.set(&session, vec![logged_out_handler]);
168
169                // Refresh the list of sessions.
170                spawn!(clone!(
171                    #[weak(rename_to = imp)]
172                    self,
173                    async move {
174                        imp.reload_user_sessions().await;
175                    }
176                ));
177
178                // Load the account management URL.
179                spawn!(clone!(
180                    #[weak(rename_to = imp)]
181                    self,
182                    async move {
183                        imp.load_account_management_url_builder().await;
184                    }
185                ));
186            }
187
188            obj.notify_session();
189        }
190
191        /// Load the builder for the account management URL of the OAuth 2.0
192        /// authorization server.
193        async fn load_account_management_url_builder(&self) {
194            let Some(session) = self.session.obj() else {
195                return;
196            };
197
198            let oauth = session.client().oauth();
199            let handle = spawn_tokio!(async move { oauth.account_management_url().await });
200
201            let url_builder = match handle.await.expect("task was not aborted") {
202                Ok(url_builder) => url_builder,
203                Err(error) => {
204                    // Ignore the error that says that OAuth 2.0 is not supported, it can happen.
205                    if !matches!(
206                        error,
207                        OAuthError::Discovery(OAuthDiscoveryError::NotSupported)
208                    ) {
209                        warn!("Could not fetch OAuth 2.0 account management URL: {error}");
210                    }
211                    None
212                }
213            };
214            self.set_account_management_url_builder(url_builder);
215        }
216
217        /// Set the builder for the account management URL of the OAuth 2.0
218        /// authorization server.
219        fn set_account_management_url_builder(
220            &self,
221            url_builder: Option<AccountManagementUrlBuilder>,
222        ) {
223            self.account_management_url_builder.replace(url_builder);
224            self.obj()
225                .emit_by_name::<()>("account-management-url-builder-changed", &[]);
226        }
227
228        /// The builder for the account management URL of the OAuth 2.0
229        /// authorization server, if any.
230        pub(super) fn account_management_url_builder(&self) -> Option<AccountManagementUrlBuilder> {
231            self.account_management_url_builder.borrow().clone()
232        }
233
234        /// Reload the sessions from the server.
235        async fn reload_user_sessions(&self) {
236            let Some(session) = self.session.obj() else {
237                return;
238            };
239
240            session.user_sessions().load().await;
241        }
242    }
243}
244
245glib::wrapper! {
246    /// Preference window to display and update account settings.
247    pub struct AccountSettings(ObjectSubclass<imp::AccountSettings>)
248        @extends gtk::Widget, adw::Dialog, adw::PreferencesDialog, @implements gtk::Accessible;
249}
250
251impl AccountSettings {
252    /// Construct new `AccountSettings` for the given session.
253    pub fn new(session: &Session) -> Self {
254        glib::Object::builder().property("session", session).build()
255    }
256
257    /// The builder for the account management URL of the OAuth 2.0
258    /// authorization server, if any.
259    fn account_management_url_builder(&self) -> Option<AccountManagementUrlBuilder> {
260        self.imp().account_management_url_builder()
261    }
262
263    /// Show the "Encryption" tab.
264    pub(crate) fn show_encryption_tab(&self) {
265        self.set_visible_page_name("encryption");
266    }
267
268    /// Show the given subpage.
269    pub(crate) fn show_subpage(&self, subpage: AccountSettingsSubpage) {
270        let Some(session) = self.session() else {
271            return;
272        };
273
274        let page: adw::NavigationPage = match subpage {
275            AccountSettingsSubpage::ChangePassword => ChangePasswordSubpage::new(&session).upcast(),
276            AccountSettingsSubpage::UserSessionList => {
277                UserSessionListSubpage::new(&session).upcast()
278            }
279            AccountSettingsSubpage::LogOut => LogOutSubpage::new(&session).upcast(),
280            AccountSettingsSubpage::DeactivateAccount => {
281                DeactivateAccountSubpage::new(&session, self).upcast()
282            }
283            AccountSettingsSubpage::IgnoredUsers => IgnoredUsersSubpage::new(&session).upcast(),
284            AccountSettingsSubpage::ImportKeys => {
285                ImportExportKeysSubpage::new(&session, ImportExportKeysSubpageMode::Import).upcast()
286            }
287            AccountSettingsSubpage::ExportKeys => {
288                ImportExportKeysSubpage::new(&session, ImportExportKeysSubpageMode::Export).upcast()
289            }
290            AccountSettingsSubpage::CryptoIdentitySetup => {
291                let view = CryptoIdentitySetupView::new(&session);
292                view.connect_completed(clone!(
293                    #[weak(rename_to = obj)]
294                    self,
295                    move |_, _| {
296                        obj.pop_subpage();
297                    }
298                ));
299
300                let page = adw::NavigationPage::builder()
301                    .tag(AccountSettingsSubpage::CryptoIdentitySetup.as_ref())
302                    .child(&view)
303                    .build();
304                page.connect_shown(clone!(
305                    #[weak]
306                    view,
307                    move |_| {
308                        view.grab_focus();
309                    }
310                ));
311
312                page
313            }
314            AccountSettingsSubpage::RecoverySetup => {
315                let view = CryptoRecoverySetupView::new(&session);
316                view.connect_completed(clone!(
317                    #[weak(rename_to = obj)]
318                    self,
319                    move |_| {
320                        obj.pop_subpage();
321                    }
322                ));
323
324                let page = adw::NavigationPage::builder()
325                    .tag(AccountSettingsSubpage::RecoverySetup.as_ref())
326                    .child(&view)
327                    .build();
328                page.connect_shown(clone!(
329                    #[weak]
330                    view,
331                    move |_| {
332                        view.grab_focus();
333                    }
334                ));
335
336                page
337            }
338        };
339
340        self.push_subpage(&page);
341    }
342
343    /// Show a subpage with the session details of the given session ID.
344    pub(crate) fn show_session_subpage(&self, device_id: &str) {
345        let Some(session) = self.session() else {
346            return;
347        };
348
349        let user_session = session.user_sessions().get(&device_id.into());
350
351        let Some(user_session) = user_session else {
352            error!("ID {device_id} is not associated to any device");
353            return;
354        };
355
356        let page = UserSessionSubpage::new(&user_session, self);
357
358        self.push_subpage(&page);
359    }
360
361    /// Connect to the signal emitted when the builder for the OAuth 2.0 account
362    /// management URL changed.
363    pub fn connect_account_management_url_builder_changed<F: Fn(&Self) + 'static>(
364        &self,
365        f: F,
366    ) -> glib::SignalHandlerId {
367        self.connect_closure(
368            "account-management-url-builder-changed",
369            true,
370            closure_local!(move |obj: Self| {
371                f(&obj);
372            }),
373        )
374    }
375}