Merge branch 'conversations-panel' into develop
This commit is contained in:
19
Cargo.toml
19
Cargo.toml
@@ -40,33 +40,38 @@ tracing = "0.1.40"
|
||||
tracing-forest = "0.1.6"
|
||||
|
||||
# SCSS -> CSS + usage in rust code
|
||||
turf = "0.8.0"
|
||||
turf = "0.9.3"
|
||||
|
||||
# Dioxus
|
||||
dioxus = { version = "0.5", default-features = false }
|
||||
dioxus-free-icons = { version = "0.8", features = ["ionicons", "font-awesome-solid"] }
|
||||
# dioxus-free-icons = { version = "0.8", features = ["ionicons", "font-awesome-solid"] }
|
||||
dioxus-free-icons = { git = "https://github.com/ASR-ASU/dioxus-free-icons.git", branch = "asr/dioxus-0.6", features = ["ionicons", "font-awesome-solid"] }
|
||||
modx = "0.1.2"
|
||||
|
||||
[patch.crates-io]
|
||||
dioxus = { git = "https://github.com/DioxusLabs/dioxus.git" }
|
||||
# Matrix rich text editor
|
||||
wysiwyg = { path = "../matrix.org/matrix-rich-text-editor/crates/wysiwyg/" }
|
||||
|
||||
[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"] }
|
||||
dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", branch = "main", 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"] }
|
||||
dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", branch = "main", features = ["desktop"] }
|
||||
|
||||
# Matrix
|
||||
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", default-features = false, features = ["rustls-tls"] }
|
||||
|
||||
|
128
build.rs
128
build.rs
@@ -1,4 +1,5 @@
|
||||
use std::env;
|
||||
use std::fmt::Display;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::io::{self, BufRead};
|
||||
@@ -10,14 +11,32 @@ use regex::Regex;
|
||||
fn main() {
|
||||
// Tell Cargo to rerun this build script if any SCSS file
|
||||
// in the 'src' directory or its subdirectories changes.
|
||||
println!("cargo:rerun-if-changed=src/**/*.scss");
|
||||
println!("cargo:rerun-if-changed=src/ui/**/*.scss");
|
||||
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
|
||||
let style_src_path = PathBuf::from("src/ui/_base.scss");
|
||||
let style_dst_path = Path::new(&out_dir).join("style_vars.rs");
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
export_color_variables(&style_src_path, &style_dst_path)
|
||||
// Global tokens
|
||||
tasks.push(Task::new(
|
||||
PathBuf::from("src/ui/_base.scss"),
|
||||
Path::new(&out_dir).join("style_tokens.rs"),
|
||||
"style".to_string(),
|
||||
));
|
||||
// variables defined by the Panel component
|
||||
tasks.push(Task::new(
|
||||
PathBuf::from("src/ui/components/_panel.scss"),
|
||||
Path::new(&out_dir).join("style_component_panel.rs"),
|
||||
"panel".to_string(),
|
||||
));
|
||||
// Variables set by the Conversations layout
|
||||
tasks.push(Task::new(
|
||||
PathBuf::from("src/ui/layouts/conversations.scss"),
|
||||
Path::new(&out_dir).join("style_layout_conversations.rs"),
|
||||
"conversations".to_string(),
|
||||
));
|
||||
|
||||
export_variables(tasks)
|
||||
}
|
||||
|
||||
// From https://doc.rust-lang.org/rust-by-example/std_misc/file/read_lines.html
|
||||
@@ -32,14 +51,21 @@ where
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CssColorVariable<'a> {
|
||||
name: &'a str,
|
||||
value: &'a str,
|
||||
struct ColorVariable {
|
||||
name: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl<'a> CssColorVariable<'a> {
|
||||
pub fn to_rust(&self) -> String {
|
||||
format!(
|
||||
impl ColorVariable {
|
||||
pub fn new(name: String, value: String) -> Self {
|
||||
Self { name, value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ColorVariable {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"const {name}: &str = \"{value}\";",
|
||||
name = self.name.replace('-', "_").to_uppercase(),
|
||||
value = self.value
|
||||
@@ -47,35 +73,83 @@ impl<'a> CssColorVariable<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn export_color_variables(src_path: &PathBuf, dst_path: &PathBuf) {
|
||||
let mut dst_file = File::create(dst_path).unwrap();
|
||||
if let Err(err) = dst_file.write(b"#[allow(dead_code)]\nmod style {") {
|
||||
#[derive(Debug)]
|
||||
struct FloatVariable {
|
||||
name: String,
|
||||
value: f64,
|
||||
}
|
||||
|
||||
impl FloatVariable {
|
||||
pub fn new(name: String, value: f64) -> Self {
|
||||
Self { name, value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FloatVariable {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"const {name}: f64 = {value};",
|
||||
name = self.name.replace('-', "_").to_uppercase(),
|
||||
value = self.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Task {
|
||||
src_path: PathBuf,
|
||||
dst_path: PathBuf,
|
||||
module_name: String,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub fn new(src_path: PathBuf, dst_path: PathBuf, module_name: String) -> Self {
|
||||
Self {
|
||||
src_path,
|
||||
dst_path,
|
||||
module_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fn export_variables(src_path: &PathBuf, dst_path: &PathBuf) {
|
||||
fn export_variables(tasks: Vec<Task>) {
|
||||
let color_re = Regex::new(r"^\$([^:]+):[[:space:]]*#([^$]+);[[:space:]]*$").unwrap();
|
||||
let variable_re = Regex::new(r"^\$([^:]+):[[:space:]]*([^;]+)[[:space:]]*;").unwrap();
|
||||
|
||||
for task in tasks {
|
||||
let mut dst_file = File::create(task.dst_path).unwrap();
|
||||
if let Err(err) = dst_file.write_fmt(format_args!(
|
||||
"#[allow(dead_code)]\nmod {} {{\n",
|
||||
task.module_name
|
||||
)) {
|
||||
println!("{}", err);
|
||||
return;
|
||||
};
|
||||
|
||||
let re = Regex::new(r"^\$([^:]+):[[:space:]]*#([^$]+);[[:space:]]*$").unwrap();
|
||||
|
||||
if let Ok(lines) = read_lines(src_path) {
|
||||
let mut variables = Vec::<Box<dyn Display>>::new();
|
||||
if let Ok(lines) = read_lines(task.src_path) {
|
||||
for line in lines.map_while(Result::ok) {
|
||||
let Some(groups) = re.captures(&line) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(groups) = color_re.captures(&line) {
|
||||
let var = ColorVariable::new(groups[1].to_string(), groups[2].to_string());
|
||||
variables.push(Box::new(var));
|
||||
} else if let Some(groups) = variable_re.captures(&line) {
|
||||
if let Ok(value) = groups[2].parse::<f64>() {
|
||||
variables.push(Box::new(FloatVariable::new(groups[1].to_string(), value)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let var = CssColorVariable {
|
||||
name: &groups[1],
|
||||
value: &groups[2],
|
||||
};
|
||||
|
||||
let rust_export = var.to_rust();
|
||||
if let Err(err) = dst_file.write_fmt(format_args!(" pub {}\n", rust_export)) {
|
||||
for variable in variables {
|
||||
if let Err(err) = dst_file.write_fmt(format_args!(" pub {}\n", variable)) {
|
||||
println!("{}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = dst_file.write(b"}\n") {
|
||||
println!("{}", err);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="UTF-8" />
|
||||
{style_include}
|
||||
{script_include}
|
||||
</head>
|
||||
<body>
|
||||
<div id="main">
|
||||
|
@@ -44,6 +44,7 @@ pub trait RoomMessagingConsumerInterface {
|
||||
#[async_trait(?Send)]
|
||||
pub trait RoomMessagingProviderInterface {
|
||||
async fn get_avatar(&self, id: &RoomId) -> anyhow::Result<Option<Avatar>>;
|
||||
async fn join(&self, room_id: &RoomId) -> anyhow::Result<bool>;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
|
@@ -123,11 +123,6 @@ impl Room {
|
||||
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;
|
||||
@@ -308,6 +303,10 @@ impl RoomStoreConsumerInterface for Room {
|
||||
self.name.borrow().clone()
|
||||
}
|
||||
|
||||
fn topic(&self) -> Option<String> {
|
||||
self.topic.clone()
|
||||
}
|
||||
|
||||
async fn avatar(&self) -> Option<Avatar> {
|
||||
self.get_avatar().await
|
||||
}
|
||||
@@ -315,4 +314,10 @@ impl RoomStoreConsumerInterface for Room {
|
||||
fn spaces(&self) -> &Vec<SpaceId> {
|
||||
&self.spaces
|
||||
}
|
||||
|
||||
async fn join(&self) {
|
||||
if let Some(messaging_provider) = &self.messaging_provider {
|
||||
let _ = messaging_provider.join(&self.id).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -28,16 +28,18 @@ pub trait RoomStoreConsumerInterface {
|
||||
fn id(&self) -> &RoomId;
|
||||
fn is_direct(&self) -> Option<bool>;
|
||||
fn name(&self) -> Option<String>;
|
||||
fn topic(&self) -> Option<String>;
|
||||
fn spaces(&self) -> &Vec<SpaceId>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn avatar(&self) -> Option<Avatar>;
|
||||
|
||||
fn spaces(&self) -> &Vec<SpaceId>;
|
||||
async fn join(&self);
|
||||
}
|
||||
|
||||
pub trait RoomStoreProviderInterface {
|
||||
fn on_new_name(&self, name: Option<String>);
|
||||
fn on_new_avatar(&self, avatar: Option<Avatar>);
|
||||
fn on_new_topic(&self, topic: Option<String>);
|
||||
fn on_new_member(&self, member: RoomMember);
|
||||
fn on_invitation(&self, invitation: Invitation);
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ use dioxus::prelude::Task;
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
event_handler::Ctx,
|
||||
media::{MediaFormat, MediaRequest, MediaThumbnailSize},
|
||||
media::{MediaFormat, MediaRequest, MediaThumbnailSettings, MediaThumbnailSize},
|
||||
room::{ParentSpace, Room},
|
||||
ruma::{
|
||||
api::client::media::get_content_thumbnail::v3::Method,
|
||||
@@ -448,10 +448,13 @@ impl Client {
|
||||
async fn on_room_avatar_event(room: &Room, senders: &Ctx<Senders>) {
|
||||
let room_id = room.room_id();
|
||||
let avatar = match room
|
||||
.avatar(MediaFormat::Thumbnail(MediaThumbnailSize {
|
||||
.avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
|
||||
size: MediaThumbnailSize {
|
||||
method: Method::Scale,
|
||||
width: uint!(256),
|
||||
height: uint!(256),
|
||||
},
|
||||
animated: false,
|
||||
}))
|
||||
.await
|
||||
{
|
||||
@@ -668,10 +671,13 @@ impl Client {
|
||||
|
||||
match client
|
||||
.account()
|
||||
.get_avatar(MediaFormat::Thumbnail(MediaThumbnailSize {
|
||||
.get_avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
|
||||
size: MediaThumbnailSize {
|
||||
method: Method::Scale,
|
||||
width: uint!(256),
|
||||
height: uint!(256),
|
||||
},
|
||||
animated: false,
|
||||
}))
|
||||
.await
|
||||
{
|
||||
@@ -685,10 +691,13 @@ impl Client {
|
||||
|
||||
if let Some(room) = client.get_room(room_id) {
|
||||
match room
|
||||
.avatar(MediaFormat::Thumbnail(MediaThumbnailSize {
|
||||
.avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
|
||||
size: MediaThumbnailSize {
|
||||
method: Method::Scale,
|
||||
width: uint!(256),
|
||||
height: uint!(256),
|
||||
},
|
||||
animated: false,
|
||||
}))
|
||||
.await
|
||||
{
|
||||
@@ -709,10 +718,13 @@ impl Client {
|
||||
|
||||
let request = MediaRequest {
|
||||
source: MediaSource::Plain(media_url),
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSize {
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSettings {
|
||||
size: MediaThumbnailSize {
|
||||
method: Method::Scale,
|
||||
width: uint!(256),
|
||||
height: uint!(256),
|
||||
},
|
||||
animated: false,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -739,10 +751,13 @@ impl Client {
|
||||
Ok(room_member) => {
|
||||
if let Some(room_member) = room_member {
|
||||
let res = match room_member
|
||||
.avatar(MediaFormat::Thumbnail(MediaThumbnailSize {
|
||||
.avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
|
||||
size: MediaThumbnailSize {
|
||||
method: Method::Scale,
|
||||
width: uint!(256),
|
||||
height: uint!(256),
|
||||
},
|
||||
animated: false,
|
||||
}))
|
||||
.await
|
||||
{
|
||||
@@ -761,6 +776,19 @@ impl Client {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn join_room(&self, room_id: &RoomId) -> anyhow::Result<bool> {
|
||||
let client = self.client.as_ref().unwrap();
|
||||
|
||||
if let Some(room) = client.get_room(room_id) {
|
||||
return match room.join().await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(err) => Err(err.into()),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn work(&mut self, mut rx: UnboundedReceiver<WorkerTask>) {
|
||||
while let Some(task) = rx.recv().await {
|
||||
self.run(task).await;
|
||||
@@ -805,6 +833,9 @@ impl Client {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
WorkerTask::JoinRoom(id, reply) => {
|
||||
reply.send(self.join_room(&id).await).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -362,6 +362,10 @@ 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 fn join(&self, room_id: &RoomId) -> anyhow::Result<bool> {
|
||||
request_to_worker!(self, WorkerTask::JoinRoom, room_id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
|
@@ -23,6 +23,7 @@ pub enum WorkerTask {
|
||||
OwnedUserId,
|
||||
Sender<anyhow::Result<Option<Vec<u8>>>>,
|
||||
),
|
||||
JoinRoom(OwnedRoomId, Sender<anyhow::Result<bool>>),
|
||||
}
|
||||
|
||||
impl Debug for WorkerTask {
|
||||
@@ -61,6 +62,10 @@ impl Debug for WorkerTask {
|
||||
.field(room_id)
|
||||
.field(user_id)
|
||||
.finish(),
|
||||
WorkerTask::JoinRoom(room_id, _) => f
|
||||
.debug_tuple("WorkerTask::JoinRoom")
|
||||
.field(room_id)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,7 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use image::imageops::FilterType;
|
||||
use image::io::Reader;
|
||||
use image::{DynamicImage, ImageFormat};
|
||||
use image::{DynamicImage, ImageFormat, ImageReader};
|
||||
use image::{GenericImage, RgbImage};
|
||||
use tracing::{error, warn};
|
||||
|
||||
@@ -13,7 +12,7 @@ cfg_if! {
|
||||
}
|
||||
|
||||
fn from_raw_to_image(raw: &Vec<u8>) -> Option<DynamicImage> {
|
||||
match Reader::new(Cursor::new(raw)).with_guessed_format() {
|
||||
match ImageReader::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),
|
||||
|
12
src/main.rs
12
src/main.rs
@@ -13,7 +13,6 @@ 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};
|
||||
|
||||
use crate::{
|
||||
@@ -29,10 +28,12 @@ cfg_if! {
|
||||
if #[cfg(target_family = "wasm")] {
|
||||
use tracing_web::MakeWebConsoleWriter;
|
||||
} else {
|
||||
use dioxus::desktop::Config;
|
||||
use std::fs::File;
|
||||
|
||||
use dioxus::desktop::Config;
|
||||
use time::format_description::well_known::Iso8601;
|
||||
use tracing_subscriber::fmt::time::UtcTime;
|
||||
use tracing_forest::ForestLayer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,15 +91,12 @@ fn app() -> Element {
|
||||
|
||||
if !SESSION.read().is_logged {
|
||||
login_coro.send(false);
|
||||
}
|
||||
|
||||
if SESSION.read().is_logged {
|
||||
rsx! {
|
||||
Conversations {}
|
||||
Login {}
|
||||
}
|
||||
} else {
|
||||
rsx! {
|
||||
Login {},
|
||||
Conversations {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
@import "../base.scss";
|
||||
|
||||
$panel-aspect-ratio: 1/1.618;
|
||||
$aspect-ratio: 0.618; // 1/1.618;
|
||||
|
||||
@mixin panel($padding-v: 2%, $padding-h: 2%) {
|
||||
padding: $padding-v $padding-h;
|
||||
|
@@ -43,7 +43,7 @@ macro_rules! svg_text_button {
|
||||
($name:ident,$style:ident,$icon:ident) => {
|
||||
pub fn $name(props: ButtonProps) -> Element {
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
Button {
|
||||
id: props.id,
|
||||
@@ -79,7 +79,7 @@ pub struct ButtonProps {
|
||||
|
||||
pub fn Button(props: ButtonProps) -> Element {
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
button {
|
||||
id: props.id,
|
||||
@@ -96,8 +96,8 @@ pub fn Button(props: ButtonProps) -> Element {
|
||||
}
|
||||
},
|
||||
|
||||
{props.children},
|
||||
},
|
||||
{props.children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,12 @@ svg_text_button!(RegisterButton, REGISTER_BUTTON, RegisterText);
|
||||
svg_text_icon!(LoginText, "LOGIN");
|
||||
svg_text_button!(LoginButton, LOGIN_BUTTON, LoginText);
|
||||
|
||||
svg_text_icon!(JoinText, "JOIN");
|
||||
svg_text_button!(JoinButton, JOIN_BUTTON, JoinText);
|
||||
|
||||
svg_text_icon!(RejectText, "REJECT");
|
||||
svg_text_button!(RejectButton, REJECT_BUTTON, RejectText);
|
||||
|
||||
svg_text_icon!(SuccessText, "OK");
|
||||
svg_text_button!(SuccessButton, SUCCESS_BUTTON, SuccessText);
|
||||
|
||||
|
@@ -53,6 +53,14 @@
|
||||
@include button(secondary, 90);
|
||||
}
|
||||
|
||||
.join-button {
|
||||
@include button(secondary, 90);
|
||||
}
|
||||
|
||||
.reject-button {
|
||||
@include button(critical, 90);
|
||||
}
|
||||
|
||||
.success-button {
|
||||
@include button(success, 100);
|
||||
}
|
||||
|
@@ -1,12 +1,19 @@
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use dioxus::prelude::*;
|
||||
use tracing::{debug, trace, warn};
|
||||
use futures_util::StreamExt;
|
||||
use tracing::{debug, 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},
|
||||
components::{
|
||||
button::{JoinButton, RejectButton},
|
||||
icons::{ChatsIcon, LogoIcon, RoomsIcon, SpacesIcon},
|
||||
},
|
||||
hooks::use_long_press,
|
||||
ACCOUNT, STORE,
|
||||
},
|
||||
};
|
||||
@@ -15,19 +22,14 @@ 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! {
|
||||
if let Some(content) = content {
|
||||
div {
|
||||
class: class_name,
|
||||
background_image: format!("url(data:image/jpeg;base64,{encoded})")
|
||||
background_image: format!("url(data:image/jpeg;base64,{})", general_purpose::STANDARD.encode(content))
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Manage acount without avatar
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -48,11 +50,11 @@ fn PresenceState(state: Option<DomainPresenceState>, class_name: Option<String>)
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
LogoIcon {},
|
||||
LogoIcon {}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
None => VNode::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,12 +66,12 @@ fn DisplayName(display_name: Option<String>, class_name: Option<String>) -> Elem
|
||||
div {
|
||||
class: class_name,
|
||||
p {
|
||||
{display_name},
|
||||
{display_name}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
None => VNode::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,52 +133,53 @@ pub fn Account() -> Element {
|
||||
});
|
||||
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
|
||||
div {
|
||||
class: ClassName::ACCOUNT,
|
||||
|
||||
{avatar},
|
||||
{presence_state},
|
||||
{display_name},
|
||||
{avatar}
|
||||
{presence_state}
|
||||
{display_name}
|
||||
|
||||
{status},
|
||||
{status}
|
||||
|
||||
div {
|
||||
class: ClassName::ACCOUNT_SPACES,
|
||||
Button {
|
||||
SpacesIcon {},
|
||||
SpacesIcon {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::ACCOUNT_CHAT,
|
||||
Button {
|
||||
ChatsIcon {},
|
||||
ChatsIcon {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::ACCOUNT_ROOM,
|
||||
Button {
|
||||
RoomsIcon {},
|
||||
RoomsIcon {}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ConversationAvatar(room_id: RoomId, on_clicked: EventHandler<RoomId>) -> Element {
|
||||
pub fn ConversationAvatar(
|
||||
room_id: RoomId,
|
||||
on_selected: Option<EventHandler<RoomId>>,
|
||||
on_pressed: Option<EventHandler<RoomId>>,
|
||||
) -> Element {
|
||||
let long_press_duration = Duration::from_millis(500);
|
||||
|
||||
let rooms = STORE.read().rooms();
|
||||
let toto = rooms.get(&room_id).unwrap();
|
||||
|
||||
let room = toto.signal();
|
||||
|
||||
let room_id = room.id();
|
||||
let room = rooms.get(&room_id).unwrap().signal();
|
||||
let room_id = Rc::new(room_id);
|
||||
let room_name = room.name();
|
||||
|
||||
let selected_room_id = use_context::<Signal<Option<RoomId>>>();
|
||||
let selected_room_id = use_signal(|| None::<RoomId>);
|
||||
|
||||
let invited_badge = if room.is_invited() {
|
||||
rsx! {
|
||||
@@ -184,20 +187,19 @@ pub fn ConversationAvatar(room_id: RoomId, on_clicked: EventHandler<RoomId>) ->
|
||||
class: ClassName::CONVERSATION_AVATAR_INVITED_BADGE,
|
||||
|
||||
p {
|
||||
"Invited",
|
||||
"Invited"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
VNode::empty()
|
||||
};
|
||||
|
||||
let is_selected = match selected_room_id.read().as_ref() {
|
||||
Some(selected_room_id) => *selected_room_id == room_id,
|
||||
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! {
|
||||
@@ -221,7 +223,7 @@ pub fn ConversationAvatar(room_id: RoomId, on_clicked: EventHandler<RoomId>) ->
|
||||
div {
|
||||
class: ClassName::CONVERSATION_AVATAR_IMAGE,
|
||||
|
||||
{placeholder},
|
||||
{placeholder}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -232,14 +234,24 @@ pub fn ConversationAvatar(room_id: RoomId, on_clicked: EventHandler<RoomId>) ->
|
||||
];
|
||||
let classes_str = classes.join(" ");
|
||||
|
||||
let on_press = {
|
||||
let room_id = room_id.clone();
|
||||
move || {
|
||||
on_selected.map(|c| c.call(room_id.as_ref().clone()));
|
||||
}
|
||||
};
|
||||
|
||||
let on_long_press = move || {
|
||||
on_pressed.map(|c| c.call(room_id.as_ref().clone()));
|
||||
};
|
||||
|
||||
let long_press_hook = use_long_press(long_press_duration, on_press, on_long_press);
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "{classes_str}",
|
||||
|
||||
onclick: move |evt| {
|
||||
on_clicked.call(room_id.clone());
|
||||
evt.stop_propagation();
|
||||
},
|
||||
..long_press_hook.handlers,
|
||||
|
||||
{avatar}
|
||||
{invited_badge}
|
||||
@@ -248,7 +260,10 @@ pub fn ConversationAvatar(room_id: RoomId, on_clicked: EventHandler<RoomId>) ->
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ConversationsCarousel(on_selected_conversation: EventHandler<RoomId>) -> Element {
|
||||
pub fn ConversationsCarousel(
|
||||
on_selected_conversation: EventHandler<RoomId>,
|
||||
on_pressed_conversation: EventHandler<RoomId>,
|
||||
) -> Element {
|
||||
let mut ordered_rooms = use_signal(Vec::<RoomId>::new);
|
||||
|
||||
use_effect(move || {
|
||||
@@ -267,7 +282,8 @@ pub fn ConversationsCarousel(on_selected_conversation: EventHandler<RoomId>) ->
|
||||
rsx! {
|
||||
ConversationAvatar {
|
||||
room_id: room.clone(),
|
||||
on_clicked: on_selected_conversation,
|
||||
on_selected: on_selected_conversation,
|
||||
on_pressed: on_pressed_conversation,
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -279,22 +295,20 @@ pub fn ConversationsCarousel(on_selected_conversation: EventHandler<RoomId>) ->
|
||||
onscroll: move |_| {
|
||||
// Catch scrolling events.
|
||||
},
|
||||
{rendered_avatars},
|
||||
{rendered_avatars}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If id is None, the Space will handle all the Conversation which have no parent (Space).
|
||||
#[component]
|
||||
pub fn Space(id: SpaceId) -> Element {
|
||||
let space = STORE.read().spaces().get(&id).unwrap().signal();
|
||||
|
||||
let name = space.name();
|
||||
|
||||
pub fn Space(id: Option<SpaceId>, on_pressed_conversation: EventHandler<RoomId>) -> Element {
|
||||
let mut selected_room_id = use_context_provider(|| Signal::new(None::<RoomId>));
|
||||
let mut displayed_rooms = use_context_provider(|| Signal::new(Vec::<RoomId>::new()));
|
||||
|
||||
let name = if let Some(id) = id {
|
||||
let space = STORE.read().spaces().get(&id).unwrap().signal();
|
||||
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 {
|
||||
@@ -303,72 +317,8 @@ pub fn Space(id: SpaceId) -> Element {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
space.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() {
|
||||
@@ -378,8 +328,11 @@ pub fn HomeSpace() -> Element {
|
||||
}
|
||||
}
|
||||
});
|
||||
Some("Home".to_string())
|
||||
};
|
||||
|
||||
let on_selected_conversation = move |room_id: RoomId| {
|
||||
STORE.write().on_selected_room(room_id.clone());
|
||||
selected_room_id.set(Some(room_id));
|
||||
};
|
||||
|
||||
@@ -407,8 +360,6 @@ pub fn HomeSpace() -> Element {
|
||||
let classes_str = space_classes.join(" ");
|
||||
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
|
||||
div {
|
||||
class: "{classes_str}",
|
||||
|
||||
@@ -422,51 +373,54 @@ pub fn HomeSpace() -> Element {
|
||||
p {
|
||||
{name}
|
||||
}
|
||||
},
|
||||
}
|
||||
ConversationsCarousel {
|
||||
on_selected_conversation,
|
||||
},
|
||||
on_pressed_conversation,
|
||||
}
|
||||
div {
|
||||
class: ClassName::SPACE_CONVERSATION_NAME,
|
||||
p {
|
||||
{selected_room_name},
|
||||
{selected_room_name}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn Spaces() -> Element {
|
||||
#[component]
|
||||
pub fn Spaces(on_pressed_conversation: EventHandler<RoomId>) -> 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() }
|
||||
Space { id: id.clone(), on_pressed_conversation }
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
|
||||
div {
|
||||
class: ClassName::SPACES,
|
||||
|
||||
{rendered_spaces},
|
||||
{rendered_spaces}
|
||||
|
||||
HomeSpace {},
|
||||
Space { on_pressed_conversation }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Search() -> Element {
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
|
||||
div {
|
||||
class: ClassName::SEARCH,
|
||||
|
||||
div {
|
||||
class: ClassName::SEARCH_TEXT,
|
||||
|
||||
TextInput {}
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::SEARCH_BUTTON,
|
||||
@@ -478,27 +432,133 @@ pub fn Search() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn Conversations() -> Element {
|
||||
#[derive(PartialEq)]
|
||||
enum ConversationOptionsMenuActions {
|
||||
Join(RoomId),
|
||||
Close,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ConversationOptionsMenu(
|
||||
room_id: RoomId,
|
||||
callbacks: Coroutine<ConversationOptionsMenuActions>,
|
||||
) -> Element {
|
||||
let room = STORE.read().rooms().get(&room_id).unwrap().signal();
|
||||
|
||||
let topic = room.topic().unwrap_or("<No topic set>".to_string());
|
||||
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
div {
|
||||
class: ClassName::CONVERSATION_OPTIONS_MENU,
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER,
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_AVATAR,
|
||||
ConversationAvatar { room_id: room_id.clone() }
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_NAME,
|
||||
p {
|
||||
{room.name()}
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_TOPIC,
|
||||
p {
|
||||
{topic}
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_CONFIG,
|
||||
p {
|
||||
"Coming soon..."
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_CLOSE_BUTTON,
|
||||
RejectButton {
|
||||
onclick: move |_| {
|
||||
callbacks.send(ConversationOptionsMenuActions::Close);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_JOIN_BUTTON,
|
||||
JoinButton {
|
||||
onclick: move |_| {
|
||||
callbacks.send(ConversationOptionsMenuActions::Join(room_id.clone()));
|
||||
callbacks.send(ConversationOptionsMenuActions::Close);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn Conversations() -> Element {
|
||||
let mut room_id = use_signal(|| None::<RoomId>);
|
||||
|
||||
let on_pressed_conversation = move |id: RoomId| {
|
||||
room_id.set(Some(id));
|
||||
};
|
||||
|
||||
let callbacks = use_coroutine(
|
||||
move |mut rx: UnboundedReceiver<ConversationOptionsMenuActions>| async move {
|
||||
while let Some(action) = rx.next().await {
|
||||
match action {
|
||||
ConversationOptionsMenuActions::Join(room_id) => {
|
||||
let rooms = STORE.read().rooms();
|
||||
if let Some(room) = rooms.get(&room_id) {
|
||||
room.join().await;
|
||||
}
|
||||
}
|
||||
ConversationOptionsMenuActions::Close => {
|
||||
room_id.set(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let menu = match room_id.read().as_ref() {
|
||||
Some(room_id) => {
|
||||
let room_id = room_id.clone();
|
||||
rsx! {
|
||||
ConversationOptionsMenu { room_id, callbacks }
|
||||
}
|
||||
}
|
||||
None => VNode::empty(),
|
||||
};
|
||||
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS,
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_ACCOUNT,
|
||||
Account {},
|
||||
},
|
||||
Account {}
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_SPACES,
|
||||
Spaces {},
|
||||
},
|
||||
Spaces { on_pressed_conversation }
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_SEARCH,
|
||||
Search {},
|
||||
},
|
||||
Search {}
|
||||
}
|
||||
}
|
||||
{menu}
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin extra-marged-button() {
|
||||
@include button-class();
|
||||
}
|
||||
|
||||
.account {
|
||||
$colum-spacing: 5%;
|
||||
$col-width: 8.75%;
|
||||
@@ -89,10 +93,6 @@
|
||||
grid-area: status;
|
||||
}
|
||||
|
||||
@mixin extra-marged-button() {
|
||||
@include button-class();
|
||||
}
|
||||
|
||||
&__spaces {
|
||||
grid-area: spaces;
|
||||
|
||||
@@ -268,6 +268,11 @@
|
||||
display: flex;
|
||||
gap: 5%;
|
||||
|
||||
&__text {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__button {
|
||||
@include button-class();
|
||||
|
||||
@@ -284,22 +289,152 @@
|
||||
$account-height: 15%;
|
||||
$search-height: 5%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: $gap;
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: min($account-height, 384px) $gap auto $gap min($search-height, 128px);
|
||||
grid-template-areas:
|
||||
"account"
|
||||
"."
|
||||
"spaces"
|
||||
"."
|
||||
"search"
|
||||
;
|
||||
|
||||
&__account {
|
||||
height: $account-height;
|
||||
max-height: 384px;
|
||||
grid-area: account;
|
||||
}
|
||||
|
||||
&__spaces {
|
||||
min-height: calc(100% - $account-height - $search-height - (2 * $gap));
|
||||
grid-area: spaces;
|
||||
}
|
||||
|
||||
&__search {
|
||||
height: $search-height;
|
||||
max-height: 128px;
|
||||
grid-area: search;
|
||||
}
|
||||
|
||||
&__menu {
|
||||
grid-area: spaces;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
%base-helper-text {
|
||||
margin: 0;
|
||||
margin-top: 0.3vh;
|
||||
|
||||
font-size: 1.2vh;
|
||||
|
||||
// TODO: Set color used for text in _base.scss file
|
||||
color: get-color(greyscale, 90);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
&.invalid {
|
||||
color: get-color(critical, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-options-menu {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
position: relative;
|
||||
top: -100%;
|
||||
margin-bottom: calc(-100% / $aspect-ratio);
|
||||
|
||||
border-radius: $border-radius;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
|
||||
&__inner {
|
||||
$padding: 5%;
|
||||
// TODO: Thin border
|
||||
@include panel($padding, $padding);
|
||||
|
||||
width: 95%;
|
||||
height: 60%;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 10% 10% 5% 15% 20% 10% 20% 10%;
|
||||
grid-template-rows: 7.5% 7.5% 5% 5% auto 5% 10%;
|
||||
grid-template-areas:
|
||||
"avatar avatar . name name name name name"
|
||||
"avatar avatar . topic topic topic topic topic"
|
||||
"avatar avatar . . . . . ."
|
||||
". . . . . . . ."
|
||||
"config config config config config config config config"
|
||||
". . . . . . . ."
|
||||
". close close close . join join ."
|
||||
;
|
||||
|
||||
&__avatar {
|
||||
grid-area: avatar;
|
||||
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
&__name {
|
||||
grid-area: name;
|
||||
|
||||
// TODO: Merge with &__display-name
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
p {
|
||||
font-size: 2.5vh;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__topic {
|
||||
grid-area: topic;
|
||||
|
||||
// TODO: Merge with &__display-name
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@extend %base-helper-text;
|
||||
p {
|
||||
font-size: 2vh;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__config {
|
||||
grid-area: config;
|
||||
|
||||
// TODO: Merge with &__display-name
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border: $border-thin;
|
||||
border-color: get-color(ternary, 90);
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
grid-area: close;
|
||||
}
|
||||
|
||||
&__join-button {
|
||||
grid-area: join;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ use dioxus_free_icons::{Icon, IconShape};
|
||||
|
||||
turf::style_sheet!("src/ui/components/icons.scss");
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/style_vars.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/style_tokens.rs"));
|
||||
|
||||
use style::{COLOR_PRIMARY_100, COLOR_TERNARY_100};
|
||||
|
||||
@@ -15,7 +15,8 @@ macro_rules! transparent_icon {
|
||||
($name:ident, $icon:ident) => {
|
||||
pub fn $name() -> Element {
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
Icon {
|
||||
class: ClassName::TRANSPARENT_ICON,
|
||||
icon: $icon,
|
||||
@@ -52,7 +53,7 @@ impl IconShape for LogoShape {
|
||||
|
||||
pub fn LogoIcon() -> Element {
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
Icon {
|
||||
icon: LogoShape,
|
||||
@@ -133,14 +134,14 @@ impl IconShape for PyramidShape {
|
||||
L {_PYRAMID_EDGES_E1_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
|
||||
M {_PYRAMID_EDGES_E1_X} {_PYRAMID_EDGES_E1_Y} \
|
||||
V {_PYRAMID_CENTRAL_EDGE_Y_LEN}",
|
||||
},
|
||||
}
|
||||
path {
|
||||
d: "\
|
||||
M {_PYRAMID_CENTRAL_EDGE_E2_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
|
||||
V {central_edge_ratio_e2_y} \
|
||||
L {left_edge_ratio_e1_x} {no_central_edge_ratio_e1_y} \
|
||||
L {_PYRAMID_LEFT_EDGE_E2_X} {_PYRAMID_LEFT_EDGE_E2_Y} Z",
|
||||
},
|
||||
}
|
||||
path {
|
||||
d: "\
|
||||
M {_PYRAMID_CENTRAL_EDGE_E2_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
|
||||
@@ -168,10 +169,11 @@ pub fn Pyramid(props: PyramidProps) -> Element {
|
||||
.unwrap_or(COLOR_TERNARY_100.to_string());
|
||||
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
Icon {
|
||||
class: ClassName::PYRAMID_ICON,
|
||||
|
||||
icon: PyramidShape { ratio: props.ratio, color, progress_color },
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ use super::{
|
||||
text_input::{PasswordInputState, PasswordTextInput, TextInput, TextInputState},
|
||||
};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/style_vars.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/style_tokens.rs"));
|
||||
|
||||
use style::{
|
||||
COLOR_PRIMARY_100, COLOR_PRIMARY_110, COLOR_PRIMARY_120, COLOR_PRIMARY_140, COLOR_PRIMARY_150,
|
||||
@@ -97,27 +97,6 @@ impl Clone for Box<dyn OnValidationError> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TextInputHandler {
|
||||
state: Signal<TextInputState>,
|
||||
}
|
||||
|
||||
impl TextInputHandler {}
|
||||
|
||||
impl OnValidationError for TextInputHandler {
|
||||
fn reset(&mut self) {
|
||||
self.state.write().reset();
|
||||
}
|
||||
|
||||
fn invalidate(&mut self, helper_text: String) {
|
||||
self.state.write().invalidate(helper_text);
|
||||
}
|
||||
|
||||
fn box_clone(&self) -> Box<dyn OnValidationError> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct UrlInputHandler {
|
||||
state: Signal<TextInputState>,
|
||||
@@ -619,7 +598,6 @@ pub fn Login() -> Element {
|
||||
});
|
||||
|
||||
if *spinner_animated.read() && SESSION.read().is_logged {
|
||||
debug!("Stop spinner");
|
||||
spinner_animated.set(false);
|
||||
}
|
||||
|
||||
@@ -745,7 +723,7 @@ pub fn Login() -> Element {
|
||||
let confirm_password_classes_str = confirm_password_classes.join(" ");
|
||||
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
div {
|
||||
class: "{classes_str}",
|
||||
@@ -757,73 +735,79 @@ pub fn Login() -> Element {
|
||||
random_avatar_future.restart()
|
||||
},
|
||||
|
||||
{avatar},
|
||||
},
|
||||
{avatar}
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::LOGIN_HOMESERVER,
|
||||
|
||||
TextInput {
|
||||
placeholder: "Homeserver URL",
|
||||
value: "{homeserver_url}",
|
||||
state: homeserver_url_state,
|
||||
oninput: on_input![data, homeserver_url],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::LOGIN_ID,
|
||||
|
||||
TextInput {
|
||||
placeholder: "{id_placeholder}",
|
||||
value: "{id}",
|
||||
state: id_state,
|
||||
oninput: on_input![data, id],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: "{password_classes_str}",
|
||||
|
||||
PasswordTextInput {
|
||||
placeholder: "Password",
|
||||
value: "{password}",
|
||||
state: password_state,
|
||||
oninput: on_input![data, password],
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: "{confirm_password_classes_str}",
|
||||
|
||||
PasswordTextInput {
|
||||
placeholder: "Confirm Password",
|
||||
value: "{confirm_password}",
|
||||
state: confirm_password_state,
|
||||
oninput: on_input![data, confirm_password],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::LOGIN_SPINNER,
|
||||
|
||||
Spinner {
|
||||
animate: *spinner_animated.read(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::LOGIN_REGISTER_BUTTON,
|
||||
|
||||
RegisterButton {
|
||||
onclick: on_clicked_register,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::LOGIN_LOGIN_BUTTON,
|
||||
|
||||
LoginButton {
|
||||
focus: true,
|
||||
onclick: on_clicked_login,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{rendered_modal},
|
||||
{rendered_modal}
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ use crate::infrastructure::services::random_svg_generators::{
|
||||
generate_random_svg_avatar, AvatarConfig, AvatarFeeling,
|
||||
};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/style_vars.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/style_tokens.rs"));
|
||||
|
||||
use style::{COLOR_CRITICAL_100, COLOR_SUCCESS_100, COLOR_WARNING_100};
|
||||
|
||||
@@ -76,10 +76,10 @@ pub fn Modal(props: ModalProps) -> Element {
|
||||
Severity::Critical => ErrorButton,
|
||||
};
|
||||
|
||||
icon.as_ref()?;
|
||||
let _ = icon.as_ref().ok_or(VNode::empty());
|
||||
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
div {
|
||||
class: ClassName::MODAL,
|
||||
@@ -90,17 +90,17 @@ pub fn Modal(props: ModalProps) -> Element {
|
||||
div {
|
||||
class: ClassName::MODAL_CONTENT_ICON,
|
||||
{icon}
|
||||
},
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::MODAL_CONTENT_TITLE,
|
||||
{props.title},
|
||||
},
|
||||
{props.title}
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::MODAL_CONTENT_MSG,
|
||||
{props.children},
|
||||
},
|
||||
{props.children}
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::MODAL_CONTENT_BUTTONS,
|
||||
@@ -109,10 +109,10 @@ pub fn Modal(props: ModalProps) -> Element {
|
||||
if let Some(cb) = &props.on_confirm {
|
||||
cb.call(evt);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -13,13 +13,14 @@ pub struct SpinnerProps {
|
||||
|
||||
pub fn Spinner(props: SpinnerProps) -> Element {
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
div {
|
||||
class: ClassName::SPINNER,
|
||||
|
||||
Icon {
|
||||
class: if props.animate { "" } else { ClassName::PAUSED },
|
||||
|
||||
icon: LogoShape,
|
||||
}
|
||||
}
|
||||
|
@@ -67,7 +67,7 @@ pub fn TextInput(props: InputProps<TextInputState>) -> Element {
|
||||
let input_classes_str = [ClassName::TEXT_INPUT_INPUT, criticity_class].join(" ");
|
||||
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
div {
|
||||
class: ClassName::TEXT_INPUT,
|
||||
@@ -83,7 +83,7 @@ pub fn TextInput(props: InputProps<TextInputState>) -> Element {
|
||||
cb.call(evt);
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::TEXT_INPUT_HELPER_TEXT,
|
||||
@@ -159,7 +159,7 @@ pub fn PasswordTextInput(props: InputProps<PasswordInputState>) -> Element {
|
||||
let input_classes = [ClassName::PASSWORD_TEXT_INPUT_INPUT, criticity_class].join(" ");
|
||||
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
div {
|
||||
class: "{text_input_classes}",
|
||||
@@ -175,7 +175,7 @@ pub fn PasswordTextInput(props: InputProps<PasswordInputState>) -> Element {
|
||||
cb.call(evt);
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if let Some(score) = score {
|
||||
div {
|
||||
@@ -184,7 +184,7 @@ pub fn PasswordTextInput(props: InputProps<PasswordInputState>) -> Element {
|
||||
ratio: score,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::PASSWORD_TEXT_INPUT_SHOW_TOGGLE,
|
||||
@@ -203,7 +203,7 @@ pub fn PasswordTextInput(props: InputProps<PasswordInputState>) -> Element {
|
||||
icon: IoEye,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::PASSWORD_TEXT_INPUT_HELPER_TEXT,
|
||||
@@ -212,7 +212,7 @@ pub fn PasswordTextInput(props: InputProps<PasswordInputState>) -> Element {
|
||||
class: criticity_class,
|
||||
{helper_text}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
@import "../_base.scss"
|
||||
@import "../_base.scss";
|
||||
|
||||
%base-text-input {
|
||||
$horizontal-padding: 1vw;
|
||||
|
@@ -7,20 +7,28 @@ turf::style_sheet!("src/ui/components/wallpaper.scss");
|
||||
|
||||
#[component]
|
||||
pub fn Wallpaper(display_version: Option<bool>) -> Element {
|
||||
let background_image = format!(
|
||||
"url({})",
|
||||
manganis::mg!(file("public/images/wallpaper-pattern.svg"))
|
||||
);
|
||||
|
||||
let version = display_version.map(|flag| if flag { Some(GIT_VERSION) } else { None });
|
||||
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
div {
|
||||
class: ClassName::WALLPAPER,
|
||||
|
||||
div {
|
||||
class: ClassName::WALLPAPER_CONTENT,
|
||||
background_image: "{background_image}",
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::WALLPAPER_VERSION,
|
||||
{version},
|
||||
|
||||
{version}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,6 @@
|
||||
overflow: hidden;
|
||||
|
||||
&__content {
|
||||
background-image: url("./images/wallpaper-pattern.svg");
|
||||
background-position: center;
|
||||
|
||||
width: 150%;
|
||||
|
3
src/ui/hooks/mod.rs
Normal file
3
src/ui/hooks/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub use use_long_press::use_long_press;
|
||||
|
||||
mod use_long_press;
|
60
src/ui/hooks/use_long_press.rs
Normal file
60
src/ui/hooks/use_long_press.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use std::{cell::RefCell, time::Duration};
|
||||
|
||||
use async_std::task;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
pub struct UseLongPress {
|
||||
_timer: UseFuture,
|
||||
pub handlers: Vec<Attribute>,
|
||||
}
|
||||
|
||||
pub fn use_long_press(
|
||||
duration: Duration,
|
||||
on_press: impl FnMut() + 'static,
|
||||
on_long_press: impl FnMut() + 'static,
|
||||
) -> UseLongPress {
|
||||
let on_press = std::rc::Rc::new(RefCell::new(on_press));
|
||||
let on_press_cb = use_callback(move |_| {
|
||||
let mut on_press = on_press.as_ref().borrow_mut();
|
||||
on_press();
|
||||
});
|
||||
|
||||
let on_long_press = std::rc::Rc::new(RefCell::new(on_long_press));
|
||||
let on_long_press_cb = use_callback(move |_| {
|
||||
let mut on_long_press = on_long_press.as_ref().borrow_mut();
|
||||
on_long_press();
|
||||
});
|
||||
|
||||
let mut timer = use_future(move || async move {
|
||||
task::sleep(duration).await;
|
||||
on_long_press_cb.call(());
|
||||
});
|
||||
|
||||
timer.cancel();
|
||||
|
||||
let selection_begin_cb = move |_: Event<PlatformEventData>| {
|
||||
timer.restart();
|
||||
};
|
||||
|
||||
let selection_end_cb = move |_: Event<PlatformEventData>| {
|
||||
if !timer.finished() {
|
||||
timer.cancel();
|
||||
on_press_cb.call(());
|
||||
}
|
||||
};
|
||||
|
||||
let mut handlers = Vec::new();
|
||||
for event_name in ["onmousedown", "ontouchstart"] {
|
||||
let value = dioxus_core::AttributeValue::listener(selection_begin_cb);
|
||||
handlers.push(Attribute::new(event_name, value, None, false));
|
||||
}
|
||||
for event_name in ["onmouseup", "ontouchend"] {
|
||||
let value = dioxus_core::AttributeValue::listener(selection_end_cb);
|
||||
handlers.push(Attribute::new(event_name, value, None, false));
|
||||
}
|
||||
|
||||
UseLongPress {
|
||||
_timer: timer,
|
||||
handlers,
|
||||
}
|
||||
}
|
@@ -1,17 +1,28 @@
|
||||
use std::rc::Rc;
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use dioxus::prelude::*;
|
||||
use futures::join;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
domain::model::room::RoomId,
|
||||
ui::{
|
||||
components::{
|
||||
chat_panel::ChatPanel, conversations::Conversations as ConversationsComponent,
|
||||
wallpaper::Wallpaper,
|
||||
},
|
||||
STORE,
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
include!(concat!(env!("OUT_DIR"), "/style_component_panel.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/style_layout_conversations.rs"));
|
||||
|
||||
// TODO: Get from SCSS
|
||||
const WIDGET_HEIGHT_RATIO: f64 = 0.95;
|
||||
const ASPECT_RATIO: f64 = 1.0 / 1.618;
|
||||
use conversations::INNER_PANEL_HEIGHT_RATIO;
|
||||
use panel::ASPECT_RATIO;
|
||||
|
||||
async fn on_carousel_scroll(
|
||||
parent_div: &Rc<MountedData>,
|
||||
@@ -44,20 +55,27 @@ async fn on_carousel_scroll(
|
||||
}
|
||||
|
||||
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 mut carousel_div = use_signal(|| None::<Rc<MountedData>>);
|
||||
|
||||
let conversation_panels_nb = 3;
|
||||
let conversation_panels = (0..conversation_panels_nb + 1).map(|i| {
|
||||
let displayed_room_ids = STORE.read().displayed_room_ids();
|
||||
|
||||
let mut conversation_panels = Vec::new();
|
||||
let mut displayed_room_ids_it = displayed_room_ids.iter().peekable();
|
||||
while let Some(room_id) = displayed_room_ids_it.next() {
|
||||
if let Some(room) = STORE.read().rooms().get(room_id) {
|
||||
let room = room.signal();
|
||||
let room_name_repr = room.name().unwrap_or(room.id().to_string());
|
||||
let inner = rsx! {
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL_INNER,
|
||||
ChatPanel { name: format!("CHAT #{i}") },
|
||||
ChatPanel { name: format!("CHAT {room_name_repr}") }
|
||||
}
|
||||
};
|
||||
|
||||
if i == conversation_panels_nb {
|
||||
// If this is the last iteration
|
||||
let panel = if displayed_room_ids_it.peek().is_none() {
|
||||
rsx! {
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL,
|
||||
@@ -72,11 +90,28 @@ fn LayoutSmall() -> Element {
|
||||
{inner}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(panel) = panel {
|
||||
conversation_panels.push(panel);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
warn!("No {} room found", room_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Add tail div to dynamic rendered conversation_panels avoids side effects on layout changes
|
||||
conversation_panels.push(
|
||||
rsx! {
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_TAIL,
|
||||
}
|
||||
}
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_SMALL,
|
||||
@@ -112,17 +147,64 @@ fn LayoutSmall() -> Element {
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_SMALL_CONVERSATIONS_PANEL_INNER,
|
||||
ConversationsComponent {},
|
||||
},
|
||||
},
|
||||
ConversationsComponent {}
|
||||
}
|
||||
}
|
||||
|
||||
{conversation_panels}
|
||||
{conversation_panels.iter()}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Tab(room_id: RoomId) -> Element {
|
||||
let rooms = STORE.read().rooms();
|
||||
let room = rooms.get(&room_id).unwrap().signal();
|
||||
|
||||
let room_avatar = if let Some(content) = room.avatar() {
|
||||
let encoded = general_purpose::STANDARD.encode(content);
|
||||
rsx! {
|
||||
div {
|
||||
class: ClassName::TAB_AVATAR_IMAGE,
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_TAIL,
|
||||
class: ClassName::TAB_AVATAR_IMAGE,
|
||||
background_image: format!("url(data:image/jpeg;base64,{encoded})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VNode::empty()
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: ClassName::TAB,
|
||||
|
||||
{room_avatar}
|
||||
|
||||
div {
|
||||
class: ClassName::TAB_NAME,
|
||||
|
||||
{room.name()}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn TabsBar(room_ids: HashSet<RoomId>) -> Element {
|
||||
let tabs = room_ids
|
||||
.iter()
|
||||
.map(|room_id| rsx! { Tab { room_id: room_id.clone() }});
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: ClassName::TABS_BAR,
|
||||
|
||||
{tabs}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn LayoutBig() -> Element {
|
||||
@@ -130,40 +212,65 @@ fn LayoutBig() -> Element {
|
||||
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 {
|
||||
let displayed_room_ids = STORE.read().displayed_room_ids();
|
||||
|
||||
let mut conversation_panels = Vec::new();
|
||||
let mut displayed_room_ids_it = displayed_room_ids.iter().peekable();
|
||||
let mut is_first = true;
|
||||
while let Some(room_id) = displayed_room_ids_it.next() {
|
||||
if let Some(room) = STORE.read().rooms().get(room_id) {
|
||||
let room = room.signal();
|
||||
let room_name_repr = format!("CHAT {}", room.name().unwrap_or(room.id().to_string()));
|
||||
let panel = if is_first {
|
||||
is_first = false;
|
||||
rsx! {
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATION_PANELS_PANEL,
|
||||
class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_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}") },
|
||||
ChatPanel { name: room_name_repr }
|
||||
}
|
||||
}
|
||||
} else if i == conversation_panels_nb {
|
||||
} else if displayed_room_ids_it.peek().is_none() {
|
||||
rsx! {
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATION_PANELS_PANEL,
|
||||
class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_PANELS_PANEL,
|
||||
onmounted: move |cx: Event<MountedData>| last_div.set(Some(cx.data())),
|
||||
ChatPanel { name: format!("CHAT #{i}") },
|
||||
ChatPanel { name: room_name_repr }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rsx! {
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATION_PANELS_PANEL,
|
||||
ChatPanel { name: format!("CHAT #{i}") },
|
||||
class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_PANELS_PANEL,
|
||||
ChatPanel { name: room_name_repr }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(panel) = panel {
|
||||
conversation_panels.push(panel);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
warn!("No {} room found", room_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Add tail div to dynamic rendered conversation_panels avoids side effects on layout changes
|
||||
conversation_panels.push(
|
||||
rsx! {
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_TAIL,
|
||||
}
|
||||
}
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_BIG,
|
||||
@@ -171,11 +278,20 @@ fn LayoutBig() -> Element {
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_PANEL,
|
||||
|
||||
ConversationsComponent {},
|
||||
},
|
||||
ConversationsComponent {}
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATION_PANELS,
|
||||
class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS,
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_TABS_BAR,
|
||||
|
||||
TabsBar { room_ids: displayed_room_ids}
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_PANELS,
|
||||
|
||||
onmounted: move |cx| async move {
|
||||
let data = cx.data();
|
||||
@@ -198,55 +314,42 @@ fn LayoutBig() -> Element {
|
||||
class: ClassName::CONVERSATIONS_VIEW_HEAD,
|
||||
}
|
||||
|
||||
{conversation_panels}
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW_TAIL,
|
||||
{conversation_panels.iter()}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {}}
|
||||
}
|
||||
}();
|
||||
let mut use_big_layout = use_signal(|| false);
|
||||
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
Wallpaper {
|
||||
display_version: true
|
||||
},
|
||||
}
|
||||
|
||||
div {
|
||||
class: ClassName::CONVERSATIONS_VIEW,
|
||||
onmounted: move |cx| {
|
||||
async move {
|
||||
|
||||
onresize: move |cx| {
|
||||
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)));
|
||||
}
|
||||
if let Ok(size) = data.get_border_box_size() {
|
||||
// Use LayoutBig if the layout can contain 2 panels side by side
|
||||
let component_width = size.height * INNER_PANEL_HEIGHT_RATIO * ASPECT_RATIO;
|
||||
let breakpoint_width = component_width * 2_f64;
|
||||
use_big_layout.set(size.width > breakpoint_width);
|
||||
}
|
||||
},
|
||||
|
||||
{layout}
|
||||
if use_big_layout() {
|
||||
LayoutBig {}
|
||||
} else {
|
||||
LayoutSmall {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -17,11 +17,13 @@
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
$inner-panel-height-ratio: 0.95;
|
||||
|
||||
.conversations-view {
|
||||
$height: 100vh;
|
||||
$width: 100vw;
|
||||
$conversations-panel-height: calc($height * 0.95);
|
||||
$conversations-panel-width: calc($conversations-panel-height * $panel-aspect-ratio);
|
||||
$conversations-panel-height: calc($height * $inner-panel-height-ratio);
|
||||
$conversations-panel-width: calc($conversations-panel-height * $aspect-ratio);
|
||||
$gap: 1%;
|
||||
$content-height: 95%;
|
||||
$ratio: 2;
|
||||
@@ -63,11 +65,11 @@
|
||||
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) {
|
||||
@media (max-aspect-ratio: $aspect-ratio) {
|
||||
width: 100%;
|
||||
}
|
||||
@media (min-aspect-ratio: $panel-aspect-ratio) {
|
||||
aspect-ratio: $panel-aspect-ratio;
|
||||
@media (min-aspect-ratio: $aspect-ratio) {
|
||||
aspect-ratio: $aspect-ratio;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,14 +107,29 @@
|
||||
|
||||
&__conversations-panel {
|
||||
height: $content-height;
|
||||
aspect-ratio: $panel-aspect-ratio;
|
||||
aspect-ratio: $aspect-ratio;
|
||||
}
|
||||
|
||||
&__conversation-panels {
|
||||
&__conversations {
|
||||
height: $content-height;
|
||||
min-width: 64px;
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap;
|
||||
|
||||
&__tabs-bar {
|
||||
height: 5%;
|
||||
width: 100%;
|
||||
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
&__panels {
|
||||
flex-grow: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow-x: scroll;
|
||||
@@ -134,3 +151,65 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-bar {
|
||||
$gap: min(1vw, 8px);
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
overflow: scroll;
|
||||
|
||||
scrollbar-width: none;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab {
|
||||
$gap: min(1vw, 8px);
|
||||
|
||||
height: 100%;
|
||||
min-width: 0px;
|
||||
max-width: 100%;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $gap;
|
||||
|
||||
border: $border-normal;
|
||||
border-color: get-color(primary, 90);
|
||||
border-radius: $border-radius;
|
||||
|
||||
padding: calc($gap / 2) $gap;
|
||||
|
||||
font-size: 2vh;
|
||||
|
||||
background-color: get-color(greyscale, 0);
|
||||
|
||||
&__avatar-image {
|
||||
height: 100%;
|
||||
aspect-ratio: 1;
|
||||
|
||||
border: $border-thin;
|
||||
border-color: get-color(greyscale, 90);
|
||||
border-radius: $border-radius;
|
||||
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
&__name {
|
||||
display: inline-block;
|
||||
margin: 0px;
|
||||
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
cursor: default3;
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ turf::style_sheet!("src/ui/layouts/login.scss");
|
||||
|
||||
pub fn Login() -> Element {
|
||||
rsx! {
|
||||
style { {STYLE_SHEET} },
|
||||
style { {STYLE_SHEET} }
|
||||
|
||||
Wallpaper {
|
||||
display_version: true
|
||||
|
@@ -18,14 +18,14 @@
|
||||
align-items: safe center;
|
||||
|
||||
&__login-panel {
|
||||
@media (max-aspect-ratio: $panel-aspect-ratio) {
|
||||
@media (max-aspect-ratio: $aspect-ratio) {
|
||||
width: 95%;
|
||||
}
|
||||
@media (min-aspect-ratio: $panel-aspect-ratio) {
|
||||
@media (min-aspect-ratio: $aspect-ratio) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
aspect-ratio: $panel-aspect-ratio;
|
||||
aspect-ratio: $aspect-ratio;
|
||||
max-height: $panel-max-height;
|
||||
|
||||
flex-shrink: 0;
|
||||
@@ -36,6 +36,6 @@
|
||||
justify-content: center;
|
||||
|
||||
// Variables inherited by children
|
||||
--aspect-ratio: #{$panel-aspect-ratio};
|
||||
--aspect-ratio: #{$aspect-ratio};
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
pub(crate) mod components;
|
||||
pub(crate) mod hooks;
|
||||
pub(crate) mod layouts;
|
||||
pub(crate) mod store;
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
pub(crate) mod room;
|
||||
pub(crate) mod space;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
@@ -21,6 +22,17 @@ use space::Space;
|
||||
pub struct Store {
|
||||
rooms: HashMap<RoomId, Rc<Room>>,
|
||||
spaces: HashMap<SpaceId, Rc<Space>>,
|
||||
|
||||
displayed_room_ids: HashSet<RoomId>,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub fn on_selected_room(&mut self, room_id: RoomId) {
|
||||
// Toggle the room_id selection
|
||||
if !self.displayed_room_ids.write().remove(&room_id) {
|
||||
self.displayed_room_ids.write().insert(room_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
|
@@ -12,13 +12,15 @@ use crate::domain::model::{
|
||||
store_interface::{RoomStoreConsumerInterface, RoomStoreProviderInterface},
|
||||
};
|
||||
|
||||
#[modx::props(id, is_direct, name, spaces)]
|
||||
#[modx::props(id, is_direct, name, topic, spaces)]
|
||||
#[modx::store]
|
||||
pub struct Store {
|
||||
id: RoomId,
|
||||
|
||||
is_direct: Option<bool>,
|
||||
name: Option<String>,
|
||||
topic: Option<String>,
|
||||
|
||||
avatar: Option<Avatar>,
|
||||
members: Vec<RoomMember>,
|
||||
invitations: Vec<Invitation>,
|
||||
@@ -31,7 +33,6 @@ pub struct Store {
|
||||
pub struct Room {
|
||||
store: RefCell<Store>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
domain: Rc<dyn RoomStoreConsumerInterface>,
|
||||
}
|
||||
|
||||
@@ -45,6 +46,7 @@ impl Room {
|
||||
room.id().clone(),
|
||||
room.is_direct(),
|
||||
room.name(),
|
||||
room.topic(),
|
||||
room.spaces().clone(),
|
||||
);
|
||||
|
||||
@@ -54,6 +56,10 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn join(&self) {
|
||||
self.domain.join().await;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_avatar(&self) -> Option<Avatar> {
|
||||
self.domain.avatar().await
|
||||
@@ -71,8 +77,18 @@ impl RoomStoreProviderInterface for Room {
|
||||
store.avatar.set(avatar);
|
||||
}
|
||||
|
||||
fn on_new_topic(&self, topic: Option<String>) {
|
||||
let mut store = self.store.borrow_mut();
|
||||
store.topic.set(topic);
|
||||
}
|
||||
|
||||
fn on_new_member(&self, member: RoomMember) {
|
||||
let mut store = self.store.borrow_mut();
|
||||
|
||||
if member.is_account_user() {
|
||||
store.is_invited.set(false);
|
||||
}
|
||||
|
||||
store.members.write().push(member);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user