Compare commits
6 Commits
8ed4ff3f2a
...
89473cfd61
Author | SHA1 | Date | |
---|---|---|---|
89473cfd61
|
|||
62015f8d13
|
|||
c8e8e2da67
|
|||
df32faa8e6
|
|||
5194899de0
|
|||
ff0ac7f982
|
71
src/base.rs
71
src/base.rs
@@ -1,71 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
use futures_util::stream::StreamExt;
|
|
||||||
use tracing::{debug, error, warn};
|
|
||||||
|
|
||||||
use crate::domain::model::account::Account;
|
|
||||||
use crate::domain::model::messaging_interface::AccountMessagingProviderInterface;
|
|
||||||
use crate::domain::model::session::Session;
|
|
||||||
use crate::infrastructure::messaging::matrix::client::Client;
|
|
||||||
use crate::infrastructure::messaging::matrix::worker_tasks::LoginStyle;
|
|
||||||
use crate::ui::store::Store;
|
|
||||||
|
|
||||||
pub async fn login(mut rx: UnboundedReceiver<bool>, session: &GlobalSignal<Session>) {
|
|
||||||
while let Some(is_logged) = rx.next().await {
|
|
||||||
error!("is_logged={is_logged}");
|
|
||||||
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 (requester, account_events_receiver) =
|
|
||||||
Client::spawn(homeserver_url.unwrap()).await;
|
|
||||||
|
|
||||||
if let Err(err) = requester.init().await {
|
|
||||||
error!("Following error occureds during client init: {}", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
error!("Before login");
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
error!("=== LOGIN END ===");
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
@@ -52,6 +52,7 @@ impl Account {
|
|||||||
self.messaging_provider = Some(provider.clone());
|
self.messaging_provider = Some(provider.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn get_room(&self, room_id: &RoomId) -> Option<Rc<Room>> {
|
pub fn get_room(&self, room_id: &RoomId) -> Option<Rc<Room>> {
|
||||||
self.by_id_rooms.borrow().get(room_id).cloned()
|
self.by_id_rooms.borrow().get(room_id).cloned()
|
||||||
}
|
}
|
||||||
|
@@ -118,6 +118,7 @@ impl Room {
|
|||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn name(&self) -> Option<String> {
|
pub fn name(&self) -> Option<String> {
|
||||||
self.name.borrow().clone()
|
self.name.borrow().clone()
|
||||||
}
|
}
|
||||||
|
@@ -65,6 +65,7 @@ impl Space {
|
|||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn name(&self) -> Option<String> {
|
pub fn name(&self) -> Option<String> {
|
||||||
self.name.borrow().clone()
|
self.name.borrow().clone()
|
||||||
}
|
}
|
||||||
|
@@ -28,7 +28,10 @@ pub trait RoomStoreConsumerInterface {
|
|||||||
fn id(&self) -> &RoomId;
|
fn id(&self) -> &RoomId;
|
||||||
fn is_direct(&self) -> Option<bool>;
|
fn is_direct(&self) -> Option<bool>;
|
||||||
fn name(&self) -> Option<String>;
|
fn name(&self) -> Option<String>;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
async fn avatar(&self) -> Option<Avatar>;
|
async fn avatar(&self) -> Option<Avatar>;
|
||||||
|
|
||||||
fn spaces(&self) -> &Vec<SpaceId>;
|
fn spaces(&self) -> &Vec<SpaceId>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
131
src/main.rs
131
src/main.rs
@@ -8,12 +8,22 @@ mod infrastructure;
|
|||||||
mod ui;
|
mod ui;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use std::rc::Rc;
|
||||||
use tracing::{debug, error};
|
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use futures_util::stream::StreamExt;
|
||||||
|
use tracing::{debug, error, warn};
|
||||||
use tracing_forest::ForestLayer;
|
use tracing_forest::ForestLayer;
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::{prelude::*, EnvFilter};
|
||||||
use tracing_subscriber::EnvFilter;
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
cfg_if! {
|
cfg_if! {
|
||||||
if #[cfg(target_family = "wasm")] {
|
if #[cfg(target_family = "wasm")] {
|
||||||
@@ -26,80 +36,65 @@ cfg_if! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::base::{login, sync_rooms};
|
async fn login(mut rx: UnboundedReceiver<bool>, session: &GlobalSignal<Session>) {
|
||||||
use crate::base::{APP_SETTINGS, ROOMS, SESSION};
|
while let Some(is_logged) = rx.next().await {
|
||||||
use crate::ui::components::login::Login;
|
if !is_logged {
|
||||||
use crate::ui::components::main_window::MainWindow;
|
let homeserver_url = session.read().homeserver_url.clone();
|
||||||
use crate::ui::layouts::login::Login;
|
let username = session.read().username.clone();
|
||||||
|
let password = session.read().password.clone();
|
||||||
|
|
||||||
mod base;
|
if homeserver_url.is_some() && username.is_some() && password.is_some() {
|
||||||
|
let (requester, account_events_receiver) =
|
||||||
|
Client::spawn(homeserver_url.unwrap()).await;
|
||||||
|
|
||||||
|
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 {
|
fn app() -> Element {
|
||||||
debug!("*** App rendering ***");
|
let login_coro = use_coroutine(|rx| login(rx, &SESSION));
|
||||||
|
|
||||||
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)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !SESSION.read().is_logged {
|
if !SESSION.read().is_logged {
|
||||||
login_coro.send(false);
|
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 {
|
if SESSION.read().is_logged {
|
||||||
debug!("Should render the MainWindow component");
|
|
||||||
rsx! {
|
rsx! {
|
||||||
MainWindow {},
|
Conversations {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rsx! {
|
rsx! {
|
||||||
|
@@ -174,6 +174,9 @@ $border-big-width: 4px;
|
|||||||
$border-big: solid $border-big-width $border-default-color;
|
$border-big: solid $border-big-width $border-default-color;
|
||||||
$border-normal-width: 2px;
|
$border-normal-width: 2px;
|
||||||
$border-normal: solid $border-normal-width $border-default-color;
|
$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).
|
// TODO: Radius should be a percentage(eg: 1024/16px).
|
||||||
$border-radius: 16px;
|
$border-radius: 16px;
|
||||||
|
18
src/ui/components/chat_panel.rs
Normal file
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
6
src/ui/components/chat_panel.scss
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@import "../_base.scss";
|
||||||
|
@import "./_panel.scss";
|
||||||
|
|
||||||
|
.chat-panel {
|
||||||
|
@include panel();
|
||||||
|
}
|
504
src/ui/components/conversations.rs
Normal file
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
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,7 +1,4 @@
|
|||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, cell::RefCell, collections::HashMap, rc::Rc};
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use const_format::formatcp;
|
use const_format::formatcp;
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
@@ -9,16 +6,18 @@ use tracing::{debug, error, warn};
|
|||||||
use validator::{Validate, ValidateArgs, ValidateEmail, ValidationError, ValidationErrors};
|
use validator::{Validate, ValidateArgs, ValidateEmail, ValidationError, ValidationErrors};
|
||||||
use zxcvbn::zxcvbn;
|
use zxcvbn::zxcvbn;
|
||||||
|
|
||||||
use crate::base::SESSION;
|
use crate::{
|
||||||
use crate::domain::model::session::Session;
|
domain::model::session::Session,
|
||||||
use crate::infrastructure::services::random_svg_generators::{
|
infrastructure::services::random_svg_generators::{generate_random_svg_shape, ShapeConfig},
|
||||||
generate_random_svg_shape, ShapeConfig,
|
ui::SESSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::button::{LoginButton, RegisterButton};
|
use super::{
|
||||||
use super::modal::{Modal, Severity};
|
button::{LoginButton, RegisterButton},
|
||||||
use super::spinner::Spinner;
|
modal::{Modal, Severity},
|
||||||
use super::text_input::{PasswordInputState, PasswordTextInput, TextInput, TextInputState};
|
spinner::Spinner,
|
||||||
|
text_input::{PasswordInputState, PasswordTextInput, TextInput, TextInputState},
|
||||||
|
};
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/style_vars.rs"));
|
include!(concat!(env!("OUT_DIR"), "/style_vars.rs"));
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ turf::style_sheet!("src/ui/components/text_input.scss");
|
|||||||
pub trait InputPropsData {}
|
pub trait InputPropsData {}
|
||||||
|
|
||||||
#[derive(Props, Clone, PartialEq)]
|
#[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>,
|
value: Option<String>,
|
||||||
placeholder: Option<String>,
|
placeholder: Option<String>,
|
||||||
oninput: Option<EventHandler<Event<FormData>>>,
|
oninput: Option<EventHandler<Event<FormData>>>,
|
||||||
|
252
src/ui/layouts/conversations.rs
Normal file
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
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 +1,2 @@
|
|||||||
|
pub(crate) mod conversations;
|
||||||
pub(crate) mod login;
|
pub(crate) mod login;
|
||||||
|
@@ -1,3 +1,13 @@
|
|||||||
pub(crate) mod components;
|
pub(crate) mod components;
|
||||||
pub(crate) mod layouts;
|
pub(crate) mod layouts;
|
||||||
pub(crate) mod store;
|
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);
|
||||||
|
@@ -30,6 +30,8 @@ pub struct Store {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Room {
|
pub struct Room {
|
||||||
store: RefCell<Store>,
|
store: RefCell<Store>,
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
domain: Rc<dyn RoomStoreConsumerInterface>,
|
domain: Rc<dyn RoomStoreConsumerInterface>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn get_avatar(&self) -> Option<Avatar> {
|
pub async fn get_avatar(&self) -> Option<Avatar> {
|
||||||
self.domain.avatar().await
|
self.domain.avatar().await
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,8 @@ pub struct Store {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Space {
|
pub struct Space {
|
||||||
store: RefCell<Store>,
|
store: RefCell<Store>,
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
domain: Rc<dyn SpaceStoreConsumerInterface>,
|
domain: Rc<dyn SpaceStoreConsumerInterface>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user