[fractal/fractal-next] account-settings: Add General tab
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] account-settings: Add General tab
- Date: Fri, 25 Feb 2022 09:48:44 +0000 (UTC)
commit 5c8c627cec690c2d7659e9da512c29f39d76bfc3
Author: Kévin Commaille <zecakeh tedomum fr>
Date: Mon Feb 14 11:28:17 2022 +0100
account-settings: Add General tab
data/resources/resources.gresource.xml | 3 +
data/resources/style.css | 32 +-
.../ui/account-settings-change-password-subpage.ui | 103 +++++
.../account-settings-deactivate-account-subpage.ui | 101 +++++
data/resources/ui/account-settings-user-page.ui | 129 ++++++
data/resources/ui/account-settings.ui | 13 +-
po/POTFILES.in | 6 +
src/components/editable_avatar.rs | 6 +-
src/session/account_settings/mod.rs | 64 ++-
.../user_page/change_password_subpage.rs | 344 +++++++++++++++
.../user_page/deactivate_account_subpage.rs | 217 ++++++++++
src/session/account_settings/user_page/mod.rs | 482 +++++++++++++++++++++
src/session/mod.rs | 63 +--
src/session/room/member.rs | 7 +
src/utils.rs | 74 ++++
15 files changed, 1577 insertions(+), 67 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 8150002c1..fd9dd2e6b 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -11,8 +11,11 @@
<file preprocess="xml-stripblanks">icons/scalable/status/explore-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/verified-symbolic.svg</file>
<file compressed="true">style.css</file>
+ <file compressed="true" preprocess="xml-stripblanks"
alias="account-settings-change-password-subpage.ui">ui/account-settings-change-password-subpage.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks"
alias="account-settings-deactivate-account-subpage.ui">ui/account-settings-deactivate-account-subpage.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="account-settings-device-row.ui">ui/account-settings-device-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="account-settings-devices-page.ui">ui/account-settings-devices-page.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks"
alias="account-settings-user-page.ui">ui/account-settings-user-page.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="account-settings.ui">ui/account-settings.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="add-account-row.ui">ui/add-account-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="avatar-with-selection.ui">ui/avatar-with-selection.ui</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index 335667d63..ba84a43c0 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -44,6 +44,19 @@ button.opaque.success {
color: @error_bg_color;
}
+preferencesgroup .body {
+ line-height: 140%;
+}
+
+preferencespage.status-page clamp > box {
+ margin: 42px 12px;
+}
+
+button.row {
+ min-height: 50px;
+ border-radius: 12px;
+}
+
/* Components */
@@ -97,9 +110,6 @@ row.entry {
animation-timing-function: ease-in-out;
outline: 0 solid transparent;
outline-offset: 2px;
- border-top: 1px solid transparent;
- border-left: 1px solid transparent;
- border-right: 1px solid transparent;
}
row.entry:focus-within {
@@ -108,26 +118,14 @@ row.entry:focus-within {
outline-offset: -2px;
}
-row.entry.success {
- border: 1px solid @success_color;
-}
-
row.entry.success:focus-within {
outline-color: @success_color;
}
-row.entry.warning {
- border: 1px solid @warning_color;
-}
-
row.entry.warning:focus-within {
outline-color: @warning_color;
}
-row.entry.error {
- border: 1px solid @error_color;
-}
-
row.entry.error:focus-within {
outline-color: @error_color;
}
@@ -150,6 +148,10 @@ row.entry levelbar.discrete block {
min-height: 5px;
}
+row.entry levelbar.discrete block.filled {
+ background-color: alpha(currentColor, 0.5);
+}
+
row.entry.accent levelbar.discrete block.filled {
background-color: @accent_color;
}
diff --git a/data/resources/ui/account-settings-change-password-subpage.ui
b/data/resources/ui/account-settings-change-password-subpage.ui
new file mode 100644
index 000000000..7065b8f15
--- /dev/null
+++ b/data/resources/ui/account-settings-change-password-subpage.ui
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ChangePasswordSubpage" parent="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkHeaderBar">
+ <property name="title-widget">
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Change Password</property>
+ <property name="single-line-mode">True</property>
+ <property name="ellipsize">end</property>
+ <property name="width-chars">5</property>
+ <style>
+ <class name="title"/>
+ </style>
+ </object>
+ </property>
+ <child type="start">
+ <object class="GtkButton" id="back">
+ <property name="icon-name">go-previous-symbolic</property>
+ <property name="action-name">win.close-subpage</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesPage">
+ <style>
+ <class name="status-page"/>
+ </style>
+ <property name="vexpand">true</property>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkImage">
+ <style>
+ <class name="extra-large-icon"/>
+ <class name="error"/>
+ </style>
+ <property name="icon-name">dialog-warning-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkLabel">
+ <style>
+ <class name="body"/>
+ </style>
+ <property name="label">Changing your password will log you out of your other
sessions.</property>
+ <property name="wrap">True</property>
+ <property name="wrap-mode">word-char</property>
+ <property name="xalign">0.0</property>
+ <property name="margin-bottom">12</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <style>
+ <class name="body"/>
+ </style>
+ <property name="label">Fractal's support for encryption is unstable so you might lose access
to your encrypted message history. It is recommended to backup your encryption keys from another Matrix
client before proceeding.</property>
+ <property name="wrap">True</property>
+ <property name="wrap-mode">word-char</property>
+ <property name="xalign">0.0</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="ComponentsPasswordEntryRow" id="password">
+ <property name="title" translatable="yes">New Password</property>
+ </object>
+ </child>
+ <child>
+ <object class="ComponentsPasswordEntryRow" id="confirm_password">
+ <property name="title" translatable="yes">Confirm New Password</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="SpinnerButton" id="button">
+ <style>
+ <class name="row"/>
+ <class name="destructive-action"/>
+ </style>
+ <property name="label" translatable="yes">Continue</property>
+ <property name="sensitive">false</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/data/resources/ui/account-settings-deactivate-account-subpage.ui
b/data/resources/ui/account-settings-deactivate-account-subpage.ui
new file mode 100644
index 000000000..d27f6e0a3
--- /dev/null
+++ b/data/resources/ui/account-settings-deactivate-account-subpage.ui
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="DeactivateAccountSubpage" parent="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkHeaderBar">
+ <property name="title-widget">
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Deactivate Account</property>
+ <property name="single-line-mode">True</property>
+ <property name="ellipsize">end</property>
+ <property name="width-chars">5</property>
+ <style>
+ <class name="title"/>
+ </style>
+ </object>
+ </property>
+ <child type="start">
+ <object class="GtkButton" id="back">
+ <property name="icon-name">go-previous-symbolic</property>
+ <property name="action-name">win.close-subpage</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesPage">
+ <style>
+ <class name="status-page"/>
+ </style>
+ <property name="vexpand">true</property>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkImage">
+ <style>
+ <class name="extra-large-icon"/>
+ <class name="error"/>
+ </style>
+ <property name="icon-name">dialog-warning-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkLabel">
+ <style>
+ <class name="body"/>
+ </style>
+ <property name="label">Deactivating your account means you will lose access to all your
messages, contacts, files, and more, forever.</property>
+ <property name="wrap">True</property>
+ <property name="wrap-mode">word-char</property>
+ <property name="xalign">0.0</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkLabel">
+ <style>
+ <class name="body"/>
+ </style>
+ <property name="label">To confirm that you really want to deactivate this account, type in
your Matrix user ID:</property>
+ <property name="wrap">True</property>
+ <property name="wrap-mode">word-char</property>
+ <property name="xalign">0.0</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="ComponentsEntryRow" id="confirmation">
+ <property name="title" translatable="yes">Matrix User ID</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="SpinnerButton" id="button">
+ <style>
+ <class name="row"/>
+ <class name="destructive-action"/>
+ </style>
+ <property name="label" translatable="yes">Continue</property>
+ <property name="sensitive">false</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/data/resources/ui/account-settings-user-page.ui b/data/resources/ui/account-settings-user-page.ui
new file mode 100644
index 000000000..1e04fd524
--- /dev/null
+++ b/data/resources/ui/account-settings-user-page.ui
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="UserPage" parent="AdwPreferencesPage">
+ <property name="icon-name">preferences-system-symbolic</property>
+ <property name="title" translatable="yes">General</property>
+ <property name="name">general</property>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="ComponentsEditableAvatar" id="avatar">
+ <binding name="avatar">
+ <lookup name="avatar">
+ <lookup name="user">
+ <lookup name="session">UserPage</lookup>
+ </lookup>
+ </lookup>
+ </binding>
+ <property name="editable">true</property>
+ <binding name="removable">
+ <closure type="gboolean" function="object_is_some">
+ <lookup name="image">
+ <lookup name="avatar">
+ <lookup name="user">
+ <lookup name="session">UserPage</lookup>
+ </lookup>
+ </lookup>
+ </lookup>
+ </closure>
+ </binding>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="ComponentsEntryRow" id="display_name">
+ <property name="title" translatable="yes">Name</property>
+ <binding name="text">
+ <lookup name="display-name">
+ <lookup name="user">
+ <lookup name="session">UserPage</lookup>
+ </lookup>
+ </lookup>
+ </binding>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup" id="change_password_group">
+ <child>
+ <object class="ComponentsButtonRow">
+ <property name="title" translatable="yes">Change Password</property>
+ <property name="to-subpage">true</property>
+ <signal name="activated" handler="show_change_password" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <property name="title" translatable="yes">Advanced Information</property>
+ <child>
+ <object class="AdwActionRow">
+ <property name="title" translatable="yes">Homeserver</property>
+ <child>
+ <object class="GtkLabel" id="homeserver">
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <property name="ellipsize">end</property>
+ <property name="selectable">true</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwActionRow">
+ <property name="title" translatable="yes">Matrix User ID</property>
+ <child>
+ <object class="GtkLabel" id="user_id">
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <property name="ellipsize">end</property>
+ <property name="selectable">true</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwActionRow">
+ <property name="title" translatable="yes">Session ID</property>
+ <child>
+ <object class="GtkLabel" id="session_id">
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <property name="ellipsize">end</property>
+ <property name="selectable">true</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="ComponentsButtonRow">
+ <style>
+ <class name="error"/>
+ </style>
+ <property name="title" translatable="yes">Deactivate Account</property>
+ <property name="to-subpage">true</property>
+ <signal name="activated" handler="show_deactivate_account" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="ChangePasswordSubpage" id="change_password_subpage">
+ <property name="session" bind-source="UserPage" bind-property="session" bind-flags="sync-create"/>
+ </object>
+ <object class="DeactivateAccountSubpage" id="deactivate_account_subpage">
+ <property name="session" bind-source="UserPage" bind-property="session" bind-flags="sync-create"/>
+ </object>
+</interface>
diff --git a/data/resources/ui/account-settings.ui b/data/resources/ui/account-settings.ui
index c4c54c6c3..c9ca479fa 100644
--- a/data/resources/ui/account-settings.ui
+++ b/data/resources/ui/account-settings.ui
@@ -2,14 +2,21 @@
<interface>
<template class="AccountSettings" parent="AdwPreferencesWindow">
<property name="title" translatable="yes">Account Settings</property>
- <property name="search-enabled">False</property>
+ <property name="search-enabled">false</property>
+ <property name="default-height">630</property>
+ <child>
+ <object class="UserPage">
+ <property name="session" bind-source="AccountSettings" bind-property="session"
bind-flags="sync-create"/>
+ </object>
+ </child>
<child>
<object class="DevicesPage">
<binding name="user">
- <lookup name="user">AccountSettings</lookup>
+ <lookup name="user">
+ <lookup name="session">AccountSettings</lookup>
+ </lookup>
</binding>
</object>
</child>
</template>
</interface>
-
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 81faf3cb5..5ac240a4b 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -5,8 +5,11 @@ data/org.gnome.FractalNext.gschema.xml.in
data/org.gnome.FractalNext.metainfo.xml.in.in
# UI files
+data/resources/ui/account-settings-change-password-subpage.ui
+data/resources/ui/account-settings-deactivate-account-subpage.ui
data/resources/ui/account-settings-device-row.ui
data/resources/ui/account-settings-devices-page.ui
+data/resources/ui/account-settings-user-page.ui
data/resources/ui/account-settings.ui
data/resources/ui/components-auth-dialog.ui
data/resources/ui/components-loading-listbox-row.ui
@@ -42,6 +45,9 @@ src/login.rs
src/secret.rs
src/session/account_settings/devices_page/device_list.rs
src/session/account_settings/devices_page/device_row.rs
+src/session/account_settings/user_page/change_password_subpage.rs
+src/session/account_settings/user_page/deactivate_account_subpage.rs
+src/session/account_settings/user_page/mod.rs
src/session/content/explore/public_room_row.rs
src/session/content/room_details/member_page/mod.rs
src/session/content/room_details/mod.rs
diff --git a/src/components/editable_avatar.rs b/src/components/editable_avatar.rs
index 76ca68489..47caba017 100644
--- a/src/components/editable_avatar.rs
+++ b/src/components/editable_avatar.rs
@@ -362,14 +362,14 @@ impl EditableAvatar {
} else {
error!("The chosen file is not an image");
let _ = self.activate_action(
- "win.message",
+ "win.add-toast",
Some(&gettext("The chosen file is not an image").to_variant()),
);
}
} else {
error!("Could not get the content type of the file");
let _ = self.activate_action(
- "win.message",
+ "win.add-toast",
Some(
&gettext("Could not determine the type of the chosen file")
.to_variant(),
@@ -379,7 +379,7 @@ impl EditableAvatar {
} else {
error!("No file chosen");
let _ = self.activate_action(
- "win.message",
+ "win.add-toast",
Some(&gettext("No file was chosen").to_variant()),
);
}
diff --git a/src/session/account_settings/mod.rs b/src/session/account_settings/mod.rs
index 08b90dd07..8e02562a9 100644
--- a/src/session/account_settings/mod.rs
+++ b/src/session/account_settings/mod.rs
@@ -1,22 +1,24 @@
-use adw::subclass::prelude::*;
-use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
+use adw::{prelude::*, subclass::prelude::*};
+use gtk::{glib, glib::FromVariant, subclass::prelude::*, CompositeTemplate};
mod devices_page;
+mod user_page;
use devices_page::DevicesPage;
+use user_page::UserPage;
-use crate::session::User;
+use super::Session;
mod imp {
use std::cell::RefCell;
- use glib::subclass::InitializingObject;
+ use glib::{subclass::InitializingObject, WeakRef};
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/FractalNext/account-settings.ui")]
pub struct AccountSettings {
- pub user: RefCell<Option<User>>,
+ pub session: RefCell<Option<WeakRef<Session>>>,
}
#[glib::object_subclass]
@@ -27,7 +29,23 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
DevicesPage::static_type();
+ UserPage::static_type();
Self::bind_template(klass);
+
+ klass.install_action("account-settings.close", None, |obj, _, _| {
+ obj.close();
+ });
+
+ klass.install_action("win.add-toast", Some("s"), |obj, _, message| {
+ if let Some(message) = message.and_then(String::from_variant) {
+ let toast = adw::Toast::new(&message);
+ obj.add_toast(&toast);
+ }
+ });
+
+ klass.install_action("win.close-subpage", None, |obj, _, _| {
+ obj.close_subpage();
+ });
}
fn instance_init(obj: &InitializingObject<Self>) {
@@ -40,11 +58,11 @@ mod imp {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::new(
- "user",
- "User",
- "The user of this account",
- User::static_type(),
- glib::ParamFlags::READWRITE,
+ "session",
+ "Session",
+ "The session",
+ Session::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
)]
});
@@ -59,14 +77,14 @@ mod imp {
pspec: &glib::ParamSpec,
) {
match pspec.name() {
- "user" => obj.set_user(value.get().unwrap()),
+ "session" => obj.set_session(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
- "user" => obj.user().to_value(),
+ "session" => obj.session().to_value(),
_ => unimplemented!(),
}
}
@@ -85,21 +103,27 @@ glib::wrapper! {
}
impl AccountSettings {
- pub fn new(parent_window: Option<&impl IsA<gtk::Window>>, user: &User) -> Self {
- glib::Object::new(&[("transient-for", &parent_window), ("user", user)])
+ pub fn new(parent_window: Option<&impl IsA<gtk::Window>>, session: &Session) -> Self {
+ glib::Object::new(&[("transient-for", &parent_window), ("session", session)])
.expect("Failed to create AccountSettings")
}
- pub fn user(&self) -> Option<User> {
- self.imp().user.borrow().clone()
+ pub fn session(&self) -> Option<Session> {
+ self.imp()
+ .session
+ .borrow()
+ .clone()
+ .and_then(|session| session.upgrade())
}
- fn set_user(&self, user: Option<User>) {
- if self.user() == user {
+ pub fn set_session(&self, session: Option<Session>) {
+ if self.session() == session {
return;
}
- self.imp().user.replace(user);
- self.notify("user");
+ self.imp()
+ .session
+ .replace(session.map(|session| session.downgrade()));
+ self.notify("session");
}
}
diff --git a/src/session/account_settings/user_page/change_password_subpage.rs
b/src/session/account_settings/user_page/change_password_subpage.rs
new file mode 100644
index 000000000..026426cc3
--- /dev/null
+++ b/src/session/account_settings/user_page/change_password_subpage.rs
@@ -0,0 +1,344 @@
+use adw::{prelude::*, subclass::prelude::*};
+use gettextrs::gettext;
+use gtk::{
+ glib::{self, clone},
+ subclass::prelude::*,
+ CompositeTemplate,
+};
+use log::error;
+use matrix_sdk::{
+ ruma::{
+ api::{
+ client::r0::account::change_password,
+ error::{FromHttpResponseError, ServerError},
+ },
+ assign,
+ },
+ Error as MatrixError, HttpError,
+};
+
+use crate::{
+ components::{AuthDialog, AuthError, PasswordEntryRow, SpinnerButton},
+ session::Session,
+ spawn,
+ utils::validate_password,
+};
+
+mod imp {
+ use glib::{subclass::InitializingObject, WeakRef};
+ use once_cell::unsync::OnceCell;
+
+ use super::*;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/account-settings-change-password-subpage.ui")]
+ pub struct ChangePasswordSubpage {
+ pub session: OnceCell<WeakRef<Session>>,
+ #[template_child]
+ pub password: TemplateChild<PasswordEntryRow>,
+ #[template_child]
+ pub confirm_password: TemplateChild<PasswordEntryRow>,
+ #[template_child]
+ pub button: TemplateChild<SpinnerButton>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for ChangePasswordSubpage {
+ const NAME: &'static str = "ChangePasswordSubpage";
+ type Type = super::ChangePasswordSubpage;
+ type ParentType = gtk::Box;
+
+ fn class_init(klass: &mut Self::Class) {
+ PasswordEntryRow::static_type();
+ SpinnerButton::static_type();
+ Self::bind_template(klass);
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for ChangePasswordSubpage {
+ fn properties() -> &'static [glib::ParamSpec] {
+ use once_cell::sync::Lazy;
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpecObject::new(
+ "session",
+ "Session",
+ "The session",
+ Session::static_type(),
+ glib::ParamFlags::READWRITE,
+ )]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "session" => obj.set_session(value.get().unwrap()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "session" => obj.session().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+
+ self.password.define_progress_steps(&[
+ >k::LEVEL_BAR_OFFSET_LOW,
+ "step2",
+ "step3",
+ >k::LEVEL_BAR_OFFSET_HIGH,
+ >k::LEVEL_BAR_OFFSET_FULL,
+ ]);
+ self.password
+ .connect_focused(clone!(@weak obj => move |entry, focused| {
+ if focused {
+ entry.set_progress_visible(true);
+ obj.validate_password();
+ } else {
+ entry.remove_css_class("warning");
+ entry.remove_css_class("success");
+ if entry.text().is_empty() {
+ entry.set_progress_visible(false);
+ }
+ }
+ }));
+ self.password
+ .connect_activated(clone!(@weak obj => move|_| {
+ spawn!(
+ clone!(@weak obj => async move {
+ obj.change_password().await;
+ })
+ );
+ }));
+ self.password.connect_changed(clone!(@weak obj => move|_| {
+ obj.validate_password();
+ }));
+
+ self.confirm_password
+ .connect_focused(clone!(@weak obj => move |entry, focused| {
+ if focused {
+ obj.validate_password_confirmation();
+ } else {
+ entry.remove_css_class("warning");
+ entry.remove_css_class("success");
+ }
+ }));
+ self.confirm_password
+ .connect_activated(clone!(@weak obj => move|_| {
+ spawn!(
+ clone!(@weak obj => async move {
+ obj.change_password().await;
+ })
+ );
+ }));
+ self.confirm_password
+ .connect_changed(clone!(@weak obj => move|_| {
+ obj.validate_password_confirmation();
+ }));
+
+ self.button.connect_clicked(clone!(@weak obj => move|_| {
+ spawn!(
+ clone!(@weak obj => async move {
+ obj.change_password().await;
+ })
+ );
+ }));
+ }
+ }
+
+ impl WidgetImpl for ChangePasswordSubpage {}
+ impl BoxImpl for ChangePasswordSubpage {}
+}
+
+glib::wrapper! {
+ /// Account settings page about the user and the session.
+ pub struct ChangePasswordSubpage(ObjectSubclass<imp::ChangePasswordSubpage>)
+ @extends gtk::Widget, gtk::Box, @implements gtk::Accessible;
+}
+
+impl ChangePasswordSubpage {
+ pub fn new(session: &Session) -> Self {
+ glib::Object::new(&[("session", session)]).expect("Failed to create ChangePasswordSubpage")
+ }
+
+ pub fn session(&self) -> Option<Session> {
+ self.imp()
+ .session
+ .get()
+ .and_then(|session| session.upgrade())
+ }
+
+ pub fn set_session(&self, session: Option<Session>) {
+ if let Some(session) = session {
+ self.imp().session.set(session.downgrade()).unwrap();
+ }
+ }
+
+ fn validate_password(&self) {
+ let entry = &self.imp().password;
+ let password = entry.text();
+
+ if password.is_empty() {
+ entry.set_hint("");
+ entry.remove_css_class("success");
+ entry.remove_css_class("warning");
+ entry.set_progress_value(0.0);
+ self.update_button();
+ return;
+ }
+
+ let validity = validate_password(&password);
+
+ entry.set_progress_value(validity.progress as f64 / 20.0);
+ if validity.progress == 100 {
+ entry.set_hint("");
+ entry.add_css_class("success");
+ entry.remove_css_class("warning");
+ } else {
+ entry.remove_css_class("success");
+ entry.add_css_class("warning");
+ if !validity.has_length {
+ entry.set_hint(&gettext("Password must be at least 8 characters long"));
+ } else if !validity.has_lowercase {
+ entry.set_hint(&gettext(
+ "Password must have at least one lower-case letter",
+ ));
+ } else if !validity.has_uppercase {
+ entry.set_hint(&gettext(
+ "Password must have at least one upper-case letter",
+ ));
+ } else if !validity.has_number {
+ entry.set_hint(&gettext("Password must have at least one digit"));
+ } else if !validity.has_symbol {
+ entry.set_hint(&gettext("Password must have at least one symbol"));
+ }
+ }
+ self.update_button();
+ }
+
+ fn validate_password_confirmation(&self) {
+ let priv_ = self.imp();
+ let entry = &priv_.confirm_password;
+ let password = priv_.password.text();
+ let confirmation = entry.text();
+
+ if confirmation.is_empty() {
+ entry.set_hint("");
+ entry.remove_css_class("success");
+ entry.remove_css_class("warning");
+ return;
+ }
+
+ if password == confirmation {
+ entry.set_hint("");
+ entry.add_css_class("success");
+ entry.remove_css_class("warning");
+ } else {
+ entry.remove_css_class("success");
+ entry.add_css_class("warning");
+ entry.set_hint(&gettext("Passwords do not match"));
+ }
+ self.update_button();
+ }
+
+ fn update_button(&self) {
+ self.imp().button.set_sensitive(self.can_change_password());
+ }
+
+ fn can_change_password(&self) -> bool {
+ let priv_ = self.imp();
+ let password = priv_.password.text();
+ let confirmation = priv_.confirm_password.text();
+
+ validate_password(&password).progress == 100 && password == confirmation
+ }
+
+ async fn change_password(&self) {
+ if !self.can_change_password() {
+ return;
+ }
+
+ let priv_ = self.imp();
+ let password = priv_.password.text();
+
+ priv_.button.set_loading(true);
+ priv_.password.set_entry_sensitive(false);
+ priv_.confirm_password.set_entry_sensitive(false);
+
+ let session = self.session().unwrap();
+ let dialog = AuthDialog::new(
+ self.root()
+ .as_ref()
+ .and_then(|root| root.downcast_ref::<gtk::Window>()),
+ &session,
+ );
+
+ let result = dialog
+ .authenticate(move |client, auth_data| {
+ let password = password.clone();
+ async move {
+ if let Some(auth) = auth_data {
+ let auth = Some(auth.as_matrix_auth_data());
+ let request = assign!(change_password::Request::new(&password), { auth });
+ client.send(request, None).await.map_err(Into::into)
+ } else {
+ let request = change_password::Request::new(&password);
+ client.send(request, None).await.map_err(Into::into)
+ }
+ }
+ })
+ .await;
+
+ match result {
+ Ok(_) => {
+ let _ = self.activate_action(
+ "win.add-toast",
+ Some(&gettext("Password changed successfully").to_variant()),
+ );
+ priv_.password.set_text("");
+ priv_.confirm_password.set_text("");
+ self.activate_action("win.close-subpage", None).unwrap();
+ }
+ Err(err) => match err {
+ AuthError::UserCancelled => {}
+ AuthError::ServerResponse(error)
+ if matches!(error.as_ref(), MatrixError::Http(HttpError::ClientApi(
+ FromHttpResponseError::Http(ServerError::Known(error)),
+ )) if error.kind.as_ref() == "M_WEAK_PASSWORD") =>
+ {
+ error!("Weak password: {:?}", error);
+ let _ = self.activate_action(
+ "win.add-toast",
+ Some(&gettext("Password rejected for being too weak").to_variant()),
+ );
+ }
+ _ => {
+ error!("Failed to change the password: {:?}", err);
+ let _ = self.activate_action(
+ "win.add-toast",
+ Some(&gettext("Could not change password").to_variant()),
+ );
+ }
+ },
+ }
+ priv_.button.set_loading(false);
+ priv_.password.set_entry_sensitive(true);
+ priv_.confirm_password.set_entry_sensitive(true);
+ }
+}
diff --git a/src/session/account_settings/user_page/deactivate_account_subpage.rs
b/src/session/account_settings/user_page/deactivate_account_subpage.rs
new file mode 100644
index 000000000..4294468a2
--- /dev/null
+++ b/src/session/account_settings/user_page/deactivate_account_subpage.rs
@@ -0,0 +1,217 @@
+use adw::{prelude::*, subclass::prelude::*};
+use gettextrs::gettext;
+use gtk::{
+ glib::{self, clone},
+ subclass::prelude::*,
+ CompositeTemplate,
+};
+use log::error;
+use matrix_sdk::ruma::{api::client::r0::account::deactivate, assign};
+
+use crate::{
+ components::{AuthDialog, EntryRow, SpinnerButton, Toast},
+ session::{Session, UserExt},
+ spawn,
+};
+
+mod imp {
+ use glib::{subclass::InitializingObject, WeakRef};
+ use once_cell::unsync::OnceCell;
+
+ use super::*;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/account-settings-deactivate-account-subpage.ui")]
+ pub struct DeactivateAccountSubpage {
+ pub session: OnceCell<WeakRef<Session>>,
+ #[template_child]
+ pub confirmation: TemplateChild<EntryRow>,
+ #[template_child]
+ pub button: TemplateChild<SpinnerButton>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for DeactivateAccountSubpage {
+ const NAME: &'static str = "DeactivateAccountSubpage";
+ type Type = super::DeactivateAccountSubpage;
+ type ParentType = gtk::Box;
+
+ fn class_init(klass: &mut Self::Class) {
+ EntryRow::static_type();
+ Self::bind_template(klass);
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for DeactivateAccountSubpage {
+ fn properties() -> &'static [glib::ParamSpec] {
+ use once_cell::sync::Lazy;
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpecObject::new(
+ "session",
+ "Session",
+ "The session",
+ Session::static_type(),
+ glib::ParamFlags::READWRITE,
+ )]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "session" => obj.set_session(value.get().unwrap()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "session" => obj.session().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+
+ self.confirmation
+ .connect_activated(clone!(@weak obj => move|_| {
+ spawn!(
+ clone!(@weak obj => async move {
+ obj.deactivate_account().await;
+ })
+ );
+ }));
+ self.confirmation
+ .connect_changed(clone!(@weak obj => move|_| {
+ obj.update_button();
+ }));
+
+ self.button.connect_clicked(clone!(@weak obj => move|_| {
+ spawn!(
+ clone!(@weak obj => async move {
+ obj.deactivate_account().await;
+ })
+ );
+ }));
+ }
+ }
+
+ impl WidgetImpl for DeactivateAccountSubpage {}
+ impl BoxImpl for DeactivateAccountSubpage {}
+}
+
+glib::wrapper! {
+ /// Account settings page about the user and the session.
+ pub struct DeactivateAccountSubpage(ObjectSubclass<imp::DeactivateAccountSubpage>)
+ @extends gtk::Widget, gtk::Box, @implements gtk::Accessible;
+}
+
+impl DeactivateAccountSubpage {
+ pub fn new(session: &Session) -> Self {
+ glib::Object::new(&[("session", session)])
+ .expect("Failed to create DeactivateAccountSubpage")
+ }
+
+ pub fn session(&self) -> Option<Session> {
+ self.imp()
+ .session
+ .get()
+ .and_then(|session| session.upgrade())
+ }
+
+ pub fn set_session(&self, session: Option<Session>) {
+ if let Some(session) = session {
+ let priv_ = self.imp();
+ priv_.session.set(session.downgrade()).unwrap();
+ priv_
+ .confirmation
+ .set_placeholder_text(Some(&self.user_id()));
+ }
+ }
+
+ fn user_id(&self) -> String {
+ self.session()
+ .as_ref()
+ .and_then(|session| session.user())
+ .unwrap()
+ .user_id()
+ .to_string()
+ }
+
+ fn update_button(&self) {
+ self.imp()
+ .button
+ .set_sensitive(self.can_deactivate_account());
+ }
+
+ fn can_deactivate_account(&self) -> bool {
+ let confirmation = self.imp().confirmation.text();
+ confirmation == self.user_id()
+ }
+
+ async fn deactivate_account(&self) {
+ if !self.can_deactivate_account() {
+ return;
+ }
+
+ let priv_ = self.imp();
+ priv_.button.set_loading(true);
+ priv_.confirmation.set_sensitive(false);
+
+ let session = self.session().unwrap();
+ let dialog = AuthDialog::new(
+ self.root()
+ .as_ref()
+ .and_then(|root| root.downcast_ref::<gtk::Window>()),
+ &session,
+ );
+
+ let result = dialog
+ .authenticate(move |client, auth_data| async move {
+ if let Some(auth) = auth_data {
+ let auth = Some(auth.as_matrix_auth_data());
+ let request = assign!(deactivate::Request::new(), { auth });
+ client.send(request, None).await.map_err(Into::into)
+ } else {
+ let request = deactivate::Request::new();
+ client.send(request, None).await.map_err(Into::into)
+ }
+ })
+ .await;
+
+ match result {
+ Ok(_) => {
+ if let Some(session) = self.session() {
+ session
+ .parent_window()
+ .unwrap()
+ .add_toast(&Toast::new(&gettext("Account successfully deactivated")));
+ session.handle_logged_out();
+ }
+ self.activate_action("account-settings.close", None)
+ .unwrap();
+ }
+ Err(err) => {
+ error!("Failed to deactivate account: {:?}", err);
+ let _ = self.activate_action(
+ "win.add-toast",
+ Some(&gettext("Could not deactivate account").to_variant()),
+ );
+ }
+ }
+ priv_.button.set_loading(false);
+ priv_.confirmation.set_sensitive(true);
+ }
+}
diff --git a/src/session/account_settings/user_page/mod.rs b/src/session/account_settings/user_page/mod.rs
new file mode 100644
index 000000000..38be6f849
--- /dev/null
+++ b/src/session/account_settings/user_page/mod.rs
@@ -0,0 +1,482 @@
+use std::{fs::File, time::Duration};
+
+use adw::{prelude::*, subclass::prelude::*};
+use gettextrs::gettext;
+use gtk::{
+ gio,
+ glib::{self, clone},
+ subclass::prelude::*,
+ CompositeTemplate,
+};
+use log::error;
+use matrix_sdk::ruma::{api::client::r0::capabilities::get_capabilities, MxcUri};
+
+mod change_password_subpage;
+mod deactivate_account_subpage;
+
+use change_password_subpage::ChangePasswordSubpage;
+use deactivate_account_subpage::DeactivateAccountSubpage;
+
+use crate::{
+ components::{ActionState, ButtonRow, EditableAvatar, EntryRow},
+ session::{Session, User, UserExt},
+ spawn, spawn_tokio,
+ utils::TemplateCallbacks,
+};
+
+mod imp {
+ use std::cell::{Cell, RefCell};
+
+ use glib::{subclass::InitializingObject, WeakRef};
+
+ use super::*;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/account-settings-user-page.ui")]
+ pub struct UserPage {
+ pub session: RefCell<Option<WeakRef<Session>>>,
+ #[template_child]
+ pub avatar: TemplateChild<EditableAvatar>,
+ #[template_child]
+ pub display_name: TemplateChild<EntryRow>,
+ #[template_child]
+ pub change_password_group: TemplateChild<adw::PreferencesGroup>,
+ #[template_child]
+ pub change_password_subpage: TemplateChild<ChangePasswordSubpage>,
+ #[template_child]
+ pub homeserver: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub user_id: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub session_id: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub deactivate_account_subpage: TemplateChild<DeactivateAccountSubpage>,
+ pub changing_avatar_to: RefCell<Option<Box<MxcUri>>>,
+ pub removing_avatar: Cell<bool>,
+ pub changing_display_name_to: RefCell<Option<String>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for UserPage {
+ const NAME: &'static str = "UserPage";
+ type Type = super::UserPage;
+ type ParentType = adw::PreferencesPage;
+
+ fn class_init(klass: &mut Self::Class) {
+ EditableAvatar::static_type();
+ EntryRow::static_type();
+ ButtonRow::static_type();
+ ChangePasswordSubpage::static_type();
+ DeactivateAccountSubpage::static_type();
+ Self::bind_template(klass);
+ Self::Type::bind_template_callbacks(klass);
+ TemplateCallbacks::bind_template_callbacks(klass);
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for UserPage {
+ fn properties() -> &'static [glib::ParamSpec] {
+ use once_cell::sync::Lazy;
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpecObject::new(
+ "session",
+ "Session",
+ "The session",
+ Session::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ )]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "session" => obj.set_session(value.get().unwrap()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "session" => obj.session().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+
+ obj.init_avatar();
+ obj.init_display_name();
+ obj.init_change_password();
+ }
+ }
+
+ impl WidgetImpl for UserPage {}
+ impl PreferencesPageImpl for UserPage {}
+}
+
+glib::wrapper! {
+ /// Account settings page about the user and the session.
+ pub struct UserPage(ObjectSubclass<imp::UserPage>)
+ @extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible;
+}
+
+#[gtk::template_callbacks]
+impl UserPage {
+ pub fn new(parent_window: &Option<gtk::Window>, session: &Session) -> Self {
+ glib::Object::new(&[("transient-for", parent_window), ("session", session)])
+ .expect("Failed to create UserPage")
+ }
+
+ pub fn session(&self) -> Option<Session> {
+ self.imp()
+ .session
+ .borrow()
+ .clone()
+ .and_then(|session| session.upgrade())
+ }
+
+ pub fn set_session(&self, session: Option<Session>) {
+ if self.session() == session {
+ return;
+ }
+ self.imp()
+ .session
+ .replace(session.map(|session| session.downgrade()));
+ self.notify("session");
+
+ self.user().avatar().connect_notify_local(
+ Some("url"),
+ clone!(@weak self as obj => move |avatar, _| {
+ obj.avatar_changed(avatar.url().as_deref());
+ }),
+ );
+ self.user().connect_notify_local(
+ Some("display-name"),
+ clone!(@weak self as obj => move |user, _| {
+ obj.display_name_changed(&user.display_name());
+ }),
+ );
+
+ spawn!(
+ glib::PRIORITY_LOW,
+ clone!(@weak self as obj => async move {
+ let priv_ = obj.imp();
+ let client = obj.session().unwrap().client();
+
+ let homeserver = client.homeserver().await;
+ priv_.homeserver.set_label(homeserver.as_ref());
+
+ let user_id = client.user_id().await.unwrap();
+ priv_.user_id.set_label(user_id.as_ref());
+
+ let session_id = client.device_id().await.unwrap();
+ priv_.session_id.set_label(session_id.as_ref());
+ })
+ );
+ }
+
+ fn user(&self) -> User {
+ self.session()
+ .as_ref()
+ .and_then(|session| session.user())
+ .unwrap()
+ .to_owned()
+ }
+
+ fn init_avatar(&self) {
+ let avatar = &self.imp().avatar;
+ avatar.connect_edit_avatar(clone!(@weak self as obj => move |_, file| {
+ spawn!(
+ clone!(@weak obj => async move {
+ obj.change_avatar(file).await;
+ })
+ );
+ }));
+ avatar.connect_remove_avatar(clone!(@weak self as obj => move |_| {
+ spawn!(
+ clone!(@weak obj => async move {
+ obj.remove_avatar().await;
+ })
+ );
+ }));
+ }
+
+ fn avatar_changed(&self, uri: Option<&MxcUri>) {
+ let priv_ = self.imp();
+ let avatar = &*priv_.avatar;
+ if uri.is_none() && priv_.removing_avatar.get() {
+ priv_.removing_avatar.set(false);
+ avatar.show_temp_image(false);
+ avatar.set_remove_state(ActionState::Success);
+ avatar.set_edit_sensitive(true);
+ let _ = self.activate_action(
+ "win.add-toast",
+ Some(&gettext("Avatar removed successfully").to_variant()),
+ );
+ glib::timeout_add_local_once(
+ Duration::from_secs(2),
+ clone!(@weak avatar => move || {
+ avatar.set_remove_state(ActionState::Default);
+ }),
+ );
+ } else if uri.is_some() {
+ let to_uri = priv_.changing_avatar_to.borrow().clone();
+ if to_uri.as_deref() == uri {
+ priv_.changing_avatar_to.take();
+ avatar.set_edit_state(ActionState::Success);
+ avatar.show_temp_image(false);
+ avatar.set_temp_image_from_file(None);
+ avatar.set_remove_sensitive(true);
+ let _ = self.activate_action(
+ "win.add-toast",
+ Some(&gettext("Avatar changed successfully").to_variant()),
+ );
+ glib::timeout_add_local_once(
+ Duration::from_secs(2),
+ clone!(@weak avatar => move || {
+ avatar.set_edit_state(ActionState::Default);
+ }),
+ );
+ }
+ }
+ }
+
+ async fn change_avatar(&self, file: gio::File) {
+ let priv_ = self.imp();
+ let avatar = &priv_.avatar;
+ avatar.set_temp_image_from_file(Some(&file));
+ avatar.show_temp_image(true);
+ avatar.set_edit_state(ActionState::Loading);
+ avatar.set_remove_sensitive(false);
+
+ let client = self.session().unwrap().client();
+ let mime = file
+ .query_info_future(
+ &gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
+ gio::FileQueryInfoFlags::NONE,
+ glib::PRIORITY_LOW,
+ )
+ .await
+ .ok()
+ .and_then(|info| info.content_type())
+ .and_then(|content_type| gio::content_type_get_mime_type(&content_type))
+ .unwrap();
+ let mut file = File::open(file.path().unwrap()).unwrap();
+
+ let client_clone = client.clone();
+ let handle =
+ spawn_tokio!(
+ async move { client_clone.upload(&mime.parse().unwrap(), &mut file).await }
+ );
+
+ let uri = match handle.await.unwrap() {
+ Ok(res) => res.content_uri,
+ Err(error) => {
+ error!("Could not upload user avatar: {}", error);
+ let _ = self.activate_action(
+ "win.add-toast",
+ Some(&gettext("Could not upload avatar").to_variant()),
+ );
+ avatar.show_temp_image(false);
+ avatar.set_temp_image_from_file(None);
+ avatar.set_edit_state(ActionState::Default);
+ avatar.set_remove_sensitive(true);
+ return;
+ }
+ };
+
+ priv_.changing_avatar_to.replace(Some(uri.clone()));
+ let handle = spawn_tokio!(async move { client.account().set_avatar_url(Some(&uri)).await });
+
+ match handle.await.unwrap() {
+ Ok(_) => {
+ let to_uri = priv_.changing_avatar_to.borrow().clone();
+ if let Some(avatar) = to_uri {
+ self.user().set_avatar_url(Some(avatar))
+ }
+ }
+ Err(error) => {
+ if priv_.changing_avatar_to.take().is_some() {
+ error!("Could not change user avatar: {}", error);
+ let _ = self.activate_action(
+ "win.add-toast",
+ Some(&gettext("Could not change avatar").to_variant()),
+ );
+ avatar.show_temp_image(false);
+ avatar.set_temp_image_from_file(None);
+ avatar.set_edit_state(ActionState::Default);
+ avatar.set_remove_sensitive(true);
+ }
+ }
+ }
+ }
+
+ async fn remove_avatar(&self) {
+ let priv_ = self.imp();
+ let avatar = &*priv_.avatar;
+ avatar.show_temp_image(true);
+ avatar.set_remove_state(ActionState::Loading);
+ avatar.set_edit_sensitive(false);
+
+ let client = self.session().unwrap().client();
+ let handle = spawn_tokio!(async move { client.account().set_avatar_url(None).await });
+ priv_.removing_avatar.set(true);
+
+ match handle.await.unwrap() {
+ Ok(_) => {
+ self.user().set_avatar_url(None);
+ }
+ Err(error) => {
+ if priv_.removing_avatar.get() {
+ priv_.removing_avatar.set(false);
+ error!("Couldn’t remove user avatar: {}", error);
+ let _ = self.activate_action(
+ "win.add-toast",
+ Some(&gettext("Could not remove avatar").to_variant()),
+ );
+ avatar.show_temp_image(false);
+ avatar.set_remove_state(ActionState::Default);
+ avatar.set_edit_sensitive(true);
+ }
+ }
+ }
+ }
+
+ fn init_display_name(&self) {
+ let entry = &*self.imp().display_name;
+ entry.connect_focused(clone!(@weak self as obj => move|entry, focused| {
+ if entry.entry_sensitive() {
+ if focused {
+ entry.set_action_state(ActionState::Confirm);
+ } else if entry.text() == obj.user().display_name() {
+ entry.set_action_state(ActionState::Default);
+ }
+ }
+ }));
+ entry.connect_activated(clone!(@weak self as obj => move|_| {
+ spawn!(
+ clone!(@weak obj => async move {
+ obj.change_display_name().await;
+ })
+ );
+ }));
+ entry.connect_cancel(clone!(@weak self as obj => move|entry| {
+ entry.set_text(&obj.user().display_name());
+ }));
+ }
+
+ fn display_name_changed(&self, name: &str) {
+ let priv_ = self.imp();
+ let entry = &*priv_.display_name;
+
+ let to_display_name = priv_
+ .changing_display_name_to
+ .borrow()
+ .clone()
+ .unwrap_or_default();
+ if to_display_name == name {
+ priv_.changing_display_name_to.take();
+ entry.remove_css_class("error");
+ entry.set_action_state(ActionState::Success);
+ entry.set_entry_sensitive(true);
+ let _ = self.activate_action(
+ "win.add-toast",
+ Some(&gettext("Name changed successfully").to_variant()),
+ );
+ glib::timeout_add_local_once(
+ Duration::from_secs(2),
+ clone!(@weak entry => move || {
+ entry.set_action_state(ActionState::Default);
+ }),
+ );
+ }
+ }
+
+ async fn change_display_name(&self) {
+ let priv_ = self.imp();
+ let entry = &*priv_.display_name;
+ entry.set_action_state(ActionState::Loading);
+ entry.set_entry_sensitive(false);
+
+ let display_name = entry.text();
+ priv_
+ .changing_display_name_to
+ .replace(Some(display_name.to_string()));
+
+ let client = self.session().unwrap().client();
+ let handle =
+ spawn_tokio!(
+ async move { client.account().set_display_name(Some(&display_name)).await }
+ );
+
+ match handle.await.unwrap() {
+ Ok(_) => {
+ let to_display_name = priv_.changing_display_name_to.borrow().clone();
+ if let Some(display_name) = to_display_name {
+ self.user().set_display_name(Some(display_name));
+ }
+ }
+ Err(err) => {
+ error!("Couldn’t change user display name: {}", err);
+ let _ = self.activate_action(
+ "win.add-toast",
+ Some(&gettext("Could not change display name").to_variant()),
+ );
+ entry.set_action_state(ActionState::Retry);
+ entry.add_css_class("error");
+ entry.set_entry_sensitive(true);
+ }
+ }
+ }
+
+ fn init_change_password(&self) {
+ spawn!(
+ glib::PRIORITY_LOW,
+ clone!(@weak self as obj => async move {
+ let client = obj.session().unwrap().client();
+
+ // Check whether the user can change their password.
+ let handle = spawn_tokio!(async move {
+ client.send(get_capabilities::Request::new(), None).await
+ });
+ match handle.await.unwrap() {
+ Ok(res) => {
+
obj.imp().change_password_group.set_visible(res.capabilities.change_password.enabled);
+ }
+ Err(error) => error!("Could not get server capabilities: {}", error),
+ }
+ })
+ );
+ }
+
+ #[template_callback]
+ fn show_change_password(&self) {
+ self.root()
+ .as_ref()
+ .and_then(|root| root.downcast_ref::<adw::PreferencesWindow>())
+ .unwrap()
+ .present_subpage(&*self.imp().change_password_subpage);
+ }
+
+ #[template_callback]
+ fn show_deactivate_account(&self) {
+ self.root()
+ .as_ref()
+ .and_then(|root| root.downcast_ref::<adw::PreferencesWindow>())
+ .unwrap()
+ .present_subpage(&*self.imp().deactivate_account_subpage);
+ }
+}
diff --git a/src/session/mod.rs b/src/session/mod.rs
index e9b1505cd..3665442c9 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -40,7 +40,7 @@ use matrix_sdk::{
assign,
identifiers::RoomId,
},
- Client, Error as MatrixError, HttpError,
+ Client, HttpError,
};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use tokio::task::JoinHandle;
@@ -394,28 +394,12 @@ impl Session {
let priv_ = self.imp();
let error = match result {
Ok((client, session)) => {
- priv_.client.replace(Some(client.clone()));
+ priv_.client.replace(Some(client));
let user = User::new(self, &session.user_id);
- priv_.user.set(user.clone()).unwrap();
+ priv_.user.set(user).unwrap();
self.notify("user");
- let handle = spawn_tokio!(async move {
- let account = client.account();
- let display_name = account.get_display_name().await?;
- let avatar_url = account.get_avatar_url().await?;
- let result: Result<_, MatrixError> = Ok((display_name, avatar_url));
- result
- });
-
- spawn!(glib::PRIORITY_LOW, async move {
- match handle.await.unwrap() {
- Ok((display_name, avatar_url)) => {
- user.set_display_name(display_name);
- user.set_avatar_url(avatar_url);
- }
- Err(error) => error!("Couldn’t fetch account metadata: {}", error),
- }
- });
+ self.update_user_profile();
let res = if store_session {
match secret::store_session(&session) {
@@ -567,10 +551,31 @@ impl Session {
.get_or_init(|| ItemList::new(&RoomList::new(self), &VerificationList::new(self)))
}
+ /// The user of this session.
pub fn user(&self) -> Option<&User> {
self.imp().user.get()
}
+ /// Update the profile of this session’s user.
+ ///
+ /// Fetches the updated profile and updates the local data.
+ pub fn update_user_profile(&self) {
+ let client = self.client();
+ let user = self.user().unwrap().to_owned();
+
+ let handle = spawn_tokio!(async move { client.account().get_profile().await });
+
+ spawn!(glib::PRIORITY_LOW, async move {
+ match handle.await.unwrap() {
+ Ok(res) => {
+ user.set_display_name(res.displayname);
+ user.set_avatar_url(res.avatar_url);
+ }
+ Err(error) => error!("Couldn’t fetch account metadata: {}", error),
+ }
+ });
+ }
+
pub fn client(&self) -> Client {
self.imp()
.client
@@ -652,8 +657,7 @@ impl Session {
))) = error
{
if let ErrorKind::UnknownToken { soft_logout: _ } = error.kind {
- self.emit_by_name::<()>("logged-out", &[]);
- self.cleanup_session();
+ self.handle_logged_out();
}
}
error!("Failed to perform sync: {:?}", error);
@@ -673,10 +677,8 @@ impl Session {
}
fn open_account_settings(&self) {
- if let Some(user) = self.user() {
- let window = AccountSettings::new(self.parent_window().as_ref(), user);
- window.show();
- }
+ let window = AccountSettings::new(self.parent_window().as_ref(), self);
+ window.show();
}
fn show_room_creation_dialog(&self) {
@@ -712,6 +714,15 @@ impl Session {
}
}
+ /// Handle that the session has been logged out.
+ ///
+ /// This should only be called if the session has been logged out without
+ /// `Session::logout`.
+ pub fn handle_logged_out(&self) {
+ self.emit_by_name::<()>("logged-out", &[]);
+ self.cleanup_session();
+ }
+
fn cleanup_session(&self) {
let priv_ = self.imp();
let info = priv_.info.get().unwrap();
diff --git a/src/session/room/member.rs b/src/session/room/member.rs
index 6e9f3394f..af94b8538 100644
--- a/src/session/room/member.rs
+++ b/src/session/room/member.rs
@@ -193,6 +193,13 @@ impl Member {
self.set_display_name(event.display_name());
self.avatar().set_url(event.avatar_url());
self.set_membership((&event.content().membership).into());
+
+ let session = self.session();
+ if let Some(user) = session.user() {
+ if user.user_id() == self.user_id() {
+ session.update_user_profile();
+ }
+ }
}
}
diff --git a/src/utils.rs b/src/utils.rs
index 4cafc6753..96128a6e7 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -244,4 +244,78 @@ impl TemplateCallbacks {
fn string_not_empty(string: Option<&str>) -> bool {
!string.unwrap_or_default().is_empty()
}
+
+ #[template_callback]
+ fn object_is_some(obj: Option<glib::Object>) -> bool {
+ obj.is_some()
+ }
+}
+
+/// The result of a password validation.
+#[derive(Debug, Default, Clone, Copy)]
+pub struct PasswordValidity {
+ /// Whether the password includes at least one lowercase letter.
+ pub has_lowercase: bool,
+ /// Whether the password includes at least one uppercase letter.
+ pub has_uppercase: bool,
+ /// Whether the password includes at least one number.
+ pub has_number: bool,
+ /// Whether the password includes at least one symbol.
+ pub has_symbol: bool,
+ /// Whether the password is at least 8 characters long.
+ pub has_length: bool,
+ /// The percentage of checks passed for the password, between 0 and 100.
+ ///
+ /// If progress is 100, the password is valid.
+ pub progress: u32,
+}
+
+impl PasswordValidity {
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+
+/// Validate a password according to the Matrix specification.
+///
+/// A password should include a lower-case letter, an upper-case letter, a
+/// number and a symbol and be at a minimum 8 characters in length.
+///
+/// See: https://spec.matrix.org/v1.1/client-server-api/#notes-on-password-management
+pub fn validate_password(password: &str) -> PasswordValidity {
+ let mut validity = PasswordValidity::new();
+
+ for char in password.chars() {
+ if char.is_numeric() {
+ validity.has_number = true;
+ } else if char.is_lowercase() {
+ validity.has_lowercase = true;
+ } else if char.is_uppercase() {
+ validity.has_uppercase = true;
+ } else {
+ validity.has_symbol = true;
+ }
+ }
+
+ validity.has_length = password.len() >= 8;
+
+ let mut passed = 0;
+ if validity.has_number {
+ passed += 1;
+ }
+ if validity.has_lowercase {
+ passed += 1;
+ }
+ if validity.has_uppercase {
+ passed += 1;
+ }
+ if validity.has_symbol {
+ passed += 1;
+ }
+ if validity.has_length {
+ passed += 1;
+ }
+ validity.progress = passed * 100 / 5;
+
+ validity
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]