use std::{ borrow::Cow, collections::{HashMap, HashSet}, }; use crossbeam_channel::Receiver; use eframe::egui::{self, Color32, Label, RichText, ScrollArea, Sense}; use matrix_sdk::{ deserialized_responses::SyncResponse, encryption::verification::{SasVerification, Verification, VerificationRequest}, room::{Joined, Room}, ruma::{ events::{ key::verification::VerificationMethod, room::{create::RoomType as CreateRoomType, message::MessageType}, AnyMessageEvent, AnyRoomEvent, AnyToDeviceEvent, }, RoomId, UserId, }, Client, RoomMember, }; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::UnboundedSender; use crate::sync; use super::Id; /// Logged in application state #[derive(Debug)] pub struct App { pub client: matrix_sdk::Client, /// 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)] pub struct RoomList { pub selected_room: Option, pub room_name: HashMap, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct Timeline { messages: Vec, #[serde(skip)] member: HashMap, #[serde(skip)] 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()) { name.into() } else if let Some(name) = room.name() { name.into() } else if let Some(alias) = room.canonical_alias() { String::from(alias).into() } else { room.room_id().to_string().into() } } fn is_selected(&self, room_id: &RoomId) -> bool { self.selected_room .as_ref() .map_or(false, |id| id == room_id) } fn selected_room(&self, client: &Client) -> Option { match self.selected_room { Some(ref selected) => client.get_room(selected), None => None, } } } impl App { pub fn new( client: Client, request: UnboundedSender, response: Receiver, sync_handle: std::thread::JoinHandle<()>, ) -> Self { Self { client, request: RequestSender(request), response, sync_handle: Some(sync_handle), room_list: RoomList::default(), timelines: HashMap::new(), verify_req: None, verify: None, error: None, entry: MessageEntry::default(), } } pub fn update(&mut self, ctx: &egui::CtxRef) { if let Ok(response) = self.response.try_recv() { self.handle_response(response); } egui::SidePanel::left(Id::RoomPanel) .max_width(400.0) .default_width(400.0) .show(ctx, |ui| { ui.add(Label::new(RichText::new("Joined").strong())); 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 { egui::Frame::group(&Default::default()).fill(Color32::from_rgb(0, 0, 0)) }; let response = group.show(ui, |ui| { ui.set_width(ui.available_width()); let name = self.room_list.room_name(&*room); ui.label(&*name); }); let response = response.response.interact(Sense::click()); if response.clicked() { self.room_list.selected_room = Some(room.room_id().clone()); } } }); let room = match self.room_list.selected_room(&self.client) { Some(room) => room, _ => return, }; egui::TopBottomPanel::top(Id::RoomSummary).show(ctx, |ui| { ui.horizontal(|ui| { ui.set_width(ui.available_width()); ui.heading(&*self.room_list.room_name(&room)); if let Some(ref target) = room.direct_target() { ui.label(target.as_str()); } else if let Some(ref alias) = room.canonical_alias() { ui.label(alias.as_str()); } if let Some(ref topic) = room.topic() { ui.label(topic); } }) }); egui::SidePanel::right(Id::MemberList).show(ctx, |ui| { ui.heading("Members"); let timeline = self.timelines.entry(room.room_id().clone()).or_default(); for member in timeline.member.values() { ui.group(|ui| { ui.set_width(ui.available_width()); ui.label(member.name()); }); } }); 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 in emojis { ui.vertical_centered(|ui| { ui.set_max_width(50.0); ui.heading(emoji.symbol); ui.label(emoji.description); }); } } }); 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::vertical().show(ui, |ui| { let timeline = self.timelines.entry(room.room_id().clone()).or_default(); for event in timeline.messages.iter() { let sender = event.sender(); let name = match timeline.member.get(sender) { Some(member) => member.name(), None => { if !timeline.member_pending.contains(sender) { self.request.joined_member(joined.clone(), sender.clone()); } sender.localpart() } }; match event { AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(msg)) => { match &msg.content.msgtype { MessageType::Text(text) => { ui.horizontal_wrapped(|ui| { ui.add(Label::new(RichText::new(name).strong())) .on_hover_text(event.sender().as_str()); ui.label(&text.body); }); } MessageType::Notice(notice) => { ui.horizontal_wrapped(|ui| { ui.add(Label::new(RichText::new(name).strong())) .on_hover_text(event.sender().as_str()); ui.add(egui::Label::new( RichText::new(¬ice.body).weak(), )); }); } MessageType::ServerNotice(notice) => { ui.add(Label::new(RichText::new(name).strong())) .on_hover_text(event.sender().as_str()); ui.add(egui::Label::new(RichText::new(¬ice.body).weak())); } MessageType::Emote(emote) => { 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 { Response::Sync(sync) => self.handle_sync(sync), Response::RoomName(room, name) => { self.room_list.room_name.insert(room, name); } Response::JoinedMember(room, member) => { self.timelines .entry(room) .or_default() .member .insert(member.user_id().clone(), member); } 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()); } }; } 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 { let event = match event.event.deserialize() { Ok(event) => event, Err(_) => continue, }; 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(); } }