Browse Source

account-settings: Refactor and clean up

af/unable-to-decryt-styling
Kévin Commaille 12 months ago
parent
commit
194beff585
No known key found for this signature in database
GPG Key ID: C971D9DBC9D678D
  1. 345
      src/session/view/account_settings/general_page/change_password_subpage.rs
  2. 10
      src/session/view/account_settings/general_page/change_password_subpage.ui
  3. 2
      src/session/view/account_settings/general_page/deactivate_account_subpage.rs
  4. 153
      src/session/view/account_settings/general_page/log_out_subpage.rs
  5. 533
      src/session/view/account_settings/general_page/mod.rs
  6. 510
      src/session/view/account_settings/notifications_page.rs
  7. 1
      src/session/view/account_settings/notifications_page.ui
  8. 57
      src/session/view/account_settings/security_page/ignored_users_subpage/ignored_user_row.rs
  9. 14
      src/session/view/account_settings/security_page/ignored_users_subpage/mod.rs
  10. 496
      src/session/view/account_settings/security_page/import_export_keys_subpage.rs
  11. 334
      src/session/view/account_settings/security_page/mod.rs
  12. 4
      src/session/view/account_settings/user_sessions_page/mod.rs
  13. 4
      src/session/view/account_settings/user_sessions_page/user_session_row.rs

345
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<Session>,
#[template_child]
pub password: TemplateChild<adw::PasswordEntryRow>,
password: TemplateChild<adw::PasswordEntryRow>,
#[template_child]
pub password_progress: TemplateChild<gtk::LevelBar>,
password_progress: TemplateChild<gtk::LevelBar>,
#[template_child]
pub password_error_revealer: TemplateChild<gtk::Revealer>,
password_error_revealer: TemplateChild<gtk::Revealer>,
#[template_child]
pub password_error: TemplateChild<gtk::Label>,
password_error: TemplateChild<gtk::Label>,
#[template_child]
pub confirm_password: TemplateChild<adw::PasswordEntryRow>,
confirm_password: TemplateChild<adw::PasswordEntryRow>,
#[template_child]
pub confirm_password_error_revealer: TemplateChild<gtk::Revealer>,
confirm_password_error_revealer: TemplateChild<gtk::Revealer>,
#[template_child]
pub confirm_password_error: TemplateChild<gtk::Label>,
confirm_password_error: TemplateChild<gtk::Label>,
#[template_child]
pub button: TemplateChild<LoadingButtonRow>,
button: TemplateChild<LoadingButtonRow>,
/// The current session.
#[property(get, set, nullable)]
session: glib::WeakRef<Session>,
}
#[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<Self>) {
@ -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<imp::ChangePasswordSubpage>)
@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<imp::ChangePasswordSubpage>)
@extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible;
}
impl ChangePasswordSubpage {
pub fn new(session: &Session) -> Self {
glib::Object::builder().property("session", session).build()
}
}

10
src/session/view/account_settings/general_page/change_password_subpage.ui

@ -68,6 +68,7 @@
<relation name="described-by">password_error</relation>
</accessibility>
<property name="title" translatable="yes">New Password</property>
<signal name="changed" handler="validate_password" swapped="yes"/>
<signal name="entry-activated" handler="change_password" swapped="yes"/>
</object>
</child>
@ -78,6 +79,14 @@
<property name="margin-top">2</property>
<property name="margin-bottom">1</property>
<property name="mode">discrete</property>
<property name="max-value">5</property>
<offsets>
<offset name="low" value="1"/>
<offset name="step2" value="2"/>
<offset name="step3" value="3"/>
<offset name="high" value="4"/>
<offset name="full" value="5"/>
</offsets>
<property name="accessible-role">presentation</property>
</object>
</child>
@ -114,6 +123,7 @@
<relation name="described-by">confirm_password_error</relation>
</accessibility>
<property name="title" translatable="yes">Confirm New Password</property>
<signal name="changed" handler="validate_password_confirmation" swapped="yes"/>
<signal name="entry-activated" handler="change_password" swapped="yes"/>
</object>
</child>

2
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<imp::DeactivateAccountSubpage>)
@extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible;
}

153
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<Session>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
stack: TemplateChild<gtk::Stack>,
#[template_child]
pub warning_box: TemplateChild<gtk::Box>,
warning_box: TemplateChild<gtk::Box>,
#[template_child]
pub warning_description: TemplateChild<gtk::Label>,
warning_description: TemplateChild<gtk::Label>,
#[template_child]
pub warning_button: TemplateChild<adw::ButtonRow>,
warning_button: TemplateChild<adw::ButtonRow>,
#[template_child]
pub logout_button: TemplateChild<LoadingButtonRow>,
logout_button: TemplateChild<LoadingButtonRow>,
#[template_child]
pub try_again_button: TemplateChild<LoadingButtonRow>,
try_again_button: TemplateChild<LoadingButtonRow>,
#[template_child]
pub remove_button: TemplateChild<LoadingButtonRow>,
remove_button: TemplateChild<LoadingButtonRow>,
/// The current session.
#[property(get, set = Self::set_session, nullable)]
session: glib::WeakRef<Session>,
}
#[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<Self>) {
@ -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<imp::LogOutSubpage>)
@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::<AccountSettings>()
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::<AccountSettings>()
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<imp::LogOutSubpage>)
@extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible;
}
impl LogOutSubpage {
pub fn new(session: &Session) -> Self {
glib::Object::builder().property("session", session).build()
}
}

533
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<EditableAvatar>,
avatar: TemplateChild<EditableAvatar>,
#[template_child]
pub display_name: TemplateChild<adw::EntryRow>,
display_name: TemplateChild<adw::EntryRow>,
#[template_child]
pub display_name_button: TemplateChild<ActionButton>,
display_name_button: TemplateChild<ActionButton>,
#[template_child]
pub change_password_group: TemplateChild<adw::PreferencesGroup>,
change_password_group: TemplateChild<adw::PreferencesGroup>,
#[template_child]
manage_account_group: TemplateChild<adw::PreferencesGroup>,
#[template_child]
pub homeserver: TemplateChild<CopyableRow>,
homeserver: TemplateChild<CopyableRow>,
#[template_child]
pub user_id: TemplateChild<CopyableRow>,
user_id: TemplateChild<CopyableRow>,
#[template_child]
pub session_id: TemplateChild<CopyableRow>,
session_id: TemplateChild<CopyableRow>,
#[template_child]
deactivate_account_button: TemplateChild<adw::ButtonRow>,
/// The current session.
@ -65,10 +61,10 @@ mod imp {
account_settings: glib::WeakRef<AccountSettings>,
/// The possible changes on the homeserver.
capabilities: RefCell<Capabilities>,
pub changing_avatar: RefCell<Option<OngoingAsyncAction<OwnedMxcUri>>>,
pub changing_display_name: RefCell<Option<OngoingAsyncAction<String>>>,
pub avatar_uri_handler: RefCell<Option<glib::SignalHandlerId>>,
pub display_name_handler: RefCell<Option<glib::SignalHandlerId>>,
changing_avatar: RefCell<Option<OngoingAsyncAction<OwnedMxcUri>>>,
changing_display_name: RefCell<Option<OngoingAsyncAction<String>>>,
avatar_uri_handler: RefCell<Option<glib::SignalHandlerId>>,
display_name_handler: RefCell<Option<glib::SignalHandlerId>>,
}
#[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<imp::GeneralPage>)
@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<imp::GeneralPage>)
@extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible;
}
impl GeneralPage {
pub fn new(session: &Session) -> Self {
glib::Object::builder().property("session", session).build()
}
}

510
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<SwitchLoadingRow>,
account_row: TemplateChild<SwitchLoadingRow>,
#[template_child]
pub session_row: TemplateChild<adw::SwitchRow>,
session_row: TemplateChild<adw::SwitchRow>,
#[template_child]
pub global: TemplateChild<adw::PreferencesGroup>,
global: TemplateChild<adw::PreferencesGroup>,
#[template_child]
pub global_all_row: TemplateChild<CheckLoadingRow>,
global_all_row: TemplateChild<CheckLoadingRow>,
#[template_child]
pub global_direct_row: TemplateChild<CheckLoadingRow>,
global_direct_row: TemplateChild<CheckLoadingRow>,
#[template_child]
pub global_mentions_row: TemplateChild<CheckLoadingRow>,
global_mentions_row: TemplateChild<CheckLoadingRow>,
#[template_child]
pub keywords: TemplateChild<gtk::ListBox>,
keywords: TemplateChild<gtk::ListBox>,
#[template_child]
pub keywords_add_row: TemplateChild<EntryAddRow>,
keywords_add_row: TemplateChild<EntryAddRow>,
/// The notifications settings of the current session.
#[property(get, set = Self::set_notifications_settings, explicit_notify)]
pub notifications_settings: BoundObjectWeakRef<NotificationsSettings>,
notifications_settings: BoundObjectWeakRef<NotificationsSettings>,
/// Whether the account section is busy.
#[property(get)]
pub account_loading: Cell<bool>,
account_loading: Cell<bool>,
/// Whether the global section is busy.
#[property(get)]
pub global_loading: Cell<bool>,
global_loading: Cell<bool>,
/// The global notifications setting, as a string.
#[property(get = Self::global_setting, set = Self::set_global_setting)]
pub global_setting: PhantomData<String>,
global_setting: PhantomData<String>,
}
#[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<imp::NotificationsPage>)
@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::<gtk::StringObject>() 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::<gtk::StringObject>() {
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::<glib::Object>() {
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::<gtk::StringObject>()
.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::<glib::Object>() {
let Ok(keyword_obj) = keyword_obj else {
break;
};
if let Some(keyword) = keyword_obj
.downcast_ref::<gtk::StringObject>()
.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<imp::NotificationsPage>)
@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()
}
}

1
src/session/view/account_settings/notifications_page.ui

@ -66,6 +66,7 @@
<object class="EntryAddRow" id="keywords_add_row">
<property name="title" translatable="yes">Add Keyword</property>
<property name="add-button-tooltip-text" translatable="yes">Add Keyword</property>
<signal name="changed" handler="update_keywords" swapped="yes"/>
<signal name="add" handler="add_keyword" swapped="yes"/>
<signal name="entry-activated" handler="add_keyword" swapped="yes"/>
</object>

57
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<LoadingButton>,
stop_ignoring_button: TemplateChild<LoadingButton>,
/// The item containing the user ID presented by this row.
#[property(get, set = Self::set_item, explicit_notify, nullable)]
pub item: RefCell<Option<gtk::StringObject>>,
item: RefCell<Option<gtk::StringObject>>,
/// The current list of ignored users.
#[property(get, set, nullable)]
pub ignored_users: RefCell<Option<IgnoredUsers>>,
ignored_users: RefCell<Option<IgnoredUsers>>,
}
#[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<Self>) {
@ -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<gtk::StringObject>) {
@ -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);
}
}
}

14
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<gtk::Stack>,
stack: TemplateChild<gtk::Stack>,
#[template_child]
pub search_bar: TemplateChild<gtk::SearchBar>,
search_bar: TemplateChild<gtk::SearchBar>,
#[template_child]
pub search_entry: TemplateChild<gtk::SearchEntry>,
search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child]
pub list_view: TemplateChild<gtk::ListView>,
pub filtered_model: gtk::FilterListModel,
list_view: TemplateChild<gtk::ListView>,
filtered_model: gtk::FilterListModel,
/// The current session.
#[property(get, set = Self::set_session, explicit_notify, nullable)]
pub session: glib::WeakRef<Session>,
pub items_changed_handler: RefCell<Option<glib::SignalHandlerId>>,
session: glib::WeakRef<Session>,
items_changed_handler: RefCell<Option<glib::SignalHandlerId>>,
}
#[glib::object_subclass]

496
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<Session>,
#[template_child]
pub description: TemplateChild<gtk::Label>,
description: TemplateChild<gtk::Label>,
#[template_child]
pub instructions: TemplateChild<gtk::Label>,
instructions: TemplateChild<gtk::Label>,
#[template_child]
pub passphrase: TemplateChild<adw::PasswordEntryRow>,
passphrase: TemplateChild<adw::PasswordEntryRow>,
#[template_child]
pub confirm_passphrase_box: TemplateChild<gtk::Box>,
confirm_passphrase_box: TemplateChild<gtk::Box>,
#[template_child]
pub confirm_passphrase: TemplateChild<adw::PasswordEntryRow>,
confirm_passphrase: TemplateChild<adw::PasswordEntryRow>,
#[template_child]
pub confirm_passphrase_error_revealer: TemplateChild<gtk::Revealer>,
confirm_passphrase_error_revealer: TemplateChild<gtk::Revealer>,
#[template_child]
pub confirm_passphrase_error: TemplateChild<gtk::Label>,
confirm_passphrase_error: TemplateChild<gtk::Label>,
#[template_child]
pub file_row: TemplateChild<adw::ActionRow>,
file_row: TemplateChild<adw::ActionRow>,
#[template_child]
pub file_button: TemplateChild<gtk::Button>,
file_button: TemplateChild<gtk::Button>,
#[template_child]
pub proceed_button: TemplateChild<LoadingButtonRow>,
proceed_button: TemplateChild<LoadingButtonRow>,
/// The current session.
#[property(get, set, nullable)]
session: glib::WeakRef<Session>,
/// The path of the file for the encryption keys.
#[property(get)]
pub file_path: RefCell<Option<gio::File>>,
file_path: RefCell<Option<gio::File>>,
/// The path of the file for the encryption keys, as a string.
#[property(get = Self::file_path_string)]
pub file_path_string: PhantomData<Option<String>>,
file_path_string: PhantomData<Option<String>>,
/// The export/import mode of the subpage.
#[property(get, set = Self::set_mode, explicit_notify, builder(ImportExportKeysSubpageMode::default()))]
pub mode: Cell<ImportExportKeysSubpageMode>,
mode: Cell<ImportExportKeysSubpageMode>,
}
#[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<Self>) {
@ -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<imp::ImportExportKeysSubpage>)
@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<gio::File>) {
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<gio::File>) {
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::<gtk::Window>();
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::<gtk::Window>();
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::<Box<dyn std::error::Error + Send>, _>(|error| Box::new(error))
} else {
encryption
.import_room_keys(file_path, passphrase.as_str())
.await
.map(|res| res.imported_count)
.map_err::<Box<dyn std::error::Error + Send>, _>(|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::<Box<dyn std::error::Error + Send>, _>(|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::<Box<dyn std::error::Error + Send>, _>(|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::<RoomKeyImportError>()
.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::<RoomKeyImportError>()
.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<imp::ImportExportKeysSubpage>)
@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()
}
}

334
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<adw::SwitchRow>,
public_read_receipts_row: TemplateChild<adw::SwitchRow>,
#[template_child]
pub typing_row: TemplateChild<adw::SwitchRow>,
typing_row: TemplateChild<adw::SwitchRow>,
#[template_child]
pub ignored_users_row: TemplateChild<ButtonCountRow>,
ignored_users_row: TemplateChild<ButtonCountRow>,
#[template_child]
pub crypto_identity_row: TemplateChild<adw::PreferencesRow>,
crypto_identity_row: TemplateChild<adw::PreferencesRow>,
#[template_child]
pub crypto_identity_icon: TemplateChild<gtk::Image>,
crypto_identity_icon: TemplateChild<gtk::Image>,
#[template_child]
pub crypto_identity_description: TemplateChild<gtk::Label>,
crypto_identity_description: TemplateChild<gtk::Label>,
#[template_child]
pub crypto_identity_btn: TemplateChild<gtk::Button>,
crypto_identity_btn: TemplateChild<gtk::Button>,
#[template_child]
pub recovery_row: TemplateChild<adw::PreferencesRow>,
recovery_row: TemplateChild<adw::PreferencesRow>,
#[template_child]
pub recovery_icon: TemplateChild<gtk::Image>,
recovery_icon: TemplateChild<gtk::Image>,
#[template_child]
pub recovery_description: TemplateChild<gtk::Label>,
recovery_description: TemplateChild<gtk::Label>,
#[template_child]
pub recovery_btn: TemplateChild<gtk::Button>,
recovery_btn: TemplateChild<gtk::Button>,
/// The current session.
#[property(get, set = Self::set_session, nullable)]
pub session: glib::WeakRef<Session>,
session: glib::WeakRef<Session>,
ignored_users_count_handler: RefCell<Option<glib::SignalHandlerId>>,
security_handlers: RefCell<Vec<glib::SignalHandlerId>>,
bindings: RefCell<Vec<glib::Binding>>,
@ -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<imp::SecurityPage>)
@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<imp::SecurityPage>)
@extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible;
}
impl SecurityPage {
pub fn new(session: &Session) -> Self {
glib::Object::builder().property("session", session).build()
}
}

4
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;

4
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};

Loading…
Cancel
Save