Merge branch 'conversations-panel' into develop
81
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
|
||||
|
||||
|
Before Width: | Height: | Size: 698 B |
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 494 B |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 221 B |
Before Width: | Height: | Size: 223 B |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 755 B |
Before Width: | Height: | Size: 864 B |
Before Width: | Height: | Size: 306 B |
Before Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 853 B |
Before Width: | Height: | Size: 430 B |
Before Width: | Height: | Size: 639 B |
Before Width: | Height: | Size: 388 B |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 323 B |
@@ -1 +0,0 @@
|
||||
<svg enable-background="new 0 0 48 48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path d="m36.5 44h-25c-1.1 0-1.8-1.2-1.3-2.2l2.8-4.8h22l2.7 4.8c.6 1-.1 2.2-1.2 2.2z" fill="#455a64"/><circle cx="24" cy="23" fill="#78909c" r="18"/><path d="m24 35c-6.6 0-12-5.4-12-12s5.4-12 12-12 12 5.4 12 12-5.4 12-12 12z" fill="#455a64"/><circle cx="24" cy="23" fill="#42a5f5" r="9"/><path d="m28.8 20c-1.2-1.4-3-2.2-4.8-2.2s-3.6.8-4.8 2.2c-.5.5-.4 1.3.1 1.8s1.3.4 1.8-.1c1.5-1.7 4.3-1.7 5.8 0 .3.3.6.4 1 .4.3 0 .6-.1.9-.3.4-.4.5-1.3 0-1.8z" fill="#90caf9"/></svg>
|
Before Width: | Height: | Size: 562 B |
Before Width: | Height: | Size: 719 B |
197
src/base.rs
@@ -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<OwnedMxcUri>,
|
||||
// pub display_name: Option<String>,
|
||||
// pub blurhash: Option<String>,
|
||||
// }
|
||||
|
||||
// impl UserInfo {
|
||||
// pub fn new(
|
||||
// avatar_url: Option<OwnedMxcUri>,
|
||||
// display_name: Option<String>,
|
||||
// blurhash: Option<String>,
|
||||
// ) -> Self {
|
||||
// Self {
|
||||
// avatar_url,
|
||||
// display_name,
|
||||
// blurhash,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub type ByIdUserInfos = HashMap<OwnedUserId, UserInfo>;
|
||||
|
||||
// #[derive(Clone)]
|
||||
// pub struct Store {
|
||||
// pub is_logged: bool,
|
||||
// pub rooms: ByIdRooms,
|
||||
// pub user_infos: ByIdUserInfos,
|
||||
// pub user_id: Option<OwnedUserId>,
|
||||
// }
|
||||
|
||||
// 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<RefCell<Requester>>,
|
||||
}
|
||||
|
||||
impl AppSettings {
|
||||
pub fn new() -> Self {
|
||||
Self { requester: None }
|
||||
}
|
||||
|
||||
pub fn set_requester(&mut self, requester: RefCell<Requester>) {
|
||||
self.requester = Some(requester);
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_room(room_id: OwnedRoomId, room: Room, by_id_rooms: &GlobalSignal<ByIdRooms>) {
|
||||
// TODO: Update rooms
|
||||
by_id_rooms
|
||||
.write()
|
||||
.insert(room_id, RefCell::<Room>::new(room));
|
||||
}
|
||||
|
||||
async fn on_joining_invitation(
|
||||
room_id: OwnedRoomId,
|
||||
room: Room,
|
||||
by_id_rooms: &GlobalSignal<ByIdRooms>,
|
||||
) {
|
||||
debug!("You're invited to join the \"{}\" room", room.id());
|
||||
// TODO: Update rooms
|
||||
by_id_rooms
|
||||
.write()
|
||||
.insert(room_id, RefCell::<Room>::new(room));
|
||||
}
|
||||
|
||||
async fn on_room_topic(room_id: OwnedRoomId, topic: String, by_id_rooms: &GlobalSignal<ByIdRooms>) {
|
||||
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<ByIdRooms>, room_id: OwnedRoomId) {
|
||||
error!("== sync_messages ==");
|
||||
|
||||
}
|
||||
|
||||
pub async fn sync_rooms(
|
||||
mut rx: UnboundedReceiver<bool>,
|
||||
receivers: Receivers,
|
||||
by_id_rooms: &GlobalSignal<ByIdRooms>,
|
||||
) {
|
||||
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<bool>,
|
||||
app_settings: &GlobalSignal<AppSettings>,
|
||||
session: &GlobalSignal<Session>,
|
||||
) {
|
||||
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<AppSettings> = Signal::global(AppSettings::new);
|
||||
pub static ROOMS: GlobalSignal<ByIdRooms> = Signal::global(ByIdRooms::new);
|
||||
pub static SESSION: GlobalSignal<Session> = Signal::global(Session::new);
|
||||
pub static CHATS_WIN_INTERFACE: GlobalSignal<ChatsWinInterface> =
|
||||
Signal::global(ChatsWinInterface::new);
|
136
src/domain/model/account.rs
Normal file
@@ -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<RoomId, Rc<Room>>;
|
||||
type Spaces = HashMap<SpaceId, Rc<Space>>;
|
||||
|
||||
pub struct Account {
|
||||
display_name: RefCell<Option<String>>,
|
||||
avatar: RefCell<Option<Vec<u8>>>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
presence_state: RefCell<Option<PresenceState>>,
|
||||
|
||||
by_id_rooms: RefCell<Rooms>,
|
||||
by_id_spaces: RefCell<Spaces>,
|
||||
|
||||
messaging_provider: Option<Rc<dyn AccountMessagingProviderInterface>>,
|
||||
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<dyn AccountMessagingProviderInterface>) {
|
||||
self.messaging_provider = Some(provider.clone());
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_room(&self, room_id: &RoomId) -> Option<Rc<Room>> {
|
||||
self.by_id_rooms.borrow().get(room_id).cloned()
|
||||
}
|
||||
|
||||
pub async fn get_display_name(&self) -> &RefCell<Option<String>> {
|
||||
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<Option<Vec<u8>>> {
|
||||
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<Room>) -> Rc<dyn RoomMessagingConsumerInterface> {
|
||||
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<dyn RoomStoreConsumerInterface>);
|
||||
|
||||
room.set_store(room_store);
|
||||
|
||||
room
|
||||
}
|
||||
|
||||
#[instrument(name = "Account", skip_all)]
|
||||
async fn on_new_space(&self, space: Rc<Space>) -> Rc<dyn SpaceMessagingConsumerInterface> {
|
||||
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<dyn SpaceStoreConsumerInterface>);
|
||||
|
||||
space.set_store(space_store);
|
||||
|
||||
space
|
||||
}
|
||||
}
|
7
src/domain/model/common.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use matrix_sdk::ruma::{presence::PresenceState as MatrixPresenceState, OwnedUserId};
|
||||
|
||||
pub type Avatar = Vec<u8>;
|
||||
|
||||
pub type PresenceState = MatrixPresenceState;
|
||||
|
||||
pub type UserId = OwnedUserId;
|
68
src/domain/model/messaging_interface.rs
Normal file
@@ -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<Room>) -> Rc<dyn RoomMessagingConsumerInterface>;
|
||||
async fn on_new_space(&self, space: Rc<Space>) -> Rc<dyn SpaceMessagingConsumerInterface>;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait AccountMessagingProviderInterface {
|
||||
async fn get_display_name(&self) -> anyhow::Result<Option<String>>;
|
||||
async fn get_avatar(&self) -> anyhow::Result<Option<Vec<u8>>>;
|
||||
|
||||
async fn run_forever(
|
||||
&self,
|
||||
account_events_consumer: &dyn AccountMessagingConsumerInterface,
|
||||
account_events_receiver: Receiver<AccountEvent>,
|
||||
) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait RoomMessagingConsumerInterface {
|
||||
async fn on_invitation(&self, _invitation: Invitation) {}
|
||||
|
||||
async fn on_new_topic(&self, _topic: Option<String>) {}
|
||||
async fn on_new_name(&self, _name: Option<String>) {}
|
||||
async fn on_new_avatar(&self, _url: Option<Avatar>) {}
|
||||
|
||||
#[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<Option<Avatar>>;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait SpaceMessagingConsumerInterface {
|
||||
async fn on_child(&self, _room_id: RoomId) {}
|
||||
async fn on_new_topic(&self, _topic: Option<String>) {}
|
||||
async fn on_new_name(&self, _name: Option<String>) {}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait SpaceMessagingProviderInterface {}
|
||||
|
||||
// TODO: Rework
|
||||
#[async_trait(?Send)]
|
||||
pub trait MemberMessagingProviderInterface {
|
||||
async fn get_avatar(
|
||||
&self,
|
||||
avatar_url: Option<AvatarUrl>,
|
||||
room_id: RoomId,
|
||||
user_id: UserId,
|
||||
) -> anyhow::Result<Option<Avatar>>;
|
||||
}
|
@@ -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;
|
||||
|
@@ -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<String>,
|
||||
|
||||
name: RefCell<Option<String>>,
|
||||
topic: Option<String>,
|
||||
is_direct: Option<bool>,
|
||||
state: Option<MatrixRoomState>,
|
||||
avatar: RefCell<Option<Avatar>>,
|
||||
|
||||
invitations: RefCell<HashMap<UserId, Invitation>>,
|
||||
members: RefCell<HashMap<UserId, RoomMember>>,
|
||||
|
||||
spaces: Vec<SpaceId>,
|
||||
|
||||
messaging_provider: Option<Rc<dyn RoomMessagingProviderInterface>>,
|
||||
store: RefCell<Option<Rc<dyn RoomStoreProviderInterface>>>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
topic: Option<String>,
|
||||
is_direct: Option<bool>,
|
||||
state: Option<MatrixRoomState>,
|
||||
spaces: Vec<SpaceId>,
|
||||
) -> 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<dyn RoomMessagingProviderInterface>,
|
||||
) {
|
||||
self.messaging_provider = Some(messaging_provider);
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &OwnedRoomId {
|
||||
pub fn set_store(&self, store: Rc<dyn RoomStoreProviderInterface>) {
|
||||
*self.store.borrow_mut() = Some(store);
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &RoomId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &Option<String> {
|
||||
&self.name
|
||||
#[allow(dead_code)]
|
||||
pub fn name(&self) -> Option<String> {
|
||||
self.name.borrow().clone()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn topic(&self) -> &Option<String> {
|
||||
&self.topic
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_topic(&mut self, topic: Option<String>) {
|
||||
self.topic = topic;
|
||||
}
|
||||
|
||||
pub fn is_direct(&self) -> &Option<bool> {
|
||||
&self.is_direct
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn state(&self) -> &Option<MatrixRoomState> {
|
||||
&self.state
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_invited(&self) -> Option<bool> {
|
||||
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<Avatar> {
|
||||
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<Option<Avatar>> {
|
||||
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<Vec<u8>> = 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<OwnedRoomId, RefCell<Room>>;
|
||||
#[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<OwnedRoomId, RefCell<Room>>;
|
||||
self.add_invitation(invitation);
|
||||
|
||||
// #[derive(Clone)]
|
||||
// pub struct Room {
|
||||
// // pub matrix_room: Arc<MatrixRoom>,
|
||||
// pub topic: Option<RefCell<String>>,
|
||||
// pub members: HashMap<OwnedUserId, RoomMember>,
|
||||
// pub is_direct: Option<bool>,
|
||||
// // pub timeline: Arc<Timeline>,
|
||||
// }
|
||||
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<MatrixRoom>,
|
||||
// topic: Option<RefCell<String>>,
|
||||
// is_direct: Option<bool>,
|
||||
// ) -> 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<String>) {
|
||||
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<String>) {
|
||||
trace!("on_new_name");
|
||||
}
|
||||
|
||||
// pub fn name(&self) -> Option<String> {
|
||||
// self.matrix_room.name()
|
||||
// }
|
||||
#[instrument(name = "Room", skip_all)]
|
||||
async fn on_new_avatar(&self, avatar: Option<Avatar>) {
|
||||
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<bool> {
|
||||
self.is_direct
|
||||
}
|
||||
|
||||
fn name(&self) -> Option<String> {
|
||||
self.name.borrow().clone()
|
||||
}
|
||||
|
||||
async fn avatar(&self) -> Option<Avatar> {
|
||||
self.get_avatar().await
|
||||
}
|
||||
|
||||
fn spaces(&self) -> &Vec<SpaceId> {
|
||||
&self.spaces
|
||||
}
|
||||
}
|
||||
|
93
src/domain/model/room_member.rs
Normal file
@@ -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<String>,
|
||||
avatar_url: Option<AvatarUrl>,
|
||||
room_id: RoomId,
|
||||
is_account_user: bool,
|
||||
|
||||
#[allow(dead_code)]
|
||||
avatar: RefCell<Option<Avatar>>,
|
||||
|
||||
messaging_provider: Rc<dyn MemberMessagingProviderInterface>,
|
||||
}
|
||||
|
||||
impl RoomMember {
|
||||
pub fn new(
|
||||
id: UserId,
|
||||
display_name: Option<String>,
|
||||
avatar_url: Option<AvatarUrl>,
|
||||
room_id: RoomId,
|
||||
is_account_user: bool,
|
||||
messaging_provider: Rc<dyn MemberMessagingProviderInterface>,
|
||||
) -> 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<String> {
|
||||
&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<Avatar> {
|
||||
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()
|
||||
}
|
||||
}
|
107
src/domain/model/space.rs
Normal file
@@ -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<Option<String>>,
|
||||
topic: RefCell<Option<String>>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
avatar: RefCell<Option<Avatar>>,
|
||||
|
||||
children: RefCell<HashSet<RoomId>>, // We don´t expect to manage nested spaces
|
||||
|
||||
messaging_provider: Option<Rc<dyn SpaceMessagingProviderInterface>>,
|
||||
store: RefCell<Option<Rc<dyn SpaceStoreProviderInterface>>>,
|
||||
}
|
||||
|
||||
impl PartialEq for Space {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Space {
|
||||
pub fn new(id: SpaceId, name: Option<String>, topic: Option<String>) -> 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<dyn SpaceMessagingProviderInterface>) {
|
||||
self.messaging_provider = Some(provider);
|
||||
}
|
||||
|
||||
pub fn set_store(&self, store: Rc<dyn SpaceStoreProviderInterface>) {
|
||||
*self.store.borrow_mut() = Some(store);
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &SpaceId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn name(&self) -> Option<String> {
|
||||
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<String>) {
|
||||
trace!("on_new_topic");
|
||||
*self.topic.borrow_mut() = topic;
|
||||
}
|
||||
|
||||
#[instrument(name = "Space", skip_all)]
|
||||
async fn on_new_name(&self, name: Option<String>) {
|
||||
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<String> {
|
||||
self.name.borrow().clone()
|
||||
}
|
||||
}
|
53
src/domain/model/store_interface.rs
Normal file
@@ -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<dyn RoomStoreConsumerInterface>,
|
||||
) -> Rc<dyn RoomStoreProviderInterface>;
|
||||
fn on_new_space(
|
||||
&self,
|
||||
space: Rc<dyn SpaceStoreConsumerInterface>,
|
||||
) -> Rc<dyn SpaceStoreProviderInterface>;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait RoomStoreConsumerInterface {
|
||||
fn id(&self) -> &RoomId;
|
||||
fn is_direct(&self) -> Option<bool>;
|
||||
fn name(&self) -> Option<String>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn avatar(&self) -> Option<Avatar>;
|
||||
|
||||
fn spaces(&self) -> &Vec<SpaceId>;
|
||||
}
|
||||
|
||||
pub trait RoomStoreProviderInterface {
|
||||
fn on_new_name(&self, name: Option<String>);
|
||||
fn on_new_avatar(&self, avatar: Option<Avatar>);
|
||||
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<String>;
|
||||
}
|
||||
|
||||
pub trait SpaceStoreProviderInterface {
|
||||
fn set_name(&self, _name: Option<String>) {}
|
||||
}
|
63
src/infrastructure/messaging/matrix/account_event.rs
Normal file
@@ -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<SpaceId>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<bool>,
|
||||
RoomState,
|
||||
RoomEventsReceiver,
|
||||
Sender<bool>,
|
||||
Span,
|
||||
),
|
||||
|
||||
NewSpace(
|
||||
OwnedRoomId,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
RoomEventsReceiver,
|
||||
Sender<bool>,
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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<Receiver<RoomEvent>>,
|
||||
pub struct Requester {
|
||||
worker_tasks_sender: UnboundedSender<WorkerTask>,
|
||||
}
|
||||
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<WorkerTask>) -> Self {
|
||||
Self {
|
||||
worker_tasks_sender,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Requester {
|
||||
pub matrix_client: Arc<MatrixClient>,
|
||||
pub tx: UnboundedSender<WorkerTask>,
|
||||
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<dyn RoomMessagingConsumerInterface>,
|
||||
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<dyn RoomMessagingConsumerInterface>,
|
||||
room_id: OwnedRoomId,
|
||||
user_id: OwnedUserId,
|
||||
user_name: Option<String>,
|
||||
avatar_url: Option<OwnedMxcUri>,
|
||||
is_account_user: bool,
|
||||
messaging_provider: Rc<dyn MemberMessagingProviderInterface>,
|
||||
) {
|
||||
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<dyn RoomMessagingConsumerInterface>,
|
||||
topic: Option<String>,
|
||||
) {
|
||||
consumer.on_new_topic(topic).await;
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn on_room_new_name(
|
||||
consumer: &Rc<dyn RoomMessagingConsumerInterface>,
|
||||
name: Option<String>,
|
||||
) {
|
||||
consumer.on_new_name(name).await;
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn on_room_new_avatar(
|
||||
consumer: &Rc<dyn RoomMessagingConsumerInterface>,
|
||||
avatar: Option<Avatar>,
|
||||
) {
|
||||
consumer.on_new_avatar(avatar).await;
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn on_space_new_child(
|
||||
consumer: &Rc<dyn SpaceMessagingConsumerInterface>,
|
||||
child_id: RoomId,
|
||||
) {
|
||||
// TODO: Make name consistent
|
||||
consumer.on_child(child_id).await;
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn on_space_new_topic(
|
||||
consumer: &Rc<dyn SpaceMessagingConsumerInterface>,
|
||||
topic: Option<String>,
|
||||
) {
|
||||
consumer.on_new_topic(topic).await;
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn on_space_new_name(
|
||||
consumer: &Rc<dyn SpaceMessagingConsumerInterface>,
|
||||
name: Option<String>,
|
||||
) {
|
||||
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<dyn SpaceMessagingConsumerInterface>,
|
||||
// space_id: OwnedRoomId,
|
||||
// avatar: Option<Avatar>,
|
||||
// ) {
|
||||
// consumer.on_new_avatar(avatar).await;
|
||||
// }
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl AccountMessagingProviderInterface for Requester {
|
||||
async fn get_display_name(&self) -> anyhow::Result<Option<String>> {
|
||||
request_to_worker!(self, WorkerTask::GetDisplayName)
|
||||
}
|
||||
|
||||
async fn get_avatar(&self) -> anyhow::Result<Option<Avatar>> {
|
||||
request_to_worker!(self, WorkerTask::GetAvatar)
|
||||
}
|
||||
|
||||
async fn run_forever(
|
||||
&self,
|
||||
account_events_consumer: &dyn AccountMessagingConsumerInterface,
|
||||
mut account_events_receiver: Receiver<AccountEvent>,
|
||||
) -> 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::<RoomId, Rc<dyn RoomMessagingConsumerInterface>>::new();
|
||||
let mut space_events_consumers =
|
||||
HashMap::<RoomId, Rc<dyn SpaceMessagingConsumerInterface>>::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<Option<Avatar>> {
|
||||
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<AvatarUrl>,
|
||||
room_id: RoomId,
|
||||
user_id: UserId,
|
||||
) -> anyhow::Result<Option<Avatar>> {
|
||||
request_to_worker!(
|
||||
self,
|
||||
WorkerTask::GetRoomMemberAvatar,
|
||||
avatar_url,
|
||||
room_id,
|
||||
user_id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
71
src/infrastructure/messaging/matrix/room_event.rs
Normal file
@@ -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<String>, Option<OwnedMxcUri>, bool, Span),
|
||||
|
||||
NewTopic(Option<String>, Span),
|
||||
NewName(Option<String>, Span),
|
||||
NewAvatar(Option<Avatar>, 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<RoomEvent>);
|
||||
|
||||
impl Clone for RoomEventsReceiver {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.resubscribe())
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomEventsReceiver {
|
||||
pub fn new(inner: Receiver<RoomEvent>) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomEventsReceiver> for Receiver<RoomEvent> {
|
||||
fn from(val: RoomEventsReceiver) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
@@ -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<EditInfo>),
|
||||
Init(Sender<anyhow::Result<()>>),
|
||||
Login(LoginStyle, Sender<anyhow::Result<()>>),
|
||||
RunForever(Sender<()>),
|
||||
GetDisplayName(Sender<anyhow::Result<Option<String>>>),
|
||||
GetAvatar(Sender<anyhow::Result<Option<Vec<u8>>>>),
|
||||
|
||||
GetRoomAvatar(OwnedRoomId, Sender<anyhow::Result<Option<Vec<u8>>>>),
|
||||
GetRoomMemberAvatar(
|
||||
Option<OwnedMxcUri>,
|
||||
OwnedRoomId,
|
||||
OwnedUserId,
|
||||
Sender<anyhow::Result<Option<Vec<u8>>>>,
|
||||
),
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1 +1,2 @@
|
||||
pub(crate) mod mozaik_builder;
|
||||
pub(crate) mod random_svg_generators;
|
||||
|
116
src/infrastructure/services/mozaik_builder.rs
Normal file
@@ -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<u8>) -> Option<DynamicImage> {
|
||||
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<u8>],
|
||||
padding_image: &Option<Vec<u8>>,
|
||||
) -> Vec<u8> {
|
||||
let placeholder = DynamicImage::new_rgb8(128, 128);
|
||||
|
||||
let images: Vec<Option<DynamicImage>> = 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<u8> = Vec::new();
|
||||
|
||||
let mut allocations: Vec<&Option<DynamicImage>> = 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<Vec<u8>>,
|
||||
padding_image: Option<Vec<u8>>,
|
||||
) -> Vec<u8> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
@@ -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<String> {
|
||||
|
||||
async fn fetch_dicebear_svg(
|
||||
r#type: &DicebearType,
|
||||
req_fields: &Vec<String>,
|
||||
req_fields: &[String],
|
||||
placeholder_fetcher: Option<Box<impl Future<Output = Option<String>>>>,
|
||||
) -> 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<impl Future<Output = Option<String>>> {
|
||||
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<impl Future<Output = Option<String>>> {
|
||||
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<impl Future<Output = Option<String>>> {
|
||||
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<impl Future<Output = Option<String>>> {
|
||||
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<impl Future<Output = Option<String>>> {
|
||||
Box::new(async move { None })
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_random_svg_avatar<'a>(config: Option<&'a AvatarConfig<'a>>) -> String {
|
||||
|
197
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<bool>, session: &GlobalSignal<Session>) {
|
||||
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#"
|
||||
// <style type="text/css">
|
||||
// html, body {
|
||||
// height: 100%;
|
||||
// width: 100%;
|
||||
|
||||
// margin: 0;
|
||||
// }
|
||||
// #main, #bodywrap {
|
||||
// height: 100%;
|
||||
// width: 100%;
|
||||
// }
|
||||
// </style>
|
||||
// "#
|
||||
// .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);
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -77,7 +77,7 @@ pub struct ButtonProps {
|
||||
children: Element,
|
||||
}
|
||||
|
||||
fn Button(props: ButtonProps) -> Element {
|
||||
pub fn Button(props: ButtonProps) -> Element {
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
|
||||
|
@@ -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;
|
||||
|
18
src/ui/components/chat_panel.rs
Normal file
@@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
6
src/ui/components/chat_panel.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
@import "../_base.scss";
|
||||
@import "./_panel.scss";
|
||||
|
||||
.chat-panel {
|
||||
@include panel();
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 {}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
"🔎"
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@@ -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%;
|
||||
}
|
||||
}
|
@@ -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<Tasks>,
|
||||
receiver: RefCell<Receiver<Tasks>>,
|
||||
}
|
||||
|
||||
impl Interface {
|
||||
pub fn new() -> Self {
|
||||
let (sender, receiver) = channel::<Tasks>(32);
|
||||
Self {
|
||||
sender,
|
||||
receiver: RefCell::new(receiver),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn receiver(&self) -> &RefCell<Receiver<Tasks>> {
|
||||
&self.receiver
|
||||
}
|
||||
|
||||
pub fn toggle_room(&self, room_id: OwnedRoomId) -> Result<usize, SendError<Tasks>> {
|
||||
self.sender.send(Tasks::ToggleRoom(room_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Interface {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
@@ -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<Interface>,
|
||||
}
|
||||
|
||||
fn render_rooms_tabs(
|
||||
by_id_rooms: &GlobalSignal<HashMap<OwnedRoomId, RefCell<Room>>>,
|
||||
displayed_room_ids: Signal<HashSet<OwnedRoomId>>,
|
||||
) -> Vec<Element> {
|
||||
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<HashMap<OwnedRoomId, RefCell<Room>>>,
|
||||
displayed_room_ids: Signal<HashSet<OwnedRoomId>>,
|
||||
) -> Vec<Element> {
|
||||
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<Receiver<Tasks>>,
|
||||
mut displayed_room_ids: Signal<HashSet<OwnedRoomId>>,
|
||||
) {
|
||||
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::<OwnedRoomId>::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()},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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)},
|
||||
},
|
||||
}
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
@import "../../_base.scss"
|
||||
|
||||
.contacts {
|
||||
height: 72%;
|
||||
background-color: white;
|
||||
}
|
@@ -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<ByIdRooms>,
|
||||
) -> Vec<RefCell<Room>> {
|
||||
let by_id_rooms = by_id_rooms.read();
|
||||
|
||||
let mut filtered_rooms = Vec::<RefCell<Room>>::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<ByIdRooms>,
|
||||
) -> Vec<RefCell<Room>> {
|
||||
let by_id_rooms = by_id_rooms.read();
|
||||
|
||||
let mut filtered_rooms = Vec::<RefCell<Room>>::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<ChatsWindowInterface>) {
|
||||
let _ = chats_window_interface.read().toggle_room(room_id.clone());
|
||||
}
|
||||
|
||||
#[derive(Props, Clone)]
|
||||
pub struct ContactsSectionProps {
|
||||
name: String,
|
||||
filter: Rc<dyn Fn(&GlobalSignal<ByIdRooms>) -> Vec<RefCell<Room>>>,
|
||||
}
|
||||
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()},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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%;
|
||||
}
|
||||
}
|
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@@ -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 = "<Enter a personal message>";
|
||||
|
||||
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<bool> = 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 {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
504
src/ui/components/conversations.rs
Normal file
@@ -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<Vec<u8>>, class_name: Option<String>) -> 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<DomainPresenceState>, class_name: Option<String>) -> 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<String>, class_name: Option<String>) -> Element {
|
||||
match display_name {
|
||||
Some(display_name) => {
|
||||
rsx! {
|
||||
div {
|
||||
class: class_name,
|
||||
p {
|
||||
{display_name},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Status(status: Option<String>, class_name: Option<String>) -> 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<RoomId>) -> 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::<Signal<Option<RoomId>>>();
|
||||
|
||||
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<RoomId>) -> Element {
|
||||
let mut ordered_rooms = use_signal(Vec::<RoomId>::new);
|
||||
|
||||
use_effect(move || {
|
||||
let rooms = use_context::<Signal<Vec<RoomId>>>();
|
||||
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::<RoomId>));
|
||||
let mut displayed_rooms = use_context_provider(|| Signal::new(Vec::<RoomId>::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::<RoomId>));
|
||||
let mut displayed_rooms = use_context_provider(|| Signal::new(Vec::<RoomId>::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 {},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
305
src/ui/components/conversations.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
@@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
.root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: white;
|
||||
}
|
||||
}
|
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
.down-arrow-icon {
|
||||
.transparent-icon {
|
||||
color: transparent;
|
||||
|
||||
path:last-child {
|
||||
|
@@ -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 {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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");
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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,
|
||||
|
@@ -67,7 +67,7 @@ $modal-max-height: 55vh;
|
||||
border-radius: $border-radius;
|
||||
|
||||
&__placeholder {
|
||||
width: calc(100% + (2 * $border-normal-width));
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ turf::style_sheet!("src/ui/components/text_input.scss");
|
||||
pub trait InputPropsData {}
|
||||
|
||||
#[derive(Props, Clone, PartialEq)]
|
||||
pub struct InputProps<D: InputPropsData + 'static + std::cmp::PartialEq> {
|
||||
pub struct InputProps<D: InputPropsData + 'static + std::cmp::PartialEq + std::clone::Clone> {
|
||||
value: Option<String>,
|
||||
placeholder: Option<String>,
|
||||
oninput: Option<EventHandler<Event<FormData>>>,
|
||||
|
@@ -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 {
|
||||
|
252
src/ui/layouts/conversations.rs
Normal file
@@ -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<MountedData>,
|
||||
first_div: &Rc<MountedData>,
|
||||
last_div: &Rc<MountedData>,
|
||||
) {
|
||||
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::<Rc<MountedData>>);
|
||||
let mut first_div = use_signal(|| None::<Rc<MountedData>>);
|
||||
let mut last_div = use_signal(|| None::<Rc<MountedData>>);
|
||||
|
||||
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<MountedData>| 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::<Rc<MountedData>>);
|
||||
let mut first_div = use_signal(|| None::<Rc<MountedData>>);
|
||||
let mut last_div = use_signal(|| None::<Rc<MountedData>>);
|
||||
|
||||
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<MountedData>| 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}
|
||||
}
|
||||
}
|
||||
}
|
136
src/ui/layouts/conversations.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 {}
|
||||
}
|
||||
}
|
||||
}
|
2
src/ui/layouts/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod conversations;
|
||||
pub(crate) mod login;
|
@@ -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<Store> = Signal::global(Store::new);
|
||||
// TODO: Merge ACCOUNT and SESSION
|
||||
pub static ACCOUNT: GlobalSignal<Account> = Signal::global(|| Account::new(&STORE));
|
||||
pub static SESSION: GlobalSignal<Session> = Signal::global(Session::new);
|
||||
|
62
src/ui/store/mod.rs
Normal file
@@ -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<RoomId, Rc<Room>>,
|
||||
spaces: HashMap<SpaceId, Rc<Space>>,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl AccountStoreProviderInterface for GlobalSignal<Store> {
|
||||
fn on_new_room(
|
||||
&self,
|
||||
domain_room: Rc<dyn RoomStoreConsumerInterface>,
|
||||
) -> Rc<dyn RoomStoreProviderInterface> {
|
||||
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<dyn SpaceStoreConsumerInterface>,
|
||||
) -> Rc<dyn SpaceStoreProviderInterface> {
|
||||
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
|
||||
}
|
||||
}
|
88
src/ui/store/room.rs
Normal file
@@ -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<bool>,
|
||||
name: Option<String>,
|
||||
avatar: Option<Avatar>,
|
||||
members: Vec<RoomMember>,
|
||||
invitations: Vec<Invitation>,
|
||||
is_invited: bool,
|
||||
|
||||
spaces: Vec<SpaceId>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Room {
|
||||
store: RefCell<Store>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
domain: Rc<dyn RoomStoreConsumerInterface>,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
pub fn signal(&self) -> Store {
|
||||
*self.store.borrow()
|
||||
}
|
||||
|
||||
pub fn from_domain(room: Rc<dyn RoomStoreConsumerInterface>) -> 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<Avatar> {
|
||||
self.domain.avatar().await
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomStoreProviderInterface for Room {
|
||||
fn on_new_name(&self, name: Option<String>) {
|
||||
let mut store = self.store.borrow_mut();
|
||||
store.name.set(name);
|
||||
}
|
||||
|
||||
fn on_new_avatar(&self, avatar: Option<Avatar>) {
|
||||
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);
|
||||
}
|
||||
}
|
53
src/ui/store/space.rs
Normal file
@@ -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<String>,
|
||||
room_ids: HashSet<RoomId>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Space {
|
||||
store: RefCell<Store>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
domain: Rc<dyn SpaceStoreConsumerInterface>,
|
||||
}
|
||||
|
||||
impl Space {
|
||||
pub fn signal(&self) -> Store {
|
||||
*self.store.borrow()
|
||||
}
|
||||
|
||||
pub fn from_domain(space: Rc<dyn SpaceStoreConsumerInterface>) -> 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<String>) {
|
||||
let mut store = self.store.borrow_mut();
|
||||
store.name.set(name);
|
||||
}
|
||||
}
|
@@ -1 +0,0 @@
|
||||
pub(crate) mod login_view;
|
@@ -8,6 +8,7 @@ impl<T> Receiver<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Sender<T>(_Sender<T>);
|
||||
|
||||
// TODO: Handle error
|
||||
|