[fractal/fractal-next] account-settings: Add General tab



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(&[
+                &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);
+    }
+}
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]