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 d8baa92040e5fbd0af22821fa39096668b7d7d47
parent 6c68df0a3b17e7e4b38b9543cacd6a91902f7ccd
Author: Marcel <mtrnord1@gmail.com>
Date:   Wed, 27 May 2020 22:16:03 +0200

Refactor code, split components, recolor scrollbars, fix missing events at the bottom

Took 2 hours 43 minutes

Diffstat:
MCargo.toml | 10+++++++---
Msrc/app.rs | 3++-
Asrc/app/components/event_list.rs | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/app/components/mod.rs | 2++
Asrc/app/components/room_list.rs | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/app/matrix.rs | 16+++++++++-------
Msrc/app/views/login.rs | 2+-
Msrc/app/views/main_view.rs | 126++++++++++++++++++-------------------------------------------------------------
Mstatic/style.scss | 14++++++++++++++
9 files changed, 324 insertions(+), 110 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -22,12 +22,12 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4.12" wasm-logger = "0.2" wee_alloc = "0.4" -yew = "0.16" -yew-router = "0.13" +yew = { git = "https://github.com/yewstack/yew" } +yew-router = { git = "https://github.com/yewstack/yew" } +yewtil = { git = "https://github.com/yewstack/yew" } futures = "0.3" matrix-sdk = { version = "0.1.0", git = "https://github.com/matrix-org/matrix-rust-sdk.git", default-features = false} url = "2.1.1" -yew_styles = "0.3.1" thiserror = "1.0" futures-locks = { git = "https://github.com/asomers/futures-locks", default-features = false } js_int = "0.1.5" @@ -41,3 +41,7 @@ version = "0.3" features = [ 'KeyboardEvent', ] + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" diff --git a/src/app.rs b/src/app.rs @@ -6,6 +6,7 @@ use crate::app::matrix::{MatrixAgent, Response}; use crate::app::views::{login::Login, main_view::MainView}; use yew_router::agent::RouteRequest::ChangeRoute; +pub mod components; mod matrix; mod views; @@ -63,7 +64,7 @@ impl Component for App { self.route_agent.send(ChangeRoute(route)); } Msg::NewMessage(response) => { - info!("NewMessage: {:#?}", response); + //info!("NewMessage: {:#?}", response); match response { Response::Error(_) => {} Response::LoggedIn(logged_in) => { diff --git a/src/app/components/event_list.rs b/src/app/components/event_list.rs @@ -0,0 +1,124 @@ +use linked_hash_set::LinkedHashSet; +use log::*; +use matrix_sdk::identifiers::RoomId; +use serde::{Deserialize, Serialize}; +use yew::prelude::*; +use yewtil::NeqAssign; + +use crate::app::matrix::{MatrixAgent, Request, Response}; +use crate::app::matrix::types::MessageWrapper; + +pub struct EventList { + link: ComponentLink<Self>, + state: State, + matrix_agent: Box<dyn Bridge<MatrixAgent>>, + props: Props, +} + +#[derive(Serialize, Deserialize, Default)] +pub struct State { + // TODO handle all events + pub events: LinkedHashSet<MessageWrapper>, +} + +pub enum Msg { + NewMessage(Response), +} + +#[derive(Clone, PartialEq, Properties)] +pub struct Props { + #[prop_or_default] + pub current_room: Option<RoomId>, + #[prop_or_default] + pub displayname: String, +} + +impl Component for EventList { + type Message = Msg; + type Properties = Props; + + fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { + let matrix_callback = link.callback(Msg::NewMessage); + let mut matrix_agent = MatrixAgent::bridge(matrix_callback); + + let state = State { + events: Default::default(), + }; + + 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 == room_id) + .collect::<LinkedHashSet<&MessageWrapper>>() + .is_empty() + { + matrix_agent.send(Request::GetOldMessages((room_id.clone(), None))); + } + } + + + + EventList { + props, + link, + matrix_agent, + state, + } + } + + fn update(&mut self, msg: Self::Message) -> bool { + match msg { + Msg::NewMessage(response) => { + match response { + Response::Sync(msg) => { + // TODO handle all events + self.state.events.insert(msg); + true + } + Response::OldMessages(messages) => { + // TODO this doesn't seem smart + let mut new_events_map = LinkedHashSet::new(); + for event in messages.into_iter() { + new_events_map.insert(event); + } + for event in self.state.events.clone().into_iter() { + new_events_map.insert(event); + } + self.state.events = new_events_map; + true + } + + _ => { + false + } + } + } + } + } + + fn change(&mut self, props: Self::Properties) -> bool { + self.props.neq_assign(props) + } + + fn view(&self) -> Html { + return html! { + <div class="container uk-flex uk-flex-column uk-width-5-6 uk-padding uk-padding-remove-bottom"> + <h1>{ self.props.displayname.clone() }</h1> + <div class="scrollable"> + { self.state.events.iter().filter(|x| x.room_id == self.props.current_room.clone().unwrap()).map(|event| self.get_event(event.clone())).collect::<Html>() } + </div> + </div> + }; + } +} + + +impl EventList { + fn get_event(&self, event: MessageWrapper) -> Html { + html! { + <p>{event.sender_displayname.clone()}{": "}{event.content.clone()}</p> + } + } +} diff --git a/src/app/components/mod.rs b/src/app/components/mod.rs @@ -0,0 +1,2 @@ +pub mod event_list; +pub mod room_list; diff --git a/src/app/components/room_list.rs b/src/app/components/room_list.rs @@ -0,0 +1,137 @@ +use std::collections::HashMap; + +use js_int::UInt; +use log::*; +use matrix_sdk::identifiers::RoomId; +use serde::{Deserialize, Serialize}; +use yew::prelude::*; +use yew::{Bridge, Bridged, Component, ComponentLink, Html}; +use yewtil::NeqAssign; + +use crate::app::matrix::types::SmallRoom; +use crate::app::matrix::{MatrixAgent, Response}; + +pub struct RoomList { + link: ComponentLink<Self>, + state: State, + matrix_agent: Box<dyn Bridge<MatrixAgent>>, + props: Props, +} + +pub enum Msg { + NewMessage(Response), + ChangeRoom(String), +} + +#[derive(Serialize, Deserialize, Default)] +pub struct State { + pub rooms: HashMap<RoomId, SmallRoom>, + pub current_room: Option<RoomId>, + loading: bool, +} + +#[derive(Clone, PartialEq, Properties)] +pub struct Props { + #[prop_or_default] + pub change_room_callback: Callback<(String, String)>, +} + +impl Component for RoomList { + type Message = Msg; + type Properties = Props; + + fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { + let matrix_callback = link.callback(Msg::NewMessage); + let matrix_agent = MatrixAgent::bridge(matrix_callback); + let state = State { + rooms: Default::default(), + current_room: None, + loading: true, + }; + + RoomList { + props, + link, + matrix_agent, + state, + } + } + + fn update(&mut self, msg: Self::Message) -> bool { + match msg { + Msg::NewMessage(response) => match response { + Response::JoinedRoomList(rooms) => { + self.state.rooms = rooms; + self.state.loading = false; + true + } + _ => 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)); + false + } + } + } + + fn change(&mut self, props: Self::Properties) -> ShouldRender { + self.props.neq_assign(props) + } + + fn view(&self) -> Html { + if self.state.loading { + return html! { + <div class="container"> + <div class="uk-position-center uk-padding"> + <span uk-spinner="ratio: 4.5"></span> + </div> + </div> + }; + } else { + return html! { + <div class="container uk-flex uk-flex-column uk-width-1-6" style="height: 100%"> + <div class="uk-padding uk-padding-remove-bottom"> + <form class="uk-search uk-search-default"> + <span uk-search-icon=""></span> + <input class="uk-search-input" type="search" placeholder="Filter Rooms..." /> + </form> + </div> + <ul class="scrollable uk-flex uk-flex-column uk-padding uk-nav-default uk-nav-parent-icon" uk-nav="" style="height: 100%"> + <li class="uk-nav-header">{"Rooms"}</li> + { self.state.rooms.iter().map(|(_, room)| self.get_room(room.clone())).collect::<Html>() } + </ul> + </div> + }; + } + } +} + +impl RoomList { + fn get_room(&self, room: SmallRoom) -> Html { + // TODO better linking than onlclick (yew limitation?) + + let room_id = room.clone().id.to_string(); + html! { + <li> + <a href="#" onclick=self.link.callback(move |e: MouseEvent| Msg::ChangeRoom(room_id.clone()))> + {room.name.clone()} + { + 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> } + } 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> } + } else { + html! {} + } + } + </a> + </li> + } + } +} diff --git a/src/app/matrix.rs b/src/app/matrix.rs @@ -79,7 +79,7 @@ pub enum Response { } impl Agent for MatrixAgent { - type Reach = Context; + type Reach = Context<MatrixAgent>; type Message = (); type Input = Request; type Output = Response; @@ -284,12 +284,14 @@ impl Agent for MatrixAgent { .await .unwrap(); let room = room.read().await; - let member = room.members.get(&sender).unwrap(); - member - .display_name - .as_ref() - .map(ToString::to_string) - .unwrap_or(sender.to_string()) + match room.members.get(&sender) { + Some(member) => member + .display_name + .as_ref() + .map(ToString::to_string) + .unwrap_or(sender.to_string()), + None => sender.to_string(), + } }; let wrapper = MessageWrapper { diff --git a/src/app/views/login.rs b/src/app/views/login.rs @@ -73,7 +73,7 @@ impl Component for Login { <div class="container"> <div class="uk-position-center uk-padding"> <h1 class="title">{"Login"}</h1> - <form class="uk-form-stacked uk-margin" onsubmit=self.link.callback(|e: Event| {e.prevent_default(); Msg::Login})> + <form class="uk-form-stacked uk-margin" onsubmit=self.link.callback(|e: FocusEvent| {e.prevent_default(); Msg::Login})> <div class="uk-margin"> <label class="uk-form-label">{"Homeserver URL"}</label> <div class="uk-form-controls"> diff --git a/src/app/views/main_view.rs b/src/app/views/main_view.rs @@ -1,16 +1,15 @@ -use std::collections::{HashMap}; use std::convert::TryFrom; -use js_int::UInt; +use linked_hash_set::LinkedHashSet; use log::*; use matrix_sdk::identifiers::RoomId; use serde::{Deserialize, Serialize}; use yew::prelude::*; use yew::ComponentLink; -use crate::app::matrix::types::{MessageWrapper, SmallRoom}; +use crate::app::components::{room_list::RoomList, event_list::EventList}; +use crate::app::matrix::types::MessageWrapper; use crate::app::matrix::{MatrixAgent, Request, Response}; -use linked_hash_set::LinkedHashSet; pub struct MainView { link: ComponentLink<Self>, @@ -20,15 +19,15 @@ pub struct MainView { pub enum Msg { NewMessage(Response), - ChangeRoom(String), + ChangeRoom((String, String)), } #[derive(Serialize, Deserialize, Default)] pub struct State { // TODO handle all events pub events: LinkedHashSet<MessageWrapper>, - pub rooms: HashMap<RoomId, SmallRoom>, pub current_room: Option<RoomId>, + pub current_room_displayname: String, } impl Component for MainView { @@ -41,8 +40,8 @@ impl Component for MainView { matrix_agent.send(Request::StartSync); let state = State { events: Default::default(), - rooms: Default::default(), current_room: None, + current_room_displayname: Default::default() }; MainView { @@ -59,32 +58,24 @@ impl Component for MainView { Response::FinishedFirstSync => { self.matrix_agent.send(Request::GetJoinedRooms); } - Response::Sync(msg) => { - // TODO handle all events - self.state.events.insert(msg); - } - Response::JoinedRoomList(rooms) => self.state.rooms = rooms, - Response::OldMessages(messages) => { - // TODO this doesn't seem smart - let mut new_events_map = LinkedHashSet::new(); - for event in messages.into_iter() { - new_events_map.insert(event); - } - for event in self.state.events.clone().into_iter() { - new_events_map.insert(event); - } - self.state.events = new_events_map; - } _ => {} } } - Msg::ChangeRoom(room) => { + Msg::ChangeRoom((displayname, room)) => { let room_id = RoomId::try_from(room).unwrap(); - if self.state.events.iter().filter(|x| x.room_id == room_id).collect::<LinkedHashSet<&MessageWrapper>>().is_empty() { + if self + .state + .events + .iter() + .filter(|x| x.room_id == room_id) + .collect::<LinkedHashSet<&MessageWrapper>>() + .is_empty() + { self.matrix_agent .send(Request::GetOldMessages((room_id.clone(), None))); } self.state.current_room = Some(room_id.clone()); + self.state.current_room_displayname = displayname; } } true @@ -95,86 +86,25 @@ impl Component for MainView { } fn view(&self) -> Html { - if !self.state.rooms.is_empty() { - if self.state.current_room.is_none() { - return html! { - <div class="uk-flex uk-height-1-1 non-scrollable-container"> - <div class="container uk-height-1-1 uk-width-1-6"> - <ul class="scrollable uk-height-1-1 uk-padding uk-nav-default uk-nav-parent-icon" uk-nav=""> - <li class="uk-nav-header">{"Rooms"}</li> - { self.state.rooms.iter().map(|(_, room)| self.get_room(room.clone())).collect::<Html>() } - </ul> - </div> - - <div class="container uk-height-1-1 uk-width-5-6 uk-padding"> - <div class="scrollable" uk-height-viewport="expand: true"> - // TODO add some content to the empty page - </div> - </div> - </div> - }; - } else { - return html! { - <div class="uk-flex uk-height-1-1 non-scrollable-container"> - <div class="container uk-height-1-1 uk-width-1-6"> - <ul class="scrollable uk-height-1-1 uk-padding uk-nav-default uk-nav-parent-icon" uk-nav=""> - <li class="uk-nav-header">{"Rooms"}</li> - { self.state.rooms.iter().map(|(_, room)| self.get_room(room.clone())).collect::<Html>() } - </ul> - </div> + if self.state.current_room.is_none() { + return html! { + <div class="uk-flex auto-scrollable-container" style="height: 100%"> + <RoomList change_room_callback=self.link.callback(Msg::ChangeRoom)/> - <div class="container uk-height-1-1 uk-width-5-6 uk-padding"> - <h1>{ self.state.rooms.iter().filter(|(id, _)| **id == self.state.current_room.clone().unwrap()).map(|(_, room)| room.name.clone()).collect::<String>() }</h1> - <div class="scrollable" uk-height-viewport="expand: true"> - { self.state.events.iter().filter(|x| x.room_id == self.state.current_room.clone().unwrap()).map(|event| self.get_event(event.clone())).collect::<Html>() } - </div> + <div class="container uk-flex uk-width-5-6 uk-padding"> + <div class="scrollable"> + // TODO add some content to the empty page </div> </div> - }; - } + </div> + }; } else { return html! { - <div class="container"> - <div class="uk-position-center uk-padding"> - <span uk-spinner="ratio: 4.5"></span> - </div> + <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() /> </div> }; } } } - -impl MainView { - fn get_event(&self, event: MessageWrapper) -> Html { - html! { - <p>{event.sender_displayname.clone()}{": "}{event.content.clone()}</p> - } - } - - fn get_room(&self, room: SmallRoom) -> Html { - // TODO better linking than onlclick (yew limitation?) - - let room_id = room.clone().id.to_string(); - html! { - <li> - <a href="#" onclick=self.link.callback(move |e: MouseEvent| Msg::ChangeRoom(room_id.clone()))> - {room.name.clone()} - { - 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> } - } 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> } - } else { - html! {} - } - } - </a> - </li> - } - } -} diff --git a/static/style.scss b/static/style.scss @@ -1,13 +1,27 @@ +$scrollbar-color: hsla(0,0%,100%,.2); +$scrollbar-bg-color: #181b21; + .scrollable { overflow-y: auto !important; overflow-x: hidden !important; height: 100% !important; + scrollbar-width: thin; + scrollbar-color: $scrollbar-bg-color $scrollbar-color; +} + +.scrollable::-webkit-scrollbar { + width: 11px; } .non-scrollable-container { overflow: hidden; } + +.auto-scrollable-container { + overflow: auto; +} + .uk-badge.red { background: #f0506e; }