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