diff --git a/src/session/view/account_settings/general_page/change_password_subpage.rs b/src/session/view/account_settings/general_page/change_password_subpage.rs index 5d4e6b1a..0bc244d9 100644 --- a/src/session/view/account_settings/general_page/change_password_subpage.rs +++ b/src/session/view/account_settings/general_page/change_password_subpage.rs @@ -1,9 +1,6 @@ use adw::{prelude::*, subclass::prelude::*}; use gettextrs::gettext; -use gtk::{ - glib::{self, clone}, - CompositeTemplate, -}; +use gtk::{glib, CompositeTemplate}; use ruma::api::client::error::ErrorKind; use tracing::error; @@ -25,25 +22,25 @@ mod imp { )] #[properties(wrapper_type = super::ChangePasswordSubpage)] pub struct ChangePasswordSubpage { - /// The current session. - #[property(get, set, nullable)] - pub session: glib::WeakRef, #[template_child] - pub password: TemplateChild, + password: TemplateChild, #[template_child] - pub password_progress: TemplateChild, + password_progress: TemplateChild, #[template_child] - pub password_error_revealer: TemplateChild, + password_error_revealer: TemplateChild, #[template_child] - pub password_error: TemplateChild, + password_error: TemplateChild, #[template_child] - pub confirm_password: TemplateChild, + confirm_password: TemplateChild, #[template_child] - pub confirm_password_error_revealer: TemplateChild, + confirm_password_error_revealer: TemplateChild, #[template_child] - pub confirm_password_error: TemplateChild, + confirm_password_error: TemplateChild, #[template_child] - pub button: TemplateChild, + button: TemplateChild, + /// The current session. + #[property(get, set, nullable)] + session: glib::WeakRef, } #[glib::object_subclass] @@ -54,7 +51,7 @@ mod imp { fn class_init(klass: &mut Self::Class) { Self::bind_template(klass); - Self::Type::bind_template_callbacks(klass); + Self::bind_template_callbacks(klass); } fn instance_init(obj: &InitializingObject) { @@ -63,201 +60,173 @@ mod imp { } #[glib::derived_properties] - impl ObjectImpl for ChangePasswordSubpage { - fn constructed(&self) { - self.parent_constructed(); - let obj = self.obj(); - - self.password_progress.set_min_value(0.0); - self.password_progress.set_max_value(5.0); - self.password_progress - .add_offset_value(gtk::LEVEL_BAR_OFFSET_LOW, 1.0); - self.password_progress.add_offset_value("step2", 2.0); - self.password_progress.add_offset_value("step3", 3.0); - self.password_progress - .add_offset_value(gtk::LEVEL_BAR_OFFSET_HIGH, 4.0); - self.password_progress - .add_offset_value(gtk::LEVEL_BAR_OFFSET_FULL, 5.0); - - self.password.connect_changed(clone!( - #[weak] - obj, - move |_| { - obj.validate_password(); - obj.validate_password_confirmation(); - } - )); - - self.confirm_password.connect_changed(clone!( - #[weak] - obj, - move |_| { - obj.validate_password_confirmation(); - } - )); - } - } + impl ObjectImpl for ChangePasswordSubpage {} impl WidgetImpl for ChangePasswordSubpage {} impl NavigationPageImpl for ChangePasswordSubpage {} -} -glib::wrapper! { - /// Account settings page about the user and the session. - pub struct ChangePasswordSubpage(ObjectSubclass) - @extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible; -} - -#[gtk::template_callbacks] -impl ChangePasswordSubpage { - pub fn new(session: &Session) -> Self { - glib::Object::builder().property("session", session).build() - } + #[gtk::template_callbacks] + impl ChangePasswordSubpage { + #[template_callback] + fn validate_password(&self) { + let entry = &self.password; + let progress = &self.password_progress; + let revealer = &self.password_error_revealer; + let label = &self.password_error; + let password = entry.text(); + + if password.is_empty() { + revealer.set_reveal_child(false); + entry.remove_css_class("success"); + entry.remove_css_class("warning"); + progress.set_value(0.0); + progress.remove_css_class("success"); + progress.remove_css_class("warning"); + self.update_button(); + return; + } - fn validate_password(&self) { - let imp = self.imp(); - let entry = &imp.password; - let progress = &imp.password_progress; - let revealer = &imp.password_error_revealer; - let label = &imp.password_error; - let password = entry.text(); + let validity = validate_password(&password); + + progress.set_value(f64::from(validity.progress) / 20.0); + if validity.progress == 100 { + revealer.set_reveal_child(false); + entry.add_css_class("success"); + entry.remove_css_class("warning"); + progress.add_css_class("success"); + progress.remove_css_class("warning"); + } else { + entry.remove_css_class("success"); + entry.add_css_class("warning"); + progress.remove_css_class("success"); + progress.add_css_class("warning"); + if !validity.has_length { + label.set_label(&gettext("Password must be at least 8 characters long")); + } else if !validity.has_lowercase { + label.set_label(&gettext( + "Password must have at least one lower-case letter", + )); + } else if !validity.has_uppercase { + label.set_label(&gettext( + "Password must have at least one upper-case letter", + )); + } else if !validity.has_number { + label.set_label(&gettext("Password must have at least one digit")); + } else if !validity.has_symbol { + label.set_label(&gettext("Password must have at least one symbol")); + } + revealer.set_reveal_child(true); + } - if password.is_empty() { - revealer.set_reveal_child(false); - entry.remove_css_class("success"); - entry.remove_css_class("warning"); - progress.set_value(0.0); - progress.remove_css_class("success"); - progress.remove_css_class("warning"); - self.update_button(); - return; + self.validate_password_confirmation(); } - let validity = validate_password(&password); - - progress.set_value(f64::from(validity.progress) / 20.0); - if validity.progress == 100 { - revealer.set_reveal_child(false); - entry.add_css_class("success"); - entry.remove_css_class("warning"); - progress.add_css_class("success"); - progress.remove_css_class("warning"); - } else { - entry.remove_css_class("success"); - entry.add_css_class("warning"); - progress.remove_css_class("success"); - progress.add_css_class("warning"); - if !validity.has_length { - label.set_label(&gettext("Password must be at least 8 characters long")); - } else if !validity.has_lowercase { - label.set_label(&gettext( - "Password must have at least one lower-case letter", - )); - } else if !validity.has_uppercase { - label.set_label(&gettext( - "Password must have at least one upper-case letter", - )); - } else if !validity.has_number { - label.set_label(&gettext("Password must have at least one digit")); - } else if !validity.has_symbol { - label.set_label(&gettext("Password must have at least one symbol")); + #[template_callback] + fn validate_password_confirmation(&self) { + let entry = &self.confirm_password; + let revealer = &self.confirm_password_error_revealer; + let label = &self.confirm_password_error; + let password = self.password.text(); + let confirmation = entry.text(); + + if confirmation.is_empty() { + revealer.set_reveal_child(false); + entry.remove_css_class("success"); + entry.remove_css_class("warning"); + return; } - revealer.set_reveal_child(true); - } - - self.update_button(); - } - fn validate_password_confirmation(&self) { - let imp = self.imp(); - let entry = &imp.confirm_password; - let revealer = &imp.confirm_password_error_revealer; - let label = &imp.confirm_password_error; - let password = imp.password.text(); - let confirmation = entry.text(); + if password == confirmation { + revealer.set_reveal_child(false); + entry.add_css_class("success"); + entry.remove_css_class("warning"); + } else { + entry.remove_css_class("success"); + entry.add_css_class("warning"); + label.set_label(&gettext("Passwords do not match")); + revealer.set_reveal_child(true); + } - if confirmation.is_empty() { - revealer.set_reveal_child(false); - entry.remove_css_class("success"); - entry.remove_css_class("warning"); - return; + self.update_button(); } - if password == confirmation { - revealer.set_reveal_child(false); - entry.add_css_class("success"); - entry.remove_css_class("warning"); - } else { - entry.remove_css_class("success"); - entry.add_css_class("warning"); - label.set_label(&gettext("Passwords do not match")); - revealer.set_reveal_child(true); + fn update_button(&self) { + self.button.set_sensitive(self.can_change_password()); } - self.update_button(); - } - - fn update_button(&self) { - self.imp().button.set_sensitive(self.can_change_password()); - } - - fn can_change_password(&self) -> bool { - let imp = self.imp(); - let password = imp.password.text(); - let confirmation = imp.confirm_password.text(); - - validate_password(&password).progress == 100 && password == confirmation - } - #[template_callback] - async fn change_password(&self) { - let Some(session) = self.session() else { - return; - }; + fn can_change_password(&self) -> bool { + let password = self.password.text(); + let confirmation = self.confirm_password.text(); - if !self.can_change_password() { - return; + validate_password(&password).progress == 100 && password == confirmation } - let imp = self.imp(); - let password = imp.password.text(); + #[template_callback] + async fn change_password(&self) { + let Some(session) = self.session.upgrade() else { + return; + }; - imp.button.set_is_loading(true); - imp.password.set_sensitive(false); - imp.confirm_password.set_sensitive(false); + if !self.can_change_password() { + return; + } - let dialog = AuthDialog::new(&session); + let password = self.password.text(); - let result = dialog - .authenticate(self, move |client, auth| { - let password = password.clone(); - async move { client.account().change_password(&password, auth).await } - }) - .await; + self.button.set_is_loading(true); + self.password.set_sensitive(false); + self.confirm_password.set_sensitive(false); - match result { - Ok(_) => { - toast!(self, gettext("Password changed successfully")); - imp.password.set_text(""); - imp.confirm_password.set_text(""); - self.activate_action("account-settings.close-subpage", None) - .unwrap(); - } - Err(error) => match error { - AuthError::UserCancelled => {} - AuthError::ServerResponse(error) - if matches!(error.client_api_error_kind(), Some(ErrorKind::WeakPassword)) => - { - error!("Weak password: {error}"); - toast!(self, gettext("Password rejected for being too weak")); - } - _ => { - error!("Could not change the password: {error:?}"); - toast!(self, gettext("Could not change password")); + let obj = self.obj(); + let dialog = AuthDialog::new(&session); + + let result = dialog + .authenticate(&*obj, move |client, auth| { + let password = password.clone(); + async move { client.account().change_password(&password, auth).await } + }) + .await; + + match result { + Ok(_) => { + toast!(obj, gettext("Password changed successfully")); + self.password.set_text(""); + self.confirm_password.set_text(""); + let _ = obj.activate_action("account-settings.close-subpage", None); } - }, + Err(error) => match error { + AuthError::UserCancelled => {} + AuthError::ServerResponse(error) + if matches!( + error.client_api_error_kind(), + Some(ErrorKind::WeakPassword) + ) => + { + error!("Weak password: {error}"); + toast!(obj, gettext("Password rejected for being too weak")); + } + _ => { + error!("Could not change the password: {error}"); + toast!(obj, gettext("Could not change password")); + } + }, + } + + self.button.set_is_loading(false); + self.password.set_sensitive(true); + self.confirm_password.set_sensitive(true); } - imp.button.set_is_loading(false); - imp.password.set_sensitive(true); - imp.confirm_password.set_sensitive(true); + } +} + +glib::wrapper! { + /// Subpage allowing the user to change the account's password. + pub struct ChangePasswordSubpage(ObjectSubclass) + @extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible; +} + +impl ChangePasswordSubpage { + pub fn new(session: &Session) -> Self { + glib::Object::builder().property("session", session).build() } } diff --git a/src/session/view/account_settings/general_page/change_password_subpage.ui b/src/session/view/account_settings/general_page/change_password_subpage.ui index 4e335e80..7bdc503c 100644 --- a/src/session/view/account_settings/general_page/change_password_subpage.ui +++ b/src/session/view/account_settings/general_page/change_password_subpage.ui @@ -68,6 +68,7 @@ password_error New Password + @@ -78,6 +79,14 @@ 2 1 discrete + 5 + + + + + + + presentation @@ -114,6 +123,7 @@ confirm_password_error Confirm New Password + diff --git a/src/session/view/account_settings/general_page/deactivate_account_subpage.rs b/src/session/view/account_settings/general_page/deactivate_account_subpage.rs index a94064c6..db73547b 100644 --- a/src/session/view/account_settings/general_page/deactivate_account_subpage.rs +++ b/src/session/view/account_settings/general_page/deactivate_account_subpage.rs @@ -185,7 +185,7 @@ mod imp { } glib::wrapper! { - /// Account settings page about the user and the session. + /// Subpage allowing the user to deactivate their account. pub struct DeactivateAccountSubpage(ObjectSubclass) @extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible; } diff --git a/src/session/view/account_settings/general_page/log_out_subpage.rs b/src/session/view/account_settings/general_page/log_out_subpage.rs index 7bde8daa..e08f826f 100644 --- a/src/session/view/account_settings/general_page/log_out_subpage.rs +++ b/src/session/view/account_settings/general_page/log_out_subpage.rs @@ -22,23 +22,23 @@ mod imp { )] #[properties(wrapper_type = super::LogOutSubpage)] pub struct LogOutSubpage { - /// The current session. - #[property(get, set = Self::set_session, nullable)] - pub session: glib::WeakRef, #[template_child] - pub stack: TemplateChild, + stack: TemplateChild, #[template_child] - pub warning_box: TemplateChild, + warning_box: TemplateChild, #[template_child] - pub warning_description: TemplateChild, + warning_description: TemplateChild, #[template_child] - pub warning_button: TemplateChild, + warning_button: TemplateChild, #[template_child] - pub logout_button: TemplateChild, + logout_button: TemplateChild, #[template_child] - pub try_again_button: TemplateChild, + try_again_button: TemplateChild, #[template_child] - pub remove_button: TemplateChild, + remove_button: TemplateChild, + /// The current session. + #[property(get, set = Self::set_session, nullable)] + session: glib::WeakRef, } #[glib::object_subclass] @@ -49,7 +49,7 @@ mod imp { fn class_init(klass: &mut Self::Class) { Self::bind_template(klass); - Self::Type::bind_template_callbacks(klass); + Self::bind_template_callbacks(klass); } fn instance_init(obj: &InitializingObject) { @@ -63,15 +63,15 @@ mod imp { impl WidgetImpl for LogOutSubpage {} impl NavigationPageImpl for LogOutSubpage {} + #[gtk::template_callbacks] impl LogOutSubpage { /// Set the current session. fn set_session(&self, session: Option<&Session>) { self.session.set(session); - self.update_warning(); } - /// Update the warning. + /// Update the warning message. fn update_warning(&self) { let Some(session) = self.session.upgrade() else { return; @@ -100,83 +100,82 @@ mod imp { // No particular problem, do not show the warning. self.warning_box.set_visible(false); } - } -} -glib::wrapper! { - /// Subpage allowing a user to log out from their account. - pub struct LogOutSubpage(ObjectSubclass) - @extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible; -} + /// Show the security tab of the settings. + #[template_callback] + fn view_security(&self) { + let Some(dialog) = self + .obj() + .ancestor(AccountSettings::static_type()) + .and_downcast::() + else { + return; + }; -#[gtk::template_callbacks] -impl LogOutSubpage { - pub fn new(session: &Session) -> Self { - glib::Object::builder().property("session", session).build() - } + dialog.pop_subpage(); + dialog.set_visible_page_name("security"); + } - /// Show the security tab of the settings. - #[template_callback] - fn view_security(&self) { - let Some(dialog) = self - .ancestor(AccountSettings::static_type()) - .and_downcast::() - else { - return; - }; - - dialog.pop_subpage(); - dialog.set_visible_page_name("security"); - } + /// Log out the current session. + #[template_callback] + async fn log_out(&self) { + let Some(session) = self.session.upgrade() else { + return; + }; - /// Log out the current session. - #[template_callback] - async fn log_out(&self) { - let Some(session) = self.session() else { - return; - }; - - let imp = self.imp(); - let is_logout_page = imp - .stack - .visible_child_name() - .is_some_and(|name| name == "logout"); - - if is_logout_page { - imp.logout_button.set_is_loading(true); - imp.warning_button.set_sensitive(false); - } else { - imp.try_again_button.set_is_loading(true); - } + let is_logout_page = self + .stack + .visible_child_name() + .is_some_and(|name| name == "logout"); - if let Err(error) = session.log_out().await { if is_logout_page { - imp.stack.set_visible_child_name("failed"); + self.logout_button.set_is_loading(true); + self.warning_button.set_sensitive(false); } else { - toast!(self, error); + self.try_again_button.set_is_loading(true); + } + + if let Err(error) = session.log_out().await { + if is_logout_page { + self.stack.set_visible_child_name("failed"); + } else { + let obj = self.obj(); + toast!(obj, error); + } } - } - if is_logout_page { - imp.logout_button.set_is_loading(false); - imp.warning_button.set_sensitive(true); - } else { - imp.try_again_button.set_is_loading(false); + if is_logout_page { + self.logout_button.set_is_loading(false); + self.warning_button.set_sensitive(true); + } else { + self.try_again_button.set_is_loading(false); + } } - } - /// Remove the current session. - #[template_callback] - async fn remove(&self) { - let Some(session) = self.session() else { - return; - }; + /// Remove the current session. + #[template_callback] + async fn remove(&self) { + let Some(session) = self.session.upgrade() else { + return; + }; - let imp = self.imp(); - imp.remove_button.set_is_loading(true); + self.remove_button.set_is_loading(true); - session.clean_up().await; + session.clean_up().await; - imp.remove_button.set_is_loading(false); + self.remove_button.set_is_loading(false); + } + } +} + +glib::wrapper! { + /// Subpage allowing a user to log out from their account. + pub struct LogOutSubpage(ObjectSubclass) + @extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible; +} + +impl LogOutSubpage { + pub fn new(session: &Session) -> Self { + glib::Object::builder().property("session", session).build() } } diff --git a/src/session/view/account_settings/general_page/mod.rs b/src/session/view/account_settings/general_page/mod.rs index b2fac496..40e25147 100644 --- a/src/session/view/account_settings/general_page/mod.rs +++ b/src/session/view/account_settings/general_page/mod.rs @@ -1,10 +1,6 @@ use adw::{prelude::*, subclass::prelude::*}; use gettextrs::gettext; -use gtk::{ - gio, - glib::{self, clone}, - CompositeTemplate, -}; +use gtk::{gio, glib, glib::clone, CompositeTemplate}; use matrix_sdk::authentication::oauth::{AccountManagementActionFull, AccountManagementUrlBuilder}; use ruma::{api::client::discovery::get_capabilities::Capabilities, OwnedMxcUri}; use tracing::error; @@ -40,21 +36,21 @@ mod imp { #[properties(wrapper_type = super::GeneralPage)] pub struct GeneralPage { #[template_child] - pub avatar: TemplateChild, + avatar: TemplateChild, #[template_child] - pub display_name: TemplateChild, + display_name: TemplateChild, #[template_child] - pub display_name_button: TemplateChild, + display_name_button: TemplateChild, #[template_child] - pub change_password_group: TemplateChild, + change_password_group: TemplateChild, #[template_child] manage_account_group: TemplateChild, #[template_child] - pub homeserver: TemplateChild, + homeserver: TemplateChild, #[template_child] - pub user_id: TemplateChild, + user_id: TemplateChild, #[template_child] - pub session_id: TemplateChild, + session_id: TemplateChild, #[template_child] deactivate_account_button: TemplateChild, /// The current session. @@ -65,10 +61,10 @@ mod imp { account_settings: glib::WeakRef, /// The possible changes on the homeserver. capabilities: RefCell, - pub changing_avatar: RefCell>>, - pub changing_display_name: RefCell>>, - pub avatar_uri_handler: RefCell>, - pub display_name_handler: RefCell>, + changing_avatar: RefCell>>, + changing_display_name: RefCell>>, + avatar_uri_handler: RefCell>, + display_name_handler: RefCell>, } #[glib::object_subclass] @@ -80,7 +76,6 @@ mod imp { fn class_init(klass: &mut Self::Class) { Self::bind_template(klass); Self::bind_template_callbacks(klass); - Self::Type::bind_template_callbacks(klass); TemplateCallbacks::bind_template_callbacks(klass); } @@ -103,13 +98,15 @@ mod imp { if prev_session == session { return; } - let obj = self.obj(); if let Some(session) = prev_session { let user = session.user(); if let Some(handler) = self.avatar_uri_handler.take() { - user.avatar_data().image().unwrap().disconnect(handler); + user.avatar_data() + .image() + .expect("user of session always has an avatar image") + .disconnect(handler); } if let Some(handler) = self.display_name_handler.take() { user.disconnect(handler); @@ -117,7 +114,7 @@ mod imp { } self.session.set(session.as_ref()); - obj.notify_session(); + self.obj().notify_session(); let Some(session) = session else { return; @@ -131,21 +128,21 @@ mod imp { let avatar_uri_handler = user .avatar_data() .image() - .unwrap() + .expect("user of session always has an avatar image") .connect_uri_string_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |avatar_image| { - obj.user_avatar_changed(avatar_image.uri().as_ref()); + imp.user_avatar_changed(avatar_image.uri().as_ref()); } )); self.avatar_uri_handler.replace(Some(avatar_uri_handler)); let display_name_handler = user.connect_display_name_notify(clone!( - #[weak] - obj, + #[weak(rename_to=imp)] + self, move |user| { - obj.user_display_name_changed(&user.display_name()); + imp.user_display_name_changed(&user.display_name()); } )); self.display_name_handler @@ -196,7 +193,7 @@ mod imp { let client = session.client(); let handle = spawn_tokio!(async move { client.get_capabilities().await }); - let capabilities = match handle.await.unwrap() { + let capabilities = match handle.await.expect("task was not aborted") { Ok(capabilities) => capabilities, Err(error) => { error!("Could not get server capabilities: {error}"); @@ -250,284 +247,286 @@ mod imp { error!("Could not launch account management URL: {error}"); } } - } -} - -glib::wrapper! { - /// Account settings page about the user and the session. - pub struct GeneralPage(ObjectSubclass) - @extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible; -} - -#[gtk::template_callbacks] -impl GeneralPage { - pub fn new(session: &Session) -> Self { - glib::Object::builder().property("session", session).build() - } - - /// Update the view when the user's avatar changed. - fn user_avatar_changed(&self, uri: Option<&OwnedMxcUri>) { - let imp = self.imp(); - if let Some(action) = imp.changing_avatar.borrow().as_ref() { - if uri != action.as_value() { - // This is not the change we expected, maybe another device did a change too. - // Let's wait for another change. + /// Update the view when the user's avatar changed. + fn user_avatar_changed(&self, uri: Option<&OwnedMxcUri>) { + if let Some(action) = self.changing_avatar.borrow().as_ref() { + if uri != action.as_value() { + // This is not the change we expected, maybe another device did a change too. + // Let's wait for another change. + return; + } + } else { + // No action is ongoing, we don't need to do anything. return; } - } else { - // No action is ongoing, we don't need to do anything. - return; - } - // Reset the state. - imp.changing_avatar.take(); - imp.avatar.success(); - if uri.is_none() { - toast!(self, gettext("Avatar removed successfully")); - } else { - toast!(self, gettext("Avatar changed successfully")); - } - } + // Reset the state. + self.changing_avatar.take(); + self.avatar.success(); - /// Change the avatar of the user with the one in the given file. - #[template_callback] - async fn change_avatar(&self, file: gio::File) { - let Some(session) = self.session() else { - return; - }; - - let imp = self.imp(); - let avatar = &imp.avatar; - avatar.edit_in_progress(); - - let info = match FileInfo::try_from_file(&file).await { - Ok(info) => info, - Err(error) => { - error!("Could not load user avatar file info: {error}"); - toast!(self, gettext("Could not load file")); - avatar.reset(); - return; - } - }; - - let data = match file.load_contents_future().await { - Ok((data, _)) => data, - Err(error) => { - error!("Could not load user avatar file: {error}"); - toast!(self, gettext("Could not load file")); - avatar.reset(); - return; + let obj = self.obj(); + if uri.is_none() { + toast!(obj, gettext("Avatar removed successfully")); + } else { + toast!(obj, gettext("Avatar changed successfully")); } - }; - - let client = session.client(); - let client_clone = client.clone(); - let handle = spawn_tokio!(async move { - client_clone - .media() - .upload(&info.mime, data.into(), None) - .await - }); - - let uri = match handle.await.unwrap() { - Ok(res) => res.content_uri, - Err(error) => { - error!("Could not upload user avatar: {error}"); - toast!(self, gettext("Could not upload avatar")); - avatar.reset(); + } + + /// Change the avatar of the user with the one in the given file. + #[template_callback] + async fn change_avatar(&self, file: gio::File) { + let Some(session) = self.session.upgrade() else { return; - } - }; - - let (action, weak_action) = OngoingAsyncAction::set(uri.clone()); - imp.changing_avatar.replace(Some(action)); - - let uri_clone = uri.clone(); - let handle = - spawn_tokio!(async move { client.account().set_avatar_url(Some(&uri_clone)).await }); - - match handle.await.unwrap() { - Ok(()) => { - // If the user is in no rooms, we won't receive the update via sync, so change - // the avatar manually if this request succeeds before the avatar is updated. - // Because this action can finish in user_avatar_changed, we must only act if - // this is still the current action. - if weak_action.is_ongoing() { - session.user().set_avatar_url(Some(uri)); + }; + + let avatar = &self.avatar; + avatar.edit_in_progress(); + + let info = match FileInfo::try_from_file(&file).await { + Ok(info) => info, + Err(error) => { + error!("Could not load user avatar file info: {error}"); + let obj = self.obj(); + toast!(obj, gettext("Could not load file")); + avatar.reset(); + return; } - } - Err(error) => { - // Because this action can finish in user_avatar_changed, we must only act if - // this is still the current action. - if weak_action.is_ongoing() { - imp.changing_avatar.take(); - error!("Could not change user avatar: {error}"); - toast!(self, gettext("Could not change avatar")); + }; + + let data = match file.load_contents_future().await { + Ok((data, _)) => data, + Err(error) => { + error!("Could not load user avatar file: {error}"); + let obj = self.obj(); + toast!(obj, gettext("Could not load file")); avatar.reset(); + return; + } + }; + + let client = session.client(); + let client_clone = client.clone(); + let handle = spawn_tokio!(async move { + client_clone + .media() + .upload(&info.mime, data.into(), None) + .await + }); + + let uri = match handle.await.expect("task was not aborted") { + Ok(res) => res.content_uri, + Err(error) => { + error!("Could not upload user avatar: {error}"); + let obj = self.obj(); + toast!(obj, gettext("Could not upload avatar")); + avatar.reset(); + return; + } + }; + + let (action, weak_action) = OngoingAsyncAction::set(uri.clone()); + self.changing_avatar.replace(Some(action)); + + let uri_clone = uri.clone(); + let handle = + spawn_tokio!( + async move { client.account().set_avatar_url(Some(&uri_clone)).await } + ); + + match handle.await.expect("task was not aborted") { + Ok(()) => { + // If the user is in no rooms, we won't receive the update via sync, so change + // the avatar manually if this request succeeds before the avatar is updated. + // Because this action can finish in user_avatar_changed, we must only act if + // this is still the current action. + if weak_action.is_ongoing() { + session.user().set_avatar_url(Some(uri)); + } + } + Err(error) => { + // Because this action can finish in user_avatar_changed, we must only act if + // this is still the current action. + if weak_action.is_ongoing() { + self.changing_avatar.take(); + error!("Could not change user avatar: {error}"); + let obj = self.obj(); + toast!(obj, gettext("Could not change avatar")); + avatar.reset(); + } } } } - } - /// Remove the current avatar of the user. - #[template_callback] - async fn remove_avatar(&self) { - let Some(session) = self.session() else { - return; - }; - - // Ask for confirmation. - let confirm_dialog = adw::AlertDialog::builder() - .default_response("cancel") - .heading(gettext("Remove Avatar?")) - .body(gettext("Do you really want to remove your avatar?")) - .build(); - confirm_dialog.add_responses(&[ - ("cancel", &gettext("Cancel")), - ("remove", &gettext("Remove")), - ]); - confirm_dialog.set_response_appearance("remove", adw::ResponseAppearance::Destructive); - - if confirm_dialog.choose_future(self).await != "remove" { - return; - } + /// Remove the current avatar of the user. + #[template_callback] + async fn remove_avatar(&self) { + let Some(session) = self.session.upgrade() else { + return; + }; + + // Ask for confirmation. + let confirm_dialog = adw::AlertDialog::builder() + .default_response("cancel") + .heading(gettext("Remove Avatar?")) + .body(gettext("Do you really want to remove your avatar?")) + .build(); + confirm_dialog.add_responses(&[ + ("cancel", &gettext("Cancel")), + ("remove", &gettext("Remove")), + ]); + confirm_dialog.set_response_appearance("remove", adw::ResponseAppearance::Destructive); - let imp = self.imp(); - let avatar = &*imp.avatar; - avatar.removal_in_progress(); + let obj = self.obj(); + if confirm_dialog.choose_future(&*obj).await != "remove" { + return; + } - let (action, weak_action) = OngoingAsyncAction::remove(); - imp.changing_avatar.replace(Some(action)); + let avatar = &*self.avatar; + avatar.removal_in_progress(); - let client = session.client(); - let handle = spawn_tokio!(async move { client.account().set_avatar_url(None).await }); + let (action, weak_action) = OngoingAsyncAction::remove(); + self.changing_avatar.replace(Some(action)); - match handle.await.unwrap() { - Ok(()) => { - // If the user is in no rooms, we won't receive the update via sync, so change - // the avatar manually if this request succeeds before the avatar is updated. - // Because this action can finish in avatar_changed, we must only act if this is - // still the current action. - if weak_action.is_ongoing() { - session.user().set_avatar_url(None); + let client = session.client(); + let handle = spawn_tokio!(async move { client.account().set_avatar_url(None).await }); + + match handle.await.expect("task was not aborted") { + Ok(()) => { + // If the user is in no rooms, we won't receive the update via sync, so change + // the avatar manually if this request succeeds before the avatar is updated. + // Because this action can finish in avatar_changed, we must only act if this is + // still the current action. + if weak_action.is_ongoing() { + session.user().set_avatar_url(None); + } } - } - Err(error) => { - // Because this action can finish in avatar_changed, we must only act if this is - // still the current action. - if weak_action.is_ongoing() { - imp.changing_avatar.take(); - error!("Could not remove user avatar: {error}"); - toast!(self, gettext("Could not remove avatar")); - avatar.reset(); + Err(error) => { + // Because this action can finish in avatar_changed, we must only act if this is + // still the current action. + if weak_action.is_ongoing() { + self.changing_avatar.take(); + error!("Could not remove user avatar: {error}"); + toast!(obj, gettext("Could not remove avatar")); + avatar.reset(); + } } } } - } - /// Update the view when the text of the display name changed. - #[template_callback] - fn display_name_changed(&self) { - self.imp() - .display_name_button - .set_visible(self.has_display_name_changed()); - } - - /// Whether the display name in the entry row is different than the user's. - fn has_display_name_changed(&self) -> bool { - let Some(session) = self.session() else { - return false; - }; - let imp = self.imp(); - let text = imp.display_name.text(); - let display_name = session.user().display_name(); + /// Update the view when the text of the display name changed. + #[template_callback] + fn display_name_changed(&self) { + self.display_name_button + .set_visible(self.has_display_name_changed()); + } - text != display_name - } + /// Whether the display name in the entry row is different than the + /// user's. + fn has_display_name_changed(&self) -> bool { + let Some(session) = self.session.upgrade() else { + return false; + }; + let text = self.display_name.text(); + let display_name = session.user().display_name(); - /// Update the view when the user's display name changed. - fn user_display_name_changed(&self, name: &str) { - let imp = self.imp(); + text != display_name + } - if let Some(action) = imp.changing_display_name.borrow().as_ref() { - if action.as_value().map(String::as_str) == Some(name) { - // This is not the change we expected, maybe another device did a change too. - // Let's wait for another change. + /// Update the view when the user's display name changed. + fn user_display_name_changed(&self, name: &str) { + if let Some(action) = self.changing_display_name.borrow().as_ref() { + if action.as_value().map(String::as_str) == Some(name) { + // This is not the change we expected, maybe another device did a change too. + // Let's wait for another change. + return; + } + } else { + // No action is ongoing, we don't need to do anything. return; } - } else { - // No action is ongoing, we don't need to do anything. - return; - } - // Reset state. - imp.changing_display_name.take(); + // Reset state. + self.changing_display_name.take(); - let entry = &imp.display_name; - let button = &imp.display_name_button; + let entry = &self.display_name; + let button = &self.display_name_button; - entry.remove_css_class("error"); - entry.set_sensitive(true); - button.set_visible(false); - button.set_state(ActionState::Confirm); - toast!(self, gettext("Name changed successfully")); - } - - /// Change the display name of the user. - #[template_callback] - async fn change_display_name(&self) { - if !self.has_display_name_changed() { - // Nothing to do. - return; + entry.remove_css_class("error"); + entry.set_sensitive(true); + button.set_visible(false); + button.set_state(ActionState::Confirm); + let obj = self.obj(); + toast!(obj, gettext("Name changed successfully")); } - let Some(session) = self.session() else { - return; - }; - let imp = self.imp(); - let entry = &imp.display_name; - let button = &imp.display_name_button; + /// Change the display name of the user. + #[template_callback] + async fn change_display_name(&self) { + if !self.has_display_name_changed() { + // Nothing to do. + return; + } + let Some(session) = self.session.upgrade() else { + return; + }; + + let entry = &self.display_name; + let button = &self.display_name_button; - entry.set_sensitive(false); - button.set_state(ActionState::Loading); + entry.set_sensitive(false); + button.set_state(ActionState::Loading); - let display_name = entry.text().trim().to_string(); + let display_name = entry.text().trim().to_string(); - let (action, weak_action) = OngoingAsyncAction::set(display_name.clone()); - imp.changing_display_name.replace(Some(action)); + let (action, weak_action) = OngoingAsyncAction::set(display_name.clone()); + self.changing_display_name.replace(Some(action)); - let client = session.client(); - let display_name_clone = display_name.clone(); - let handle = spawn_tokio!(async move { - client - .account() - .set_display_name(Some(&display_name_clone)) - .await - }); - - match handle.await.unwrap() { - Ok(()) => { - // If the user is in no rooms, we won't receive the update via sync, so change - // the avatar manually if this request succeeds before the avatar is updated. - // Because this action can finish in user_display_name_changed, we must only act - // if this is still the current action. - if weak_action.is_ongoing() { - session.user().set_name(Some(display_name)); + let client = session.client(); + let display_name_clone = display_name.clone(); + let handle = spawn_tokio!(async move { + client + .account() + .set_display_name(Some(&display_name_clone)) + .await + }); + + match handle.await.expect("task was not aborted") { + Ok(()) => { + // If the user is in no rooms, we won't receive the update via sync, so change + // the avatar manually if this request succeeds before the avatar is updated. + // Because this action can finish in user_display_name_changed, we must only act + // if this is still the current action. + if weak_action.is_ongoing() { + session.user().set_name(Some(display_name)); + } } - } - Err(error) => { - // Because this action can finish in user_display_name_changed, we must only act - // if this is still the current action. - if weak_action.is_ongoing() { - imp.changing_display_name.take(); - error!("Could not change user display name: {error}"); - toast!(self, gettext("Could not change display name")); - button.set_state(ActionState::Retry); - entry.add_css_class("error"); - entry.set_sensitive(true); + Err(error) => { + // Because this action can finish in user_display_name_changed, we must only act + // if this is still the current action. + if weak_action.is_ongoing() { + self.changing_display_name.take(); + error!("Could not change user display name: {error}"); + let obj = self.obj(); + toast!(obj, gettext("Could not change display name")); + button.set_state(ActionState::Retry); + entry.add_css_class("error"); + entry.set_sensitive(true); + } } } } } } + +glib::wrapper! { + /// Account settings page about the user and the session. + pub struct GeneralPage(ObjectSubclass) + @extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible; +} + +impl GeneralPage { + pub fn new(session: &Session) -> Self { + glib::Object::builder().property("session", session).build() + } +} diff --git a/src/session/view/account_settings/notifications_page.rs b/src/session/view/account_settings/notifications_page.rs index d47868a7..8ba29330 100644 --- a/src/session/view/account_settings/notifications_page.rs +++ b/src/session/view/account_settings/notifications_page.rs @@ -25,33 +25,33 @@ mod imp { #[properties(wrapper_type = super::NotificationsPage)] pub struct NotificationsPage { #[template_child] - pub account_row: TemplateChild, + account_row: TemplateChild, #[template_child] - pub session_row: TemplateChild, + session_row: TemplateChild, #[template_child] - pub global: TemplateChild, + global: TemplateChild, #[template_child] - pub global_all_row: TemplateChild, + global_all_row: TemplateChild, #[template_child] - pub global_direct_row: TemplateChild, + global_direct_row: TemplateChild, #[template_child] - pub global_mentions_row: TemplateChild, + global_mentions_row: TemplateChild, #[template_child] - pub keywords: TemplateChild, + keywords: TemplateChild, #[template_child] - pub keywords_add_row: TemplateChild, + keywords_add_row: TemplateChild, /// The notifications settings of the current session. #[property(get, set = Self::set_notifications_settings, explicit_notify)] - pub notifications_settings: BoundObjectWeakRef, + notifications_settings: BoundObjectWeakRef, /// Whether the account section is busy. #[property(get)] - pub account_loading: Cell, + account_loading: Cell, /// Whether the global section is busy. #[property(get)] - pub global_loading: Cell, + global_loading: Cell, /// The global notifications setting, as a string. #[property(get = Self::global_setting, set = Self::set_global_setting)] - pub global_setting: PhantomData, + global_setting: PhantomData, } #[glib::object_subclass] @@ -62,7 +62,7 @@ mod imp { fn class_init(klass: &mut Self::Class) { Self::bind_template(klass); - Self::Type::bind_template_callbacks(klass); + Self::bind_template_callbacks(klass); klass.install_property_action("notifications.set-global-default", "global-setting"); } @@ -73,24 +73,12 @@ mod imp { } #[glib::derived_properties] - impl ObjectImpl for NotificationsPage { - fn constructed(&self) { - self.parent_constructed(); - let obj = self.obj(); - - self.keywords_add_row.connect_changed(clone!( - #[weak] - obj, - move |_| { - obj.update_keywords(); - } - )); - } - } + impl ObjectImpl for NotificationsPage {} impl WidgetImpl for NotificationsPage {} impl PreferencesPageImpl for NotificationsPage {} + #[gtk::template_callbacks] impl NotificationsPage { /// Set the notifications settings of the current session. fn set_notifications_settings( @@ -100,30 +88,29 @@ mod imp { if self.notifications_settings.obj().as_ref() == notifications_settings { return; } - let obj = self.obj(); self.notifications_settings.disconnect_signals(); if let Some(settings) = notifications_settings { let account_enabled_handler = settings.connect_account_enabled_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_account(); + imp.update_account(); } )); let session_enabled_handler = settings.connect_session_enabled_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_session(); + imp.update_session(); } )); let global_setting_handler = settings.connect_global_setting_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_global(); + imp.update_global(); } )); @@ -147,28 +134,28 @@ mod imp { self.keywords.bind_model( Some(&flattened_list), clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, #[upgrade_or_else] || { adw::ActionRow::new().upcast() }, - move |item| obj.create_keyword_row(item) + move |item| imp.create_keyword_row(item) ), ); } else { self.keywords.bind_model( None::<&gio::ListModel>, clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, #[upgrade_or_else] || { adw::ActionRow::new().upcast() }, - move |item| obj.create_keyword_row(item) + move |item| imp.create_keyword_row(item) ), ); } - obj.update_account(); - obj.notify_notifications_settings(); + self.update_account(); + self.obj().notify_notifications_settings(); } /// The global notifications setting, as a string. @@ -187,186 +174,167 @@ mod imp { return; }; - let obj = self.obj(); spawn!(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, async move { - obj.global_setting_changed(default).await; + imp.global_setting_changed(default).await; } )); } - } -} - -glib::wrapper! { - /// Preferences page to edit global notification settings. - pub struct NotificationsPage(ObjectSubclass) - @extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible; -} -#[gtk::template_callbacks] -impl NotificationsPage { - pub fn new(notifications_settings: &NotificationsSettings) -> Self { - glib::Object::builder() - .property("notifications-settings", notifications_settings) - .build() - } + /// Update the section about the account. + fn update_account(&self) { + let Some(settings) = self.notifications_settings.obj() else { + return; + }; - /// Update the section about the account. - fn update_account(&self) { - let Some(settings) = self.notifications_settings() else { - return; - }; - let imp = self.imp(); + let checked = settings.account_enabled(); + self.account_row.set_is_active(checked); + self.account_row.set_sensitive(!self.account_loading.get()); - let checked = settings.account_enabled(); - imp.account_row.set_is_active(checked); - imp.account_row.set_sensitive(!self.account_loading()); + // Other sections will be disabled or not. + self.update_session(); + } - // Other sections will be disabled or not. - self.update_session(); - } + /// Update the section about the session. + fn update_session(&self) { + let Some(settings) = self.notifications_settings.obj() else { + return; + }; - /// Update the section about the session. - fn update_session(&self) { - let Some(settings) = self.notifications_settings() else { - return; - }; - let imp = self.imp(); + self.session_row.set_active(settings.session_enabled()); + self.session_row.set_sensitive(settings.account_enabled()); - imp.session_row.set_active(settings.session_enabled()); - imp.session_row.set_sensitive(settings.account_enabled()); + // Other sections will be disabled or not. + self.update_global(); + self.update_keywords(); + } - // Other sections will be disabled or not. - self.update_global(); - self.update_keywords(); - } + /// Update the section about global. + fn update_global(&self) { + let Some(settings) = self.notifications_settings.obj() else { + return; + }; - /// Update the section about global. - fn update_global(&self) { - let Some(settings) = self.notifications_settings() else { - return; - }; - let imp = self.imp(); + // Updates the active radio button. + self.obj().notify_global_setting(); - // Updates the active radio button. - self.notify_global_setting(); + let sensitive = settings.account_enabled() + && settings.session_enabled() + && !self.global_loading.get(); + self.global.set_sensitive(sensitive); + } - let sensitive = - settings.account_enabled() && settings.session_enabled() && !self.global_loading(); - imp.global.set_sensitive(sensitive); - } + /// Update the section about keywords. + #[template_callback] + fn update_keywords(&self) { + let Some(settings) = self.notifications_settings.obj() else { + return; + }; - /// Update the section about keywords. - fn update_keywords(&self) { - let Some(settings) = self.notifications_settings() else { - return; - }; - let imp = self.imp(); + let sensitive = settings.account_enabled() && settings.session_enabled(); + self.keywords.set_sensitive(sensitive); - let sensitive = settings.account_enabled() && settings.session_enabled(); - imp.keywords.set_sensitive(sensitive); + if !sensitive { + // Nothing else to update. + return; + } - if !sensitive { - // Nothing else to update. - return; + self.keywords_add_row + .set_inhibit_add(!self.can_add_keyword()); } - imp.keywords_add_row - .set_inhibit_add(!self.can_add_keyword()); - } + fn set_account_loading(&self, loading: bool) { + self.account_loading.set(loading); + self.obj().notify_account_loading(); + } - fn set_account_loading(&self, loading: bool) { - self.imp().account_loading.set(loading); - self.notify_account_loading(); - } + #[template_callback] + async fn account_switched(&self) { + let Some(settings) = self.notifications_settings.obj() else { + return; + }; - #[template_callback] - async fn account_switched(&self) { - let Some(settings) = self.notifications_settings() else { - return; - }; - let imp = self.imp(); - - let enabled = imp.account_row.is_active(); - if enabled == settings.account_enabled() { - // Nothing to do. - return; - } + let enabled = self.account_row.is_active(); + if enabled == settings.account_enabled() { + // Nothing to do. + return; + } - imp.account_row.set_sensitive(false); - self.set_account_loading(true); + self.account_row.set_sensitive(false); + self.set_account_loading(true); + + if settings.set_account_enabled(enabled).await.is_err() { + let msg = if enabled { + gettext("Could not enable account notifications") + } else { + gettext("Could not disable account notifications") + }; + let obj = self.obj(); + toast!(obj, msg); + } - if settings.set_account_enabled(enabled).await.is_err() { - let msg = if enabled { - gettext("Could not enable account notifications") - } else { - gettext("Could not disable account notifications") - }; - toast!(self, msg); + self.set_account_loading(false); + self.update_account(); } - self.set_account_loading(false); - self.update_account(); - } - - #[template_callback] - fn session_switched(&self) { - let Some(settings) = self.notifications_settings() else { - return; - }; - let imp = self.imp(); + #[template_callback] + fn session_switched(&self) { + let Some(settings) = self.notifications_settings.obj() else { + return; + }; - settings.set_session_enabled(imp.session_row.is_active()); - } + settings.set_session_enabled(self.session_row.is_active()); + } - fn set_global_loading(&self, loading: bool, setting: NotificationsGlobalSetting) { - let imp = self.imp(); + fn set_global_loading(&self, loading: bool, setting: NotificationsGlobalSetting) { + // Only show the spinner on the selected one. + self.global_all_row + .set_is_loading(loading && setting == NotificationsGlobalSetting::All); + self.global_direct_row.set_is_loading( + loading && setting == NotificationsGlobalSetting::DirectAndMentions, + ); + self.global_mentions_row + .set_is_loading(loading && setting == NotificationsGlobalSetting::MentionsOnly); - // Only show the spinner on the selected one. - imp.global_all_row - .set_is_loading(loading && setting == NotificationsGlobalSetting::All); - imp.global_direct_row - .set_is_loading(loading && setting == NotificationsGlobalSetting::DirectAndMentions); - imp.global_mentions_row - .set_is_loading(loading && setting == NotificationsGlobalSetting::MentionsOnly); + self.global_loading.set(loading); + self.obj().notify_global_loading(); + } - self.imp().global_loading.set(loading); - self.notify_global_loading(); - } + #[template_callback] + async fn global_setting_changed(&self, setting: NotificationsGlobalSetting) { + let Some(settings) = self.notifications_settings.obj() else { + return; + }; - #[template_callback] - async fn global_setting_changed(&self, setting: NotificationsGlobalSetting) { - let Some(settings) = self.notifications_settings() else { - return; - }; - let imp = self.imp(); + if setting == settings.global_setting() { + // Nothing to do. + return; + } - if setting == settings.global_setting() { - // Nothing to do. - return; - } + self.global.set_sensitive(false); + self.set_global_loading(true, setting); - imp.global.set_sensitive(false); - self.set_global_loading(true, setting); + if settings.set_global_setting(setting).await.is_err() { + let obj = self.obj(); + toast!( + obj, + gettext("Could not change global notifications setting") + ); + } - if settings.set_global_setting(setting).await.is_err() { - toast!( - self, - gettext("Could not change global notifications setting") - ); + self.set_global_loading(false, setting); + self.update_global(); } - self.set_global_loading(false, setting); - self.update_global(); - } - - /// Create a row in the keywords list for the given item. - fn create_keyword_row(&self, item: &glib::Object) -> gtk::Widget { - let imp = self.imp(); + /// Create a row in the keywords list for the given item. + fn create_keyword_row(&self, item: &glib::Object) -> gtk::Widget { + let Some(string_obj) = item.downcast_ref::() else { + // It can only be the dummy item to add a new keyword. + return self.keywords_add_row.clone().upcast(); + }; - if let Some(string_obj) = item.downcast_ref::() { let keyword = string_obj.string(); let row = RemovableRow::new(); row.set_title(&keyword); @@ -376,113 +344,123 @@ impl NotificationsPage { ))); row.connect_remove(clone!( - #[weak(rename_to = obj)] + #[weak(rename_to = imp)] self, move |row| { - obj.remove_keyword(row); + imp.remove_keyword(row); } )); row.upcast() - } else { - // It can only be the dummy item to add a new keyword. - imp.keywords_add_row.clone().upcast() } - } - /// Remove the keyword from the given row. - fn remove_keyword(&self, row: &RemovableRow) { - let Some(settings) = self.notifications_settings() else { - return; - }; - - row.set_is_loading(true); - - spawn!(clone!( - #[weak(rename_to = obj)] - self, - #[weak] - row, - async move { - if settings.remove_keyword(row.title().into()).await.is_err() { - toast!(obj, gettext("Could not remove notification keyword")); - } + /// Remove the keyword from the given row. + fn remove_keyword(&self, row: &RemovableRow) { + let Some(settings) = self.notifications_settings.obj() else { + return; + }; - row.set_is_loading(false); - } - )); - } + row.set_is_loading(true); - /// Whether we can add the keyword that is currently in the entry. - fn can_add_keyword(&self) -> bool { - let imp = self.imp(); + let obj = self.obj(); + spawn!(clone!( + #[weak] + obj, + #[weak] + row, + async move { + if settings.remove_keyword(row.title().into()).await.is_err() { + toast!(obj, gettext("Could not remove notification keyword")); + } - // Cannot add a keyword if section is disabled. - if !imp.keywords.is_sensitive() { - return false; + row.set_is_loading(false); + } + )); } - // Cannot add a keyword if a keyword is already being added. - if imp.keywords_add_row.is_loading() { - return false; - } + /// Whether we can add the keyword that is currently in the entry. + fn can_add_keyword(&self) -> bool { + // Cannot add a keyword if section is disabled. + if !self.keywords.is_sensitive() { + return false; + } - let text = imp.keywords_add_row.text().to_lowercase(); + // Cannot add a keyword if a keyword is already being added. + if self.keywords_add_row.is_loading() { + return false; + } - // Cannot add an empty keyword. - if text.is_empty() { - return false; - } + let text = self.keywords_add_row.text().to_lowercase(); - // Cannot add a keyword without the API. - let Some(settings) = self.notifications_settings() else { - return false; - }; + // Cannot add an empty keyword. + if text.is_empty() { + return false; + } - // Cannot add a keyword that already exists. - let keywords_list = settings.keywords_list(); - for keyword_obj in keywords_list.iter::() { - let Ok(keyword_obj) = keyword_obj else { - break; + // Cannot add a keyword without the API. + let Some(settings) = self.notifications_settings.obj() else { + return false; }; - if let Some(keyword) = keyword_obj - .downcast_ref::() - .map(gtk::StringObject::string) - { - if keyword.to_lowercase() == text { - return false; + // Cannot add a keyword that already exists. + let keywords_list = settings.keywords_list(); + for keyword_obj in keywords_list.iter::() { + let Ok(keyword_obj) = keyword_obj else { + break; + }; + + if let Some(keyword) = keyword_obj + .downcast_ref::() + .map(gtk::StringObject::string) + { + if keyword.to_lowercase() == text { + return false; + } } } + + true } - true - } + /// Add the keyword that is currently in the entry. + #[template_callback] + async fn add_keyword(&self) { + if !self.can_add_keyword() { + return; + } - /// Add the keyword that is currently in the entry. - #[template_callback] - async fn add_keyword(&self) { - if !self.can_add_keyword() { - return; - } + let Some(settings) = self.notifications_settings.obj() else { + return; + }; - let Some(settings) = self.notifications_settings() else { - return; - }; + self.keywords_add_row.set_is_loading(true); - let imp = self.imp(); - imp.keywords_add_row.set_is_loading(true); + let keyword = self.keywords_add_row.text().into(); - let keyword = imp.keywords_add_row.text().into(); + if settings.add_keyword(keyword).await.is_err() { + let obj = self.obj(); + toast!(obj, gettext("Could not add notification keyword")); + } else { + // Adding the keyword was successful, reset the entry. + self.keywords_add_row.set_text(""); + } - if settings.add_keyword(keyword).await.is_err() { - toast!(self, gettext("Could not add notification keyword")); - } else { - // Adding the keyword was successful, reset the entry. - imp.keywords_add_row.set_text(""); + self.keywords_add_row.set_is_loading(false); + self.update_keywords(); } + } +} + +glib::wrapper! { + /// Preferences page to edit global notification settings. + pub struct NotificationsPage(ObjectSubclass) + @extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible; +} - imp.keywords_add_row.set_is_loading(false); - self.update_keywords(); +impl NotificationsPage { + pub fn new(notifications_settings: &NotificationsSettings) -> Self { + glib::Object::builder() + .property("notifications-settings", notifications_settings) + .build() } } diff --git a/src/session/view/account_settings/notifications_page.ui b/src/session/view/account_settings/notifications_page.ui index 42d3345e..17944368 100644 --- a/src/session/view/account_settings/notifications_page.ui +++ b/src/session/view/account_settings/notifications_page.ui @@ -66,6 +66,7 @@ Add Keyword Add Keyword + diff --git a/src/session/view/account_settings/security_page/ignored_users_subpage/ignored_user_row.rs b/src/session/view/account_settings/security_page/ignored_users_subpage/ignored_user_row.rs index c0574e49..42a7fbbb 100644 --- a/src/session/view/account_settings/security_page/ignored_users_subpage/ignored_user_row.rs +++ b/src/session/view/account_settings/security_page/ignored_users_subpage/ignored_user_row.rs @@ -18,13 +18,13 @@ mod imp { #[properties(wrapper_type = super::IgnoredUserRow)] pub struct IgnoredUserRow { #[template_child] - pub stop_ignoring_button: TemplateChild, + stop_ignoring_button: TemplateChild, /// The item containing the user ID presented by this row. #[property(get, set = Self::set_item, explicit_notify, nullable)] - pub item: RefCell>, + item: RefCell>, /// The current list of ignored users. #[property(get, set, nullable)] - pub ignored_users: RefCell>, + ignored_users: RefCell>, } #[glib::object_subclass] @@ -35,7 +35,7 @@ mod imp { fn class_init(klass: &mut Self::Class) { Self::bind_template(klass); - Self::Type::bind_template_callbacks(klass); + Self::bind_template_callbacks(klass); } fn instance_init(obj: &InitializingObject) { @@ -49,6 +49,7 @@ mod imp { impl WidgetImpl for IgnoredUserRow {} impl BoxImpl for IgnoredUserRow {} + #[gtk::template_callbacks] impl IgnoredUserRow { /// Set the item containing the user ID presented by this row. fn set_item(&self, item: Option) { @@ -62,6 +63,30 @@ mod imp { // Reset the state of the button. self.stop_ignoring_button.set_is_loading(false); } + + /// Stop ignoring the user of this row. + #[template_callback] + async fn stop_ignoring_user(&self) { + let Some(user_id) = self + .item + .borrow() + .as_ref() + .and_then(|string_object| UserId::parse(string_object.string()).ok()) + else { + return; + }; + let Some(ignored_users) = self.ignored_users.borrow().clone() else { + return; + }; + + self.stop_ignoring_button.set_is_loading(true); + + if ignored_users.remove(&user_id).await.is_err() { + let obj = self.obj(); + toast!(obj, gettext("Could not stop ignoring user")); + self.stop_ignoring_button.set_is_loading(false); + } + } } } @@ -71,34 +96,10 @@ glib::wrapper! { @extends gtk::Widget, gtk::Box, @implements gtk::Accessible; } -#[gtk::template_callbacks] impl IgnoredUserRow { pub fn new(ignored_users: &IgnoredUsers) -> Self { glib::Object::builder() .property("ignored-users", ignored_users) .build() } - - /// Stop ignoring the user of this row. - #[template_callback] - async fn stop_ignoring_user(&self) { - let Some(user_id) = self - .item() - .map(|i| i.string()) - .and_then(|s| UserId::parse(&s).ok()) - else { - return; - }; - let Some(ignored_users) = self.ignored_users() else { - return; - }; - - let imp = self.imp(); - imp.stop_ignoring_button.set_is_loading(true); - - if ignored_users.remove(&user_id).await.is_err() { - toast!(self, gettext("Could not stop ignoring user")); - imp.stop_ignoring_button.set_is_loading(false); - } - } } diff --git a/src/session/view/account_settings/security_page/ignored_users_subpage/mod.rs b/src/session/view/account_settings/security_page/ignored_users_subpage/mod.rs index 2132f441..d65c8841 100644 --- a/src/session/view/account_settings/security_page/ignored_users_subpage/mod.rs +++ b/src/session/view/account_settings/security_page/ignored_users_subpage/mod.rs @@ -21,18 +21,18 @@ mod imp { #[properties(wrapper_type = super::IgnoredUsersSubpage)] pub struct IgnoredUsersSubpage { #[template_child] - pub stack: TemplateChild, + stack: TemplateChild, #[template_child] - pub search_bar: TemplateChild, + search_bar: TemplateChild, #[template_child] - pub search_entry: TemplateChild, + search_entry: TemplateChild, #[template_child] - pub list_view: TemplateChild, - pub filtered_model: gtk::FilterListModel, + list_view: TemplateChild, + filtered_model: gtk::FilterListModel, /// The current session. #[property(get, set = Self::set_session, explicit_notify, nullable)] - pub session: glib::WeakRef, - pub items_changed_handler: RefCell>, + session: glib::WeakRef, + items_changed_handler: RefCell>, } #[glib::object_subclass] diff --git a/src/session/view/account_settings/security_page/import_export_keys_subpage.rs b/src/session/view/account_settings/security_page/import_export_keys_subpage.rs index b59c29ec..c294f14c 100644 --- a/src/session/view/account_settings/security_page/import_export_keys_subpage.rs +++ b/src/session/view/account_settings/security_page/import_export_keys_subpage.rs @@ -33,38 +33,38 @@ mod imp { )] #[properties(wrapper_type = super::ImportExportKeysSubpage)] pub struct ImportExportKeysSubpage { - /// The current session. - #[property(get, set, nullable)] - pub session: glib::WeakRef, #[template_child] - pub description: TemplateChild, + description: TemplateChild, #[template_child] - pub instructions: TemplateChild, + instructions: TemplateChild, #[template_child] - pub passphrase: TemplateChild, + passphrase: TemplateChild, #[template_child] - pub confirm_passphrase_box: TemplateChild, + confirm_passphrase_box: TemplateChild, #[template_child] - pub confirm_passphrase: TemplateChild, + confirm_passphrase: TemplateChild, #[template_child] - pub confirm_passphrase_error_revealer: TemplateChild, + confirm_passphrase_error_revealer: TemplateChild, #[template_child] - pub confirm_passphrase_error: TemplateChild, + confirm_passphrase_error: TemplateChild, #[template_child] - pub file_row: TemplateChild, + file_row: TemplateChild, #[template_child] - pub file_button: TemplateChild, + file_button: TemplateChild, #[template_child] - pub proceed_button: TemplateChild, + proceed_button: TemplateChild, + /// The current session. + #[property(get, set, nullable)] + session: glib::WeakRef, /// The path of the file for the encryption keys. #[property(get)] - pub file_path: RefCell>, + file_path: RefCell>, /// The path of the file for the encryption keys, as a string. #[property(get = Self::file_path_string)] - pub file_path_string: PhantomData>, + file_path_string: PhantomData>, /// The export/import mode of the subpage. #[property(get, set = Self::set_mode, explicit_notify, builder(ImportExportKeysSubpageMode::default()))] - pub mode: Cell, + mode: Cell, } #[glib::object_subclass] @@ -75,7 +75,7 @@ mod imp { fn class_init(klass: &mut Self::Class) { klass.bind_template(); - Self::Type::bind_template_callbacks(klass); + Self::bind_template_callbacks(klass); } fn instance_init(obj: &InitializingObject) { @@ -87,25 +87,25 @@ mod imp { impl ObjectImpl for ImportExportKeysSubpage { fn constructed(&self) { self.parent_constructed(); - self.obj().update_for_mode(); + self.update_for_mode(); } } impl WidgetImpl for ImportExportKeysSubpage {} impl NavigationPageImpl for ImportExportKeysSubpage {} + #[gtk::template_callbacks] impl ImportExportKeysSubpage { /// Set the export/import mode of the subpage. fn set_mode(&self, mode: ImportExportKeysSubpageMode) { if self.mode.get() == mode { return; } - let obj = self.obj(); self.mode.set(mode); - obj.update_for_mode(); - obj.clear(); - obj.notify_mode(); + self.update_for_mode(); + self.clear(); + self.obj().notify_mode(); } /// The path to export the keys to, as a string. @@ -116,263 +116,273 @@ mod imp { .and_then(gio::File::path) .map(|path| path.to_string_lossy().to_string()) } - } -} -glib::wrapper! { - /// Subpage to export room encryption keys for backup. - pub struct ImportExportKeysSubpage(ObjectSubclass) - @extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible; -} + /// Whether the subpage is in export mode. + fn is_export(&self) -> bool { + self.mode.get() == ImportExportKeysSubpageMode::Export + } -#[gtk::template_callbacks] -impl ImportExportKeysSubpage { - pub fn new(session: &Session, mode: ImportExportKeysSubpageMode) -> Self { - glib::Object::builder() - .property("session", session) - .property("mode", mode) - .build() - } + /// Set the path of the file for the encryption keys. + fn set_file_path(&self, path: Option) { + if *self.file_path.borrow() == path { + return; + } - /// Whether the subpage is in export mode. - fn is_export(&self) -> bool { - self.mode() == ImportExportKeysSubpageMode::Export - } + self.file_path.replace(path); + self.update_button(); - /// Set the path of the file for the encryption keys. - fn set_file_path(&self, path: Option) { - let imp = self.imp(); - if *imp.file_path.borrow() == path { - return; + let obj = self.obj(); + obj.notify_file_path(); + obj.notify_file_path_string(); } - imp.file_path.replace(path); - self.update_button(); - self.notify_file_path(); - self.notify_file_path_string(); - } - - /// Reset the subpage's fields. - fn clear(&self) { - let imp = self.imp(); - - self.set_file_path(None); - imp.passphrase.set_text(""); - imp.confirm_passphrase.set_text(""); - } + /// Reset the subpage's fields. + fn clear(&self) { + self.set_file_path(None); + self.passphrase.set_text(""); + self.confirm_passphrase.set_text(""); + } - /// Update the UI for the current mode. - fn update_for_mode(&self) { - let imp = self.imp(); + /// Update the UI for the current mode. + fn update_for_mode(&self) { + let obj = self.obj(); - if self.is_export() { - // Translators: 'Room encryption keys' are encryption keys for all rooms. - self.set_title(&gettext("Export Room Encryption Keys")); - imp.description.set_label(&gettext( + if self.is_export() { // Translators: 'Room encryption keys' are encryption keys for all rooms. - "Exporting your room encryption keys allows you to make a backup to be able to decrypt your messages in end-to-end encrypted rooms on another device or with another Matrix client.", - )); - imp.instructions.set_label(&gettext( - "The backup must be stored in a safe place and must be protected with a strong passphrase that will be used to encrypt the data.", - )); - imp.confirm_passphrase_box.set_visible(true); - imp.proceed_button.set_title(&gettext("Export Keys")); - } else { - // Translators: 'Room encryption keys' are encryption keys for all rooms. - self.set_title(&gettext("Import Room Encryption Keys")); - imp.description.set_label(&gettext( + obj.set_title(&gettext("Export Room Encryption Keys")); + self.description.set_label(&gettext( + // Translators: 'Room encryption keys' are encryption keys for all rooms. + "Exporting your room encryption keys allows you to make a backup to be able to decrypt your messages in end-to-end encrypted rooms on another device or with another Matrix client.", + )); + self.instructions.set_label(&gettext( + "The backup must be stored in a safe place and must be protected with a strong passphrase that will be used to encrypt the data.", + )); + self.confirm_passphrase_box.set_visible(true); + self.proceed_button.set_title(&gettext("Export Keys")); + } else { // Translators: 'Room encryption keys' are encryption keys for all rooms. - "Importing your room encryption keys allows you to decrypt your messages in end-to-end encrypted rooms with a previous backup from a Matrix client.", - )); - imp.instructions.set_label(&gettext( - "Enter the passphrase provided when the backup file was created.", - )); - imp.confirm_passphrase_box.set_visible(false); - imp.proceed_button.set_title(&gettext("Import Keys")); - } - - self.update_button(); - } + obj.set_title(&gettext("Import Room Encryption Keys")); + self.description.set_label(&gettext( + // Translators: 'Room encryption keys' are encryption keys for all rooms. + "Importing your room encryption keys allows you to decrypt your messages in end-to-end encrypted rooms with a previous backup from a Matrix client.", + )); + self.instructions.set_label(&gettext( + "Enter the passphrase provided when the backup file was created.", + )); + self.confirm_passphrase_box.set_visible(false); + self.proceed_button.set_title(&gettext("Import Keys")); + } - /// Open a dialog to choose the file. - #[template_callback] - async fn choose_file(&self) { - let is_export = self.mode() == ImportExportKeysSubpageMode::Export; - - let dialog = gtk::FileDialog::builder() - .modal(true) - .accept_label(gettext("Choose")) - .build(); - - if let Some(file) = self.file_path() { - dialog.set_initial_file(Some(&file)); - } else if is_export { - // Translators: Do no translate "fractal" as it is the application - // name. - dialog.set_initial_name(Some(&format!("{}.txt", gettext("fractal-encryption-keys")))); + self.update_button(); } - let parent_window = self.root().and_downcast::(); - let res = if is_export { - dialog.set_title(&gettext("Save Encryption Keys To…")); - dialog.save_future(parent_window.as_ref()).await - } else { - dialog.set_title(&gettext("Import Encryption Keys From…")); - dialog.open_future(parent_window.as_ref()).await - }; - - match res { - Ok(file) => { - self.set_file_path(Some(file)); + /// Open a dialog to choose the file. + #[template_callback] + async fn choose_file(&self) { + let is_export = self.is_export(); + + let dialog = gtk::FileDialog::builder() + .modal(true) + .accept_label(gettext("Choose")) + .build(); + + if let Some(file) = self.file_path.borrow().as_ref() { + dialog.set_initial_file(Some(file)); + } else if is_export { + // Translators: Do no translate "fractal" as it is the application + // name. + dialog + .set_initial_name(Some(&format!("{}.txt", gettext("fractal-encryption-keys")))); } - Err(error) => { - if error.matches(gtk::DialogError::Dismissed) { - debug!("File dialog dismissed by user"); - } else { - error!("Could not access file: {error:?}"); - toast!(self, gettext("Could not access file")); + + let obj = self.obj(); + let parent_window = obj.root().and_downcast::(); + let res = if is_export { + dialog.set_title(&gettext("Save Encryption Keys To…")); + dialog.save_future(parent_window.as_ref()).await + } else { + dialog.set_title(&gettext("Import Encryption Keys From…")); + dialog.open_future(parent_window.as_ref()).await + }; + + match res { + Ok(file) => { + self.set_file_path(Some(file)); + } + Err(error) => { + if error.matches(gtk::DialogError::Dismissed) { + debug!("File dialog dismissed by user"); + } else { + error!("Could not access file: {error:?}"); + toast!(obj, gettext("Could not access file")); + } } } } - } - /// Validate the passphrase confirmation. - #[template_callback] - fn validate_passphrase_confirmation(&self) { - let imp = self.imp(); - let entry = &imp.confirm_passphrase; - let revealer = &imp.confirm_passphrase_error_revealer; - let label = &imp.confirm_passphrase_error; - let passphrase = imp.passphrase.text(); - let confirmation = entry.text(); - - if !self.is_export() || confirmation.is_empty() { - revealer.set_reveal_child(false); - entry.remove_css_class("success"); - entry.remove_css_class("warning"); + /// Validate the passphrase confirmation. + #[template_callback] + fn validate_passphrase_confirmation(&self) { + let entry = &self.confirm_passphrase; + let revealer = &self.confirm_passphrase_error_revealer; + let label = &self.confirm_passphrase_error; + let passphrase = self.passphrase.text(); + let confirmation = entry.text(); + + if !self.is_export() || confirmation.is_empty() { + revealer.set_reveal_child(false); + entry.remove_css_class("success"); + entry.remove_css_class("warning"); + + self.update_button(); + return; + } + + if passphrase == confirmation { + revealer.set_reveal_child(false); + entry.add_css_class("success"); + entry.remove_css_class("warning"); + } else { + label.set_label(&gettext("Passphrases do not match")); + revealer.set_reveal_child(true); + entry.remove_css_class("success"); + entry.add_css_class("warning"); + } self.update_button(); - return; } - if passphrase == confirmation { - revealer.set_reveal_child(false); - entry.add_css_class("success"); - entry.remove_css_class("warning"); - } else { - label.set_label(&gettext("Passphrases do not match")); - revealer.set_reveal_child(true); - entry.remove_css_class("success"); - entry.add_css_class("warning"); + /// Update the state of the button. + fn update_button(&self) { + self.proceed_button.set_sensitive(self.can_proceed()); } - self.update_button(); - } + /// Whether we can proceed to the import/export. + fn can_proceed(&self) -> bool { + let has_file_path = self + .file_path + .borrow() + .as_ref() + .is_some_and(|file| file.path().is_some()); + let passphrase = self.passphrase.text(); - /// Update the state of the button. - fn update_button(&self) { - self.imp().proceed_button.set_sensitive(self.can_proceed()); - } + let mut can_proceed = has_file_path && !passphrase.is_empty(); - /// Whether we can proceed to the import/export. - fn can_proceed(&self) -> bool { - let imp = self.imp(); - let file_path = imp.file_path.borrow(); - let passphrase = imp.passphrase.text(); - - let mut res = file_path - .as_ref() - .filter(|file| file.path().is_some()) - .is_some() - && !passphrase.is_empty(); - - if self.is_export() { - let confirmation = imp.confirm_passphrase.text(); - res = res && passphrase == confirmation; - } - - res - } + if self.is_export() { + let confirmation = self.confirm_passphrase.text(); + can_proceed &= passphrase == confirmation; + } - /// Proceed to the import/export. - #[template_callback] - async fn proceed(&self) { - if !self.can_proceed() { - return; + can_proceed } - let imp = self.imp(); - let file_path = self.file_path().and_then(|file| file.path()).unwrap(); - let passphrase = imp.passphrase.text(); - let is_export = self.is_export(); - - imp.proceed_button.set_is_loading(true); - imp.file_button.set_sensitive(false); - imp.passphrase.set_sensitive(false); - imp.confirm_passphrase.set_sensitive(false); - - let encryption = self.session().unwrap().client().encryption(); - - let handle = spawn_tokio!(async move { - if is_export { - encryption - .export_room_keys(file_path, passphrase.as_str(), |_| true) - .await - .map(|()| 0usize) - .map_err::, _>(|error| Box::new(error)) - } else { - encryption - .import_room_keys(file_path, passphrase.as_str()) - .await - .map(|res| res.imported_count) - .map_err::, _>(|error| Box::new(error)) + /// Proceed to the import/export. + #[template_callback] + async fn proceed(&self) { + if !self.can_proceed() { + return; } - }); - match handle.await.unwrap() { - Ok(nb) => { + let Some(file_path) = self.file_path.borrow().as_ref().and_then(gio::File::path) else { + return; + }; + let Some(session) = self.session.upgrade() else { + return; + }; + + let obj = self.obj(); + let passphrase = self.passphrase.text(); + let is_export = self.is_export(); + + self.proceed_button.set_is_loading(true); + self.file_button.set_sensitive(false); + self.passphrase.set_sensitive(false); + self.confirm_passphrase.set_sensitive(false); + + let encryption = session.client().encryption(); + + let handle = spawn_tokio!(async move { if is_export { - toast!(self, gettext("Room encryption keys exported successfully")); + encryption + .export_room_keys(file_path, passphrase.as_str(), |_| true) + .await + .map(|()| 0usize) + .map_err::, _>(|error| Box::new(error)) } else { - let n = nb.try_into().unwrap_or(u32::MAX); - toast!( - self, - ngettext_f( - "Imported 1 room encryption key", - "Imported {n} room encryption keys", - n, - &[("n", &n.to_string())] - ) - ); + encryption + .import_room_keys(file_path, passphrase.as_str()) + .await + .map(|res| res.imported_count) + .map_err::, _>(|error| Box::new(error)) } - self.clear(); - self.activate_action("account-settings.close-subpage", None) - .unwrap(); - } - Err(err) => { - if is_export { - error!("Could not export the keys: {err:?}"); - toast!(self, gettext("Could not export the keys")); - } else if err - .downcast_ref::() - .filter(|err| { - matches!(err, RoomKeyImportError::Export(KeyExportError::InvalidMac)) - }) - .is_some() - { - toast!( - self, - gettext("The passphrase doesn't match the one used to export the keys.") - ); - } else { - error!("Could not import the keys: {err:?}"); - toast!(self, gettext("Could not import the keys")); + }); + + match handle.await.expect("task was not aborted") { + Ok(nb) => { + if is_export { + toast!(obj, gettext("Room encryption keys exported successfully")); + } else { + let n = nb.try_into().unwrap_or(u32::MAX); + toast!( + obj, + ngettext_f( + "Imported 1 room encryption key", + "Imported {n} room encryption keys", + n, + &[("n", &n.to_string())] + ) + ); + } + + self.clear(); + let _ = obj.activate_action("account-settings.close-subpage", None); + } + Err(error) => { + if is_export { + error!("Could not export the keys: {error}"); + toast!(obj, gettext("Could not export the keys")); + } else if error + .downcast_ref::() + .filter(|error| { + matches!( + error, + RoomKeyImportError::Export(KeyExportError::InvalidMac) + ) + }) + .is_some() + { + toast!( + obj, + gettext( + "The passphrase doesn't match the one used to export the keys." + ) + ); + } else { + error!("Could not import the keys: {error}"); + toast!(obj, gettext("Could not import the keys")); + } } } + + self.proceed_button.set_is_loading(false); + self.file_button.set_sensitive(true); + self.passphrase.set_sensitive(true); + self.confirm_passphrase.set_sensitive(true); } - imp.proceed_button.set_is_loading(false); - imp.file_button.set_sensitive(true); - imp.passphrase.set_sensitive(true); - imp.confirm_passphrase.set_sensitive(true); + } +} + +glib::wrapper! { + /// Subpage to import or export room encryption keys for backup. + pub struct ImportExportKeysSubpage(ObjectSubclass) + @extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible; +} + +impl ImportExportKeysSubpage { + pub fn new(session: &Session, mode: ImportExportKeysSubpageMode) -> Self { + glib::Object::builder() + .property("session", session) + .property("mode", mode) + .build() } } diff --git a/src/session/view/account_settings/security_page/mod.rs b/src/session/view/account_settings/security_page/mod.rs index fe2e015e..b031ba87 100644 --- a/src/session/view/account_settings/security_page/mod.rs +++ b/src/session/view/account_settings/security_page/mod.rs @@ -28,30 +28,30 @@ mod imp { #[properties(wrapper_type = super::SecurityPage)] pub struct SecurityPage { #[template_child] - pub public_read_receipts_row: TemplateChild, + public_read_receipts_row: TemplateChild, #[template_child] - pub typing_row: TemplateChild, + typing_row: TemplateChild, #[template_child] - pub ignored_users_row: TemplateChild, + ignored_users_row: TemplateChild, #[template_child] - pub crypto_identity_row: TemplateChild, + crypto_identity_row: TemplateChild, #[template_child] - pub crypto_identity_icon: TemplateChild, + crypto_identity_icon: TemplateChild, #[template_child] - pub crypto_identity_description: TemplateChild, + crypto_identity_description: TemplateChild, #[template_child] - pub crypto_identity_btn: TemplateChild, + crypto_identity_btn: TemplateChild, #[template_child] - pub recovery_row: TemplateChild, + recovery_row: TemplateChild, #[template_child] - pub recovery_icon: TemplateChild, + recovery_icon: TemplateChild, #[template_child] - pub recovery_description: TemplateChild, + recovery_description: TemplateChild, #[template_child] - pub recovery_btn: TemplateChild, + recovery_btn: TemplateChild, /// The current session. #[property(get, set = Self::set_session, nullable)] - pub session: glib::WeakRef, + session: glib::WeakRef, ignored_users_count_handler: RefCell>, security_handlers: RefCell>, bindings: RefCell>, @@ -103,7 +103,6 @@ mod imp { if prev_session.as_ref() == session { return; } - let obj = self.obj(); if let Some(session) = prev_session { if let Some(handler) = self.ignored_users_count_handler.take() { @@ -158,25 +157,25 @@ mod imp { let security = session.security(); let crypto_identity_state_handler = security.connect_crypto_identity_state_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_crypto_identity(); + imp.update_crypto_identity(); } )); let verification_state_handler = security.connect_verification_state_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_crypto_identity(); + imp.update_crypto_identity(); } )); let recovery_state_handler = security.connect_recovery_state_notify(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, move |_| { - obj.update_recovery(); + imp.update_recovery(); } )); @@ -189,169 +188,168 @@ mod imp { self.session.set(session); - obj.update_crypto_identity(); - obj.update_recovery(); + self.update_crypto_identity(); + self.update_recovery(); - obj.notify_session(); + self.obj().notify_session(); } - } -} -glib::wrapper! { - /// Security settings page. - pub struct SecurityPage(ObjectSubclass) - @extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible; -} - -impl SecurityPage { - pub fn new(session: &Session) -> Self { - glib::Object::builder().property("session", session).build() - } - - /// Update the crypto identity section. - fn update_crypto_identity(&self) { - let Some(session) = self.session() else { - return; - }; - let imp = self.imp(); - let security = session.security(); - - let crypto_identity_state = security.crypto_identity_state(); - if matches!( - crypto_identity_state, - CryptoIdentityState::Unknown | CryptoIdentityState::Missing - ) { - imp.crypto_identity_icon - .set_icon_name(Some("verified-danger-symbolic")); - imp.crypto_identity_icon.remove_css_class("success"); - imp.crypto_identity_icon.remove_css_class("warning"); - imp.crypto_identity_icon.add_css_class("error"); - - imp.crypto_identity_row - .set_title(&gettext("No Crypto Identity")); - imp.crypto_identity_description.set_label(&gettext( - "Verifying your own devices or other users is not possible", - )); - - imp.crypto_identity_btn.set_label(&gettext("Enable…")); - imp.crypto_identity_btn - .update_property(&[gtk::accessible::Property::Label(&gettext( - "Enable Crypto Identity", - ))]); - imp.crypto_identity_btn.add_css_class("suggested-action"); - - return; - } - - let verification_state = security.verification_state(); - if verification_state == SessionVerificationState::Verified { - imp.crypto_identity_icon - .set_icon_name(Some("verified-symbolic")); - imp.crypto_identity_icon.add_css_class("success"); - imp.crypto_identity_icon.remove_css_class("warning"); - imp.crypto_identity_icon.remove_css_class("error"); - - imp.crypto_identity_row - .set_title(&gettext("Crypto Identity Enabled")); - imp.crypto_identity_description.set_label(&gettext( - "The crypto identity exists and this device is verified", - )); - - imp.crypto_identity_btn.set_label(&gettext("Reset…")); - imp.crypto_identity_btn - .update_property(&[gtk::accessible::Property::Label(&gettext( - "Reset Crypto Identity", - ))]); - imp.crypto_identity_btn.remove_css_class("suggested-action"); - } else { - imp.crypto_identity_icon - .set_icon_name(Some("verified-warning-symbolic")); - imp.crypto_identity_icon.remove_css_class("success"); - imp.crypto_identity_icon.add_css_class("warning"); - imp.crypto_identity_icon.remove_css_class("error"); - - imp.crypto_identity_row - .set_title(&gettext("Crypto Identity Incomplete")); - imp.crypto_identity_description.set_label(&gettext( - "The crypto identity exists but this device is not verified", - )); - - imp.crypto_identity_btn.set_label(&gettext("Verify…")); - imp.crypto_identity_btn - .update_property(&[gtk::accessible::Property::Label(&gettext( - "Verify This Session", - ))]); - imp.crypto_identity_btn.add_css_class("suggested-action"); - } - } - - /// Update the recovery section. - fn update_recovery(&self) { - let Some(session) = self.session() else { - return; - }; - let imp = self.imp(); - - let recovery_state = session.security().recovery_state(); - match recovery_state { - RecoveryState::Unknown | RecoveryState::Disabled => { - imp.recovery_icon.set_icon_name(Some("sync-off-symbolic")); - imp.recovery_icon.remove_css_class("success"); - imp.recovery_icon.remove_css_class("warning"); - imp.recovery_icon.add_css_class("error"); - - imp.recovery_row - .set_title(&gettext("Account Recovery Disabled")); - imp.recovery_description.set_label(&gettext( - "Enable recovery to be able to restore your account without another device", + /// Update the crypto identity section. + fn update_crypto_identity(&self) { + let Some(session) = self.session.upgrade() else { + return; + }; + let security = session.security(); + + let crypto_identity_state = security.crypto_identity_state(); + if matches!( + crypto_identity_state, + CryptoIdentityState::Unknown | CryptoIdentityState::Missing + ) { + self.crypto_identity_icon + .set_icon_name(Some("verified-danger-symbolic")); + self.crypto_identity_icon.remove_css_class("success"); + self.crypto_identity_icon.remove_css_class("warning"); + self.crypto_identity_icon.add_css_class("error"); + + self.crypto_identity_row + .set_title(&gettext("No Crypto Identity")); + self.crypto_identity_description.set_label(&gettext( + "Verifying your own devices or other users is not possible", )); - imp.recovery_btn.set_label(&gettext("Enable…")); - imp.recovery_btn + self.crypto_identity_btn.set_label(&gettext("Enable…")); + self.crypto_identity_btn .update_property(&[gtk::accessible::Property::Label(&gettext( - "Enable Account Recovery", + "Enable Crypto Identity", ))]); - imp.recovery_btn.add_css_class("suggested-action"); + self.crypto_identity_btn.add_css_class("suggested-action"); + + return; } - RecoveryState::Enabled => { - imp.recovery_icon.set_icon_name(Some("sync-on-symbolic")); - imp.recovery_icon.add_css_class("success"); - imp.recovery_icon.remove_css_class("warning"); - imp.recovery_icon.remove_css_class("error"); - - imp.recovery_row - .set_title(&gettext("Account Recovery Enabled")); - imp.recovery_description.set_label(&gettext( - "Your signing keys and encryption keys are synchronized", + + let verification_state = security.verification_state(); + if verification_state == SessionVerificationState::Verified { + self.crypto_identity_icon + .set_icon_name(Some("verified-symbolic")); + self.crypto_identity_icon.add_css_class("success"); + self.crypto_identity_icon.remove_css_class("warning"); + self.crypto_identity_icon.remove_css_class("error"); + + self.crypto_identity_row + .set_title(&gettext("Crypto Identity Enabled")); + self.crypto_identity_description.set_label(&gettext( + "The crypto identity exists and this device is verified", )); - imp.recovery_btn.set_label(&gettext("Reset…")); - imp.recovery_btn + self.crypto_identity_btn.set_label(&gettext("Reset…")); + self.crypto_identity_btn .update_property(&[gtk::accessible::Property::Label(&gettext( - "Reset Account Recovery Key", + "Reset Crypto Identity", ))]); - imp.recovery_btn.remove_css_class("suggested-action"); - } - RecoveryState::Incomplete => { - imp.recovery_icon - .set_icon_name(Some("sync-partial-symbolic")); - imp.recovery_icon.remove_css_class("success"); - imp.recovery_icon.add_css_class("warning"); - imp.recovery_icon.remove_css_class("error"); - - imp.recovery_row - .set_title(&gettext("Account Recovery Incomplete")); - imp.recovery_description.set_label(&gettext( - "Recover to synchronize your signing keys and encryption keys", + self.crypto_identity_btn + .remove_css_class("suggested-action"); + } else { + self.crypto_identity_icon + .set_icon_name(Some("verified-warning-symbolic")); + self.crypto_identity_icon.remove_css_class("success"); + self.crypto_identity_icon.add_css_class("warning"); + self.crypto_identity_icon.remove_css_class("error"); + + self.crypto_identity_row + .set_title(&gettext("Crypto Identity Incomplete")); + self.crypto_identity_description.set_label(&gettext( + "The crypto identity exists but this device is not verified", )); - imp.recovery_btn.set_label(&gettext("Recover…")); - imp.recovery_btn + self.crypto_identity_btn.set_label(&gettext("Verify…")); + self.crypto_identity_btn .update_property(&[gtk::accessible::Property::Label(&gettext( - "Recover Account Data", + "Verify This Session", ))]); - imp.recovery_btn.add_css_class("suggested-action"); + self.crypto_identity_btn.add_css_class("suggested-action"); } } + + /// Update the recovery section. + fn update_recovery(&self) { + let Some(session) = self.session.upgrade() else { + return; + }; + + let recovery_state = session.security().recovery_state(); + match recovery_state { + RecoveryState::Unknown | RecoveryState::Disabled => { + self.recovery_icon.set_icon_name(Some("sync-off-symbolic")); + self.recovery_icon.remove_css_class("success"); + self.recovery_icon.remove_css_class("warning"); + self.recovery_icon.add_css_class("error"); + + self.recovery_row + .set_title(&gettext("Account Recovery Disabled")); + self.recovery_description.set_label(&gettext( + "Enable recovery to be able to restore your account without another device", + )); + + self.recovery_btn.set_label(&gettext("Enable…")); + self.recovery_btn + .update_property(&[gtk::accessible::Property::Label(&gettext( + "Enable Account Recovery", + ))]); + self.recovery_btn.add_css_class("suggested-action"); + } + RecoveryState::Enabled => { + self.recovery_icon.set_icon_name(Some("sync-on-symbolic")); + self.recovery_icon.add_css_class("success"); + self.recovery_icon.remove_css_class("warning"); + self.recovery_icon.remove_css_class("error"); + + self.recovery_row + .set_title(&gettext("Account Recovery Enabled")); + self.recovery_description.set_label(&gettext( + "Your signing keys and encryption keys are synchronized", + )); + + self.recovery_btn.set_label(&gettext("Reset…")); + self.recovery_btn + .update_property(&[gtk::accessible::Property::Label(&gettext( + "Reset Account Recovery Key", + ))]); + self.recovery_btn.remove_css_class("suggested-action"); + } + RecoveryState::Incomplete => { + self.recovery_icon + .set_icon_name(Some("sync-partial-symbolic")); + self.recovery_icon.remove_css_class("success"); + self.recovery_icon.add_css_class("warning"); + self.recovery_icon.remove_css_class("error"); + + self.recovery_row + .set_title(&gettext("Account Recovery Incomplete")); + self.recovery_description.set_label(&gettext( + "Recover to synchronize your signing keys and encryption keys", + )); + + self.recovery_btn.set_label(&gettext("Recover…")); + self.recovery_btn + .update_property(&[gtk::accessible::Property::Label(&gettext( + "Recover Account Data", + ))]); + self.recovery_btn.add_css_class("suggested-action"); + } + } + } + } +} + +glib::wrapper! { + /// Security settings page. + pub struct SecurityPage(ObjectSubclass) + @extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible; +} + +impl SecurityPage { + pub fn new(session: &Session) -> Self { + glib::Object::builder().property("session", session).build() } } diff --git a/src/session/view/account_settings/user_sessions_page/mod.rs b/src/session/view/account_settings/user_sessions_page/mod.rs index 4dc663ff..08587870 100644 --- a/src/session/view/account_settings/user_sessions_page/mod.rs +++ b/src/session/view/account_settings/user_sessions_page/mod.rs @@ -1,5 +1,5 @@ -use adw::subclass::prelude::*; -use gtk::{gio, glib, glib::clone, prelude::*, CompositeTemplate}; +use adw::{prelude::*, subclass::prelude::*}; +use gtk::{gio, glib, glib::clone, CompositeTemplate}; use tracing::error; mod user_session_row; diff --git a/src/session/view/account_settings/user_sessions_page/user_session_row.rs b/src/session/view/account_settings/user_sessions_page/user_session_row.rs index 19ea35eb..f1005b85 100644 --- a/src/session/view/account_settings/user_sessions_page/user_session_row.rs +++ b/src/session/view/account_settings/user_sessions_page/user_session_row.rs @@ -1,5 +1,5 @@ -use adw::prelude::*; -use gtk::{glib, subclass::prelude::*, CompositeTemplate}; +use adw::{prelude::*, subclass::prelude::*}; +use gtk::{glib, CompositeTemplate}; use crate::{session::model::UserSession, utils::template_callbacks::TemplateCallbacks};