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