Skip to main content

fractal/login/
session_setup_view.rs

1use 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/// A page of the session setup stack.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17enum SessionSetupPage {
18    /// The loading page.
19    Loading,
20    /// The crypto identity setup view.
21    CryptoIdentity,
22    /// The recovery view.
23    Recovery,
24}
25
26impl SessionSetupPage {
27    /// Get the name of this page.
28    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    /// Get the page matching the given name.
37    ///
38    /// Panics if the name does not match any of the variants.
39    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        /// The current session.
66        #[property(get, set = Self::set_session, construct_only)]
67        session: glib::WeakRef<Session>,
68        /// The crypto identity view.
69        crypto_identity_view: OnceCell<CryptoIdentitySetupView>,
70        /// The recovery view.
71        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                    // The session setup is done.
100                    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        /// The visible page of the stack.
137        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        /// The crypto identity view.
147        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        /// The recovery view.
174        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        /// Set the current session.
195        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        /// Load the session state.
211        async fn load(&self) {
212            let Some(session) = self.session.upgrade() else {
213                return;
214            };
215
216            // Make sure the encryption API is ready.
217            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        /// Check whether we need to show the session setup.
228        fn check_session_setup(&self) {
229            let Some(session) = self.session.upgrade() else {
230                return;
231            };
232            let security = session.security();
233
234            // Stop listening to notifications.
235            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            // Wait if we don't know the crypto identity state.
243            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            // Wait if we don't know the verification state.
257            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            // Wait if we don't know the recovery state.
271            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                // No need for setup.
288                self.emit_completed();
289                return;
290            }
291
292            self.init();
293        }
294
295        /// Initialize this view.
296        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        /// Check whether we need to enable or set up recovery.
317        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        /// Switch to the recovery view.
336        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        /// Focus the proper widget for the current page.
346        #[template_callback]
347        fn focus_default_widget(&self) {
348            if !self.stack.is_transition_running() {
349                // Focus the default widget when the transition has ended.
350                self.grab_focus();
351            }
352        }
353
354        // Emit the `completed` signal.
355        #[template_callback]
356        fn emit_completed(&self) {
357            self.obj().emit_by_name::<()>("completed", &[]);
358        }
359    }
360}
361
362glib::wrapper! {
363    /// A view with the different flows to verify a session.
364    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    /// The tag for this page.
371    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    /// Connect to the signal emitted when the setup is completed.
378    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}