diff --git a/src/main.rs b/src/main.rs index 74f8378..92be9c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use eframe::NativeOptions; #[cfg(not(target_arch = "wasm32"))] fn main() { let app = ui::App::default(); - let mut options = NativeOptions::default(); - options.transparent = true; + let options = NativeOptions::default(); + //options.transparent = true; eframe::run_native(Box::new(app), options); } diff --git a/src/sync.rs b/src/sync.rs index 7c544e1..82ee35f 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -7,8 +7,11 @@ use std::sync::{ use crossbeam_channel::Sender; use matrix_sdk::{ + encryption::verification::{ + QrVerification, SasVerification, Verification, VerificationRequest, + }, room::Joined, - ruma::{RoomId, UserId}, + ruma::{events::room::message::MessageEventContent, RoomId, UserId}, Client, RoomMember, }; use tokio::sync::{mpsc::UnboundedReceiver, Notify}; @@ -22,10 +25,25 @@ pub enum Request { Login(Url, String, String), /// Restore session Restore(matrix_sdk::Session), - /// Get the calculated name for a room + /// Get the calculated name for a room. RoomName(RoomId), + /// Send a message to a room. + Message(Joined, String), /// Get a member from a joined room JoinedMember(Joined, UserId), + /// Get the verification request with the flow id. + VerifyRequest(UserId, String), + /// Cancel a verification attempt. + VerifyCancel(VerificationRequest), + /// Accept a verification request. + VerifyAccept(VerificationRequest), + /// Find an active verification with the flow id. + VerifyStart(UserId, String), + /// Start SAS verification flow. + VerifyStartSas(VerificationRequest), + VerifySasConfirm(SasVerification), + /// Start QR code verification flow. + VerifyStartQr(VerificationRequest), /// Stop syncing Quit, } @@ -38,6 +56,12 @@ pub enum Response { RoomName(RoomId, String), /// Retrived a member frmo a joined room. JoinedMember(RoomId, RoomMember), + /// Got a verification request. + VerifyRequest(VerificationRequest), + /// Started SAS verification. + VerifySas(SasVerification), + /// Started QR verification. + VerifyQr(QrVerification), /// An error happened while responding to a request Error(matrix_sdk::Error), } @@ -99,7 +123,48 @@ async fn handle_request( }, Request::JoinedMember(room, user) => { if let Some(member) = room.get_member(&user).await? { - response.send(Response::JoinedMember(room.room_id().clone(), member)); + response.send(Response::JoinedMember(room.room_id().clone(), member))?; + } + } + Request::Message(room, message) => { + let event = MessageEventContent::text_plain(message); + room.send(event, None).await?; + } + Request::VerifyRequest(user, flow_id) => { + let verification = client.get_verification_request(&user, &flow_id).await; + if let Some(verification) = verification { + response.send(Response::VerifyRequest(verification))?; + } + } + Request::VerifyCancel(verify) => { + verify.cancel().await?; + } + Request::VerifyAccept(verify) => { + verify.accept().await?; + } + Request::VerifyStart(sender, flow_id) => { + if let Some(verify) = client.get_verification(&sender, &flow_id).await { + match verify { + // TODO: auto-accepting is very naughty + Verification::SasV1(sas) => { + sas.accept().await?; + response.send(Response::VerifySas(sas))? + } + Verification::QrV1(qr) => response.send(Response::VerifyQr(qr))?, + }; + } + } + Request::VerifyStartSas(verify) => { + if let Some(sas) = verify.start_sas().await? { + response.send(Response::VerifySas(sas))?; + } + } + Request::VerifySasConfirm(sas) => { + sas.confirm().await?; + } + Request::VerifyStartQr(verify) => { + if let Some(qr) = verify.generate_qr_code().await? { + response.send(Response::VerifyQr(qr))?; } } Request::Quit => { diff --git a/src/ui.rs b/src/ui.rs index c2aa941..28a0e3f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -34,7 +34,11 @@ impl epi::App for App { storage .get_string("rooms") .and_then(|s| ron::from_str(&s).ok()) - .map(|list| view.room_list = list); + .map(|list| view.room_list = dbg!(list)); + storage + .get_string("entry") + .and_then(|s| ron::from_str(&s).ok()) + .map(|entry| view.entry = entry); } } @@ -49,6 +53,9 @@ impl epi::App for App { if let Ok(rooms) = ron::to_string(&main.room_list) { storage.set_string("rooms", rooms) } + if let Ok(entry) = ron::to_string(&main.entry) { + storage.set_string("entry", entry) + } } } } @@ -56,7 +63,7 @@ impl epi::App for App { fn on_exit(&mut self) { match self.view { View::Main(ref mut main) => { - main.request.send(sync::Request::Quit).ok(); + main.request.quit(); main.sync_handle.take().map(|t| t.join()); } _ => (), @@ -100,9 +107,7 @@ impl View { }); let view = session::App::new(client, req_tx, res_rx, handle); for room in view.client.rooms() { - view.request - .send(sync::Request::RoomName(room.room_id().clone())) - .ok(); + view.request.room_name(room.room_id().clone()); } *self = View::Main(view); @@ -119,9 +124,23 @@ impl Default for View { } } +/// Named ID's for widgets that need one. #[derive(Debug, Clone, Copy, Hash)] pub enum Id { + /// Side panel with the room list. RoomPanel, + /// Top panel with room name an such. RoomSummary, + /// Panel with members of a room. MemberList, + /// Message entry. + MessageEntry, + /// Error message. + ErrorPanel, + /// Panel showing verification requests. + VerificationPanel, + /// Window for a SAS verification session. + SasVerification, + /// Window for a QR verification session. + QrVerification, } diff --git a/src/ui/session.rs b/src/ui/session.rs index 50ded11..f03df1a 100644 --- a/src/ui/session.rs +++ b/src/ui/session.rs @@ -7,9 +7,14 @@ use crossbeam_channel::Receiver; use eframe::egui::{self, Color32, Label, ScrollArea, Sense}; use matrix_sdk::{ deserialized_responses::SyncResponse, - room::Room, + encryption::verification::{SasVerification, Verification, VerificationRequest}, + room::{Joined, Room}, ruma::{ - events::{room::message::MessageType, AnyMessageEvent, AnyRoomEvent}, + events::{ + key::verification::VerificationMethod, + room::{create::RoomType as CreateRoomType, message::MessageType}, + AnyMessageEvent, AnyRoomEvent, AnyToDeviceEvent, + }, RoomId, UserId, }, Client, RoomMember, @@ -25,13 +30,25 @@ use super::Id; #[derive(Debug)] pub struct App { pub client: matrix_sdk::Client, - pub request: UnboundedSender, + /// Request sender. + pub request: RequestSender, + /// Response receiver. pub response: Receiver, + /// Handle to the sync loop thread. pub sync_handle: Option>, + /// Error message. pub error: Option, + /// State of an active verification request. + pub verify_req: Option, + /// State of a started verification session. + pub verify: Option, + /// Data for the room list pub room_list: RoomList, + /// Data for storing a timeline pub timelines: HashMap, + /// Message entry + pub entry: MessageEntry, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] @@ -49,6 +66,17 @@ pub struct Timeline { member_pending: HashSet, } +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct MessageEntry { + text: HashMap, +} + +impl MessageEntry { + pub fn get(&mut self, room: &RoomId) -> &mut String { + self.text.entry(room.clone()).or_default() + } +} + impl RoomList { fn room_name<'a>(&'a self, room: &matrix_sdk::BaseRoom) -> Cow<'a, str> { if let Some(name) = self.room_name.get(room.room_id()) { @@ -85,13 +113,16 @@ impl App { ) -> Self { Self { client, - request, + request: RequestSender(request), response, sync_handle: Some(sync_handle), room_list: RoomList::default(), - error: None, timelines: HashMap::new(), + verify_req: None, + verify: None, + error: None, + entry: MessageEntry::default(), } } @@ -101,11 +132,20 @@ impl App { } egui::SidePanel::left(Id::RoomPanel) - .max_width(800.0) + .max_width(400.0) .default_width(400.0) .show(ctx, |ui| { ui.add(egui::Label::new("Joined").strong()); - for room in self.client.joined_rooms() { + + let mut joined = self.client.joined_rooms(); + joined.sort_by_key(|room| self.room_list.room_name(&room).to_uppercase()); + joined.retain(|room| { + room.create_content() + .and_then(|c| c.room_type) + .map_or(true, |t| t != CreateRoomType::Space) + }); + + for room in joined { let group = if self.room_list.is_selected(room.room_id()) { egui::Frame::group(&Default::default()) } else { @@ -150,11 +190,99 @@ impl App { } }); + if self.error.is_some() { + egui::TopBottomPanel::top(Id::ErrorPanel).show(ctx, |ui| { + if ui.button("x").clicked() { + self.error = None; + return; + } + ui.label(self.error.as_ref().unwrap()); + }); + } + + if let Some(ref verify_req) = self.verify_req { + egui::TopBottomPanel::top(Id::VerificationPanel).show(ctx, |ui| { + ui.label(format!( + "Verification request for {}", + verify_req.other_user_id(), + )); + if !verify_req.is_ready() { + if verify_req.is_cancelled() { + ui.label("Verification attempt canceled"); + return; + } + if verify_req.we_started() { + ui.label("Waiting for other user to accept verification."); + } else { + if ui.button("Accept").clicked() { + self.request.verify_accept(verify_req.clone()) + } + } + if verify_req.is_ready() { + let methods = verify_req.their_supported_methods().unwrap(); + for method in methods { + if method == VerificationMethod::SasV1 { + if ui.button("Verify with emoji").clicked() { + self.request.verify_start_sas(verify_req.clone()); + } + } + } + } + if ui.button("Cancel").clicked() { + self.request.verify_cancel(verify_req.clone()) + } + } + }); + } + if let Some(verify) = self.verify.clone() { + match verify { + Verification::SasV1(sas) => { + egui::Window::new("Emoji verification") + .id(egui::Id::new(Id::SasVerification)) + .fixed_size([500.0, 300.0]) + .show(ctx, |ui| { + if let Some(cancel) = sas.cancel_info() { + ui.label("Verification cancelled"); + ui.label(cancel.reason()); + } + ui.horizontal_wrapped(|ui| { + if let Some(emojis) = sas.emoji() { + for (emoji, name) in emojis { + ui.vertical_centered(|ui| { + ui.set_max_width(50.0); + ui.heading(emoji); + ui.label(name); + }); + } + } + }); + ui.horizontal(|ui| { + if ui.button("Confirm").clicked() { + self.request.verify_sas_confirm(sas); + } + if ui.button("Close").clicked() { + self.verify = None; + } + }) + }); + } + Verification::QrV1(_qr) => { + egui::Window::new("QR code verification") + .id(egui::Id::new(Id::QrVerification)) + .auto_sized() + .show(ctx, |ui| { + ui.label("Not implemented yet oops"); + }); + } + } + } + let joined = match room { Room::Joined(ref room) => room, _ => return, }; + // Main panel with the timeline egui::CentralPanel::default().show(ctx, |ui| { ScrollArea::auto_sized().show(ui, |ui| { let timeline = self.timelines.entry(room.room_id().clone()).or_default(); @@ -164,12 +292,7 @@ impl App { Some(member) => member.name(), None => { if !timeline.member_pending.contains(sender) { - self.request - .send(sync::Request::JoinedMember( - joined.clone(), - sender.clone(), - )) - .ok(); + self.request.joined_member(joined.clone(), sender.clone()); } sender.localpart() } @@ -178,14 +301,18 @@ impl App { AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(msg)) => { match &msg.content.msgtype { MessageType::Text(text) => { - ui.add(Label::new(name).strong()) - .on_hover_text(event.sender()); - ui.label(&text.body); + ui.horizontal_wrapped(|ui| { + ui.add(Label::new(name).strong()) + .on_hover_text(event.sender()); + ui.label(&text.body); + }); } MessageType::Notice(notice) => { - ui.add(Label::new(name).strong()) - .on_hover_text(event.sender()); - ui.add(egui::Label::new(¬ice.body).weak()); + ui.horizontal_wrapped(|ui| { + ui.add(Label::new(name).strong()) + .on_hover_text(event.sender()); + ui.add(egui::Label::new(¬ice.body).weak()); + }); } MessageType::ServerNotice(notice) => { ui.add(Label::new(name).strong()) @@ -196,35 +323,62 @@ impl App { ui.label(format!("* {} {}", name, emote.body)); } _ => (), - } + }; } _ => (), } } - }) + }); + }); + egui::TopBottomPanel::bottom(Id::MessageEntry).show(ctx, |ui| { + ui.add( + egui::TextEdit::multiline(self.entry.get(joined.room_id())) + .desired_width(ui.available_width()), + ); + if ui.button("Send").clicked() { + self.send_message(joined.clone()); + } }); } + fn send_message(&mut self, room: Joined) { + let entry = self.entry.get(room.room_id()); + self.request.message(room, std::mem::take(entry)); + } + fn handle_response(&mut self, response: sync::Response) { + use sync::Response; match response { - sync::Response::RoomName(room, name) => { + Response::Sync(sync) => self.handle_sync(sync), + Response::RoomName(room, name) => { self.room_list.room_name.insert(room, name); } - sync::Response::JoinedMember(room, member) => { + Response::JoinedMember(room, member) => { self.timelines .entry(room) .or_default() .member .insert(member.user_id().clone(), member); } - sync::Response::Error(e) => { + Response::VerifyRequest(verification) => { + self.verify_req = Some(verification); + } + Response::VerifySas(sas) => { + self.verify_req = None; + self.verify = Some(Verification::SasV1(sas)); + } + Response::VerifyQr(qr) => { + self.verify_req = None; + self.verify = Some(Verification::QrV1(qr)); + } + Response::Error(e) => { self.error = Some(e.to_string()); } - sync::Response::Sync(sync) => self.handle_sync(sync), }; } fn handle_sync(&mut self, sync: SyncResponse) { + dbg!(&sync); for (id, room) in sync.rooms.join { let timeline = self.timelines.entry(id.clone()).or_default(); for event in room.timeline.events { @@ -235,5 +389,75 @@ impl App { timeline.messages.push(event.into_full_event(id.clone())); } } + for to_device in sync.to_device.events { + let to_device = match to_device.deserialize() { + Ok(to_device) => to_device, + Err(_) => continue, + }; + match to_device { + AnyToDeviceEvent::KeyVerificationRequest(req) => { + self.request + .verify_request(req.sender, req.content.transaction_id); + } + AnyToDeviceEvent::KeyVerificationStart(start) => { + if self.verify_req.is_none() { + self.request + .verify_start(start.sender, start.content.transaction_id) + } + } + _ => (), + } + } + } +} + +#[derive(Debug)] +pub struct RequestSender(UnboundedSender); + +impl RequestSender { + pub fn room_name(&self, name: RoomId) { + self.0.send(sync::Request::RoomName(name)).ok(); + } + + pub fn joined_member(&self, room: Joined, id: UserId) { + self.0.send(sync::Request::JoinedMember(room, id)).ok(); + } + + pub fn message(&self, room: Joined, message: String) { + self.0.send(sync::Request::Message(room, message)).ok(); + } + + pub fn verify_request(&self, user: UserId, flow_id: String) { + self.0 + .send(sync::Request::VerifyRequest(user, flow_id)) + .ok(); + } + + pub fn verify_cancel(&self, verify: VerificationRequest) { + self.0.send(sync::Request::VerifyCancel(verify)).ok(); + } + + pub fn verify_accept(&self, verify: VerificationRequest) { + self.0.send(sync::Request::VerifyAccept(verify)).ok(); + } + + pub fn verify_start(&self, user: UserId, flow_id: String) { + self.0.send(sync::Request::VerifyStart(user, flow_id)).ok(); + } + + pub fn verify_start_sas(&self, verify: VerificationRequest) { + self.0.send(sync::Request::VerifyStartSas(verify)).ok(); + } + + pub fn verify_sas_confirm(&self, sas: SasVerification) { + self.0.send(sync::Request::VerifySasConfirm(sas)).ok(); + } + + pub fn verify_start_qr(&self, verify: VerificationRequest) { + self.0.send(sync::Request::VerifyStartQr(verify)).ok(); + } + + pub fn quit(&self) { + self.0.send(sync::Request::Quit).ok(); } }