fractal/identity_verification_view/
sas_page.rs

1use std::collections::HashMap;
2
3use adw::subclass::prelude::*;
4use gettextrs::gettext;
5use gtk::{gio, glib, glib::clone, prelude::*, CompositeTemplate};
6
7use super::sas_emoji::SasEmoji;
8use crate::{
9    components::LoadingButton, gettext_f, prelude::*, session::model::IdentityVerification, toast,
10    utils::BoundObjectWeakRef,
11};
12
13mod imp {
14    use std::cell::RefCell;
15
16    use glib::subclass::InitializingObject;
17
18    use super::*;
19
20    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
21    #[template(resource = "/org/gnome/Fractal/ui/identity_verification_view/sas_page.ui")]
22    #[properties(wrapper_type = super::SasPage)]
23    pub struct SasPage {
24        /// The current identity verification.
25        #[property(get, set = Self::set_verification, explicit_notify, nullable)]
26        pub verification: BoundObjectWeakRef<IdentityVerification>,
27        pub display_name_handler: RefCell<Option<glib::SignalHandlerId>>,
28        #[template_child]
29        pub title: TemplateChild<gtk::Label>,
30        #[template_child]
31        pub instructions: TemplateChild<gtk::Label>,
32        #[template_child]
33        pub row_1: TemplateChild<gtk::Box>,
34        #[template_child]
35        pub row_2: TemplateChild<gtk::Box>,
36        #[template_child]
37        pub mismatch_btn: TemplateChild<LoadingButton>,
38        #[template_child]
39        pub match_btn: TemplateChild<LoadingButton>,
40    }
41
42    #[glib::object_subclass]
43    impl ObjectSubclass for SasPage {
44        const NAME: &'static str = "IdentityVerificationSasPage";
45        type Type = super::SasPage;
46        type ParentType = adw::Bin;
47
48        fn class_init(klass: &mut Self::Class) {
49            Self::bind_template(klass);
50            Self::Type::bind_template_callbacks(klass);
51        }
52
53        fn instance_init(obj: &InitializingObject<Self>) {
54            obj.init_template();
55        }
56    }
57
58    #[glib::derived_properties]
59    impl ObjectImpl for SasPage {
60        fn dispose(&self) {
61            if let Some(verification) = self.verification.obj() {
62                if let Some(handler) = self.display_name_handler.take() {
63                    verification.user().disconnect(handler);
64                }
65            }
66        }
67    }
68
69    impl WidgetImpl for SasPage {
70        fn grab_focus(&self) -> bool {
71            self.match_btn.grab_focus()
72        }
73    }
74
75    impl BinImpl for SasPage {}
76
77    impl SasPage {
78        /// Set the current identity verification.
79        fn set_verification(&self, verification: Option<&IdentityVerification>) {
80            let prev_verification = self.verification.obj();
81
82            if prev_verification.as_ref() == verification {
83                return;
84            }
85            let obj = self.obj();
86
87            obj.reset();
88
89            if let Some(verification) = prev_verification {
90                if let Some(handler) = self.display_name_handler.take() {
91                    verification.user().disconnect(handler);
92                }
93            }
94            self.verification.disconnect_signals();
95
96            if let Some(verification) = verification {
97                let display_name_handler = verification.user().connect_display_name_notify(clone!(
98                    #[weak]
99                    obj,
100                    move |_| {
101                        obj.update_labels();
102                    }
103                ));
104                self.display_name_handler
105                    .replace(Some(display_name_handler));
106
107                let sas_data_changed_handler = verification.connect_sas_data_changed(clone!(
108                    #[weak]
109                    obj,
110                    move |_| {
111                        obj.update_labels();
112                        obj.fill_rows();
113                    }
114                ));
115
116                self.verification
117                    .set(verification, vec![sas_data_changed_handler]);
118            }
119
120            obj.update_labels();
121            obj.fill_rows();
122            obj.notify_verification();
123        }
124    }
125}
126
127glib::wrapper! {
128    /// A page to confirm if SAS verification data matches.
129    pub struct SasPage(ObjectSubclass<imp::SasPage>)
130        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
131}
132
133#[gtk::template_callbacks]
134impl SasPage {
135    pub fn new() -> Self {
136        glib::Object::new()
137    }
138
139    /// Update the labels for the current verification.
140    fn update_labels(&self) {
141        let Some(verification) = self.verification() else {
142            return;
143        };
144        let imp = self.imp();
145
146        if verification.is_self_verification() {
147            imp.title.set_label(&gettext("Verify Session"));
148            if verification.sas_supports_emoji() {
149                imp.instructions.set_label(&gettext(
150                    "Check if the same emoji appear in the same order on the other client.",
151                ));
152            } else {
153                imp.instructions.set_label(&gettext(
154                    "Check if the same numbers appear in the same order on the other client.",
155                ));
156            }
157        } else {
158            let name = verification.user().display_name();
159            imp.title.set_markup(&gettext("Verification Request"));
160            if verification.sas_supports_emoji() {
161                imp.instructions.set_markup(&gettext_f(
162                    // Translators: Do NOT translate the content between '{' and '}', this is a
163                    // variable name.
164                    "Ask {user} if they see the following emoji appear in the same order on their screen.",
165                    &[("user", &format!("<b>{name}</b>"))]
166                ));
167            } else {
168                imp.instructions.set_markup(&gettext_f(
169                    // Translators: Do NOT translate the content between '{' and '}', this is a
170                    // variable name.
171                    "Ask {user} if they see the following numbers appear in the same order on their screen.",
172                    &[("user", &format!("<b>{name}</b>"))]
173                ));
174            }
175        }
176    }
177
178    /// Reset the UI to its initial state.
179    pub fn reset(&self) {
180        self.reset_buttons();
181        self.fill_rows();
182    }
183
184    /// Reset the buttons to their initial state.
185    fn reset_buttons(&self) {
186        let imp = self.imp();
187
188        imp.mismatch_btn.set_is_loading(false);
189        imp.match_btn.set_is_loading(false);
190        self.set_sensitive(true);
191    }
192
193    /// Empty the rows.
194    fn clean_rows(&self) {
195        let imp = self.imp();
196
197        while let Some(child) = imp.row_1.first_child() {
198            imp.row_1.remove(&child);
199        }
200
201        while let Some(child) = imp.row_2.first_child() {
202            imp.row_2.remove(&child);
203        }
204    }
205
206    /// Fill the rows with the current SAS data.
207    fn fill_rows(&self) {
208        let Some(verification) = self.verification() else {
209            return;
210        };
211        let imp = self.imp();
212
213        // Make sure the rows are empty.
214        self.clean_rows();
215
216        if let Some(emoji_list) = verification.sas_emoji() {
217            let emoji_i18n = sas_emoji_i18n();
218            for (index, emoji) in emoji_list.iter().enumerate() {
219                let emoji_name = emoji_i18n
220                    .get(emoji.description)
221                    .map_or(emoji.description, String::as_str);
222                let emoji_widget = SasEmoji::new(emoji.symbol, emoji_name);
223
224                if index < 4 {
225                    imp.row_1.append(&emoji_widget);
226                } else {
227                    imp.row_2.append(&emoji_widget);
228                }
229            }
230        } else if let Some((a, b, c)) = verification.sas_decimals() {
231            let container = gtk::Box::builder()
232                .spacing(24)
233                .css_classes(["emoji"])
234                .build();
235            container.append(&gtk::Label::builder().label(a.to_string()).build());
236            container.append(&gtk::Label::builder().label(b.to_string()).build());
237            container.append(&gtk::Label::builder().label(c.to_string()).build());
238            imp.row_1.append(&container);
239        }
240    }
241
242    #[template_callback]
243    async fn data_mismatch(&self) {
244        let Some(verification) = self.verification() else {
245            return;
246        };
247
248        self.imp().mismatch_btn.set_is_loading(true);
249        self.set_sensitive(false);
250
251        if verification.sas_mismatch().await.is_err() {
252            toast!(self, gettext("Could not send that the data does not match"));
253            self.reset_buttons();
254        }
255    }
256
257    #[template_callback]
258    async fn data_match(&self) {
259        let Some(verification) = self.verification() else {
260            return;
261        };
262
263        self.imp().match_btn.set_is_loading(true);
264        self.set_sensitive(false);
265
266        if verification.sas_match().await.is_err() {
267            toast!(
268                self,
269                gettext("Could not send confirmation that the data matches")
270            );
271            self.reset_buttons();
272        }
273    }
274}
275
276/// Get the SAS emoji translations for the current locale.
277///
278/// Returns a map of emoji name to its translation.
279fn sas_emoji_i18n() -> HashMap<String, String> {
280    for lang in glib::language_names()
281        .into_iter()
282        .flat_map(|locale| glib::locale_variants(&locale))
283    {
284        if let Some(emoji_i18n) = gio::resources_lookup_data(
285            &format!("/org/gnome/Fractal/sas-emoji/{lang}.json"),
286            gio::ResourceLookupFlags::NONE,
287        )
288        .ok()
289        .and_then(|data| serde_json::from_slice(&data).ok())
290        {
291            return emoji_i18n;
292        }
293    }
294
295    HashMap::new()
296}