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