diff --git a/Cargo.toml b/Cargo.toml index ded1960..0050bd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,43 +5,74 @@ edition = "2021" [features] default = [] -desktop = ["dioxus/desktop"] -web = ["dioxus/web"] [dependencies] -dioxus = "0.5.*" -dioxus-free-icons = { version = "0.8", features = ["material-design-icons-navigation", "ionicons"] } -dioxus-sdk = { version = "0.5.*", features = ["utils"] } - -# matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", branch = "main", default-features = false, features = ["js", "rustls-tls"] } -matrix-sdk = { version = "0.7.*", default-features = false, features = ["js", "rustls-tls"] } - +# Errors anyhow = "1.0.75" -url = "2.5.0" -dirs = "5.0.1" -ctrlc-async = "3.2.2" thiserror = "1.0.50" -turf = "0.8.*" -tokio = { version = "1.34.0", default-features = false, features = ["rt", "sync"] } -log = "0.4.20" -futures-util = "0.3.29" -futures = "0.3.29" -rand = "0.8.5" -reqwest = "0.11.24" -validator = { version = "0.17.0", features = ["derive"] } -const_format = "0.2.32" -zxcvbn = "2.2.2" + +# Async async-std = "1.12.0" -tracing = "0.1.40" -tracing-web = "0.1.3" -tracing-subscriber = "0.3.18" +async-trait = "0.1.80" +futures = "0.3.29" +futures-util = "0.3.29" +tokio = { version = "1.34.0", default-features = false, features = ["rt", "sync"] } +tokio-stream = "0.1.15" + +# Utils +base64 = "0.22.0" +const_format = "0.2.32" +rand = "0.8.5" +validator = { version = "0.17.0", features = ["derive"] } +# Http client +reqwest = "0.11.24" +# Password strength estimation +zxcvbn = "2.2.2" +# Image processing/conversion +image = "0.25.1" +# Get the application version git-version = "0.3.9" +# Conditional compilation +cfg-if = "1.0.0" + +# Logging/tracing +tracing = "0.1.40" +tracing-forest = "0.1.6" + +# SCSS -> CSS + usage in rust code +turf = "0.8.0" + +# Dioxus +dioxus = { version = "0.5", default-features = false } +dioxus-free-icons = { version = "0.8", features = ["ionicons", "font-awesome-solid"] } +modx = "0.1.2" + +[patch.crates-io] +dioxus = { git = "https://github.com/DioxusLabs/dioxus.git" } [target.'cfg(target_family = "wasm")'.dependencies] +# Logging/tracing +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +tracing-web = "0.1.3" +# Dioxus +dioxus = { features = ["web"] } web-sys = "0.3.69" +# Matrix +matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", default-features = false, features = ["rustls-tls", "js"] } + +[target.'cfg(not(target_family = "wasm"))'.dependencies] +# Utils +time = "0.3.36" +# Logging/tracing +tracing-subscriber = { version = "0.3.18", features = ["env-filter", "time"] } +# Dioxus +dioxus = { features = ["desktop"] } +# Matrix +matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", default-features = false, features = ["rustls-tls"] } [build-dependencies] regex = "1.10.3" + [package.metadata.turf] minify = true diff --git a/public/images/add_user.png b/public/images/add_user.png deleted file mode 100644 index f13708a..0000000 Binary files a/public/images/add_user.png and /dev/null differ diff --git a/public/images/add_user2.png b/public/images/add_user2.png deleted file mode 100644 index 82eea87..0000000 Binary files a/public/images/add_user2.png and /dev/null differ diff --git a/public/images/aerobutton_border.png b/public/images/aerobutton_border.png deleted file mode 100644 index 1f1c388..0000000 Binary files a/public/images/aerobutton_border.png and /dev/null differ diff --git a/public/images/aerobutton_border_down.png b/public/images/aerobutton_border_down.png deleted file mode 100644 index 656a1f3..0000000 Binary files a/public/images/aerobutton_border_down.png and /dev/null differ diff --git a/public/images/ban_user.png b/public/images/ban_user.png deleted file mode 100644 index 1a679ed..0000000 Binary files a/public/images/ban_user.png and /dev/null differ diff --git a/public/images/brush.png b/public/images/brush.png deleted file mode 100644 index c5e2ff4..0000000 Binary files a/public/images/brush.png and /dev/null differ diff --git a/public/images/button_border.png b/public/images/button_border.png deleted file mode 100644 index 2102a4c..0000000 Binary files a/public/images/button_border.png and /dev/null differ diff --git a/public/images/button_border_disabled.png b/public/images/button_border_disabled.png deleted file mode 100644 index a5a0298..0000000 Binary files a/public/images/button_border_disabled.png and /dev/null differ diff --git a/public/images/button_border_focus.png b/public/images/button_border_focus.png deleted file mode 100644 index 83f333f..0000000 Binary files a/public/images/button_border_focus.png and /dev/null differ diff --git a/public/images/default-avatar.png b/public/images/default-avatar.png deleted file mode 100644 index 295d49d..0000000 Binary files a/public/images/default-avatar.png and /dev/null differ diff --git a/public/images/directory.png b/public/images/directory.png deleted file mode 100644 index 061952d..0000000 Binary files a/public/images/directory.png and /dev/null differ diff --git a/public/images/games.png b/public/images/games.png deleted file mode 100644 index 11cfa77..0000000 Binary files a/public/images/games.png and /dev/null differ diff --git a/public/images/letter.png b/public/images/letter.png deleted file mode 100644 index 6d10b8e..0000000 Binary files a/public/images/letter.png and /dev/null differ diff --git a/public/images/logo-msn.png b/public/images/logo-msn.png deleted file mode 100644 index 9590615..0000000 Binary files a/public/images/logo-msn.png and /dev/null differ diff --git a/public/images/medias.png b/public/images/medias.png deleted file mode 100644 index b5db00b..0000000 Binary files a/public/images/medias.png and /dev/null differ diff --git a/public/images/news.png b/public/images/news.png deleted file mode 100644 index 1063df2..0000000 Binary files a/public/images/news.png and /dev/null differ diff --git a/public/images/phone.png b/public/images/phone.png deleted file mode 100644 index ce1af32..0000000 Binary files a/public/images/phone.png and /dev/null differ diff --git a/public/images/settings.png b/public/images/settings.png deleted file mode 100644 index 6a23c2b..0000000 Binary files a/public/images/settings.png and /dev/null differ diff --git a/public/images/status_away.png b/public/images/status_away.png deleted file mode 100644 index 39486a6..0000000 Binary files a/public/images/status_away.png and /dev/null differ diff --git a/public/images/status_busy.png b/public/images/status_busy.png deleted file mode 100644 index 471ff6b..0000000 Binary files a/public/images/status_busy.png and /dev/null differ diff --git a/public/images/status_online.png b/public/images/status_online.png deleted file mode 100644 index dd53972..0000000 Binary files a/public/images/status_online.png and /dev/null differ diff --git a/public/images/tbc_transfert.png b/public/images/tbc_transfert.png deleted file mode 100644 index 4ec8570..0000000 Binary files a/public/images/tbc_transfert.png and /dev/null differ diff --git a/public/images/webcam.svg b/public/images/webcam.svg deleted file mode 100644 index 7c3f49d..0000000 --- a/public/images/webcam.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/images/wizz.png.old b/public/images/wizz.png.old deleted file mode 100644 index d7b1d5d..0000000 Binary files a/public/images/wizz.png.old and /dev/null differ diff --git a/src/base.rs b/src/base.rs deleted file mode 100644 index 515beca..0000000 --- a/src/base.rs +++ /dev/null @@ -1,197 +0,0 @@ -// Cf. https://dioxuslabs.com/learn/0.4/reference/use_coroutine -// In order to use/run the rx.next().await statement you will need to extend the [Stream] trait -// (used by [UnboundedReceiver]) by adding 'futures_util' as a dependency to your project -// and adding the use futures_util::stream::StreamExt; -use std::cell::RefCell; - -use dioxus::prelude::*; -use futures_util::stream::StreamExt; -use log::{debug, error, warn}; -use matrix_sdk::ruma::OwnedRoomId; -use tokio::select; - -use crate::domain::model::room::{ByIdRooms, Room}; -use crate::domain::model::session::Session; -use crate::infrastructure::messaging::matrix::client::{Client, RoomEvent}; -use crate::infrastructure::messaging::matrix::requester::{Receivers, Requester}; -use crate::infrastructure::messaging::matrix::worker_tasks::LoginStyle; -use crate::ui::components::chats_window::interface::Interface as ChatsWinInterface; - -// #[derive(Clone, Debug)] -// pub struct UserInfo { -// pub avatar_url: Option, -// pub display_name: Option, -// pub blurhash: Option, -// } - -// impl UserInfo { -// pub fn new( -// avatar_url: Option, -// display_name: Option, -// blurhash: Option, -// ) -> Self { -// Self { -// avatar_url, -// display_name, -// blurhash, -// } -// } -// } - -// pub type ByIdUserInfos = HashMap; - -// #[derive(Clone)] -// pub struct Store { -// pub is_logged: bool, -// pub rooms: ByIdRooms, -// pub user_infos: ByIdUserInfos, -// pub user_id: Option, -// } - -// impl Store { -// pub fn new() -> Self { -// Self { -// is_logged: false, -// rooms: HashMap::new(), -// user_infos: HashMap::new(), -// user_id: None, -// } -// } -// } - -// impl PartialEq for Store { -// fn eq(&self, other: &Self) -> bool { -// self.is_logged == other.is_logged -// && self.user_id == other.user_id -// && self.user_infos.len() == other.user_infos.len() -// && self -// .user_infos -// .keys() -// .all(|k| other.user_infos.contains_key(k)) -// && self.rooms.len() == other.rooms.len() -// && self.rooms.keys().all(|k| other.rooms.contains_key(k)) -// } -// } - -// impl Eq for Store {} - -pub struct AppSettings { - pub requester: Option>, -} - -impl AppSettings { - pub fn new() -> Self { - Self { requester: None } - } - - pub fn set_requester(&mut self, requester: RefCell) { - self.requester = Some(requester); - } -} - -async fn on_room(room_id: OwnedRoomId, room: Room, by_id_rooms: &GlobalSignal) { - // TODO: Update rooms - by_id_rooms - .write() - .insert(room_id, RefCell::::new(room)); -} - -async fn on_joining_invitation( - room_id: OwnedRoomId, - room: Room, - by_id_rooms: &GlobalSignal, -) { - debug!("You're invited to join the \"{}\" room", room.id()); - // TODO: Update rooms - by_id_rooms - .write() - .insert(room_id, RefCell::::new(room)); -} - -async fn on_room_topic(room_id: OwnedRoomId, topic: String, by_id_rooms: &GlobalSignal) { - if let Some(room) = by_id_rooms.read().get(&room_id) { - let mut room = room.borrow_mut(); - room.set_topic(Some(topic)); - } else { - warn!("No room found with the \"{}\" id", room_id); - } -} - -pub async fn sync_messages(by_id_rooms: &GlobalSignal, room_id: OwnedRoomId) { - error!("== sync_messages =="); - -} - -pub async fn sync_rooms( - mut rx: UnboundedReceiver, - receivers: Receivers, - by_id_rooms: &GlobalSignal, -) { - if let Some(_is_logged) = rx.next().await { - let mut rooms_receiver = receivers.room_receiver.borrow_mut(); - - loop { - // TODO: Remove select if no more receivers will be used. - select! { - res = rooms_receiver.recv() => { - if let Ok(room_event) = res { - match room_event { - RoomEvent::MemberEvent(room_id, room) => on_room(room_id, room, &by_id_rooms).await, - RoomEvent::InviteEvent(room_id, room) => on_joining_invitation(room_id, room, &by_id_rooms).await, - RoomEvent::TopicEvent(room_id, topic) => on_room_topic(room_id, topic, &by_id_rooms).await, - }; - } - }, - } - } - } -} - -pub async fn login( - mut rx: UnboundedReceiver, - app_settings: &GlobalSignal, - session: &GlobalSignal, -) { - while let Some(is_logged) = rx.next().await { - if !is_logged { - let homeserver_url = session.read().homeserver_url.clone(); - let username = session.read().username.clone(); - let password = session.read().password.clone(); - - if homeserver_url.is_some() && username.is_some() && password.is_some() { - let client = Client::spawn(homeserver_url.unwrap()).await; - - if let Err(err) = client.init().await { - error!("Following error occureds during client init: {}", err); - } - - match client - .login(LoginStyle::Password(username.unwrap(), password.unwrap())) - .await - { - Ok(_) => { - debug!("successfully logged"); - session.write().is_logged = true; - } - Err(err) => { - error!("Error during login: {err}"); - // TODO: Handle invalid login - // invalid_login.modify(|_| true); - } - } - app_settings.write().set_requester(RefCell::new(client)); - } else { - warn!("At least one of the following values is/are invalid: homeserver, username or password"); - } - } else { - warn!("already logged... skip login"); - } - } - error!("=== LOGIN END ==="); -} - -pub static APP_SETTINGS: GlobalSignal = Signal::global(AppSettings::new); -pub static ROOMS: GlobalSignal = Signal::global(ByIdRooms::new); -pub static SESSION: GlobalSignal = Signal::global(Session::new); -pub static CHATS_WIN_INTERFACE: GlobalSignal = - Signal::global(ChatsWinInterface::new); diff --git a/src/domain/model/account.rs b/src/domain/model/account.rs new file mode 100644 index 0000000..642c829 --- /dev/null +++ b/src/domain/model/account.rs @@ -0,0 +1,136 @@ +use std::{cell::RefCell, collections::HashMap, rc::Rc}; + +use async_trait::async_trait; +use tracing::{error, instrument, trace}; + +use super::{ + common::PresenceState, + messaging_interface::{ + AccountMessagingConsumerInterface, AccountMessagingProviderInterface, + RoomMessagingConsumerInterface, SpaceMessagingConsumerInterface, + }, + room::{Room, RoomId}, + space::{Space, SpaceId}, + store_interface::{ + AccountStoreProviderInterface, RoomStoreConsumerInterface, SpaceStoreConsumerInterface, + }, +}; + +type Rooms = HashMap>; +type Spaces = HashMap>; + +pub struct Account { + display_name: RefCell>, + avatar: RefCell>>, + + #[allow(dead_code)] + presence_state: RefCell>, + + by_id_rooms: RefCell, + by_id_spaces: RefCell, + + messaging_provider: Option>, + store: &'static dyn AccountStoreProviderInterface, +} + +impl Account { + pub fn new(store: &'static dyn AccountStoreProviderInterface) -> Self { + Self { + display_name: RefCell::new(None), + avatar: RefCell::new(None), + presence_state: RefCell::new(None), + + by_id_rooms: RefCell::new(Rooms::new()), + by_id_spaces: RefCell::new(Spaces::new()), + + messaging_provider: None, + store, + } + } + + pub fn set_messaging_provider(&mut self, provider: Rc) { + self.messaging_provider = Some(provider.clone()); + } + + #[allow(dead_code)] + pub fn get_room(&self, room_id: &RoomId) -> Option> { + self.by_id_rooms.borrow().get(room_id).cloned() + } + + pub async fn get_display_name(&self) -> &RefCell> { + if self.display_name.borrow().is_none() { + if let Some(requester) = &self.messaging_provider { + let resp = requester.get_display_name().await; + if let Ok(display_name) = resp { + if let Some(display_name) = display_name { + self.display_name.borrow_mut().replace(display_name); + } else { + self.display_name.borrow_mut().take(); + } + } else { + error!("err={:?}", resp); + } + } + } + &self.display_name + } + + pub async fn get_avatar(&self) -> &RefCell>> { + if self.avatar.borrow().is_none() { + if let Some(requester) = &self.messaging_provider { + let resp = requester.get_avatar().await; + if let Ok(avatar) = resp { + if let Some(avatar) = avatar { + self.avatar.borrow_mut().replace(avatar); + } else { + self.avatar.borrow_mut().take(); + } + } else { + error!("err={:?}", resp); + } + } + } + &self.avatar + } +} + +#[async_trait(?Send)] +impl AccountMessagingConsumerInterface for Account { + #[instrument(name = "Account", skip_all)] + async fn on_new_room(&self, room: Rc) -> Rc { + trace!("on_new_room"); + + let room_id = room.id().clone(); + + self.by_id_rooms + .borrow_mut() + .insert(room_id, Rc::clone(&room)); + + let room_store = self + .store + .on_new_room(Rc::clone(&room) as Rc); + + room.set_store(room_store); + + room + } + + #[instrument(name = "Account", skip_all)] + async fn on_new_space(&self, space: Rc) -> Rc { + trace!("on_new_space"); + + let space_id = space.id().clone(); + + self.by_id_spaces + .borrow_mut() + .insert(space_id, Rc::clone(&space)); + + let space_store = self + .store + .on_new_space(Rc::clone(&space) as Rc); + + space.set_store(space_store); + + space + } +} diff --git a/src/domain/model/common.rs b/src/domain/model/common.rs new file mode 100644 index 0000000..ed71f00 --- /dev/null +++ b/src/domain/model/common.rs @@ -0,0 +1,7 @@ +use matrix_sdk::ruma::{presence::PresenceState as MatrixPresenceState, OwnedUserId}; + +pub type Avatar = Vec; + +pub type PresenceState = MatrixPresenceState; + +pub type UserId = OwnedUserId; diff --git a/src/domain/model/messaging_interface.rs b/src/domain/model/messaging_interface.rs new file mode 100644 index 0000000..0d7e4ef --- /dev/null +++ b/src/domain/model/messaging_interface.rs @@ -0,0 +1,68 @@ +use std::rc::Rc; + +use async_trait::async_trait; +use tokio::sync::broadcast::Receiver; + +use super::{ + common::{Avatar, UserId}, + room::{Invitation, Room, RoomId}, + room_member::{AvatarUrl, RoomMember}, + space::Space, +}; +use crate::infrastructure::messaging::matrix::account_event::AccountEvent; + +#[async_trait(?Send)] +pub trait AccountMessagingConsumerInterface { + async fn on_new_room(&self, room: Rc) -> Rc; + async fn on_new_space(&self, space: Rc) -> Rc; +} + +#[async_trait(?Send)] +pub trait AccountMessagingProviderInterface { + async fn get_display_name(&self) -> anyhow::Result>; + async fn get_avatar(&self) -> anyhow::Result>>; + + async fn run_forever( + &self, + account_events_consumer: &dyn AccountMessagingConsumerInterface, + account_events_receiver: Receiver, + ) -> anyhow::Result<()>; +} + +#[async_trait(?Send)] +pub trait RoomMessagingConsumerInterface { + async fn on_invitation(&self, _invitation: Invitation) {} + + async fn on_new_topic(&self, _topic: Option) {} + async fn on_new_name(&self, _name: Option) {} + async fn on_new_avatar(&self, _url: Option) {} + + #[allow(dead_code)] + async fn on_membership(&self, _member: RoomMember) {} +} + +#[async_trait(?Send)] +pub trait RoomMessagingProviderInterface { + async fn get_avatar(&self, id: &RoomId) -> anyhow::Result>; +} + +#[async_trait(?Send)] +pub trait SpaceMessagingConsumerInterface { + async fn on_child(&self, _room_id: RoomId) {} + async fn on_new_topic(&self, _topic: Option) {} + async fn on_new_name(&self, _name: Option) {} +} + +#[async_trait(?Send)] +pub trait SpaceMessagingProviderInterface {} + +// TODO: Rework +#[async_trait(?Send)] +pub trait MemberMessagingProviderInterface { + async fn get_avatar( + &self, + avatar_url: Option, + room_id: RoomId, + user_id: UserId, + ) -> anyhow::Result>; +} diff --git a/src/domain/model/mod.rs b/src/domain/model/mod.rs index 87868cd..8a6ead6 100644 --- a/src/domain/model/mod.rs +++ b/src/domain/model/mod.rs @@ -1,2 +1,8 @@ +pub(crate) mod account; +pub(crate) mod common; +pub(crate) mod messaging_interface; pub(crate) mod room; +pub(crate) mod room_member; pub(crate) mod session; +pub(crate) mod space; +pub(crate) mod store_interface; diff --git a/src/domain/model/room.rs b/src/domain/model/room.rs index 131deeb..d9089a2 100644 --- a/src/domain/model/room.rs +++ b/src/domain/model/room.rs @@ -1,145 +1,318 @@ -use std::cell::RefCell; -use std::{collections::HashMap, sync::Arc}; +use std::{ + cell::RefCell, + collections::HashMap, + fmt::{Debug, Formatter}, + rc::Rc, +}; -use matrix_sdk::ruma::OwnedRoomId; -use matrix_sdk::{Room as MatrixRoom, RoomState as MatrixRoomState}; -use tracing::error; +use async_trait::async_trait; +use futures::future::{join, join_all}; +use matrix_sdk::{ruma::OwnedRoomId, RoomState as MatrixRoomState}; +use tracing::{debug, debug_span, error, instrument, trace}; -pub(crate) type RoomId = OwnedRoomId; +use super::{ + common::{Avatar, UserId}, + messaging_interface::{RoomMessagingConsumerInterface, RoomMessagingProviderInterface}, + room_member::RoomMember, + space::SpaceId, + store_interface::{RoomStoreConsumerInterface, RoomStoreProviderInterface}, +}; +use crate::infrastructure::services::mozaik_builder::create_mozaik; -#[derive(Clone, Debug)] -pub(crate) struct Room { +pub type RoomId = OwnedRoomId; + +#[derive(PartialEq, Clone)] +pub struct Invitation { + invitee_id: UserId, + sender_id: UserId, + is_account_user: bool, +} + +impl Invitation { + pub fn new(invitee_id: UserId, sender_id: UserId, is_account_user: bool) -> Self { + Self { + invitee_id, + sender_id, + is_account_user, + } + } + + pub fn is_account_user(&self) -> bool { + self.is_account_user + } +} + +impl Debug for Invitation { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_tuple("Invitation") + .field(&self.invitee_id) + .field(&self.sender_id) + .field(&self.is_account_user) + .finish() + } +} + +pub struct Room { id: RoomId, - name: Option, + + name: RefCell>, topic: Option, is_direct: Option, state: Option, + avatar: RefCell>, + + invitations: RefCell>, + members: RefCell>, + + spaces: Vec, + + messaging_provider: Option>, + store: RefCell>>, +} + +impl PartialEq for Room { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } } impl Room { - fn new( + pub fn new( id: RoomId, + // TODO: move space at the end of the list of params name: Option, topic: Option, is_direct: Option, state: Option, + spaces: Vec, ) -> Self { Self { id, - name, + + name: RefCell::new(name), topic, is_direct, state, + avatar: RefCell::new(None), + invitations: RefCell::new(HashMap::new()), + members: RefCell::new(HashMap::new()), + spaces, + + messaging_provider: None, + store: RefCell::new(None), } } - // TODO: Use a factory instead... - pub async fn from_matrix_room(matrix_room: &MatrixRoom) -> Self { - // let room_topic = matrix_room.topic().map(RefCell::new); - - let id = RoomId::from(matrix_room.room_id()); - let name = matrix_room.name(); - let room_topic = matrix_room.topic(); - let is_direct = match matrix_room.is_direct().await { - Ok(is_direct) => Some(is_direct), - Err(err) => { - error!("Unable to know if the room \"{id}\" is direct: {err}"); - None - } - }; - let state = Some(matrix_room.state()); - - Self::new(id, name, room_topic, is_direct, state) - - // room.timeline.subscribe().await - - // Arc::new(matrix_room.to_owned()), + pub fn set_messaging_provider( + &mut self, + messaging_provider: Rc, + ) { + self.messaging_provider = Some(messaging_provider); } - pub fn id(&self) -> &OwnedRoomId { + pub fn set_store(&self, store: Rc) { + *self.store.borrow_mut() = Some(store); + } + + pub fn id(&self) -> &RoomId { &self.id } - pub fn name(&self) -> &Option { - &self.name + #[allow(dead_code)] + pub fn name(&self) -> Option { + self.name.borrow().clone() } + #[allow(dead_code)] pub fn topic(&self) -> &Option { &self.topic } + + #[allow(dead_code)] pub fn set_topic(&mut self, topic: Option) { self.topic = topic; } - pub fn is_direct(&self) -> &Option { - &self.is_direct - } - + #[allow(dead_code)] pub fn state(&self) -> &Option { &self.state } + + #[allow(dead_code)] pub fn is_invited(&self) -> Option { - match self.state { - Some(state) => Some(state == MatrixRoomState::Invited), - None => None, + self.state.map(|state| state == MatrixRoomState::Invited) + } + + #[instrument(name = "Room", skip_all)] + fn add_invitation(&self, invitation: Invitation) { + self.members.borrow_mut().remove(&invitation.invitee_id); + + self.invitations + .borrow_mut() + .insert(invitation.invitee_id.clone(), invitation.clone()); + + if let Some(store) = self.store.borrow().as_ref() { + store.on_invitation(invitation); + } + } + + #[instrument(name = "Room", skip_all)] + fn add_member(&self, member: RoomMember) { + let mut members = self.members.borrow_mut(); + + members.insert(member.id().clone(), member.clone()); + + // USe the member display name to name the room if it's direct and has no name set. + if self.name.borrow().is_none() && members.len() == 1 { + if let Some(member_display_name) = member.display_name() { + let name = Some(member_display_name.clone()); + + self.name.borrow_mut().clone_from(&name); + + if let Some(store) = self.store.borrow().as_ref() { + store.on_new_name(name); + } + } + } + + if let Some(store) = self.store.borrow().as_ref() { + store.on_new_member(member); + } + } + + pub async fn get_avatar(&self) -> Option { + if self.avatar.borrow().is_none() { + if let Some(requester) = &self.messaging_provider { + let resp = requester.get_avatar(&self.id).await; + if let Ok(avatar) = resp { + if let Some(avatar) = avatar { + return Some(avatar); + } else { + debug!("The room has no avatar... let's generate one"); + match self.gen_room_avatar_with_members().await { + Ok(avatar) => { + if let Some(avatar) = avatar { + return Some(avatar); + } + } + Err(err) => { + error!("err={}", err); + } + } + } + } else { + error!("err={:?}", resp); + } + } + } + self.avatar.borrow().clone() + } + + #[instrument(name = "Room", skip_all)] + async fn gen_room_avatar_with_members(&self) -> anyhow::Result> { + let mut account_member = None::<&RoomMember>; + let mut other_members = Vec::<&RoomMember>::new(); + + let members = self.members.borrow(); + for member in members.values() { + if member.is_account_user() { + account_member = Some(member); + } else { + other_members.push(member); + } + } + + let other_avatars_futures = + join_all(other_members.iter().map(|member| member.get_avatar())); + + let (other_avatars, account_avatar) = if let Some(account_member) = account_member { + join(other_avatars_futures, account_member.get_avatar()).await + } else { + ( + join_all(other_members.iter().map(|member| member.get_avatar())).await, + None, + ) + }; + + let other_avatars: Vec> = other_avatars.into_iter().flatten().collect(); + + if account_avatar.is_some() || !other_avatars.is_empty() { + let _guard = debug_span!("AvatarRendering").entered(); + Ok(Some( + create_mozaik(256, 256, other_avatars, account_avatar).await, + )) + } else { + Ok(None) } } } -pub type ByIdRooms = HashMap>; +#[async_trait(?Send)] +impl RoomMessagingConsumerInterface for Room { + #[instrument(name = "Room", skip_all)] + async fn on_invitation(&self, invitation: Invitation) { + trace!("on_invitation"); + let sender_id = invitation.sender_id.clone(); -// pub type ByIdRooms = HashMap>; + self.add_invitation(invitation); -// #[derive(Clone)] -// pub struct Room { -// // pub matrix_room: Arc, -// pub topic: Option>, -// pub members: HashMap, -// pub is_direct: Option, -// // pub timeline: Arc, -// } + if self.is_direct.unwrap_or(false) { + debug!("1to1 conversation, using the {} avatar", &sender_id); + if let Ok(avatar) = self.gen_room_avatar_with_members().await { + debug!("Avatar successfully generated"); + self.avatar.borrow_mut().clone_from(&avatar); + if let Some(store) = self.store.borrow().as_ref() { + store.on_new_avatar(avatar); + } + } + } + } -// impl Room { -// pub async fn new( -// matrix_room: Arc, -// topic: Option>, -// is_direct: Option, -// ) -> Self { -// // TODO: Filter events -// // let timeline = Arc::new(matrix_room.timeline_builder().build().await.ok().unwrap()); -// Self { -// matrix_room, -// topic, -// members: HashMap::new(), -// is_direct, -// // timeline, -// } -// } + #[instrument(name = "Room", skip_all)] + async fn on_membership(&self, member: RoomMember) { + trace!("on_membership"); + self.add_member(member); + } -// pub async fn from_matrix_room(matrix_room: &MatrixRoom) -> Self { -// let room_topic = matrix_room.topic().map(RefCell::new); + #[instrument(name = "Room", skip_all)] + async fn on_new_topic(&self, _topic: Option) { + trace!("on_new_topic"); + } -// Self::new( -// Arc::new(matrix_room.to_owned()), -// room_topic, -// matrix_room.is_direct().await.ok(), -// ) -// .await -// // room.timeline.subscribe().await -// } + #[instrument(name = "Room", skip_all)] + async fn on_new_name(&self, _name: Option) { + trace!("on_new_name"); + } -// pub fn name(&self) -> Option { -// self.matrix_room.name() -// } + #[instrument(name = "Room", skip_all)] + async fn on_new_avatar(&self, avatar: Option) { + trace!("on_new_avatar"); + self.avatar.borrow_mut().clone_from(&avatar); + if let Some(store) = self.store.borrow().as_ref() { + store.on_new_avatar(avatar); + } + } +} -// pub fn id(&self) -> OwnedRoomId { -// OwnedRoomId::from(self.matrix_room.room_id()) -// } -// } +#[async_trait(?Send)] +impl RoomStoreConsumerInterface for Room { + fn id(&self) -> &RoomId { + &self.id + } -// impl PartialEq for Room { -// fn eq(&self, other: &Self) -> bool { -// // TODO: Look for a better way to compare Matrix rooms -// self.matrix_room.room_id() == other.matrix_room.room_id() -// } -// } + fn is_direct(&self) -> Option { + self.is_direct + } + + fn name(&self) -> Option { + self.name.borrow().clone() + } + + async fn avatar(&self) -> Option { + self.get_avatar().await + } + + fn spaces(&self) -> &Vec { + &self.spaces + } +} diff --git a/src/domain/model/room_member.rs b/src/domain/model/room_member.rs new file mode 100644 index 0000000..ac7cdd2 --- /dev/null +++ b/src/domain/model/room_member.rs @@ -0,0 +1,93 @@ +use std::{ + cell::RefCell, + fmt::{Debug, Formatter}, + rc::Rc, +}; + +use matrix_sdk::ruma::OwnedMxcUri; +use tracing::error; + +use super::{ + common::{Avatar, UserId}, + messaging_interface::MemberMessagingProviderInterface, + room::RoomId, +}; + +pub type AvatarUrl = OwnedMxcUri; + +#[derive(Clone)] +pub struct RoomMember { + id: UserId, + + display_name: Option, + avatar_url: Option, + room_id: RoomId, + is_account_user: bool, + + #[allow(dead_code)] + avatar: RefCell>, + + messaging_provider: Rc, +} + +impl RoomMember { + pub fn new( + id: UserId, + display_name: Option, + avatar_url: Option, + room_id: RoomId, + is_account_user: bool, + messaging_provider: Rc, + ) -> Self { + Self { + id, + display_name, + avatar_url, + room_id, + is_account_user, + avatar: RefCell::new(None), + messaging_provider, + } + } + + pub fn id(&self) -> &UserId { + &self.id + } + + pub fn display_name(&self) -> &Option { + &self.display_name + } + + #[allow(dead_code)] + pub fn room_id(&self) -> &RoomId { + &self.room_id + } + + pub fn is_account_user(&self) -> bool { + self.is_account_user + } + + pub async fn get_avatar(&self) -> Option { + match self + .messaging_provider + .get_avatar( + self.avatar_url.clone(), + self.room_id.clone(), + self.id.clone(), + ) + .await + { + Ok(avatar) => avatar, + Err(err) => { + error!("err={}", err); + None + } + } + } +} + +impl Debug for RoomMember { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("RoomMember").field("id", &self.id).finish() + } +} diff --git a/src/domain/model/space.rs b/src/domain/model/space.rs new file mode 100644 index 0000000..3974faa --- /dev/null +++ b/src/domain/model/space.rs @@ -0,0 +1,107 @@ +use std::{cell::RefCell, collections::HashSet, rc::Rc}; + +use async_trait::async_trait; +use matrix_sdk::ruma::OwnedRoomId; +use tracing::{instrument, trace}; + +use super::{ + common::Avatar, + messaging_interface::{SpaceMessagingConsumerInterface, SpaceMessagingProviderInterface}, + room::RoomId, + store_interface::{SpaceStoreConsumerInterface, SpaceStoreProviderInterface}, +}; + +pub type SpaceId = OwnedRoomId; + +// TODO: Add membership? +pub struct Space { + id: SpaceId, + + name: RefCell>, + topic: RefCell>, + + #[allow(dead_code)] + avatar: RefCell>, + + children: RefCell>, // We donĀ“t expect to manage nested spaces + + messaging_provider: Option>, + store: RefCell>>, +} + +impl PartialEq for Space { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Space { + pub fn new(id: SpaceId, name: Option, topic: Option) -> Self { + Self { + id, + + name: RefCell::new(name), + topic: RefCell::new(topic), + + #[allow(dead_code)] + avatar: RefCell::new(None), + + children: RefCell::new(HashSet::new()), + + messaging_provider: None, + store: RefCell::new(None), + } + } + + pub fn set_messaging_provider(&mut self, provider: Rc) { + self.messaging_provider = Some(provider); + } + + pub fn set_store(&self, store: Rc) { + *self.store.borrow_mut() = Some(store); + } + + pub fn id(&self) -> &SpaceId { + &self.id + } + + #[allow(dead_code)] + pub fn name(&self) -> Option { + self.name.borrow().clone() + } +} + +#[async_trait(?Send)] +impl SpaceMessagingConsumerInterface for Space { + #[instrument(name = "Space", skip_all)] + async fn on_child(&self, room_id: RoomId) { + trace!("on_child"); + self.children.borrow_mut().insert(room_id); + } + + #[instrument(name = "Space", skip_all)] + async fn on_new_topic(&self, topic: Option) { + trace!("on_new_topic"); + *self.topic.borrow_mut() = topic; + } + + #[instrument(name = "Space", skip_all)] + async fn on_new_name(&self, name: Option) { + trace!("on_new_name"); + self.name.borrow_mut().clone_from(&name); + + if let Some(store) = self.store.borrow().as_ref() { + store.set_name(name); + } + } +} + +impl SpaceStoreConsumerInterface for Space { + fn id(&self) -> &SpaceId { + &self.id + } + + fn name(&self) -> Option { + self.name.borrow().clone() + } +} diff --git a/src/domain/model/store_interface.rs b/src/domain/model/store_interface.rs new file mode 100644 index 0000000..3cc5747 --- /dev/null +++ b/src/domain/model/store_interface.rs @@ -0,0 +1,53 @@ +use std::rc::Rc; + +use async_trait::async_trait; + +use super::{ + common::Avatar, + room::{Invitation, RoomId}, + room_member::RoomMember, + space::SpaceId, +}; + +#[allow(dead_code)] +pub trait AccountStoreConsumerInterface {} + +pub trait AccountStoreProviderInterface { + fn on_new_room( + &self, + room: Rc, + ) -> Rc; + fn on_new_space( + &self, + space: Rc, + ) -> Rc; +} + +#[async_trait(?Send)] +pub trait RoomStoreConsumerInterface { + fn id(&self) -> &RoomId; + fn is_direct(&self) -> Option; + fn name(&self) -> Option; + + #[allow(dead_code)] + async fn avatar(&self) -> Option; + + fn spaces(&self) -> &Vec; +} + +pub trait RoomStoreProviderInterface { + fn on_new_name(&self, name: Option); + fn on_new_avatar(&self, avatar: Option); + fn on_new_member(&self, member: RoomMember); + fn on_invitation(&self, invitation: Invitation); +} + +#[allow(dead_code)] +pub trait SpaceStoreConsumerInterface { + fn id(&self) -> &SpaceId; + fn name(&self) -> Option; +} + +pub trait SpaceStoreProviderInterface { + fn set_name(&self, _name: Option) {} +} diff --git a/src/infrastructure/messaging/matrix/account_event.rs b/src/infrastructure/messaging/matrix/account_event.rs new file mode 100644 index 0000000..4a565e2 --- /dev/null +++ b/src/infrastructure/messaging/matrix/account_event.rs @@ -0,0 +1,63 @@ +use std::fmt::{Debug, Formatter}; + +use matrix_sdk::{ruma::OwnedRoomId, RoomState}; +use tracing::Span; + +use super::room_event::RoomEventsReceiver; +use crate::{domain::model::space::SpaceId, utils::Sender}; + +#[derive(Clone)] +pub enum AccountEvent { + NewRoom( + OwnedRoomId, + Vec, + Option, + Option, + Option, + RoomState, + RoomEventsReceiver, + Sender, + Span, + ), + + NewSpace( + OwnedRoomId, + Option, + Option, + RoomEventsReceiver, + Sender, + Span, + ), +} + +impl Debug for AccountEvent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::NewRoom( + id, + spaces, + name, + topic, + is_direct, + state, + _events_receiver, + _sender, + _span, + ) => f + .debug_tuple("AccountEvent::NewRoom") + .field(id) + .field(spaces) + .field(name) + .field(topic) + .field(is_direct) + .field(state) + .finish(), + Self::NewSpace(id, name, topic, _events_receiver, _sender, _span) => f + .debug_tuple("AccountEvent::NewSpace") + .field(id) + .field(name) + .field(topic) + .finish(), + } + } +} diff --git a/src/infrastructure/messaging/matrix/client.rs b/src/infrastructure/messaging/matrix/client.rs index 5b0dc34..008ce76 100644 --- a/src/infrastructure/messaging/matrix/client.rs +++ b/src/infrastructure/messaging/matrix/client.rs @@ -1,37 +1,47 @@ -use std::borrow::Borrow; -use std::cell::RefCell; -use std::sync::Arc; -use std::time::Duration; +use std::{ + borrow::Borrow, + collections::HashMap, + sync::{Arc, Mutex}, +}; -use async_std::task; +use async_std::stream::StreamExt; use dioxus::prelude::Task; -use log::{debug, error}; -use tokio::sync::broadcast; -use tokio::sync::broadcast::Sender; -use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; -use tokio::sync::oneshot; -use tokio::task::JoinHandle; - use matrix_sdk::{ config::SyncSettings, event_handler::Ctx, - room::Room as MatrixRoom, + media::{MediaFormat, MediaRequest, MediaThumbnailSize}, + room::{ParentSpace, Room}, ruma::{ + api::client::media::get_content_thumbnail::v3::Method, events::{ room::{ - member::{RoomMemberEventContent, StrippedRoomMemberEvent}, - topic::RoomTopicEventContent, + avatar::{RoomAvatarEventContent, StrippedRoomAvatarEvent}, + create::{RoomCreateEventContent, StrippedRoomCreateEvent}, + member::{MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent}, + name::{RoomNameEventContent, StrippedRoomNameEvent}, + topic::{RoomTopicEventContent, StrippedRoomTopicEvent}, + MediaSource, }, SyncStateEvent, }, - OwnedRoomId, + uint, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UserId, }, - Client as MatrixClient, RoomState as MatrixRoomState, + Client as MatrixClient, RoomState, }; +use tokio::sync::{ + broadcast, + broadcast::{error::SendError, Receiver, Sender}, + mpsc::{unbounded_channel, UnboundedReceiver}, +}; +use tracing::{debug, debug_span, error, instrument, warn, Instrument, Span}; -use super::requester::{Receivers, Requester}; -use super::worker_tasks::{LoginStyle, WorkerTask}; -use crate::domain::model::room::Room; +use super::{ + account_event::AccountEvent, + requester::Requester, + room_event::{RoomEvent, RoomEventsReceiver}, + worker_tasks::{LoginStyle, WorkerTask}, +}; +use crate::utils::oneshot; #[derive(thiserror::Error, Debug)] pub enum ClientError { @@ -39,21 +49,53 @@ pub enum ClientError { Matrix(#[from] matrix_sdk::Error), } -#[derive(Clone)] -pub enum RoomEvent { - TopicEvent(OwnedRoomId, String), - MemberEvent(OwnedRoomId, Room), - InviteEvent(OwnedRoomId, Room), -} - #[derive(Clone)] struct Senders { - room_events_sender: Sender, + account_events_sender: Sender, + room_events_senders: Arc>>>, } impl Senders { - fn new(room_events_sender: Sender) -> Self { - Self { room_events_sender } + fn new(account_events_sender: Sender) -> Self { + Self { + account_events_sender, + room_events_senders: Arc::new(Mutex::new(HashMap::new())), + } + } + + fn contains(&self, room_id: &RoomId) -> bool { + let room_senders = self.room_events_senders.lock().unwrap(); + + room_senders.contains_key(room_id) + } + + fn send(&self, room_id: &RoomId, event: RoomEvent) -> Result<(), SendError> { + let room_senders = self.room_events_senders.lock().unwrap(); + + if let Some(room_sender) = room_senders.get(room_id) { + if let Err(err) = room_sender.send(event) { + warn!("Unable to send event to the {room_id} room: {err}"); + return Err(err); + } + } else { + warn!("No sender found for {room_id} room"); + // TODO: Return error + } + Ok(()) + } + + fn add_room(&self, room_id: &OwnedRoomId) -> Option { + let mut senders = self.room_events_senders.lock().unwrap(); + if !senders.contains_key(room_id) { + let (room_sender, room_receiver) = broadcast::channel(32); + + senders.insert(room_id.clone(), room_sender); + debug!("Create sender for {room_id} room"); + + Some(RoomEventsReceiver::new(room_receiver)) + } else { + None + } } } @@ -65,211 +107,467 @@ pub struct Client { } impl Client { - pub fn new(client: Arc, room_events_sender: Sender) -> Self { + pub fn new(client: Arc, account_events_sender: Sender) -> Self { Self { initialized: false, client: Some(client), sync_task: None, - senders: Senders::new(room_events_sender), + senders: Senders::new(account_events_sender), } } - // async fn on_sync_typing_event(_ev: SyncTypingEvent, room: MatrixRoom) { - // debug!("== on_sync_typing_event =="); - // let room_id = room.room_id().to_owned(); - // dbg!(room_id); - // } + #[instrument(skip_all)] + async fn create_space( + senders: &Ctx, + room_id: &OwnedRoomId, + room: Option<&Room>, + ) -> anyhow::Result<(), SendError> { + if let Some(receiver) = senders.add_room(room_id) { + let current_span = Span::current(); - // async fn on_presence_event(_ev: PresenceEvent) { - // debug!("== on_presence_event =="); - // dbg!(_ev); - // } + let mut name = None; + let mut topic = None; + if let Some(room) = room { + name = room.name(); + topic = room.topic(); + } - // async fn on_sync_state_event(ev: SyncStateEvent, _room: MatrixRoom) { - // error!("== on_sync_state_event =="); - // if let SyncStateEvent::Original(ev) = ev { - // dbg!(ev); - // } - // } + let (reply, mut response) = oneshot::(); - // async fn on_original_sync_room_message_event( - // ev: OriginalSyncRoomMessageEvent, - // _matrix_room: MatrixRoom, - // _senders: Ctx, - // ) { - // error!("== on_original_sync_room_message_event =="); - // error!("ev={:?}", ev.content); - // } + // We can't use Room instance here, because dyn PaginableRoom is not Sync + let event = AccountEvent::NewSpace( + room_id.clone(), + name.clone(), + topic.clone(), + receiver, + reply, + current_span.clone(), + ); + senders.account_events_sender.send(event)?; + + // We're expecting a response indicating that the client is able to compute the next RoomEvent + response.recv().await; + + let events = vec![ + RoomEvent::NewTopic(topic, current_span.clone()), + RoomEvent::NewName(name, current_span), + ]; + + for event in events { + if let Err(_err) = senders.send(room_id, event.clone()) { + // TODO: Return an error + } + } + } + + Ok(()) + } + + #[instrument(skip_all)] + async fn create_room( + senders: &Ctx, + room: &Room, + ) -> anyhow::Result<(), SendError> { + let room_id = room.room_id().to_owned(); + + if let Some(receiver) = senders.add_room(&room_id) { + let (reply, mut response) = oneshot::(); + + let is_direct = match room.is_direct().await { + Ok(is_direct) => Some(is_direct), + Err(err) => { + warn!("Unable to know if the {room_id} room is direct: {err}"); + None + } + }; + + let mut parents = vec![]; + + if let Ok(mut spaces) = room.parent_spaces().await { + while let Some(parent) = spaces.next().await { + match parent { + Ok(parent) => match parent { + ParentSpace::Reciprocal(parent) => { + parents.push(parent.room_id().to_owned()); + } + _ => todo!(), + }, + Err(err) => { + error!("{err}"); + } + } + } + } + + // We can't use Room instance here, because dyn PaginableRoom is not Sync + let event = AccountEvent::NewRoom( + room_id.clone(), + parents.clone(), + room.name(), + room.topic(), + is_direct, + room.state(), + receiver, + reply, + Span::current(), + ); + + senders.account_events_sender.send(event)?; + + // We're expecting a response indicating that the client is able to compute the next RoomEvent + response.recv().await; + } + Ok(()) + } + + #[instrument(skip_all)] + async fn add_room( + senders: &Ctx, + room: &Room, + ) -> anyhow::Result<(), SendError> { + let room_id = room.room_id().to_owned(); + + if room.is_space() { + Self::create_space(senders, &room_id, Some(room)).await + } else { + let mut parents = vec![]; + + if let Ok(mut spaces) = room.parent_spaces().await { + while let Some(parent) = spaces.next().await { + match parent { + Ok(parent) => match parent { + ParentSpace::Reciprocal(parent) => { + parents.push(parent.room_id().to_owned()); + } + _ => { + warn!( + "Only ParentSpace::Reciprocal taken into account, skip {:?}", + parent + ); + } + }, + Err(err) => { + error!("{err}"); + } + } + } + } + + for parent in parents { + // Create a minimal space to make the relation consistent... its content will be sync later. + if !senders.contains(&parent) { + let _ = Self::create_space(senders, &parent, None).await; + } + + let event = RoomEvent::NewChild(room_id.clone(), Span::current()); + if let Err(_err) = senders.send(&parent, event) { + // TODO: Return an error + } + } + + Self::create_room(senders, room).await + } + } + + async fn on_stripped_room_create_event( + _ev: StrippedRoomCreateEvent, + room: Room, + senders: Ctx, + ) { + let span = debug_span!("Matrix::NewRoom", r = ?room.room_id()); + + let _ = Self::add_room(&senders, &room).instrument(span).await; + } + + // SyncStateEvent: A possibly-redacted state event without a room_id. + async fn on_sync_room_create_event( + _ev: SyncStateEvent, + room: Room, + senders: Ctx, + ) { + let span = debug_span!("Matrix::NewRoom", r = ?room.room_id()); + + let _ = Self::add_room(&senders, &room).instrument(span).await; + } + + #[instrument(skip_all)] + fn on_invite_room_member_event( + user_id: OwnedUserId, + inviter_id: OwnedUserId, + room: &Room, + matrix_client: &MatrixClient, + senders: &Ctx, + ) { + if let Some(client_user_id) = matrix_client.user_id() { + let room_id = room.room_id(); + let is_account_user = user_id == client_user_id; + + debug!( + "{} (account user: {is_account_user}) invited by {} to join the {} room", + &user_id, &inviter_id, &room_id + ); + + let event = + RoomEvent::Invitation(user_id, inviter_id, is_account_user, Span::current()); + + if let Err(_err) = senders.send(room_id, event) { + // TODO: Return an error + } + } + } + + #[instrument(skip_all)] + fn on_join_room_member_event( + user_id: OwnedUserId, + displayname: Option, + avatar_url: Option, + room: &Room, + matrix_client: &MatrixClient, + senders: &Ctx, + ) { + if let Some(client_user_id) = matrix_client.user_id() { + let is_account_user = user_id == client_user_id; + let room_id = room.room_id(); + + debug!("{} has joined the {} room", &user_id, &room_id); + + let event = RoomEvent::Join( + user_id, + displayname, + avatar_url, + is_account_user, + Span::current(), + ); + + if let Err(_err) = senders.send(room_id, event) { + // TODO: Return an error + } + } + } + + // This function is called on each m.room.member event for an invited room preview (room not already joined). async fn on_stripped_room_member_event( ev: StrippedRoomMemberEvent, matrix_client: MatrixClient, - matrix_room: MatrixRoom, + room: Room, senders: Ctx, ) { - if ev.state_key == matrix_client.user_id().unwrap() - && matrix_room.state() == MatrixRoomState::Invited - { - let room_id = matrix_room.room_id(); - let room = Room::from_matrix_room(&matrix_room).await; + match room.state() { + RoomState::Invited => { + let user_id = &ev.state_key; - if let Err(err) = senders - .room_events_sender - .send(RoomEvent::InviteEvent(room_id.to_owned(), room)) - { - error!( - "Unable to publish the new room with \"{}\" id: {}", - room_id, err - ); + match ev.content.membership { + MembershipState::Invite => { + let span = debug_span!("Matrix::RoomInvitation", r = ?room.room_id()); + + span.in_scope(|| { + Self::on_invite_room_member_event( + user_id.clone(), + ev.sender, + &room, + &matrix_client, + &senders, + ) + }); + } + MembershipState::Join => { + let span = + debug_span!("Matrix::RoomJoin", r = ?room.room_id(), u = ?user_id) + .entered(); + + span.in_scope(|| { + Self::on_join_room_member_event( + ev.sender, + ev.content.displayname, + ev.content.avatar_url, + &room, + &matrix_client, + &senders, + ) + }); + } + _ => { + error!("TODO: {:?}", ev); + } + } + } + _ => { + error!("TODO: {:?}", ev); } } } - async fn on_room_topic_event( - ev: SyncStateEvent, - matrix_room: MatrixRoom, + // SyncStateEvent: A possibly-redacted state event without a room_id. + // RoomMemberEventContent: The content of an m.room.member event. + async fn on_sync_room_member_event( + ev: SyncStateEvent, + matrix_client: MatrixClient, + room: Room, senders: Ctx, ) { if let SyncStateEvent::Original(ev) = ev { - let room_id = matrix_room.room_id(); + match ev.content.membership { + MembershipState::Invite => { + let span = debug_span!("Matrix::RoomInvitation", r = ?room.room_id()); - if let Err(err) = senders - .room_events_sender - .send(RoomEvent::TopicEvent(room_id.to_owned(), ev.content.topic)) - { - error!("Unable to publish the \"{}\" new topic: {}", room_id, err); + span.in_scope(|| { + let invitee_id = ev.state_key; + + Self::on_invite_room_member_event( + invitee_id, + ev.sender, + &room, + &matrix_client, + &senders, + ) + }); + } + MembershipState::Join => { + let user_id = ev.sender; + let span = debug_span!("Matrix::RoomJoin", r = ?room.room_id(), u = ?user_id) + .entered(); + + span.in_scope(|| { + Self::on_join_room_member_event( + user_id, + ev.content.displayname, + ev.content.avatar_url, + &room, + &matrix_client, + &senders, + ) + }); + } + _ => error!("TODO"), } } } - async fn on_room_member_event( - ev: SyncStateEvent, - matrix_room: MatrixRoom, + #[instrument(skip_all)] + async fn on_room_avatar_event(room: &Room, senders: &Ctx) { + let room_id = room.room_id(); + let avatar = match room + .avatar(MediaFormat::Thumbnail(MediaThumbnailSize { + method: Method::Scale, + width: uint!(256), + height: uint!(256), + })) + .await + { + Ok(avatar) => avatar, + Err(err) => { + warn!("Unable to fetch avatar for {}: {err}", &room_id); + None + } + }; + + let event = RoomEvent::NewAvatar(avatar, Span::current()); + + if let Err(_err) = senders.send(room_id, event) { + // TODO: Return an error + } + } + + async fn on_stripped_room_avatar_event( + _ev: StrippedRoomAvatarEvent, + room: Room, + senders: Ctx, + ) { + let span = debug_span!("Matrix::RoomAvatar", r = ?room.room_id()); + + Self::on_room_avatar_event(&room, &senders) + .instrument(span) + .await; + } + + async fn on_sync_room_avatar_event( + ev: SyncStateEvent, + room: Room, senders: Ctx, ) { if let SyncStateEvent::Original(_ev) = ev { - let room_id = matrix_room.room_id(); - let room = Room::from_matrix_room(&matrix_room).await; + dioxus::prelude::spawn(async move { + let span = debug_span!("Matrix::RoomAvatar", r = ?room.room_id()); - if let Err(err) = senders - .room_events_sender - .send(RoomEvent::MemberEvent(room_id.to_owned(), room)) - { - error!( - "Unable to publish the new room with \"{}\" id: {}", - room_id, err - ); - } + Self::on_room_avatar_event(&room, &senders) + .instrument(span) + .await; + }); } } - // async fn on_sync_message_like_room_message_event( - // ev: SyncMessageLikeEvent, - // _room: MatrixRoom, - // _client: MatrixClient, - // ) { - // debug!("== on_sync_message_like_room_message_event =="); - // dbg!(ev); - // } + #[instrument(skip_all)] + fn on_room_name_event(name: Option, room: &Room, senders: &Ctx) { + let event = RoomEvent::NewName(name, Span::current()); - // async fn on_sync_message_like_reaction_event( - // ev: SyncMessageLikeEvent, - // _room: MatrixRoom, - // ) { - // debug!("== on_sync_message_like_reaction_event =="); - // dbg!(ev); - // } + if let Err(_err) = senders.send(room.room_id(), event) { + // TODO: Return an error + } + } - // async fn on_original_sync_room_redaction_event( - // ev: OriginalSyncRoomRedactionEvent, - // _room: MatrixRoom, - // ) { - // debug!("== on_original_sync_room_redaction_event =="); - // dbg!(ev); - // } + async fn on_stripped_room_name_event( + ev: StrippedRoomNameEvent, + room: Room, + senders: Ctx, + ) { + let span = debug_span!("Matrix::RoomName", r = ?room.room_id()); - // async fn on_original_sync_room_member_event( - // _ev: OriginalSyncRoomMemberEvent, - // _room: MatrixRoom, - // _client: MatrixClient, - // ) { - // debug!("== on_original_sync_room_member_event =="); + span.in_scope(|| { + Self::on_room_name_event(ev.content.name, &room, &senders); + }); + } - // let mut store = store_ctx.read().unwrap().to_owned(); - // dbg!(store.rooms.keys()); - // let is_direct = room.is_direct().await.ok(); - // store.rooms.insert( - // OwnedRoomId::from(room_id), - // Arc::new(RwLock::new(Room::new(Arc::new(room), None, is_direct))), - // ); - // let _ = store_ctx.write(store); - // } + async fn on_sync_room_name_event( + ev: SyncStateEvent, + room: Room, + senders: Ctx, + ) { + if let SyncStateEvent::Original(ev) = ev { + let span = debug_span!("Matrix::RoomName", r = ?room.room_id()); - // async fn on_original_sync_key_verif_start_event( - // ev: OriginalSyncKeyVerificationStartEvent, - // _client: MatrixClient, - // ) { - // debug!("== on_original_sync_key_verif_start_event =="); - // dbg!(ev); - // } + span.in_scope(|| { + Self::on_room_name_event(Some(ev.content.name), &room, &senders); + }); + } + } - // async fn on_original_sync_key_verif_key_event( - // ev: OriginalSyncKeyVerificationKeyEvent, - // _client: MatrixClient, - // ) { - // debug!("== on_original_sync_key_verif_key_event =="); - // dbg!(ev); - // } + #[instrument(skip_all)] + fn on_room_topic_event(topic: Option, room: &Room, senders: &Ctx) { + let event = RoomEvent::NewTopic(topic, Span::current()); - // async fn on_original_sync_key_verif_done_event( - // ev: OriginalSyncKeyVerificationDoneEvent, - // _client: MatrixClient, - // ) { - // debug!("== on_original_sync_key_verif_done_event =="); - // dbg!(ev); - // } + if let Err(_err) = senders.send(room.room_id(), event) { + // TODO: Return an error + } + } - // async fn on_device_key_verif_req_event( - // ev: ToDeviceKeyVerificationRequestEvent, - // _client: MatrixClient, - // ) { - // debug!("== on_device_key_verif_req_event =="); - // dbg!(ev); - // } + async fn on_stripped_room_topic_event( + ev: StrippedRoomTopicEvent, + room: Room, + senders: Ctx, + ) { + let span = debug_span!("Matrix::RoomTopic", r = ?room.room_id()); - // async fn on_device_key_verif_start_event( - // ev: ToDeviceKeyVerificationStartEvent, - // _client: MatrixClient, - // ) { - // debug!("== on_device_key_verif_start_event =="); - // dbg!(ev); - // } + span.in_scope(|| { + Self::on_room_topic_event(ev.content.topic, &room, &senders); + }); + } - // async fn on_device_key_verif_key_event( - // ev: ToDeviceKeyVerificationKeyEvent, - // _client: MatrixClient, - // ) { - // debug!("== on_device_key_verif_key_event =="); - // dbg!(ev); - // } + async fn on_sync_room_topic_event( + ev: SyncStateEvent, + room: Room, + senders: Ctx, + ) { + if let SyncStateEvent::Original(ev) = ev { + let span = debug_span!("Matrix::RoomTopic", r = ?room.room_id()); - // async fn on_device_key_verif_done_event( - // ev: ToDeviceKeyVerificationDoneEvent, - // _client: MatrixClient, - // ) { - // debug!("== on_device_key_verif_done_event =="); - // dbg!(ev); - // } - - // async fn on_room_event(ev: SomeEvent, _senders: Ctx) { - // debug!("== on_room_event({}) ==", ev.) - // } - - pub async fn spawn(homeserver_url: String) -> Requester { - let (tx, rx) = unbounded_channel::(); - - let (room_sender, room_receiver) = broadcast::channel(32); + span.in_scope(|| { + Self::on_room_topic_event(Some(ev.content.topic), &room, &senders); + }); + } + } + pub async fn spawn(homeserver_url: String) -> (Requester, Receiver) { let matrix_client = Arc::new( MatrixClient::builder() .homeserver_url(&homeserver_url) @@ -278,56 +576,49 @@ impl Client { .unwrap(), ); - let mut client = Client::new(matrix_client.clone(), room_sender); + let (worker_tasks_sender, worker_tasks_receiver) = unbounded_channel::(); + let (account_events_sender, account_events_receiver) = + broadcast::channel::(32); + + let mut client = Client::new(matrix_client, account_events_sender); dioxus::prelude::spawn(async move { - client.work(rx).await; + client.work(worker_tasks_receiver).await; }); - Requester { - matrix_client, - tx, - receivers: Receivers { - room_receiver: RefCell::new(room_receiver), - }, - } + (Requester::new(worker_tasks_sender), account_events_receiver) } fn init(&mut self) { if let Some(client) = self.client.borrow() { + // TODO: Remove clone? client.add_event_handler_context(self.senders.clone()); + let _ = client.add_event_handler(Client::on_stripped_room_create_event); + let _ = client.add_event_handler(Client::on_sync_room_create_event); + let _ = client.add_event_handler(Client::on_stripped_room_member_event); - let _ = client.add_event_handler(Client::on_room_topic_event); - let _ = client.add_event_handler(Client::on_room_member_event); + let _ = client.add_event_handler(Client::on_sync_room_member_event); - // let _ = client.add_event_handler(Client::on_sync_typing_event); - // let _ = client.add_event_handler(Client::on_presence_event); - // let _ = client.add_event_handler(Client::on_sync_state_event); - // let _ = client.add_event_handler(Client::on_original_sync_room_message_event); + let _ = client.add_event_handler(Client::on_stripped_room_avatar_event); + let _ = client.add_event_handler(Client::on_sync_room_avatar_event); - // let _ = client.add_event_handler(Client::on_sync_message_like_room_message_event); - // let _ = client.add_event_handler(Client::on_sync_message_like_reaction_event); - // let _ = client.add_event_handler(Client::on_original_sync_room_redaction_event); - // let _ = client.add_event_handler(Client::on_original_sync_room_member_event); - // let _ = client.add_event_handler(Client::on_original_sync_key_verif_start_event); - // let _ = client.add_event_handler(Client::on_original_sync_key_verif_key_event); - // let _ = client.add_event_handler(Client::on_original_sync_key_verif_done_event); - // let _ = client.add_event_handler(Client::on_device_key_verif_req_event); - // let _ = client.add_event_handler(Client::on_device_key_verif_start_event); - // let _ = client.add_event_handler(Client::on_device_key_verif_key_event); - // let _ = client.add_event_handler(Client::on_device_key_verif_done_event); + let _ = client.add_event_handler(Client::on_stripped_room_name_event); + let _ = client.add_event_handler(Client::on_sync_room_name_event); + + let _ = client.add_event_handler(Client::on_stripped_room_topic_event); + let _ = client.add_event_handler(Client::on_sync_room_topic_event); self.initialized = true; } } - async fn login_and_sync(&mut self, style: LoginStyle) -> anyhow::Result<()> { - let client = self.client.clone().unwrap(); + async fn login(&mut self, style: LoginStyle) -> anyhow::Result<()> { + let client = self.client.as_ref().unwrap(); match style { LoginStyle::Password(username, password) => { - let _resp = client + client .matrix_auth() .login_username(&username, &password) .initial_device_display_name("TODO") @@ -337,7 +628,11 @@ impl Client { } } - let (synchronized_tx, synchronized_rx) = oneshot::channel::(); + Ok(()) + } + + async fn run_forever(&mut self) { + let client = self.client.clone().unwrap(); let task = dioxus::prelude::spawn(async move { // Sync once so we receive the client state and old messages @@ -350,87 +645,125 @@ impl Client { }; if let Some(sync_token) = sync_token_option { - let settings = SyncSettings::default().token(sync_token); - debug!("User connected to the homeserver, start syncing"); - if let Err(err) = synchronized_tx.send(true) { - error!("Unable to notify that the Matrix client is now synchronized ({err})"); - } - + let settings = SyncSettings::default().token(sync_token); let _ = client.sync(settings).await; } }); self.sync_task = Some(task); - - // self.start_background_tasks(synchronized_rx); - - Ok(()) } - // async fn register_room_events(&self, room_id: OwnedRoomId) { - // let client = self.client.unwrap(); + async fn get_display_name(&mut self) -> anyhow::Result> { + let client = self.client.as_ref().unwrap(); - // client.add_room_event_handler(&room_id, Client::on_room_event); - // } + match client.account().get_display_name().await { + Ok(display_name) => Ok(display_name), + Err(err) => Err(err.into()), + } + } - // async fn refresh_rooms( - // matrix_client: &Arc, - // room_events_sender: &Sender, - // ) { - // let joined_matrix_rooms_ref = &matrix_client.joined_rooms(); - // let invited_matrix_rooms_ref = &matrix_client.invited_rooms(); + async fn get_avatar(&mut self) -> anyhow::Result>> { + let client = self.client.as_ref().unwrap(); - // for matrix_rooms in [joined_matrix_rooms_ref, invited_matrix_rooms_ref] { - // for matrix_room in matrix_rooms.iter() { - // let room = Room::from_matrix_room(matrix_room).await; - // let event = RoomEvent::MemberEvent(room.id().clone(), room); + match client + .account() + .get_avatar(MediaFormat::Thumbnail(MediaThumbnailSize { + method: Method::Scale, + width: uint!(256), + height: uint!(256), + })) + .await + { + Ok(avatar) => Ok(avatar), + Err(err) => Err(err.into()), + } + } - // if let Err(err) = room_events_sender.send(event) { - // error!("Error: {}", err); - // } - // } - // } - // } + async fn get_room_avatar(&mut self, room_id: &OwnedRoomId) -> anyhow::Result>> { + let client = self.client.as_ref().unwrap(); - // async fn refresh_rooms_forever( - // matrix_client: Arc, - // room_events_sender: &Sender, - // ) { - // // TODO: Add interval to config - // let period_sec = Duration::from_secs(5); + if let Some(room) = client.get_room(room_id) { + match room + .avatar(MediaFormat::Thumbnail(MediaThumbnailSize { + method: Method::Scale, + width: uint!(256), + height: uint!(256), + })) + .await + { + Ok(avatar) => Ok(avatar), + Err(err) => Err(err.into()), + } + } else { + warn!("No room found with the \"{}\" id", room_id.as_str()); + // TODO: Return an error if the room has not been found + Ok(None) + } + } - // loop { - // Self::refresh_rooms(&matrix_client, room_events_sender).await; + // TODO: Share MediaRequest with other media requests + async fn get_thumbnail(&self, media_url: OwnedMxcUri) -> anyhow::Result> { + let client = self.client.as_ref().unwrap(); + let media = client.media(); - // task::sleep(period_sec).await; - // } - // } + let request = MediaRequest { + source: MediaSource::Plain(media_url), + format: MediaFormat::Thumbnail(MediaThumbnailSize { + method: Method::Scale, + width: uint!(256), + height: uint!(256), + }), + }; - // fn start_background_tasks(&mut self, synchronized_rx: oneshot::Receiver) { - // let client = self.client.clone().unwrap(); - // let room_events_sender = self.senders.room_events_sender.clone(); + let res = media.get_media_content(&request, true).await; - // let task = dioxus::prelude::spawn(async move { - // if let Err(err) = synchronized_rx.await { - // error!("Unable to setup the rx channel notifying that the Matrix client is now synchronized ({err})"); - // } + Ok(res?) + } - // debug!("Start room refreshing forever"); + async fn get_room_member_avatar( + &self, + avatar_url: &Option, + room_id: &RoomId, + user_id: &UserId, + ) -> anyhow::Result>> { + let client = self.client.as_ref().unwrap(); - // let _ = Self::refresh_rooms_forever(client, &room_events_sender).await; - // }); - // self.background_task = Some(task); - // } + if let Some(room) = client.get_room(room_id) { + match avatar_url { + Some(avatar_url) => { + let thumbnail = self.get_thumbnail(avatar_url.clone()).await; + return Ok(Some(thumbnail?)); + } + None => match room.get_member(user_id).await { + Ok(room_member) => { + if let Some(room_member) = room_member { + let res = match room_member + .avatar(MediaFormat::Thumbnail(MediaThumbnailSize { + method: Method::Scale, + width: uint!(256), + height: uint!(256), + })) + .await + { + Ok(avatar) => Ok(avatar), + Err(err) => Err(err.into()), + }; + return res; + } + } + Err(err) => { + warn!("Unable to get room member {user_id}: {err}"); + } + }, + } + } + Ok(None) + } async fn work(&mut self, mut rx: UnboundedReceiver) { - loop { - match rx.recv().await { - Some(task) => self.run(task).await, - None => { - break; - } - } + while let Some(task) = rx.recv().await { + self.run(task).await; } if let Some(task) = self.sync_task.take() { @@ -441,17 +774,37 @@ impl Client { async fn run(&mut self, task: WorkerTask) { match task { WorkerTask::Init(reply) => { - assert!(!self.initialized); self.init(); - reply.send(()).await; + reply.send(Ok(())).await; + } + WorkerTask::RunForever(reply) => { + { + self.run_forever().await; + reply.send(()) + } + .await } WorkerTask::Login(style, reply) => { - assert!(self.initialized); - reply.send(self.login_and_sync(style).await).await; - } // WorkerTask::registerRoomEvents(room_id, reply) => { - // assert!(self.initialized); - // reply.send(self.register_room_events(room_id).await).await; - // } + reply.send(self.login(style).await).await; + } + WorkerTask::GetDisplayName(reply) => { + reply.send(self.get_display_name().await).await; + } + WorkerTask::GetAvatar(reply) => { + reply.send(self.get_avatar().await).await; + } + + WorkerTask::GetRoomAvatar(id, reply) => { + reply.send(self.get_room_avatar(&id).await).await; + } + WorkerTask::GetRoomMemberAvatar(avatar_url, room_id, user_id, reply) => { + reply + .send( + self.get_room_member_avatar(&avatar_url, &room_id, &user_id) + .await, + ) + .await; + } } } } diff --git a/src/infrastructure/messaging/matrix/mod.rs b/src/infrastructure/messaging/matrix/mod.rs index e0e9b7d..0856f53 100644 --- a/src/infrastructure/messaging/matrix/mod.rs +++ b/src/infrastructure/messaging/matrix/mod.rs @@ -1,3 +1,5 @@ +pub(crate) mod account_event; pub(crate) mod client; pub(crate) mod requester; +pub(crate) mod room_event; pub(crate) mod worker_tasks; diff --git a/src/infrastructure/messaging/matrix/requester.rs b/src/infrastructure/messaging/matrix/requester.rs index c984ad3..85f7f29 100644 --- a/src/infrastructure/messaging/matrix/requester.rs +++ b/src/infrastructure/messaging/matrix/requester.rs @@ -1,64 +1,386 @@ -use std::cell::RefCell; -use std::sync::Arc; +use std::{collections::HashMap, rc::Rc}; -use matrix_sdk::Client as MatrixClient; -use tokio::sync::broadcast::Receiver; -use tokio::sync::mpsc::UnboundedSender; +use async_trait::async_trait; +use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId}; +use tokio::{ + select, + sync::{broadcast::Receiver, mpsc::UnboundedSender}, +}; +use tokio_stream::{wrappers::BroadcastStream, StreamExt, StreamMap}; +use tracing::{error, instrument, Instrument}; -use super::client::RoomEvent; -use super::worker_tasks::{LoginStyle, WorkerTask}; -use crate::utils::oneshot; +use super::{ + account_event::AccountEvent, + room_event::RoomEvent, + worker_tasks::{LoginStyle, WorkerTask}, +}; +use crate::{ + domain::model::{ + common::{Avatar, UserId}, + messaging_interface::{ + AccountMessagingConsumerInterface, AccountMessagingProviderInterface, + MemberMessagingProviderInterface, RoomMessagingConsumerInterface, + RoomMessagingProviderInterface, SpaceMessagingConsumerInterface, + SpaceMessagingProviderInterface, + }, + room::{Invitation, Room, RoomId}, + room_member::{AvatarUrl, RoomMember}, + space::Space, + }, + utils::oneshot, +}; -pub struct Receivers { - pub room_receiver: RefCell>, +pub struct Requester { + worker_tasks_sender: UnboundedSender, } -impl Clone for Receivers { + +impl Clone for Requester { fn clone(&self) -> Self { Self { - room_receiver: RefCell::new(self.room_receiver.borrow().resubscribe()), + worker_tasks_sender: self.worker_tasks_sender.clone(), } } } -impl PartialEq for Receivers { - fn eq(&self, other: &Self) -> bool { - self.room_receiver - .borrow() - .same_channel(&other.room_receiver.borrow()) + +impl Requester { + pub fn new(worker_tasks_sender: UnboundedSender) -> Self { + Self { + worker_tasks_sender, + } } } -pub struct Requester { - pub matrix_client: Arc, - pub tx: UnboundedSender, - pub receivers: Receivers, +// TODO: Is there a way to avoid this duplication? +macro_rules! request_to_worker { + ($self:ident, $task:expr) => { + { + let (reply, mut response) = oneshot(); + + let task = $task(reply); + + if let Err(err) = $self.worker_tasks_sender.send(task) { + let msg = format!("Unable to request to the Matrix client: {err}"); + return Err(anyhow::Error::msg(msg)); + } + + match response.recv().await { + Some(result) => result, + None => Err(anyhow::Error::msg("TBD")), + } + } + }; + + ($self:ident, $task:expr $(, $arg:expr)+) => { + { + let (reply, mut response) = oneshot(); + + let task = $task($($arg),*, reply); + + if let Err(err) = $self.worker_tasks_sender.send(task) { + let msg = format!("Unable to request to the Matrix client: {err}"); + return Err(anyhow::Error::msg(msg)); + } + + match response.recv().await { + Some(result) => result, + None => Err(anyhow::Error::msg("TBD")), + } + } + }; } impl Requester { pub async fn init(&self) -> anyhow::Result<()> { - let (reply, mut response) = oneshot(); - - if let Err(err) = self.tx.send(WorkerTask::Init(reply)) { - let msg = format!("Unable to request the init of the Matrix client: {err}"); - return Err(anyhow::Error::msg(msg)); - } - - match response.recv().await { - Some(result) => Ok(result), - None => Err(anyhow::Error::msg("TBD")), - } + request_to_worker!(self, WorkerTask::Init) } pub async fn login(&self, style: LoginStyle) -> anyhow::Result<()> { - let (reply, mut response) = oneshot(); + request_to_worker!(self, WorkerTask::Login, style) + } - if let Err(err) = self.tx.send(WorkerTask::Login(style, reply)) { + #[instrument(skip_all)] + async fn on_room_invitation( + consumer: &Rc, + user_id: OwnedUserId, + sender_id: OwnedUserId, + is_account_user: bool, + ) { + let invitation = Invitation::new(user_id, sender_id, is_account_user); + consumer.on_invitation(invitation).await; + } + + #[instrument(skip_all)] + async fn on_room_join( + consumer: &Rc, + room_id: OwnedRoomId, + user_id: OwnedUserId, + user_name: Option, + avatar_url: Option, + is_account_user: bool, + messaging_provider: Rc, + ) { + let member = RoomMember::new( + UserId::from(user_id), + user_name, + avatar_url, + room_id, + is_account_user, + messaging_provider, + ); + consumer.on_membership(member).await; + } + + #[instrument(skip_all)] + async fn on_room_new_topic( + consumer: &Rc, + topic: Option, + ) { + consumer.on_new_topic(topic).await; + } + + #[instrument(skip_all)] + async fn on_room_new_name( + consumer: &Rc, + name: Option, + ) { + consumer.on_new_name(name).await; + } + + #[instrument(skip_all)] + async fn on_room_new_avatar( + consumer: &Rc, + avatar: Option, + ) { + consumer.on_new_avatar(avatar).await; + } + + #[instrument(skip_all)] + async fn on_space_new_child( + consumer: &Rc, + child_id: RoomId, + ) { + // TODO: Make name consistent + consumer.on_child(child_id).await; + } + + #[instrument(skip_all)] + async fn on_space_new_topic( + consumer: &Rc, + topic: Option, + ) { + consumer.on_new_topic(topic).await; + } + + #[instrument(skip_all)] + async fn on_space_new_name( + consumer: &Rc, + name: Option, + ) { + consumer.on_new_name(name).await; + } + + // #[instrument(name="SpaceAvatar", skip_all, fields(s = %space_id, a = avatar.is_some()))] + // async fn on_space_new_avatar( + // consumer: &Rc, + // space_id: OwnedRoomId, + // avatar: Option, + // ) { + // consumer.on_new_avatar(avatar).await; + // } +} + +#[async_trait(?Send)] +impl AccountMessagingProviderInterface for Requester { + async fn get_display_name(&self) -> anyhow::Result> { + request_to_worker!(self, WorkerTask::GetDisplayName) + } + + async fn get_avatar(&self) -> anyhow::Result> { + request_to_worker!(self, WorkerTask::GetAvatar) + } + + async fn run_forever( + &self, + account_events_consumer: &dyn AccountMessagingConsumerInterface, + mut account_events_receiver: Receiver, + ) -> anyhow::Result<()> { + // TODO: manage the result provided by response + let (run_forever_tx, _run_forever_rx) = oneshot(); + + if let Err(err) = self + .worker_tasks_sender + .send(WorkerTask::RunForever(run_forever_tx)) + { let msg = format!("Unable to request login to the Matrix client: {err}"); return Err(anyhow::Error::msg(msg)); } - match response.recv().await { - Some(result) => result, - None => Err(anyhow::Error::msg("TBD")), + let mut rooms_events_streams = StreamMap::new(); + let mut spaces_events_streams = StreamMap::new(); + + let mut room_events_consumers = + HashMap::>::new(); + let mut space_events_consumers = + HashMap::>::new(); + + // TODO: Fix this... + let client = Rc::new(self.clone()); + + loop { + select! { + res = account_events_receiver.recv() => { + if let Ok(account_event) = res { + match account_event { + AccountEvent::NewRoom( + id, + spaces, + name, + topic, + is_direct, + state, + receiver, + new_room_tx, + span + ) => { + let mut room = Room::new(id, name, topic, is_direct, Some(state), spaces); + let room_id = room.id().clone(); + + room.set_messaging_provider(client.clone()); + + let room = Rc::new(room); + + let stream = BroadcastStream::new(receiver.into()); + rooms_events_streams.insert(room_id.clone(), stream); + + let room_events_consumer = account_events_consumer.on_new_room(room) + .instrument(span) + .await; + room_events_consumers.insert(room_id, room_events_consumer); + + // We're now ready to recv and compute RoomEvent. + new_room_tx.send(true).await; + }, + AccountEvent::NewSpace(id, name, topic, receiver, new_space_tx, span) => { + let mut space = Space::new(id, name, topic); + let space_id = space.id().clone(); + + space.set_messaging_provider(client.clone()); + + let space = Rc::new(space); + + let stream = BroadcastStream::new(receiver.into()); + spaces_events_streams.insert(space_id.clone(), stream); + + let space_events_consumer = account_events_consumer.on_new_space(space) + .instrument(span) + .await; + space_events_consumers.insert(space_id, space_events_consumer); + + // We're now ready to recv and compute SpaceEvent. + new_space_tx.send(true).await; + }, + }; + } + }, + Some((room_id, room_event)) = rooms_events_streams.next() => { + if let Ok(room_event) = room_event { + if let Some(consumer) = room_events_consumers.get(&room_id) { + match room_event { + RoomEvent::Invitation(user_id, sender_id, is_account_user, span) => { + Self::on_room_invitation(consumer, user_id, sender_id, is_account_user) + .instrument(span) + .await; + }, + RoomEvent::Join(user_id, user_name, avatar_url, is_account_user, span) => { + Self::on_room_join( + consumer, + room_id, + user_id, + user_name, + avatar_url, + is_account_user, + client.clone()) + .instrument(span) + .await; + }, + RoomEvent::NewTopic(topic, span) => { + Self::on_room_new_topic(consumer, topic) + .instrument(span) + .await; + }, + RoomEvent::NewName(name, span) => { + Self::on_room_new_name(consumer, name) + .instrument(span) + .await; + }, + RoomEvent::NewAvatar(avatar, span) => { + Self::on_room_new_avatar(consumer, avatar) + .instrument(span) + .await; + } + // RoomEvent::NewAvatar(avatar) => Self::on_room_new_avatar(consumer, avatar).await, + _ => error!("TODO: {:?}", &room_event), + } + } else { + error!("No consumer found for {} room", &room_id); + } + } + }, + Some((space_id, room_event)) = spaces_events_streams.next() => { + if let Ok(room_event) = room_event { + if let Some(consumer) = space_events_consumers.get(&space_id) { + match room_event { + RoomEvent::NewTopic(topic, span) => { + Self::on_space_new_topic(consumer, topic) + .instrument(span) + .await; + }, + RoomEvent::NewName(name, span) => { + Self::on_space_new_name(consumer, name) + .instrument(span) + .await; + }, + RoomEvent::NewChild(child_id, span) => { + Self::on_space_new_child(consumer, child_id) + .instrument(span) + .await; + }, + _ => error!("TODO: {:?}", &room_event), + } + } else { + error!("No consumer found for {} space", &space_id); + } + } + } + } } } } + +#[async_trait(?Send)] +impl RoomMessagingProviderInterface for Requester { + async fn get_avatar(&self, room_id: &RoomId) -> anyhow::Result> { + request_to_worker!(self, WorkerTask::GetRoomAvatar, room_id.clone()) + } +} + +#[async_trait(?Send)] +impl SpaceMessagingProviderInterface for Requester {} + +#[async_trait(?Send)] +impl MemberMessagingProviderInterface for Requester { + async fn get_avatar( + &self, + avatar_url: Option, + room_id: RoomId, + user_id: UserId, + ) -> anyhow::Result> { + request_to_worker!( + self, + WorkerTask::GetRoomMemberAvatar, + avatar_url, + room_id, + user_id + ) + } +} diff --git a/src/infrastructure/messaging/matrix/room_event.rs b/src/infrastructure/messaging/matrix/room_event.rs new file mode 100644 index 0000000..e3edfeb --- /dev/null +++ b/src/infrastructure/messaging/matrix/room_event.rs @@ -0,0 +1,71 @@ +use std::fmt::{Debug, Formatter}; + +use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId}; +use tokio::sync::broadcast::Receiver; +use tracing::Span; + +use crate::domain::model::common::Avatar; + +#[derive(Clone)] +pub enum RoomEvent { + Invitation(OwnedUserId, OwnedUserId, bool, Span), + Join(OwnedUserId, Option, Option, bool, Span), + + NewTopic(Option, Span), + NewName(Option, Span), + NewAvatar(Option, Span), + NewChild(OwnedRoomId, Span), +} + +impl Debug for RoomEvent { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + Self::Invitation(invitee_id, sender_id, is_account_user, _span) => f + .debug_tuple("RoomEvent::Invitation") + .field(invitee_id) + .field(sender_id) + .field(is_account_user) + .finish(), + Self::Join(user_id, user_name, avatar_url, is_account_user, _span) => f + .debug_tuple("RoomEvent::Join") + .field(user_id) + .field(user_name) + .field(avatar_url) + .field(is_account_user) + .finish(), + Self::NewTopic(topic, _span) => { + f.debug_tuple("RoomEvent::NewTopic").field(topic).finish() + } + Self::NewName(name, _span) => f.debug_tuple("RoomEvent::NewName").field(name).finish(), + Self::NewAvatar(avatar, _span) => f + // Self::NewAvatar(avatar) => f + .debug_tuple("RoomEvent::NewAvatar") + .field(&format!("is_some: {}", &avatar.is_some())) + .finish(), + Self::NewChild(room_id, _span) => f + .debug_tuple("SpaceEvent::NewChild") + .field(room_id) + .finish(), + } + } +} + +pub struct RoomEventsReceiver(Receiver); + +impl Clone for RoomEventsReceiver { + fn clone(&self) -> Self { + Self(self.0.resubscribe()) + } +} + +impl RoomEventsReceiver { + pub fn new(inner: Receiver) -> Self { + Self(inner) + } +} + +impl From for Receiver { + fn from(val: RoomEventsReceiver) -> Self { + val.0 + } +} diff --git a/src/infrastructure/messaging/matrix/worker_tasks.rs b/src/infrastructure/messaging/matrix/worker_tasks.rs index e81d4e6..e8bb291 100644 --- a/src/infrastructure/messaging/matrix/worker_tasks.rs +++ b/src/infrastructure/messaging/matrix/worker_tasks.rs @@ -1,19 +1,28 @@ use std::fmt::{Debug, Formatter}; +use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId}; + use crate::utils::Sender; #[derive(Debug)] pub enum LoginStyle { - // SessionRestore(Session), Password(String, String), } pub enum WorkerTask { - // Init(AsyncProgramStore, ClientReply<()>), - // Init(ClientReply<()>), - Init(Sender<()>), - //Login(LoginStyle, ClientReply), + Init(Sender>), Login(LoginStyle, Sender>), + RunForever(Sender<()>), + GetDisplayName(Sender>>), + GetAvatar(Sender>>>), + + GetRoomAvatar(OwnedRoomId, Sender>>>), + GetRoomMemberAvatar( + Option, + OwnedRoomId, + OwnedUserId, + Sender>>>, + ), } impl Debug for WorkerTask { @@ -24,11 +33,34 @@ impl Debug for WorkerTask { .field(&format_args!("_")) // .field(&format_args!("_")) .finish(), + WorkerTask::RunForever(_) => f + .debug_tuple("WorkerTask::RunForever") + .field(&format_args!("_")) + .finish(), WorkerTask::Login(style, _) => f .debug_tuple("WorkerTask::Login") .field(style) // .field(&format_args!("_")) .finish(), + WorkerTask::GetDisplayName(_) => f + .debug_tuple("WorkerTask::GetDisplayName") + .field(&format_args!("_")) + .finish(), + WorkerTask::GetAvatar(_) => f + .debug_tuple("WorkerTask::GetAvatar") + .field(&format_args!("_")) + .finish(), + + WorkerTask::GetRoomAvatar(id, _) => f + .debug_tuple("WorkerTask::GetRoomAvatar") + .field(id) + .finish(), + WorkerTask::GetRoomMemberAvatar(room_id, user_id, avatar_url, _) => f + .debug_tuple("WorkerTask::GetRoomMemberAvatar") + .field(avatar_url) + .field(room_id) + .field(user_id) + .finish(), } } } diff --git a/src/infrastructure/services/mod.rs b/src/infrastructure/services/mod.rs index 6e340f5..4244fb5 100644 --- a/src/infrastructure/services/mod.rs +++ b/src/infrastructure/services/mod.rs @@ -1 +1,2 @@ +pub(crate) mod mozaik_builder; pub(crate) mod random_svg_generators; diff --git a/src/infrastructure/services/mozaik_builder.rs b/src/infrastructure/services/mozaik_builder.rs new file mode 100644 index 0000000..fb946b4 --- /dev/null +++ b/src/infrastructure/services/mozaik_builder.rs @@ -0,0 +1,116 @@ +use std::io::Cursor; + +use image::imageops::FilterType; +use image::io::Reader; +use image::{DynamicImage, ImageFormat}; +use image::{GenericImage, RgbImage}; +use tracing::{error, warn}; + +cfg_if! { + if #[cfg(not(target_family = "wasm"))] { + use tokio::task::spawn_blocking; + } +} + +fn from_raw_to_image(raw: &Vec) -> Option { + match Reader::new(Cursor::new(raw)).with_guessed_format() { + Ok(reader) => match reader.decode() { + Ok(image) => return Some(image), + Err(err) => error!("Unable to decode the image: {}", err), + }, + Err(err) => { + error!("Unable to read the image: {}", err) + } + } + None +} + +fn create_mozaik_( + width_px: u32, + height_px: u32, + images: &[Vec], + padding_image: &Option>, +) -> Vec { + let placeholder = DynamicImage::new_rgb8(128, 128); + + let images: Vec> = images.iter().map(from_raw_to_image).collect(); + let padding_image = if let Some(padding_image) = padding_image { + from_raw_to_image(padding_image) + } else { + None + }; + + let mut bytes: Vec = Vec::new(); + + let mut allocations: Vec<&Option> = vec![]; + let mut images_per_row = 1; + let mut images_per_col = 1; + + match images.len() { + 0 => { + allocations.push(&padding_image); + } + 1 => { + allocations.push(&images[0]); + } + 2 => { + allocations.extend_from_slice(&[&images[0], &images[1], &images[1], &images[0]]); + images_per_row = 2; + images_per_col = 2; + } + _ => { + // TODO: Manage other cases + warn!("For now, we only manage the rendering of mozaic with less than 3 images"); + return bytes; + } + } + + let image_width_px = width_px / images_per_row; + let image_height_px = height_px / images_per_col; + + let mut output = RgbImage::new(width_px, height_px); + + let mut row_pos = 0; + for (index, image) in allocations.iter().enumerate() { + if index > 0 && index % images_per_row as usize == 0 { + row_pos += 1; + } + + let col_pos = index - (images_per_row as usize * row_pos); + + let image = *image; + + let scaled = image + .as_ref() + .unwrap_or(&placeholder) + .resize_to_fill(image_width_px, image_height_px, FilterType::Nearest) + .into_rgb8(); + + let output_image_pos_x = col_pos as u32 * image_width_px; + let output_image_pos_y = row_pos as u32 * image_height_px; + + let _ = output.copy_from(&scaled, output_image_pos_x, output_image_pos_y); + } + + let _ = output.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Jpeg); + + bytes +} + +pub async fn create_mozaik( + width_px: u32, + height_px: u32, + images: Vec>, + padding_image: Option>, +) -> Vec { + cfg_if! { + if #[cfg(target_family = "wasm")] { + create_mozaik_(width_px, height_px, &images, &padding_image) + } + else { + spawn_blocking(move || { + create_mozaik_(width_px, height_px, &images, &padding_image) + }).await.unwrap() + } + } +} diff --git a/src/infrastructure/services/random_svg_generators.rs b/src/infrastructure/services/random_svg_generators.rs index ed1a7c8..2ca21e0 100644 --- a/src/infrastructure/services/random_svg_generators.rs +++ b/src/infrastructure/services/random_svg_generators.rs @@ -3,15 +3,17 @@ use std::future::Future; use std::sync::OnceLock; use std::{collections::HashMap, future::IntoFuture}; -use log::error; use rand::distributions::{Alphanumeric, DistString}; use reqwest::Result as RequestResult; +use tracing::error; -#[cfg(feature = "desktop")] -use tokio::fs::read_to_string; - -#[cfg(feature = "web")] -use web_sys; +cfg_if! { + if #[cfg(target_family = "wasm")] { + use web_sys; + } else { + use tokio::fs::read_to_string; + } +} #[derive(Eq, PartialEq, Hash)] pub enum AvatarFeeling { @@ -116,7 +118,7 @@ async fn fetch_text(req: String) -> RequestResult { async fn fetch_dicebear_svg( r#type: &DicebearType, - req_fields: &Vec, + req_fields: &[String], placeholder_fetcher: Option>>>, ) -> String { // TODO: Use configuration file @@ -145,39 +147,38 @@ async fn fetch_dicebear_svg( text.unwrap_or("".to_string()) } -#[cfg(feature = "desktop")] -fn gen_placeholder_fetcher<'a>(path: &'static str) -> Box>> { - let path = format!("./public/{}", &path); - Box::new(async move { - match read_to_string(&path).await { - Ok(content) => Some(content), - Err(err) => { - error!( - "Error during the access to the {path} file: {}", - err.to_string() - ); - None - } +cfg_if! { + if #[cfg(target_family = "wasm")] { + fn gen_placeholder_fetcher<'a>(path: &'static str) -> Box>> { + Box::new(async move { + let url = format!("{}{}", web_sys::window().unwrap().origin(), path); + match fetch_text(url).await { + Ok(content) => Some(content), + Err(err) => { + error!("Error during {path} fetching: {}", err.to_string()); + None + } + } + }) } - }) -} -#[cfg(feature = "web")] -fn gen_placeholder_fetcher<'a>(path: &'static str) -> Box>> { - Box::new(async move { - let url = format!("{}{}", web_sys::window().unwrap().origin(), path); - match fetch_text(url).await { - Ok(content) => Some(content), - Err(err) => { - error!("Error during {path} fetching: {}", err.to_string()); - None - } + } + else { + fn gen_placeholder_fetcher(path: &'static str) -> Box>> { + let path = format!("./public/{}", &path); + Box::new(async move { + match read_to_string(&path).await { + Ok(content) => Some(content), + Err(err) => { + error!( + "Error during the access to the {path} file: {}", + err.to_string() + ); + None + } + } + }) } - }) -} - -#[cfg(not(any(feature = "desktop", feature = "web")))] -fn gen_placeholder_fetcher<'a>(_path: &'static str) -> Box>> { - Box::new(async move { None }) + } } pub async fn generate_random_svg_avatar<'a>(config: Option<&'a AvatarConfig<'a>>) -> String { diff --git a/src/main.rs b/src/main.rs index 592256b..c29d3f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,125 +1,142 @@ #![allow(non_snake_case)] +#[macro_use] +extern crate cfg_if; + mod domain; mod infrastructure; mod ui; mod utils; +use std::rc::Rc; + use dioxus::prelude::*; +use futures_util::stream::StreamExt; +use tracing::{debug, error, warn}; +use tracing_forest::ForestLayer; +use tracing_subscriber::{prelude::*, EnvFilter}; -#[cfg(feature = "desktop")] -use dioxus::desktop::Config; +use crate::{ + domain::model::{messaging_interface::AccountMessagingProviderInterface, session::Session}, + infrastructure::messaging::matrix::{client::Client, worker_tasks::LoginStyle}, + ui::{ + layouts::{conversations::Conversations, login::Login}, + ACCOUNT, SESSION, + }, +}; -use tracing::debug; -use tracing_subscriber::prelude::*; +cfg_if! { + if #[cfg(target_family = "wasm")] { + use tracing_web::MakeWebConsoleWriter; + } else { + use dioxus::desktop::Config; + use std::fs::File; + use time::format_description::well_known::Iso8601; + use tracing_subscriber::fmt::time::UtcTime; + } +} -#[cfg(feature = "web")] -use tracing_web::MakeWebConsoleWriter; +async fn login(mut rx: UnboundedReceiver, session: &GlobalSignal) { + while let Some(is_logged) = rx.next().await { + if !is_logged { + let homeserver_url = session.read().homeserver_url.clone(); + let username = session.read().username.clone(); + let password = session.read().password.clone(); -use crate::base::{login, sync_rooms}; -use crate::base::{APP_SETTINGS, ROOMS, SESSION}; -use crate::ui::components::login::Login; -use crate::ui::components::main_window::MainWindow; -use crate::ui::views::login_view::LoginView; + if homeserver_url.is_some() && username.is_some() && password.is_some() { + let (requester, account_events_receiver) = + Client::spawn(homeserver_url.unwrap()).await; -mod base; + if let Err(err) = requester.init().await { + warn!("Unable to login: {}", err); + } + + match requester + .login(LoginStyle::Password(username.unwrap(), password.unwrap())) + .await + { + Ok(_) => { + debug!("successfully logged"); + session.write().is_logged = true; + + let requester = Rc::new(requester); + + dioxus::prelude::spawn(async move { + ACCOUNT.write().set_messaging_provider(requester.clone()); + + let _ = requester + .run_forever(&*ACCOUNT.read(), account_events_receiver) + .await; + }); + } + Err(err) => { + error!("Error during login: {err}"); + // TODO: Handle invalid login + // invalid_login.modify(|_| true); + return; + } + } + } else { + warn!("At least one of the following values is/are invalid: homeserver, username or password"); + } + } else { + warn!("already logged... skip login"); + } + } +} fn app() -> Element { - debug!("*** App rendering ***"); - - let login_coro = use_coroutine(|rx| login(rx, &APP_SETTINGS, &SESSION)); - - let mut sync_rooms_coro = None; - - if let Some(requester) = &APP_SETTINGS.read().requester { - sync_rooms_coro = Some(use_coroutine(|rx| { - sync_rooms(rx, requester.borrow().receivers.clone(), &ROOMS) - })); - } + let login_coro = use_coroutine(|rx| login(rx, &SESSION)); if !SESSION.read().is_logged { login_coro.send(false); - } else { - if let Some(coro) = sync_rooms_coro { - coro.send(true); - } - - // if chats_win_state.read().is_none() { - // let chats_window = dioxus_desktop::use_window(cx); - - // let receivers = app_settings - // .read() - // .requester - // .as_ref() - // .unwrap() - // .borrow() - // .receivers - // .clone(); - - // let chats_props = ChatsWindowProps { - // receivers, - // interface: chats_win_interface_ref.clone(), - // }; - - // let chats_dom = VirtualDom::new_with_props(ChatsWindow, chats_props); - - // let window_cfg = Config::default().with_custom_head( - // r#" - // - // "# - // .to_owned(), - // ); - - // let chats_window_desktop_service = chats_window.new_window(chats_dom, window_cfg); - // chats_win_state.set(Some(chats_window_desktop_service)); - // } } if SESSION.read().is_logged { - debug!("Should render the MainWindow component"); rsx! { - MainWindow {}, + Conversations {} } } else { rsx! { - LoginView {}, + Login {}, } } } fn main() { - #[cfg(feature = "desktop")] - { - let fmt_layer = tracing_subscriber::fmt::layer() - .with_filter(tracing::level_filters::LevelFilter::DEBUG); - tracing_subscriber::registry().with(fmt_layer).init(); + let mut builder = LaunchBuilder::new(); - let config = Config::new().with_menu(None); - let builder = LaunchBuilder::new().with_cfg(config); - builder.launch(app); + let mut layers = Vec::new(); + + cfg_if! { + if #[cfg(target_family = "wasm")] { + let console_layer = tracing_subscriber::fmt::layer() + .with_ansi(false) // Only partially supported across browsers + .without_time() // std::time is not available in browsers, see note below + .with_writer(MakeWebConsoleWriter::new()) // write events to the console + .boxed(); + layers.push(console_layer); + } else { + let config = Config::new().with_menu(None); + builder = builder.with_cfg(config); + + let log_file = File::create("/tmp/bg92.log").unwrap(); + let file_layer = tracing_subscriber::fmt::layer() + .with_ansi(false) + .with_writer(log_file) + .with_timer(UtcTime::new(Iso8601::DATE_TIME)) + .boxed(); + layers.push(file_layer); + + let console_layer = ForestLayer::default().boxed(); + layers.push(console_layer); + } } - #[cfg(feature = "web")] - { - let fmt_layer = tracing_subscriber::fmt::layer() - .with_ansi(false) // Only partially supported across browsers - .without_time() // std::time is not available in browsers, see note below - .with_writer(MakeWebConsoleWriter::new()) // write events to the console - .with_filter(tracing::level_filters::LevelFilter::INFO); - tracing_subscriber::registry().with(fmt_layer).init(); // Install these as subscribers to tracing events + tracing_subscriber::registry() + .with(layers) + .with(EnvFilter::from_default_env()) + .init(); - let builder = LaunchBuilder::new(); - builder.launch(app); - } + builder.launch(app); } diff --git a/src/ui/_base.scss b/src/ui/_base.scss index e535f8c..a443cc3 100644 --- a/src/ui/_base.scss +++ b/src/ui/_base.scss @@ -174,6 +174,9 @@ $border-big-width: 4px; $border-big: solid $border-big-width $border-default-color; $border-normal-width: 2px; $border-normal: solid $border-normal-width $border-default-color; +$border-thin-width: 1px; +$border-thin: solid $border-thin-width $border-default-color; + // TODO: Radius should be a percentage(eg: 1024/16px). $border-radius: 16px; @@ -194,9 +197,18 @@ $transition-duration: 300ms; font-weight: bold; } +// Cf. https://css-tricks.com/box-sizing/ +html { + box-sizing: border-box; +} +*, *:before, *:after { + box-sizing: inherit; +} + body { height: 100vh; width: 100vw; + margin: 0px; padding: 0px; outline: 0px; @@ -217,37 +229,3 @@ input { ::selection { background-color: transparent; } - - -// TODO: To remove once the design updated. -.aeroButton { - height: 50%; - min-height: 16px; - aspect-ratio: 1; - background-color: transparent; - border: 2px solid transparent; - background-size: contain !important; - margin-right: 1%; -} -.aeroButton:hover { - border-image: url(/public/images/aerobutton_border.png) 2 round; -} -.aeroButton:active { - border-image: url(/public/images/aerobutton_border_down.png) 2 round; -} - -.button { - height: 50%; - min-height: 16px; - aspect-ratio: 1; - background-color: transparent; - border: 2px solid transparent; - background-size: contain !important; - margin-right: 1%; -} -.button:hover { - border-image: url(/public/images/button_border.png) 2 round; -} -.button:active { - border-image: url(/public/images/button_border_down.png) 2 round; -} diff --git a/src/ui/components/_panel.scss b/src/ui/components/_panel.scss index 4c18199..9dc3efd 100644 --- a/src/ui/components/_panel.scss +++ b/src/ui/components/_panel.scss @@ -1,14 +1,13 @@ @import "../base.scss"; $panel-aspect-ratio: 1/1.618; -$panel-padding-v: 5%; -$panel-padding-h: 5%; -%panel { - padding: $panel-padding-v $panel-padding-h; +@mixin panel($padding-v: 2%, $padding-h: 2%) { + padding: $padding-v $padding-h; + + height: 100%; + width: 100%; - height: calc(100% - $panel-padding-v - (2 * $border-big-width)); - width: calc(100% - $panel-padding-h - (2 * $border-big-width)); flex-shrink: 0; border: $border-big; diff --git a/src/ui/components/avatar_selector.rs b/src/ui/components/avatar_selector.rs deleted file mode 100644 index cca0bdc..0000000 --- a/src/ui/components/avatar_selector.rs +++ /dev/null @@ -1,54 +0,0 @@ -use dioxus::prelude::*; - -turf::style_sheet!("src/ui/components/avatar_selector.scss"); - -pub fn AvatarSelector() -> Element { - rsx! { - style { {STYLE_SHEET} }, - - div { - class: ClassName::AVATAR_SELECTOR, - svg { - view_box: "0 0 100 100", - linearGradient { - id: "avatar-gradient", - x1: 1, - y1: 1, - x2: 0, - y2: 0, - stop { - offset: "0%", - stop_color: "rgb(138, 191, 209)", - } - stop { - offset: "60%", - stop_color: "rgb(236, 246, 249)", - } - }, - filter { - id: "avatar-shadow", - feDropShadow { - dx: 2, - dy: 2, - std_deviation: 3, - flood_opacity: 0.5, - }, - }, - rect { - x: "10", - y: "10", - width: "80", - height: "80", - rx: "12", - fill: "url('#avatar-gradient')", - filter: "url('#avatar-shadow')", - stroke: "grey", - }, - }, - img { - class: ClassName::AVATAR_SELECTOR_PICTURE, - src: "/public/images/default-avatar.png", - }, - }, - } -} diff --git a/src/ui/components/avatar_selector.scss b/src/ui/components/avatar_selector.scss deleted file mode 100644 index 44d843b..0000000 --- a/src/ui/components/avatar_selector.scss +++ /dev/null @@ -1,17 +0,0 @@ -.avatar-selector { - position: relative; - height: 100%; - aspect-ratio: 1; - - &__picture { - $height: 65%; - $margin: calc(100% - $height) / 2; - - position: absolute; - height: $height; - aspect-ratio: 1; - - top: $margin; - right: $margin; - } -} diff --git a/src/ui/components/button.rs b/src/ui/components/button.rs index 1c044de..225b29d 100644 --- a/src/ui/components/button.rs +++ b/src/ui/components/button.rs @@ -77,7 +77,7 @@ pub struct ButtonProps { children: Element, } -fn Button(props: ButtonProps) -> Element { +pub fn Button(props: ButtonProps) -> Element { rsx! { style { {STYLE_SHEET} }, diff --git a/src/ui/components/button.scss b/src/ui/components/button.scss index 4476bf9..e16ecd6 100644 --- a/src/ui/components/button.scss +++ b/src/ui/components/button.scss @@ -5,16 +5,22 @@ aspect-ratio: 3.5; border: $border-normal; - border-radius: $border-radius; + border-radius: 5%; color: get-color(greyscale, 0); font-family: "Geist"; font-weight: bold; + // To center the inner svg + // TODO: Find a more efficient way to center + display: flex; + align-items: center; + justify-content: center; + svg { - height: 100%; - width: 100%; + height: 85%; + width: 85%; text { font-size: 50; diff --git a/src/ui/components/chat_panel.rs b/src/ui/components/chat_panel.rs new file mode 100644 index 0000000..0ba0b6c --- /dev/null +++ b/src/ui/components/chat_panel.rs @@ -0,0 +1,18 @@ +use dioxus::prelude::*; + +turf::style_sheet!("src/ui/components/chat_panel.scss"); + +#[component] +pub fn ChatPanel(name: String) -> Element { + rsx! { + style { {STYLE_SHEET} }, + + div { + class: ClassName::CHAT_PANEL, + + div { + {name} + } + } + } +} diff --git a/src/ui/components/chat_panel.scss b/src/ui/components/chat_panel.scss new file mode 100644 index 0000000..ce05d8a --- /dev/null +++ b/src/ui/components/chat_panel.scss @@ -0,0 +1,6 @@ +@import "../_base.scss"; +@import "./_panel.scss"; + +.chat-panel { + @include panel(); +} diff --git a/src/ui/components/chats_window/chats_window.scss b/src/ui/components/chats_window/chats_window.scss deleted file mode 100644 index 10200ef..0000000 --- a/src/ui/components/chats_window/chats_window.scss +++ /dev/null @@ -1,94 +0,0 @@ -@import "../../_base.scss" - -.chats-window { - height: 100%; - width: 100%; - - $horizontal-padding-margin: calc((2*100%)/1980); - - .tabs { - height: 2%; - width: 100%; - - display: flex; - flex-flow: row; - overflow-x: scroll; - - &::-webkit-scrollbar { - height: 0px; - } - - .tab { - height: 100%; - flex-grow: 1; - padding: 0 $horizontal-padding-margin; - - display: flex; - - button { - height: 100%; - width: 100%; - - display: flex; - flex-direction: row; - align-items: center; - - $clamped-horizontal-padding-margin: clamp(5px, $horizontal-padding-margin, $horizontal-padding-margin); - margin: 0 $clamped-horizontal-padding-margin; - padding: 0 $clamped-horizontal-padding-margin; - - white-space: nowrap; - - background-color: #EFF9F9; - border: $border-style; - - $radius: calc((6*100%)/1980); - $clamped-radius: clamp(6px, $radius, $radius); - border-radius: $clamped-radius $clamped-radius 0 0; - - font-size: $font-size; - - img { - height: $icon-size; - aspect-ratio: 1; - } - } - } - } - - .chat { - height: 98%; - width: 100%; - - background-color: #ECF6F9; - - .header { - height: 7%; - - border: $border-style; - - .info { - height: 45%; - display: flex; - flex-direction: column; - - padding-left: 2%; - - background: linear-gradient(180deg, #BFE3EB, #DEFBFE); - - font-size: $font-size; - - .room-name { - margin: 0; - margin-top: 1%; - font-weight: bold; - } - - .room-topic { - margin: 0; - color: darkgrey; - } - } - } - } -} diff --git a/src/ui/components/chats_window/conversation.rs b/src/ui/components/chats_window/conversation.rs deleted file mode 100644 index e3c5f31..0000000 --- a/src/ui/components/chats_window/conversation.rs +++ /dev/null @@ -1,87 +0,0 @@ -use dioxus::prelude::*; -use log::error; -use matrix_sdk::ruma::OwnedRoomId; - -use super::edit_section::EditSection; -use crate::base::{sync_messages, ROOMS}; -use crate::ui::components::avatar_selector::AvatarSelector; -use crate::ui::components::icons::DownArrowIcon; - -turf::style_sheet!("src/ui/components/chats_window/conversation.scss"); - -#[component] -pub(super) fn Conversation(room_id: OwnedRoomId) -> Element { - error!("Conversation {} rendering", room_id); - - let _sync_message_coro: Coroutine<()> = - use_coroutine(|_: UnboundedReceiver<_>| sync_messages(&ROOMS, room_id)); - - - rsx! { - style { {STYLE_SHEET} }, - - div { - class: ClassName::CONVERSATION, - div { - class: ClassName::ROOM_EVENTS, - ul { - li { - class: ClassName::ROOM_EVENT, - div { - p { - class: ClassName::TITLE, - "MON POTE says:" - }, - p { - class: ClassName::CONTENT, - "Coucou mon pote", - }, - }, - }, - }, - }, - div { - class: ClassName::OTHER_AVATAR_SELECTOR_CONTAINER, - div { - class: ClassName::AVATAR_SELECTOR, - AvatarSelector {}, - }, - div { - class: ClassName::WEBCAM, - img { - src: "images/webcam.svg" - }, - }, - div { - class: ClassName::ARROW_ICON, - DownArrowIcon {} - }, - }, - div { - class: ClassName::HOLDER, - "••••••" - }, - div { - class: ClassName::EDIT_SECTION, - EditSection {}, - }, - div { - class: ClassName::MY_AVATAR_SELECTOR_CONTAINER, - div { - class: ClassName::AVATAR_SELECTOR, - AvatarSelector {}, - }, - div { - class: ClassName::WEBCAM, - img { - src: "images/webcam.svg" - }, - }, - div { - class: ClassName::ARROW_ICON, - DownArrowIcon {} - }, - }, - }, - } -} diff --git a/src/ui/components/chats_window/conversation.scss b/src/ui/components/chats_window/conversation.scss deleted file mode 100644 index ca1ae3a..0000000 --- a/src/ui/components/chats_window/conversation.scss +++ /dev/null @@ -1,113 +0,0 @@ -@import "../../_base.scss" - -.conversation { - $padding-top: 2%; - - height: calc(93% - $padding-top); - - padding-left: 2%; - padding-top: $padding-top; - - display: grid; - grid-template-columns: 75% 25%; - grid-template-rows: 70% 1% 29%; - cursor: pointer; - - .holder { - display: flex; - justify-content: center; - align-items: center; - - grid-column: 1; - grid-row: 2; - - color: darkgrey; - } - - .room-events { - display: flex; - flex-flow: column; - justify-content: flex-start; - - border: $border-style; - background-color: #FFFFFF; - - ul { - margin: 0; - padding-left: 0; - } - - li { - list-style-type: none; - } - - .room-event { - display: flex; - flex-flow: column; - justify-content: space-between; - - padding-top: 1%; - - font-size: $font-size; - - .title { - margin: 0; - } - - .content { - margin: 0; - padding-left: 2%; - } - } - } - - %selector-container { - aspect-ratio: 1; - - grid-column: 2; - - display: grid; - grid-template-columns: 10% 15% 50% 15% 10%; - grid-template-rows: 80% 20%; - - .avatar-selector { - grid-column-start: 1; - grid-column-end: 6; - aspect-ratio: 1; - } - - .webcam { - grid-column: 2; - grid-row: 2; - - aspect-ratio: 1; - } - - .arrow-icon { - grid-column: 4; - grid-row: 2; - - svg { - path:last-child { - fill: black; - } - } - } - } - - .other-avatar-selector-container { - @extend %selector-container; - - grid-row: 1; - } - - .my-avatar-selector-container { - @extend %selector-container; - - grid-row: 3; - } - - .edit-section { - grid-row: 3; - } -} diff --git a/src/ui/components/chats_window/edit_section.rs b/src/ui/components/chats_window/edit_section.rs deleted file mode 100644 index 3400783..0000000 --- a/src/ui/components/chats_window/edit_section.rs +++ /dev/null @@ -1,52 +0,0 @@ -use dioxus::prelude::*; - -turf::style_sheet!("src/ui/components/chats_window/edit_section.scss"); - -pub fn EditSection() -> Element { - rsx! { - style { {STYLE_SHEET} }, - - div { - class: ClassName::INPUT_AREA, - - div { - class: ClassName::BUTTONS, - - button { - "šŸ˜€" - }, - - button { - "šŸ˜‰" - }, - - button { - "😓" - }, - - button { - "šŸ”Š" - }, - }, - - textarea { - class: ClassName::EDIT, - placeholder: "Type your message here...", - }, - - div { - class: ClassName::CMD_BUTTONS, - - button { - class: ClassName::SEND_BUTTON, - - "Send" - }, - - button { - "šŸ”Ž" - }, - }, - }, - } -} diff --git a/src/ui/components/chats_window/edit_section.scss b/src/ui/components/chats_window/edit_section.scss deleted file mode 100644 index 97c94b6..0000000 --- a/src/ui/components/chats_window/edit_section.scss +++ /dev/null @@ -1,55 +0,0 @@ -@import "../../_base.scss" - -.input-area { - height: 100%; - width: 100%; - - margin-bottom: 2%; - - .buttons { - $padding-top-bottom: 0.5%; - - height: calc(10% - ($padding-top-bottom * 2)); - padding-left: 2%; - padding-top: $padding-top-bottom; - padding-bottom: $padding-top-bottom; - - display: flex; - flex-direction: row; - align-items: center; - - border: $border-style; - background: linear-gradient(180deg, #F5FDFF, #E3ECF0, #F5FDFF); - - button { - @extend .aeroButton; - height: $icon-size; - - padding: 0; - margin: 0; - margin-right: 2%; - - font-size: larger; - } - } - - .edit { - height: 80%; - // Remove border from width - width: calc(100% - 2px); - - padding: 0; - margin: 0; - } - - .cmd-buttons { - height: 7%; - display: flex; - flex-direction: row; - justify-content: flex-end; - } - - .send-button { - width: 15%; - } -} diff --git a/src/ui/components/chats_window/interface.rs b/src/ui/components/chats_window/interface.rs deleted file mode 100644 index 8595910..0000000 --- a/src/ui/components/chats_window/interface.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::cell::RefCell; - -use matrix_sdk::ruma::OwnedRoomId; -use tokio::sync::broadcast::error::SendError; -use tokio::sync::broadcast::{channel, Receiver, Sender}; - -#[derive(Clone)] -pub enum Tasks { - ToggleRoom(OwnedRoomId), -} - -pub struct Interface { - sender: Sender, - receiver: RefCell>, -} - -impl Interface { - pub fn new() -> Self { - let (sender, receiver) = channel::(32); - Self { - sender, - receiver: RefCell::new(receiver), - } - } - - pub(super) fn receiver(&self) -> &RefCell> { - &self.receiver - } - - pub fn toggle_room(&self, room_id: OwnedRoomId) -> Result> { - self.sender.send(Tasks::ToggleRoom(room_id)) - } -} - -impl Default for Interface { - fn default() -> Self { - Self::new() - } -} diff --git a/src/ui/components/chats_window/mod.rs b/src/ui/components/chats_window/mod.rs deleted file mode 100644 index 3549f18..0000000 --- a/src/ui/components/chats_window/mod.rs +++ /dev/null @@ -1,171 +0,0 @@ -mod conversation; -mod edit_section; -mod navbar; - -pub(crate) mod interface; - -use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; - -use dioxus::prelude::*; -use log::{debug, error}; -use matrix_sdk::ruma::OwnedRoomId; -use tokio::sync::broadcast::Receiver; - -use crate::base::{sync_rooms, ROOMS}; -use crate::domain::model::room::Room; -use crate::infrastructure::messaging::matrix::requester::Receivers; -use conversation::Conversation; -use navbar::Navbar; - -use interface::{Interface, Tasks}; - -turf::style_sheet!("src/ui/components/chats_window/chats_window.scss"); - -#[derive(Props, Clone, PartialEq)] -pub struct ChatsWindowProps { - pub receivers: Receivers, - pub interface: Signal, -} - -fn render_rooms_tabs( - by_id_rooms: &GlobalSignal>>, - displayed_room_ids: Signal>, -) -> Vec { - let rooms_ref = by_id_rooms.read(); - let displayed_room_ids = displayed_room_ids.read(); - rooms_ref - .values() - .filter(|room| displayed_room_ids.contains(room.borrow().id())) - .map(|room| { - let room = room.borrow(); - let room_name = match room.name() { - Some(room_name) => room_name.clone(), - None => room.id().to_string(), - }; - rsx!( - div { - class: ClassName::TAB, - button { - img { - src: "/public/images/status_online.png", - }, - "{room_name}", - }, - }, - ) - }) - .collect() -} - -fn render_rooms_conversations( - by_id_rooms: &GlobalSignal>>, - displayed_room_ids: Signal>, -) -> Vec { - let rooms_ref = by_id_rooms.read(); - let displayed_room_ids = displayed_room_ids.read(); - rooms_ref - .values() - .filter(|room| displayed_room_ids.contains(room.borrow().id())) - .map(|room| { - let room = room.borrow(); - let room_id = room.id(); - rsx!(Conversation { - room_id: room_id.clone() - },) - }) - .collect() -} - -async fn handle_controls( - receiver_ref: &RefCell>, - mut displayed_room_ids: Signal>, -) { - loop { - let result = receiver_ref.borrow_mut().recv().await; - match result { - Ok(task) => match task { - Tasks::ToggleRoom(room_id) => { - error!("ON TOGGLE ROOM {}", room_id); - let mut displayed_room_ids = displayed_room_ids.write(); - match displayed_room_ids.take(&room_id) { - Some(_) => { - error!("{} room already dispayed... close it", room_id); - } - None => { - error!("{} room isn't dispayed... open it", room_id); - displayed_room_ids.insert(room_id); - } - } - } - }, - Err(err) => error!("{}", err), - } - } -} - -pub fn ChatsWindow(props: ChatsWindowProps) -> Element { - debug!("ChatsWindow rendering"); - - let receivers = &props.receivers; - let interface_ref = &props.interface; - - let displayed_room_ids = use_signal(HashSet::::new); - - let sync_rooms_coro = use_coroutine(|rx| { - to_owned![receivers]; - sync_rooms(rx, receivers, &ROOMS) - }); - sync_rooms_coro.send(true); - - let _: Coroutine<()> = use_coroutine(|_: UnboundedReceiver<_>| { - to_owned![interface_ref, displayed_room_ids]; - async move { - let interface = interface_ref.read(); - let receiver = &interface.receiver(); - handle_controls(receiver, displayed_room_ids).await - } - }); - - let rendered_rooms_tabs = render_rooms_tabs(&ROOMS, displayed_room_ids); - let rendered_rooms_conversations = render_rooms_conversations(&ROOMS, displayed_room_ids); - - rsx! { - style { {STYLE_SHEET} }, - - div { - class: ClassName::CHATS_WINDOW, - - div { - class: ClassName::TABS, - {rendered_rooms_tabs.into_iter()}, - }, - - div { - class: ClassName::CHAT, - - div { - class: ClassName::HEADER, - - div { - class: ClassName::INFO, - - p { - class: ClassName::ROOM_NAME, - "MON POTE", - }, - - p { - class: ClassName::ROOM_TOPIC, - "LE STATUT A MON POTE", - }, - }, - - Navbar {}, - }, - - {rendered_rooms_conversations.into_iter()}, - }, - }, - } -} diff --git a/src/ui/components/chats_window/navbar.rs b/src/ui/components/chats_window/navbar.rs deleted file mode 100644 index ee463ab..0000000 --- a/src/ui/components/chats_window/navbar.rs +++ /dev/null @@ -1,50 +0,0 @@ -use dioxus::prelude::*; -use log::debug; - -turf::style_sheet!("src/ui/components/chats_window/navbar.scss"); - -pub fn Navbar() -> Element { - debug!("Navbar rendering"); - - rsx! { - style { {STYLE_SHEET} }, - - div { - class: ClassName::NAVBAR, - - button { - style: "background: url(/public/images/add_user2.png) center no-repeat", - }, - - button { - style: "background: url(/public/images/directory.png) center no-repeat", - }, - - button { - style: "background: url(/public/images/phone.png) center no-repeat", - }, - - button { - style: "background: url(/public/images/medias.png) center no-repeat", - }, - - button { - style: "background: url(/public/images/games.png) center no-repeat", - }, - - button { - style: "background: url(/public/images/ban_user.png) center no-repeat", - }, - - button { - class: ClassName::FLEX_RIGHT_AERO_BUTTON, - style: "background: url(/public/images/brush.png) center no-repeat", - }, - - button { - class: ClassName::FLEX_LAST_BUTTON, - style: "background: url(/public/images/settings.png) center no-repeat", - }, - }, - } -} diff --git a/src/ui/components/chats_window/navbar.scss b/src/ui/components/chats_window/navbar.scss deleted file mode 100644 index 64b5596..0000000 --- a/src/ui/components/chats_window/navbar.scss +++ /dev/null @@ -1,26 +0,0 @@ -@import "../../_base.scss" - -.navbar { - height: 55%; - padding-left: 2%; - padding-right: 2%; - - background: linear-gradient(180deg, #A9D3E0, #F0F9FA); - - display: flex; - flex-direction: row; - align-items: center; - - button { - @extend .aeroButton; - padding-right: 2%; - } - - .flex-right-aero-button { - margin-left: auto; - } - - .flex-last-button { - margin: 0; - } -} diff --git a/src/ui/components/contacts_window/contacts.rs b/src/ui/components/contacts_window/contacts.rs deleted file mode 100644 index af1efca..0000000 --- a/src/ui/components/contacts_window/contacts.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::rc::Rc; - -use dioxus::prelude::*; -use log::debug; - -use crate::ui::components::contacts_window::contacts_section::{ - filter_people_conversations, filter_room_conversations, ContactsSection, -}; - -turf::style_sheet!("src/ui/components/contacts_window/contacts.scss"); - -pub fn Contacts() -> Element { - debug!("Contacts rendering"); - - // TODO: Test overflow - // TODO: Add offline users ? - rsx! { - style { {STYLE_SHEET} }, - - div { - class: ClassName::CONTACTS, - ContactsSection {name: "Groups", filter: Rc::new(filter_room_conversations)}, - ContactsSection {name: "Available", filter: Rc::new(filter_people_conversations)}, - }, - } -} diff --git a/src/ui/components/contacts_window/contacts.scss b/src/ui/components/contacts_window/contacts.scss deleted file mode 100644 index 25af475..0000000 --- a/src/ui/components/contacts_window/contacts.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import "../../_base.scss" - -.contacts { - height: 72%; - background-color: white; -} diff --git a/src/ui/components/contacts_window/contacts_section.rs b/src/ui/components/contacts_window/contacts_section.rs deleted file mode 100644 index 83fc4e3..0000000 --- a/src/ui/components/contacts_window/contacts_section.rs +++ /dev/null @@ -1,149 +0,0 @@ -use std::cell::RefCell; -use std::rc::Rc; - -use dioxus::prelude::*; -use dioxus_free_icons::icons::io_icons::IoChevronDown; -use dioxus_free_icons::Icon; -use log::debug; - -use crate::base::{CHATS_WIN_INTERFACE, ROOMS}; -use crate::domain::model::room::{ByIdRooms, Room, RoomId}; -use crate::ui::components::chats_window::interface::Interface as ChatsWindowInterface; - -turf::style_sheet!("src/ui/components/contacts_window/contacts_section.scss"); - -fn ContactsArrow() -> Element { - rsx! { - style { {STYLE_SHEET} }, - Icon { - icon: IoChevronDown, - }, - } -} - -static NO_NAME_REPR: &str = "No name"; -static NO_SUBJECT_REPR: &str = "No subject"; - -pub(super) fn filter_people_conversations( - by_id_rooms: &GlobalSignal, -) -> Vec> { - let by_id_rooms = by_id_rooms.read(); - - let mut filtered_rooms = Vec::>::with_capacity(by_id_rooms.len()); - - for room in by_id_rooms.values() { - let is_direct = room.borrow().is_direct().unwrap(); - if !is_direct { - filtered_rooms.push(room.to_owned()); - } - } - filtered_rooms -} - -pub(super) fn filter_room_conversations( - by_id_rooms: &GlobalSignal, -) -> Vec> { - let by_id_rooms = by_id_rooms.read(); - - let mut filtered_rooms = Vec::>::with_capacity(by_id_rooms.len()); - - for room in by_id_rooms.values() { - let is_direct = room.borrow().is_direct().unwrap(); - if is_direct { - filtered_rooms.push(room.to_owned()); - } - } - filtered_rooms -} - -// TODO: Handle errors -fn on_clicked_room(room_id: &RoomId, chats_window_interface: &GlobalSignal) { - let _ = chats_window_interface.read().toggle_room(room_id.clone()); -} - -#[derive(Props, Clone)] -pub struct ContactsSectionProps { - name: String, - filter: Rc) -> Vec>>, -} -impl PartialEq for ContactsSectionProps { - fn eq(&self, other: &Self) -> bool { - self.name == other.name && Rc::ptr_eq(&self.filter, &other.filter) - } -} - -pub fn ContactsSection(props: ContactsSectionProps) -> Element { - debug!("ContactsSection rendering"); - - let contacts = props.filter.to_owned()(&ROOMS); - let contacts_len = contacts.len(); - - let mut show = use_signal(|| false); - - let classes = [ - ClassName::SECTION, - if *show.read() { ClassName::ACTIVE } else { "" }, - ] - .join(" "); - - let rendered_contacts = contacts.into_iter().map(|room| { - let room = room.borrow(); - - let topic = room.topic().clone().unwrap_or("".to_string()); - let name = match room.name() { - Some(name) => name.clone(), - None => NO_NAME_REPR.to_string(), - }; - let id = room.id().clone(); - let is_invited = room.is_invited().unwrap_or(false); - - let formatted = format!( - "{name} - {}", - if is_invited { - "Invited - ".to_string() - } else { - "".to_string() - } - ); - - rsx! { - li { - onclick: move |_| on_clicked_room(&id, &CHATS_WIN_INTERFACE), - img { - src: "/public/images/status_online.png", - }, - p { - {formatted}, - }, - p { - style: "color: darkgrey;", - {topic}, - }, - } - } - }); - - rsx! { - style { {STYLE_SHEET} }, - - div { - class: "{classes}", - - p { - class: ClassName::HEADER, - onclick: move |_| { - let state = *show.read(); - show.set(!state) - }, - - ContactsArrow {}, - - {format!("{} ({contacts_len})", props.name)}, - }, - - ul { - {rendered_contacts.into_iter()}, - }, - }, - } -} diff --git a/src/ui/components/contacts_window/contacts_section.scss b/src/ui/components/contacts_window/contacts_section.scss deleted file mode 100644 index 24b7531..0000000 --- a/src/ui/components/contacts_window/contacts_section.scss +++ /dev/null @@ -1,67 +0,0 @@ -@import "../../_base.scss" - -.section { - width: 100%; - font-size: $font-size; - - &.active { - ul { - height: 0; - opacity: 0; - } - - svg { - transform: rotate(180deg); - } - } - - .header { - height: 2%; - width: 98%; - display: flex; - flex-direction: row; - align-items: center; - cursor: pointer; - margin: 0; - margin-left: 1%; - padding-top: 1%; - font-weight: bold; - } - - ul { - height: 100%; - margin: 0; - overflow: hidden; - opacity: 1; - transition: 0.4s ease; - } - - li { - list-style-type: none; - cursor: pointer; - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; - - img { - height: $icon-size; - aspect-ratio: 1; - } - - p { - margin: 0; - } - } - - svg { - transition: 0.4s ease; - } - - .contact { - list-style-type: none; - margin: 0 auto; - text-align: left; - cursor: pointer - } -} diff --git a/src/ui/components/contacts_window/contacts_window.scss b/src/ui/components/contacts_window/contacts_window.scss deleted file mode 100644 index 26a8865..0000000 --- a/src/ui/components/contacts_window/contacts_window.scss +++ /dev/null @@ -1,84 +0,0 @@ -@import "../../_base.scss"; - -.contactsWindow { - width: 100%; - height: 100%; - - background-color: #ECF6F9; - font-family: "Tahoma", sans-serif; - - border: thin solid #707070; - border-radius: 8px; - box-shadow: 0 0 5px #00000050; - - - .header { - height: 10%; - width: 100%; - - .titleBar { - height: 60%; - width: 100%; - background: - linear-gradient(180deg, #7DC5E3, #3883A3); - } - - .userInfo { - height: 40%; - width: 100%; - background: - linear-gradient(180deg, #00658B, #0077A6); - } - } - - .contactsNav { - height: calc(31/1080*100%); - background: - linear-gradient(180deg, #00658B, #0077A6); - - .inner { - margin-left: 1%; - margin-right: 1%; - height: 100%; - display: flex; - align-items: center; - - .flexRightAeroButton { - @extend .aeroButton; - - margin-left: auto; - } - } - } - - .search { - height: calc(38/1080*100%); - width: 100%; - - border-bottom: thin solid #e2eaf3; - - .inner { - height: 100%; - width: 98%; - padding-left: 1%; - display: flex; - flex-direction: row; - align-items: center; - - .searchInput { - height: calc(23/38*100%); - width: 100%; - margin-right: 1%; - border: thin solid #c7c7c7; - box-shadow: inset 0 0 calc(3/1080*100%) #0000002a; - font-size: 8pt; - - padding-left: 1%; - } - } - } - - .footer { - height: 10%; - } -} diff --git a/src/ui/components/contacts_window/mod.rs b/src/ui/components/contacts_window/mod.rs deleted file mode 100644 index a5abe64..0000000 --- a/src/ui/components/contacts_window/mod.rs +++ /dev/null @@ -1,98 +0,0 @@ -mod contacts; -mod contacts_section; -mod user_infos; - -use dioxus::prelude::*; -use log::debug; - -use crate::ui::components::contacts_window::contacts::Contacts; -use crate::ui::components::contacts_window::user_infos::UserInfos; - -turf::style_sheet!("src/ui/components/contacts_window/contacts_window.scss"); - -pub fn ContactsWindow() -> Element { - debug!("ContactsWindow rendering"); - - rsx! { - style { {STYLE_SHEET} }, - - div { - class: ClassName::CONTACTS_WINDOW, - - div { - class: ClassName::HEADER, - - div { - class: ClassName::TITLE_BAR, - }, - - div { - class: ClassName::USER_INFO, - }, - - UserInfos {}, - }, - - div { - class: ClassName::CONTACTS_NAV, - div { - class: ClassName::INNER, - - button { - class: ClassName::AERO_BUTTON, - style: "background: url(/public/images/letter.png) center no-repeat", - }, - button { - class: ClassName::AERO_BUTTON, - style: "background: url(/public/images/directory.png) no-repeat center", - }, - button { - class: ClassName::AERO_BUTTON, - style: "background: url(/public/images/news.png) no-repeat center", - }, - - button { - class: ClassName::FLEX_RIGHT_AERO_BUTTON, - style: "background: url(/public/images/brush.png) no-repeat center", - }, - button { - class: ClassName::AERO_BUTTON, - style: "background: url(/public/images/settings.png) no-repeat center", - }, - - }, - - }, - - div { - class: ClassName::SEARCH, - - div { - class: ClassName::INNER, - - input { - class: ClassName::SEARCH_INPUT, - placeholder: "Find a contact...", - r#type: "text", - }, - - button { - class: ClassName::BUTTON, - style: "background: url(/public/images/add_user.png) no-repeat center", - }, - - button { - class: ClassName::BUTTON, - style: "background: url(/public/images/tbc_transfert.png) no-repeat center", - }, - }, - }, - - Contacts {}, - - div { - class: ClassName::FOOTER, - }, - }, - } -} diff --git a/src/ui/components/contacts_window/user_infos.rs b/src/ui/components/contacts_window/user_infos.rs deleted file mode 100644 index 6698c63..0000000 --- a/src/ui/components/contacts_window/user_infos.rs +++ /dev/null @@ -1,76 +0,0 @@ -use dioxus::prelude::*; -use log::debug; - -use crate::ui::components::avatar_selector::AvatarSelector; -use crate::ui::components::icons::DownArrowIcon; - -turf::style_sheet!("src/ui/components/contacts_window/user_infos.scss"); - -static MESSAGE_PLACEHOLDER: &str = ""; - -pub fn UserInfos() -> Element { - debug!("UserInfos rendering"); - - // let app_settings = use_atom_ref(cx, &APP_SETTINGS); - // let store = &app_settings.read().store; - - // println!("----------------------------------"); - // println!("UserInfos rendering"); - // // println!("store={:?}", &store); - // dbg!(&store.user_id); - // println!("----------------------------------"); - - // let user_id = store.user_id..as_ref().unwrap(); - - // let mut user_info_option = None; - let user_display_name_option: Option = None; - let user_display_name = "AIE"; - - // let user_id_option = &store.user_id; - // if user_id_option.is_some() { - // let user_id = user_id_option.as_ref().unwrap(); - // let user_info_option = store.user_infos.get(user_id); - // if user_info_option.is_some() { - // user_display_name_option = user_info_option.unwrap().display_name.as_ref(); - // } - // } - - rsx! { - style { {STYLE_SHEET} }, - - div { - class: ClassName::USER_INFO, - - div { - class: ClassName::AVATAR_SELECTOR, - AvatarSelector {}, - }, - - div { - class: ClassName::INFO_CONTAINER, - - div { - class: ClassName::USER_ID, - p { - class: ClassName::USER_NAME, - if user_display_name_option.is_some() { "{user_display_name}" } else { "AIE" }, - }, - p { - class: ClassName::USER_STATUS, - "(Busy)", - }, - DownArrowIcon {}, - }, - - div { - class: ClassName::USER_MESSAGE, - p { - // TODO: Handle user message - {MESSAGE_PLACEHOLDER}, - } - DownArrowIcon {}, - }, - }, - }, - } -} diff --git a/src/ui/components/contacts_window/user_infos.scss b/src/ui/components/contacts_window/user_infos.scss deleted file mode 100644 index 86f7efa..0000000 --- a/src/ui/components/contacts_window/user_infos.scss +++ /dev/null @@ -1,63 +0,0 @@ -@import "../../_base.scss" - -.userInfo { - position: relative; - height: 75%; - width: 99%; - top: -75%; - left: 1%; - aspect-ratio: 1; - z-index: 1; - display: flex; - flex-direction: row; - align-items: center; - - .avatarSelector { - height: 100%; - aspect-ratio: 1; - } - - .infoContainer { - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: center; - height: 100%; - width: 100%; - - .userId { - @extend .aeroButton; - - height: 30%; - width: fit-content; - display: flex; - text-align: begin; - align-items: center; - - .userName { - display: inline-block; - width: fit-content; - color: white; - margin: 0; - } - - .userStatus { - display: inline-block; - width: fit-content; - color: #B9DDE7; - } - } - - .userMessage { - @extend .aeroButton; - - width: fit-content; - height: 30%; - display: flex; - text-align: begin; - align-items: center; - margin: 0; - color: white; - } - } -} diff --git a/src/ui/components/conversations.rs b/src/ui/components/conversations.rs new file mode 100644 index 0000000..597c2fe --- /dev/null +++ b/src/ui/components/conversations.rs @@ -0,0 +1,504 @@ +use base64::{engine::general_purpose, Engine as _}; +use dioxus::prelude::*; +use tracing::{debug, trace, warn}; + +use super::{button::Button, icons::SearchIcon, text_input::TextInput}; +use crate::{ + domain::model::{common::PresenceState as DomainPresenceState, room::RoomId, space::SpaceId}, + ui::{ + components::icons::{ChatsIcon, LogoIcon, RoomsIcon, SpacesIcon}, + ACCOUNT, STORE, + }, +}; + +turf::style_sheet!("src/ui/components/conversations.scss"); + +#[component] +fn AccountAvatar(content: Option>, class_name: Option) -> Element { + match content { + Some(content) => { + let encoded = general_purpose::STANDARD.encode(content); + rsx! { + div { + class: class_name, + background_image: format!("url(data:image/jpeg;base64,{encoded})") + } + } + } + // TODO: Manage acount without avatar + None => None, + } +} + +#[component] +fn PresenceState(state: Option, class_name: Option) -> Element { + let class_name = class_name.unwrap_or("".to_string()); + + match state { + Some(state) => { + let state_class = match state { + DomainPresenceState::Online => ClassName::ONLINE, + DomainPresenceState::Offline => ClassName::OFFLINE, + DomainPresenceState::Unavailable => ClassName::UNAVAILABLE, + _ => ClassName::UNAVAILABLE, + }; + + let classes = [class_name.as_str(), state_class].join(" "); + + rsx! { + div { + class: classes, + LogoIcon {}, + } + } + } + None => None, + } +} + +#[component] +fn DisplayName(display_name: Option, class_name: Option) -> Element { + match display_name { + Some(display_name) => { + rsx! { + div { + class: class_name, + p { + {display_name}, + } + } + } + } + None => None, + } +} + +#[component] +fn Status(status: Option, class_name: Option) -> Element { + let status = status.unwrap_or("Type your status".to_string()); + + rsx! { + div { + class: class_name, + TextInput { + placeholder: status, + } + } + } +} + +pub fn Account() -> Element { + let avatar = use_resource(move || async move { + let account = ACCOUNT.read(); + let avatar = account.get_avatar().await; + rsx! { + AccountAvatar { + class_name: ClassName::ACCOUNT_AVATAR, + content: avatar.borrow().clone(), + } + } + }); + + let presence_state = use_resource(move || async move { + // TODO: Fetch the state from the domain + rsx! { + PresenceState { + state: DomainPresenceState::Online, + class_name: ClassName::ACCOUNT_PRESENCE_STATE, + } + } + }); + + let display_name = use_resource(move || async move { + let account = ACCOUNT.read(); + let display_name = account.get_display_name().await; + rsx! { + DisplayName { + class_name: ClassName::ACCOUNT_DISPLAY_NAME, + display_name: display_name.borrow().clone(), + } + } + }); + + let status = use_resource(move || async move { + // TODO: Fetch the status from the domain + rsx! { + Status { + class_name: ClassName::ACCOUNT_STATUS, + status: "Coucou, Je suis BG92".to_string(), + } + } + }); + + rsx! { + style { {STYLE_SHEET} }, + + div { + class: ClassName::ACCOUNT, + + {avatar}, + {presence_state}, + {display_name}, + + {status}, + + div { + class: ClassName::ACCOUNT_SPACES, + Button { + SpacesIcon {}, + } + }, + + div { + class: ClassName::ACCOUNT_CHAT, + Button { + ChatsIcon {}, + } + }, + + div { + class: ClassName::ACCOUNT_ROOM, + Button { + RoomsIcon {}, + } + }, + } + } +} + +#[component] +pub fn ConversationAvatar(room_id: RoomId, on_clicked: EventHandler) -> Element { + let rooms = STORE.read().rooms(); + let toto = rooms.get(&room_id).unwrap(); + + let room = toto.signal(); + + let room_id = room.id(); + let room_name = room.name(); + + let selected_room_id = use_context::>>(); + + let invited_badge = if room.is_invited() { + rsx! { + div { + class: ClassName::CONVERSATION_AVATAR_INVITED_BADGE, + + p { + "Invited", + } + } + } + } else { + None + }; + + let is_selected = match selected_room_id.read().as_ref() { + Some(selected_room_id) => *selected_room_id == room_id, + None => false, + }; + + // let avatar = if let Some(Some(content)) = &*avatar.read() { + let avatar = if let Some(content) = room.avatar() { + let encoded = general_purpose::STANDARD.encode(content); + rsx! { + div { + class: ClassName::CONVERSATION_AVATAR_IMAGE, + background_image: format!("url(data:image/jpeg;base64,{encoded})"), + } + } + } else { + let placeholder = room_name + .unwrap_or("?".to_string()) + .to_uppercase() + .chars() + .next() + .unwrap_or('?') + .to_string(); + + debug!("Use of {} placeholder for {}", placeholder, room_id); + + rsx! { + div { + class: ClassName::CONVERSATION_AVATAR_IMAGE, + + {placeholder}, + } + } + }; + + let classes = [ + ClassName::CONVERSATION_AVATAR, + if is_selected { ClassName::SELECTED } else { "" }, + ]; + let classes_str = classes.join(" "); + + rsx! { + div { + class: "{classes_str}", + + onclick: move |evt| { + on_clicked.call(room_id.clone()); + evt.stop_propagation(); + }, + + {avatar} + {invited_badge} + } + } +} + +#[component] +pub fn ConversationsCarousel(on_selected_conversation: EventHandler) -> Element { + let mut ordered_rooms = use_signal(Vec::::new); + + use_effect(move || { + let rooms = use_context::>>(); + let rooms = rooms.read(); + for room in rooms.iter() { + if !ordered_rooms.peek().contains(room) { + ordered_rooms.push(room.clone()); + } + } + ordered_rooms.retain(|room| rooms.contains(room)); + }); + + let ordered_rooms = ordered_rooms.read(); + let rendered_avatars = ordered_rooms.iter().map(|room| { + rsx! { + ConversationAvatar { + room_id: room.clone(), + on_clicked: on_selected_conversation, + } + } + }); + + rsx! { + div { + class: ClassName::SPACE_CONVERSATIONS_CAROUSEL, + // TODO: Needed? + onscroll: move |_| { + // Catch scrolling events. + }, + {rendered_avatars}, + } + } +} + +#[component] +pub fn Space(id: SpaceId) -> Element { + let space = STORE.read().spaces().get(&id).unwrap().signal(); + + let name = space.name(); + + let mut selected_room_id = use_context_provider(|| Signal::new(None::)); + let mut displayed_rooms = use_context_provider(|| Signal::new(Vec::::new())); + + use_effect(move || { + // let rooms = STORE.read().rooms(); + let rooms = STORE.peek().rooms(); + let room_ids = space.room_ids(); + for room_id in room_ids { + if rooms.contains_key(&room_id) { + displayed_rooms.write().push(room_id); + } + } + }); + + let on_selected_conversation = move |room_id: RoomId| { + trace!(""); + selected_room_id.set(Some(room_id)); + }; + + let mut space_classes: [&str; 2] = [ClassName::SPACE, ""]; + + let mut selected_room_name = "".to_string(); + + if let Some(room_id) = selected_room_id.read().as_ref() { + space_classes[1] = ClassName::DISPLAY_CONVERSATION_NAME; + + if let Some(room) = STORE.read().rooms().get(room_id) { + let room = room.signal(); + + if let Some(name) = room.name() { + selected_room_name = name; + } else { + debug!("No name set for {} room", &room_id); + selected_room_name = room_id.to_string(); + } + } else { + warn!("No room found for the {} id", &room_id); + } + } + + let classes_str = space_classes.join(" "); + + rsx! { + style { {STYLE_SHEET} }, + + div { + class: "{classes_str}", + + // Deselect the conversation on clicks outside of the ConversationAvatar + onclick: move |_| { + selected_room_id.set(None); + }, + + div { + class: ClassName::SPACE_NAME, + p { + {name} + } + }, + ConversationsCarousel { + on_selected_conversation, + }, + div { + class: ClassName::SPACE_CONVERSATION_NAME, + p { + {selected_room_name}, + } + } + } + } +} + +#[component] +pub fn HomeSpace() -> Element { + let name = "Home"; + + let mut selected_room_id = use_context_provider(|| Signal::new(None::)); + let mut displayed_rooms = use_context_provider(|| Signal::new(Vec::::new())); + + use_effect(move || { + let rooms = STORE.read().rooms(); + for room in rooms.values() { + if room.signal().spaces().is_empty() { + let room_id = room.signal().id(); + displayed_rooms.write().push(room_id); + } + } + }); + + let on_selected_conversation = move |room_id: RoomId| { + selected_room_id.set(Some(room_id)); + }; + + let mut space_classes: [&str; 2] = [ClassName::SPACE, ""]; + + let mut selected_room_name = "".to_string(); + + if let Some(room_id) = selected_room_id.read().as_ref() { + space_classes[1] = ClassName::DISPLAY_CONVERSATION_NAME; + + if let Some(room) = STORE.read().rooms().get(room_id) { + let room = room.signal(); + + if let Some(name) = room.name() { + selected_room_name = name; + } else { + debug!("No name set for {} room", &room_id); + selected_room_name = room_id.to_string(); + } + } else { + warn!("No room found for the {} id", &room_id); + } + } + + let classes_str = space_classes.join(" "); + + rsx! { + style { {STYLE_SHEET} }, + + div { + class: "{classes_str}", + + // Deselect the conversation on clicks outside of the ConversationAvatar + onclick: move |_| { + selected_room_id.set(None); + }, + + div { + class: ClassName::SPACE_NAME, + p { + {name} + } + }, + ConversationsCarousel { + on_selected_conversation, + }, + div { + class: ClassName::SPACE_CONVERSATION_NAME, + p { + {selected_room_name}, + } + } + } + } +} + +pub fn Spaces() -> Element { + let spaces = STORE.read().spaces(); + let space_ids = spaces.keys().clone().last(); + + let rendered_spaces = space_ids.map(|id| { + rsx! { + Space { id: id.clone() } + } + }); + + rsx! { + style { {STYLE_SHEET} }, + + div { + class: ClassName::SPACES, + + {rendered_spaces}, + + HomeSpace {}, + } + } +} + +pub fn Search() -> Element { + rsx! { + style { {STYLE_SHEET} }, + + div { + class: ClassName::SEARCH, + + TextInput {} + + div { + class: ClassName::SEARCH_BUTTON, + Button { + SearchIcon {} + } + } + } + } +} + +pub fn Conversations() -> Element { + rsx! { + style { {STYLE_SHEET} }, + + div { + class: ClassName::CONVERSATIONS, + + div { + class: ClassName::CONVERSATIONS_ACCOUNT, + Account {}, + }, + + div { + class: ClassName::CONVERSATIONS_SPACES, + Spaces {}, + }, + + div { + class: ClassName::CONVERSATIONS_SEARCH, + Search {}, + }, + } + } +} diff --git a/src/ui/components/conversations.scss b/src/ui/components/conversations.scss new file mode 100644 index 0000000..c565a92 --- /dev/null +++ b/src/ui/components/conversations.scss @@ -0,0 +1,305 @@ +@import "../base.scss"; +@import "./_panel.scss"; +@import "./button.scss"; + +@mixin button-class { + button { + @include button(secondary, 90); + width: 100%; + max-height: 128px; + } +} + +.account { + $colum-spacing: 5%; + $col-width: 8.75%; + $button-width: 20%; + $button-height: calc(100% / 3); + $buttons-row-margin-top: 10%; + + height: 100%; + width: 100%; + + display: grid; + grid-template-columns: auto $colum-spacing repeat(2, calc($button-width/2)) $colum-spacing $button-width $colum-spacing $button-width; + grid-template-rows: 30% auto $button-height; + row-gap: 5%; + grid-template-areas: + "avatar . state name name name name name" + "avatar . status status status status status status" + "avatar . spaces spaces . chat . room" + ; + + &__avatar { + grid-area: avatar; + + display: flex; + align-items: center; + justify-content: center; + + border: $border-normal; + border-radius: $border-radius; + + background-size: cover; + background-position: center; + } + + &__presence-state { + grid-area: state; + + svg { + height: 100%; + width: 100%; + + stroke: get-color(greyscale, 90); + } + + &.online { + svg { + fill: get-color(primary, 100); + } + } + &.offline { + svg { + fill: get-color(ternary, 100); + } + } + &.unavailable { + svg { + fill: get-color(greyscale, 80); + } + } + } + + &__display-name { + grid-area: name; + + display: flex; + align-items: center; + justify-content: center; + + p { + font-size: 2.5vh; + margin: 0; + text-align: center; + } + } + + &__status { + grid-area: status; + } + + @mixin extra-marged-button() { + @include button-class(); + } + + &__spaces { + grid-area: spaces; + + @include extra-marged-button(); + } + + &__chat { + grid-area: chat; + + @include extra-marged-button(); + } + + &__room { + grid-area: room; + + @include extra-marged-button(); + } +} + +.spaces { + $gap: 1%; + $spaces-to-display: 5; + + width: 100%; + height: 100%; + + display: flex; + flex-direction: column; + gap: $gap; + + overflow-y: scroll; + // TODO: Manage android, Safari, ... + scrollbar-width: none; + + $space-height: calc((100% - (($spaces-to-display - 1) * (1%))) / $spaces-to-display); + + --space-height: #{$space-height}; +} + +.space { + $gap: 5%; + $vertical-padding: 1%; + $horizontal-padding: 1%; + + height: var(--space-height); + width: 100%; + + flex-shrink: 0; + + border: $border-normal; + border-color: get-color(primary, 100); + border-radius: $border-radius; + + padding: $vertical-padding $horizontal-padding; + + $name-height: 15%; + $conversation-name-height: 15%; + + display: grid; + + grid-template-columns: 100%; + grid-template-rows: $name-height $gap auto 0% 0%; + grid-template-areas: + "name" + "." + "conversations-carousel" + "." + "conversation-name" + ; + + transition: $transition-duration; + + &.display-conversation-name { + grid-template-rows: $name-height $gap auto $gap $conversation-name-height; + } + + cursor: default; + + p { + margin: 0; + } + + &__name { + grid-area: name; + + display: flex; + align-items: center; + justify-content: left; + + font-size: 2vh; + } + + &__conversations-carousel { + grid-area: conversations-carousel; + + display: flex; + flex-flow: row; + gap: 1%; + + overflow-x: scroll; + + // TODO: Manage android, Safari, ... + scrollbar-width: none; + } + + &__conversation-name { + grid-area: conversation-name; + + display: flex; + align-items: center; + justify-content: center; + + font-size: 2vh; + } +} + +.conversation-avatar { + height: 100%; + aspect-ratio: 1; + + flex-shrink: 0; + + border: $border-thin; + border-radius: $border-radius; + + overflow: hidden; + + filter: brightness(90%); + + &.selected { + filter: brightness(120%); + } + + &__image { + height: 100%; + width: 100%; + + background-size: cover; + + display: flex; + align-items: center; + justify-content: center; + + font-size: 6vh; + color: get-color(primary, 80); + } + + &__invited-badge { + $height: 20%; + + height: $height; + width: 100%; + + position: relative; + top: calc($height * -1); + + color: get-color(greyscale, 0); + + display: flex; + align-items: center; + justify-content: center; + + font-size: 1.5vh; + + background-color: get-color(ternary, 100); + } +} + +.search { + height: 100%; + width: 100%; + + display: flex; + gap: 5%; + + &__button { + @include button-class(); + + width: 20%; + + flex-shrink: 0; + } +} + +.conversations { + @include panel(); + + $gap: 1%; + $account-height: 15%; + $search-height: 5%; + + display: flex; + flex-direction: column; + justify-content: space-between; + gap: $gap; + + &__account { + height: $account-height; + max-height: 384px; + } + + &__spaces { + min-height: calc(100% - $account-height - $search-height - (2 * $gap)); + } + + &__search { + height: $search-height; + max-height: 128px; + } +} diff --git a/src/ui/components/header.rs b/src/ui/components/header.rs deleted file mode 100644 index 22294ee..0000000 --- a/src/ui/components/header.rs +++ /dev/null @@ -1,25 +0,0 @@ -use dioxus::prelude::*; - -turf::style_sheet!("src/ui/components/header.scss"); - -pub fn Header() -> Element { - rsx! { - style { {STYLE_SHEET} }, - - div { - class: ClassName::ROOT, - img { - src: "/public/images/logo-msn.png" - } - svg { - view_box: "0 0 100 10", - text { - y: "55%", - dominant_baseline: "middle", - font_size: "5", - "Windows Live Messenger", - }, - }, - } - } -} diff --git a/src/ui/components/header.scss b/src/ui/components/header.scss deleted file mode 100644 index f087add..0000000 --- a/src/ui/components/header.scss +++ /dev/null @@ -1,13 +0,0 @@ -.root { - height: 100%; - width: 100%; - display: flex; - - img { - height: 100%; - } - - svg { - fill: white; - } -} diff --git a/src/ui/components/icons.rs b/src/ui/components/icons.rs index d15a782..99fcb3d 100644 --- a/src/ui/components/icons.rs +++ b/src/ui/components/icons.rs @@ -1,6 +1,8 @@ use const_format::formatcp; use dioxus::prelude::*; -use dioxus_free_icons::icons::md_navigation_icons::MdArrowDropDown; +use dioxus_free_icons::icons::fa_solid_icons::{ + FaComments, FaLayerGroup, FaMagnifyingGlass, FaPeopleGroup, +}; use dioxus_free_icons::{Icon, IconShape}; turf::style_sheet!("src/ui/components/icons.scss"); @@ -9,13 +11,51 @@ include!(concat!(env!("OUT_DIR"), "/style_vars.rs")); use style::{COLOR_PRIMARY_100, COLOR_TERNARY_100}; -pub fn DownArrowIcon() -> Element { +macro_rules! transparent_icon { + ($name:ident, $icon:ident) => { + pub fn $name() -> Element { + rsx! { + style { {STYLE_SHEET} }, + Icon { + class: ClassName::TRANSPARENT_ICON, + icon: $icon, + } + } + } + }; +} + +transparent_icon!(SearchIcon, FaMagnifyingGlass); +transparent_icon!(SpacesIcon, FaLayerGroup); +transparent_icon!(ChatsIcon, FaComments); +transparent_icon!(RoomsIcon, FaPeopleGroup); + +#[derive(Clone, PartialEq)] +pub(crate) struct LogoShape; +impl IconShape for LogoShape { + fn view_box(&self) -> &str { + "0 0 184 94" + } + fn xmlns(&self) -> &str { + "http://www.w3.org/2000/svg" + } + fn child_elements(&self) -> Element { + rsx! { + path { + "stroke-linejoin": "round", + "stroke-width": "6", + d: "M121.208 2 2 57.011l70.927-.265L61.363 92 182 36.724h-69.498L121.208 2Z" + } + } + } +} + +pub fn LogoIcon() -> Element { rsx! { style { {STYLE_SHEET} }, Icon { - class: ClassName::DOWN_ARROW_ICON, - icon: MdArrowDropDown, + icon: LogoShape, } } } diff --git a/src/ui/components/icons.scss b/src/ui/components/icons.scss index 494370c..5faeb01 100644 --- a/src/ui/components/icons.scss +++ b/src/ui/components/icons.scss @@ -1,4 +1,4 @@ -.down-arrow-icon { +.transparent-icon { color: transparent; path:last-child { diff --git a/src/ui/components/loading.rs b/src/ui/components/loading.rs deleted file mode 100644 index e2aa62d..0000000 --- a/src/ui/components/loading.rs +++ /dev/null @@ -1,28 +0,0 @@ -use dioxus::prelude::*; -use log::debug; - -use super::spinner::Spinner; -use super::wallpaper::Wallpaper; - -turf::style_sheet!("src/ui/components/loading.scss"); - -pub fn LoadingPage() -> Element { - debug!("LoadingPage rendering"); - - rsx! { - style { {STYLE_SHEET} }, - - div { - class: ClassName::LOADING, - - Wallpaper { - display_version: true - }, - - div { - class: ClassName::LOADING_SPINNER, - Spinner {}, - } - } - } -} diff --git a/src/ui/components/loading.scss b/src/ui/components/loading.scss deleted file mode 100644 index f26b2a1..0000000 --- a/src/ui/components/loading.scss +++ /dev/null @@ -1,80 +0,0 @@ -@import "../_base.scss" -@import "./spinner.scss" - -.loading { - height: 100%; - width: 100%; - - display: flex; - align-items: center; - justify-content: center; - - &__spinner { - height: 5%; - aspect-ratio: $logo-aspect-ratio; - - position: absolute; - - $logo-center-pos: calc(50% + ($background-height / 2) - ($logo-height / 2)); - - @media (0px < height <= calc($background-height * 5)) { - top: $logo-center-pos; - } - @media (calc($background-height * 5) < height <= calc($background-height * 6)) { - top: calc($logo-center-pos + ($background-height * 1)); - } - @media (calc($background-height * 6) < height <= calc($background-height * 8)) { - top: calc($logo-center-pos + ($background-height * 2)); - } - @media (calc($background-height * 8) < height <= calc($background-height * 10)) { - top: calc($logo-center-pos + ($background-height * 3)); - } - @media (calc($background-height * 10) < height <= calc($background-height * 12)) { - top: calc($logo-center-pos + ($background-height * 4)); - } - @media (calc($background-height * 12) < height <= calc($background-height * 14)) { - top: calc($logo-center-pos + ($background-height * 5)); - } - @media (calc($background-height * 14) < height <= calc($background-height * 16)) { - top: calc($logo-center-pos + ($background-height * 6)); - } - @media (calc($background-height * 16) < height <= calc($background-height * 18)) { - top: calc($logo-center-pos + ($background-height * 7)); - } - @media (calc($background-height * 18) < height <= calc($background-height * 20)) { - top: calc($logo-center-pos + ($background-height * 8)); - } - @media (calc($background-height * 20) < height <= calc($background-height * 22)) { - top: calc($logo-center-pos + ($background-height * 9)); - } - @media (calc($background-height * 22) < height <= calc($background-height * 24)) { - top: calc($logo-center-pos + ($background-height * 10)); - } - @media (calc($background-height * 24) < height <= calc($background-height * 26)) { - top: calc($logo-center-pos + ($background-height * 11)); - } - @media (calc($background-height * 26) < height <= calc($background-height * 28)) { - top: calc($logo-center-pos + ($background-height * 12)); - } - @media (calc($background-height * 28) < height <= calc($background-height * 30)) { - top: calc($logo-center-pos + ($background-height * 13)); - } - @media (calc($background-height * 30) < height <= calc($background-height * 32)) { - top: calc($logo-center-pos + ($background-height * 14)); - } - @media (calc($background-height * 32) < height <= calc($background-height * 34)) { - top: calc($logo-center-pos + ($background-height * 15)); - } - @media (calc($background-height * 34) < height <= calc($background-height * 36)) { - top: calc($logo-center-pos + ($background-height * 16)); - } - @media (calc($background-height * 36) < height <= calc($background-height * 38)) { - top: calc($logo-center-pos + ($background-height * 17)); - } - @media (calc($background-height * 38) < height <= calc($background-height * 40)) { - top: calc($logo-center-pos + ($background-height * 18)); - } - - background-color: get-color(greyscale, 0); - } -} diff --git a/src/ui/components/login.rs b/src/ui/components/login.rs index 87f981d..fce17f0 100644 --- a/src/ui/components/login.rs +++ b/src/ui/components/login.rs @@ -1,24 +1,23 @@ -use std::borrow::Cow; -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; +use std::{borrow::Cow, cell::RefCell, collections::HashMap, rc::Rc}; use const_format::formatcp; use dioxus::prelude::*; -use log::{debug, error, warn}; +use tracing::{debug, error, warn}; use validator::{Validate, ValidateArgs, ValidateEmail, ValidationError, ValidationErrors}; use zxcvbn::zxcvbn; -use crate::base::SESSION; -use crate::domain::model::session::Session; -use crate::infrastructure::services::random_svg_generators::{ - generate_random_svg_shape, ShapeConfig, +use crate::{ + domain::model::session::Session, + infrastructure::services::random_svg_generators::{generate_random_svg_shape, ShapeConfig}, + ui::SESSION, }; -use super::button::{LoginButton, RegisterButton}; -use super::modal::{Modal, Severity}; -use super::spinner::Spinner; -use super::text_input::{PasswordInputState, PasswordTextInput, TextInput, TextInputState}; +use super::{ + button::{LoginButton, RegisterButton}, + modal::{Modal, Severity}, + spinner::Spinner, + text_input::{PasswordInputState, PasswordTextInput, TextInput, TextInputState}, +}; include!(concat!(env!("OUT_DIR"), "/style_vars.rs")); @@ -538,7 +537,7 @@ fn generate_modal( on_confirm: on_confirm, div { - {rendered_suggestions.into_iter()} + {rendered_suggestions.iter()} } } } @@ -608,15 +607,16 @@ pub fn Login() -> Element { generate_random_svg_shape(Some(&shape_config)).await }); - let avatar = match &*random_avatar_future.read_unchecked() { - Some(svg) => Some(rsx! { - div { - class: ClassName::LOGIN_AVATAR_CONTENT, - dangerous_inner_html: svg.as_str(), + let avatar = (*random_avatar_future.read_unchecked()) + .as_ref() + .map(|svg| { + rsx! { + div { + class: ClassName::LOGIN_AVATAR_CONTENT, + dangerous_inner_html: svg.as_str(), + } } - }), - None => None, - }; + }); if *spinner_animated.read() && SESSION.read().is_logged { debug!("Stop spinner"); diff --git a/src/ui/components/login.scss b/src/ui/components/login.scss index 59692c4..8953a3a 100644 --- a/src/ui/components/login.scss +++ b/src/ui/components/login.scss @@ -3,7 +3,8 @@ @import "./spinner.scss"; .login { - @extend %panel; + $padding: 5%; + @include panel($padding, $padding); $aspect-ratio: var(--aspect-ratio); @@ -72,7 +73,7 @@ overflow: hidden; &__content { - height: calc(100% + (2 * $border-normal-width)); + height: 100%; aspect-ratio: 1; } } diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index ecab4e3..c7b4402 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -1,12 +1,8 @@ -pub(crate) mod avatar_selector; pub(crate) mod button; -pub(crate) mod chats_window; -pub(crate) mod contacts_window; -pub(crate) mod header; +pub(crate) mod chat_panel; +pub(crate) mod conversations; pub(crate) mod icons; -pub(crate) mod loading; pub(crate) mod login; -pub(crate) mod main_window; pub(crate) mod modal; pub(crate) mod spinner; pub(crate) mod text_input; diff --git a/src/ui/components/modal.rs b/src/ui/components/modal.rs index 59a5016..483c8ea 100644 --- a/src/ui/components/modal.rs +++ b/src/ui/components/modal.rs @@ -59,15 +59,16 @@ pub fn Modal(props: ModalProps) -> Element { let random_figure_future = use_resource(move || async move { generate_random_svg_avatar(avatar_config).await }); - let icon = match &*random_figure_future.read_unchecked() { - Some(svg) => Some(rsx! { - div { - class: ClassName::MODAL_CONTENT_ICON_PLACEHOLDER, - dangerous_inner_html: svg.as_str(), + let icon = (*random_figure_future.read_unchecked()) + .as_ref() + .map(|svg| { + rsx! { + div { + class: ClassName::MODAL_CONTENT_ICON_PLACEHOLDER, + dangerous_inner_html: svg.as_str(), + } } - }), - None => None, - }; + }); let button_class = match &props.severity { Severity::Ok => SuccessButton, diff --git a/src/ui/components/modal.scss b/src/ui/components/modal.scss index c6db37a..6c7ae37 100644 --- a/src/ui/components/modal.scss +++ b/src/ui/components/modal.scss @@ -67,7 +67,7 @@ $modal-max-height: 55vh; border-radius: $border-radius; &__placeholder { - width: calc(100% + (2 * $border-normal-width)); + width: 100%; height: 100%; } diff --git a/src/ui/components/spinner.rs b/src/ui/components/spinner.rs index 91ef65c..68750b1 100644 --- a/src/ui/components/spinner.rs +++ b/src/ui/components/spinner.rs @@ -1,28 +1,10 @@ use dioxus::prelude::*; -use dioxus_free_icons::{Icon, IconShape}; +use dioxus_free_icons::Icon; + +use crate::ui::components::icons::LogoShape; turf::style_sheet!("src/ui/components/spinner.scss"); -#[derive(Clone, PartialEq)] -struct _Spinner; -impl IconShape for _Spinner { - fn view_box(&self) -> &str { - "0 0 184 94" - } - fn xmlns(&self) -> &str { - "http://www.w3.org/2000/svg" - } - fn child_elements(&self) -> Element { - rsx! { - path { - "stroke-linejoin": "round", - "stroke-width": "6", - d: "M121.208 2 2 57.011l70.927-.265L61.363 92 182 36.724h-69.498L121.208 2Z" - } - } - } -} - #[derive(PartialEq, Clone, Props)] pub struct SpinnerProps { #[props(default = true)] @@ -38,7 +20,7 @@ pub fn Spinner(props: SpinnerProps) -> Element { Icon { class: if props.animate { "" } else { ClassName::PAUSED }, - icon: _Spinner, + icon: LogoShape, } } } diff --git a/src/ui/components/text_input.rs b/src/ui/components/text_input.rs index 1ecb3ef..07e4fcb 100644 --- a/src/ui/components/text_input.rs +++ b/src/ui/components/text_input.rs @@ -10,7 +10,7 @@ turf::style_sheet!("src/ui/components/text_input.scss"); pub trait InputPropsData {} #[derive(Props, Clone, PartialEq)] -pub struct InputProps { +pub struct InputProps { value: Option, placeholder: Option, oninput: Option>>, diff --git a/src/ui/components/text_input.scss b/src/ui/components/text_input.scss index d83cba6..ce09044 100644 --- a/src/ui/components/text_input.scss +++ b/src/ui/components/text_input.scss @@ -3,17 +3,14 @@ %base-text-input { $horizontal-padding: 1vw; - height: calc(100% - (2 * $border-normal-width)); - width: calc(100% - (2 * $horizontal-padding)); + height: 100%; + width: 100%; border: $border-normal; border-color: get-color(primary, 90); border-radius: $border-radius; - padding-left: $horizontal-padding; - padding-right: $horizontal-padding; - padding-top: 0px; - padding-bottom: 0px; + padding: 0px $horizontal-padding; } %base-input { diff --git a/src/ui/layouts/conversations.rs b/src/ui/layouts/conversations.rs new file mode 100644 index 0000000..80cb846 --- /dev/null +++ b/src/ui/layouts/conversations.rs @@ -0,0 +1,252 @@ +use std::rc::Rc; + +use dioxus::prelude::*; +use futures::join; + +turf::style_sheet!("src/ui/layouts/conversations.scss"); + +use crate::ui::components::chat_panel::ChatPanel; +use crate::ui::components::conversations::Conversations as ConversationsComponent; +use crate::ui::components::wallpaper::Wallpaper; + +// TODO: Get from SCSS +const WIDGET_HEIGHT_RATIO: f64 = 0.95; +const ASPECT_RATIO: f64 = 1.0 / 1.618; + +async fn on_carousel_scroll( + parent_div: &Rc, + first_div: &Rc, + last_div: &Rc, +) { + let results = join!( + parent_div.get_scroll_offset(), + parent_div.get_scroll_size(), + last_div.get_client_rect() + ); + if let (Ok(offset), Ok(size), Ok(last_div_rect)) = results { + let left = offset.x; + let width = size.width; + // The left border of the first div has been exceeded, scrool to the last div. + if left <= 0.0 { + let _ = last_div.scroll_to(ScrollBehavior::Smooth).await; + } + // The left border of the last div has been exceeded, scrool to the first div. + else { + let last_div_width = last_div_rect.size.width; + let distance_to_tail = width - left - last_div_width; + + if distance_to_tail < 1.0 { + let first_div = first_div.as_ref(); //.unwrap(); + let _ = first_div.scroll_to(ScrollBehavior::Smooth).await; + } + } + } +} + +fn LayoutSmall() -> Element { + let mut carousel_div = use_signal(|| None::>); + let mut first_div = use_signal(|| None::>); + let mut last_div = use_signal(|| None::>); + + let conversation_panels_nb = 3; + let conversation_panels = (0..conversation_panels_nb + 1).map(|i| { + let inner = rsx! { + div { + class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL_INNER, + ChatPanel { name: format!("CHAT #{i}") }, + } + }; + + if i == conversation_panels_nb { + rsx! { + div { + class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL, + onmounted: move |cx: Event| last_div.set(Some(cx.data())), + {inner} + } + } + } else { + rsx! { + div { + class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL, + {inner} + } + } + } + }); + + rsx! { + style { {STYLE_SHEET} }, + + div { + class: ClassName::CONVERSATIONS_VIEW_SMALL, + + onmounted: move |cx| async move { + let data = cx.data(); + carousel_div.set(Some(data)); + }, + + onscroll: move |_| { + async move { + if let (Some(carousel_div), Some(first_div), Some(last_div)) = ( + carousel_div.read().as_ref(), + first_div.read().as_ref(), + last_div.read().as_ref(), + ) { + on_carousel_scroll(carousel_div, first_div, last_div).await; + } + } + }, + + div { + class: ClassName::CONVERSATIONS_VIEW_HEAD, + } + + div { + class: ClassName::CONVERSATIONS_VIEW_SMALL_CONVERSATIONS_PANEL, + onmounted: move |cx| async move { + let data = cx.data(); + let _ = data.as_ref().scroll_to(ScrollBehavior::Smooth).await; + first_div.set(Some(data)); + }, + + div { + class: ClassName::CONVERSATIONS_VIEW_SMALL_CONVERSATIONS_PANEL_INNER, + ConversationsComponent {}, + }, + }, + + {conversation_panels} + + div { + class: ClassName::CONVERSATIONS_VIEW_TAIL, + } + } + } +} + +fn LayoutBig() -> Element { + let mut carousel_div = use_signal(|| None::>); + let mut first_div = use_signal(|| None::>); + let mut last_div = use_signal(|| None::>); + + let conversation_panels_nb = 3; + let conversation_panels = (0..conversation_panels_nb + 1).map(|i| { + if i == 0 { + rsx! { + div { + class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATION_PANELS_PANEL, + onmounted: move |cx| async move { + let data = cx.data(); + let _ = data.as_ref().scroll_to(ScrollBehavior::Smooth).await; + first_div.set(Some(data)); + }, + ChatPanel { name: format!("CHAT #{i}") }, + } + } + } else if i == conversation_panels_nb { + rsx! { + div { + class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATION_PANELS_PANEL, + onmounted: move |cx: Event| last_div.set(Some(cx.data())), + ChatPanel { name: format!("CHAT #{i}") }, + } + } + } else { + rsx! { + div { + class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATION_PANELS_PANEL, + ChatPanel { name: format!("CHAT #{i}") }, + } + } + } + }); + + rsx! { + style { {STYLE_SHEET} }, + + div { + class: ClassName::CONVERSATIONS_VIEW_BIG, + + div { + class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_PANEL, + + ConversationsComponent {}, + }, + + div { + class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATION_PANELS, + + onmounted: move |cx| async move { + let data = cx.data(); + carousel_div.set(Some(data)); + }, + + onscroll: move |_| { + async move { + if let (Some(carousel_div), Some(first_div), Some(last_div)) = ( + carousel_div.read().as_ref(), + first_div.read().as_ref(), + last_div.read().as_ref(), + ) { + on_carousel_scroll(carousel_div, first_div, last_div).await; + } + } + }, + + div { + class: ClassName::CONVERSATIONS_VIEW_HEAD, + } + + {conversation_panels} + + div { + class: ClassName::CONVERSATIONS_VIEW_TAIL, + } + + }, + } + } +} + +pub fn Conversations() -> Element { + let mut view_size = use_signal(|| None::<(f64, f64)>); + + // TODO: Make the layout reactive (on window resize) + let layout = { + move || { + if let Some((width, height)) = view_size.read().as_ref() { + let component_width = height * WIDGET_HEIGHT_RATIO * ASPECT_RATIO; + let breakpoint_width = component_width * 2_f64; + + if *width >= breakpoint_width { + return rsx! { LayoutBig {} }; + } + } + rsx! {LayoutSmall {}} + } + }(); + + rsx! { + style { {STYLE_SHEET} }, + + Wallpaper { + display_version: true + }, + + div { + class: ClassName::CONVERSATIONS_VIEW, + onmounted: move |cx| { + async move { + let data = cx.data(); + + if let Ok(client_rect) = data.get_client_rect().await { + view_size.set(Some((client_rect.size.width, client_rect.size.height))); + } + } + }, + + {layout} + } + } +} diff --git a/src/ui/layouts/conversations.scss b/src/ui/layouts/conversations.scss new file mode 100644 index 0000000..8844a62 --- /dev/null +++ b/src/ui/layouts/conversations.scss @@ -0,0 +1,136 @@ +@import "../_base.scss"; +@import "../components/_panel.scss"; + +.conversations-view-head { + height: 100%; + width: 1px; + + flex-shrink: 0; + scroll-snap-align: end; +} + +.conversations-view-tail { + height: 100%; + width: 1px; + + flex-shrink: 0; + scroll-snap-align: start; +} + +.conversations-view { + $height: 100vh; + $width: 100vw; + $conversations-panel-height: calc($height * 0.95); + $conversations-panel-width: calc($conversations-panel-height * $panel-aspect-ratio); + $gap: 1%; + $content-height: 95%; + $ratio: 2; + + height: 100%; + width: 100%; + + position: relative; + top: -100vh; + margin-bottom: -100vh; + + &__small { + scroll-snap-type: x mandatory; + + height: 100%; + width: 100%; + + display: flex; + flex-direction: row; + gap: $gap; + + justify-content: safe center; + align-items: safe center; + + overflow-x: scroll; + + &__conversations-panel { + height: $content-height; + width: 100%; + + flex-shrink: 0; + scroll-snap-align: center; + + display: flex; + align-items: center; + justify-content: center; + + &__inner { + height: 100%; + + // TODO: Is aspect-ratio the best criteria to defined that inner shall take all the available space ? + @media (max-aspect-ratio: $panel-aspect-ratio) { + width: 100%; + } + @media (min-aspect-ratio: $panel-aspect-ratio) { + aspect-ratio: $panel-aspect-ratio; + } + } + } + + &__panel { + flex-shrink: 0; + + height: $content-height; + width: 100%; + + display: flex; + align-items: center; + justify-content: center; + + scroll-snap-align: center; + + &__inner { + height: 100%; + width: calc(100% - (2 * $gap)); + } + } + } + + &__big { + height: 100%; + width: 100%; + width: calc(100% - (2 * $gap)); + + display: flex; + flex-direction: row; + align-items: safe center; + gap: $gap; + + margin: 0 $gap; + + &__conversations-panel { + height: $content-height; + aspect-ratio: $panel-aspect-ratio; + } + + &__conversation-panels { + height: $content-height; + + flex-grow: 1; + + display: flex; + flex-direction: row; + overflow-x: scroll; + + justify-content: safe center; + align-items: safe center; + scroll-snap-type: x mandatory; + + gap: $gap; + + &__panel { + flex-shrink: 0; + + height: 100%; + width: 100%; + + scroll-snap-align: center; + } + } + } +} diff --git a/src/ui/views/login_view.rs b/src/ui/layouts/login.rs similarity index 67% rename from src/ui/views/login_view.rs rename to src/ui/layouts/login.rs index 50d527d..807661b 100644 --- a/src/ui/views/login_view.rs +++ b/src/ui/layouts/login.rs @@ -1,11 +1,11 @@ use dioxus::prelude::*; -use crate::ui::components::login::Login; +use crate::ui::components::login::Login as LoginComponent; use crate::ui::components::wallpaper::Wallpaper; -turf::style_sheet!("src/ui/views/login_view.scss"); +turf::style_sheet!("src/ui/layouts/login.scss"); -pub fn LoginView() -> Element { +pub fn Login() -> Element { rsx! { style { {STYLE_SHEET} }, @@ -18,7 +18,7 @@ pub fn LoginView() -> Element { div { class: ClassName::LOGIN_VIEW_LOGIN_PANEL, - Login {} + LoginComponent {} } } } diff --git a/src/ui/views/login_view.scss b/src/ui/layouts/login.scss similarity index 100% rename from src/ui/views/login_view.scss rename to src/ui/layouts/login.scss diff --git a/src/ui/layouts/mod.rs b/src/ui/layouts/mod.rs new file mode 100644 index 0000000..1d1f310 --- /dev/null +++ b/src/ui/layouts/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod conversations; +pub(crate) mod login; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1cccdfe..d3dc2dd 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,2 +1,13 @@ pub(crate) mod components; -pub(crate) mod views; +pub(crate) mod layouts; +pub(crate) mod store; + +use dioxus::prelude::{GlobalSignal, Signal}; + +use super::domain::model::{account::Account, session::Session}; +use store::Store; + +pub static STORE: GlobalSignal = Signal::global(Store::new); +// TODO: Merge ACCOUNT and SESSION +pub static ACCOUNT: GlobalSignal = Signal::global(|| Account::new(&STORE)); +pub static SESSION: GlobalSignal = Signal::global(Session::new); diff --git a/src/ui/store/mod.rs b/src/ui/store/mod.rs new file mode 100644 index 0000000..208ae74 --- /dev/null +++ b/src/ui/store/mod.rs @@ -0,0 +1,62 @@ +pub(crate) mod room; +pub(crate) mod space; + +use std::{collections::HashMap, rc::Rc}; + +use async_trait::async_trait; +use dioxus::prelude::*; +use tracing::error; + +use crate::domain::model::room::RoomId; +use crate::domain::model::space::SpaceId; +use crate::domain::model::store_interface::{ + AccountStoreProviderInterface, RoomStoreConsumerInterface, RoomStoreProviderInterface, + SpaceStoreConsumerInterface, SpaceStoreProviderInterface, +}; + +use room::Room; +use space::Space; + +#[modx::store] +pub struct Store { + rooms: HashMap>, + spaces: HashMap>, +} + +#[async_trait(?Send)] +impl AccountStoreProviderInterface for GlobalSignal { + fn on_new_room( + &self, + domain_room: Rc, + ) -> Rc { + let room_id = domain_room.id(); + let room = Rc::new(Room::from_domain(Rc::clone(&domain_room))); + + let mut rooms = self.read().rooms; + rooms.write().insert(room_id.clone(), Rc::clone(&room)); + + let spaces = self.read().spaces; + for space_id in domain_room.spaces() { + if let Some(space) = spaces.read().get(space_id) { + space.add_room(room_id.clone()); + } else { + error!("No {} space found", space_id); + } + } + + room + } + + fn on_new_space( + &self, + domain_space: Rc, + ) -> Rc { + let space_id = domain_space.id(); + let space = Rc::new(Space::from_domain(Rc::clone(&domain_space))); + + let mut spaces = self.read().spaces; + spaces.write().insert(space_id.clone(), Rc::clone(&space)); + + space + } +} diff --git a/src/ui/store/room.rs b/src/ui/store/room.rs new file mode 100644 index 0000000..413ec93 --- /dev/null +++ b/src/ui/store/room.rs @@ -0,0 +1,88 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use dioxus::prelude::*; + +use crate::domain::model::common::Avatar; +use crate::domain::model::room::Invitation; +use crate::domain::model::space::SpaceId; +use crate::domain::model::{ + room::RoomId, + room_member::RoomMember, + store_interface::{RoomStoreConsumerInterface, RoomStoreProviderInterface}, +}; + +#[modx::props(id, is_direct, name, spaces)] +#[modx::store] +pub struct Store { + id: RoomId, + + is_direct: Option, + name: Option, + avatar: Option, + members: Vec, + invitations: Vec, + is_invited: bool, + + spaces: Vec, +} + +#[derive(Clone)] +pub struct Room { + store: RefCell, + + #[allow(dead_code)] + domain: Rc, +} + +impl Room { + pub fn signal(&self) -> Store { + *self.store.borrow() + } + + pub fn from_domain(room: Rc) -> Self { + let props = StoreProps::new( + room.id().clone(), + room.is_direct(), + room.name(), + room.spaces().clone(), + ); + + Self { + store: RefCell::new(Store::new(props)), + domain: room, + } + } + + #[allow(dead_code)] + pub async fn get_avatar(&self) -> Option { + self.domain.avatar().await + } +} + +impl RoomStoreProviderInterface for Room { + fn on_new_name(&self, name: Option) { + let mut store = self.store.borrow_mut(); + store.name.set(name); + } + + fn on_new_avatar(&self, avatar: Option) { + let mut store = self.store.borrow_mut(); + store.avatar.set(avatar); + } + + fn on_new_member(&self, member: RoomMember) { + let mut store = self.store.borrow_mut(); + store.members.write().push(member); + } + + fn on_invitation(&self, invitation: Invitation) { + let mut store = self.store.borrow_mut(); + + if !store.is_invited() && invitation.is_account_user() { + store.is_invited.set(true); + } + + store.invitations.write().push(invitation); + } +} diff --git a/src/ui/store/space.rs b/src/ui/store/space.rs new file mode 100644 index 0000000..deff902 --- /dev/null +++ b/src/ui/store/space.rs @@ -0,0 +1,53 @@ +use std::rc::Rc; +use std::{cell::RefCell, collections::HashSet}; + +use dioxus::prelude::*; + +use crate::domain::model::{ + room::RoomId, + space::SpaceId, + store_interface::{SpaceStoreConsumerInterface, SpaceStoreProviderInterface}, +}; + +#[modx::props(id, name)] +#[modx::store] +pub struct Store { + id: SpaceId, + name: Option, + room_ids: HashSet, +} + +#[derive(Clone)] +pub struct Space { + store: RefCell, + + #[allow(dead_code)] + domain: Rc, +} + +impl Space { + pub fn signal(&self) -> Store { + *self.store.borrow() + } + + pub fn from_domain(space: Rc) -> Self { + let props = StoreProps::new(space.id().clone(), space.name()); + + Self { + store: RefCell::new(Store::new(props)), + domain: space, + } + } + + pub fn add_room(&self, room_id: RoomId) { + let mut store = self.store.borrow_mut(); + store.room_ids.write().insert(room_id); + } +} + +impl SpaceStoreProviderInterface for Space { + fn set_name(&self, name: Option) { + let mut store = self.store.borrow_mut(); + store.name.set(name); + } +} diff --git a/src/ui/views/mod.rs b/src/ui/views/mod.rs deleted file mode 100644 index 1701ad6..0000000 --- a/src/ui/views/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod login_view; diff --git a/src/utils.rs b/src/utils.rs index 64baf06..c612e1f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -8,6 +8,7 @@ impl Receiver { } } +#[derive(Clone)] pub struct Sender(_Sender); // TODO: Handle error