13 changed files with 749 additions and 457 deletions
@ -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<adw::ActionRow>, |
||||
#[template_child] |
||||
log_out_button: TemplateChild<adw::ButtonRow>, |
||||
#[template_child] |
||||
loading_disconnect_button: TemplateChild<LoadingButtonRow>, |
||||
#[template_child] |
||||
open_url_disconnect_button: TemplateChild<adw::ButtonRow>, |
||||
/// The user session displayed by this subpage.
|
||||
#[property(get, set = Self::set_user_session, construct_only)] |
||||
user_session: BoundObject<UserSession>, |
||||
/// The ancestor [`AccountSettings`].
|
||||
#[property(get, set = Self::set_account_settings, construct_only)] |
||||
account_settings: BoundConstructOnlyObject<AccountSettings>, |
||||
} |
||||
|
||||
#[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<Self>) { |
||||
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<Url> { |
||||
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::<gtk::Window>()) |
||||
.await |
||||
{ |
||||
error!("Could not launch account management URL: {error}"); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// Account settings subpage about a user session.
|
||||
pub struct UserSessionSubpage(ObjectSubclass<imp::UserSessionSubpage>) |
||||
@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() |
||||
} |
||||
} |
||||
@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<interface> |
||||
<template class="UserSessionSubpage" parent="AdwNavigationPage"> |
||||
<binding name="title"> |
||||
<lookup name="display-name-or-device-id"> |
||||
<lookup name="user-session"> |
||||
UserSessionSubpage |
||||
</lookup> |
||||
</lookup> |
||||
</binding> |
||||
<property name="child"> |
||||
<object class="AdwToolbarView"> |
||||
<child type="top"> |
||||
<object class="AdwHeaderBar"/> |
||||
</child> |
||||
<property name="content"> |
||||
<object class="AdwPreferencesPage"> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="CopyableRow"> |
||||
<property name="title" translatable="yes">Session ID</property> |
||||
<property name="main-title">subtitle</property> |
||||
<property name="copy-button-tooltip-text" translatable="yes">Copy Session ID</property> |
||||
<property name="toast-text" translatable="yes">Session ID copied to clipboard</property> |
||||
<binding name="subtitle"> |
||||
<lookup name="device-id-string"> |
||||
<lookup name="user-session"> |
||||
UserSessionSubpage |
||||
</lookup> |
||||
</lookup> |
||||
</binding> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwActionRow"> |
||||
<style> |
||||
<class name="property"/> |
||||
</style> |
||||
<property name="title" translatable="yes">Public Name</property> |
||||
<binding name="subtitle"> |
||||
<lookup name="display-name"> |
||||
<lookup name="user-session"> |
||||
UserSessionSubpage |
||||
</lookup> |
||||
</lookup> |
||||
</binding> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwActionRow" id="verified_status"/> |
||||
</child> |
||||
<child> |
||||
<object class="AdwActionRow"> |
||||
<style> |
||||
<class name="property"/> |
||||
</style> |
||||
<property name="title" translatable="yes">Last Seen</property> |
||||
<binding name="subtitle"> |
||||
<closure type="gchararray" function="unwrap_string_or_empty"> |
||||
<lookup name="last-seen-datetime-string"> |
||||
<lookup name="user-session">UserSessionSubpage</lookup> |
||||
</lookup> |
||||
</closure> |
||||
</binding> |
||||
<binding name="visible"> |
||||
<closure type="gboolean" function="string_not_empty"> |
||||
<lookup name="last-seen-datetime-string"> |
||||
<lookup name="user-session">UserSessionSubpage</lookup> |
||||
</lookup> |
||||
</closure> |
||||
</binding> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwActionRow"> |
||||
<style> |
||||
<class name="property"/> |
||||
</style> |
||||
<property name="title" translatable="yes">Last Location</property> |
||||
<binding name="subtitle"> |
||||
<closure type="gchararray" function="unwrap_string_or_empty"> |
||||
<lookup name="last-seen-ip"> |
||||
<lookup name="user-session">UserSessionSubpage</lookup> |
||||
</lookup> |
||||
</closure> |
||||
</binding> |
||||
<binding name="visible"> |
||||
<closure type="gboolean" function="string_not_empty"> |
||||
<lookup name="last-seen-ip"> |
||||
<lookup name="user-session">UserSessionSubpage</lookup> |
||||
</lookup> |
||||
</closure> |
||||
</binding> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="AdwButtonRow" id="log_out_button"> |
||||
<property name="title" translatable="yes">Log Out</property> |
||||
<property name="end-icon-name">go-next-symbolic</property> |
||||
<property name="action-name">account-settings.show-subpage</property> |
||||
<property name="action-target">'log-out'</property> |
||||
<style> |
||||
<class name="destructive-action"/> |
||||
</style> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="LoadingButtonRow" id="loading_disconnect_button"> |
||||
<property name="title" translatable="yes">Disconnect</property> |
||||
<style> |
||||
<class name="destructive-action"/> |
||||
</style> |
||||
<signal name="activated" handler="disconnect_with_request" swapped="yes"/> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwButtonRow" id="open_url_disconnect_button"> |
||||
<property name="visible">False</property> |
||||
<property name="title" translatable="yes">Disconnect</property> |
||||
<property name="end-icon-name">external-link-symbolic</property> |
||||
<style> |
||||
<class name="destructive-action"/> |
||||
</style> |
||||
<signal name="activated" handler="open_disconnect_url" swapped="yes"/> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</property> |
||||
</object> |
||||
</property> |
||||
</template> |
||||
</interface> |
||||
Loading…
Reference in new issue