From 735bda23f7e6883a2d5f77281e596ef150f9f6d7 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Tue, 16 Nov 2021 17:53:08 +0100 Subject: [PATCH] verification: Add widget to display incoming verification request --- data/resources/resources.gresource.xml | 1 + data/resources/ui/content.ui | 25 + data/resources/ui/incoming-verification.ui | 455 ++++++++++++++++++ src/meson.build | 1 + src/session/content/mod.rs | 51 +- .../verification/incoming_verification.rs | 425 ++++++++++++++++ src/session/verification/mod.rs | 2 + src/session/verification/verification_list.rs | 39 +- 8 files changed, 986 insertions(+), 13 deletions(-) create mode 100644 data/resources/ui/incoming-verification.ui create mode 100644 src/session/verification/incoming_verification.rs diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index 7ea0ad7c..57a90645 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -44,6 +44,7 @@ ui/room-creation.ui ui/session-verification.ui ui/verification-emoji.ui + ui/incoming-verification.ui ui/qr-code-scanner.ui style.css icons/scalable/actions/send-symbolic.svg diff --git a/data/resources/ui/content.ui b/data/resources/ui/content.ui index 9a56762c..e8b6156a 100644 --- a/data/resources/ui/content.ui +++ b/data/resources/ui/content.ui @@ -54,6 +54,31 @@ + + + vertical + + + + + + crossfade + + + + go-previous-symbolic + content.go-back + + + + + + + + + + + diff --git a/data/resources/ui/incoming-verification.ui b/data/resources/ui/incoming-verification.ui new file mode 100644 index 00000000..f477b60f --- /dev/null +++ b/data/resources/ui/incoming-verification.ui @@ -0,0 +1,455 @@ + + + + + diff --git a/src/meson.build b/src/meson.build index 74d0465f..db22af8f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -98,6 +98,7 @@ sources = files( 'session/verification/mod.rs', 'session/verification/emoji.rs', 'session/verification/identity_verification.rs', + 'session/verification/incoming_verification.rs', 'session/verification/session_verification.rs', 'session/verification/verification_list.rs', ) diff --git a/src/session/content/mod.rs b/src/session/content/mod.rs index 1b0ecbd2..69b6e69b 100644 --- a/src/session/content/mod.rs +++ b/src/session/content/mod.rs @@ -18,7 +18,7 @@ use self::room_history::RoomHistory; use self::state_row::StateRow; use crate::session::sidebar::{Entry, EntryType}; -use crate::session::verification::IdentityVerification; +use crate::session::verification::{IdentityVerification, IncomingVerification, VerificationMode}; use adw::subclass::prelude::*; use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate}; @@ -40,7 +40,7 @@ mod imp { pub session: RefCell>>, pub item: RefCell>, pub error_list: RefCell>, - pub category_handler: RefCell>, + pub signal_handler: RefCell>, #[template_child] pub stack: TemplateChild, #[template_child] @@ -51,6 +51,10 @@ mod imp { pub explore: TemplateChild, #[template_child] pub empty_page: TemplateChild, + #[template_child] + pub verification_page: TemplateChild, + #[template_child] + pub incoming_verification: TemplateChild, } #[glib::object_subclass] @@ -144,6 +148,17 @@ mod imp { _ => unimplemented!(), } } + + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + self.stack + .connect_visible_child_notify(clone!(@weak obj => move |stack| { + let priv_ = imp::Content::from_instance(&obj); + if stack.visible_child().as_ref() != Some(priv_.verification_page.upcast_ref::()) { + priv_.incoming_verification.set_request(None); + } + })); + } } impl WidgetImpl for Content {} @@ -189,22 +204,32 @@ impl Content { return; } - if let Some(category_handler) = priv_.category_handler.take() { + if let Some(signal_handler) = priv_.signal_handler.take() { if let Some(item) = self.item() { - item.disconnect(category_handler); + item.disconnect(signal_handler); } } - if let Some(ref room) = item { - if room.is::() { - let handler_id = room.connect_notify_local( + if let Some(ref item) = item { + if item.is::() { + let handler_id = item.connect_notify_local( Some("category"), clone!(@weak self as obj => move |_, _| { obj.set_visible_child(); }), ); - priv_.category_handler.replace(Some(handler_id)); + priv_.signal_handler.replace(Some(handler_id)); + } + + if item.is::() { + let handler_id = item.connect_notify_local(Some("mode"), clone!(@weak self as obj => move |request, _| { + let request = request.downcast_ref::().unwrap(); + if request.mode() == VerificationMode::Cancelled || request.mode() == VerificationMode::Error || request.mode() == VerificationMode::Dismissed { + obj.set_item(None); + } + })); + priv_.signal_handler.replace(Some(handler_id)); } } @@ -249,7 +274,15 @@ impl Content { priv_.stack.set_visible_child(&*priv_.explore); } Some(o) if o.is::() => { - todo!("Incoming verifications arn't implemented yet"); + if let Some(item) = priv_ + .item + .borrow() + .as_ref() + .and_then(|item| item.downcast_ref::()) + { + priv_.incoming_verification.set_request(Some(item.clone())); + priv_.stack.set_visible_child(&*priv_.verification_page); + } } _ => {} } diff --git a/src/session/verification/incoming_verification.rs b/src/session/verification/incoming_verification.rs new file mode 100644 index 00000000..30d14b08 --- /dev/null +++ b/src/session/verification/incoming_verification.rs @@ -0,0 +1,425 @@ +use adw::subclass::prelude::*; +use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate}; +use log::warn; + +use crate::components::SpinnerButton; +use crate::contrib::screenshot; +use crate::contrib::QRCode; +use crate::contrib::QRCodeExt; +use crate::contrib::QrCodeScanner; +use crate::session::verification::{Emoji, IdentityVerification, VerificationMode}; +use crate::spawn; +use gettextrs::gettext; +use matrix_sdk::encryption::verification::QrVerificationData; + +mod imp { + use super::*; + use glib::subclass::InitializingObject; + use glib::SignalHandlerId; + use std::cell::RefCell; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/org/gnome/FractalNext/incoming-verification.ui")] + pub struct IncomingVerification { + pub request: RefCell>, + #[template_child] + pub qrcode: TemplateChild, + #[template_child] + pub emoji_row_1: TemplateChild, + #[template_child] + pub emoji_row_2: TemplateChild, + #[template_child] + pub emoji_match_btn: TemplateChild, + #[template_child] + pub emoji_not_match_btn: TemplateChild, + #[template_child] + pub start_emoji_btn: TemplateChild, + #[template_child] + pub start_emoji_btn2: TemplateChild, + #[template_child] + pub start_emoji_btn3: TemplateChild, + #[template_child] + pub scan_qr_code_btn: TemplateChild, + #[template_child] + pub accept_btn: TemplateChild, + #[template_child] + pub dismiss_btn: TemplateChild, + #[template_child] + pub take_screenshot_btn2: TemplateChild, + #[template_child] + pub take_screenshot_btn3: TemplateChild, + #[template_child] + pub main_stack: TemplateChild, + #[template_child] + pub qr_code_scanner: TemplateChild, + #[template_child] + pub done_btn: TemplateChild, + pub mode_handler: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for IncomingVerification { + const NAME: &'static str = "IncomingVerification"; + type Type = super::IncomingVerification; + type ParentType = adw::Bin; + + fn class_init(klass: &mut Self::Class) { + SpinnerButton::static_type(); + QRCode::static_type(); + Emoji::static_type(); + QrCodeScanner::static_type(); + + klass.install_action("verification.dismiss", None, move |obj, _, _| { + obj.dismiss(); + }); + + Self::bind_template(klass); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for IncomingVerification { + fn properties() -> &'static [glib::ParamSpec] { + use once_cell::sync::Lazy; + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![glib::ParamSpec::new_object( + "request", + "Request", + "The Object holding the data for the verification", + IdentityVerification::static_type(), + glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY, + )] + }); + + PROPERTIES.as_ref() + } + + fn set_property( + &self, + obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + match pspec.name() { + "request" => obj.set_request(value.get().unwrap()), + _ => unimplemented!(), + } + } + + fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "request" => obj.request().to_value(), + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + self.accept_btn + .connect_clicked(clone!(@weak obj => move |button| { + let priv_ = imp::IncomingVerification::from_instance(&obj); + button.set_loading(true); + priv_.dismiss_btn.set_sensitive(false); + obj.accept(); + })); + + self.emoji_match_btn + .connect_clicked(clone!(@weak obj => move |button| { + let priv_ = imp::IncomingVerification::from_instance(&obj); + button.set_loading(true); + priv_.emoji_not_match_btn.set_sensitive(false); + if let Some(request) = obj.request() { + request.emoji_match(); + } + })); + + self.emoji_not_match_btn + .connect_clicked(clone!(@weak obj => move |button| { + let priv_ = imp::IncomingVerification::from_instance(&obj); + button.set_loading(true); + priv_.emoji_match_btn.set_sensitive(false); + if let Some(request) = obj.request() { + request.emoji_not_match(); + } + })); + + self.start_emoji_btn + .connect_clicked(clone!(@weak obj => move |button| { + let priv_ = imp::IncomingVerification::from_instance(&obj); + button.set_loading(true); + priv_.scan_qr_code_btn.set_sensitive(false); + if let Some(request) = obj.request() { + request.start_sas(); + } + })); + self.start_emoji_btn2 + .connect_clicked(clone!(@weak obj => move |button| { + let priv_ = imp::IncomingVerification::from_instance(&obj); + button.set_loading(true); + priv_.take_screenshot_btn2.set_sensitive(false); + if let Some(request) = obj.request() { + request.start_sas(); + } + })); + self.start_emoji_btn3 + .connect_clicked(clone!(@weak obj => move |button| { + let priv_ = imp::IncomingVerification::from_instance(&obj); + button.set_loading(true); + priv_.take_screenshot_btn3.set_sensitive(false); + if let Some(request) = obj.request() { + request.start_sas(); + } + })); + + self.scan_qr_code_btn + .connect_clicked(clone!(@weak obj => move |button| { + let priv_ = imp::IncomingVerification::from_instance(&obj); + button.set_loading(true); + priv_.start_emoji_btn.set_sensitive(false); + if priv_.qr_code_scanner.has_camera() { + obj.start_scanning(); + } else { + obj.take_screenshot(); + } + })); + + self.take_screenshot_btn2 + .connect_clicked(clone!(@weak obj => move |button| { + let priv_ = imp::IncomingVerification::from_instance(&obj); + button.set_loading(true); + priv_.start_emoji_btn2.set_sensitive(false); + obj.take_screenshot(); + })); + + self.take_screenshot_btn3 + .connect_clicked(clone!(@weak obj => move |button| { + let priv_ = imp::IncomingVerification::from_instance(&obj); + button.set_loading(true); + priv_.start_emoji_btn3.set_sensitive(false); + obj.take_screenshot(); + })); + + self.done_btn.connect_clicked(clone!(@weak obj => move |_| { + obj.dismiss(); + })); + + self.qr_code_scanner + .connect_code_detected(clone!(@weak obj => move |_, data| { + obj.finish_scanning(data); + })); + + self.qr_code_scanner.connect_notify_local( + Some("has-camera"), + clone!(@weak obj => move |_, _| { + obj.update_camera_state(); + }), + ); + obj.update_camera_state(); + } + + fn dispose(&self, obj: &Self::Type) { + if let Some(request) = obj.request() { + if let Some(handler) = self.mode_handler.take() { + request.disconnect(handler); + } + } + } + } + + impl WidgetImpl for IncomingVerification { + fn map(&self, widget: &Self::Type) { + self.parent_map(widget); + widget.update_view(); + } + } + impl BinImpl for IncomingVerification {} +} + +glib::wrapper! { + pub struct IncomingVerification(ObjectSubclass) + @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; +} + +impl IncomingVerification { + pub fn new(request: &IdentityVerification) -> Self { + glib::Object::new(&[("request", request)]).expect("Failed to create IncomingVerification") + } + + pub fn request(&self) -> Option { + let priv_ = imp::IncomingVerification::from_instance(self); + priv_.request.borrow().clone() + } + + pub fn set_request(&self, request: Option) { + let priv_ = imp::IncomingVerification::from_instance(self); + let previous_request = self.request(); + + if previous_request == request { + return; + } + + self.reset(); + + if let Some(previous_request) = previous_request { + if let Some(handler) = priv_.mode_handler.take() { + previous_request.disconnect(handler); + } + } + + if let Some(ref request) = request { + let handler = request.connect_notify_local( + Some("mode"), + clone!(@weak self as obj => move |_, _| { + obj.update_view(); + }), + ); + self.update_view(); + + priv_.mode_handler.replace(Some(handler)); + } + + priv_.request.replace(request); + self.notify("request"); + } + + fn reset(&self) { + let priv_ = imp::IncomingVerification::from_instance(self); + priv_.accept_btn.set_loading(false); + priv_.accept_btn.set_sensitive(true); + priv_.dismiss_btn.set_sensitive(true); + priv_.scan_qr_code_btn.set_loading(false); + priv_.scan_qr_code_btn.set_sensitive(true); + priv_.emoji_not_match_btn.set_loading(false); + priv_.emoji_not_match_btn.set_sensitive(true); + priv_.emoji_match_btn.set_loading(false); + priv_.emoji_match_btn.set_sensitive(true); + priv_.start_emoji_btn.set_loading(false); + priv_.start_emoji_btn.set_sensitive(true); + priv_.start_emoji_btn2.set_loading(false); + priv_.start_emoji_btn2.set_sensitive(true); + priv_.start_emoji_btn3.set_loading(false); + priv_.start_emoji_btn3.set_sensitive(true); + priv_.take_screenshot_btn2.set_loading(false); + priv_.take_screenshot_btn2.set_sensitive(true); + priv_.take_screenshot_btn3.set_loading(false); + priv_.take_screenshot_btn3.set_sensitive(true); + + self.clean_emoji(); + } + + fn clean_emoji(&self) { + let priv_ = imp::IncomingVerification::from_instance(self); + + while let Some(child) = priv_.emoji_row_1.first_child() { + priv_.emoji_row_1.remove(&child); + } + + while let Some(child) = priv_.emoji_row_2.first_child() { + priv_.emoji_row_2.remove(&child); + } + } + + pub fn accept(&self) { + if let Some(request) = self.request() { + request.accept_incoming(); + } + } + + pub fn dismiss(&self) { + if let Some(request) = self.request() { + request.dismiss(); + } + } + + fn update_view(&self) { + let priv_ = imp::IncomingVerification::from_instance(self); + if let Some(request) = self.request() { + match request.mode() { + VerificationMode::IdentityNotFound => { + // TODO: what should we do if we don't find the identity + } + VerificationMode::Requested => { + priv_.main_stack.set_visible_child_name("accept-request"); + } + VerificationMode::QrV1Show => { + if let Some(qrcode) = request.qr_code() { + priv_.qrcode.set_qrcode(qrcode); + priv_.main_stack.set_visible_child_name("qrcode"); + } else { + warn!("Failed to get qrcode for QrVerification"); + request.start_sas(); + } + } + VerificationMode::QrV1Scan => { + self.start_scanning(); + } + VerificationMode::SasV1 => { + self.clean_emoji(); + // TODO: implement sas fallback when emojis arn't supported + if let Some(emoji) = request.emoji() { + for (index, emoji) in emoji.iter().enumerate() { + if index < 4 { + priv_.emoji_row_1.append(&Emoji::new(emoji)); + } else { + priv_.emoji_row_2.append(&Emoji::new(emoji)); + } + } + priv_.main_stack.set_visible_child_name("emoji"); + } + } + VerificationMode::Completed => { + priv_.main_stack.set_visible_child_name("completed"); + } + _ => {} + } + } + } + + fn start_scanning(&self) { + spawn!(clone!(@weak self as obj => async move { + let priv_ = imp::IncomingVerification::from_instance(&obj); + if priv_.qr_code_scanner.start().await { + priv_.main_stack.set_visible_child_name("scan-qr-code"); + } else { + priv_.main_stack.set_visible_child_name("no-camera"); + } + })); + } + + fn take_screenshot(&self) { + spawn!(clone!(@weak self as obj => async move { + let root = obj.root().unwrap(); + if let Some(code) = screenshot::capture(&root).await { + obj.finish_scanning(code); + } else { + obj.reset(); + } + })); + } + + fn finish_scanning(&self, data: QrVerificationData) { + let priv_ = imp::IncomingVerification::from_instance(self); + priv_.qr_code_scanner.stop(); + if let Some(request) = self.request() { + request.scanned_qr_code(data); + } + priv_.main_stack.set_visible_child_name("qr-code-scanned"); + } + + fn update_camera_state(&self) { + let priv_ = imp::IncomingVerification::from_instance(self); + if priv_.qr_code_scanner.has_camera() { + priv_ + .scan_qr_code_btn + .set_label(&gettext("Scan QR code with this session")) + } else { + priv_ + .scan_qr_code_btn + .set_label(&gettext("Take a Screenshot of a Qr Code")) + } + } +} diff --git a/src/session/verification/mod.rs b/src/session/verification/mod.rs index 9fc05594..489f7d8e 100644 --- a/src/session/verification/mod.rs +++ b/src/session/verification/mod.rs @@ -1,9 +1,11 @@ mod emoji; mod identity_verification; +mod incoming_verification; mod session_verification; mod verification_list; pub use self::emoji::Emoji; pub use self::identity_verification::{IdentityVerification, Mode as VerificationMode}; +pub use self::incoming_verification::IncomingVerification; pub use self::session_verification::SessionVerification; pub use self::verification_list::VerificationList; diff --git a/src/session/verification/verification_list.rs b/src/session/verification/verification_list.rs index f3d73421..fedb7a59 100644 --- a/src/session/verification/verification_list.rs +++ b/src/session/verification/verification_list.rs @@ -1,7 +1,10 @@ -use gtk::{gio, glib, prelude::*, subclass::prelude::*}; +use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*}; use matrix_sdk::ruma::{api::client::r0::sync::sync_events::ToDevice, events::AnyToDeviceEvent}; -use crate::session::{verification::IdentityVerification, Session}; +use crate::session::{ + verification::{IdentityVerification, VerificationMode}, + Session, +}; mod imp { use glib::object::WeakRef; @@ -99,8 +102,10 @@ impl VerificationList { let priv_ = imp::VerificationList::from_instance(self); for event in &to_device.events { - if let Ok(AnyToDeviceEvent::KeyVerificationRequest(_event)) = event.deserialize() { - //TODO: implement handling of incomming requests + if let Ok(AnyToDeviceEvent::KeyVerificationRequest(event)) = event.deserialize() { + let request = IdentityVerification::new(self.session().user().unwrap()); + request.set_flow_id(Some(event.content.transaction_id.to_owned())); + self.add(request); } } @@ -115,9 +120,35 @@ impl VerificationList { let length = { let mut list = priv_.list.borrow_mut(); let length = list.len(); + request.connect_notify_local(Some("mode"), clone!(@weak self as obj => move |request, _| { + if request.mode() == VerificationMode::Error || request.mode() == VerificationMode::Cancelled || request.mode() == VerificationMode::Dismissed || request.mode() == VerificationMode::Completed { + obj.remove(request); + } + })); list.push(request); length as u32 }; self.items_changed(length, 0, 1) } + + pub fn remove(&self, request: &IdentityVerification) { + let priv_ = imp::VerificationList::from_instance(self); + let position = { + let mut list = priv_.list.borrow_mut(); + let mut position = None; + for (index, item) in list.iter().enumerate() { + if item == request { + position = Some(index); + break; + } + } + if let Some(position) = position { + list.remove(position); + } + position + }; + if let Some(position) = position { + self.items_changed(position as u32, 1, 0); + } + } }