fractal/login/
session_setup_view.rs1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{
3 glib,
4 glib::{clone, closure_local},
5};
6
7use crate::{
8 components::crypto::{
9 CryptoIdentitySetupNextStep, CryptoIdentitySetupView, CryptoRecoverySetupView,
10 },
11 session::{CryptoIdentityState, RecoveryState, Session, SessionVerificationState},
12 spawn, spawn_tokio,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17enum SessionSetupPage {
18 Loading,
20 CryptoIdentity,
22 Recovery,
24}
25
26impl SessionSetupPage {
27 const fn name(self) -> &'static str {
29 match self {
30 Self::Loading => "loading",
31 Self::CryptoIdentity => "crypto-identity",
32 Self::Recovery => "recovery",
33 }
34 }
35
36 fn from_name(name: &str) -> Self {
40 match name {
41 "loading" => Self::Loading,
42 "crypto-identity" => Self::CryptoIdentity,
43 "recovery" => Self::Recovery,
44 _ => panic!("Unknown SessionSetupPage: {name}"),
45 }
46 }
47}
48
49mod imp {
50 use std::{
51 cell::{OnceCell, RefCell},
52 sync::LazyLock,
53 };
54
55 use glib::subclass::{InitializingObject, Signal};
56
57 use super::*;
58
59 #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
60 #[template(resource = "/org/gnome/Fractal/ui/login/session_setup_view.ui")]
61 #[properties(wrapper_type = super::SessionSetupView)]
62 pub struct SessionSetupView {
63 #[template_child]
64 stack: TemplateChild<gtk::Stack>,
65 #[property(get, set = Self::set_session, construct_only)]
67 session: glib::WeakRef<Session>,
68 crypto_identity_view: OnceCell<CryptoIdentitySetupView>,
70 recovery_view: OnceCell<CryptoRecoverySetupView>,
72 session_handler: RefCell<Option<glib::SignalHandlerId>>,
73 security_handler: RefCell<Option<glib::SignalHandlerId>>,
74 }
75
76 #[glib::object_subclass]
77 impl ObjectSubclass for SessionSetupView {
78 const NAME: &'static str = "SessionSetupView";
79 type Type = super::SessionSetupView;
80 type ParentType = adw::NavigationPage;
81
82 fn class_init(klass: &mut Self::Class) {
83 Self::bind_template(klass);
84 Self::bind_template_callbacks(klass);
85
86 klass.set_css_name("setup-view");
87 }
88
89 fn instance_init(obj: &InitializingObject<Self>) {
90 obj.init_template();
91 }
92 }
93
94 #[glib::derived_properties]
95 impl ObjectImpl for SessionSetupView {
96 fn signals() -> &'static [Signal] {
97 static SIGNALS: LazyLock<Vec<Signal>> = LazyLock::new(|| {
98 vec![
99 Signal::builder("completed").build(),
101 ]
102 });
103 SIGNALS.as_ref()
104 }
105
106 fn dispose(&self) {
107 if let Some(session) = self.session.upgrade() {
108 if let Some(handler) = self.session_handler.take() {
109 session.disconnect(handler);
110 }
111 if let Some(handler) = self.security_handler.take() {
112 session.security().disconnect(handler);
113 }
114 }
115 }
116 }
117
118 impl WidgetImpl for SessionSetupView {
119 fn grab_focus(&self) -> bool {
120 match self.visible_stack_page() {
121 SessionSetupPage::Loading => false,
122 SessionSetupPage::CryptoIdentity => self.crypto_identity_view().grab_focus(),
123 SessionSetupPage::Recovery => self.recovery_view().grab_focus(),
124 }
125 }
126 }
127
128 impl NavigationPageImpl for SessionSetupView {
129 fn shown(&self) {
130 self.grab_focus();
131 }
132 }
133
134 #[gtk::template_callbacks]
135 impl SessionSetupView {
136 fn visible_stack_page(&self) -> SessionSetupPage {
138 SessionSetupPage::from_name(
139 &self
140 .stack
141 .visible_child_name()
142 .expect("SessionSetupView stack should always have a visible child name"),
143 )
144 }
145
146 fn crypto_identity_view(&self) -> &CryptoIdentitySetupView {
148 self.crypto_identity_view.get_or_init(|| {
149 let session = self
150 .session
151 .upgrade()
152 .expect("Session should still have a strong reference");
153 let crypto_identity_view = CryptoIdentitySetupView::new(&session);
154
155 crypto_identity_view.connect_completed(clone!(
156 #[weak(rename_to = imp)]
157 self,
158 move |_, next| {
159 match next {
160 CryptoIdentitySetupNextStep::None => imp.emit_completed(),
161 CryptoIdentitySetupNextStep::EnableRecovery => imp.check_recovery(true),
162 CryptoIdentitySetupNextStep::CompleteRecovery => {
163 imp.check_recovery(false);
164 }
165 }
166 }
167 ));
168
169 crypto_identity_view
170 })
171 }
172
173 fn recovery_view(&self) -> &CryptoRecoverySetupView {
175 self.recovery_view.get_or_init(|| {
176 let session = self
177 .session
178 .upgrade()
179 .expect("Session should still have a strong reference");
180 let recovery_view = CryptoRecoverySetupView::new(&session);
181
182 recovery_view.connect_completed(clone!(
183 #[weak(rename_to = imp)]
184 self,
185 move |_| {
186 imp.emit_completed();
187 }
188 ));
189
190 recovery_view
191 })
192 }
193
194 fn set_session(&self, session: &Session) {
196 self.session.set(Some(session));
197
198 let ready_handler = session.connect_ready(clone!(
199 #[weak(rename_to = imp)]
200 self,
201 move |_| {
202 spawn!(async move {
203 imp.load().await;
204 });
205 }
206 ));
207 self.session_handler.replace(Some(ready_handler));
208 }
209
210 async fn load(&self) {
212 let Some(session) = self.session.upgrade() else {
213 return;
214 };
215
216 let encryption = session.client().encryption();
218 spawn_tokio!(async move {
219 encryption.wait_for_e2ee_initialization_tasks().await;
220 })
221 .await
222 .unwrap();
223
224 self.check_session_setup();
225 }
226
227 fn check_session_setup(&self) {
229 let Some(session) = self.session.upgrade() else {
230 return;
231 };
232 let security = session.security();
233
234 if let Some(handler) = self.session_handler.take() {
236 session.disconnect(handler);
237 }
238 if let Some(handler) = self.security_handler.take() {
239 security.disconnect(handler);
240 }
241
242 let crypto_identity_state = security.crypto_identity_state();
244 if crypto_identity_state == CryptoIdentityState::Unknown {
245 let handler = security.connect_crypto_identity_state_notify(clone!(
246 #[weak(rename_to = imp)]
247 self,
248 move |_| {
249 imp.check_session_setup();
250 }
251 ));
252 self.security_handler.replace(Some(handler));
253 return;
254 }
255
256 let verification_state = security.verification_state();
258 if verification_state == SessionVerificationState::Unknown {
259 let handler = security.connect_verification_state_notify(clone!(
260 #[weak(rename_to = imp)]
261 self,
262 move |_| {
263 imp.check_session_setup();
264 }
265 ));
266 self.security_handler.replace(Some(handler));
267 return;
268 }
269
270 let recovery_state = security.recovery_state();
272 if recovery_state == RecoveryState::Unknown {
273 let handler = security.connect_recovery_state_notify(clone!(
274 #[weak(rename_to = imp)]
275 self,
276 move |_| {
277 imp.check_session_setup();
278 }
279 ));
280 self.security_handler.replace(Some(handler));
281 return;
282 }
283
284 if verification_state == SessionVerificationState::Verified
285 && recovery_state == RecoveryState::Enabled
286 {
287 self.emit_completed();
289 return;
290 }
291
292 self.init();
293 }
294
295 fn init(&self) {
297 let Some(session) = self.session.upgrade() else {
298 return;
299 };
300
301 let verification_state = session.security().verification_state();
302 if verification_state == SessionVerificationState::Unverified {
303 let crypto_identity_view = self.crypto_identity_view();
304
305 self.stack.add_named(
306 crypto_identity_view,
307 Some(SessionSetupPage::CryptoIdentity.name()),
308 );
309 self.stack
310 .set_visible_child_name(SessionSetupPage::CryptoIdentity.name());
311 } else {
312 self.switch_to_recovery();
313 }
314 }
315
316 fn check_recovery(&self, enable_only: bool) {
318 let Some(session) = self.session.upgrade() else {
319 return;
320 };
321
322 match session.security().recovery_state() {
323 RecoveryState::Disabled => {
324 self.switch_to_recovery();
325 }
326 RecoveryState::Incomplete if !enable_only => {
327 self.switch_to_recovery();
328 }
329 _ => {
330 self.emit_completed();
331 }
332 }
333 }
334
335 fn switch_to_recovery(&self) {
337 let recovery_view = self.recovery_view();
338
339 self.stack
340 .add_named(recovery_view, Some(SessionSetupPage::Recovery.name()));
341 self.stack
342 .set_visible_child_name(SessionSetupPage::Recovery.name());
343 }
344
345 #[template_callback]
347 fn focus_default_widget(&self) {
348 if !self.stack.is_transition_running() {
349 self.grab_focus();
351 }
352 }
353
354 #[template_callback]
356 fn emit_completed(&self) {
357 self.obj().emit_by_name::<()>("completed", &[]);
358 }
359 }
360}
361
362glib::wrapper! {
363 pub struct SessionSetupView(ObjectSubclass<imp::SessionSetupView>)
365 @extends gtk::Widget, adw::NavigationPage,
366 @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
367}
368
369impl SessionSetupView {
370 pub(super) const TAG: &str = "session-setup";
372
373 pub fn new(session: &Session) -> Self {
374 glib::Object::builder().property("session", session).build()
375 }
376
377 pub fn connect_completed<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
379 self.connect_closure(
380 "completed",
381 true,
382 closure_local!(move |obj: Self| {
383 f(&obj);
384 }),
385 )
386 }
387}