fractal/session/view/account_settings/general_page/
mod.rs1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::gettext;
3use gtk::{gio, glib, glib::clone, CompositeTemplate};
4use matrix_sdk::authentication::oauth::{AccountManagementActionFull, AccountManagementUrlBuilder};
5use ruma::{api::client::discovery::get_capabilities::Capabilities, OwnedMxcUri};
6use tracing::error;
7
8mod change_password_subpage;
9mod deactivate_account_subpage;
10mod log_out_subpage;
11
12pub use self::{
13 change_password_subpage::ChangePasswordSubpage,
14 deactivate_account_subpage::DeactivateAccountSubpage, log_out_subpage::LogOutSubpage,
15};
16use super::AccountSettings;
17use crate::{
18 components::{ActionButton, ActionState, ButtonCountRow, CopyableRow, EditableAvatar},
19 prelude::*,
20 session::model::Session,
21 spawn, spawn_tokio, toast,
22 utils::{media::FileInfo, OngoingAsyncAction, TemplateCallbacks},
23};
24
25mod imp {
26 use std::cell::RefCell;
27
28 use glib::subclass::InitializingObject;
29
30 use super::*;
31
32 #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
33 #[template(
34 resource = "/org/gnome/Fractal/ui/session/view/account_settings/general_page/mod.ui"
35 )]
36 #[properties(wrapper_type = super::GeneralPage)]
37 pub struct GeneralPage {
38 #[template_child]
39 avatar: TemplateChild<EditableAvatar>,
40 #[template_child]
41 display_name: TemplateChild<adw::EntryRow>,
42 #[template_child]
43 display_name_button: TemplateChild<ActionButton>,
44 #[template_child]
45 user_id: TemplateChild<CopyableRow>,
46 #[template_child]
47 user_sessions_row: TemplateChild<ButtonCountRow>,
48 #[template_child]
49 change_password_row: TemplateChild<adw::ButtonRow>,
50 #[template_child]
51 manage_account_row: TemplateChild<adw::ButtonRow>,
52 #[template_child]
53 homeserver: TemplateChild<CopyableRow>,
54 #[template_child]
55 session_id: TemplateChild<CopyableRow>,
56 #[template_child]
57 deactivate_account_button: TemplateChild<adw::ButtonRow>,
58 #[property(get, set = Self::set_session, nullable)]
60 session: glib::WeakRef<Session>,
61 #[property(get, set = Self::set_account_settings, nullable)]
63 account_settings: glib::WeakRef<AccountSettings>,
64 capabilities: RefCell<Capabilities>,
66 changing_avatar: RefCell<Option<OngoingAsyncAction<OwnedMxcUri>>>,
67 changing_display_name: RefCell<Option<OngoingAsyncAction<String>>>,
68 avatar_uri_handler: RefCell<Option<glib::SignalHandlerId>>,
69 display_name_handler: RefCell<Option<glib::SignalHandlerId>>,
70 user_sessions_count_handler: RefCell<Option<glib::SignalHandlerId>>,
71 }
72
73 #[glib::object_subclass]
74 impl ObjectSubclass for GeneralPage {
75 const NAME: &'static str = "AccountSettingsGeneralPage";
76 type Type = super::GeneralPage;
77 type ParentType = adw::PreferencesPage;
78
79 fn class_init(klass: &mut Self::Class) {
80 Self::bind_template(klass);
81 Self::bind_template_callbacks(klass);
82 TemplateCallbacks::bind_template_callbacks(klass);
83 }
84
85 fn instance_init(obj: &InitializingObject<Self>) {
86 obj.init_template();
87 }
88 }
89
90 #[glib::derived_properties]
91 impl ObjectImpl for GeneralPage {}
92
93 impl WidgetImpl for GeneralPage {}
94 impl PreferencesPageImpl for GeneralPage {}
95
96 #[gtk::template_callbacks]
97 impl GeneralPage {
98 fn set_session(&self, session: Option<Session>) {
100 let prev_session = self.session.upgrade();
101 if prev_session == session {
102 return;
103 }
104
105 if let Some(session) = prev_session {
106 let user = session.user();
107
108 if let Some(handler) = self.avatar_uri_handler.take() {
109 user.avatar_data()
110 .image()
111 .expect("user of session always has an avatar image")
112 .disconnect(handler);
113 }
114 if let Some(handler) = self.display_name_handler.take() {
115 user.disconnect(handler);
116 }
117 if let Some(handler) = self.user_sessions_count_handler.take() {
118 session.user_sessions().other_sessions().disconnect(handler);
119 }
120 }
121
122 self.session.set(session.as_ref());
123 self.obj().notify_session();
124
125 let Some(session) = session else {
126 return;
127 };
128
129 self.user_id.set_subtitle(session.user_id().as_str());
130 self.homeserver.set_subtitle(session.homeserver().as_str());
131 self.session_id.set_subtitle(session.session_id());
132
133 let user = session.user();
134 let avatar_uri_handler = user
135 .avatar_data()
136 .image()
137 .expect("user of session always has an avatar image")
138 .connect_uri_string_notify(clone!(
139 #[weak(rename_to = imp)]
140 self,
141 move |avatar_image| {
142 imp.user_avatar_changed(avatar_image.uri().as_ref());
143 }
144 ));
145 self.avatar_uri_handler.replace(Some(avatar_uri_handler));
146
147 let display_name_handler = user.connect_display_name_notify(clone!(
148 #[weak(rename_to=imp)]
149 self,
150 move |user| {
151 imp.user_display_name_changed(&user.display_name());
152 }
153 ));
154 self.display_name_handler
155 .replace(Some(display_name_handler));
156
157 let other_user_sessions = session.user_sessions().other_sessions();
158 let user_sessions_count_handler = other_user_sessions.connect_items_changed(clone!(
159 #[weak(rename_to = imp)]
160 self,
161 move |other_user_sessions, _, _, _| {
162 imp.user_sessions_row
163 .set_count((other_user_sessions.n_items() + 1).to_string());
164 }
165 ));
166 self.user_sessions_row
167 .set_count((other_user_sessions.n_items() + 1).to_string());
168 self.user_sessions_count_handler
169 .replace(Some(user_sessions_count_handler));
170
171 spawn!(
172 glib::Priority::LOW,
173 clone!(
174 #[weak(rename_to = imp)]
175 self,
176 async move {
177 imp.load_capabilities().await;
178 }
179 )
180 );
181 }
182
183 fn set_account_settings(&self, account_settings: Option<&AccountSettings>) {
185 self.account_settings.set(account_settings);
186
187 if let Some(account_settings) = account_settings {
188 account_settings.connect_account_management_url_builder_changed(clone!(
189 #[weak(rename_to = imp)]
190 self,
191 move |_| {
192 imp.update_capabilities();
193 }
194 ));
195 }
196
197 self.update_capabilities();
198 }
199
200 fn account_management_url_builder(&self) -> Option<AccountManagementUrlBuilder> {
203 self.account_settings
204 .upgrade()
205 .and_then(|s| s.account_management_url_builder())
206 }
207
208 async fn load_capabilities(&self) {
210 let Some(session) = self.session.upgrade() else {
211 return;
212 };
213 let client = session.client();
214
215 let handle = spawn_tokio!(async move { client.get_capabilities().await });
216 let capabilities = match handle.await.expect("task was not aborted") {
217 Ok(capabilities) => capabilities,
218 Err(error) => {
219 error!("Could not get server capabilities: {error}");
220 Capabilities::default()
221 }
222 };
223
224 self.capabilities.replace(capabilities);
225 self.update_capabilities();
226 }
227
228 fn update_capabilities(&self) {
231 let Some(session) = self.session.upgrade() else {
232 return;
233 };
234
235 let uses_oauth_api = session.uses_oauth_api();
236 let has_account_management_url = self.account_management_url_builder().is_some();
237 let capabilities = self.capabilities.borrow();
238
239 self.avatar
240 .set_editable(capabilities.set_avatar_url.enabled);
241 self.display_name
242 .set_editable(capabilities.set_displayname.enabled);
243 self.change_password_row
244 .set_visible(!has_account_management_url && capabilities.change_password.enabled);
245 self.manage_account_row
246 .set_visible(has_account_management_url);
247 self.deactivate_account_button
248 .set_visible(!uses_oauth_api || has_account_management_url);
249 }
250
251 #[template_callback]
253 async fn manage_account(&self) {
254 let Some(url_builder) = self.account_management_url_builder() else {
255 error!("Could not find open account management URL");
256 return;
257 };
258
259 let url = url_builder
260 .action(AccountManagementActionFull::Profile)
261 .build();
262
263 if let Err(error) = gtk::UriLauncher::new(url.as_str())
264 .launch_future(self.obj().root().and_downcast_ref::<gtk::Window>())
265 .await
266 {
267 error!("Could not launch account management URL: {error}");
268 }
269 }
270
271 fn user_avatar_changed(&self, uri: Option<&OwnedMxcUri>) {
273 if let Some(action) = self.changing_avatar.borrow().as_ref() {
274 if uri != action.as_value() {
275 return;
278 }
279 } else {
280 return;
282 }
283
284 self.changing_avatar.take();
286 self.avatar.success();
287
288 let obj = self.obj();
289 if uri.is_none() {
290 toast!(obj, gettext("Avatar removed successfully"));
291 } else {
292 toast!(obj, gettext("Avatar changed successfully"));
293 }
294 }
295
296 #[template_callback]
298 async fn change_avatar(&self, file: gio::File) {
299 let Some(session) = self.session.upgrade() else {
300 return;
301 };
302
303 let avatar = &self.avatar;
304 avatar.edit_in_progress();
305
306 let info = match FileInfo::try_from_file(&file).await {
307 Ok(info) => info,
308 Err(error) => {
309 error!("Could not load user avatar file info: {error}");
310 toast!(self.obj(), gettext("Could not load file"));
311 avatar.reset();
312 return;
313 }
314 };
315
316 let data = match file.load_contents_future().await {
317 Ok((data, _)) => data,
318 Err(error) => {
319 error!("Could not load user avatar file: {error}");
320 toast!(self.obj(), gettext("Could not load file"));
321 avatar.reset();
322 return;
323 }
324 };
325
326 let client = session.client();
327 let client_clone = client.clone();
328 let handle = spawn_tokio!(async move {
329 client_clone
330 .media()
331 .upload(&info.mime, data.into(), None)
332 .await
333 });
334
335 let uri = match handle.await.expect("task was not aborted") {
336 Ok(res) => res.content_uri,
337 Err(error) => {
338 error!("Could not upload user avatar: {error}");
339 toast!(self.obj(), gettext("Could not upload avatar"));
340 avatar.reset();
341 return;
342 }
343 };
344
345 let (action, weak_action) = OngoingAsyncAction::set(uri.clone());
346 self.changing_avatar.replace(Some(action));
347
348 let uri_clone = uri.clone();
349 let handle =
350 spawn_tokio!(
351 async move { client.account().set_avatar_url(Some(&uri_clone)).await }
352 );
353
354 match handle.await.expect("task was not aborted") {
355 Ok(()) => {
356 if weak_action.is_ongoing() {
361 session.user().set_avatar_url(Some(uri));
362 }
363 }
364 Err(error) => {
365 if weak_action.is_ongoing() {
368 self.changing_avatar.take();
369 error!("Could not change user avatar: {error}");
370 toast!(self.obj(), gettext("Could not change avatar"));
371 avatar.reset();
372 }
373 }
374 }
375 }
376
377 #[template_callback]
379 async fn remove_avatar(&self) {
380 let Some(session) = self.session.upgrade() else {
381 return;
382 };
383
384 let confirm_dialog = adw::AlertDialog::builder()
386 .default_response("cancel")
387 .heading(gettext("Remove Avatar?"))
388 .body(gettext("Do you really want to remove your avatar?"))
389 .build();
390 confirm_dialog.add_responses(&[
391 ("cancel", &gettext("Cancel")),
392 ("remove", &gettext("Remove")),
393 ]);
394 confirm_dialog.set_response_appearance("remove", adw::ResponseAppearance::Destructive);
395
396 let obj = self.obj();
397 if confirm_dialog.choose_future(&*obj).await != "remove" {
398 return;
399 }
400
401 let avatar = &*self.avatar;
402 avatar.removal_in_progress();
403
404 let (action, weak_action) = OngoingAsyncAction::remove();
405 self.changing_avatar.replace(Some(action));
406
407 let client = session.client();
408 let handle = spawn_tokio!(async move { client.account().set_avatar_url(None).await });
409
410 match handle.await.expect("task was not aborted") {
411 Ok(()) => {
412 if weak_action.is_ongoing() {
417 session.user().set_avatar_url(None);
418 }
419 }
420 Err(error) => {
421 if weak_action.is_ongoing() {
424 self.changing_avatar.take();
425 error!("Could not remove user avatar: {error}");
426 toast!(obj, gettext("Could not remove avatar"));
427 avatar.reset();
428 }
429 }
430 }
431 }
432
433 #[template_callback]
435 fn display_name_changed(&self) {
436 self.display_name_button
437 .set_visible(self.has_display_name_changed());
438 }
439
440 fn has_display_name_changed(&self) -> bool {
443 let Some(session) = self.session.upgrade() else {
444 return false;
445 };
446 let text = self.display_name.text();
447 let display_name = session.user().display_name();
448
449 text != display_name
450 }
451
452 fn user_display_name_changed(&self, name: &str) {
454 if let Some(action) = self.changing_display_name.borrow().as_ref() {
455 if action.as_value().map(String::as_str) == Some(name) {
456 return;
459 }
460 } else {
461 return;
463 }
464
465 self.changing_display_name.take();
467
468 let entry = &self.display_name;
469 let button = &self.display_name_button;
470
471 entry.remove_css_class("error");
472 entry.set_sensitive(true);
473 button.set_visible(false);
474 button.set_state(ActionState::Confirm);
475 toast!(self.obj(), gettext("Name changed successfully"));
476 }
477
478 #[template_callback]
480 async fn change_display_name(&self) {
481 if !self.has_display_name_changed() {
482 return;
484 }
485 let Some(session) = self.session.upgrade() else {
486 return;
487 };
488
489 let entry = &self.display_name;
490 let button = &self.display_name_button;
491
492 entry.set_sensitive(false);
493 button.set_state(ActionState::Loading);
494
495 let display_name = entry.text().trim().to_string();
496
497 let (action, weak_action) = OngoingAsyncAction::set(display_name.clone());
498 self.changing_display_name.replace(Some(action));
499
500 let client = session.client();
501 let display_name_clone = display_name.clone();
502 let handle = spawn_tokio!(async move {
503 client
504 .account()
505 .set_display_name(Some(&display_name_clone))
506 .await
507 });
508
509 match handle.await.expect("task was not aborted") {
510 Ok(()) => {
511 if weak_action.is_ongoing() {
516 session.user().set_name(Some(display_name));
517 }
518 }
519 Err(error) => {
520 if weak_action.is_ongoing() {
523 self.changing_display_name.take();
524 error!("Could not change user display name: {error}");
525 toast!(self.obj(), gettext("Could not change display name"));
526 button.set_state(ActionState::Retry);
527 entry.add_css_class("error");
528 entry.set_sensitive(true);
529 }
530 }
531 }
532 }
533 }
534}
535
536glib::wrapper! {
537 pub struct GeneralPage(ObjectSubclass<imp::GeneralPage>)
539 @extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible;
540}
541
542impl GeneralPage {
543 pub fn new(session: &Session) -> Self {
544 glib::Object::builder().property("session", session).build()
545 }
546}