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 774306eb22d4e2e14e42735183651e5e3b249c47
parent a6305433d3982b40467586ca7f53031af1318f83
Author: Marcel <mtrnord1@gmail.com>
Date:   Tue, 16 Jun 2020 18:39:28 +0200

Add basic notifications

Took 1 hour 19 minutes

Diffstat:
MCargo.toml | 6+++++-
Msrc/app/components/event_list.rs | 5++++-
Msrc/app/components/events/image.rs | 2+-
Msrc/app/components/events/mod.rs | 15+++++++++++++++
Msrc/app/components/events/video.rs | 2+-
Msrc/app/matrix/mod.rs | 9+++++----
Msrc/app/matrix/sync.rs | 42+++++++++++++++++++++++++++++++++++-------
Msrc/app/matrix/types.rs | 9+++++----
Msrc/lib.rs | 1+
Asrc/utils/mod.rs | 1+
Asrc/utils/notifications.rs | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 148 insertions(+), 19 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -48,7 +48,11 @@ version = "0.3" features = [ 'KeyboardEvent', 'HtmlElement', - 'DomStringMap' + 'DomStringMap', + 'Notification', + 'NotificationPermission', + 'NotificationOptions', + 'Window' ] [patch.crates-io] diff --git a/src/app/components/event_list.rs b/src/app/components/event_list.rs @@ -7,8 +7,11 @@ use matrix_sdk::{ use std::collections::HashMap; use yew::prelude::*; -use crate::app::components::events::{image::Image, notice::Notice, text::Text, video::Video}; +use crate::app::components::events::{ + get_sender_avatar, image::Image, notice::Notice, text::Text, video::Video, +}; use crate::app::matrix::{MatrixAgent, Request, Response}; +use crate::utils::notifications::Notifications; pub struct EventList { link: ComponentLink<Self>, diff --git a/src/app/components/events/image.rs b/src/app/components/events/image.rs @@ -3,8 +3,8 @@ use matrix_sdk::{ events::room::message::{ImageMessageEventContent, MessageEvent}, Room, }; -use yew::prelude::*; use rand::random; +use yew::prelude::*; pub(crate) struct Image { props: Props, diff --git a/src/app/components/events/mod.rs b/src/app/components/events/mod.rs @@ -1,5 +1,7 @@ +use crate::app::matrix::types::get_media_download_url; use matrix_sdk::events::room::message::MessageEvent; use matrix_sdk::Room; +use url::Url; pub mod image; pub mod notice; @@ -24,3 +26,16 @@ pub fn get_sender_displayname(room: Room, event: MessageEvent) -> String { .unwrap_or_else(|| event.sender.to_string()), } } + +pub fn get_sender_avatar(homeserver_url: Url, room: Room, event: MessageEvent) -> Option<String> { + match room.members.get(&event.sender) { + None => None, + Some(member) => { + let avatar_url_mxc = member.avatar_url.as_ref().map(ToString::to_string); + match avatar_url_mxc { + None => None, + Some(v) => Some(get_media_download_url(&homeserver_url, v)), + } + } + } +} diff --git a/src/app/components/events/video.rs b/src/app/components/events/video.rs @@ -3,8 +3,8 @@ use matrix_sdk::{ events::room::message::{MessageEvent, VideoMessageEventContent}, Room, }; -use yew::prelude::*; use rand::random; +use yew::prelude::*; pub(crate) struct Video { props: Props, diff --git a/src/app/matrix/mod.rs b/src/app/matrix/mod.rs @@ -334,13 +334,14 @@ impl Agent for MatrixAgent { oks.into_iter().map(Result::unwrap).collect(); for event in deserialized_events.into_iter().rev() { + // TODO deduplicate betweeen this and sync if let RoomEvent::RoomMessage(mut event) = event { if let MessageEventContent::Image(mut image_event) = event.clone().content { if image_event.url.is_some() { let new_url = Some(get_media_download_url( - agent.matrix_client.clone().unwrap(), + agent.matrix_client.clone().unwrap().homeserver(), image_event.url.unwrap(), )); image_event.url = new_url; @@ -349,7 +350,7 @@ impl Agent for MatrixAgent { 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(), + agent.matrix_client.clone().unwrap().homeserver(), info.thumbnail_url.unwrap(), )); info.thumbnail_url = new_url; @@ -361,7 +362,7 @@ impl Agent for MatrixAgent { if let MessageEventContent::Video(mut video_event) = event.content { if video_event.url.is_some() { let new_url = Some(get_video_media_download_url( - agent.matrix_client.clone().unwrap(), + agent.matrix_client.clone().unwrap().homeserver(), video_event.url.unwrap(), )); video_event.url = new_url; @@ -370,7 +371,7 @@ impl Agent for MatrixAgent { let mut info = video_event.info.unwrap(); if info.thumbnail_url.is_some() { let new_url = Some(get_media_download_url( - agent.matrix_client.clone().unwrap(), + agent.matrix_client.clone().unwrap().homeserver(), info.thumbnail_url.unwrap(), )); info.thumbnail_url = new_url; diff --git a/src/app/matrix/sync.rs b/src/app/matrix/sync.rs @@ -1,16 +1,22 @@ +use std::sync::Arc; use std::time::Duration; -use crate::app::matrix::types::{get_media_download_url, get_video_media_download_url}; -use crate::app::matrix::Response; use log::*; use matrix_sdk::{ api::r0::sync::sync_events::Response as SyncResponse, events::{collections::all::RoomEvent, room::message::MessageEventContent}, identifiers::RoomId, - Client, SyncSettings, + locks::RwLock, + Client, Room, SyncSettings, }; +use wasm_bindgen_futures::spawn_local; use yew::Callback; +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; + pub struct Sync { pub(crate) matrix_client: Client, pub(crate) callback: Callback<Response>, @@ -47,10 +53,31 @@ impl Sync { // TODO handle all messages... 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(); + + let cloned_event = event.clone(); + let client = self.matrix_client.clone(); + let local_room_id = room_id.clone(); + spawn_local(async move { + 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.clone(), cloned_event.clone()); + let notification = + Notifications::new(avatar_url, displayname, text_event.body.clone()); + notification.show(); + }); + } if let MessageEventContent::Image(mut image_event) = event.clone().content { if image_event.url.is_some() { let new_url = Some(get_media_download_url( - self.matrix_client.clone(), + self.matrix_client.clone().homeserver(), image_event.url.unwrap(), )); image_event.url = new_url; @@ -59,7 +86,7 @@ impl Sync { let mut info = image_event.info.unwrap(); if info.thumbnail_url.is_some() { let new_url = Some(get_media_download_url( - self.matrix_client.clone(), + self.matrix_client.clone().homeserver(), info.thumbnail_url.unwrap(), )); info.thumbnail_url = new_url; @@ -71,7 +98,7 @@ impl Sync { if let MessageEventContent::Video(mut video_event) = event.content { if video_event.url.is_some() { let new_url = Some(get_video_media_download_url( - self.matrix_client.clone(), + self.matrix_client.clone().homeserver(), video_event.url.unwrap(), )); video_event.url = new_url; @@ -80,7 +107,7 @@ impl Sync { let mut info = video_event.info.unwrap(); if info.thumbnail_url.is_some() { let new_url = Some(get_media_download_url( - self.matrix_client.clone(), + self.matrix_client.clone().homeserver(), info.thumbnail_url.unwrap(), )); info.thumbnail_url = new_url; @@ -89,6 +116,7 @@ impl Sync { } event.content = MessageEventContent::Video(video_event); } + let resp = Response::Sync((room_id.clone(), event.clone())); self.callback.emit(resp); } diff --git a/src/app/matrix/types.rs b/src/app/matrix/types.rs @@ -1,6 +1,7 @@ use matrix_sdk::Client; +use url::Url; -pub fn get_media_download_url(client: Client, mxc_url: String) -> String { +pub fn get_media_download_url(homeserver: &Url, 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(); @@ -9,12 +10,12 @@ pub fn get_media_download_url(client: Client, mxc_url: String) -> String { "_matrix/media/r0/download/{}/{}/fix.jpg", server_name, media_id, ); - let mut new_url = client.homeserver().clone(); + let mut new_url = homeserver.clone(); new_url.set_path(new_path.as_str()); new_url.to_string() } -pub fn get_video_media_download_url(client: Client, mxc_url: String) -> String { +pub fn get_video_media_download_url(homeserver: &Url, 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(); @@ -23,7 +24,7 @@ pub fn get_video_media_download_url(client: Client, mxc_url: String) -> String { "_matrix/media/r0/download/{}/{}/fix.mp4", server_name, media_id, ); - let mut new_url = client.homeserver().clone(); + let mut new_url = homeserver.clone(); new_url.set_path(new_path.as_str()); new_url.to_string() } diff --git a/src/lib.rs b/src/lib.rs @@ -10,6 +10,7 @@ use wasm_bindgen::prelude::*; pub mod app; mod constants; mod errors; +pub mod utils; #[derive(RustEmbed, I18nEmbed)] #[folder = "i18n/mo"] // path to the compiled localization resources diff --git a/src/utils/mod.rs b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod notifications; diff --git a/src/utils/notifications.rs b/src/utils/notifications.rs @@ -0,0 +1,75 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use web_sys::{window, Notification, NotificationOptions, NotificationPermission, Window}; + +#[derive(Clone)] +pub(crate) struct Notifications { + window: Option<Window>, + avatar: Option<String>, + displayname: String, + content: String, +} + +impl Notifications { + pub fn new(avatar: Option<String>, displayname: String, content: String) -> Self { + Notifications { + window: None, + avatar, + displayname, + content, + } + } + fn browser_support(&mut self) -> bool { + return match &self.window { + None => match window() { + Some(v) => { + self.window = window(); + match v.get("Notification") { + Some(_) => true, + _ => false, + } + } + + _ => false, + }, + Some(v) => match v.get("Notification") { + Some(_) => true, + _ => false, + }, + }; + } + + fn notifications_allowed(&self) -> bool { + match Notification::permission() { + NotificationPermission::Granted => true, + _ => false, + } + } + + pub fn show(&self) { + if !self.notifications_allowed() { + let self_clone = self.clone(); + let cb = Closure::wrap(Box::new(move || { + if self_clone.notifications_allowed() { + self_clone.clone().show_actual(); + } + }) as Box<dyn FnMut()>); + + Notification::request_permission_with_permission_callback(cb.as_ref().unchecked_ref()); + cb.forget(); + } else { + self.show_actual(); + } + } + + fn show_actual(&self) { + let mut options = NotificationOptions::new() as NotificationOptions; + let mut options = options.body(&self.content).tag("daydream") as &mut NotificationOptions; + let options = if self.avatar.is_some() { + options.icon(self.avatar.as_ref().unwrap()) + } else { + options + }; + Notification::new_with_options(&self.displayname, &options); + } +}