fractal/login/
session_setup_view.rs

1use 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/// A page of the session setup stack.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumString, strum::AsRefStr)]
18#[strum(serialize_all = "kebab-case")]
19enum SessionSetupPage {
20    /// The loading page.
21    Loading,
22    /// The crypto identity setup view.
23    CryptoIdentity,
24    /// The recovery view.
25    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        /// The current session.
45        #[property(get, set = Self::set_session, construct_only)]
46        session: glib::WeakRef<Session>,
47        /// The crypto identity view.
48        crypto_identity_view: OnceCell<CryptoIdentitySetupView>,
49        /// The recovery view.
50        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                    // The session setup is done.
79                    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        /// The visible page of the stack.
116        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        /// The crypto identity view.
124        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        /// The recovery view.
151        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        /// Set the current session.
172        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        /// Load the session state.
188        async fn load(&self) {
189            let Some(session) = self.session.upgrade() else {
190                return;
191            };
192
193            // Make sure the encryption API is ready.
194            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        /// Check whether we need to show the session setup.
205        fn check_session_setup(&self) {
206            let Some(session) = self.session.upgrade() else {
207                return;
208            };
209            let security = session.security();
210
211            // Stop listening to notifications.
212            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            // Wait if we don't know the crypto identity state.
220            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            // Wait if we don't know the verification state.
234            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            // Wait if we don't know the recovery state.
248            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                // No need for setup.
265                self.emit_completed();
266                return;
267            }
268
269            self.init();
270        }
271
272        /// Initialize this view.
273        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        /// Check whether we need to enable or set up recovery.
294        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        /// Switch to the recovery view.
313        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        /// Focus the proper widget for the current page.
323        #[template_callback]
324        fn focus_default_widget(&self) {
325            if !self.stack.is_transition_running() {
326                // Focus the default widget when the transition has ended.
327                self.grab_focus();
328            }
329        }
330
331        // Emit the `completed` signal.
332        #[template_callback]
333        fn emit_completed(&self) {
334            self.obj().emit_by_name::<()>("completed", &[]);
335        }
336    }
337}
338
339glib::wrapper! {
340    /// A view with the different flows to verify a session.
341    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    /// Connect to the signal emitted when the setup is completed.
351    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}