Browse Source

account-settings: Add General tab

merge-requests/1327/merge
Kévin Commaille 4 years ago
parent
commit
5c8c627cec
No known key found for this signature in database
GPG Key ID: DD507DAE96E8245C
  1. 3
      data/resources/resources.gresource.xml
  2. 32
      data/resources/style.css
  3. 103
      data/resources/ui/account-settings-change-password-subpage.ui
  4. 101
      data/resources/ui/account-settings-deactivate-account-subpage.ui
  5. 129
      data/resources/ui/account-settings-user-page.ui
  6. 13
      data/resources/ui/account-settings.ui
  7. 6
      po/POTFILES.in
  8. 6
      src/components/editable_avatar.rs
  9. 64
      src/session/account_settings/mod.rs
  10. 344
      src/session/account_settings/user_page/change_password_subpage.rs
  11. 217
      src/session/account_settings/user_page/deactivate_account_subpage.rs
  12. 482
      src/session/account_settings/user_page/mod.rs
  13. 63
      src/session/mod.rs
  14. 7
      src/session/room/member.rs
  15. 74
      src/utils.rs

3
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>

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

103
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>

101
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>

129
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>

13
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>

6
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

6
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()),
);
}

64
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");
}
}

344
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(&[
&gtk::LEVEL_BAR_OFFSET_LOW,
"step2",
"step3",
&gtk::LEVEL_BAR_OFFSET_HIGH,
&gtk::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);
}
}

217
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);
}
}

482
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);
}
}

63
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();

7
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();
}
}
}
}

74
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
}

Loading…
Cancel
Save