fractal/identity_verification_view/
sas_page.rs1use 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 #[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 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 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 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 "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 "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 pub fn reset(&self) {
180 self.reset_buttons();
181 self.fill_rows();
182 }
183
184 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 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 fn fill_rows(&self) {
208 let Some(verification) = self.verification() else {
209 return;
210 };
211 let imp = self.imp();
212
213 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(>k::Label::builder().label(a.to_string()).build());
236 container.append(>k::Label::builder().label(b.to_string()).build());
237 container.append(>k::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
276fn 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}