egui-test/src/ui/session.rs

469 lines
17 KiB
Rust

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<sync::Response>,
/// Handle to the sync loop thread.
pub sync_handle: Option<std::thread::JoinHandle<()>>,
/// Error message.
pub error: Option<String>,
/// State of an active verification request.
pub verify_req: Option<VerificationRequest>,
/// State of a started verification session.
pub verify: Option<Verification>,
/// Data for the room list
pub room_list: RoomList,
/// Data for storing a timeline
pub timelines: HashMap<RoomId, Timeline>,
/// Message entry
pub entry: MessageEntry,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct RoomList {
pub selected_room: Option<matrix_sdk::ruma::identifiers::RoomId>,
pub room_name: HashMap<RoomId, String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Timeline {
messages: Vec<AnyRoomEvent>,
#[serde(skip)]
member: HashMap<UserId, RoomMember>,
#[serde(skip)]
member_pending: HashSet<UserId>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct MessageEntry {
text: HashMap<RoomId, String>,
}
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<matrix_sdk::room::Room> {
match self.selected_room {
Some(ref selected) => client.get_room(selected),
None => None,
}
}
}
impl App {
pub fn new(
client: Client,
request: UnboundedSender<sync::Request>,
response: Receiver<sync::Response>,
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(&notice.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(&notice.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<sync::Request>);
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();
}
}