Skip to main content

fractal/identity_verification_view/
choose_method_page.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::gettext;
3use gtk::{glib, glib::clone};
4
5use crate::{
6    components::LoadingButton,
7    contrib::QRCode,
8    gettext_f,
9    prelude::*,
10    session::{IdentityVerification, VerificationSupportedMethods},
11    toast,
12    utils::BoundObjectWeakRef,
13};
14
15mod imp {
16    use std::cell::RefCell;
17
18    use glib::subclass::InitializingObject;
19
20    use super::*;
21
22    #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
23    #[template(resource = "/org/gnome/Fractal/ui/identity_verification_view/choose_method_page.ui")]
24    #[properties(wrapper_type = super::ChooseMethodPage)]
25    pub struct ChooseMethodPage {
26        /// The current identity verification.
27        #[property(get, set = Self::set_verification, explicit_notify, nullable)]
28        pub verification: BoundObjectWeakRef<IdentityVerification>,
29        pub display_name_handler: RefCell<Option<glib::SignalHandlerId>>,
30        #[template_child]
31        pub title: TemplateChild<gtk::Label>,
32        #[template_child]
33        pub instructions: TemplateChild<gtk::Label>,
34        #[template_child]
35        pub qrcode: TemplateChild<QRCode>,
36        #[template_child]
37        pub cannot_scan_label: TemplateChild<gtk::Label>,
38        #[template_child]
39        pub scan_qr_code_btn: TemplateChild<LoadingButton>,
40        #[template_child]
41        pub start_sas_btn: TemplateChild<LoadingButton>,
42        #[template_child]
43        pub cancel_btn: TemplateChild<LoadingButton>,
44    }
45
46    #[glib::object_subclass]
47    impl ObjectSubclass for ChooseMethodPage {
48        const NAME: &'static str = "IdentityVerificationChooseMethodPage";
49        type Type = super::ChooseMethodPage;
50        type ParentType = adw::Bin;
51
52        fn class_init(klass: &mut Self::Class) {
53            Self::bind_template(klass);
54            Self::Type::bind_template_callbacks(klass);
55        }
56
57        fn instance_init(obj: &InitializingObject<Self>) {
58            obj.init_template();
59        }
60    }
61
62    #[glib::derived_properties]
63    impl ObjectImpl for ChooseMethodPage {
64        fn dispose(&self) {
65            if let Some(verification) = self.verification.obj()
66                && let Some(handler) = self.display_name_handler.take()
67            {
68                verification.user().disconnect(handler);
69            }
70        }
71    }
72
73    impl WidgetImpl for ChooseMethodPage {
74        fn grab_focus(&self) -> bool {
75            if self.scan_qr_code_btn.is_visible() {
76                self.scan_qr_code_btn.grab_focus()
77            } else if self.start_sas_btn.is_visible() {
78                self.start_sas_btn.grab_focus()
79            } else {
80                self.cancel_btn.grab_focus()
81            }
82        }
83    }
84
85    impl BinImpl for ChooseMethodPage {}
86
87    impl ChooseMethodPage {
88        /// Set the current identity verification.
89        fn set_verification(&self, verification: Option<&IdentityVerification>) {
90            let prev_verification = self.verification.obj();
91
92            if prev_verification.as_ref() == verification {
93                return;
94            }
95            let obj = self.obj();
96
97            obj.reset();
98
99            if let Some(verification) = prev_verification
100                && let Some(handler) = self.display_name_handler.take()
101            {
102                verification.user().disconnect(handler);
103            }
104            self.verification.disconnect_signals();
105
106            if let Some(verification) = verification {
107                let display_name_handler = verification.user().connect_display_name_notify(clone!(
108                    #[weak]
109                    obj,
110                    move |_| {
111                        obj.update_page();
112                    }
113                ));
114                self.display_name_handler
115                    .replace(Some(display_name_handler));
116
117                let supported_methods_handler =
118                    verification.connect_supported_methods_notify(clone!(
119                        #[weak]
120                        obj,
121                        move |_| {
122                            obj.update_page();
123                        }
124                    ));
125
126                self.verification
127                    .set(verification, vec![supported_methods_handler]);
128            }
129
130            obj.update_page();
131            obj.notify_verification();
132        }
133    }
134}
135
136glib::wrapper! {
137    /// A page that shows a QR code or allows to choose between different flows.
138    pub struct ChooseMethodPage(ObjectSubclass<imp::ChooseMethodPage>)
139        @extends gtk::Widget, adw::Bin,
140        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
141}
142
143#[gtk::template_callbacks]
144impl ChooseMethodPage {
145    pub fn new() -> Self {
146        glib::Object::new()
147    }
148
149    /// Update the UI for the current verification.
150    fn update_page(&self) {
151        let Some(verification) = self.verification() else {
152            return;
153        };
154        let imp = self.imp();
155        let supported_methods = verification.supported_methods();
156
157        let qr_code_visible = if supported_methods.contains(VerificationSupportedMethods::QR_SHOW) {
158            if let Some(qr_code) = verification.qr_code() {
159                imp.qrcode.set_qrcode(qr_code);
160                true
161            } else {
162                false
163            }
164        } else {
165            false
166        };
167        let scan_qr_code_visible =
168            supported_methods.contains(VerificationSupportedMethods::QR_SCAN);
169        let sas_visible = supported_methods.contains(VerificationSupportedMethods::SAS);
170
171        let options_nb =
172            u8::from(qr_code_visible) + u8::from(scan_qr_code_visible) + u8::from(sas_visible);
173        let has_several_options = options_nb > 1;
174
175        if verification.is_self_verification() {
176            imp.title.set_label(&gettext("Verify Session"));
177
178            if qr_code_visible {
179                imp.instructions
180                    .set_label(&gettext("Scan this QR code from the other session."));
181            }
182        } else {
183            let name = verification.user().display_name();
184            imp.title.set_label(&gettext("Verification Request"));
185
186            if qr_code_visible {
187                imp.instructions.set_markup(&gettext_f(
188                    // Translators: Do NOT translate the content between '{' and '}', this is a
189                    // variable name.
190                    "Ask {user} to scan this QR code from their session.",
191                    &[("user", &format!("<b>{name}</b>"))],
192                ));
193            }
194        }
195
196        if !qr_code_visible {
197            if has_several_options {
198                imp.instructions
199                    .set_label(&gettext("Select a verification method to proceed."));
200            } else {
201                imp.instructions
202                    .set_label(&gettext("Click on the verification method to proceed."));
203            }
204        }
205
206        imp.qrcode.set_visible(qr_code_visible);
207        imp.scan_qr_code_btn.set_visible(scan_qr_code_visible);
208        imp.start_sas_btn.set_visible(sas_visible);
209    }
210
211    /// Reset the UI to its initial state.
212    pub fn reset(&self) {
213        let imp = self.imp();
214
215        imp.scan_qr_code_btn.set_is_loading(false);
216        imp.start_sas_btn.set_is_loading(false);
217        imp.cancel_btn.set_is_loading(false);
218
219        self.set_sensitive(true);
220    }
221
222    /// Switch to the screen to scan a QR Code.
223    #[template_callback]
224    async fn start_qr_code_scan(&self) {
225        let Some(verification) = self.verification() else {
226            return;
227        };
228        let imp = self.imp();
229
230        imp.scan_qr_code_btn.set_is_loading(true);
231        self.set_sensitive(false);
232
233        if verification.start_qr_code_scan().await.is_err() {
234            toast!(self, gettext("Could not access camera"));
235            self.reset();
236        }
237    }
238
239    /// Start a SAS verification.
240    #[template_callback]
241    async fn start_sas(&self) {
242        let Some(verification) = self.verification() else {
243            return;
244        };
245        let imp = self.imp();
246
247        imp.start_sas_btn.set_is_loading(true);
248        self.set_sensitive(false);
249
250        if verification.start_sas().await.is_err() {
251            toast!(self, gettext("Could not start emoji verification"));
252            self.reset();
253        }
254    }
255
256    /// Cancel the verification.
257    #[template_callback]
258    async fn cancel(&self) {
259        let Some(verification) = self.verification() else {
260            return;
261        };
262
263        self.imp().cancel_btn.set_is_loading(true);
264        self.set_sensitive(false);
265
266        if verification.cancel().await.is_err() {
267            toast!(self, gettext("Could not cancel the verification"));
268            self.reset();
269        }
270    }
271}