daydream

A small matrix web client written in rust
git clone git://archive.git.mtrnord.blog/daydream-mx/daydream.git
Log | Files | Refs | README | LICENSE

commit ebdc916cb49bcf3a7685a07abf0ac0013bb8a5a6
parent b0186dad8a6b9434b12621779b626aaad435d953
Author: Marcel <mtrnord1@gmail.com>
Date:   Tue, 26 May 2020 16:26:22 +0200

Print sync and use some bootstrap css

Took 1 hour 28 minutes

Diffstat:
Msrc/app.rs | 47+++++++++++++++++++++++------------------------
Msrc/app/matrix.rs | 56++++++++++++++++++++++++++++++++++++++++++++------------
Asrc/app/matrix/sync.rs | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/app/views/login.rs | 72++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/app/views/main_view.rs | 35++++++++++++++++++++++++++++-------
Msrc/errors.rs | 4++--
Msrc/lib.rs | 4+---
Mstatic/index.html | 9+++++++++
Mstatic/style.scss | 2++
9 files changed, 213 insertions(+), 78 deletions(-)

diff --git a/src/app.rs b/src/app.rs @@ -2,8 +2,8 @@ use log::*; use yew::{prelude::*, virtual_dom::VNode}; use yew_router::{prelude::*, Switch}; +use crate::app::matrix::{MatrixAgent, Response}; use crate::app::views::{login::Login, main_view::MainView}; -use crate::app::matrix::{Response, MatrixAgent}; use yew_router::agent::RouteRequest::ChangeRoute; mod matrix; @@ -65,10 +65,10 @@ impl Component for App { Msg::NewMessage(response) => { info!("NewMessage: {:#?}", response); match response { - Response::Error(_) => {}, + Response::Error(_) => {} Response::LoggedIn(logged_in) => { info!("client_logged_in"); - info!("{}",logged_in); + info!("{}", logged_in); let route: Route = if logged_in { //self.state.logged_in = true; @@ -83,7 +83,8 @@ impl Component for App { info!("{:#?}", route.clone()); self.route = Some(route.clone()); self.route_agent.send(ChangeRoute(route)); - }, + } + _ => {} } } } @@ -98,28 +99,26 @@ impl Component for App { info!("rendered App!"); info!("Route: {:#?}", &self.route); html! { - <div> - { - match &self.route { - None => {info!("NoneRoute"); html! {<Login />}}, - Some(route) => match AppRoute::switch(route.clone()) { - Some(AppRoute::MainView) => { - info!("MainViewRoute"); - html! { - <MainView /> - } - }, - Some(AppRoute::Start) => { - info!("StartRoute"); - html! { - <Login /> - } - }, - None => VNode::from("404") - } + { + match &self.route { + None => {info!("NoneRoute"); html! {<Login />}}, + Some(route) => match AppRoute::switch(route.clone()) { + Some(AppRoute::MainView) => { + info!("MainViewRoute"); + html! { + <MainView /> + } + }, + Some(AppRoute::Start) => { + info!("StartRoute"); + html! { + <Login /> + } + }, + None => VNode::from("404") } } - </div> + } } } } diff --git a/src/app/matrix.rs b/src/app/matrix.rs @@ -3,7 +3,7 @@ use std::convert::TryFrom; use std::sync::{Arc, Mutex}; use log::*; -use matrix_sdk::{Client, ClientConfig, Session, Error}; +use matrix_sdk::{Client, ClientConfig, Session, SyncSettings}; use serde_derive::{Deserialize, Serialize}; use url::Url; use wasm_bindgen_futures::spawn_local; @@ -14,6 +14,8 @@ use yew::worker::*; use crate::constants::AUTH_KEY; use crate::errors::MatrixError; +mod sync; + #[derive(Serialize, Deserialize, Default, Clone, Debug)] pub struct MatrixClient { pub(crate) homeserver: Option<String>, @@ -34,6 +36,7 @@ pub struct MatrixAgent { link: AgentLink<MatrixAgent>, matrix_state: MatrixClient, matrix_client: Option<Client>, + // TODO make arc mutex :( subscribers: HashSet<HandlerId>, storage: Arc<Mutex<StorageService>>, session: Option<SessionStore>, @@ -46,12 +49,15 @@ pub enum Request { SetPassword(String), Login(), GetLoggedIn, + StartSync, } #[derive(Serialize, Deserialize, Debug, Clone)] pub enum Response { Error(MatrixError), LoggedIn(bool), + // TODO properly handle sync events + Sync(String), } impl Agent for MatrixAgent { @@ -61,7 +67,9 @@ impl Agent for MatrixAgent { type Output = Response; fn create(link: AgentLink<Self>) -> Self { - let storage = Arc::new(Mutex::new(StorageService::new(Area::Local).expect("storage was disabled by the user"))); + let storage = Arc::new(Mutex::new( + StorageService::new(Area::Local).expect("storage was disabled by the user"), + )); let session: Option<SessionStore> = { if let Json(Ok(restored_model)) = storage.lock().unwrap().restore(AUTH_KEY) { Some(restored_model) @@ -112,14 +120,17 @@ impl Agent for MatrixAgent { let password = password.clone(); let client = client.clone(); let subscribers = self.subscribers.clone(); - let mut agent = self.clone(); + let agent = self.clone(); spawn_local(async move { // TODO handle login error if agent.session.is_some() { let stored_session = agent.session.clone().unwrap(); let session = Session { access_token: stored_session.access_token, - user_id: matrix_sdk::identifiers::UserId::try_from(stored_session.user_id.as_str()).unwrap(), + user_id: matrix_sdk::identifiers::UserId::try_from( + stored_session.user_id.as_str(), + ) + .unwrap(), device_id: stored_session.device_id, }; client.restore_login(session).await; @@ -131,7 +142,8 @@ impl Agent for MatrixAgent { None, Some("Daydream".to_string()), ) - .await.unwrap(); + .await + .unwrap(); let session_store = SessionStore { access_token: login_response.access_token, user_id: login_response.user_id.to_string(), @@ -178,6 +190,13 @@ impl Agent for MatrixAgent { } }); } + Request::StartSync => { + // Always clone agent after having tried to login! + let agent = self.clone(); + spawn_local(async move { + agent.start_sync().await; + }); + } } } @@ -186,23 +205,35 @@ impl Agent for MatrixAgent { } } +unsafe impl Send for MatrixAgent {} +unsafe impl std::marker::Sync for MatrixAgent {} + impl MatrixAgent { + async fn start_sync(&self) { + let sync = sync::Sync { + matrix_client: self.matrix_client.clone().unwrap(), + callback: |x: Response| { + for sub in self.subscribers.iter() { + self.link.respond(*sub, x.clone()); + } + }, + }; + sync.start_sync().await; + } + async fn get_logged_in(&self) -> bool { if self.matrix_client.is_none() { return false; } - self.matrix_client - .clone() - .unwrap() - .logged_in() - .await + self.matrix_client.clone().unwrap().logged_in().await } fn login(&mut self) -> Option<Client> { return if (self.matrix_state.homeserver.is_none() || self.matrix_state.username.is_none() || self.matrix_state.password.is_none() - || self.matrix_client.is_some()) && self.session.is_none() + || self.matrix_client.is_some()) + && self.session.is_none() { let resp = Response::Error(MatrixError::MissingFields); for sub in self.subscribers.iter() { @@ -221,7 +252,8 @@ impl MatrixAgent { let stored_session = self.session.clone().unwrap(); let session = Session { access_token: stored_session.access_token, - user_id: matrix_sdk::identifiers::UserId::try_from(stored_session.user_id.as_str()).unwrap(), + user_id: matrix_sdk::identifiers::UserId::try_from(stored_session.user_id.as_str()) + .unwrap(), device_id: stored_session.device_id, }; let client_clone = client.clone(); diff --git a/src/app/matrix/sync.rs b/src/app/matrix/sync.rs @@ -0,0 +1,62 @@ +use crate::app::matrix::Response; +use log::*; +use matrix_sdk::{ + api::r0::sync::sync_events::Response as SyncResponse, + events::collections::all::RoomEvent, + events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent}, + identifiers::RoomId, + Client, SyncSettings, +}; + +pub struct Sync<F> +where + F: Fn(Response) + std::marker::Sync, +{ + pub(crate) matrix_client: Client, + pub(crate) callback: F, +} + +impl<F> Sync<F> +where + F: Fn(Response) + std::marker::Sync, +{ + pub async fn start_sync(&self) { + let client = self.matrix_client.clone(); + client.clone().sync(SyncSettings::default()).await.unwrap(); + + let settings = SyncSettings::default().token(client.clone().sync_token().await.unwrap()); + client + .clone() + .sync_forever(settings, |response| self.on_sync_response(response)) + .await; + } + + async fn on_sync_response(&self, response: SyncResponse) { + info!("Synced"); + + for (room_id, room) in response.rooms.join { + for event in room.timeline.events { + if let Ok(event) = event.deserialize() { + self.on_room_message(&room_id, event).await + } + } + } + } + + async fn on_room_message(&self, room_id: &RoomId, event: RoomEvent) { + // TODO handle all messages... (Extra class?) + let msg_body = if let RoomEvent::RoomMessage(MessageEvent { + content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }), + .. + }) = event + { + msg_body.clone() + } else { + return; + }; + + info!("Received message event {:?}", &msg_body); + let resp = Response::Sync(msg_body); + (self.callback)(resp); + } +} diff --git a/src/app/views/login.rs b/src/app/views/login.rs @@ -20,7 +20,6 @@ pub enum Msg { Nope, } - impl Component for Login { type Message = Msg; type Properties = (); @@ -73,35 +72,48 @@ impl Component for Login { fn view(&self) -> Html { info!("rendered Login!"); html! { - <div> - <input class="server" - placeholder="Homeserver URL" - value=&self.homeserver - oninput=self.link.callback(|e: InputData| Msg::SetHomeserver(e.value)) - //onkeypress=self.link.callback(|e: KeyboardEvent| { - // if e.key() == "Enter" { Msg::Add } else { Msg::Nope } - //}) - /> - <input class="username" - placeholder="MXID/Username" - value=&self.username - oninput=self.link.callback(|e: InputData| Msg::SetUsername(e.value)) - //onkeypress=self.link.callback(|e: KeyboardEvent| { - // if e.key() == "Enter" { Msg::Add } else { Msg::Nope } - //}) - /> - <input class="password" - placeholder="Password" - type="password" - value=&self.password - oninput=self.link.callback(|e: InputData| Msg::SetPassword(e.value)) - /> - <button onclick=self.link.callback(|_: MouseEvent| Msg::Login) - onkeypress=self.link.callback(|e: KeyboardEvent| { - if e.key() == "Enter" { Msg::Login } else { Msg::Nope } - })> - { "Login" } - </button> + <div class="container h-100"> + <div class="row align-items-center h-100"> + <h1>{"Login"}</h1> + <form class="col-6 mx-auto" onsubmit=self.link.callback(|e: Event| {e.prevent_default(); Msg::Login})> + <div class="form-group"> + <label for="homeserver">{"Homeserver URL"}</label> + <input + class="form-control" + type="url" + id="homeserver" + placeholder="Homeserver URL" + value=&self.homeserver + oninput=self.link.callback(|e: InputData| Msg::SetHomeserver(e.value)) + /> + </div> + <div class="form-group"> + <label for="username">{"MXID/Username"}</label> + <input + class="form-control" + id="username" + placeholder="MXID/Username" + value=&self.username + oninput=self.link.callback(|e: InputData| Msg::SetUsername(e.value)) + /> + </div> + <div class="form-group"> + <label for="password">{"Password"}</label> + <input + class="form-control" + type="password" + id="password" + placeholder="Password" + value=&self.password + oninput=self.link.callback(|e: InputData| Msg::SetPassword(e.value)) + /> + </div> + + <button type="submit" class="btn btn-primary"> + { "Login" } + </button> + </form> + </div> </div> } } diff --git a/src/app/views/main_view.rs b/src/app/views/main_view.rs @@ -3,7 +3,8 @@ use serde_derive::{Deserialize, Serialize}; use yew::prelude::*; use yew::ComponentLink; -use crate::app::matrix::{MatrixAgent, Response}; +use crate::app::matrix::{MatrixAgent, Request, Response}; +use wasm_bindgen::__rt::std::collections::{HashMap, HashSet}; pub struct MainView { link: ComponentLink<Self>, @@ -15,8 +16,11 @@ pub enum Msg { NewMessage(Response), } -#[derive(Serialize, Deserialize)] -pub struct State {} +#[derive(Serialize, Deserialize, Default)] +pub struct State { + // TODO handle all events + pub events: HashSet<String>, +} impl Component for MainView { type Message = Msg; @@ -24,8 +28,11 @@ impl Component for MainView { fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self { let matrix_callback = link.callback(Msg::NewMessage); - let matrix_agent = MatrixAgent::bridge(matrix_callback); - let state = State {}; + let mut matrix_agent = MatrixAgent::bridge(matrix_callback); + matrix_agent.send(Request::StartSync); + let state = State { + events: Default::default(), + }; MainView { link, @@ -41,7 +48,11 @@ impl Component for MainView { match response { Response::LoggedIn(v) => { info!("client_logged_in: {}", v); - }, + } + Response::Sync(msg) => { + // TODO handle all events + self.state.events.insert(msg); + } _ => {} } } @@ -56,7 +67,17 @@ impl Component for MainView { fn view(&self) -> Html { info!("rendered MainView!"); html! { - <p>{"Test"}</p> + <div class="container h-100"> + { self.state.events.iter().map(|event| self.get_event(event)).collect::<Html>() } + </div> + } + } +} + +impl MainView { + fn get_event(&self, event: &String) -> Html { + html! { + <><p>{event}</p><br/></> } } } diff --git a/src/errors.rs b/src/errors.rs @@ -1,5 +1,5 @@ -use serde_derive::{Deserialize, Serialize};use thiserror::Error; - +use serde_derive::{Deserialize, Serialize}; +use thiserror::Error; #[derive(Error, Debug, Clone, Serialize, Deserialize)] pub enum MatrixError { diff --git a/src/lib.rs b/src/lib.rs @@ -1,11 +1,10 @@ - #![recursion_limit = "512"] #[macro_use] extern crate cfg_if; mod app; -mod errors; mod constants; +mod errors; use wasm_bindgen::prelude::*; @@ -31,7 +30,6 @@ cfg_if! { } } - // This is the entry point for the web app #[wasm_bindgen] pub fn run_app() -> Result<(), JsValue> { diff --git a/static/index.html b/static/index.html @@ -2,9 +2,18 @@ <html lang="en"> <head> <meta charset="utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>Daydream</title> + <!-- TODO Serve locally!--> + <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> </head> <body> <script src="/daydream.js"></script> + + <!-- TODO Serve locally!--> + <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> + <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script> </body> </html> diff --git a/static/style.scss b/static/style.scss @@ -0,0 +1,2 @@ +html { height: 100%; } +body { height: 100%; }