From 4b846fd9d7d492f09bc3c471999aa883d85ae4f4 Mon Sep 17 00:00:00 2001 From: Titouan Real Date: Fri, 28 Feb 2025 19:55:33 +0100 Subject: [PATCH] user-sessions-page: Redesign --- po/POTFILES.in | 4 +- src/session/model/user_sessions_list/mod.rs | 18 +- .../user_sessions_list/other_sessions_list.rs | 10 + .../model/user_sessions_list/user_session.rs | 181 ++++++++++- src/session/view/account_settings/mod.rs | 56 +++- src/session/view/account_settings/mod.ui | 1 - .../user_sessions_page/mod.rs | 84 ++--- .../user_sessions_page/mod.ui | 4 +- .../user_sessions_page/user_session_row.rs | 302 +----------------- .../user_sessions_page/user_session_row.ui | 179 +++++------ .../user_session_subpage.rs | 227 +++++++++++++ .../user_session_subpage.ui | 139 ++++++++ src/ui-resources.gresource.xml | 1 + 13 files changed, 749 insertions(+), 457 deletions(-) create mode 100644 src/session/view/account_settings/user_sessions_page/user_session_subpage.rs create mode 100644 src/session/view/account_settings/user_sessions_page/user_session_subpage.ui diff --git a/po/POTFILES.in b/po/POTFILES.in index bc1ea274..5f4f534e 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -79,6 +79,7 @@ src/session/model/room/permissions.rs src/session/model/room_list/mod.rs src/session/model/sidebar_data/section/name.rs src/session/model/sidebar_data/icon_item.rs +src/session/model/user_sessions_list/user_session.rs src/session/view/account_settings/general_page/change_password_subpage.rs src/session/view/account_settings/general_page/change_password_subpage.ui src/session/view/account_settings/general_page/deactivate_account_subpage.rs @@ -98,8 +99,9 @@ src/session/view/account_settings/security_page/import_export_keys_subpage.ui src/session/view/account_settings/security_page/mod.rs src/session/view/account_settings/security_page/mod.ui src/session/view/account_settings/user_sessions_page/mod.ui -src/session/view/account_settings/user_sessions_page/user_session_row.rs src/session/view/account_settings/user_sessions_page/user_session_row.ui +src/session/view/account_settings/user_sessions_page/user_session_subpage.rs +src/session/view/account_settings/user_sessions_page/user_session_subpage.ui src/session/view/content/explore/mod.ui src/session/view/content/explore/public_room_row.rs src/session/view/content/explore/servers_popover.ui diff --git a/src/session/model/user_sessions_list/mod.rs b/src/session/model/user_sessions_list/mod.rs index d91ea6fd..4936c5bd 100644 --- a/src/session/model/user_sessions_list/mod.rs +++ b/src/session/model/user_sessions_list/mod.rs @@ -1,7 +1,7 @@ use futures_util::StreamExt; use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*}; use matrix_sdk::encryption::identities::UserDevices; -use ruma::OwnedUserId; +use ruma::{OwnedDeviceId, OwnedUserId}; use tokio::task::AbortHandle; use tracing::error; @@ -259,6 +259,17 @@ mod imp { self.set_loading_state(LoadingState::Ready); } + /// Find the user session with the given device ID, if any. + pub(super) fn get(&self, device_id: &OwnedDeviceId) -> Option { + if let Some(current_session) = self.current_session.borrow().as_ref() { + if current_session.device_id() == device_id { + return Some(current_session.clone()); + } + } + + self.other_sessions.get(device_id) + } + /// Set the loading state of the list. fn set_loading_state(&self, loading_state: LoadingState) { if self.loading_state.get() == loading_state { @@ -296,6 +307,11 @@ impl UserSessionsList { pub(crate) async fn load(&self) { self.imp().load().await; } + + /// Find the user session with the given device ID, if any. + pub(crate) fn get(&self, device_id: &OwnedDeviceId) -> Option { + self.imp().get(device_id) + } } impl Default for UserSessionsList { diff --git a/src/session/model/user_sessions_list/other_sessions_list.rs b/src/session/model/user_sessions_list/other_sessions_list.rs index df8f1f93..b31f97a4 100644 --- a/src/session/model/user_sessions_list/other_sessions_list.rs +++ b/src/session/model/user_sessions_list/other_sessions_list.rs @@ -105,6 +105,11 @@ mod imp { session.emit_disconnected(); } } + + /// Find the user session with the given device ID, if any. + pub(super) fn get(&self, device_id: &OwnedDeviceId) -> Option { + self.map.borrow().get(device_id).cloned() + } } } @@ -123,6 +128,11 @@ impl OtherSessionsList { pub(super) fn update(&self, session: &Session, data_list: Vec) { self.imp().update(session, data_list); } + + /// Find the user session with the given device ID, if any. + pub(super) fn get(&self, device_id: &OwnedDeviceId) -> Option { + self.imp().get(device_id) + } } impl Default for OtherSessionsList { diff --git a/src/session/model/user_sessions_list/user_session.rs b/src/session/model/user_sessions_list/user_session.rs index 3448dc92..382985c7 100644 --- a/src/session/model/user_sessions_list/user_session.rs +++ b/src/session/model/user_sessions_list/user_session.rs @@ -1,4 +1,10 @@ -use gtk::{glib, glib::closure_local, prelude::*, subclass::prelude::*}; +use gettextrs::gettext; +use gtk::{ + glib, + glib::{clone, closure_local}, + prelude::*, + subclass::prelude::*, +}; use matrix_sdk::encryption::identities::Device as CryptoDevice; use ruma::{api::client::device::Device as DeviceData, DeviceId, OwnedDeviceId}; use tracing::{debug, error}; @@ -7,7 +13,9 @@ use crate::{ components::{AuthDialog, AuthError}, prelude::*, session::model::Session, + system_settings::ClockFormat, utils::matrix::timestamp_to_date, + Application, }; /// The possible sources of the user data. @@ -77,9 +85,12 @@ mod imp { /// The ID of the user session, as a string. #[property(get = Self::device_id_string)] device_id_string: PhantomData, - /// The display name of the user session. + /// The display name of the device. #[property(get = Self::display_name)] display_name: PhantomData, + /// The display name of the device, or the device id as a fallback. + #[property(get = Self::display_name_or_device_id)] + display_name_or_device_id: PhantomData, /// The last IP address used by the user session. #[property(get = Self::last_seen_ip)] last_seen_ip: PhantomData>, @@ -90,9 +101,13 @@ mod imp { /// The last time the user session was used, as a `GDateTime`. #[property(get = Self::last_seen_datetime)] last_seen_datetime: PhantomData>, + /// The last time the user session was used, as a formatted string. + #[property(get = Self::last_seen_datetime_string)] + last_seen_datetime_string: PhantomData>, /// Whether this user session is verified. #[property(get = Self::verified)] verified: PhantomData, + system_settings_handler: RefCell>, } #[glib::object_subclass] @@ -103,6 +118,28 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for UserSession { + fn constructed(&self) { + self.parent_constructed(); + + let obj = self.obj(); + let system_settings = Application::default().system_settings(); + let system_settings_handler = system_settings.connect_clock_format_notify(clone!( + #[weak] + obj, + move |_| { + obj.notify_last_seen_datetime_string(); + } + )); + self.system_settings_handler + .replace(Some(system_settings_handler)); + } + + fn dispose(&self) { + if let Some(handler) = self.system_settings_handler.take() { + Application::default().system_settings().disconnect(handler); + } + } + fn signals() -> &'static [Signal] { static SIGNALS: LazyLock> = LazyLock::new(|| vec![Signal::builder("disconnected").build()]); @@ -140,12 +177,15 @@ mod imp { let obj = self.obj(); if self.display_name() != old_display_name { obj.notify_display_name(); + obj.notify_display_name_or_device_id(); } if self.last_seen_ip() != old_last_seen_ip { obj.notify_last_seen_ip(); } if self.last_seen_ts() != old_last_seen_ts { obj.notify_last_seen_ts(); + obj.notify_last_seen_datetime(); + obj.notify_last_seen_datetime_string(); } if self.verified() != old_verified { obj.notify_verified(); @@ -159,12 +199,24 @@ mod imp { /// The display name of the device. fn display_name(&self) -> String { + self.data + .borrow() + .as_ref() + .and_then(UserSessionData::api) + .and_then(|d| d.display_name.clone()) + .unwrap_or_default() + } + + /// The display name of the device, or the device id as a fallback. + fn display_name_or_device_id(&self) -> String { if let Some(display_name) = self .data .borrow() .as_ref() .and_then(UserSessionData::api) - .and_then(|d| d.display_name.clone()) + .and_then(|d| d.display_name.as_ref().map(|s| s.trim())) + .filter(|s| !s.is_empty()) + .map(ToOwned::to_owned) { display_name } else { @@ -201,6 +253,129 @@ mod imp { .map(timestamp_to_date) } + /// The last time the user session was used, as a `GDateTime`. + pub(super) fn last_seen_datetime_string(&self) -> Option { + let datetime = self.last_seen_datetime()?; + + let clock_format = Application::default().system_settings().clock_format(); + let use_24 = clock_format == ClockFormat::TwentyFourHours; + + // This was ported from Nautilus and simplified for our use case. + // See: https://gitlab.gnome.org/GNOME/nautilus/-/blob/1c5bd3614a35cfbb49de087bc10381cdef5a218f/src/nautilus-file.c#L5001 + let now = glib::DateTime::now_local().unwrap(); + let format; + let days_ago = { + let today_midnight = glib::DateTime::from_local( + now.year(), + now.month(), + now.day_of_month(), + 0, + 0, + 0f64, + ) + .expect("constructing GDateTime works"); + + let date = glib::DateTime::from_local( + datetime.year(), + datetime.month(), + datetime.day_of_month(), + 0, + 0, + 0f64, + ) + .expect("constructing GDateTime works"); + + today_midnight.difference(&date).as_days() + }; + + // Show only the time if date is on today + if days_ago == 0 { + if use_24 { + // Translators: Time in 24h format, i.e. "23:04". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + format = gettext("Last seen at %H:%M"); + } else { + // Translators: Time in 12h format, i.e. "11:04 PM". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + format = gettext("Last seen at %I:%M %p"); + } + } + // Show the word "Yesterday" and time if date is on yesterday + else if days_ago == 1 { + if use_24 { + // Translators: this a time in 24h format, i.e. "Last seen yesterday at 23:04". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen yesterday at %H:%M"); + } else { + // Translators: this is a time in 12h format, i.e. "Last seen Yesterday at 11:04 + // PM". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen yesterday at %I:%M %p"); + } + } + // Show a week day and time if date is in the last week + else if days_ago > 1 && days_ago < 7 { + if use_24 { + // Translators: this is the name of the week day followed by a time in 24h + // format, i.e. "Last seen Monday at 23:04". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen %A at %H:%M"); + } else { + // Translators: this is the week day name followed by a time in 12h format, i.e. + // "Last seen Monday at 11:04 PM". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen %A at %I:%M %p"); + } + } else if datetime.year() == now.year() { + if use_24 { + // Translators: this is the month and day and the time in 24h format, i.e. "Last + // seen February 3 at 23:04". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen %B %-e at %H:%M"); + } else { + // Translators: this is the month and day and the time in 12h format, i.e. "Last + // seen February 3 at 11:04 PM". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen %B %-e at %I:%M %p"); + } + } else if use_24 { + // Translators: this is the full date and the time in 24h format, i.e. "Last + // seen February 3 2015 at 23:04". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen %B %-e %Y at %H:%M"); + } else { + // Translators: this is the full date and the time in 12h format, i.e. "Last + // seen February 3 2015 at 11:04 PM". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen %B %-e %Y at %I:%M %p"); + } + + Some( + datetime + .format(&format) + .expect("formatting GDateTime works") + .into(), + ) + } + /// Whether this device is verified. fn verified(&self) -> bool { self.data diff --git a/src/session/view/account_settings/mod.rs b/src/session/view/account_settings/mod.rs index eb80c750..9a8bf87c 100644 --- a/src/session/view/account_settings/mod.rs +++ b/src/session/view/account_settings/mod.rs @@ -4,6 +4,7 @@ use gtk::{ glib::{clone, closure_local}, CompositeTemplate, }; +use tracing::error; use url::Url; mod general_page; @@ -17,7 +18,7 @@ use self::{ security_page::{ IgnoredUsersSubpage, ImportExportKeysSubpage, ImportExportKeysSubpageMode, SecurityPage, }, - user_sessions_page::UserSessionsPage, + user_sessions_page::{UserSessionSubpage, UserSessionsPage}, }; use crate::{ components::crypto::{CryptoIdentitySetupView, CryptoRecoverySetupView}, @@ -95,6 +96,26 @@ mod imp { }, ); + klass.install_action( + "account-settings.show-session-subpage", + Some(&String::static_variant_type()), + |obj, _, param| { + obj.show_session_subpage( + ¶m + .and_then(glib::Variant::get::) + .expect("The parameter should be a string"), + ); + }, + ); + + klass.install_action_async( + "account-settings.reload-user-sessions", + None, + |obj, _, _| async move { + obj.imp().reload_user_sessions().await; + }, + ); + klass.install_action("account-settings.close", None, |obj, _, _| { obj.close(); }); @@ -145,10 +166,10 @@ mod imp { // Refresh the list of sessions. spawn!(clone!( - #[weak] - session, + #[weak(rename_to = imp)] + self, async move { - session.user_sessions().load().await; + imp.reload_user_sessions().await; } )); @@ -184,6 +205,15 @@ mod imp { pub(super) fn account_management_url(&self) -> Option { self.account_management_url.borrow().clone() } + + /// Reload the sessions from the server. + async fn reload_user_sessions(&self) { + let Some(session) = self.session.obj() else { + return; + }; + + session.user_sessions().load().await; + } } } @@ -276,6 +306,24 @@ impl AccountSettings { self.push_subpage(&page); } + /// Show a subpage with the session details of the given session ID. + pub(crate) fn show_session_subpage(&self, device_id: &str) { + let Some(session) = self.session() else { + return; + }; + + let user_session = session.user_sessions().get(&device_id.into()); + + let Some(user_session) = user_session else { + error!("ID {device_id} is not associated to any device"); + return; + }; + + let page = UserSessionSubpage::new(&user_session, self); + + self.push_subpage(&page); + } + /// Connect to the signal emitted when the account management URL changed. pub fn connect_account_management_url_changed( &self, diff --git a/src/session/view/account_settings/mod.ui b/src/session/view/account_settings/mod.ui index 998b3b1d..38d92f07 100644 --- a/src/session/view/account_settings/mod.ui +++ b/src/session/view/account_settings/mod.ui @@ -30,7 +30,6 @@ AccountSettings - AccountSettings diff --git a/src/session/view/account_settings/user_sessions_page/mod.rs b/src/session/view/account_settings/user_sessions_page/mod.rs index c8b898fa..4dc663ff 100644 --- a/src/session/view/account_settings/user_sessions_page/mod.rs +++ b/src/session/view/account_settings/user_sessions_page/mod.rs @@ -3,8 +3,10 @@ use gtk::{gio, glib, glib::clone, prelude::*, CompositeTemplate}; use tracing::error; mod user_session_row; +mod user_session_subpage; use self::user_session_row::UserSessionRow; +pub use self::user_session_subpage::UserSessionSubpage; use super::AccountSettings; use crate::{ session::model::{UserSession, UserSessionsList}, @@ -34,9 +36,6 @@ mod imp { stack: TemplateChild, #[template_child] other_sessions: TemplateChild, - /// The ancestor [`AccountSettings`]. - #[property(get, set = Self::set_account_settings, explicit_notify, nullable)] - account_settings: glib::WeakRef, /// The list of user sessions. #[property(get, set = Self::set_user_sessions, explicit_notify, nullable)] user_sessions: BoundObject, @@ -52,7 +51,7 @@ mod imp { fn class_init(klass: &mut Self::Class) { Self::bind_template(klass); - Self::Type::bind_template_callbacks(klass); + Self::bind_template_callbacks(klass); } fn instance_init(obj: &InitializingObject) { @@ -62,6 +61,12 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for UserSessionsPage { + fn constructed(&self) { + self.parent_constructed(); + + self.init_other_sessions(); + } + fn dispose(&self) { if let Some(user_sessions) = self.user_sessions.obj() { if let Some(handler) = self.other_sessions_handler.take() { @@ -77,16 +82,8 @@ mod imp { impl WidgetImpl for UserSessionsPage {} impl PreferencesPageImpl for UserSessionsPage {} + #[gtk::template_callbacks] impl UserSessionsPage { - /// Set the ancestor [`AccountSettings`]. - fn set_account_settings(&self, account_settings: Option<&AccountSettings>) { - self.account_settings.set(account_settings); - - if let Some(account_settings) = account_settings { - self.init_other_sessions(account_settings); - } - } - /// Set the list of user sessions. fn set_user_sessions(&self, user_sessions: Option) { let prev_user_sessions = self.user_sessions.obj(); @@ -162,7 +159,7 @@ mod imp { } /// Initialize the list of other sessions. - fn init_other_sessions(&self, account_settings: &AccountSettings) { + fn init_other_sessions(&self) { let last_seen_ts_sorter = gtk::NumericSorter::builder() .expression(UserSession::this_expression("last-seen-ts")) .sort_order(gtk::SortType::Descending) @@ -175,32 +172,20 @@ mod imp { self.other_sessions_sorted_model .set_sorter(Some(&multi_sorter)); - self.other_sessions.bind_model( - Some(&self.other_sessions_sorted_model), - clone!( - #[weak] - account_settings, - #[upgrade_or_else] - || adw::Bin::new().upcast(), - move |item| { - let Some(user_session) = item.downcast_ref::() else { - error!("Did not get a user session as an item of user session list"); - return adw::Bin::new().upcast(); - }; - - UserSessionRow::new(user_session, &account_settings).upcast() - } - ), - ); + self.other_sessions + .bind_model(Some(&self.other_sessions_sorted_model), move |item| { + let Some(user_session) = item.downcast_ref::() else { + error!("Did not get a user session as an item of user session list"); + return adw::Bin::new().upcast(); + }; + + UserSessionRow::new(user_session).upcast() + }); } /// The current page of the other sessions stack according to the /// current state. fn current_other_sessions_page(&self) -> &str { - if self.account_settings.upgrade().is_none() { - return "loading"; - } - let Some(user_sessions) = self.user_sessions.obj() else { return "loading"; }; @@ -227,11 +212,6 @@ mod imp { self.current_session.remove(&child); } - let Some(account_settings) = self.account_settings.upgrade() else { - self.current_session_group.set_visible(false); - return; - }; - let current_session = self.user_sessions.obj().and_then(|s| s.current_session()); let Some(current_session) = current_session else { self.current_session_group.set_visible(false); @@ -239,9 +219,20 @@ mod imp { }; self.current_session - .append(&UserSessionRow::new(¤t_session, &account_settings)); + .append(&UserSessionRow::new(¤t_session)); self.current_session_group.set_visible(true); } + + /// Show the session subpage. + #[template_callback] + fn show_session_subpage(&self, row: &UserSessionRow) { + let obj = self.obj(); + + let _ = obj.activate_action( + "account-settings.show-session-subpage", + Some(&row.user_session().unwrap().device_id_string().to_variant()), + ); + } } } @@ -252,22 +243,11 @@ glib::wrapper! { @implements gtk::Accessible; } -#[gtk::template_callbacks] impl UserSessionsPage { /// Construct a new empty `UserSessionsPage`. pub fn new() -> Self { glib::Object::new() } - - /// Reload the user sessions list. - #[template_callback] - async fn reload_list(&self) { - let Some(user_sessions) = self.user_sessions() else { - return; - }; - - user_sessions.load().await; - } } impl Default for UserSessionsPage { diff --git a/src/session/view/account_settings/user_sessions_page/mod.ui b/src/session/view/account_settings/user_sessions_page/mod.ui index e26ab3bf..484011d5 100644 --- a/src/session/view/account_settings/user_sessions_page/mod.ui +++ b/src/session/view/account_settings/user_sessions_page/mod.ui @@ -12,6 +12,7 @@ Current Session + @@ -47,6 +48,7 @@ Other Active Sessions + @@ -78,7 +80,7 @@ true Try Again center - + account-settings.reload-user-sessions + + + + UserSessionRow + + + + + + + + verified-symbolic + + Verified + + + + + UserSessionRow + + + + + + + + verified-warning-symbolic + + Not verified + + + + + + UserSessionRow + + + + + + + + + + 0.0 end - - - AccountSettingsUserSessionRow - - - - - - + + + + UserSessionRow + + + - - AccountSettingsUserSessionRow - + + + UserSessionRow + + - verified-symbolic - - Verified - - - 0.0 - end - - - - - - - - - AccountSettingsUserSessionRow - - - - 0.0 - end - - - AccountSettingsUserSessionRow - - - - - - - - 6 - - - False - - - - - - - False - - - - - center - - - Disconnect Session - end - open_url_disconnect_button - - - - - external-link-symbolic - presentation - center - - - - - - + + go-next-symbolic + presentation diff --git a/src/session/view/account_settings/user_sessions_page/user_session_subpage.rs b/src/session/view/account_settings/user_sessions_page/user_session_subpage.rs new file mode 100644 index 00000000..aea4b912 --- /dev/null +++ b/src/session/view/account_settings/user_sessions_page/user_session_subpage.rs @@ -0,0 +1,227 @@ +use adw::{prelude::*, subclass::prelude::*}; +use gettextrs::gettext; +use gtk::{glib, glib::clone, CompositeTemplate}; +use tracing::error; +use url::Url; + +use super::AccountSettings; +use crate::{ + components::{AuthError, LoadingButtonRow}, + gettext_f, + session::model::UserSession, + toast, + utils::{oauth, template_callbacks::TemplateCallbacks, BoundConstructOnlyObject, BoundObject}, +}; + +mod imp { + use glib::subclass::InitializingObject; + + use super::*; + + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] + #[template( + resource = "/org/gnome/Fractal/ui/session/view/account_settings/user_sessions_page/user_session_subpage.ui" + )] + #[properties(wrapper_type = super::UserSessionSubpage)] + pub struct UserSessionSubpage { + #[template_child] + verified_status: TemplateChild, + #[template_child] + log_out_button: TemplateChild, + #[template_child] + loading_disconnect_button: TemplateChild, + #[template_child] + open_url_disconnect_button: TemplateChild, + /// The user session displayed by this subpage. + #[property(get, set = Self::set_user_session, construct_only)] + user_session: BoundObject, + /// The ancestor [`AccountSettings`]. + #[property(get, set = Self::set_account_settings, construct_only)] + account_settings: BoundConstructOnlyObject, + } + + #[glib::object_subclass] + impl ObjectSubclass for UserSessionSubpage { + const NAME: &'static str = "UserSessionSubpage"; + type Type = super::UserSessionSubpage; + type ParentType = adw::NavigationPage; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + Self::bind_template_callbacks(klass); + TemplateCallbacks::bind_template_callbacks(klass); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for UserSessionSubpage { + fn constructed(&self) { + self.parent_constructed(); + + self.update_disconnect_button(); + } + } + + impl WidgetImpl for UserSessionSubpage {} + impl NavigationPageImpl for UserSessionSubpage {} + + #[gtk::template_callbacks] + impl UserSessionSubpage { + /// Set the user session displayed by this subpage. + fn set_user_session(&self, user_session: UserSession) { + let obj = self.obj(); + + let verified_handler = user_session.connect_verified_notify(clone!( + #[weak(rename_to = imp)] + self, + move |_| { + imp.update_verified(); + } + )); + let disconnected_handler = user_session.connect_disconnected(clone!( + #[weak] + obj, + move |_| { + let _ = obj.activate_action("account-settings.close-subpage", None); + } + )); + + self.user_session + .set(user_session, vec![verified_handler, disconnected_handler]); + + self.update_verified(); + + obj.notify_user_session(); + } + + fn update_verified(&self) { + let Some(user_session) = self.user_session.obj() else { + return; + }; + + self.verified_status.remove_css_class("success"); + self.verified_status.remove_css_class("error"); + if user_session.verified() { + // Translators: As in 'A verified session'. + self.verified_status.set_title(&gettext("Verified")); + self.verified_status.add_css_class("success"); + } else { + // Translators: As in 'A verified session'. + self.verified_status.set_title(&gettext("Not verified")); + self.verified_status.add_css_class("error"); + } + } + + /// Set the ancestor [`AccountSettings`]. + fn set_account_settings(&self, account_settings: AccountSettings) { + let handler = account_settings.connect_account_management_url_changed(clone!( + #[weak(rename_to = imp)] + self, + move |_| { + imp.update_disconnect_button(); + } + )); + self.account_settings.set(account_settings, vec![handler]); + } + + /// The account management URL of the authentication issuer, if any. + fn account_management_url(&self) -> Option { + self.account_settings.obj().account_management_url() + } + + /// Update the visible disconnect button. + fn update_disconnect_button(&self) { + let Some(user_session) = self.user_session.obj() else { + return; + }; + + if user_session.is_current() { + self.log_out_button.set_visible(true); + self.loading_disconnect_button.set_visible(false); + self.open_url_disconnect_button.set_visible(false); + } else if self.account_management_url().is_some() { + self.log_out_button.set_visible(false); + self.loading_disconnect_button.set_visible(false); + self.open_url_disconnect_button.set_visible(true); + } else { + self.log_out_button.set_visible(false); + self.loading_disconnect_button.set_visible(true); + self.open_url_disconnect_button.set_visible(false); + } + } + + /// Disconnect the user session by making a request to the homeserver. + #[template_callback] + async fn disconnect_with_request(&self) { + let obj = self.obj(); + let Some(user_session) = self.user_session.obj() else { + return; + }; + + self.loading_disconnect_button.set_is_loading(true); + + match user_session.delete(&*obj).await { + Ok(()) => { + let _ = obj.activate_action("account-settings.reload-user-sessions", None); + } + Err(AuthError::UserCancelled) => { + self.loading_disconnect_button.set_is_loading(false); + } + Err(_) => { + let device_name = user_session.display_name_or_device_id(); + // Translators: Do NOT translate the content between '{' and '}', this is a + // variable name. + let error_message = gettext_f( + "Could not disconnect device “{device_name}”", + &[("device_name", &device_name)], + ); + toast!(obj, error_message); + self.loading_disconnect_button.set_is_loading(false); + } + } + } + + // Open the account management URL to disconnect the session. + #[template_callback] + async fn open_disconnect_url(&self) { + let Some(user_session) = self.user_session.obj() else { + return; + }; + + let device_id = user_session.device_id_string().into(); + let Some(mut url) = self.account_management_url() else { + error!("Could not find open account management URL"); + return; + }; + + oauth::AccountManagementAction::SessionEnd { device_id } + .add_to_account_management_url(&mut url); + + if let Err(error) = gtk::UriLauncher::new(url.as_ref()) + .launch_future(self.obj().root().and_downcast_ref::()) + .await + { + error!("Could not launch account management URL: {error}"); + } + } + } +} + +glib::wrapper! { + /// Account settings subpage about a user session. + pub struct UserSessionSubpage(ObjectSubclass) + @extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible; +} + +impl UserSessionSubpage { + pub fn new(user_session: &UserSession, account_settings: &AccountSettings) -> Self { + glib::Object::builder() + .property("user-session", user_session) + .property("account-settings", account_settings) + .build() + } +} diff --git a/src/session/view/account_settings/user_sessions_page/user_session_subpage.ui b/src/session/view/account_settings/user_sessions_page/user_session_subpage.ui new file mode 100644 index 00000000..778b98b6 --- /dev/null +++ b/src/session/view/account_settings/user_sessions_page/user_session_subpage.ui @@ -0,0 +1,139 @@ + + + + diff --git a/src/ui-resources.gresource.xml b/src/ui-resources.gresource.xml index 234a36a0..1f3916b8 100644 --- a/src/ui-resources.gresource.xml +++ b/src/ui-resources.gresource.xml @@ -74,6 +74,7 @@ session/view/account_settings/security_page/mod.ui session/view/account_settings/user_sessions_page/mod.ui session/view/account_settings/user_sessions_page/user_session_row.ui + session/view/account_settings/user_sessions_page/user_session_subpage.ui session/view/content/explore/mod.ui session/view/content/explore/public_room_row.ui session/view/content/explore/server_row.ui