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 024552abb7fad7766ac403152d69dde59921f6e8
parent 3667dd7b2fce482f599fb81502e364ae45ab81c0
Author: Marcel <mtrnord1@gmail.com>
Date:   Fri,  3 Jul 2020 16:49:03 +0200

Add initial redesign stuff (not finished), move sync to worker (not finished)

Took 5 hours 59 minutes

Diffstat:
M.github/workflows/check.yml | 5++---
M.github/workflows/deploy.yml | 4+++-
M.github/workflows/tests.yml | 1+
MCargo.toml | 19++++++++++---------
Mpackage.json | 17+++++++++--------
Msrc/app/components/event_list.rs | 79+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/app/components/events/notice.rs | 3+--
Msrc/app/components/events/text.rs | 3+--
Dsrc/app/components/room_list.rs | 242-------------------------------------------------------------------------------
Asrc/app/components/room_list/item.rs | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/app/components/room_list/mod.rs | 228+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/app/matrix/mod.rs | 69++++++++++++++++++++++++---------------------------------------------
Msrc/app/matrix/sync.rs | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/app/mod.rs | 33+++++++++++++++++++++++++++++++--
Msrc/app/views/main_view.rs | 5++---
Msrc/errors.rs | 2+-
Msrc/lib.rs | 27+++++++++++++++++++++++----
Msrc/utils/mod.rs | 1+
Asrc/utils/string_utils.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Astartup_helper/worker/worker.js | 4++++
Mstatic/custom-css.scss | 301+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mstatic/dark_theme/_globals.scss | 2++
Dstatic/fonts.scss | 1-
Mstatic/index.html | 3+++
Mstatic/light_theme/_globals.scss | 2++
Mstatic/variable-ovewrites.scss | 2++
Mwebpack.config.js | 38+++++++++++++++++++++++++++++++++-----
27 files changed, 923 insertions(+), 411 deletions(-)

diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml @@ -8,10 +8,10 @@ jobs: - name: Setup Rust uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: nightly - name: Run fmt run: cargo fmt -- --check - name: Run clippy run: cargo clippy -- --deny=warnings - name: Run check - run: cargo check -\ No newline at end of file + run: cargo check diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml @@ -13,9 +13,11 @@ jobs: - name: Setup Rust uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: nightly target: wasm32-unknown-unknown + - run: rustup default nightly + - name: Setup Node uses: actions/setup-node@v1 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml @@ -9,6 +9,7 @@ jobs: containers: [1, 2, 3] steps: - uses: actions/checkout@v1 + - run: rustup default nightly - uses: cypress-io/github-action@v1 with: browser: chrome diff --git a/Cargo.toml b/Cargo.toml @@ -15,29 +15,30 @@ console_error_panic_hook = { version = "0.1" } log = "0.4" tracing = {version = "0.1", features = ["log-always"] } serde = { version = "1.0", features = ["rc", "derive"] } -serde_json = "1.0.53" +serde_json = "1.0" wasm-bindgen = "0.2" -wasm-bindgen-futures = "0.4.12" +wasm-bindgen-futures = "0.4" wasm-logger = "0.2" -wee_alloc = "0.4" +#wee_alloc = "0.4" +lazy_static = "1.4.0" # Yew -yew = { git = "https://github.com/yewstack/yew" } -yew-router = { git = "https://github.com/yewstack/yew" } -yewtil = { git = "https://github.com/yewstack/yew" } +yew = { git = "https://github.com/daydream-mx/yew.git", branch = "fix-webpack-worker", features = ["webpack"] } +yew-router = { git = "https://github.com/daydream-mx/yew.git", branch = "fix-webpack-worker" } +yewtil = { git = "https://github.com/daydream-mx/yew.git", branch = "fix-webpack-worker" } # Matrix -matrix-sdk = { version = "0.1.0", git = "https://github.com/MTRNord/matrix-rust-sdk", branch = "daydream", default-features = false}# features = ["encryption"]} +matrix-sdk = { version = "0.1.0", git = "https://github.com/MTRNord/matrix-rust-sdk", branch = "daydream", default-features = false, features = ["messages"]}# features = ["encryption"]} url = "2.1.1" thiserror = "1.0" futures-locks = { git = "https://github.com/asomers/futures-locks", default-features = false } # Markdown -pulldown-cmark = "0.7.1" +pulldown-cmark = "0.7.2" # Translations tr = { version = "0.1.3", default-features = false, features = ["gettext"]} -i18n-embed = { version = "0.4", features = ["web-sys-requester"] } +i18n-embed = { version = "0.6", features = ["web-sys-requester"] } rust-embed = { version = "5", features = ["debug-embed", "compression"]} # Make links links again! diff --git a/package.json b/package.json @@ -6,30 +6,31 @@ "build:dev": "webpack --mode development", "build": "webpack --mode production", "start:dev": "webpack-dev-server --mode development", - "start": "yarn run start:dev", + "start": "webpack-dev-server --mode production", "start:prod": "webpack-dev-server --mode production", "cy:open": "cypress open", "cy:run:ci": "cypress run --browser chrome --record --key 99194b81-775d-4e98-9bac-8f2cc12bc62e --ci-build-id \"${GITHUB_SHA}-${GITHUB_WORKFLOW}-${GITHUB_EVENT_NAME}\" --group github-action-e2e --parallel" }, "devDependencies": { "@wasm-tool/wasm-pack-plugin": "^1.3.1", - "copy-webpack-plugin": "^6.0.1", + "copy-webpack-plugin": "^6.0.3", "cross-env": "^7.0.2", - "css-loader": "^3.5.3", - "cypress": "^4.8.0", + "css-loader": "^3.6.0", + "cypress": "^4.9.0", "file-loader": "^6.0.0", "html-webpack-plugin": "^4.3.0", "mini-css-extract-plugin": "^0.9.0", - "sass": "^1.26.5", - "sass-loader": "^8.0.2", + "sass": "^1.26.9", + "sass-loader": "^9.0.0", "start-server-and-test": "^1.11.0", "style-loader": "^1.2.1", + "terser-webpack-plugin": "^3.0.6", "wasm-pack": "^0.9.1", "webpack": "^4.43.0", - "webpack-cli": "^3.3.11", + "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0" }, "dependencies": { - "uikit": "^3.4.6" + "uikit": "^3.5.4" } } diff --git a/src/app/components/event_list.rs b/src/app/components/event_list.rs @@ -70,27 +70,34 @@ impl Component for EventList { match msg { Msg::NewMessage(response) => { match response { - Response::Sync((room_id, msg)) => { + Response::Sync((room_id, raw_msg)) => { // TODO handle all events - if self.state.events.contains_key(&room_id) { - if !(self.state.events[&room_id] - .iter() - .map(|x| x.event_id.clone()) - .any(|x| x == msg.event_id)) - { - self.state.events.get_mut(&room_id).unwrap().push(msg); - room_id == self.props.current_room.clone().unwrap().room_id + if let Ok(msg) = raw_msg.deserialize() { + if self.state.events.contains_key(&room_id) { + if !(self.state.events[&room_id] + .iter() + .any(|x| x.event_id == msg.event_id)) + { + self.state.events.get_mut(&room_id).unwrap().push(msg); + room_id == self.props.current_room.clone().unwrap().room_id + } else { + false + } } else { - false + let mut msgs = vec![msg]; + self.state.events.insert(room_id.clone(), msgs); + room_id == self.props.current_room.clone().unwrap().room_id } } else { - let mut msgs = Vec::new(); - msgs.push(msg); - self.state.events.insert(room_id.clone(), msgs); - room_id == self.props.current_room.clone().unwrap().room_id + false } } Response::OldMessages((room_id, mut messages)) => { + let mut deserialized_messages: Vec<MessageEvent> = messages + .iter() + .map(|x| x.deserialize()) + .filter_map(Result::ok) + .collect(); // This is a clippy false positive #[allow(clippy::map_entry)] if self.state.events.contains_key(&room_id) { @@ -98,10 +105,10 @@ impl Component for EventList { .events .get_mut(&room_id) .unwrap() - .append(messages.as_mut()); + .append(deserialized_messages.as_mut()); true } else { - self.state.events.insert(room_id, messages); + self.state.events.insert(room_id, deserialized_messages); true } } @@ -139,30 +146,30 @@ impl Component for EventList { fn view(&self) -> Html { return html! { - <div class="event-list container uk-flex uk-flex-column uk-width-5-6"> - <div class="room-title"><div><h1>{ self.props.current_room.as_ref().unwrap().display_name() }</h1></div></div> - <div class="scrollable" style="height: 100%"> - { - if self.state.events.contains_key(&self.props.current_room.as_ref().unwrap().room_id) { - let events = self.state.events[&self.props.current_room.as_ref().unwrap().room_id].clone(); - let mut elements: Vec<Html> = Vec::new(); - for (pos, event) in self.state.events[&self.props.current_room.as_ref().unwrap().room_id].iter().enumerate() { - if pos == 0 { - elements.push(self.get_event(None, event)); - } else { - elements.push(self.get_event(Some(events[pos - 1].clone()), event)); + <div class="event-list"> + <div class="room-title"><h1>{ self.props.current_room.as_ref().unwrap().display_name() }</h1></div> + <div class="scrollable" style="height: auto; flex-grow: 1;"> + <div class="message-container"> + { + if self.state.events.contains_key(&self.props.current_room.as_ref().unwrap().room_id) { + let events = self.state.events[&self.props.current_room.as_ref().unwrap().room_id].clone(); + let mut elements: Vec<Html> = Vec::new(); + for (pos, event) in self.state.events[&self.props.current_room.as_ref().unwrap().room_id].iter().enumerate() { + if pos == 0 { + elements.push(self.get_event(None, event)); + } else { + elements.push(self.get_event(Some(events[pos - 1].clone()), event)); + } } + elements.into_iter().collect::<Html>() + } else { + html! {} } - elements.into_iter().collect::<Html>() - } else { - html! {} } - } - <div id="anchor"></div> - </div> - <div class="uk-margin"> - <Input on_submit=&self.on_submit/> + <div id="anchor"></div> + </div> </div> + <Input on_submit=&self.on_submit/> </div> }; } diff --git a/src/app/components/events/notice.rs b/src/app/components/events/notice.rs @@ -80,8 +80,7 @@ impl Component for Notice { if new_user { let full_html = format!( "<p style=\"opacity: .6;\"><displayname>{}: </displayname>{}</p>", - sender_displayname, - content + sender_displayname, content ); let js_text_event = { let div = web_sys::window() diff --git a/src/app/components/events/text.rs b/src/app/components/events/text.rs @@ -113,8 +113,7 @@ impl Component for Text { } else if new_user { let full_html = format!( "<p><displayname>{}: </displayname>{}</p>", - sender_displayname, - content + sender_displayname, content ); let js_text_event = { let div = web_sys::window() diff --git a/src/app/components/room_list.rs b/src/app/components/room_list.rs @@ -1,242 +0,0 @@ -use std::collections::HashMap; -use std::include_str; - -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 tr::tr; - -use crate::app::components::raw_html::RawHTML; -use crate::app::matrix::{MatrixAgent, Request, Response}; - -pub struct RoomList { - link: ComponentLink<Self>, - state: State, - matrix_agent: Box<dyn Bridge<MatrixAgent>>, - props: Props, -} - -#[allow(clippy::large_enum_variant)] -pub enum Msg { - NewMessage(Response), - ChangeRoom(Room), - SetFilter(String), - ToggleTheme, -} - -#[derive(Serialize, Deserialize, Default)] -pub struct State { - 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<Room>, -} - -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, - search_query: None, - dark_theme: false, - did_fetch: false, - }; - - 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; - self.state.did_fetch = false; - true - } - // Better Initial Sync Detection - Response::SyncPing => { - if self.state.rooms.is_empty() && !self.state.did_fetch { - self.matrix_agent.send(Request::GetJoinedRooms); - self.state.did_fetch = true; - } - false - } - // Handle new rooms from sync - 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)); - } - false - } - Response::JoinedRoom((room_id, room)) => { - self.state.rooms.insert(room_id, room); - true - } - _ => false, - }, - Msg::ChangeRoom(room) => { - if self.state.current_room.is_some() && self.state.current_room.as_ref().unwrap() == &room { - return false; - } - self.props.change_room_callback.emit(room.clone()); - self.state.current_room = Some(room); - true - } - Msg::SetFilter(query) => { - self.state.search_query = Some(query); - true - } - Msg::ToggleTheme => { - self.state.dark_theme = !self.state.dark_theme; - let theme = if self.state.dark_theme { - "dark" - } else { - "light" - }; - document() - .document_element() - .unwrap() - .dyn_into::<HtmlElement>() - .unwrap() - .dataset() - .set("theme", theme) - .unwrap(); - true - } - } - } - - fn change(&mut self, props: Self::Properties) -> ShouldRender { - self.props.neq_assign(props) - } - - //noinspection RsTypeCheck - fn view(&self) -> Html { - if self.state.loading { - html! { - <div class="container"> - <div class="uk-position-center uk-padding"> - <RawHTML inner_html=include_str!("../svgs/loading_animation.svg")/> - </div> - </div> - } - } else { - html! { - <div class="container roomlist 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"> - <span class="material-icons" id ="ma-icon">{"search"}</span> - <input - class="uk-search-input" - type="search" - placeholder={ - tr!( - // Placeholder text for the roomlist filtering - "Filter Rooms..." - ) - } - 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"> - { - tr!( - // Header of the Roomlist - "Rooms" - ) - } - </li> - { - 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.display_name().to_lowercase().contains(&self.state.search_query.as_ref().unwrap().to_lowercase())).map(|(_, room)| self.get_room(room)).collect::<Html>() - } - } - </ul> - <div class="toggleWrapper uk-margin uk-flex uk-flex-column uk-flex-center" style="height: 60px;"> - <input type="checkbox" class="dn" id="dn" checked=self.state.dark_theme value=self.state.dark_theme onclick=self.link.callback(|e: MouseEvent| {Msg::ToggleTheme})/> - <label for="dn" class="toggle"> - <span class="toggle__handler"> - <span class="crater crater--1"></span> - <span class="crater crater--2"></span> - <span class="crater crater--3"></span> - </span> - <span class="star star--1"></span> - <span class="star star--2"></span> - <span class="star star--3"></span> - <span class="star star--4"></span> - <span class="star star--5"></span> - //<span class="star star--6"></span> - </label> - </div> - </div> - } - } - } -} - -impl RoomList { - 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 { - "" - } - } else { - "" - }; - - let room = matrix_room.clone(); - html! { - <li class=classes> - <a onclick=self.link.callback(move |e: MouseEvent| Msg::ChangeRoom(room.clone()))> - {matrix_room.display_name()} - { - 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 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! {} - } - } - </a> - </li> - } - } -} diff --git a/src/app/components/room_list/item.rs b/src/app/components/room_list/item.rs @@ -0,0 +1,94 @@ +use matrix_sdk::{events::room::message::MessageEventContent, js_int::UInt, Room}; +use rand::random; +use yew::prelude::*; + +use crate::app::components::events::{get_sender_avatar, get_sender_displayname, is_new_user}; +use crate::app::matrix::types::get_media_download_url; +use url::Url; + +pub(crate) struct RoomItem { + props: Props, + link: ComponentLink<Self>, +} + +pub enum Msg { + ChangeRoom(Room), +} + +#[derive(Clone, Properties, Debug)] +pub struct Props { + #[prop_or_default] + pub room: Option<Room>, + + #[prop_or_default] + pub change_room_callback: Callback<Room>, +} + +impl Component for RoomItem { + type Message = Msg; + type Properties = Props; + + fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { + RoomItem { props, link } + } + + fn update(&mut self, msg: Self::Message) -> bool { + match msg { + Msg::ChangeRoom(room) => { + self.props.change_room_callback.emit(room); + } + } + false + } + + fn change(&mut self, props: Self::Properties) -> bool { + // TODO fix the PartialEq hack + if format!("{:#?}", self.props) != format!("{:#?}", props) { + self.props = props; + true + } else { + false + } + } + + //noinspection RsTypeCheck + fn view(&self) -> Html { + let room = self.props.room.clone().unwrap(); + + // TODO placeholder for encrypted rooms + let last_message = match self + .props + .room + .as_ref() + .unwrap() + .messages + .clone() + .into_iter() + .last() + { + None => "".to_string(), + Some(m) => { + let content = m.content.clone(); + if let MessageEventContent::Text(text_event) = content { + text_event.body + } else { + "".to_string() + } + } + }; + html! { + <div class="room-list-item"> + <a onclick=self.link.callback(move |e: MouseEvent| Msg::ChangeRoom(room.clone()))> + <div class="content"> + // TODO remove placeholder + <img class="avatar" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAARUlEQVRYhe3OMQ0AIADAMBKUowmBoIKMo0f/jrnX+dmoA4KCdUBQsA4ICtYBQcE6IChYBwQF64CgYB0QFKwDgoJ1QPC1C8gY0kSgNLTWAAAAAElFTkSuQmCC"/> + <div> + <h5 class="name">{self.props.room.as_ref().unwrap().display_name()}</h5> + <p class="latest-msg">{last_message}</p> + </div> + </div> + </a> + </div> + } + } +} diff --git a/src/app/components/room_list/mod.rs b/src/app/components/room_list/mod.rs @@ -0,0 +1,228 @@ +use std::collections::HashMap; +use std::include_str; + +use log::*; +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 tr::tr; + +use crate::app::components::raw_html::RawHTML; +use crate::app::components::room_list::item::RoomItem; +use crate::app::matrix::{MatrixAgent, Request, Response}; + +mod item; + +pub struct RoomList { + link: ComponentLink<Self>, + state: State, + matrix_agent: Box<dyn Bridge<MatrixAgent>>, + props: Props, +} + +#[allow(clippy::large_enum_variant)] +pub enum Msg { + NewMessage(Response), + ChangeRoom(Room), + SetFilter(String), + ToggleTheme, +} + +#[derive(Serialize, Deserialize, Default)] +pub struct State { + rooms: HashMap<RoomId, Room>, + current_room: Option<Room>, + loading: bool, + search_query: Option<String>, + dark_theme: bool, +} + +#[derive(Clone, PartialEq, Properties)] +pub struct Props { + #[prop_or_default] + pub change_room_callback: Callback<Room>, +} + +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, + search_query: None, + dark_theme: false, + }; + + RoomList { + props, + link, + matrix_agent, + state, + } + } + + fn update(&mut self, msg: Self::Message) -> bool { + match msg { + Msg::NewMessage(response) => match response { + // Handle new rooms from sync + Response::JoinedRoomSync(room_id) => { + info!("Got JoinedRoomSync"); + if !(self.state.rooms.contains_key(&room_id)) { + self.matrix_agent.send(Request::GetJoinedRoom(room_id)); + } + false + } + Response::JoinedRoom((room_id, room)) => { + self.state.rooms.insert(room_id, room); + if self.state.loading { + self.state.loading = false; + } + true + } + _ => false, + }, + Msg::ChangeRoom(room) => { + if self.state.current_room.is_some() + && self.state.current_room.as_ref().unwrap() == &room + { + return false; + } + self.props.change_room_callback.emit(room.clone()); + self.state.current_room = Some(room); + true + } + Msg::SetFilter(query) => { + self.state.search_query = Some(query); + true + } + Msg::ToggleTheme => { + self.state.dark_theme = !self.state.dark_theme; + let theme = if self.state.dark_theme { + "dark" + } else { + "light" + }; + document() + .document_element() + .unwrap() + .dyn_into::<HtmlElement>() + .unwrap() + .dataset() + .set("theme", theme) + .unwrap(); + true + } + } + } + + fn change(&mut self, props: Self::Properties) -> ShouldRender { + self.props.neq_assign(props) + } + + //noinspection RsTypeCheck + fn view(&self) -> Html { + if self.state.loading { + html! { + <div class="container"> + <div class="uk-position-center uk-padding"> + <RawHTML inner_html=include_str!("../../svgs/loading_animation.svg")/> + </div> + </div> + } + } else { + html! { + <div class="roomlist" style="height: 100%"> + <div class="top-bar"> + <div class="userdata"> + </div> + <div class="search"> + <div> + <span class="material-icons">{"search"}</span> + <input + class="search-input" + type="search" + placeholder={ + tr!( + // Placeholder text for the roomlist filtering + "Filter Rooms..." + ) + } + value=&self.state.search_query.as_ref().unwrap_or(&"".to_string()) + oninput=self.link.callback(|e: InputData| Msg::SetFilter(e.value)) /> + </div> + </div> + </div> + + <div class="scrollable list"> + { + 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.display_name().to_lowercase().contains(&self.state.search_query.as_ref().unwrap().to_lowercase())).map(|(_, room)| self.get_room(room)).collect::<Html>() + } + } + </div> + <div class="bottom-bar"> + <div class="toggleWrapper"> + <input type="checkbox" class="dn" id="dn" checked=self.state.dark_theme value=self.state.dark_theme onclick=self.link.callback(|e: MouseEvent| {Msg::ToggleTheme})/> + <label for="dn" class="toggle"> + <span class="toggle__handler"> + <span class="crater crater--1"></span> + <span class="crater crater--2"></span> + <span class="crater crater--3"></span> + </span> + <span class="star star--1"></span> + <span class="star star--2"></span> + <span class="star star--3"></span> + <span class="star star--4"></span> + <span class="star star--5"></span> + //<span class="star star--6"></span> + </label> + </div> + </div> + </div> + } + } + } +} + +impl RoomList { + fn get_room(&self, matrix_room: &Room) -> Html { + let room = matrix_room.clone(); + html! { + <RoomItem change_room_callback=self.link.callback(Msg::ChangeRoom) room=Some(room)/> + } + /*html! { + <li class=classes> + <a onclick=self.link.callback(move |e: MouseEvent| Msg::ChangeRoom(room.clone()))> + {matrix_room.display_name()} + { + 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 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! {} + } + } + </a> + </li> + }*/ + } +} diff --git a/src/app/matrix/mod.rs b/src/app/matrix/mod.rs @@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize}; use url::Url; use wasm_bindgen_futures::spawn_local; use yew::format::Json; -use yew::services::{storage::Area, StorageService}; +use yew::worker::*; use yew::worker::*; use crate::app::matrix::types::{get_media_download_url, get_video_media_download_url}; @@ -55,36 +55,35 @@ pub struct MatrixAgent { matrix_client: Option<Client>, // TODO make arc mutex :( subscribers: HashSet<HandlerId>, - storage: Arc<Mutex<StorageService>>, session: Option<SessionStore>, } -#[derive(Debug)] +#[derive(Serialize, Deserialize, Debug)] pub enum Request { SetHomeserver(String), SetUsername(String), SetPassword(String), + SetSession(SessionStore), Login, GetLoggedIn, GetOldMessages((RoomId, Option<String>)), StartSync, - GetJoinedRooms, GetJoinedRoom(RoomId), SendMessage((RoomId, String)), } #[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub enum Response { Error(MatrixError), LoggedIn(bool), // TODO properly handle sync events - Sync((RoomId, MessageEvent)), + Sync((RoomId, EventJson<MessageEvent>)), + JoinedRoomSync(RoomId), SyncPing, - JoinedRoomList(HashMap<RoomId, Room>), - Userdata(), - OldMessages((RoomId, Vec<MessageEvent>)), + OldMessages((RoomId, Vec<EventJson<MessageEvent>>)), JoinedRoom((RoomId, Room)), + SaveSession(SessionStore) } #[derive(Debug, Clone)] @@ -93,29 +92,18 @@ pub enum Msg { } impl Agent for MatrixAgent { - type Reach = Context<MatrixAgent>; + type Reach = Public<Self>; type Message = Msg; type Input = Request; 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 session: Option<SessionStore> = { - if let Json(Ok(restored_model)) = storage.lock().unwrap().restore(AUTH_KEY) { - Some(restored_model) - } else { - None - } - }; MatrixAgent { link, matrix_state: Default::default(), matrix_client: None, subscribers: HashSet::new(), - storage, - session, + session: Default::default(), } } @@ -134,6 +122,10 @@ impl Agent for MatrixAgent { } fn handle_input(&mut self, msg: Self::Input, _: HandlerId) { match msg { + Request::SetSession(session) => { + self.session = Some(session); + } + Request::SetHomeserver(homeserver) => { self.matrix_state.homeserver = Some(homeserver); } @@ -184,10 +176,9 @@ impl Agent for MatrixAgent { device_id: login_response.device_id, homeserver_url: client.homeserver().to_string(), }; - let mut storage = agent.storage.lock().unwrap(); - storage.store(AUTH_KEY, Json(&session_store)); - for sub in subscribers.iter() { + let resp = Response::SaveSession(session_store.clone()); + agent.link.respond(*sub, resp); let resp = Response::LoggedIn(true); agent.link.respond(*sub, resp); } @@ -265,24 +256,6 @@ impl Agent for MatrixAgent { agent.start_sync().await; }); } - Request::GetJoinedRooms => { - let agent = self.clone(); - 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 resp = Response::JoinedRoomList(rooms_unarced.clone()); - agent.link.respond(*sub, resp); - } - }); - } Request::GetOldMessages((room_id, from)) => { let agent = self.clone(); spawn_local(async move { @@ -320,7 +293,7 @@ impl Agent for MatrixAgent { .unwrap(); // TODO save end point for future loading - let mut wrapped_messages: Vec<MessageEvent> = Vec::new(); + let mut wrapped_messages: Vec<EventJson<MessageEvent>> = Vec::new(); let chunk_iter: Vec<EventJson<RoomEvent>> = messsages.chunk; let (oks, _): (Vec<_>, Vec<_>) = chunk_iter .iter() @@ -377,7 +350,9 @@ impl Agent for MatrixAgent { } event.content = MessageEventContent::Video(video_event); } - wrapped_messages.push(event.clone()); + + let serialized_event = EventJson::from(event.clone()); + wrapped_messages.push(serialized_event); } } @@ -439,6 +414,10 @@ impl Agent for MatrixAgent { fn disconnected(&mut self, id: HandlerId) { self.subscribers.remove(&id); } + + fn name_of_resource() -> &'static str { + "worker.js" + } } unsafe impl Send for MatrixAgent {} diff --git a/src/app/matrix/sync.rs b/src/app/matrix/sync.rs @@ -1,10 +1,15 @@ use std::sync::Arc; +use std::sync::Mutex; use std::time::Duration; use log::*; use matrix_sdk::{ api::r0::sync::sync_events::Response as SyncResponse, - events::{collections::all::RoomEvent, room::message::MessageEventContent}, + events::{ + collections::all::{RoomEvent, StateEvent}, + room::message::MessageEventContent, + EventJson, + }, identifiers::RoomId, locks::RwLock, Client, Room, SyncSettings, @@ -12,10 +17,17 @@ use matrix_sdk::{ use wasm_bindgen_futures::spawn_local; use yew::Callback; +use lazy_static::lazy_static; + use crate::app::components::events::{get_sender_avatar, get_sender_displayname}; use crate::app::matrix::types::{get_media_download_url, get_video_media_download_url}; use crate::app::matrix::Response; use crate::utils::notifications::Notifications; +use serde::Serialize; + +lazy_static! { + static ref SYNC_NUMBER: Mutex<i32> = Mutex::new(0); +} pub struct Sync { pub(crate) matrix_client: Client, @@ -37,21 +49,45 @@ impl Sync { async fn on_sync_response(&self, response: SyncResponse) { debug!("got sync!"); + // FIXME: Is there a smarter way? let resp = Response::SyncPing; self.callback.emit(resp); for (room_id, room) in response.rooms.join { + for event in room.state.events { + if let Ok(event) = event.deserialize() { + self.on_state_event(&room_id, event).await + } + } for event in room.timeline.events { if let Ok(event) = event.deserialize() { self.on_room_message(&room_id, event).await } } } + let mut sync_number = SYNC_NUMBER.lock().unwrap(); + if *sync_number == 0 { + *sync_number = 1; + } + } + + async fn on_state_event(&self, room_id: &RoomId, event: StateEvent) { + if let StateEvent::RoomCreate(event) = event { + info!("Sent JoinedRoomSync State"); + let resp = Response::JoinedRoomSync(room_id.clone()); + self.callback.emit(resp); + } } async fn on_room_message(&self, room_id: &RoomId, event: RoomEvent) { // TODO handle all messages... + if let RoomEvent::RoomCreate(_create_event) = event.clone() { + info!("Sent JoinedRoomSync Timeline"); + let resp = Response::JoinedRoomSync(room_id.clone()); + self.callback.emit(resp); + } + if let RoomEvent::RoomMessage(mut event) = event { if let MessageEventContent::Text(text_event) = event.clone().content { let homeserver_url = self.matrix_client.clone().homeserver().clone(); @@ -59,28 +95,41 @@ impl Sync { let cloned_event = event.clone(); let client = self.matrix_client.clone(); let local_room_id = room_id.clone(); - spawn_local(async move { - if Notifications::browser_support() { - let room: Arc<RwLock<Room>> = client - .clone() - .get_joined_room(&local_room_id.clone()) - .await - .unwrap(); - let read_clone = room.read().await; - let clean_room = (*read_clone).clone(); - let avatar_url = get_sender_avatar( - homeserver_url, - clean_room.clone(), - cloned_event.clone(), - ); - let displayname = - get_sender_displayname(clean_room, cloned_event.clone()); - - let notification = - Notifications::new(avatar_url, displayname, text_event.body.clone()); - notification.show(); - } - }); + let sync_number = SYNC_NUMBER.lock().unwrap(); + if *sync_number == 1 { + spawn_local(async move { + if Notifications::browser_support() { + let room: Arc<RwLock<Room>> = client + .clone() + .get_joined_room(&local_room_id.clone()) + .await + .unwrap(); + let read_clone = room.read().await; + let clean_room = (*read_clone).clone(); + let avatar_url = get_sender_avatar( + homeserver_url, + clean_room.clone(), + cloned_event.clone(), + ); + let room_name = clean_room.display_name(); + let displayname = + get_sender_displayname(clean_room, cloned_event.clone()); + + let mut title = "".to_string(); + if displayname == room_name { + title = displayname; + } else { + title = format!("{} ({})", displayname, room_name); + } + + /* + TODO fix by moving it out of the worker + let notification = + Notifications::new(avatar_url, title, text_event.body.clone()); + notification.show();*/ + } + }); + } } if let MessageEventContent::Image(mut image_event) = event.clone().content { if image_event.url.is_some() { @@ -125,7 +174,8 @@ impl Sync { event.content = MessageEventContent::Video(video_event); } - let resp = Response::Sync((room_id.clone(), event.clone())); + let serialized_event = EventJson::from(event.clone()); + let resp = Response::Sync((room_id.clone(), serialized_event)); self.callback.emit(resp); } } diff --git a/src/app/mod.rs b/src/app/mod.rs @@ -2,12 +2,17 @@ use yew::{prelude::*, virtual_dom::VNode}; use yew_router::agent::RouteRequest::ChangeRoute; use yew_router::{prelude::*, Switch}; -use crate::app::matrix::{MatrixAgent, Response}; +use crate::app::matrix::{MatrixAgent, Response, SessionStore}; use crate::app::views::{login::Login, main_view::MainView}; use log::*; +use std::sync::{Arc, Mutex}; +use yew::services::StorageService; +use yew::services::storage::Area; +use crate::constants::AUTH_KEY; +use yew::format::Json; pub mod components; -mod matrix; +pub mod matrix; mod views; #[derive(Switch, Clone)] @@ -30,6 +35,8 @@ pub struct App { matrix_agent: Box<dyn Bridge<MatrixAgent>>, route: Option<Route<()>>, route_agent: Box<dyn Bridge<RouteAgent<()>>>, + session: Option<SessionStore>, + storage: Arc<Mutex<StorageService>>, } impl Component for App { @@ -37,6 +44,17 @@ impl Component for App { type Properties = (); fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self { + 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) + } else { + None + } + }; let route_agent = RouteAgent::bridge(link.callback(Msg::RouteChanged)); let mut matrix_agent = MatrixAgent::bridge(link.callback(Msg::NewMessage)); matrix_agent.send(matrix::Request::GetLoggedIn); @@ -44,6 +62,8 @@ impl Component for App { matrix_agent, route_agent, route: None, + session, + storage, } } @@ -61,8 +81,17 @@ impl Component for App { Msg::NewMessage(response) => { //info!("NewMessage: {:#?}", response); match response { + Response::SaveSession(session) => { + let mut storage = self.storage.lock().unwrap(); + storage.store(AUTH_KEY, Json(&session)); + } Response::Error(_) => {} Response::LoggedIn(logged_in) => { + if !logged_in && self.session.is_some() { + self.matrix_agent.send(matrix::Request::SetSession(self.session.clone().unwrap())); + self.matrix_agent.send(matrix::Request::GetLoggedIn); + return false; + } let route: Route = if logged_in { //self.state.logged_in = true; diff --git a/src/app/views/main_view.rs b/src/app/views/main_view.rs @@ -71,8 +71,8 @@ impl Component for MainView { html! { <div class="uk-flex auto-scrollable-container" style="height: 100%"> <RoomList change_room_callback=self.link.callback(Msg::ChangeRoom)/> - <div class="event-list container uk-flex uk-flex-column uk-width-5-6"> - <div class="room-title"><div><h1>{ self.state.current_room.as_ref().unwrap().display_name() }</h1></div></div> + <div class="event-list"> + <div class="room-title"><h1>{ self.state.current_room.as_ref().unwrap().display_name() }</h1></div> <h4> { tr!( @@ -94,4 +94,3 @@ impl Component for MainView { } } } - diff --git a/src/errors.rs b/src/errors.rs @@ -20,7 +20,7 @@ impl fmt::Display for Field { } // TODO figure out a way to translate this -#[derive(Error, Debug, Clone)] +#[derive(Error, Debug, Clone, Serialize, Deserialize)] pub enum MatrixError { #[error("No Matrix Client is available yet")] MissingClient, diff --git a/src/lib.rs b/src/lib.rs @@ -1,6 +1,6 @@ #![recursion_limit = "512"] extern crate console_error_panic_hook; -extern crate wee_alloc; +//extern crate wee_alloc; use console_error_panic_hook::set_once as set_panic_hook; use i18n_embed::{language_loader, I18nEmbed, WebLanguageRequester}; @@ -18,10 +18,11 @@ struct Translations; language_loader!(DaydreamLanguageLoader); -#[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 + +//====== Running the primary frontend ======// #[wasm_bindgen] pub fn run_app() -> Result<(), JsValue> { // If the `console_error_panic_hook` feature is enabled this will set a panic hook, otherwise @@ -38,3 +39,21 @@ pub fn run_app() -> Result<(), JsValue> { yew::start_app::<app::App>(); Ok(()) } + +//====== Running the worker ======/// + +// We need to import the Threaded trait to register the worker +use yew::agent::Threaded; + +/// This gets called by the worker.js entrypoint +/// We need to wrap it in wasm_bindgen so the worker knows the spin the the yew worker instance +#[wasm_bindgen] +pub fn init_worker() { + // Spawning a yew component without StartApp requires initializing + yew::initialize(); + + // ... registering the worker + app::matrix::MatrixAgent::register(); +} + +// you would need a new launch function for each of the unique workers you want to register diff --git a/src/utils/mod.rs b/src/utils/mod.rs @@ -1 +1,2 @@ pub mod notifications; +pub mod string_utils; diff --git a/src/utils/string_utils.rs b/src/utils/string_utils.rs @@ -0,0 +1,51 @@ +use std::ops::{Bound, RangeBounds}; + +pub trait StringUtils { + fn substring(&self, start: usize, len: usize) -> &str; + fn slice(&self, range: impl RangeBounds<usize>) -> &str; +} + +impl StringUtils for str { + fn substring(&self, start: usize, len: usize) -> &str { + let mut char_pos = 0; + let mut byte_start = 0; + let mut it = self.chars(); + loop { + if char_pos == start { + break; + } + if let Some(c) = it.next() { + char_pos += 1; + byte_start += c.len_utf8(); + } else { + break; + } + } + char_pos = 0; + let mut byte_end = byte_start; + loop { + if char_pos == len { + break; + } + if let Some(c) = it.next() { + char_pos += 1; + byte_end += c.len_utf8(); + } else { + break; + } + } + &self[byte_start..byte_end] + } + fn slice(&self, range: impl RangeBounds<usize>) -> &str { + let start = match range.start_bound() { + Bound::Included(bound) | Bound::Excluded(bound) => *bound, + Bound::Unbounded => 0, + }; + let len = match range.end_bound() { + Bound::Included(bound) => *bound + 1, + Bound::Excluded(bound) => *bound, + Bound::Unbounded => self.len(), + } - start; + self.substring(start, len) + } +} diff --git a/startup_helper/worker/worker.js b/startup_helper/worker/worker.js @@ -0,0 +1,4 @@ +import("../../pkg").then(wasm => { + // Call the library function we exported with wasm-bindgen in lib.rs + wasm.init_worker(); +}); diff --git a/static/custom-css.scss b/static/custom-css.scss @@ -1,7 +1,7 @@ .scrollable { overflow-y: auto !important; overflow-x: hidden !important; - height: 100vh; + height: 100%; scrollbar-width: thin; scrollbar-color: $scrollbar-color $scrollbar-bg-color; } @@ -78,7 +78,7 @@ em { .error { color: #992E2E; - font-family: Roboto; + font-family: 'Roboto', sans-serif; font-style: normal; font-weight: 500; font-size: 16px; @@ -91,10 +91,6 @@ em { margin: 0; } -.roomlist { - min-width: 260px; -} - .uk-search, .uk-inline { position: relative; } @@ -141,7 +137,6 @@ li { @import "sun-loading-animation.scss"; @import "lightbox.scss"; -@import "fonts.scss"; .daydream-title { font-family: 'Dancing Script', cursive; @@ -221,7 +216,7 @@ _::-webkit-full-page-media, _:future, :root .login-page-bg { } .login-title { - font-family: Roboto; + font-family: 'Roboto', sans-serif; font-style: normal; font-weight: bold; font-size: 48px; @@ -252,7 +247,7 @@ _::-webkit-full-page-media, _:future, :root .login-page-bg { transition: all 0.3s ease 0s; border-radius: 45px; - font-family: Roboto; + font-family: 'Roboto', sans-serif; font-style: normal; font-weight: normal; font-size: 24px; @@ -295,7 +290,7 @@ input.login-input { /* Subtitle 1 / Roboto Regular */ - font-family: Roboto; + font-family: 'Roboto', sans-serif; font-style: normal; font-weight: normal; font-size: 16px; @@ -344,25 +339,285 @@ body { } .room-title { - margin-bottom: 1.5rem; + width: 100%; + height: 100%; + max-height: 5.875rem; + + //h1 { + // margin: 0 !important; + // height: 5.875rem; + //} + + &::before { + content: ''; + position: absolute; + width: 100%; + height: 5.875rem; + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.2), 0 4px 5px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14); + -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.2), 0 4px 5px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14); + -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.2), 0 4px 5px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14); + } +} + +.event-list { + height: 100%; + display: flex; + flex-direction: column; width: 100%; - overflow: hidden; - padding-bottom: 10px; } -.room-title > div { +.message-container { width: 100%; - //height: 100px; + max-width: 67.0625rem; - box-shadow: 0 1px 10px rgba(0, 0, 0, 0.2), 0 4px 5px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14); - -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.2), 0 4px 5px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14); - -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.2), 0 4px 5px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14); + margin: 0 auto; } -.event-list { - padding: 40px; - padding-bottom: 0 !important; - padding-right: 0 !important; - height: 100%; + +.roomlist { + width: 100%; + max-width: 31.5rem; + display: flex; + flex-direction: column; + position: relative; + z-index: 0; + + .list { + width: 100%; + flex-grow: 1; + height: auto !important; + z-index: -1; + } + + .top-bar { + width: 100%; + + height: 138px; + + .userdata { + width: 100%; + height: 5.875rem; + + background-color: $accent-color; + + padding-left: 1.5625rem; + padding-right: 1.1875rem; + -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */ + -moz-box-sizing: border-box; /* Firefox, other Gecko */ + box-sizing: border-box; /* Opera/IE 8+ */ + } + + .search { + width: 100%; + + div { + height: 2.75rem; + + display: flex; + flex-direction: row; + align-items: center; + + padding-left: 1.5625rem; + padding-right: 1.1875rem; + -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */ + -moz-box-sizing: border-box; /* Firefox, other Gecko */ + box-sizing: border-box; /* Opera/IE 8+ */ + z-index: 1; + + width: 100%; + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.2), 0 4px 5px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14); + -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.2), 0 4px 5px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14); + -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.2), 0 4px 5px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14); + + .material-icons { + margin-top: 0.625rem; + margin-bottom: 0.625rem; + } + + .search-input { + /* + * 1. Define consistent box sizing. + * 3. Remove `border-radius` in iOS. + * 4. Change font properties to `inherit` in all browsers + * 5. Show the overflow in Edge. + * 6. Remove default style in iOS. + * 7. Vertical alignment + * 8. Take the full container width + * 9. Style + */ + /* 1 */ + box-sizing: border-box; + /* 3 */ + border-radius: 0; + /* 4 */ + font: inherit; + /* 5 */ + overflow: visible; + /* 6 */ + -webkit-appearance: none; + /* 7 */ + vertical-align: middle; + /* 8 */ + width: 100%; + /* 9 */ + border: none; + + + margin-top: 0.625rem; + margin-bottom: 0.625rem; + margin-left: 3.206875rem; + + /* H6 / Roboto Medium */ + + font-family: 'Roboto', sans-serif; + font-style: normal; + font-weight: 500; + font-size: 20px; + line-height: 23px; + display: flex; + align-items: center; + letter-spacing: 0.15px; + + color: $search-grey; + + &:focus { + // Chrome override + outline: none 0; + } + + + /* + * Remove the inner padding and cancel buttons in Chrome on OS X and Safari on OS X. + */ + + &::-webkit-search-cancel-button, + &::-webkit-search-decoration { + -webkit-appearance: none; + } + + /* + * Removes placeholder transparency in Firefox. + */ + + &::-moz-placeholder { + opacity: 1; + } + + } + } + } + + } + + .bottom-bar { + height: 4.375rem; + width: 100%; + position: relative; + + &::after { + content: ''; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: -1; + box-shadow: -10px -1px 10px rgba(0, 0, 0, 0.2), -10px -4px 5px rgba(0, 0, 0, 0.12), -10px -2px 4px rgba(0, 0, 0, 0.14); + } + + .toggleWrapper { + height: 100%; + // Following later becomes button stuff + margin: 9px 54px; + } + } +} + +// TODO make room-list-item responsive +.room-list-item { + &:hover { + background-color: #d9d9d9; + } + + a { + &:hover { + text-decoration: none !important; + } + + background-color: #F2F2F2; + height: 5rem; + + .content { + margin-left: 35px; + height: 5rem; + display: flex; + flex-direction: row; + width: 100%; + max-width: 28.8125rem; + + img.avatar { + border-radius: 50%; + box-shadow: 0 1px 8px rgba(0, 0, 0, 0.2), 0 3px 4px rgba(0, 0, 0, 0.12), 0 3px 3px rgba(0, 0, 0, 0.14); + } + + .avatar { + margin-right: 28px; + width: 48px; + height: 48px; + box-sizing: border-box; + margin-bottom: 16px; + margin-top: 16px; + } + + div { + margin-bottom: 3px; + + .name { + /* RoomList Names */ + + font-family: Roboto, sans-serif; + font-style: normal; + font-weight: 500; + font-size: 18px; + line-height: 23px; + margin: 0 !important; + margin-top: 18px !important; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + + /* Black */ + + color: #000000; + + width: 100%; + max-width: 22rem; + } + + p.latest-msg { + font-family: Roboto, sans-serif; + font-style: normal; + font-weight: normal; + font-size: 14px; + line-height: 28px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + + letter-spacing: 0.5px; + margin: 0 !important; + + /* Search Grey */ + color: #737373; + + width: 100%; + max-width: 22rem; + display: inline-block; + } + } + } + + } } + diff --git a/static/dark_theme/_globals.scss b/static/dark_theme/_globals.scss @@ -2,8 +2,10 @@ --global-background: #1c1c21; --global-scrollbar-color: hsla(0,0%,100%,.2); --global-scrollbar-bg-color: transparent; + --global-accent-color: #732222; --global-emphasis-color: #fff; --global-color: #fff; --icon-link-active-color: darken(#fff, 5%); --active-menu: #dddddd; + --search-grey: #737373; } diff --git a/static/fonts.scss b/static/fonts.scss @@ -1 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700&family=Roboto:wght@300;400;500;700&display=swap'); diff --git a/static/index.html b/static/index.html @@ -5,6 +5,9 @@ <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="Description" content="A Matrix Client written in Rust using WebAssembly."> + <link rel="dns-prefetch" href="//fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <title>Daydream</title> </head> diff --git a/static/light_theme/_globals.scss b/static/light_theme/_globals.scss @@ -1,8 +1,10 @@ [data-theme="light"] { --global-scrollbar-color: black; --global-scrollbar-bg-color: transparent; + --global-accent-color: #732222; --global-emphasis-color: #333; --global-color: #333333; --icon-link-active-color: darken(#666, 5%); --active-menu: #666666; + --search-grey: #737373; } diff --git a/static/variable-ovewrites.scss b/static/variable-ovewrites.scss @@ -1,6 +1,8 @@ // Custom component colors $scrollbar-color: var(--global-scrollbar-color); $scrollbar-bg-color: var(--global-scrollbar-bg-color); +$accent-color: var(--global-accent-color); +$search-grey: var(--search-grey); // UIKit Colors $global-background: var(--global-background); diff --git a/webpack.config.js b/webpack.config.js @@ -3,13 +3,14 @@ const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); const distPath = path.resolve(__dirname, "dist"); -module.exports = (env, argv) => { +const appConfig = (env, argv) => { return { devServer: { contentBase: distPath, - compress: argv.mode === 'production', + compress: process.env.WEBPACK_MODE === 'production', port: 8888, historyApiFallback: true }, @@ -19,6 +20,12 @@ module.exports = (env, argv) => { filename: "daydream.js", webassemblyModuleFilename: "daydream.wasm" }, + optimization: { + minimize: true, + minimizer: [new TerserPlugin({ + parallel: true, + })], + }, module: { rules: [ { @@ -33,7 +40,7 @@ module.exports = (env, argv) => { include: [path.resolve(__dirname, "static"), path.resolve(__dirname, "node_modules")], use: [ // fallback to style-loader in development - process.env.NODE_ENV !== 'production' + process.env.WEBPACK_MODE !== 'production' ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', @@ -69,7 +76,28 @@ module.exports = (env, argv) => { new HtmlWebpackPlugin({ template: path.resolve(__dirname, "static/index.html") }) - ] - //watch: argv.mode !== 'production' + ], + watch: process.env.WEBPACK_MODE !== 'production', + watchOptions: { + poll: true + } }; }; + +// This config actually generates both +const workerConfig = { + entry: "./startup_helper/worker/worker.js", + target: "webworker", + plugins: [], + resolve: { + extensions: [".js", ".wasm"] + }, + output: { + path: distPath, + filename: "worker.js" + } +}; + +module.exports = (env, argv) => { + return [appConfig(env, argv), workerConfig] +};