Basic layout
parent
245fe8e6f5
commit
aa8125dfef
|
@ -826,9 +826,11 @@ dependencies = [
|
||||||
name = "egui-test"
|
name = "egui-test"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
"eframe",
|
"eframe",
|
||||||
"futures",
|
"futures",
|
||||||
"matrix-sdk",
|
"matrix-sdk",
|
||||||
|
"ron",
|
||||||
"serde",
|
"serde",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
|
|
|
@ -7,8 +7,10 @@ resolver = "2"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
crossbeam-channel = "0.5"
|
||||||
eframe = { version = "0.14", features = ["persistence", "http"] }
|
eframe = { version = "0.14", features = ["persistence", "http"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
ron = "0.6"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tokio = { version = "*", features = ["full"] }
|
tokio = { version = "*", features = ["full"] }
|
||||||
url = { version = "2.2", features = ["serde"] }
|
url = { version = "2.2", features = ["serde"] }
|
||||||
|
|
49
src/main.rs
49
src/main.rs
|
@ -1,4 +1,5 @@
|
||||||
pub mod matrix;
|
pub mod matrix;
|
||||||
|
pub mod sync;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
use eframe::NativeOptions;
|
use eframe::NativeOptions;
|
||||||
|
@ -6,51 +7,7 @@ use eframe::NativeOptions;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn main() {
|
fn main() {
|
||||||
let app = ui::App::default();
|
let app = ui::App::default();
|
||||||
let options = NativeOptions::default();
|
let mut options = NativeOptions::default();
|
||||||
|
options.transparent = true;
|
||||||
eframe::run_native(Box::new(app), options);
|
eframe::run_native(Box::new(app), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*#[derive(Clone, Debug, Default)]
|
|
||||||
struct Login {
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl epi::App for Login {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
"retrix"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) {
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
ui.style_mut().body_text_style = egui::TextStyle::Button;
|
|
||||||
ui.add_space(ui.available_height() / 3.0);
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
//ui.style_mut() .visuals .widgets .noninteractive .bg_stroke .color = egui::Color32::TRANSPARENT;
|
|
||||||
ui.group(|ui| {
|
|
||||||
//ui.reset_style();
|
|
||||||
ui.set_max_width(300.0);
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.heading("Test");
|
|
||||||
ui.label("Username");
|
|
||||||
ui.text_edit_singleline(&mut self.username);
|
|
||||||
ui.label("Password:");
|
|
||||||
ui.text_edit_singleline(&mut self.password);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
/*let mut ui = ui.child_ui(
|
|
||||||
egui::Rect::from_center_size(
|
|
||||||
(ui.available_width() / 2.0, ui.available_height() / 2.0).into(),
|
|
||||||
(300.0, 500.0).into(),
|
|
||||||
),
|
|
||||||
egui::Layout::top_down(egui::Align::Min),
|
|
||||||
);
|
|
||||||
ui.heading("Test");
|
|
||||||
ui.label("Username");
|
|
||||||
ui.text_edit_singleline(&mut self.username);
|
|
||||||
ui.label("Password:");
|
|
||||||
ui.text_edit_singleline(&mut self.password);*/
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
use std::path::{Path, PathBuf};
|
//! Utility functions for working matrix-related tasks
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use matrix_sdk::{
|
use matrix_sdk::{
|
||||||
config::ClientConfig,
|
config::ClientConfig,
|
||||||
|
@ -6,6 +11,7 @@ use matrix_sdk::{
|
||||||
ruma::{DeviceIdBox, UserId},
|
ruma::{DeviceIdBox, UserId},
|
||||||
Client,
|
Client,
|
||||||
};
|
};
|
||||||
|
use ron::ser::PrettyConfig;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
@ -20,6 +26,29 @@ pub struct Session {
|
||||||
pub device_id: DeviceIdBox,
|
pub device_id: DeviceIdBox,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
/// Read a `Session` from the filesystem.
|
||||||
|
pub fn from_fs() -> Result<Self, LoginError> {
|
||||||
|
let path = path()?.join("session.ron");
|
||||||
|
let file = File::open(&path)?;
|
||||||
|
let session = ron::de::from_reader(file)?;
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a matrix client and restore this session.
|
||||||
|
pub fn restore(self) -> Result<Client, LoginError> {
|
||||||
|
let session = matrix_sdk::Session {
|
||||||
|
access_token: self.access_token,
|
||||||
|
user_id: self.user_id,
|
||||||
|
device_id: self.device_id,
|
||||||
|
};
|
||||||
|
let client = Client::new(self.homeserver)?;
|
||||||
|
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
runtime.block_on(client.restore_login(session))?;
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a matrix client and log it in to the server at the given URL with the
|
/// Create a matrix client and log it in to the server at the given URL with the
|
||||||
/// given credentials.
|
/// given credentials.
|
||||||
pub fn login(url: &str, user: &str, password: &str) -> Result<Client, LoginError> {
|
pub fn login(url: &str, user: &str, password: &str) -> Result<Client, LoginError> {
|
||||||
|
@ -37,6 +66,8 @@ pub fn login(url: &str, user: &str, password: &str) -> Result<Client, LoginError
|
||||||
user_id: response.user_id,
|
user_id: response.user_id,
|
||||||
device_id: response.device_id,
|
device_id: response.device_id,
|
||||||
};
|
};
|
||||||
|
let file = File::create(path()?.join("session.ron"))?;
|
||||||
|
ron::ser::to_writer_pretty(file, &session, PrettyConfig::new())?;
|
||||||
Ok(client)
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +80,8 @@ pub enum LoginError {
|
||||||
Sdk(matrix_sdk::Error),
|
Sdk(matrix_sdk::Error),
|
||||||
/// I/O error
|
/// I/O error
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
|
/// Serialization error
|
||||||
|
Ron(ron::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for LoginError {
|
impl std::fmt::Display for LoginError {
|
||||||
|
@ -57,6 +90,7 @@ impl std::fmt::Display for LoginError {
|
||||||
LoginError::Url(_) => write!(f, "Invalid homeserver address"),
|
LoginError::Url(_) => write!(f, "Invalid homeserver address"),
|
||||||
LoginError::Sdk(e) => write!(f, "{}", e),
|
LoginError::Sdk(e) => write!(f, "{}", e),
|
||||||
LoginError::Io(e) => write!(f, "Filesystem error: {}", e),
|
LoginError::Io(e) => write!(f, "Filesystem error: {}", e),
|
||||||
|
LoginError::Ron(e) => write!(f, "Serialization error: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,6 +115,12 @@ impl From<std::io::Error> for LoginError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ron::Error> for LoginError {
|
||||||
|
fn from(e: ron::Error) -> Self {
|
||||||
|
LoginError::Ron(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Configuration for `Clients`.
|
/// Configuration for `Clients`.
|
||||||
fn config() -> Result<ClientConfig, std::io::Error> {
|
fn config() -> Result<ClientConfig, std::io::Error> {
|
||||||
Ok(ClientConfig::new().store_path(&path()?))
|
Ok(ClientConfig::new().store_path(&path()?))
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
//! Synchronozation mechanism between gui thread and async runtime
|
||||||
|
|
||||||
|
use std::sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crossbeam_channel::Sender;
|
||||||
|
use matrix_sdk::{ruma::RoomId, Client};
|
||||||
|
use tokio::sync::{mpsc::UnboundedReceiver, Notify};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
static QUIT: Notify = Notify::const_new();
|
||||||
|
|
||||||
|
/// Request to perform an action or retrieve data.
|
||||||
|
pub enum Request {
|
||||||
|
/// Perform login
|
||||||
|
Login(Url, String, String),
|
||||||
|
/// Restore session
|
||||||
|
Restore(matrix_sdk::Session),
|
||||||
|
/// Get the calculated name for a room
|
||||||
|
RoomName(RoomId),
|
||||||
|
/// Stop syncing
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response to a request.
|
||||||
|
pub enum Response {
|
||||||
|
/// Response from the synchronization loop
|
||||||
|
Sync(matrix_sdk::deserialized_responses::SyncResponse),
|
||||||
|
/// Calculated the name for a room
|
||||||
|
RoomName(RoomId, String),
|
||||||
|
/// An error happened while responding to a request
|
||||||
|
Error(matrix_sdk::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the synchronization loop
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn run(client: Client, mut request: UnboundedReceiver<Request>, response: Sender<Response>) {
|
||||||
|
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
runtime.block_on(async move {
|
||||||
|
let quit = Arc::new(AtomicBool::new(false));
|
||||||
|
let sync_handle = {
|
||||||
|
let client = client.clone();
|
||||||
|
let response = response.clone();
|
||||||
|
let quit = quit.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
client
|
||||||
|
.sync_with_callback(Default::default(), |sync| async {
|
||||||
|
response.send(Response::Sync(sync)).ok();
|
||||||
|
match quit.load(Ordering::Acquire) {
|
||||||
|
false => matrix_sdk::LoopCtrl::Continue,
|
||||||
|
true => dbg!(matrix_sdk::LoopCtrl::Break),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
let request = tokio::select! {
|
||||||
|
request = request.recv() => match request {
|
||||||
|
Some(request) => request,
|
||||||
|
None => break,
|
||||||
|
},
|
||||||
|
notif = QUIT.notified() => break,
|
||||||
|
};
|
||||||
|
tokio::spawn(handle_request(
|
||||||
|
request,
|
||||||
|
client.clone(),
|
||||||
|
response.clone(),
|
||||||
|
quit.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
sync_handle.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_request(
|
||||||
|
request: Request,
|
||||||
|
client: Client,
|
||||||
|
response: Sender<Response>,
|
||||||
|
quit: Arc<AtomicBool>,
|
||||||
|
) -> Result<(), SyncError> {
|
||||||
|
match request {
|
||||||
|
Request::Login(_, _, _) => todo!(),
|
||||||
|
Request::Restore(session) => client.restore_login(session).await?,
|
||||||
|
Request::RoomName(room_id) => match client.get_room(&room_id) {
|
||||||
|
Some(room) => response.send(Response::RoomName(room_id, room.display_name().await?))?,
|
||||||
|
None => (),
|
||||||
|
},
|
||||||
|
Request::Quit => {
|
||||||
|
dbg!(quit.store(true, Ordering::SeqCst));
|
||||||
|
QUIT.notify_one();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum SyncError {
|
||||||
|
Sdk(matrix_sdk::Error),
|
||||||
|
Send,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for SyncError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "sync error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for SyncError {}
|
||||||
|
|
||||||
|
impl From<matrix_sdk::Error> for SyncError {
|
||||||
|
fn from(e: matrix_sdk::Error) -> Self {
|
||||||
|
Self::Sdk(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<crossbeam_channel::SendError<T>> for SyncError {
|
||||||
|
fn from(_: crossbeam_channel::SendError<T>) -> Self {
|
||||||
|
Self::Send
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<matrix_sdk::StoreError> for SyncError {
|
||||||
|
fn from(e: matrix_sdk::StoreError) -> Self {
|
||||||
|
Self::Sdk(e.into())
|
||||||
|
}
|
||||||
|
}
|
82
src/ui.rs
82
src/ui.rs
|
@ -1,6 +1,9 @@
|
||||||
use eframe::{egui, epi};
|
use eframe::{egui, epi};
|
||||||
|
|
||||||
use crate::matrix;
|
use crate::{
|
||||||
|
matrix::{self, Session},
|
||||||
|
sync,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
|
@ -16,23 +19,63 @@ impl epi::App for App {
|
||||||
"retrix"
|
"retrix"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setup(
|
||||||
|
&mut self,
|
||||||
|
_ctx: &egui::CtxRef,
|
||||||
|
_frame: &mut epi::Frame<'_>,
|
||||||
|
storage: Option<&dyn epi::Storage>,
|
||||||
|
) {
|
||||||
|
let client = match Session::from_fs().and_then(Session::restore) {
|
||||||
|
Ok(session) => session,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
let view = self.view.make_main(client);
|
||||||
|
if let Some(storage) = storage {
|
||||||
|
storage
|
||||||
|
.get_string("rooms")
|
||||||
|
.and_then(|s| ron::from_str(&s).ok())
|
||||||
|
.map(|list| view.room_list = list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&mut self, storage: &mut dyn epi::Storage) {
|
||||||
|
match self.view {
|
||||||
|
View::Login(ref login) => {
|
||||||
|
if let Ok(login) = ron::to_string(login) {
|
||||||
|
storage.set_string("login", login);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
View::Main(ref main) => {
|
||||||
|
if let Ok(rooms) = ron::to_string(&main.room_list) {
|
||||||
|
storage.set_string("rooms", rooms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_exit(&mut self) {
|
||||||
|
match self.view {
|
||||||
|
View::Main(ref mut main) => {
|
||||||
|
main.request.send(sync::Request::Quit).ok();
|
||||||
|
main.sync_handle.take().map(|t| t.join());
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn update(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) {
|
fn update(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) {
|
||||||
match self.view {
|
match self.view {
|
||||||
View::Login(ref mut login) => {
|
View::Login(ref mut login) => {
|
||||||
if login.update(ctx) {
|
if login.update(ctx) {
|
||||||
let client =
|
let client =
|
||||||
match matrix::login(&login.homeserver, &login.username, &login.password) {
|
match matrix::login(&login.homeserver, &login.user, &login.password) {
|
||||||
Ok(client) => client,
|
Ok(client) => client,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
login.error = Some(e.to_string());
|
login.error = Some(e.to_string());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.view = View::Main(session::App::with_client(client.clone()));
|
self.view.make_main(client);
|
||||||
std::thread::spawn(move || {
|
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
||||||
rt.block_on(client.sync(Default::default()))
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
View::Main(ref mut view) => view.update(ctx),
|
View::Main(ref mut view) => view.update(ctx),
|
||||||
|
@ -47,6 +90,29 @@ pub enum View {
|
||||||
Main(session::App),
|
Main(session::App),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl View {
|
||||||
|
pub fn make_main(&mut self, client: matrix_sdk::Client) -> &mut session::App {
|
||||||
|
let (req_tx, req_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
let (res_tx, res_rx) = crossbeam_channel::bounded(128);
|
||||||
|
let client2 = client.clone();
|
||||||
|
let handle = std::thread::spawn(move || {
|
||||||
|
sync::run(client2, req_rx, res_tx);
|
||||||
|
});
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
*self = View::Main(view);
|
||||||
|
match *self {
|
||||||
|
View::Main(ref mut main) => main,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for View {
|
impl Default for View {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
View::Login(login::Login::default())
|
View::Login(login::Login::default())
|
||||||
|
@ -56,4 +122,6 @@ impl Default for View {
|
||||||
#[derive(Debug, Clone, Copy, Hash)]
|
#[derive(Debug, Clone, Copy, Hash)]
|
||||||
pub enum Id {
|
pub enum Id {
|
||||||
RoomPanel,
|
RoomPanel,
|
||||||
|
RoomSummary,
|
||||||
|
MemberList,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
use eframe::egui::{self, Color32, TextEdit};
|
use eframe::egui::{self, Color32, TextEdit};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct Login {
|
pub struct Login {
|
||||||
pub homeserver: String,
|
pub homeserver: String,
|
||||||
pub username: String,
|
pub user: String,
|
||||||
|
#[serde(skip)]
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -15,12 +18,14 @@ impl Login {
|
||||||
ui.add_space(ui.available_height() / 3.0);
|
ui.add_space(ui.available_height() / 3.0);
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
//ui.style_mut() .visuals .widgets .noninteractive .bg_stroke .color = egui::Color32::TRANSPARENT;
|
//ui.style_mut() .visuals .widgets .noninteractive .bg_stroke .color = egui::Color32::TRANSPARENT;
|
||||||
|
ui.heading("Agrix");
|
||||||
|
ui.add_space(10.0);
|
||||||
ui.group(|ui| {
|
ui.group(|ui| {
|
||||||
//ui.reset_style();
|
//ui.reset_style();
|
||||||
|
ui.set_max_width(300.0);
|
||||||
if let Some(ref error) = self.error {
|
if let Some(ref error) = self.error {
|
||||||
ui.colored_label(Color32::from_rgb(255, 0, 0), error);
|
ui.colored_label(Color32::from_rgb(255, 0, 0), error);
|
||||||
}
|
}
|
||||||
ui.set_max_width(300.0);
|
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.heading("Log in");
|
ui.heading("Log in");
|
||||||
ui.label("Homeserver:");
|
ui.label("Homeserver:");
|
||||||
|
@ -29,7 +34,7 @@ impl Login {
|
||||||
.hint_text("https://example.net"),
|
.hint_text("https://example.net"),
|
||||||
);
|
);
|
||||||
ui.label("Username:");
|
ui.label("Username:");
|
||||||
ui.add(TextEdit::singleline(&mut self.username).hint_text("alice"));
|
ui.add(TextEdit::singleline(&mut self.user).hint_text("alice"));
|
||||||
ui.label("Password:");
|
ui.label("Password:");
|
||||||
ui.add(TextEdit::singleline(&mut self.password).password(true));
|
ui.add(TextEdit::singleline(&mut self.password).password(true));
|
||||||
if ui.button("Log in").clicked() {
|
if ui.button("Log in").clicked() {
|
||||||
|
|
|
@ -1,43 +1,179 @@
|
||||||
use eframe::egui::{self, Sense};
|
use std::{borrow::Cow, collections::HashMap};
|
||||||
use futures::executor::block_on;
|
|
||||||
use matrix_sdk::Client;
|
use crossbeam_channel::Receiver;
|
||||||
|
use eframe::egui::{self, Color32, ScrollArea, Sense};
|
||||||
|
use matrix_sdk::{
|
||||||
|
deserialized_responses::SyncResponse,
|
||||||
|
room::Room,
|
||||||
|
ruma::{events::AnyRoomEvent, RoomId, UserId},
|
||||||
|
Client, RoomMember,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
|
use crate::sync;
|
||||||
|
|
||||||
use super::Id;
|
use super::Id;
|
||||||
|
|
||||||
/// Logged in application state
|
/// Logged in application state
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
client: matrix_sdk::Client,
|
pub client: matrix_sdk::Client,
|
||||||
selected_room: Option<matrix_sdk::ruma::identifiers::RoomId>,
|
pub request: UnboundedSender<sync::Request>,
|
||||||
|
pub response: Receiver<sync::Response>,
|
||||||
|
pub sync_handle: Option<std::thread::JoinHandle<()>>,
|
||||||
|
pub error: Option<String>,
|
||||||
|
|
||||||
|
pub room_list: RoomList,
|
||||||
|
pub timelines: HashMap<RoomId, Timeline>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
impl App {
|
||||||
pub fn with_client(client: Client) -> Self {
|
pub fn new(
|
||||||
|
client: Client,
|
||||||
|
request: UnboundedSender<sync::Request>,
|
||||||
|
response: Receiver<sync::Response>,
|
||||||
|
sync_handle: std::thread::JoinHandle<()>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
client,
|
client,
|
||||||
selected_room: None,
|
request,
|
||||||
|
response,
|
||||||
|
|
||||||
|
sync_handle: Some(sync_handle),
|
||||||
|
room_list: RoomList::default(),
|
||||||
|
error: None,
|
||||||
|
timelines: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, ctx: &egui::CtxRef) {
|
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)
|
egui::SidePanel::left(Id::RoomPanel)
|
||||||
.max_width(800.0)
|
.max_width(800.0)
|
||||||
|
.default_width(400.0)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.add(egui::Label::new("Joined").strong());
|
ui.add(egui::Label::new("Joined").strong());
|
||||||
for room in self.client.joined_rooms() {
|
for room in self.client.joined_rooms() {
|
||||||
let response = ui.group(|ui| {
|
let group = if self.room_list.is_selected(room.room_id()) {
|
||||||
if let Some(name) = room.name() {
|
egui::Frame::group(&Default::default())
|
||||||
ui.label(name.to_string());
|
} else {
|
||||||
}
|
egui::Frame::group(&Default::default()).fill(Color32::from_rgb(0, 0, 0))
|
||||||
if let Some(alias) = room.canonical_alias() {
|
};
|
||||||
ui.label(alias.to_string());
|
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());
|
let response = response.response.interact(Sense::click());
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
self.selected_room = Some(room.room_id().clone());
|
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 room = match room {
|
||||||
|
Room::Joined(room) => room,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
let timeline = self.timelines.entry(room.room_id().clone()).or_default();
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
ScrollArea::auto_sized().show(ui, |ui| {
|
||||||
|
for message in timeline.messages.iter() {
|
||||||
|
ui.label(message.sender().as_str());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_response(&mut self, response: sync::Response) {
|
||||||
|
match response {
|
||||||
|
sync::Response::RoomName(room, name) => {
|
||||||
|
self.room_list.room_name.insert(room, name);
|
||||||
|
}
|
||||||
|
sync::Response::Error(e) => {
|
||||||
|
self.error = Some(e.to_string());
|
||||||
|
}
|
||||||
|
sync::Response::Sync(sync) => self.handle_sync(sync),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_sync(&mut self, sync: SyncResponse) {
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue