Browse Source

auth-dialog: Port to AdwAlertDialog

merge-requests/1461/merge
Kévin Commaille 2 years ago committed by Kévin Commaille
parent
commit
48e54022b1
  1. 1
      po/POTFILES.in
  2. 189
      src/components/auth_dialog.rs
  3. 132
      src/components/auth_dialog.ui
  4. 23
      src/session/model/user_sessions_list/user_session.rs
  5. 4
      src/session/view/account_settings/general_page/change_password_subpage.rs
  6. 4
      src/session/view/account_settings/general_page/deactivate_account_subpage.rs
  7. 6
      src/session/view/account_settings/user_sessions_page/user_session_row.rs
  8. 4
      src/verification_view/session_verification_view.rs

1
po/POTFILES.in

@ -10,6 +10,7 @@ src/account_switcher/account_switcher_popover.ui
src/account_switcher/session_item.ui
src/application.rs
src/components/action_button.ui
src/components/auth_dialog.rs
src/components/auth_dialog.ui
src/components/editable_avatar.rs
src/components/editable_avatar.ui

189
src/components/auth_dialog.rs

@ -1,13 +1,9 @@
use std::{cell::Cell, fmt::Debug, future::Future};
use adw::subclass::prelude::*;
use gtk::{
gdk,
gio::prelude::*,
glib::{self, clone, closure_local},
prelude::*,
CompositeTemplate,
};
use std::{fmt::Debug, future::Future};
use adw::{prelude::*, subclass::prelude::*};
use futures_channel::oneshot;
use gettextrs::gettext;
use gtk::{glib, glib::clone, CompositeTemplate};
use matrix_sdk::Error;
use ruma::{
api::client::{
@ -44,17 +40,16 @@ pub enum AuthError {
/// The parent `Session` could not be upgraded.
#[error("The session could not be upgraded")]
NoSession,
/// The parent `gtk::Widget` could not be upgraded.
#[error("The parent widget could not be upgraded")]
NoParentWidget,
}
mod imp {
use std::cell::RefCell;
use glib::{
object::WeakRef,
subclass::{InitializingObject, Signal},
SignalHandlerId,
};
use once_cell::sync::Lazy;
use glib::subclass::InitializingObject;
use super::*;
@ -62,39 +57,30 @@ mod imp {
#[template(resource = "/org/gnome/Fractal/ui/components/auth_dialog.ui")]
#[properties(wrapper_type = super::AuthDialog)]
pub struct AuthDialog {
#[property(get, set, construct_only)]
/// The parent session.
pub session: WeakRef<Session>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
#[template_child]
pub password: TemplateChild<gtk::PasswordEntry>,
#[template_child]
pub error: TemplateChild<gtk::Label>,
#[template_child]
pub button_cancel: TemplateChild<gtk::Button>,
#[template_child]
pub button_ok: TemplateChild<gtk::Button>,
#[template_child]
pub open_browser_btn: TemplateChild<gtk::Button>,
pub open_browser_btn_handler: RefCell<Option<SignalHandlerId>>,
pub open_browser_btn_handler: RefCell<Option<glib::SignalHandlerId>>,
#[template_child]
pub error: TemplateChild<gtk::Label>,
#[property(get, set, construct_only)]
/// The parent session.
pub session: glib::WeakRef<Session>,
#[property(get)]
/// The parent widget.
pub parent: glib::WeakRef<gtk::Widget>,
pub sender: RefCell<Option<oneshot::Sender<String>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for AuthDialog {
const NAME: &'static str = "ComponentsAuthDialog";
type Type = super::AuthDialog;
type ParentType = adw::Window;
type ParentType = adw::AlertDialog;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
klass.add_binding(gdk::Key::Escape, gdk::ModifierType::empty(), |obj| {
obj.emit_by_name::<()>("response", &[&false]);
glib::Propagation::Stop
});
}
fn instance_init(obj: &InitializingObject<Self>) {
@ -108,38 +94,25 @@ mod imp {
self.parent_constructed();
let obj = self.obj();
self.button_cancel
.connect_clicked(clone!(@weak obj => move |_| {
obj.emit_by_name::<()>("response", &[&false]);
}));
self.button_ok
.connect_clicked(clone!(@weak obj => move |_| {
obj.emit_by_name::<()>("response", &[&true]);
self.password
.connect_changed(clone!(@weak obj => move |password| {
obj.set_response_enabled("confirm", !password.text().is_empty());
}));
obj.connect_close_request(
clone!(@weak obj => @default-return glib::Propagation::Proceed, move |_| {
obj.emit_by_name::<()>("response", &[&false]);
glib::Propagation::Proceed
}),
);
}
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder("response")
.param_types([bool::static_type()])
.action()
.build()]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for AuthDialog {}
impl WindowImpl for AuthDialog {}
impl AdwWindowImpl for AuthDialog {}
impl AdwDialogImpl for AuthDialog {}
impl AdwAlertDialogImpl for AuthDialog {
fn response(&self, response: &str) {
if let Some(sender) = self.sender.take() {
if sender.send(response.to_owned()).is_err() {
error!("Failed to send response");
}
}
}
}
}
glib::wrapper! {
@ -147,15 +120,12 @@ glib::wrapper! {
///
/// [User-Interaction Authentication API]: https://spec.matrix.org/v1.7/client-server-api/#user-interactive-authentication-api
pub struct AuthDialog(ObjectSubclass<imp::AuthDialog>)
@extends gtk::Widget, adw::Window, gtk::Window, @implements gtk::Accessible;
@extends gtk::Widget, adw::Dialog, adw::AlertDialog, @implements gtk::Accessible;
}
impl AuthDialog {
pub fn new(transient_for: Option<&impl IsA<gtk::Window>>, session: &Session) -> Self {
glib::Object::builder()
.property("transient-for", transient_for)
.property("session", session)
.build()
pub fn new(session: &Session) -> Self {
glib::Object::builder().property("session", session).build()
}
/// Authenticates the user to the server via an authentication flow.
@ -168,12 +138,15 @@ impl AuthDialog {
FN: Fn(matrix_sdk::Client, Option<AuthData>) -> F1 + Send + 'static + Sync + Clone,
>(
&self,
parent: &impl IsA<gtk::Widget>,
callback: FN,
) -> Result<Response, AuthError> {
let Some(client) = self.session().map(|s| s.client()) else {
return Err(AuthError::NoSession);
};
self.imp().parent.set(Some(parent.upcast_ref()));
let mut auth_data = None;
loop {
@ -256,12 +229,18 @@ impl AuthDialog {
return Err(AuthError::NoSession);
};
let stack = &self.imp().stack;
stack.set_visible_child_name(AuthType::Password.as_ref());
let imp = self.imp();
imp.password.set_visible(true);
imp.open_browser_btn.set_visible(false);
self.set_body(&gettext(
"Please authenticate the operation with your password",
));
self.set_response_enabled("confirm", false);
self.show_and_wait_for_response().await?;
let user_id = session.user_id().to_string();
let password = self.imp().password.text().to_string();
let password = imp.password.text().into();
let data = assign!(
Password::new(UserIdentifier::UserIdOrLocalpart(user_id), password),
@ -289,9 +268,17 @@ impl AuthDialog {
};
let uiaa_session = uiaa_session.ok_or(AuthError::MissingSessionId)?;
let imp = self.imp();
imp.password.set_visible(false);
imp.open_browser_btn.set_visible(true);
self.set_body(&gettext(
"Please authenticate the operation via the browser and, once completed, press confirm",
));
self.set_response_enabled("confirm", false);
let homeserver = client.homeserver();
self.imp().stack.set_visible_child_name("fallback");
self.setup_fallback_page(homeserver.as_str(), stage.as_ref(), &uiaa_session);
self.show_and_wait_for_response().await?;
Ok(AuthData::FallbackAcknowledgement(
@ -301,34 +288,33 @@ impl AuthDialog {
/// Lets the user complete the current stage.
async fn show_and_wait_for_response(&self) -> Result<(), AuthError> {
let (sender, receiver) = futures_channel::oneshot::channel();
let sender = Cell::new(Some(sender));
let Some(parent) = self.parent() else {
return Err(AuthError::NoParentWidget);
};
let handler_id = self.connect_response(move |_, response| {
if let Some(sender) = sender.take() {
sender.send(response).unwrap();
}
});
let (sender, receiver) = futures_channel::oneshot::channel();
self.imp().sender.replace(Some(sender));
self.present();
self.present(&parent);
let result = receiver.await.unwrap();
self.disconnect(handler_id);
self.close();
result.then_some(()).ok_or(AuthError::UserCancelled)
if result == "confirm" {
Ok(())
} else {
Err(AuthError::UserCancelled)
}
}
fn show_auth_error(&self, auth_error: &Option<StandardErrorBody>) {
let imp = self.imp();
let visible = if let Some(auth_error) = auth_error {
if let Some(auth_error) = auth_error {
imp.error.set_label(&auth_error.message);
true
} else {
false
};
imp.error.set_visible(visible);
}
imp.error.set_visible(auth_error.is_some());
}
fn setup_fallback_page(&self, homeserver: &str, auth_type: &str, uiaa_session: &str) {
@ -346,23 +332,22 @@ impl AuthDialog {
.open_browser_btn
.connect_clicked(clone!(@weak self as obj => move |_| {
let uri = uri.clone();
spawn!(clone!(@weak obj => async move {
if let Err(error) = gtk::UriLauncher::new(&uri).launch_future(obj.transient_for().as_ref()).await {
spawn!(async move {
let Some(parent) = obj.parent() else {
return;
};
if let Err(error) = gtk::UriLauncher::new(&uri)
.launch_future(parent.root().and_downcast_ref::<gtk::Window>())
.await
{
error!("Could not launch URI: {error}");
}
}));
obj.set_response_enabled("confirm", true);
});
}));
imp.open_browser_btn_handler.replace(Some(handler));
}
pub fn connect_response<F: Fn(&Self, bool) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_closure(
"response",
true,
closure_local!(move |obj: Self, response: bool| {
f(&obj, response);
}),
)
}
}

132
src/components/auth_dialog.ui

@ -1,96 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ComponentsAuthDialog" parent="AdwWindow">
<property name="modal">true</property>
<property name="hide-on-close">true</property>
<property name="title"/>
<property name="resizable">0</property>
<property name="default-widget">button_ok</property>
<style>
<class name="message"/>
<class name="dialog"/>
</style>
<child>
<template class="ComponentsAuthDialog" parent="AdwAlertDialog">
<property name="heading" translatable="yes">Authentication</property>
<property name="default-response">confirm</property>
<property name="close-response">cancel</property>
<property name="extra_child">
<object class="GtkBox">
<property name="spacing">12</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="halign">center</property>
<property name="label" translatable="yes">Authentication</property>
<property name="margin-top">24</property>
<style>
<class name="title-2"/>
</style>
<object class="GtkPasswordEntry" id="password">
<property name="activates-default">True</property>
<property name="show-peek-icon">True</property>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="hhomogeneous">False</property>
<property name="vhomogeneous">False</property>
<property name="margin-bottom">12</property>
<property name="margin-start">24</property>
<property name="margin-end">24</property>
<child>
<object class="GtkStackPage">
<property name="name">m.login.password</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Please authenticate the operation with your password</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="max-width-chars">60</property>
<property name="halign">center</property>
<property name="valign">start</property>
</object>
</child>
<child>
<object class="GtkPasswordEntry" id="password">
<property name="activates-default">True</property>
<property name="show-peek-icon">True</property>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">fallback</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Please authenticate the operation via the browser and once completed press confirm.</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="max-width-chars">60</property>
<property name="halign">center</property>
<property name="valign">start</property>
</object>
</child>
<child>
<object class="GtkButton" id="open_browser_btn">
<property name="can-shrink">true</property>
<property name="label" translatable="yes">Authenticate via Browser</property>
<property name="halign">center</property>
<style>
<class name="suggested-action"/>
<class name="pill"/>
<class name="large"/>
</style>
</object>
</child>
</object>
</property>
</object>
</child>
<object class="GtkButton" id="open_browser_btn">
<property name="can-shrink">true</property>
<property name="label" translatable="yes">Authenticate via Browser</property>
<property name="halign">center</property>
<style>
<class name="suggested-action"/>
<class name="pill"/>
<class name="large"/>
</style>
</object>
</child>
<child>
@ -104,30 +37,11 @@
<property name="margin-bottom">12</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="hexpand">True</property>
<property name="homogeneous">True</property>
<property name="halign">fill</property>
<child>
<object class="GtkButton" id="button_cancel">
<property name="label" translatable="yes">Cancel</property>
</object>
</child>
<child>
<object class="GtkButton" id="button_ok">
<property name="label" translatable="yes">Confirm</property>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
<style>
<class name="dialog-action-area"/>
</style>
</object>
</child>
</object>
</child>
</property>
<responses>
<response id="cancel" translatable="yes">_Cancel</response>
<response id="confirm" translatable="yes" appearance="suggested" enabled="false">C_onfirm</response>
</responses>
</template>
</interface>

23
src/session/model/user_sessions_list/user_session.rs

@ -4,7 +4,7 @@ use ruma::{
api::client::device::{delete_device, Device as DeviceData},
assign, DeviceId,
};
use tracing::error;
use tracing::{debug, error};
use crate::{
components::{AuthDialog, AuthError},
@ -168,20 +168,17 @@ impl UserSession {
/// Deletes the `UserSession`.
///
/// Requires a window because it might show a dialog for UIAA.
pub async fn delete(
&self,
transient_for: Option<&impl IsA<gtk::Window>>,
) -> Result<(), AuthError> {
/// Requires a widget because it might show a dialog for UIAA.
pub async fn delete(&self, parent: &impl IsA<gtk::Widget>) -> Result<(), AuthError> {
let Some(session) = self.session() else {
return Err(AuthError::NoSession);
};
let device_id = self.imp().data().device_id().to_owned();
let dialog = AuthDialog::new(transient_for, &session);
let dialog = AuthDialog::new(&session);
let res = dialog
.authenticate(move |client, auth| {
.authenticate(parent, move |client, auth| {
let device_id = device_id.clone();
async move {
let request = assign!(delete_device::v3::Request::new(device_id), { auth });
@ -193,10 +190,12 @@ impl UserSession {
match res {
Ok(_) => Ok(()),
Err(error) => {
error!(
"Failed to delete user session {}: {error:?}",
self.device_id()
);
let device_id = self.device_id();
if matches!(error, AuthError::UserCancelled) {
debug!("Deletion of user session {device_id} cancelled by user");
} else {
error!("Failed to delete user session {device_id}: {error:?}");
}
Err(error)
}
}

4
src/session/view/account_settings/general_page/change_password_subpage.rs

@ -227,10 +227,10 @@ impl ChangePasswordSubpage {
imp.password.set_sensitive(false);
imp.confirm_password.set_sensitive(false);
let dialog = AuthDialog::new(self.root().and_downcast_ref::<gtk::Window>(), &session);
let dialog = AuthDialog::new(&session);
let result = dialog
.authenticate(move |client, auth| {
.authenticate(self, move |client, auth| {
let password = password.clone();
async move {
let request =

4
src/session/view/account_settings/general_page/deactivate_account_subpage.rs

@ -127,10 +127,10 @@ impl DeactivateAccountSubpage {
imp.button.set_loading(true);
imp.confirmation.set_sensitive(false);
let dialog = AuthDialog::new(self.root().and_downcast_ref::<gtk::Window>(), &session);
let dialog = AuthDialog::new(&session);
let result = dialog
.authenticate(move |client, auth| async move {
.authenticate(self, move |client, auth| async move {
let request = assign!(deactivate::v3::Request::new(), { auth });
client.send(request, None).await.map_err(Into::into)
})

6
src/session/view/account_settings/user_sessions_page/user_session_row.rs

@ -152,10 +152,8 @@ impl UserSessionRow {
self.imp().disconnect_button.set_loading(true);
spawn!(clone!(@weak self as obj, @weak user_session => async move {
let window = obj.root().and_downcast::<gtk::Window>();
match user_session.delete(window.as_ref()).await {
spawn!(clone!(@weak self as obj => async move {
match user_session.delete(&obj).await {
Ok(_) => obj.set_visible(false),
Err(AuthError::UserCancelled) => {},
Err(_) => {

4
src/verification_view/session_verification_view.rs

@ -414,10 +414,10 @@ impl SessionVerificationView {
let Some(session) = self.session() else {
return;
};
let dialog = AuthDialog::new(self.root().and_downcast_ref::<gtk::Window>(), &session);
let dialog = AuthDialog::new(&session);
let result = dialog
.authenticate(move |client, auth| async move {
.authenticate(self, move |client, auth| async move {
client.encryption().bootstrap_cross_signing(auth).await
})
.await;

Loading…
Cancel
Save