fractal/session/view/account_settings/
mod.rs1use 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#[derive(Debug, Clone, Copy, Eq, PartialEq, glib::Variant, strum::AsRefStr)]
34pub(crate) enum AccountSettingsSubpage {
35 ChangePassword,
37 UserSessionList,
39 LogOut,
41 DeactivateAccount,
43 IgnoredUsers,
45 ImportKeys,
47 ExportKeys,
49 CryptoIdentitySetup,
51 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 #[property(get, set = Self::set_session, nullable)]
68 session: BoundObjectWeakRef<Session>,
69 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 ¶m
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 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 spawn!(clone!(
171 #[weak(rename_to = imp)]
172 self,
173 async move {
174 imp.reload_user_sessions().await;
175 }
176 ));
177
178 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 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 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 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 pub(super) fn account_management_url_builder(&self) -> Option<AccountManagementUrlBuilder> {
231 self.account_management_url_builder.borrow().clone()
232 }
233
234 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 pub struct AccountSettings(ObjectSubclass<imp::AccountSettings>)
248 @extends gtk::Widget, adw::Dialog, adw::PreferencesDialog, @implements gtk::Accessible;
249}
250
251impl AccountSettings {
252 pub fn new(session: &Session) -> Self {
254 glib::Object::builder().property("session", session).build()
255 }
256
257 fn account_management_url_builder(&self) -> Option<AccountManagementUrlBuilder> {
260 self.imp().account_management_url_builder()
261 }
262
263 pub(crate) fn show_encryption_tab(&self) {
265 self.set_visible_page_name("encryption");
266 }
267
268 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 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 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}