use adw::{prelude::*, subclass::prelude::*};
use gtk::{
glib,
glib::{clone, closure_local},
CompositeTemplate,
};
use crate::{
components::crypto::{
CryptoIdentitySetupNextStep, CryptoIdentitySetupView, CryptoRecoverySetupView,
},
session::model::{CryptoIdentityState, RecoveryState, Session, SessionVerificationState},
spawn, spawn_tokio,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumString, strum::AsRefStr)]
#[strum(serialize_all = "kebab-case")]
enum SessionSetupPage {
Loading,
CryptoIdentity,
Recovery,
}
mod imp {
use std::cell::{OnceCell, RefCell};
use glib::subclass::{InitializingObject, Signal};
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/login/session_setup_view.ui")]
#[properties(wrapper_type = super::SessionSetupView)]
pub struct SessionSetupView {
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
#[property(get, set = Self::set_session, construct_only)]
pub session: glib::WeakRef<Session>,
crypto_identity_view: OnceCell<CryptoIdentitySetupView>,
recovery_view: OnceCell<CryptoRecoverySetupView>,
session_handler: RefCell<Option<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
impl ObjectSubclass for SessionSetupView {
const NAME: &'static str = "SessionSetupView";
type Type = super::SessionSetupView;
type ParentType = adw::NavigationPage;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
klass.set_css_name("setup-view");
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for SessionSetupView {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![
Signal::builder("completed").build(),
]
});
SIGNALS.as_ref()
}
fn dispose(&self) {
if let Some(session) = self.session.upgrade() {
if let Some(handler) = self.session_handler.take() {
session.disconnect(handler);
}
}
}
}
impl WidgetImpl for SessionSetupView {
fn grab_focus(&self) -> bool {
match self.visible_stack_page() {
SessionSetupPage::Loading => false,
SessionSetupPage::CryptoIdentity => self.crypto_identity_view().grab_focus(),
SessionSetupPage::Recovery => self.recovery_view().grab_focus(),
}
}
}
impl NavigationPageImpl for SessionSetupView {
fn shown(&self) {
self.grab_focus();
}
}
impl SessionSetupView {
fn visible_stack_page(&self) -> SessionSetupPage {
self.stack
.visible_child_name()
.and_then(|n| n.as_str().try_into().ok())
.unwrap()
}
fn crypto_identity_view(&self) -> &CryptoIdentitySetupView {
self.crypto_identity_view.get_or_init(|| {
let session = self
.session
.upgrade()
.expect("Session should still have a strong reference");
let crypto_identity_view = CryptoIdentitySetupView::new(&session);
crypto_identity_view.connect_completed(clone!(
#[weak(rename_to = imp)]
self,
move |_, next| {
match next {
CryptoIdentitySetupNextStep::None => imp.obj().emit_completed(),
CryptoIdentitySetupNextStep::EnableRecovery => imp.check_recovery(true),
CryptoIdentitySetupNextStep::CompleteRecovery => {
imp.check_recovery(false)
}
}
}
));
crypto_identity_view
})
}
fn recovery_view(&self) -> &CryptoRecoverySetupView {
self.recovery_view.get_or_init(|| {
let session = self
.session
.upgrade()
.expect("Session should still have a strong reference");
let recovery_view = CryptoRecoverySetupView::new(&session);
let obj = self.obj();
recovery_view.connect_completed(clone!(
#[weak]
obj,
move |_| {
obj.emit_completed();
}
));
recovery_view
})
}
fn set_session(&self, session: &Session) {
self.session.set(Some(session));
let ready_handler = session.connect_ready(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
spawn!(async move {
imp.load().await;
});
}
));
self.session_handler.replace(Some(ready_handler));
}
async fn load(&self) {
let Some(session) = self.session.upgrade() else {
return;
};
let encryption = session.client().encryption();
spawn_tokio!(async move {
encryption.wait_for_e2ee_initialization_tasks().await;
})
.await
.unwrap();
self.check_session_setup();
}
fn check_session_setup(&self) {
let Some(session) = self.session.upgrade() else {
return;
};
if let Some(handler) = self.session_handler.take() {
session.disconnect(handler);
}
let crypto_identity_state = session.crypto_identity_state();
if crypto_identity_state == CryptoIdentityState::Unknown {
let handler = session.connect_crypto_identity_state_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.check_session_setup();
}
));
self.session_handler.replace(Some(handler));
return;
}
let verification_state = session.verification_state();
if verification_state == SessionVerificationState::Unknown {
let handler = session.connect_verification_state_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.check_session_setup();
}
));
self.session_handler.replace(Some(handler));
return;
}
let recovery_state = session.recovery_state();
if recovery_state == RecoveryState::Unknown {
let handler = session.connect_recovery_state_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.check_session_setup();
}
));
self.session_handler.replace(Some(handler));
return;
}
if verification_state == SessionVerificationState::Verified
&& recovery_state == RecoveryState::Enabled
{
self.obj().emit_completed();
return;
}
self.init();
}
fn init(&self) {
let Some(session) = self.session.upgrade() else {
return;
};
let verification_state = session.verification_state();
if verification_state == SessionVerificationState::Unverified {
let crypto_identity_view = self.crypto_identity_view();
self.stack.add_named(
crypto_identity_view,
Some(SessionSetupPage::CryptoIdentity.as_ref()),
);
self.stack
.set_visible_child_name(SessionSetupPage::CryptoIdentity.as_ref());
} else {
self.switch_to_recovery();
}
}
pub(super) fn check_recovery(&self, enable_only: bool) {
let Some(session) = self.session.upgrade() else {
return;
};
match session.recovery_state() {
RecoveryState::Disabled => {
self.switch_to_recovery();
}
RecoveryState::Incomplete if !enable_only => {
self.switch_to_recovery();
}
_ => {
self.obj().emit_completed();
}
}
}
fn switch_to_recovery(&self) {
let recovery_view = self.recovery_view();
self.stack
.add_named(recovery_view, Some(SessionSetupPage::Recovery.as_ref()));
self.stack
.set_visible_child_name(SessionSetupPage::Recovery.as_ref());
}
}
}
glib::wrapper! {
pub struct SessionSetupView(ObjectSubclass<imp::SessionSetupView>)
@extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible;
}
#[gtk::template_callbacks]
impl SessionSetupView {
pub fn new(session: &Session) -> Self {
glib::Object::builder().property("session", session).build()
}
#[template_callback]
fn grab_focus(&self) {
let imp = self.imp();
if !imp.stack.is_transition_running() {
imp.grab_focus();
}
}
#[template_callback]
fn emit_completed(&self) {
self.emit_by_name::<()>("completed", &[]);
}
pub fn connect_completed<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_closure(
"completed",
true,
closure_local!(move |obj: Self| {
f(&obj);
}),
)
}
}