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 9c583642b50b3fe1ecec6f6ce1ac80e5f70ff31d
parent b027d11fa8ce0b69c22f3fd5ffabc2d9f007ed2d
Author: Marcel <mtrnord1@gmail.com>
Date:   Mon,  1 Jun 2020 11:16:41 +0200

Use sdk structs instead of wrapper types and fix some issues along the way

Took 7 hours 9 minutes

Diffstat:
MCargo.toml | 9++++-----
Mpackage.json | 4+++-
Msrc/app/components/event_list.rs | 174+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/app/components/room_list.rs | 90+++++++++++++++++++++++++++++++++----------------------------------------------
Msrc/app/matrix.rs | 224++++++++++++++++++++++++++++++++++---------------------------------------------
Msrc/app/matrix/sync.rs | 80++++++-------------------------------------------------------------------------
Msrc/app/matrix/types.rs | 139++++++++-----------------------------------------------------------------------
Msrc/app/views/login.rs | 11+++--------
Msrc/app/views/main_view.rs | 54++++++++++++++++--------------------------------------
Msrc/errors.rs | 2--
Msrc/lib.rs | 32+++++++-------------------------
Mtests/web.rs | 3+++
Myarn.lock | 40++++++++++++++++++++++++++++++++++++++++
13 files changed, 311 insertions(+), 551 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "yew-wasm-pack-template" -version = "0.2.0" -authors = ["Yew Maintainers <maintainers@yew.rs"] +name = "daydream" +version = "0.1.0" +authors = ["MTRNord <info@nordgedanken.de>"] edition = "2018" license = "AGPL-v3.0" repository = "https://github.com/MTRNord/Daydream" @@ -11,8 +11,7 @@ description = "A Matrix Web client written in Rust" crate-type = ["cdylib", "rlib"] [dependencies] -cfg-if = "0.1" -console_error_panic_hook = { version = "0.1", optional = true } +console_error_panic_hook = { version = "0.1" } log = "0.4" serde = { version = "1.0", features = ["rc", "derive"] } serde_json = "1.0.53" diff --git a/package.json b/package.json @@ -1,7 +1,8 @@ { "private": true, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "wasm-pack test --firefox --headless", + "tarpaulin-test": "cargo tarpaulin -v", "dev": "webpack --mode development", "build": "webpack --mode production", "start:dev": "webpack-dev-server --mode development", @@ -10,6 +11,7 @@ "devDependencies": { "@wasm-tool/wasm-pack-plugin": "^1.3.1", "copy-webpack-plugin": "^6.0.1", + "cross-env": "^7.0.2", "css-loader": "^3.5.3", "html-webpack-plugin": "^4.3.0", "mini-css-extract-plugin": "^0.9.0", diff --git a/src/app/components/event_list.rs b/src/app/components/event_list.rs @@ -1,10 +1,13 @@ -use linked_hash_set::LinkedHashSet; +use std::collections::HashMap; + use log::*; -use matrix_sdk::identifiers::{EventId, RoomId}; -use serde::{Deserialize, Serialize}; +use matrix_sdk::{ + events::room::message::{MessageEvent, MessageEventContent}, + identifiers::{EventId, RoomId}, + Room, +}; use yew::prelude::*; -use crate::app::matrix::types::MessageWrapper; use crate::app::matrix::{MatrixAgent, Request, Response}; pub struct EventList { @@ -14,11 +17,10 @@ pub struct EventList { props: Props, } -#[derive(Serialize, Deserialize, Default)] +#[derive(Default)] pub struct State { // TODO handle all events - // TODO use roomId based hashmap - pub events: LinkedHashSet<MessageWrapper>, + pub events: HashMap<RoomId, Vec<MessageEvent>>, pub message: Option<String>, } @@ -32,9 +34,7 @@ pub enum Msg { #[derive(Clone, PartialEq, Properties, Debug)] pub struct Props { #[prop_or_default] - pub current_room: Option<RoomId>, - #[prop_or_default] - pub displayname: String, + pub current_room: Option<Room>, } impl Component for EventList { @@ -50,15 +50,9 @@ impl Component for EventList { message: None, }; - if props.clone().current_room.is_some() { - let room_id = props.clone().current_room.clone().unwrap(); - if state - .events - .iter() - .filter(|x| x.room_id.clone().unwrap() == room_id) - .collect::<LinkedHashSet<&MessageWrapper>>() - .is_empty() - { + if props.current_room.is_some() { + let room_id = props.current_room.clone().unwrap().room_id; + if !state.events.contains_key(&room_id) { matrix_agent.send(Request::GetOldMessages((room_id.clone(), None))); } } @@ -75,40 +69,30 @@ impl Component for EventList { match msg { Msg::NewMessage(response) => { match response { - Response::Sync(msg) => { + Response::Sync((room_id, msg)) => { // TODO handle all events - if !(self - .state - .events + if !(self.state.events[&room_id] .iter() .map(|x| x.event_id.clone()) .collect::<Vec<EventId>>() - .contains(&msg.event_id.clone())) + .contains(&msg.event_id)) { - self.state.events.insert(msg); + self.state.events.get_mut(&room_id).unwrap().push(msg); } true } - Response::OldMessages(messages) => { - // TODO this doesn't seem smart - // let mut new_events_map = LinkedHashSet::new(); - info!("{}", self.state.events.len()); - self.state.events = self - .state - .events - .clone() - .into_iter() - .chain(messages) - .collect(); - info!("{}", self.state.events.len()); - /*for event in self.state.events.clone().into_iter() { - new_events_map.insert(event); + Response::OldMessages((room_id, mut messages)) => { + if self.state.events.contains_key(&room_id) { + self.state + .events + .get_mut(&room_id) + .unwrap() + .append(messages.as_mut()); + true + } else { + self.state.events.insert(room_id, messages); + true } - for event in messages.clone().into_iter() { - new_events_map.insert(event); - } - self.state.events = new_events_map.clone();*/ - true } _ => false, @@ -122,7 +106,7 @@ impl Component for EventList { info!("Sending Message"); if self.state.message.is_some() { self.matrix_agent.send(Request::SendMessage(( - self.props.current_room.clone().unwrap(), + self.props.current_room.clone().unwrap().room_id, self.state.message.clone().unwrap(), ))); self.state.message = None; @@ -136,17 +120,9 @@ impl Component for EventList { fn change(&mut self, props: Self::Properties) -> bool { if self.props != props { - info!("{:#?}", props); if props.clone().current_room.is_some() { - let room_id = props.clone().current_room.clone().unwrap(); - if self - .state - .events - .iter() - .filter(|x| x.room_id.clone().unwrap() == room_id) - .collect::<LinkedHashSet<&MessageWrapper>>() - .is_empty() - { + let room_id = props.clone().current_room.unwrap().room_id; + if !self.state.events.contains_key(&room_id) { self.matrix_agent .send(Request::GetOldMessages((room_id.clone(), None))); } @@ -161,23 +137,28 @@ impl Component for EventList { fn view(&self) -> Html { return html! { <div class="container uk-flex uk-flex-column uk-width-5-6 uk-padding uk-padding-remove-bottom" style="height: 100%"> - <h1>{ self.props.displayname.clone() }</h1> + <h1>{ self.props.current_room.as_ref().unwrap().display_name() }</h1> <div class="scrollable" style="height: 100%"> - { self.state.events.iter().filter(|x| x.room_id.clone().unwrap() == self.props.current_room.clone().unwrap()).map(|event| self.get_event(event.clone())).collect::<Html>() } + { + if self.state.events.contains_key(&self.props.current_room.as_ref().unwrap().room_id) { + self.state.events[&self.props.current_room.as_ref().unwrap().room_id].iter().map(|event| self.get_event(event)).collect::<Html>() + } else { + html!{} + } + } <div id="anchor"></div> </div> - <form class="uk-margin" - onsubmit=self.link.callback(|e: FocusEvent| {e.prevent_default(); Msg::Nope}) - onkeypress=self.link.callback(|e: KeyboardEvent| { - if e.key() == "Enter" { Msg::SendMessage } else { Msg::Nope } - })> + <form class="uk-margin" + onsubmit=self.link.callback(|e: FocusEvent| {e.prevent_default(); Msg::Nope}) + onkeypress=self.link.callback(|e: KeyboardEvent| { + if e.key() == "Enter" { Msg::SendMessage } else { Msg::Nope } + })> <div> <div class="uk-inline" style="display: block !important;"> <span class="uk-form-icon" uk-icon="icon: pencil"></span> <input class="uk-input" type="text" - value=&self.state.message.clone().unwrap_or("".to_string()) + value=&self.state.message.as_ref().unwrap_or(&"".to_string()) oninput=self.link.callback(|e: InputData| Msg::SetMessage(e.value)) - /> </div> </div> @@ -190,37 +171,46 @@ impl Component for EventList { impl EventList { // Typeinspection of IDEA breaks with this :D //noinspection RsTypeCheck - fn get_event(&self, event: MessageWrapper) -> Html { - if event.event_type == "m.text" { - html! { - <p>{event.sender_displayname.unwrap_or(event.sender.to_string()).clone()}{": "}{event.content.clone()}</p> + fn get_event(&self, event: &MessageEvent) -> Html { + let sender_displayname = { + let room = self.props.current_room.as_ref().unwrap(); + let member = room.members.get(&event.sender).unwrap(); + member + .display_name + .as_ref() + .map(ToString::to_string) + .unwrap_or(event.sender.to_string()) + }; + match &event.content { + MessageEventContent::Text(text_event) => { + html! { + <p>{sender_displayname}{": "}{text_event.body.clone()}</p> + } + } + MessageEventContent::Notice(notice_event) => { + html! { + <p style="opacity: .6;">{sender_displayname}{": "}{notice_event.body.clone()}</p> + } } - } else if event.event_type == "m.notice" { - html! { - <p style="opacity: .6;">{event.sender_displayname.unwrap_or(event.sender.to_string()).clone()}{": "}{event.content.clone()}</p> + MessageEventContent::Image(image_event) => { + let caption = format!("{}: {}", sender_displayname, image_event.body); + + let image_url = image_event.url.clone().unwrap(); + let thumbnail = match image_event.info.clone().unwrap().thumbnail_url { + None => image_url.clone(), + Some(v) => v, + }; + html! { + <div uk-lightbox=""> + <a class="uk-inline" href=image_url data-caption=caption > + <img src=thumbnail alt=caption /> + </a> + </div> + } } - } else if event.event_type == "m.image" { - let caption = format!( - "{}: {}", - event - .sender_displayname - .unwrap_or(event.sender.to_string()) - .clone(), - event.content.clone() - ); - let thumbnail = match event.info.clone().unwrap().thumbnail_url { - None => event.info.clone().unwrap().url.clone().unwrap(), - Some(v) => v, - }; - html! { - <div uk-lightbox=""> - <a class="uk-inline" href=event.info.clone().unwrap().url.clone().unwrap() data-caption=caption.clone() > - <img src=thumbnail.clone() alt=caption.clone() /> - </a> - </div> + _ => { + html! {} } - } else { - html! {} } } } diff --git a/src/app/components/room_list.rs b/src/app/components/room_list.rs @@ -1,17 +1,16 @@ use std::collections::HashMap; use log::*; -use matrix_sdk::{identifiers::RoomId, js_int::UInt}; +use matrix_sdk::{identifiers::RoomId, js_int::UInt, Room}; use serde::{Deserialize, Serialize}; use wasm_bindgen::JsCast; use web_sys::HtmlElement; use yew::prelude::*; +use yew::utils::document; use yew::{Bridge, Bridged, Component, ComponentLink, Html}; use yewtil::NeqAssign; -use crate::app::matrix::types::SmallRoom; use crate::app::matrix::{MatrixAgent, Request, Response}; -use yew::utils::document; pub struct RoomList { link: ComponentLink<Self>, @@ -22,24 +21,25 @@ pub struct RoomList { pub enum Msg { NewMessage(Response), - ChangeRoom(String), + ChangeRoom(Room), SetFilter(String), ToggleTheme, } #[derive(Serialize, Deserialize, Default)] pub struct State { - rooms: HashMap<RoomId, SmallRoom>, - current_room: Option<String>, + rooms: HashMap<RoomId, Room>, + current_room: Option<Room>, loading: bool, search_query: Option<String>, dark_theme: bool, + did_fetch: bool, } #[derive(Clone, PartialEq, Properties)] pub struct Props { #[prop_or_default] - pub change_room_callback: Callback<(String, String)>, + pub change_room_callback: Callback<Room>, } impl Component for RoomList { @@ -55,6 +55,7 @@ impl Component for RoomList { loading: true, search_query: None, dark_theme: false, + did_fetch: false, }; RoomList { @@ -71,30 +72,23 @@ impl Component for RoomList { Response::JoinedRoomList(rooms) => { self.state.rooms = rooms; self.state.loading = false; + self.state.did_fetch = false; true } // Better Initial Sync Detection Response::SyncPing => { - if self.state.rooms.is_empty() { + if self.state.rooms.is_empty() && !self.state.did_fetch { self.matrix_agent.send(Request::GetJoinedRooms); - return true; + self.state.did_fetch = true; } false } // Handle new rooms from sync - Response::Sync(msg) => { - if !(self - .state - .rooms - .keys() - .map(|x| x.clone()) - .collect::<Vec<RoomId>>() - .contains(&msg.room_id.clone().unwrap())) - { - self.matrix_agent - .send(Request::GetJoinedRoom(msg.room_id.clone().unwrap())); + Response::Sync((room_id, _msg)) => { + if !(self.state.rooms.contains_key(&room_id)) && !self.state.did_fetch { + self.matrix_agent.send(Request::GetJoinedRoom(room_id)); } - true + false } Response::JoinedRoom((room_id, room)) => { self.state.rooms.insert(room_id, room); @@ -103,16 +97,7 @@ impl Component for RoomList { _ => false, }, Msg::ChangeRoom(room) => { - let displayname = self - .state - .rooms - .iter() - .filter(|(id, _)| **id == room) - .map(|(_, room)| room.name.clone()) - .collect::<String>(); - self.props - .change_room_callback - .emit((displayname, room.clone())); + self.props.change_room_callback.emit(room.clone()); self.state.current_room = Some(room); true } @@ -144,17 +129,18 @@ impl Component for RoomList { self.props.neq_assign(props) } + //noinspection RsTypeCheck fn view(&self) -> Html { - if self.state.loading { - return html! { + return if self.state.loading { + html! { <div class="container"> <div class="uk-position-center uk-padding"> <span uk-spinner="ratio: 4.5"></span> </div> </div> - }; + } } else { - return html! { + html! { <div class="container uk-flex uk-flex-column uk-width-1-6" style="height: 100%"> <div class="uk-padding uk-padding-remove-bottom" style="height: 50px"> <form class="uk-search uk-search-default"> @@ -163,17 +149,17 @@ impl Component for RoomList { class="uk-search-input" type="search" placeholder="Filter Rooms..." - value=&self.state.search_query.clone().unwrap_or("".to_string()) + value=&self.state.search_query.as_ref().unwrap_or(&"".to_string()) oninput=self.link.callback(|e: InputData| Msg::SetFilter(e.value)) /> </form> </div> <ul class="scrollable uk-flex uk-flex-column uk-padding uk-nav-default uk-nav-parent-icon uk-padding-remove-bottom" uk-nav=""> <li class="uk-nav-header">{"Rooms"}</li> { - if self.state.search_query.is_none() || (self.state.search_query.clone().unwrap_or("".to_string()) == "".to_string()) { - self.state.rooms.iter().map(|(_, room)| self.get_room(room.clone())).collect::<Html>() + if self.state.search_query.is_none() || (self.state.search_query.as_ref().unwrap_or(&"".to_string()) == &"".to_string()) { + self.state.rooms.iter().map(|(_, room)| self.get_room(room)).collect::<Html>() } else { - self.state.rooms.iter().filter(|(_, room)| room.name.to_lowercase().contains(&self.state.search_query.clone().unwrap().to_lowercase())).map(|(_, room)| self.get_room(room.clone())).collect::<Html>() + self.state.rooms.iter().filter(|(_, room)| room.display_name().to_lowercase().contains(&self.state.search_query.as_ref().unwrap().to_lowercase())).map(|(_, room)| self.get_room(room)).collect::<Html>() } } </ul> @@ -194,17 +180,15 @@ impl Component for RoomList { </label> </div> </div> - }; - } + } + }; } } impl RoomList { - fn get_room(&self, room: SmallRoom) -> Html { - // TODO better linking than onlclick (yew limitation?) - - let classes = if self.state.current_room.clone().is_some() { - if self.state.current_room.clone().unwrap() == room.id.to_string() { + fn get_room(&self, matrix_room: &Room) -> Html { + let classes = if self.state.current_room.is_some() { + if self.state.current_room.as_ref().unwrap().room_id == matrix_room.room_id { "uk-active" } else { "" @@ -213,21 +197,21 @@ impl RoomList { "" }; - let room_id = room.clone().id.to_string(); + let room = matrix_room.clone(); html! { <li class=classes> - <a onclick=self.link.callback(move |e: MouseEvent| Msg::ChangeRoom(room_id.clone()))> - {room.name.clone()} + <a onclick=self.link.callback(move |e: MouseEvent| Msg::ChangeRoom(room.clone()))> + {matrix_room.display_name()} { - if room.unread_notifications.is_some() && room.unread_notifications.unwrap() != UInt::from(0u32) { - html! { <span class="uk-badge uk-margin-small-left">{room.unread_notifications.unwrap()}</span> } + if matrix_room.unread_notifications.is_some() && matrix_room.unread_notifications.unwrap() != UInt::from(0u32) { + html! { <span class="uk-badge uk-margin-small-left">{matrix_room.unread_notifications.unwrap()}</span> } } else { html! {} } } { - if room.unread_highlight.is_some() && room.unread_highlight.unwrap() != UInt::from(0u32) { - html! { <span class="uk-badge red uk-margin-small-left">{room.unread_highlight.unwrap()}</span> } + if matrix_room.unread_highlight.is_some() && matrix_room.unread_highlight.unwrap() != UInt::from(0u32) { + html! { <span class="uk-badge red uk-margin-small-left">{matrix_room.unread_highlight.unwrap()}</span> } } else { html! {} } diff --git a/src/app/matrix.rs b/src/app/matrix.rs @@ -1,14 +1,17 @@ use std::collections::{HashMap, HashSet}; -use std::convert::{TryFrom, TryInto}; +use std::convert::TryFrom; use std::sync::{Arc, Mutex}; +use comrak::{format_html, parse_document, Arena, ComrakOptions}; use futures_locks::RwLock; -use linked_hash_set::LinkedHashSet; use log::*; use matrix_sdk::{ api::r0::{filter::RoomEventFilter, message::get_message_events::Direction}, - events::collections::all::RoomEvent, - events::room::message::{MessageEventContent, TextMessageEventContent}, + events::{ + collections::all::RoomEvent, + room::message::{MessageEvent, MessageEventContent, TextMessageEventContent}, + EventJson, + }, identifiers::RoomId, js_int::UInt, Client, ClientConfig, MessagesRequestBuilder, Room, Session, @@ -20,10 +23,9 @@ use yew::format::Json; use yew::services::{storage::Area, StorageService}; use yew::worker::*; -use crate::app::matrix::types::{ImageInfoWrapper, MessageWrapper, SmallRoom}; +use crate::app::matrix::types::get_media_download_url; use crate::constants::AUTH_KEY; use crate::errors::MatrixError; -use comrak::{format_html, parse_document, Arena, ComrakOptions}; mod sync; pub mod types; @@ -69,21 +71,20 @@ pub enum Request { SendMessage((RoomId, String)), } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Debug, Clone)] pub enum Response { Error(MatrixError), LoggedIn(bool), // TODO properly handle sync events - Sync(MessageWrapper), + Sync((RoomId, MessageEvent)), SyncPing, - FinishedFirstSync, - JoinedRoomList(HashMap<RoomId, SmallRoom>), + JoinedRoomList(HashMap<RoomId, Room>), Userdata(), - OldMessages(LinkedHashSet<MessageWrapper>), - JoinedRoom((RoomId, SmallRoom)), + OldMessages((RoomId, Vec<MessageEvent>)), + JoinedRoom((RoomId, Room)), } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Debug, Clone)] pub enum Msg { OnSyncResponse(Response), } @@ -131,13 +132,13 @@ impl Agent for MatrixAgent { fn handle_input(&mut self, msg: Self::Input, _: HandlerId) { match msg { Request::SetHomeserver(homeserver) => { - self.matrix_state.homeserver = Some(homeserver.clone()); + self.matrix_state.homeserver = Some(homeserver); } Request::SetUsername(username) => { - self.matrix_state.username = Some(username.clone()); + self.matrix_state.username = Some(username); } Request::SetPassword(password) => { - self.matrix_state.password = Some(password.clone()); + self.matrix_state.password = Some(password); } Request::Login() => { let login_client = self.login(); @@ -148,18 +149,15 @@ impl Agent for MatrixAgent { } return; } - let client = login_client.clone().unwrap(); + let client = login_client.unwrap(); let username = self.matrix_state.username.clone().unwrap(); let password = self.matrix_state.password.clone().unwrap(); - let username = username.clone(); - let password = password.clone(); - let client = client.clone(); let subscribers = self.subscribers.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 stored_session = agent.session.unwrap(); let session = Session { access_token: stored_session.access_token, user_id: matrix_sdk::identifiers::UserId::try_from( @@ -171,19 +169,14 @@ impl Agent for MatrixAgent { client.restore_login(session).await; } else { let login_response: matrix_sdk::api::r0::session::login::Response = client - .login( - username.clone(), - password.clone(), - None, - Some("Daydream".to_string()), - ) + .login(username, password, None, Some("Daydream".to_string())) .await .unwrap(); let session_store = SessionStore { access_token: login_response.access_token, user_id: login_response.user_id.to_string(), device_id: login_response.device_id, - homeserver_url: client.homeserver().clone().into_string(), + homeserver_url: client.homeserver().to_string(), }; let mut storage = agent.storage.lock().unwrap(); storage.store(AUTH_KEY, Json(&session_store)); @@ -196,7 +189,6 @@ impl Agent for MatrixAgent { }); } Request::GetLoggedIn => { - let subscribers = self.subscribers.clone(); let login_client = self.login(); if login_client.is_none() { for sub in self.subscribers.iter() { @@ -215,8 +207,8 @@ impl Agent for MatrixAgent { if !logged_in && agent.session.is_some() { error!("Not logged in but got session"); } else { - for sub in subscribers.iter() { - let resp = Response::LoggedIn(logged_in.clone()); + for sub in agent.subscribers.iter() { + let resp = Response::LoggedIn(logged_in); agent.link.respond(*sub, resp); } } @@ -231,27 +223,18 @@ impl Agent for MatrixAgent { } Request::GetJoinedRooms => { let agent = self.clone(); - let client = agent.matrix_client.clone().unwrap(); spawn_local(async move { + let rooms: Arc<RwLock<HashMap<RoomId, Arc<RwLock<Room>>>>> = + agent.matrix_client.unwrap().joined_rooms(); + + let readable_rooms = rooms.read().await; + let mut rooms_unarced: HashMap<RoomId, Room> = HashMap::new(); + for (id, room) in readable_rooms.iter() { + let unarced_room = (*room.write().await).clone(); + rooms_unarced.insert(id.clone(), unarced_room); + } for sub in agent.subscribers.iter() { - let rooms: Arc<RwLock<HashMap<RoomId, Arc<RwLock<Room>>>>> = - client.clone().joined_rooms(); - let mut rooms_list_hack = HashMap::new(); - for (id, room) in rooms.read().await.iter() { - let small_room = SmallRoom { - name: room.read().await.display_name(), - unread_notifications: room - .read() - .await - .unread_notifications - .clone(), - unread_highlight: room.read().await.unread_highlight.clone(), - id: id.clone(), - }; - rooms_list_hack.insert(id.clone(), small_room); - } - - let resp = Response::JoinedRoomList(rooms_list_hack); + let resp = Response::JoinedRoomList(rooms_unarced.clone()); agent.link.respond(*sub, resp); } }); @@ -261,14 +244,21 @@ impl Agent for MatrixAgent { } Request::GetOldMessages((room_id, from)) => { let agent = self.clone(); - let client = self.matrix_client.clone().unwrap(); spawn_local(async move { let mut builder = &mut MessagesRequestBuilder::new(); builder = builder.room_id(room_id.clone()); if from.is_some() { builder = builder.from(from.unwrap()); } else { - builder = builder.from(client.clone().sync_token().await.unwrap()); + builder = builder.from( + agent + .matrix_client + .clone() + .unwrap() + .sync_token() + .await + .unwrap(), + ); } let filter = RoomEventFilter { types: Some(vec!["m.room.message".to_string()]), @@ -279,93 +269,73 @@ impl Agent for MatrixAgent { .direction(Direction::Backward) .limit(UInt::new(30).unwrap()); - let messsages = client.room_messages(builder.clone()).await.unwrap(); + // TODO handle error gracefully + let messsages = agent + .matrix_client + .clone() + .unwrap() + .room_messages(builder.clone()) + .await + .unwrap(); // TODO save end point for future loading - let mut wrapped_messages: LinkedHashSet<MessageWrapper> = LinkedHashSet::new(); - for event in messsages.chunk.iter().rev() { - if let Ok(event) = event.deserialize() { - match event { - RoomEvent::RoomMessage(event) => { - let wrapped_event_result: Result<MessageWrapper, MatrixError> = - event.try_into(); - match wrapped_event_result { - Ok(mut wrapped_event) => { - if wrapped_event.room_id.is_none() { - wrapped_event.room_id = Some(room_id.clone()); - } - - wrapped_event.sender_displayname = Some( - wrapped_event.get_displayname(client.clone()).await, - ); - - // Convert mxc URLs - if wrapped_event.info.is_some() { - let mxc_url = wrapped_event - .info - .clone() - .unwrap() - .url - .clone() - .unwrap(); - let download_url = wrapped_event - .get_media_download_url( - client.clone(), - mxc_url, - ); - let mxc_thumbnail_url = wrapped_event - .info - .clone() - .unwrap() - .thumbnail_url - .clone() - .unwrap(); - let thumbnail_download_url = wrapped_event - .get_media_download_url( - client.clone(), - mxc_thumbnail_url, - ); - wrapped_event.info = Some(ImageInfoWrapper { - url: Some(download_url), - thumbnail_url: Some(thumbnail_download_url), - }); - } - - wrapped_messages.insert(wrapped_event); - } - Err(_) => { - // Ignore events we cant parse - continue; - } - } + let mut wrapped_messages: Vec<MessageEvent> = Vec::new(); + let chunk_iter: Vec<EventJson<RoomEvent>> = messsages.chunk; + let (oks, _): (Vec<_>, Vec<_>) = chunk_iter + .iter() + .map(|event| event.deserialize()) + .partition(Result::is_ok); + + let deserialized_events: Vec<RoomEvent> = + oks.into_iter().map(Result::unwrap).collect(); + + for event in deserialized_events.into_iter().rev() { + if let RoomEvent::RoomMessage(mut event) = event { + if let MessageEventContent::Image(mut image_event) = event.content { + if image_event.url.is_some() { + let new_url = Some(get_media_download_url( + agent.matrix_client.clone().unwrap(), + image_event.url.unwrap(), + )); + image_event.url = new_url; } - _ => { - continue; + if image_event.info.is_some() { + let mut info = image_event.info.unwrap(); + if info.thumbnail_url.is_some() { + let new_url = Some(get_media_download_url( + agent.matrix_client.clone().unwrap(), + info.thumbnail_url.unwrap(), + )); + info.thumbnail_url = new_url; + } + image_event.info = Some(info); } + event.content = MessageEventContent::Image(image_event); } + wrapped_messages.push(event.clone()); } } for sub in agent.subscribers.iter() { - let resp = Response::OldMessages(wrapped_messages.clone()); + let resp = + Response::OldMessages((room_id.clone(), wrapped_messages.clone())); agent.link.respond(*sub, resp); } }); } Request::GetJoinedRoom(room_id) => { let agent = self.clone(); - let client = self.matrix_client.clone().unwrap(); spawn_local(async move { - let room: Arc<RwLock<Room>> = - client.get_joined_room(&room_id.clone()).await.unwrap(); - let small_room = SmallRoom { - name: room.read().await.display_name(), - unread_notifications: room.read().await.unread_notifications.clone(), - unread_highlight: room.read().await.unread_highlight.clone(), - id: room_id.clone(), - }; + let room: Arc<RwLock<Room>> = agent + .matrix_client + .unwrap() + .get_joined_room(&room_id) + .await + .unwrap(); + let read_clone = room.clone().read().await; + let clean_room = (*read_clone).clone(); for sub in agent.subscribers.iter() { - let resp = Response::JoinedRoom((room_id.clone(), small_room.clone())); + let resp = Response::JoinedRoom((room_id.clone(), clean_room.clone())); agent.link.respond(*sub, resp); } }); @@ -375,8 +345,7 @@ impl Agent for MatrixAgent { spawn_local(async move { let arena = Arena::new(); - let root = - parse_document(&arena, message.clone().as_str(), &ComrakOptions::default()); + let root = parse_document(&arena, message.as_str(), &ComrakOptions::default()); let mut html = vec![]; format_html(root, &ComrakOptions::default(), &mut html).unwrap(); @@ -386,9 +355,8 @@ impl Agent for MatrixAgent { let content; if formatted_message == message { - content = MessageEventContent::Text(TextMessageEventContent::new_plain( - message.clone(), - )); + content = + MessageEventContent::Text(TextMessageEventContent::new_plain(message)); } else { content = MessageEventContent::Text(TextMessageEventContent { body: message.into(), @@ -425,7 +393,7 @@ impl MatrixAgent { if self.matrix_client.is_none() { return false; } - self.matrix_client.clone().unwrap().logged_in().await + self.matrix_client.as_ref().unwrap().logged_in().await } fn login(&mut self) -> Option<Client> { diff --git a/src/app/matrix/sync.rs b/src/app/matrix/sync.rs @@ -1,16 +1,13 @@ -use std::convert::TryInto; +use std::time::Duration; use log::*; use matrix_sdk::{ api::r0::sync::sync_events::Response as SyncResponse, events::collections::all::RoomEvent, identifiers::RoomId, Client, SyncSettings, }; +use yew::Callback; -use crate::app::matrix::types::{MessageWrapper, ImageInfoWrapper}; use crate::app::matrix::Response; -use crate::errors::MatrixError; -use yew::Callback; -use std::time::Duration; pub struct Sync { pub(crate) matrix_client: Client, @@ -20,31 +17,16 @@ pub struct Sync { impl Sync { pub async fn start_sync(&self) { let client = self.matrix_client.clone(); - let resp = client.clone().sync(SyncSettings::default()).await; - match resp { - _ => { - let resp = Response::FinishedFirstSync; - self.callback.emit(resp); - } - } - let settings = match client.clone().sync_token().await { - None => { - SyncSettings::default().timeout(Duration::from_secs(30)).full_state(true) - }, - Some(token) => { - SyncSettings::default().token(token).timeout(Duration::from_secs(30)).full_state(true) - }, - }; + let settings = SyncSettings::default().timeout(Duration::from_secs(30)); client - .clone() .sync_forever(settings, |response| self.on_sync_response(response)) .await; } async fn on_sync_response(&self, response: SyncResponse) { for (room_id, room) in response.rooms.join { - // Is there a smarter way? + // FIXME: Is there a smarter way? let resp = Response::SyncPing; self.callback.emit(resp); for event in room.timeline.events { @@ -60,58 +42,8 @@ impl Sync { match event { RoomEvent::RoomMessage(event) => { - let wrapped_event_result: Result<MessageWrapper, MatrixError> = event.try_into(); - match wrapped_event_result { - Ok(mut wrapped_event) => { - if wrapped_event.room_id.is_none() { - wrapped_event.room_id = Some(room_id.clone()); - } - - wrapped_event.sender_displayname = Some( - wrapped_event - .get_displayname(self.matrix_client.clone()) - .await, - ); - - // Convert mxc URLs - if wrapped_event.info.is_some() { - let mxc_url = wrapped_event - .info - .clone() - .unwrap() - .url - .clone() - .unwrap(); - let download_url = wrapped_event - .get_media_download_url( - self.matrix_client.clone(), - mxc_url, - ); - let mxc_thumbnail_url = wrapped_event - .info - .clone() - .unwrap() - .thumbnail_url - .clone() - .unwrap(); - let thumbnail_download_url = wrapped_event - .get_media_download_url( - self.matrix_client.clone(), - mxc_thumbnail_url, - ); - wrapped_event.info = Some(ImageInfoWrapper { - url: Some(download_url), - thumbnail_url: Some(thumbnail_download_url), - }); - } - let resp = Response::Sync(wrapped_event); - self.callback.emit(resp); - } - Err(_) => { - // Ignore events we cant parse - return; - } - } + let resp = Response::Sync((room_id.clone(), event)); + self.callback.emit(resp); } _ => { return; diff --git a/src/app/matrix/types.rs b/src/app/matrix/types.rs @@ -1,126 +1,15 @@ -use std::convert::TryFrom; -use std::sync::Arc; - -use futures_locks::RwLock; -use matrix_sdk::{ - events::room::message::{MessageEvent, MessageEventContent}, - identifiers::{EventId, RoomId, UserId}, - js_int::UInt, - Client, Room, -}; -use serde::{Deserialize, Serialize}; - -use crate::errors::MatrixError; - -// TODO: Add Into trait -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct SmallRoom { - pub(crate) name: String, - pub(crate) unread_notifications: Option<UInt>, - pub(crate) unread_highlight: Option<UInt>, - pub(crate) id: RoomId, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Hash, Eq, PartialEq)] -pub struct ImageInfoWrapper { - pub(crate) url: Option<String>, - pub(crate) thumbnail_url: Option<String>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Hash, Eq, PartialEq)] -pub struct MessageWrapper { - pub(crate) sender_displayname: Option<String>, - pub(crate) room_id: Option<RoomId>, - pub(crate) event_id: EventId, - pub(crate) event_type: String, - pub(crate) sender: UserId, - // TODO use ruma structs - pub(crate) content: String, - pub(crate) info: Option<ImageInfoWrapper>, -} - -impl MessageWrapper { - pub async fn get_displayname(&self, client: Client) -> String { - let room: Arc<RwLock<Room>> = client - .get_joined_room(&self.room_id.clone().unwrap()) - .await - .unwrap(); - let room = room.read().await; - let member = match room.members.get(&self.sender.clone()) { - None => { - return self.sender.clone().to_string(); - } - Some(v) => v, - }; - member - .display_name - .as_ref() - .map(ToString::to_string) - .unwrap_or(self.sender.clone().to_string()) - } - - pub fn get_media_download_url(&self, client: Client, mxc_url: String) -> String { - let url_parts_raw = mxc_url.clone().replace("mxc://", ""); - let url_parts: Vec<&str> = url_parts_raw.split("/").collect(); - let server_name = url_parts.first().unwrap().to_string(); - let media_id = url_parts.last().unwrap().to_string(); - let new_path = format!( - "_matrix/media/r0/download/{}/{}/fix.jpg", - server_name.clone(), - media_id.clone() - ); - let mut new_url = client.clone().homeserver().clone(); - new_url.set_path(new_path.as_str()); - new_url.to_string() - } -} - -impl TryFrom<MessageEvent> for MessageWrapper { - type Error = MatrixError; - - fn try_from(event: MessageEvent) -> Result<Self, Self::Error> { - return match event.content { - MessageEventContent::Text(text_event) => { - Ok(MessageWrapper { - sender_displayname: None, // We cant get it without a Client and therefor cant calculate it here - room_id: event.room_id, - event_id: event.event_id, - event_type: "m.text".to_string(), - sender: event.sender, - content: text_event.body, - info: None, - }) - } - MessageEventContent::Image(image_event) => { - let thumbnail_url = match image_event.info { - None => None, - Some(i) => i.thumbnail_url, - }; - Ok(MessageWrapper { - sender_displayname: None, // We cant get it without a Client and therefor cant calculate it here - room_id: event.room_id, - event_id: event.event_id, - event_type: "m.image".to_string(), - sender: event.sender, - content: image_event.body, - info: Some(ImageInfoWrapper { - url: image_event.url, - thumbnail_url, - }), - }) - } - MessageEventContent::Notice(notice_event) => { - Ok(MessageWrapper { - sender_displayname: None, // We cant get it without a Client and therefor cant calculate it here - room_id: event.room_id, - event_id: event.event_id, - event_type: "m.notice".to_string(), - sender: event.sender, - content: notice_event.body, - info: None, - }) - } - _ => Err(MatrixError::UnsupportedEvent), - }; - } +use matrix_sdk::Client; + +pub fn get_media_download_url(client: Client, mxc_url: String) -> String { + let url_parts_raw = mxc_url.replace("mxc://", ""); + let url_parts: Vec<&str> = url_parts_raw.split("/").collect(); + let server_name = url_parts.first().unwrap().to_string(); + let media_id = url_parts.last().unwrap().to_string(); + let new_path = format!( + "_matrix/media/r0/download/{}/{}/fix.jpg", + server_name, media_id, + ); + let mut new_url = client.homeserver().clone(); + new_url.set_path(new_path.as_str()); + new_url.to_string() } diff --git a/src/app/views/login.rs b/src/app/views/login.rs @@ -17,7 +17,6 @@ pub enum Msg { SetUsername(String), SetPassword(String), Login, - Nope, } impl Component for Login { @@ -40,27 +39,23 @@ impl Component for Login { match msg { Msg::SetHomeserver(homeserver) => { self.homeserver = homeserver.clone(); - self.matrix_agent - .send(Request::SetHomeserver(homeserver.clone())); + self.matrix_agent.send(Request::SetHomeserver(homeserver)); true } Msg::SetUsername(username) => { self.username = username.clone(); - self.matrix_agent - .send(Request::SetUsername(username.clone())); + self.matrix_agent.send(Request::SetUsername(username)); true } Msg::SetPassword(password) => { self.password = password.clone(); - self.matrix_agent - .send(Request::SetPassword(password.clone())); + self.matrix_agent.send(Request::SetPassword(password)); true } Msg::Login => { self.matrix_agent.send(Request::Login()); false } - Msg::Nope => false, } } diff --git a/src/app/views/main_view.rs b/src/app/views/main_view.rs @@ -1,32 +1,24 @@ -use std::convert::TryFrom; - -use linked_hash_set::LinkedHashSet; use log::*; -use matrix_sdk::identifiers::RoomId; +use matrix_sdk::Room; use serde::{Deserialize, Serialize}; use yew::prelude::*; use yew::ComponentLink; use crate::app::components::{event_list::EventList, room_list::RoomList}; -use crate::app::matrix::types::MessageWrapper; -use crate::app::matrix::{MatrixAgent, Request, Response}; +use crate::app::matrix::{MatrixAgent, Request}; pub struct MainView { link: ComponentLink<Self>, state: State, - matrix_agent: Box<dyn Bridge<MatrixAgent>>, } pub enum Msg { - NewMessage(Response), - ChangeRoom((String, String)), + ChangeRoom(Room), } #[derive(Serialize, Deserialize, Default)] pub struct State { - // TODO handle all events - pub events: LinkedHashSet<MessageWrapper>, - pub current_room: Option<RoomId>, + pub current_room: Option<Room>, pub current_room_displayname: String, } @@ -35,35 +27,20 @@ impl Component for MainView { type Properties = (); fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self { - let matrix_callback = link.callback(Msg::NewMessage); - let mut matrix_agent = MatrixAgent::bridge(matrix_callback); + let mut matrix_agent = MatrixAgent::dispatcher(); matrix_agent.send(Request::StartSync); let state = State { - events: Default::default(), current_room: None, current_room_displayname: Default::default(), }; - MainView { - link, - matrix_agent, - state, - } + MainView { link, state } } fn update(&mut self, msg: Self::Message) -> bool { match msg { - Msg::NewMessage(response) => match response { - Response::FinishedFirstSync => { - //self.matrix_agent.send(Request::GetJoinedRooms); - } - _ => {} - }, - Msg::ChangeRoom((displayname, room)) => { - let room_id = RoomId::try_from(room).unwrap(); - - self.state.current_room = Some(room_id.clone()); - self.state.current_room_displayname = displayname; + Msg::ChangeRoom(room) => { + self.state.current_room = Some(room); } } true @@ -73,9 +50,10 @@ impl Component for MainView { false } + //noinspection RsTypeCheck fn view(&self) -> Html { - if self.state.current_room.is_none() { - return html! { + return if self.state.current_room.is_none() { + html! { <div class="uk-flex auto-scrollable-container uk-background-default" style="height: 100%"> <RoomList change_room_callback=self.link.callback(Msg::ChangeRoom)/> @@ -85,14 +63,14 @@ impl Component for MainView { </div> </div> </div> - }; + } } else { - return html! { + html! { <div class="uk-flex auto-scrollable-container" style="height: 100%"> <RoomList change_room_callback=self.link.callback(Msg::ChangeRoom)/> - <EventList current_room=self.state.current_room.clone() displayname=self.state.current_room_displayname.clone() /> + <EventList current_room=self.state.current_room.as_ref().unwrap() /> </div> - }; - } + } + }; } } diff --git a/src/errors.rs b/src/errors.rs @@ -7,6 +7,4 @@ pub enum MatrixError { MissingClient, #[error("Missing required Data")] MissingFields, - #[error("This Matrix Event is not yet supported by Daydream")] - UnsupportedEvent, } diff --git a/src/lib.rs b/src/lib.rs @@ -1,34 +1,16 @@ #![recursion_limit = "512"] -#[macro_use] -extern crate cfg_if; +extern crate console_error_panic_hook; +extern crate wee_alloc; + +use console_error_panic_hook::set_once as set_panic_hook; +use wasm_bindgen::prelude::*; mod app; mod constants; mod errors; -use wasm_bindgen::prelude::*; - -cfg_if! { - // When the `console_error_panic_hook` feature is enabled, we can call the - // `set_panic_hook` function to get better error messages if we ever panic. - if #[cfg(feature = "console_error_panic_hook")] { - extern crate console_error_panic_hook; - use console_error_panic_hook::set_once as set_panic_hook; - } else { - #[inline] - fn set_panic_hook() {} - } -} - -cfg_if! { - // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global - // allocator. - if #[cfg(feature = "wee_alloc")] { - extern crate wee_alloc; - #[global_allocator] - static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; - } -} +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; // This is the entry point for the web app #[wasm_bindgen] diff --git a/tests/web.rs b/tests/web.rs @@ -11,3 +11,6 @@ wasm_bindgen_test_configure!(run_in_browser); fn pass() { assert_eq!(1 + 1, 2); } + +#[wasm_bindgen_test] +fn login() {} diff --git a/yarn.lock b/yarn.lock @@ -1062,6 +1062,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-env@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9" + integrity sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw== + dependencies: + cross-spawn "^7.0.1" + cross-spawn@6.0.5, cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -1073,6 +1080,15 @@ cross-spawn@6.0.5, cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -3366,6 +3382,11 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -4006,11 +4027,23 @@ shebang-command@^1.2.0: dependencies: shebang-regex "^1.0.0" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -4801,6 +4834,13 @@ which@^1.2.14, which@^1.2.9, which@^1.3.1: dependencies: isexe "^2.0.0" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + worker-farm@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"