Compare commits
99 Commits
cbe32c250e
...
fix/hide-p
Author | SHA1 | Date | |
---|---|---|---|
c6effdfa15
|
|||
f17986fa16 | |||
cc65e7d5ff
|
|||
1f42eaa37c | |||
acbe15ed69
|
|||
4671a5ee51
|
|||
9e7ba84576
|
|||
a8a7b16e9f
|
|||
8684086c74
|
|||
cd0a763c0a
|
|||
4d6d6d3515
|
|||
2bdd0b6a6d
|
|||
a9996d448c
|
|||
d5e92f282a
|
|||
285d4ba590
|
|||
6b8cef176f
|
|||
60756b7e72
|
|||
18ee33d512
|
|||
e2c20e4c64
|
|||
28aa250f58
|
|||
d29a3a0821
|
|||
570c792bf4
|
|||
fb37125740 | |||
6ce395cf7c | |||
a4eae624d3
|
|||
f53e4fbadf
|
|||
b4bc48d576
|
|||
b435bc73a7
|
|||
a741c6ea8e
|
|||
c761b203cc
|
|||
d85a2a97b7
|
|||
403d238463
|
|||
581a3d159a
|
|||
4bbe863a56
|
|||
722d98f5d1
|
|||
219fac87b1
|
|||
c9deeea36f
|
|||
d19a8a7f7d
|
|||
5bc8ac409e
|
|||
6586edf287
|
|||
a533f1869d
|
|||
6b9ef5dc90
|
|||
93b6027c73
|
|||
8026b6fa32
|
|||
8652d56f51
|
|||
c5045c328c | |||
ba96ad77d3
|
|||
5ff06c93f4 | |||
831085e8b6
|
|||
691dc7572a
|
|||
b728f6efcd
|
|||
d8e3d49d95
|
|||
9b2ab337b2
|
|||
deb3e273f4
|
|||
abea905feb
|
|||
c7955d5571
|
|||
f99296bdce
|
|||
7989d86af1
|
|||
d2108fa6fc
|
|||
44ba3d4d23
|
|||
5206fb13c8
|
|||
27934c7fc9
|
|||
9d95bd4481
|
|||
648be8ba72
|
|||
b7b98dff15
|
|||
aaafa91cbe
|
|||
9a5f7ae504
|
|||
d5d996eec3
|
|||
73c5b70ba8
|
|||
f0463213cf
|
|||
e55992aed5
|
|||
cea60ce695
|
|||
271e865d40
|
|||
ffe759e749
|
|||
9baa7f290a
|
|||
d566a4927f
|
|||
1ad4d444fb
|
|||
204e11b8b1
|
|||
f566d88df2
|
|||
89473cfd61
|
|||
62015f8d13
|
|||
c8e8e2da67
|
|||
df32faa8e6
|
|||
5194899de0
|
|||
ff0ac7f982
|
|||
8ed4ff3f2a
|
|||
e7e1a4d663
|
|||
19d64d7ac5
|
|||
35e191eb62
|
|||
8c244ce4a7
|
|||
a1fe74f53e
|
|||
f43f54c120
|
|||
cd6506fb03
|
|||
b5da0ee992
|
|||
df2d924c65
|
|||
54c7073b98
|
|||
fdae149c4a
|
|||
0b898dce52
|
|||
7b6781a007
|
18
.woodpecker/.audit.yaml
Normal file
18
.woodpecker/.audit.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
variables:
|
||||||
|
- shared-config: &shared-config
|
||||||
|
image: rg.fr-par.scw.cloud/asr-projects/beau-gosse-du-92/ci-lint-audit:latest
|
||||||
|
pull: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: dependencies
|
||||||
|
<<: *shared-config
|
||||||
|
commands: |
|
||||||
|
cargo deny check
|
||||||
|
# Not ready to block PR on fail
|
||||||
|
failure: ignore
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- lint
|
18
.woodpecker/.dependencies.yaml
Normal file
18
.woodpecker/.dependencies.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
steps:
|
||||||
|
- name: renovate
|
||||||
|
image: renovate/renovate
|
||||||
|
pull: true
|
||||||
|
commands:
|
||||||
|
- renovate $${CI_REPO}
|
||||||
|
environment:
|
||||||
|
RENOVATE_PLATFORM: gitea
|
||||||
|
RENOVATE_ENDPOINT: https://git.adrien.run
|
||||||
|
RENOVATE_GIT_AUTHOR: renovate-bot <renovate-bot@adrien.run>
|
||||||
|
RENOVATE_TOKEN:
|
||||||
|
from_secret: renovate-bot-mr-token
|
||||||
|
LOG_LEVEL: debug
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: cron
|
||||||
|
cron: renovate
|
||||||
|
- event: manual
|
@@ -16,10 +16,6 @@ steps:
|
|||||||
repo: rg.fr-par.scw.cloud/asr-projects/beau-gosse-du-92-web
|
repo: rg.fr-par.scw.cloud/asr-projects/beau-gosse-du-92-web
|
||||||
container: beau-gosse-du-92-web
|
container: beau-gosse-du-92-web
|
||||||
tag: ${CI_COMMIT_SHA}
|
tag: ${CI_COMMIT_SHA}
|
||||||
secrets:
|
|
||||||
- kubernetes_cert
|
|
||||||
- kubernetes_server
|
|
||||||
- kubernetes_token
|
|
||||||
|
|
||||||
when:
|
when:
|
||||||
- event: push
|
- event: push
|
||||||
|
@@ -6,6 +6,7 @@ steps:
|
|||||||
repo: asr-projects/beau-gosse-du-92-web
|
repo: asr-projects/beau-gosse-du-92-web
|
||||||
tags: ${CI_COMMIT_SHA}
|
tags: ${CI_COMMIT_SHA}
|
||||||
auto_tag: true
|
auto_tag: true
|
||||||
|
cache: false
|
||||||
username: nologin
|
username: nologin
|
||||||
password:
|
password:
|
||||||
from_secret: registry-password
|
from_secret: registry-password
|
||||||
@@ -13,3 +14,6 @@ steps:
|
|||||||
when:
|
when:
|
||||||
- event: push
|
- event: push
|
||||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||||
|
|
||||||
|
# depends_on:
|
||||||
|
# - validate
|
||||||
|
17
.woodpecker/.lint-audit-image.yaml
Normal file
17
.woodpecker/.lint-audit-image.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
steps:
|
||||||
|
dockerize:
|
||||||
|
image: woodpeckerci/plugin-kaniko
|
||||||
|
settings:
|
||||||
|
registry: rg.fr-par.scw.cloud
|
||||||
|
repo: asr-projects/beau-gosse-du-92/ci-lint-audit
|
||||||
|
dockerfile: ./docker/Dockerfile.ci-lint-audit
|
||||||
|
tags: latest
|
||||||
|
auto_tag: false
|
||||||
|
cache: false
|
||||||
|
username: nologin
|
||||||
|
password:
|
||||||
|
from_secret: registry-password
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
path: ./docker/Dockerfile.ci-lint-audit
|
45
.woodpecker/.lint.yaml
Normal file
45
.woodpecker/.lint.yaml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
variables:
|
||||||
|
- shared-config: &shared-config
|
||||||
|
image: rg.fr-par.scw.cloud/asr-projects/beau-gosse-du-92/ci-lint-audit:latest
|
||||||
|
pull: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: format
|
||||||
|
<<: *shared-config
|
||||||
|
commands: |
|
||||||
|
cargo fmt --all --check
|
||||||
|
|
||||||
|
- name: sort derives
|
||||||
|
<<: *shared-config
|
||||||
|
commands: |
|
||||||
|
cargo sort-derives --check
|
||||||
|
|
||||||
|
- name: clippy
|
||||||
|
<<: *shared-config
|
||||||
|
commands: |
|
||||||
|
cargo clippy --all --all-features -- -D warnings
|
||||||
|
# Not ready to block PR on fail
|
||||||
|
failure: ignore
|
||||||
|
|
||||||
|
- name: spellcheck
|
||||||
|
<<: *shared-config
|
||||||
|
commands: |
|
||||||
|
cargo spellcheck
|
||||||
|
|
||||||
|
- name: dependencies
|
||||||
|
<<: *shared-config
|
||||||
|
commands: |
|
||||||
|
cargo udeps
|
||||||
|
|
||||||
|
- name: dockerizable (web)
|
||||||
|
image: woodpeckerci/plugin-kaniko
|
||||||
|
settings:
|
||||||
|
registry: rg.fr-par.scw.cloud
|
||||||
|
repo: asr-projects/beau-gosse-du-92-web
|
||||||
|
username: nologin
|
||||||
|
password:
|
||||||
|
from_secret: registry-password
|
||||||
|
dry-run: true
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: pull_request
|
86
Cargo.toml
86
Cargo.toml
@@ -2,50 +2,82 @@
|
|||||||
name = "beau-gosse-du-92"
|
name = "beau-gosse-du-92"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
desktop = ["dioxus/desktop"]
|
|
||||||
web = ["dioxus/web"]
|
[package.metadata.spellcheck]
|
||||||
|
config = "./spellcheck.toml"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus = "0.5.*"
|
# Errors
|
||||||
dioxus-sdk = { version = "0.5.*", features = ["utils"] }
|
|
||||||
dioxus-free-icons = { version = "0.8.*", features = ["ionicons", "font-awesome-solid", "material-design-icons-navigation"] }
|
|
||||||
modx = "0.1.2"
|
|
||||||
|
|
||||||
# matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", branch = "main", default-features = false, features = ["js", "rustls-tls"] }
|
|
||||||
matrix-sdk = { version = "0.7.*", default-features = false, features = ["js", "rustls-tls"] }
|
|
||||||
|
|
||||||
anyhow = "1.0.75"
|
anyhow = "1.0.75"
|
||||||
url = "2.5.0"
|
|
||||||
dirs = "5.0.1"
|
|
||||||
ctrlc-async = "3.2.2"
|
|
||||||
thiserror = "1.0.50"
|
thiserror = "1.0.50"
|
||||||
turf = "0.8.*"
|
|
||||||
tokio = { version = "1.34.0", default-features = false, features = ["rt", "sync"] }
|
# Async
|
||||||
log = "0.4.20"
|
|
||||||
futures-util = "0.3.29"
|
|
||||||
futures = "0.3.29"
|
|
||||||
rand = "0.8.5"
|
|
||||||
reqwest = "0.11.24"
|
|
||||||
validator = { version = "0.17.0", features = ["derive"] }
|
|
||||||
const_format = "0.2.32"
|
|
||||||
zxcvbn = "2.2.2"
|
|
||||||
async-std = "1.12.0"
|
async-std = "1.12.0"
|
||||||
tracing = "0.1.40"
|
|
||||||
tracing-web = "0.1.3"
|
|
||||||
tracing-subscriber = "0.3.18"
|
|
||||||
git-version = "0.3.9"
|
|
||||||
async-trait = "0.1.80"
|
async-trait = "0.1.80"
|
||||||
|
futures = "0.3.29"
|
||||||
|
futures-util = "0.3.29"
|
||||||
|
tokio = { version = "1.34.0", default-features = false, features = ["rt", "sync"] }
|
||||||
tokio-stream = "0.1.15"
|
tokio-stream = "0.1.15"
|
||||||
|
|
||||||
|
# Utils
|
||||||
|
base64 = "0.22.0"
|
||||||
|
const_format = "0.2.32"
|
||||||
|
rand = "0.8.5"
|
||||||
|
validator = { version = "0.17.0", features = ["derive"] }
|
||||||
|
# Http client
|
||||||
|
reqwest = "0.11.24"
|
||||||
|
# Password strength estimation
|
||||||
|
zxcvbn = "2.2.2"
|
||||||
|
# Image processing/conversion
|
||||||
image = "0.25.1"
|
image = "0.25.1"
|
||||||
|
# Get the application version
|
||||||
|
git-version = "0.3.9"
|
||||||
|
# Conditional compilation
|
||||||
|
cfg-if = "1.0.0"
|
||||||
|
|
||||||
|
# Logging/tracing
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tracing-forest = "0.1.6"
|
||||||
|
|
||||||
|
# SCSS -> CSS + usage in rust code
|
||||||
|
turf = "0.9.3"
|
||||||
|
|
||||||
|
# Dioxus
|
||||||
|
dioxus-free-icons = { version = "0.9", features = ["ionicons", "font-awesome-solid"] }
|
||||||
|
modx = "0.1.4"
|
||||||
|
|
||||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||||
|
# Logging/tracing
|
||||||
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
|
tracing-web = "0.1.3"
|
||||||
|
|
||||||
|
# Dioxus
|
||||||
|
dioxus = { version = "0.6.3", features = ["web"] }
|
||||||
web-sys = "0.3.69"
|
web-sys = "0.3.69"
|
||||||
|
|
||||||
|
# Matrix
|
||||||
|
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", rev = "fa6066b8", 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 = { version = "0.6.3", features = ["desktop"] }
|
||||||
|
|
||||||
|
# Matrix
|
||||||
|
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", rev = "fa6066b8", default-features = false, features = ["rustls-tls"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
regex = "1.10.3"
|
regex = "1.10.3"
|
||||||
|
|
||||||
[package.metadata.turf]
|
[package.metadata.turf]
|
||||||
minify = true
|
minify = true
|
||||||
|
|
||||||
|
@@ -7,14 +7,7 @@ title = "BG92"
|
|||||||
|
|
||||||
[web.watcher]
|
[web.watcher]
|
||||||
reload_html = true
|
reload_html = true
|
||||||
watch_path = ["public/index.html", "public/assets/index.css", "src"]
|
watch_path = ["Dioxus.toml", "public/index.html", "src"]
|
||||||
|
|
||||||
[web.resource]
|
|
||||||
style = ["assets/index.css"]
|
|
||||||
|
|
||||||
[web.resource.dev]
|
|
||||||
style = []
|
|
||||||
script = []
|
|
||||||
|
|
||||||
[[web.proxy]]
|
[[web.proxy]]
|
||||||
backend = "http://localhost:8000/api/"
|
backend = "http://localhost:8000/api/"
|
||||||
|
13
Dockerfile
13
Dockerfile
@@ -1,7 +1,4 @@
|
|||||||
FROM rust:latest AS builder
|
FROM rg.fr-par.scw.cloud/asr-projects/dioxus-cli-0.6.3:latest AS builder
|
||||||
|
|
||||||
# Homemade docker image providing the dioxus-cli
|
|
||||||
COPY --from=rg.fr-par.scw.cloud/asr-projects/dioxus-cli:asr-0.5.2 /usr/local/bin/dx /usr/local/bin/dx
|
|
||||||
|
|
||||||
ARG JOBS_NB=${JOBS_NB:-default}
|
ARG JOBS_NB=${JOBS_NB:-default}
|
||||||
# Disable incremental compilation
|
# Disable incremental compilation
|
||||||
@@ -10,16 +7,18 @@ ARG CARGO_INCREMENTAL=0
|
|||||||
|
|
||||||
WORKDIR /usr/src/beau-gosse-du-92
|
WORKDIR /usr/src/beau-gosse-du-92
|
||||||
|
|
||||||
|
# git is required by the git-version crate
|
||||||
RUN apt update \
|
RUN apt update \
|
||||||
&& apt install -y git-lfs
|
&& apt install -y --no-install-recommends git \
|
||||||
|
&& apt clean
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN /usr/local/bin/dx build -r --platform web -- -j ${JOBS_NB}
|
RUN dx build -r --platform web -- -j ${JOBS_NB}
|
||||||
|
|
||||||
|
|
||||||
FROM nginx:mainline-alpine-slim
|
FROM nginx:mainline-alpine-slim
|
||||||
|
|
||||||
WORKDIR /usr/share/nginx/html
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
COPY --from=builder /usr/src/beau-gosse-du-92/dist .
|
COPY --from=builder /usr/src/beau-gosse-du-92/target/dx/beau-gosse-du-92/release/web/public .
|
||||||
|
12
README.md
12
README.md
@@ -14,8 +14,8 @@ reinvent the wheel. This solution provides:
|
|||||||
- [Open-source protocol](https://spec.matrix.org/v1.9/).
|
- [Open-source protocol](https://spec.matrix.org/v1.9/).
|
||||||
- Features expected for a messaging solution in 2024 (multi-devices management, emojis, integrations, redaction,
|
- Features expected for a messaging solution in 2024 (multi-devices management, emojis, integrations, redaction,
|
||||||
spaces, ...).
|
spaces, ...).
|
||||||
- Multi-platforms clients (Android, iOS and Webclient).
|
- Multi-platforms clients (Android, iOS and web-client).
|
||||||
- SDK available for each platform and a new Rust sdk supporting all the previously listed platforms.
|
- SDK available for each platform and a new Rust SDK supporting all the previously listed platforms.
|
||||||
- Conference stack ([Element Call](https://github.com/element-hq/element-call)).
|
- Conference stack ([Element Call](https://github.com/element-hq/element-call)).
|
||||||
- End-to-end encryption.
|
- End-to-end encryption.
|
||||||
- Federation management.
|
- Federation management.
|
||||||
@@ -28,16 +28,16 @@ API)](https://spec.matrix.org/v1.9/client-server-api/) protocol.
|
|||||||
|
|
||||||
Even if the Rust SDK is still in beta, it seems to be the future one (cf. [Element X - experience the future of
|
Even if the Rust SDK is still in beta, it seems to be the future one (cf. [Element X - experience the future of
|
||||||
Element!](https://element.io/blog/element-x-experience-the-future-of-element/)) and a good choice for someone starting a
|
Element!](https://element.io/blog/element-x-experience-the-future-of-element/)) and a good choice for someone starting a
|
||||||
new client... from my point of vue.
|
new client... from my point of view.
|
||||||
|
|
||||||
The SDK choosen, a Rust (to avoid to use the bindings provided by the matrix-rust-sdk and mostly because I want to
|
The SDK chosen, a Rust (to avoid to use the bindings provided by the matrix-rust-sdk and mostly because I want to
|
||||||
learn Rust) graphical library should be selected. The [Dioxus](https://dioxuslabs.com/) one seems to do the job:
|
learn Rust) graphical library should be selected. The [Dioxus](https://dioxuslabs.com/) one seems to do the job:
|
||||||
|
|
||||||
- React-inspired library for Rust.
|
- React-inspired library for Rust.
|
||||||
- Multi-platforms (use of Webview or WGPU-enabled renderers).
|
- Multi-platforms (use of Web-view or WGPU-enabled renderers).
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
- [ ] Test dioxus-radio.
|
- [ ] Test dioxus-radio.
|
||||||
- [ ] Design system ?
|
- [ ] Design system ?
|
||||||
- [ ] Implement MSN messenger features using Matrix.org sdk...
|
- [ ] Implement MSN messenger features using Matrix.org SDK...
|
||||||
|
139
build.rs
139
build.rs
@@ -1,4 +1,5 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
use std::fmt::Display;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::io::{self, BufRead};
|
use std::io::{self, BufRead};
|
||||||
@@ -10,14 +11,33 @@ use regex::Regex;
|
|||||||
fn main() {
|
fn main() {
|
||||||
// Tell Cargo to rerun this build script if any SCSS file
|
// Tell Cargo to rerun this build script if any SCSS file
|
||||||
// in the 'src' directory or its subdirectories changes.
|
// 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 out_dir = env::var("OUT_DIR").unwrap();
|
||||||
|
|
||||||
let style_src_path = PathBuf::from("src/ui/_base.scss");
|
// let mut tasks = Vec::new();
|
||||||
let style_dst_path = Path::new(&out_dir).join("style_vars.rs");
|
let tasks = vec![
|
||||||
|
// Global tokens
|
||||||
|
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
|
||||||
|
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
|
||||||
|
Task::new(
|
||||||
|
PathBuf::from("src/ui/layouts/conversations.scss"),
|
||||||
|
Path::new(&out_dir).join("style_layout_conversations.rs"),
|
||||||
|
"conversations".to_string(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
export_color_variables(&style_src_path, &style_dst_path)
|
export_variables(tasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
// From https://doc.rust-lang.org/rust-by-example/std_misc/file/read_lines.html
|
// From https://doc.rust-lang.org/rust-by-example/std_misc/file/read_lines.html
|
||||||
@@ -32,14 +52,21 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct CssColorVariable<'a> {
|
struct ColorVariable {
|
||||||
name: &'a str,
|
name: String,
|
||||||
value: &'a str,
|
value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CssColorVariable<'a> {
|
impl ColorVariable {
|
||||||
pub fn to_rust(&self) -> String {
|
pub fn new(name: String, value: String) -> Self {
|
||||||
format!(
|
Self { name, value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ColorVariable {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
"const {name}: &str = \"{value}\";",
|
"const {name}: &str = \"{value}\";",
|
||||||
name = self.name.replace('-', "_").to_uppercase(),
|
name = self.name.replace('-', "_").to_uppercase(),
|
||||||
value = self.value
|
value = self.value
|
||||||
@@ -47,35 +74,83 @@ impl<'a> CssColorVariable<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn export_color_variables(src_path: &PathBuf, dst_path: &PathBuf) {
|
#[derive(Debug)]
|
||||||
let mut dst_file = File::create(dst_path).unwrap();
|
struct FloatVariable {
|
||||||
if let Err(err) = dst_file.write(b"#[allow(dead_code)]\nmod style {") {
|
name: String,
|
||||||
println!("{}", err);
|
value: f64,
|
||||||
return;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let re = Regex::new(r"^\$([^:]+):[[:space:]]*#([^$]+);[[:space:]]*$").unwrap();
|
impl FloatVariable {
|
||||||
|
pub fn new(name: String, value: f64) -> Self {
|
||||||
|
Self { name, value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(lines) = read_lines(src_path) {
|
impl Display for FloatVariable {
|
||||||
for line in lines.map_while(Result::ok) {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let Some(groups) = re.captures(&line) else {
|
write!(
|
||||||
continue;
|
f,
|
||||||
};
|
"const {name}: f64 = {value};",
|
||||||
|
name = self.name.replace('-', "_").to_uppercase(),
|
||||||
|
value = self.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let var = CssColorVariable {
|
struct Task {
|
||||||
name: &groups[1],
|
src_path: PathBuf,
|
||||||
value: &groups[2],
|
dst_path: PathBuf,
|
||||||
};
|
module_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
let rust_export = var.to_rust();
|
impl Task {
|
||||||
if let Err(err) = dst_file.write_fmt(format_args!(" pub {}\n", rust_export)) {
|
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 mut variables = Vec::<Box<dyn Display>>::new();
|
||||||
|
if let Ok(lines) = read_lines(task.src_path) {
|
||||||
|
for line in lines.map_while(Result::ok) {
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for variable in variables {
|
||||||
|
if let Err(err) = dst_file.write_fmt(format_args!(" pub {}\n", variable)) {
|
||||||
println!("{}", err);
|
println!("{}", err);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(err) = dst_file.write(b"}\n") {
|
if let Err(err) = dst_file.write(b"}\n") {
|
||||||
println!("{}", err);
|
println!("{}", err);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
298
deny.toml
Normal file
298
deny.toml
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
# This template contains all of the possible sections and their default values
|
||||||
|
|
||||||
|
# Note that all fields that take a lint level have these possible values:
|
||||||
|
# * deny - An error will be produced and the check will fail
|
||||||
|
# * warn - A warning will be produced, but the check will not fail
|
||||||
|
# * allow - No warning or error will be produced, though in some cases a note
|
||||||
|
# will be
|
||||||
|
|
||||||
|
# The values provided in this template are the default values that will be used
|
||||||
|
# when any section or field is not specified in your own configuration
|
||||||
|
|
||||||
|
# Root options
|
||||||
|
|
||||||
|
# The graph table configures how the dependency graph is constructed and thus
|
||||||
|
# which crates the checks are performed against
|
||||||
|
[graph]
|
||||||
|
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||||
|
# only the specified targets will be checked when running `cargo deny check`.
|
||||||
|
# This means, if a particular package is only ever used as a target specific
|
||||||
|
# dependency, such as, for example, the `nix` crate only being used via the
|
||||||
|
# `target_family = "unix"` configuration, that only having windows targets in
|
||||||
|
# this list would mean the nix crate, as well as any of its exclusive
|
||||||
|
# dependencies not shared by any other crates, would be ignored, as the target
|
||||||
|
# list here is effectively saying which targets you are building for.
|
||||||
|
targets = [
|
||||||
|
# The triple can be any string, but only the target triples built in to
|
||||||
|
# rustc (as of 1.40) can be checked against actual config expressions
|
||||||
|
#"x86_64-unknown-linux-musl",
|
||||||
|
# You can also specify which target_features you promise are enabled for a
|
||||||
|
# particular target. target_features are currently not validated against
|
||||||
|
# the actual valid features supported by the target architecture.
|
||||||
|
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||||
|
]
|
||||||
|
# When creating the dependency graph used as the source of truth when checks are
|
||||||
|
# executed, this field can be used to prune crates from the graph, removing them
|
||||||
|
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
|
||||||
|
# is pruned from the graph, all of its dependencies will also be pruned unless
|
||||||
|
# they are connected to another crate in the graph that hasn't been pruned,
|
||||||
|
# so it should be used with care. The identifiers are [Package ID Specifications]
|
||||||
|
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
|
||||||
|
#exclude = []
|
||||||
|
# If true, metadata will be collected with `--all-features`. Note that this can't
|
||||||
|
# be toggled off if true, if you want to conditionally enable `--all-features` it
|
||||||
|
# is recommended to pass `--all-features` on the cmd line instead
|
||||||
|
all-features = false
|
||||||
|
# If true, metadata will be collected with `--no-default-features`. The same
|
||||||
|
# caveat with `all-features` applies
|
||||||
|
no-default-features = false
|
||||||
|
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||||
|
# is specified on the cmd line they will take precedence over this option.
|
||||||
|
#features = []
|
||||||
|
|
||||||
|
# The output table provides options for how/if diagnostics are outputted
|
||||||
|
[output]
|
||||||
|
# When outputting inclusion graphs in diagnostics that include features, this
|
||||||
|
# option can be used to specify the depth at which feature edges will be added.
|
||||||
|
# This option is included since the graphs can be quite large and the addition
|
||||||
|
# of features from the crate(s) to all of the graph roots can be far too verbose.
|
||||||
|
# This option can be overridden via `--feature-depth` on the cmd line
|
||||||
|
feature-depth = 1
|
||||||
|
|
||||||
|
# This section is considered when running `cargo deny check advisories`
|
||||||
|
# More documentation for the advisories section can be found here:
|
||||||
|
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||||
|
[advisories]
|
||||||
|
# The path where the advisory databases are cloned/fetched into
|
||||||
|
#db-path = "$CARGO_HOME/advisory-dbs"
|
||||||
|
# The url(s) of the advisory databases to use
|
||||||
|
#db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||||
|
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||||
|
# output a note when they are encountered.
|
||||||
|
ignore = [
|
||||||
|
#"RUSTSEC-0000-0000",
|
||||||
|
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||||
|
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||||
|
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||||
|
]
|
||||||
|
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||||
|
# If this is false, then it uses a built-in git library.
|
||||||
|
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||||
|
# See Git Authentication for more information about setting up git authentication.
|
||||||
|
#git-fetch-with-cli = true
|
||||||
|
|
||||||
|
# This section is considered when running `cargo deny check licenses`
|
||||||
|
# More documentation for the licenses section can be found here:
|
||||||
|
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||||
|
[licenses]
|
||||||
|
version = 2
|
||||||
|
# List of explicitly allowed licenses
|
||||||
|
# See https://spdx.org/licenses/ for list of possible licenses
|
||||||
|
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||||
|
allow = [
|
||||||
|
# Free software licenses compatible with (A)GPL.
|
||||||
|
# List extracted from: https://www.gnu.org/licenses/license-list.en.html#GPLCompatibleLicenses
|
||||||
|
# "GPL-3.0",
|
||||||
|
# "GPL-2.0",
|
||||||
|
"LGPL-3.0",
|
||||||
|
# "LGPL-2.1",
|
||||||
|
"AGPL-3.0",
|
||||||
|
# "FSFAP",
|
||||||
|
"Apache-2.0",
|
||||||
|
"Apache-2.0 WITH LLVM-exception",
|
||||||
|
# "Artistic-2.0",
|
||||||
|
# "ClArtistic",
|
||||||
|
# "Sleepycat",
|
||||||
|
"BSL-1.0",
|
||||||
|
"BSD-3-Clause",
|
||||||
|
# "CECILL-2.0",
|
||||||
|
# "BSD-3-Clause-Clear",
|
||||||
|
# "ECL-2.0",
|
||||||
|
# "EFL-2.0",
|
||||||
|
# "EUDatagrid",
|
||||||
|
"MIT",
|
||||||
|
"BSD-2-Clause",
|
||||||
|
# "FTL",
|
||||||
|
# "HPND",
|
||||||
|
# "iMatix",
|
||||||
|
# "Imlib2",
|
||||||
|
# "IJG",
|
||||||
|
# "Intel",
|
||||||
|
"ISC",
|
||||||
|
"MPL-2.0",
|
||||||
|
"NCSA",
|
||||||
|
# "OLDAP-2.7",
|
||||||
|
# "NIST-PD",
|
||||||
|
# "CC-PDDC",
|
||||||
|
"CC0-1.0",
|
||||||
|
# "Python-2.0",
|
||||||
|
# "Ruby",
|
||||||
|
# "SGI-B-2.0",
|
||||||
|
# "SMLNJ",
|
||||||
|
# "UPL-1.0",
|
||||||
|
"Unlicense",
|
||||||
|
# "Vim",
|
||||||
|
# "W3C",
|
||||||
|
# "WTFPL",
|
||||||
|
# "X11",
|
||||||
|
# "XFree86-1.1",
|
||||||
|
"Zlib",
|
||||||
|
# "zlib-acknowledgement",
|
||||||
|
# "ZPL-2.0",
|
||||||
|
# "ZPL-2.1",
|
||||||
|
# Not expressely listed as (A)GPL compatible in the page above, but
|
||||||
|
# according to https://opensource.org/licenses/0BSD it is a modification
|
||||||
|
# of the ISC license, which is compatible. Its text is also extremely
|
||||||
|
# simple and allows using the code for any purpose
|
||||||
|
"0BSD",
|
||||||
|
# Permissive license used by the Unicode consortium, similar in spirit
|
||||||
|
# to other permissive licenses:
|
||||||
|
# https://spdx.org/licenses/Unicode-DFS-2016.html
|
||||||
|
"Unicode-DFS-2016",
|
||||||
|
# Permissive license used by the Unicode consortium, similar in spirit
|
||||||
|
# to other permissive licenses:
|
||||||
|
# https://spdx.org/licenses/Unicode-3.0.html
|
||||||
|
"Unicode-3.0",
|
||||||
|
]
|
||||||
|
# The confidence threshold for detecting a license from license text.
|
||||||
|
# The higher the value, the more closely the license text must be to the
|
||||||
|
# canonical license text of a valid SPDX license file.
|
||||||
|
# [possible values: any between 0.0 and 1.0].
|
||||||
|
confidence-threshold = 0.8
|
||||||
|
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||||
|
# aren't accepted for every possible crate as with the normal allow list
|
||||||
|
exceptions = [
|
||||||
|
# Each entry is the crate and version constraint, and its specific allow
|
||||||
|
# list
|
||||||
|
#{ allow = ["Zlib"], crate = "adler32" },
|
||||||
|
]
|
||||||
|
|
||||||
|
# Some crates don't have (easily) machine readable licensing information,
|
||||||
|
# adding a clarification entry for it allows you to manually specify the
|
||||||
|
# licensing information
|
||||||
|
# [[licenses.clarify]]
|
||||||
|
# The package spec the clarification applies to
|
||||||
|
# crate = "ring"
|
||||||
|
# The SPDX expression for the license requirements of the crate
|
||||||
|
# expression = "MIT AND ISC AND OpenSSL"
|
||||||
|
# One or more files in the crate's source used as the "source of truth" for
|
||||||
|
# the license expression. If the contents match, the clarification will be used
|
||||||
|
# when running the license check, otherwise the clarification will be ignored
|
||||||
|
# and the crate will be checked normally, which may produce warnings or errors
|
||||||
|
# depending on the rest of your configuration
|
||||||
|
# license-files = [
|
||||||
|
#Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||||
|
# { path = "LICENSE", hash = 0xbd0eed23 }
|
||||||
|
# ]
|
||||||
|
|
||||||
|
[licenses.private]
|
||||||
|
# If true, ignores workspace crates that aren't published, or are only
|
||||||
|
# published to private registries.
|
||||||
|
# To see how to mark a crate as unpublished (to the official registry),
|
||||||
|
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
||||||
|
ignore = false
|
||||||
|
# One or more private registries that you might publish crates to, if a crate
|
||||||
|
# is only published to private registries, and ignore is true, the crate will
|
||||||
|
# not have its license(s) checked
|
||||||
|
registries = [
|
||||||
|
#"https://sekretz.com/registry
|
||||||
|
]
|
||||||
|
|
||||||
|
# This section is considered when running `cargo deny check bans`.
|
||||||
|
# More documentation about the 'bans' section can be found here:
|
||||||
|
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||||
|
[bans]
|
||||||
|
# Lint level for when multiple versions of the same crate are detected
|
||||||
|
multiple-versions = "warn"
|
||||||
|
# Lint level for when a crate version requirement is `*`
|
||||||
|
wildcards = "allow"
|
||||||
|
# The graph highlighting used when creating dotgraphs for crates
|
||||||
|
# with multiple versions
|
||||||
|
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
||||||
|
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||||
|
# * all - Both lowest-version and simplest-path are used
|
||||||
|
highlight = "all"
|
||||||
|
# The default lint level for `default` features for crates that are members of
|
||||||
|
# the workspace that is being checked. This can be overridden by allowing/denying
|
||||||
|
# `default` on a crate-by-crate basis if desired.
|
||||||
|
workspace-default-features = "allow"
|
||||||
|
# The default lint level for `default` features for external crates that are not
|
||||||
|
# members of the workspace. This can be overridden by allowing/denying `default`
|
||||||
|
# on a crate-by-crate basis if desired.
|
||||||
|
external-default-features = "allow"
|
||||||
|
# List of crates that are allowed. Use with care!
|
||||||
|
allow = [
|
||||||
|
#"ansi_term@0.11.0",
|
||||||
|
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||||
|
]
|
||||||
|
# List of crates to deny
|
||||||
|
deny = [
|
||||||
|
#"ansi_term@0.11.0",
|
||||||
|
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||||
|
# Wrapper crates can optionally be specified to allow the crate when it
|
||||||
|
# is a direct dependency of the otherwise banned crate
|
||||||
|
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||||
|
]
|
||||||
|
|
||||||
|
# List of features to allow/deny
|
||||||
|
# Each entry the name of a crate and a version range. If version is
|
||||||
|
# not specified, all versions will be matched.
|
||||||
|
#[[bans.features]]
|
||||||
|
#crate = "reqwest"
|
||||||
|
# Features to not allow
|
||||||
|
#deny = ["json"]
|
||||||
|
# Features to allow
|
||||||
|
#allow = [
|
||||||
|
# "rustls",
|
||||||
|
# "__rustls",
|
||||||
|
# "__tls",
|
||||||
|
# "hyper-rustls",
|
||||||
|
# "rustls",
|
||||||
|
# "rustls-pemfile",
|
||||||
|
# "rustls-tls-webpki-roots",
|
||||||
|
# "tokio-rustls",
|
||||||
|
# "webpki-roots",
|
||||||
|
#]
|
||||||
|
# If true, the allowed features must exactly match the enabled feature set. If
|
||||||
|
# this is set there is no point setting `deny`
|
||||||
|
#exact = true
|
||||||
|
|
||||||
|
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||||
|
skip = [
|
||||||
|
#"ansi_term@0.11.0",
|
||||||
|
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||||
|
]
|
||||||
|
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||||
|
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||||
|
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||||
|
# by default infinite.
|
||||||
|
skip-tree = [
|
||||||
|
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||||
|
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||||
|
]
|
||||||
|
|
||||||
|
# This section is considered when running `cargo deny check sources`.
|
||||||
|
# More documentation about the 'sources' section can be found here:
|
||||||
|
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||||
|
[sources]
|
||||||
|
# Lint level for what to happen when a crate from a crate registry that is not
|
||||||
|
# in the allow list is encountered
|
||||||
|
unknown-registry = "warn"
|
||||||
|
# Lint level for what to happen when a crate from a git repository that is not
|
||||||
|
# in the allow list is encountered
|
||||||
|
unknown-git = "warn"
|
||||||
|
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
||||||
|
# if not specified. If it is specified but empty, no registries are allowed.
|
||||||
|
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||||
|
# List of URLs for allowed Git repositories
|
||||||
|
allow-git = [
|
||||||
|
"https://github.com/matrix-org/matrix-rust-sdk.git"
|
||||||
|
]
|
||||||
|
|
||||||
|
[sources.allow-org]
|
||||||
|
# github.com organizations to allow git sources for
|
||||||
|
github = []
|
||||||
|
# gitlab.com organizations to allow git sources for
|
||||||
|
gitlab = []
|
||||||
|
# bitbucket.org organizations to allow git sources for
|
||||||
|
bitbucket = []
|
26
docker/Dockerfile.ci-lint-audit
Normal file
26
docker/Dockerfile.ci-lint-audit
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
FROM rust:1.86 AS builder
|
||||||
|
|
||||||
|
RUN apt update \
|
||||||
|
&& apt install -y --no-install-recommends libclang-dev hunspell \
|
||||||
|
&& apt clean
|
||||||
|
|
||||||
|
RUN rustup default nightly \
|
||||||
|
&& rustup component add rustfmt clippy
|
||||||
|
|
||||||
|
RUN cargo install cargo-binstall
|
||||||
|
|
||||||
|
RUN cargo binstall cargo-sort-derives cargo-spellcheck cargo-udeps cargo-deny
|
||||||
|
|
||||||
|
|
||||||
|
FROM debian:trixie-slim
|
||||||
|
|
||||||
|
RUN apt update \
|
||||||
|
&& apt install -y --no-install-recommends ca-certificates git rustup build-essential \
|
||||||
|
libssl-dev pkg-config libglib2.0-0 libpango-1.0-0 libatk1.0-dev libgdk-pixbuf-2.0-dev \
|
||||||
|
libcairo2-dev libgtk-3-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev \
|
||||||
|
&& apt clean
|
||||||
|
|
||||||
|
COPY --from=builder /usr/local/rustup/toolchains/nightly-x86_64-unknown-linux-gnu/ /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/
|
||||||
|
COPY --from=builder /usr/local/cargo/bin /root/.cargo/bin/
|
||||||
|
|
||||||
|
RUN rustup default nightly
|
@@ -1,207 +0,0 @@
|
|||||||
|
|
||||||
@font-face {
|
|
||||||
src: url("../fonts/Geist/Geist-Medium.woff2") format("woff2");
|
|
||||||
font-family: "Geist";
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
src: url("../fonts/Geist/Geist-Bold.woff2") format("woff2");
|
|
||||||
font-family: "Geist";
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
font-family: "Geist";
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
#main {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes multicolor {
|
|
||||||
0% { fill: #1DB2CF; /* color-primary-100 */ }
|
|
||||||
33% { fill: #7E6BB6; /* color-secondary-100 */ }
|
|
||||||
66% { fill: #D53583; /* color-ternary-100 */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--wallpaper-pattern-height: 128px;
|
|
||||||
--spinner-height: 5%;
|
|
||||||
--window-center-pos: calc(50% + (var(--wallpaper-pattern-height) / 2) - (var(--spinner-height) / 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @media (0px < height <= calc(var(--wallpaper-pattern-height) * 5)) { */
|
|
||||||
@media (min-height: 0px) and (max-height: 640px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: var(--window-center-pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 5) < height <= calc($wallpaper-pattern-height * 6)) { */
|
|
||||||
@media (min-height: 641px) and (max-height: 768px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 6) < height <= calc($wallpaper-pattern-height * 8)) { */
|
|
||||||
@media (min-height: 769px) and (max-height: 1024px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 8) < height <= calc($wallpaper-pattern-height * 10)) { */
|
|
||||||
@media (min-height: 1025px) and (max-height: 1280px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 10) < height <= calc($wallpaper-pattern-height * 12)) { */
|
|
||||||
@media (min-height: 1281px) and (max-height: 1536px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 4));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 12) < height <= calc($wallpaper-pattern-height * 14)) { */
|
|
||||||
@media (min-height: 1537px) and (max-height: 1792px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 14) < height <= calc($wallpaper-pattern-height * 16)) { */
|
|
||||||
@media (min-height: 1793px) and (max-height: 2048px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 6));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 16) < height <= calc($wallpaper-pattern-height * 18)) { */
|
|
||||||
@media (min-height: 2049px) and (max-height: 2304px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 7));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 18) < height <= calc($wallpaper-pattern-height * 20)) { */
|
|
||||||
@media (min-height: 2305px) and (max-height: 2560px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 8));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 20) < height <= calc($wallpaper-pattern-height * 22)) { */
|
|
||||||
@media (min-height: 2561px) and (max-height: 2816px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 9));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 22) < height <= calc($wallpaper-pattern-height * 24)) { */
|
|
||||||
@media (min-height: 2817px) and (max-height: 3072px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 10));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 24) < height <= calc($wallpaper-pattern-height * 26)) { */
|
|
||||||
@media (min-height: 3073px) and (max-height: 3328px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 11));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 26) < height <= calc($wallpaper-pattern-height * 28)) { */
|
|
||||||
@media (min-height: 3329px) and (max-height: 3584px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 12));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 28) < height <= calc($wallpaper-pattern-height * 30)) { */
|
|
||||||
@media (min-height: 3585px) and (max-height: 3840px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 13));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 30) < height <= calc($wallpaper-pattern-height * 32)) { */
|
|
||||||
@media (min-height: 3841px) and (max-height: 4096px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 14));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 32) < height <= calc($wallpaper-pattern-height * 34)) { */
|
|
||||||
@media (min-height: 4097px) and (max-height: 4352px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 15));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 34) < height <= calc($wallpaper-pattern-height * 36)) { */
|
|
||||||
@media (min-height: 4353px) and (max-height: 4608px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 16));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 36) < height <= calc($wallpaper-pattern-height * 38)) { */
|
|
||||||
@media (min-height: 4609px) and (max-height: 4864px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 17));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* @media (calc($wallpaper-pattern-height * 38) < height <= calc($wallpaper-pattern-height * 40)) { */
|
|
||||||
@media (min-height: 4865px) and (max-height: 5120px) {
|
|
||||||
:root {
|
|
||||||
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 18));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.wallpaper {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
z-index: -1;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
background-image: url("../images/wallpaper-pattern.svg");
|
|
||||||
background-position: center;
|
|
||||||
|
|
||||||
width: 150%;
|
|
||||||
height: 150%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
height: var(--spinner-height);
|
|
||||||
aspect-ratio: 2;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
top: var(--spinner-top);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
--fps: 4;
|
|
||||||
--duration_sec: 3;
|
|
||||||
--steps: calc(var(--duration_sec) * var(--fps));
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
fill: #1DB2CF; /* color-primary-100 */
|
|
||||||
stroke: #1B1B1B; /* greyscale-90 */
|
|
||||||
|
|
||||||
animation: 3s multicolor linear infinite;
|
|
||||||
animation-timing-function: steps(var(--steps), end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,4 +1,3 @@
|
|||||||
<!-- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1152 128"> -->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="128" width="384" viewBox="0 0 384 128">
|
<svg xmlns="http://www.w3.org/2000/svg" height="128" width="384" viewBox="0 0 384 128">
|
||||||
<pattern id="p" width="384" height="128" patternUnits="userSpaceOnUse" stroke="#1B1B1B" stroke-linejoin="round" stroke-width="4">
|
<pattern id="p" width="384" height="128" patternUnits="userSpaceOnUse" stroke="#1B1B1B" stroke-linejoin="round" stroke-width="4">
|
||||||
<path fill="#1DB2CF" d="M 9.736 -15 L -30 3.337 l 23.642 -0.088 L -10.212 15 L 30 -3.425 H 6.834 L 9.736 -15 Z"/>
|
<path fill="#1DB2CF" d="M 9.736 -15 L -30 3.337 l 23.642 -0.088 L -10.212 15 L 30 -3.425 H 6.834 L 9.736 -15 Z"/>
|
||||||
@@ -6,9 +5,9 @@
|
|||||||
<path fill="#1DB2CF" d="M 393.736 -15 L 354 3.337 l 23.642 -0.088 L 373.788 15 L 414 -3.425 H 390.834 L 393.736 -15 Z"/>
|
<path fill="#1DB2CF" d="M 393.736 -15 L 354 3.337 l 23.642 -0.088 L 373.788 15 L 414 -3.425 H 390.834 L 393.736 -15 Z"/>
|
||||||
<path fill="#7E6BB6" d="M 105.736 50 L 66 68.337 l 23.642 -0.088 L 85.788 80 L 126 61.575 H 102.834 L 105.736 50 Z"/>
|
<path fill="#7E6BB6" d="M 105.736 50 L 66 68.337 l 23.642 -0.088 L 85.788 80 L 126 61.575 H 102.834 L 105.736 50 Z"/>
|
||||||
<path fill="#7E6BB6" d="M 297.736 50 L 258 68.337 l 23.642 -0.088 L 277.788 80 L 318 61.575 H 294.834 L 297.736 50 Z"/>
|
<path fill="#7E6BB6" d="M 297.736 50 L 258 68.337 l 23.642 -0.088 L 277.788 80 L 318 61.575 H 294.834 L 297.736 50 Z"/>
|
||||||
<path fill="#1DB2CF" d="M 9.736 114 L -30 132.337 l 23.642 -0.088 L -10.212 144 L 30 125.575 H 6.834 L 9.736 114 Z"/>
|
<path fill="#1DB2CF" d="M 9.736 113 L -30 131.337 l 23.642 -0.088 L -10.212 143 L 30 124.575 H 6.834 L 9.736 113 Z"/>
|
||||||
<path fill="#D53583" d="M 201.736 114 L 162 132.337 l 23.642 -0.088 L 181.788 144 L 222 125.575 H 198.834 L 201.736 114 Z"/>
|
<path fill="#D53583" d="M 201.736 113 L 162 131.337 l 23.642 -0.088 L 181.788 143 L 222 124.575 H 198.834 L 201.736 113 Z"/>
|
||||||
<path fill="#1DB2CF" d="M 393.736 114 L 354 132.337 l 23.642 -0.088 L 373.788 144 L 414 125.575 H 390.834 L 393.736 114 Z"/>
|
<path fill="#1DB2CF" d="M 393.736 113 L 354 131.337 l 23.642 -0.088 L 373.788 143 L 414 124.575 H 390.834 L 393.736 113 Z"/>
|
||||||
</pattern>
|
</pattern>
|
||||||
<rect fill="url(#p)" width="100%" height="100%"/>
|
<rect fill="url(#p)" width="100%" height="100%"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,16 +1,207 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<!-- Hack to avoid the addition of the script part by dioxus-cli -->
|
<title>{app_name}</title>
|
||||||
<!-- <title>{base_path}</title> -->
|
|
||||||
<!-- <title>{app_name}</title> -->
|
|
||||||
|
|
||||||
<title></title>
|
|
||||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
{style_include}
|
<style>
|
||||||
{script_include}
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
font-family: "Geist";
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes multicolor {
|
||||||
|
0% { fill: #1DB2CF; /* color-primary-100 */ }
|
||||||
|
33% { fill: #7E6BB6; /* color-secondary-100 */ }
|
||||||
|
66% { fill: #D53583; /* color-ternary-100 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--wallpaper-pattern-height: 128px;
|
||||||
|
--spinner-height: 5%;
|
||||||
|
--window-center-pos: calc(50% + (var(--wallpaper-pattern-height) / 2) - (var(--spinner-height) / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @media (0px < height <= calc(var(--wallpaper-pattern-height) * 5)) { */
|
||||||
|
@media (min-height: 0px) and (max-height: 640px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: var(--window-center-pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 5) < height <= calc($wallpaper-pattern-height * 6)) { */
|
||||||
|
@media (min-height: 641px) and (max-height: 768px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 6) < height <= calc($wallpaper-pattern-height * 8)) { */
|
||||||
|
@media (min-height: 769px) and (max-height: 1024px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 8) < height <= calc($wallpaper-pattern-height * 10)) { */
|
||||||
|
@media (min-height: 1025px) and (max-height: 1280px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 10) < height <= calc($wallpaper-pattern-height * 12)) { */
|
||||||
|
@media (min-height: 1281px) and (max-height: 1536px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 12) < height <= calc($wallpaper-pattern-height * 14)) { */
|
||||||
|
@media (min-height: 1537px) and (max-height: 1792px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 14) < height <= calc($wallpaper-pattern-height * 16)) { */
|
||||||
|
@media (min-height: 1793px) and (max-height: 2048px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 16) < height <= calc($wallpaper-pattern-height * 18)) { */
|
||||||
|
@media (min-height: 2049px) and (max-height: 2304px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 7));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 18) < height <= calc($wallpaper-pattern-height * 20)) { */
|
||||||
|
@media (min-height: 2305px) and (max-height: 2560px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 20) < height <= calc($wallpaper-pattern-height * 22)) { */
|
||||||
|
@media (min-height: 2561px) and (max-height: 2816px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 9));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 22) < height <= calc($wallpaper-pattern-height * 24)) { */
|
||||||
|
@media (min-height: 2817px) and (max-height: 3072px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 24) < height <= calc($wallpaper-pattern-height * 26)) { */
|
||||||
|
@media (min-height: 3073px) and (max-height: 3328px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 11));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 26) < height <= calc($wallpaper-pattern-height * 28)) { */
|
||||||
|
@media (min-height: 3329px) and (max-height: 3584px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 12));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 28) < height <= calc($wallpaper-pattern-height * 30)) { */
|
||||||
|
@media (min-height: 3585px) and (max-height: 3840px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 13));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 30) < height <= calc($wallpaper-pattern-height * 32)) { */
|
||||||
|
@media (min-height: 3841px) and (max-height: 4096px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 14));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 32) < height <= calc($wallpaper-pattern-height * 34)) { */
|
||||||
|
@media (min-height: 4097px) and (max-height: 4352px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 34) < height <= calc($wallpaper-pattern-height * 36)) { */
|
||||||
|
@media (min-height: 4353px) and (max-height: 4608px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 36) < height <= calc($wallpaper-pattern-height * 38)) { */
|
||||||
|
@media (min-height: 4609px) and (max-height: 4864px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 17));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* @media (calc($wallpaper-pattern-height * 38) < height <= calc($wallpaper-pattern-height * 40)) { */
|
||||||
|
@media (min-height: 4865px) and (max-height: 5120px) {
|
||||||
|
:root {
|
||||||
|
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 18));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.wallpaper {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
background-image: url("data:image/svg+xml, <svg xmlns='http://www.w3.org/2000/svg' height='128' width='384' viewBox='0 0 384 128'><pattern id='p' width='384' height='128' patternUnits='userSpaceOnUse' stroke='%231B1B1B' stroke-linejoin='round' stroke-width='4'><path fill='%231DB2CF' d='M 9.736 -15 L -30 3.337 l 23.642 -0.088 L -10.212 15 L 30 -3.425 H 6.834 L 9.736 -15 Z'/><path fill='%23D53583' d='M 201.736 -15 L 162 3.337 l 23.642 -0.088 L 181.788 15 L 222 -3.425 H 198.834 L 201.736 -15 Z'/><path fill='%231DB2CF' d='M 393.736 -15 L 354 3.337 l 23.642 -0.088 L 373.788 15 L 414 -3.425 H 390.834 L 393.736 -15 Z'/><path fill='%237E6BB6' d='M 105.736 50 L 66 68.337 l 23.642 -0.088 L 85.788 80 L 126 61.575 H 102.834 L 105.736 50 Z'/><path fill='%237E6BB6' d='M 297.736 50 L 258 68.337 l 23.642 -0.088 L 277.788 80 L 318 61.575 H 294.834 L 297.736 50 Z'/><path fill='%231DB2CF' d='M 9.736 113 L -30 131.337 l 23.642 -0.088 L -10.212 143 L 30 124.575 H 6.834 L 9.736 113 Z'/><path fill='%23D53583' d='M 201.736 113 L 162 131.337 l 23.642 -0.088 L 181.788 143 L 222 124.575 H 198.834 L 201.736 113 Z'/><path fill='%231DB2CF' d='M 393.736 113 L 354 131.337 l 23.642 -0.088 L 373.788 143 L 414 124.575 H 390.834 L 393.736 113 Z'/></pattern><rect fill='url(%23p)' width='100%' height='100%'/></svg>");
|
||||||
|
background-position: center;
|
||||||
|
backgrond-size: var(--wallpaper-pattern-height);
|
||||||
|
|
||||||
|
width: 150%;
|
||||||
|
height: 150%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
height: var(--spinner-height);
|
||||||
|
aspect-ratio: 2;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: var(--spinner-top);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
--fps: 4;
|
||||||
|
--duration_sec: 3;
|
||||||
|
--steps: calc(var(--duration_sec) * var(--fps));
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
fill: #1DB2CF; /* color-primary-100 */
|
||||||
|
stroke: #1B1B1B; /* greyscale-90 */
|
||||||
|
|
||||||
|
animation: 3s multicolor linear infinite;
|
||||||
|
animation-timing-function: steps(var(--steps), end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="main">
|
<div id="main">
|
||||||
@@ -19,7 +210,7 @@
|
|||||||
<div class="content"></div>
|
<div class="content"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="spinner">
|
<div class="spinner">
|
||||||
<svg viewBox="0 0 184 94" xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg" width='184' height='94' viewBox="0 0 184 94">
|
||||||
<path
|
<path
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="6"
|
stroke-width="6"
|
||||||
@@ -29,25 +220,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" async>
|
|
||||||
import init from "{base_path}/assets/dioxus/beau-gosse-du-92.js";
|
|
||||||
|
|
||||||
// Ensure that everything needed to render preloader has been downloaded
|
|
||||||
// before fetching the wasm bundle.
|
|
||||||
window.onload=function() {
|
|
||||||
init("{base_path}/assets/dioxus/beau-gosse-du-92_bg.wasm").then(
|
|
||||||
wasm => {
|
|
||||||
const preloader = document.getElementById("preloader");
|
|
||||||
if (preloader !== undefined) {
|
|
||||||
preloader.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wasm.__wbindgen_start == undefined) {
|
|
||||||
wasm.main();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
3
renovate.json
Normal file
3
renovate.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
6
spellcheck.dic
Normal file
6
spellcheck.dic
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
5
|
||||||
|
Dioxus
|
||||||
|
MSN
|
||||||
|
renderers
|
||||||
|
SDK
|
||||||
|
TODO
|
10
spellcheck.toml
Normal file
10
spellcheck.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Also take into account developer comments
|
||||||
|
dev_comments = false
|
||||||
|
|
||||||
|
# Skip the README.md file as defined in the cargo manifest
|
||||||
|
skip_readme = false
|
||||||
|
|
||||||
|
[Hunspell]
|
||||||
|
lang = "en_US"
|
||||||
|
search_dirs = [ "." ]
|
||||||
|
extra_dictionaries = [ "./spellcheck.dic" ]
|
72
src/base.rs
72
src/base.rs
@@ -1,72 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
use futures_util::stream::StreamExt;
|
|
||||||
use log::{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());
|
|
||||||
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);
|
|
@@ -1,7 +1,7 @@
|
|||||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use tracing::error;
|
use tracing::{error, instrument, trace};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
common::PresenceState,
|
common::PresenceState,
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -95,7 +96,10 @@ impl Account {
|
|||||||
|
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
impl AccountMessagingConsumerInterface for Account {
|
impl AccountMessagingConsumerInterface for Account {
|
||||||
|
#[instrument(name = "Account", skip_all)]
|
||||||
async fn on_new_room(&self, room: Rc<Room>) -> Rc<dyn RoomMessagingConsumerInterface> {
|
async fn on_new_room(&self, room: Rc<Room>) -> Rc<dyn RoomMessagingConsumerInterface> {
|
||||||
|
trace!("on_new_room");
|
||||||
|
|
||||||
let room_id = room.id().clone();
|
let room_id = room.id().clone();
|
||||||
|
|
||||||
self.by_id_rooms
|
self.by_id_rooms
|
||||||
@@ -111,7 +115,10 @@ impl AccountMessagingConsumerInterface for Account {
|
|||||||
room
|
room
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(name = "Account", skip_all)]
|
||||||
async fn on_new_space(&self, space: Rc<Space>) -> Rc<dyn SpaceMessagingConsumerInterface> {
|
async fn on_new_space(&self, space: Rc<Space>) -> Rc<dyn SpaceMessagingConsumerInterface> {
|
||||||
|
trace!("on_new_space");
|
||||||
|
|
||||||
let space_id = space.id().clone();
|
let space_id = space.id().clone();
|
||||||
|
|
||||||
self.by_id_spaces
|
self.by_id_spaces
|
||||||
|
@@ -5,8 +5,8 @@ use tokio::sync::broadcast::Receiver;
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
common::{Avatar, UserId},
|
common::{Avatar, UserId},
|
||||||
room::{Room, RoomId},
|
room::{Invitation, Room, RoomId},
|
||||||
room_member::RoomMember,
|
room_member::{AvatarUrl, RoomMember},
|
||||||
space::Space,
|
space::Space,
|
||||||
};
|
};
|
||||||
use crate::infrastructure::messaging::matrix::account_event::AccountEvent;
|
use crate::infrastructure::messaging::matrix::account_event::AccountEvent;
|
||||||
@@ -31,10 +31,11 @@ pub trait AccountMessagingProviderInterface {
|
|||||||
|
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
pub trait RoomMessagingConsumerInterface {
|
pub trait RoomMessagingConsumerInterface {
|
||||||
async fn on_invitation(&self) {}
|
async fn on_invitation(&self, _invitation: Invitation) {}
|
||||||
|
|
||||||
async fn on_new_topic(&self, _topic: Option<String>) {}
|
async fn on_new_topic(&self, _topic: Option<String>) {}
|
||||||
async fn on_new_name(&self, _name: Option<String>) {}
|
async fn on_new_name(&self, _name: Option<String>) {}
|
||||||
|
async fn on_new_avatar(&self, _url: Option<Avatar>) {}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
async fn on_membership(&self, _member: RoomMember) {}
|
async fn on_membership(&self, _member: RoomMember) {}
|
||||||
@@ -43,7 +44,7 @@ pub trait RoomMessagingConsumerInterface {
|
|||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
pub trait RoomMessagingProviderInterface {
|
pub trait RoomMessagingProviderInterface {
|
||||||
async fn get_avatar(&self, id: &RoomId) -> anyhow::Result<Option<Avatar>>;
|
async fn get_avatar(&self, id: &RoomId) -> anyhow::Result<Option<Avatar>>;
|
||||||
async fn get_members(&self, id: &RoomId) -> anyhow::Result<Vec<RoomMember>>;
|
async fn join(&self, room_id: &RoomId) -> anyhow::Result<bool>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
@@ -61,7 +62,8 @@ pub trait SpaceMessagingProviderInterface {}
|
|||||||
pub trait MemberMessagingProviderInterface {
|
pub trait MemberMessagingProviderInterface {
|
||||||
async fn get_avatar(
|
async fn get_avatar(
|
||||||
&self,
|
&self,
|
||||||
room_id: &RoomId,
|
avatar_url: Option<AvatarUrl>,
|
||||||
user_id: &UserId,
|
room_id: RoomId,
|
||||||
|
user_id: UserId,
|
||||||
) -> anyhow::Result<Option<Avatar>>;
|
) -> anyhow::Result<Option<Avatar>>;
|
||||||
}
|
}
|
||||||
|
@@ -1,36 +1,71 @@
|
|||||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::HashMap,
|
||||||
|
fmt::{Debug, Formatter},
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures::future::{join, join_all};
|
use futures::future::{join, join_all};
|
||||||
use matrix_sdk::ruma::OwnedRoomId;
|
use matrix_sdk::{ruma::OwnedRoomId, RoomState as MatrixRoomState};
|
||||||
use matrix_sdk::RoomState as MatrixRoomState;
|
use tracing::{debug, debug_span, error, instrument, trace};
|
||||||
use tracing::{debug, error, trace};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
common::{Avatar, UserId},
|
common::{Avatar, UserId},
|
||||||
messaging_interface::{RoomMessagingConsumerInterface, RoomMessagingProviderInterface},
|
messaging_interface::{RoomMessagingConsumerInterface, RoomMessagingProviderInterface},
|
||||||
room_member::RoomMember,
|
room_member::RoomMember,
|
||||||
space::SpaceId,
|
space::SpaceId,
|
||||||
store_interface::RoomStoreProviderInterface,
|
store_interface::{RoomStoreConsumerInterface, RoomStoreProviderInterface},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::infrastructure::services::mozaik_builder::create_mozaik;
|
use crate::infrastructure::services::mozaik_builder::create_mozaik;
|
||||||
|
|
||||||
pub type RoomId = OwnedRoomId;
|
pub type RoomId = OwnedRoomId;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct Invitation {
|
||||||
|
invitee_id: UserId,
|
||||||
|
sender_id: UserId,
|
||||||
|
is_account_user: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Invitation {
|
||||||
|
pub fn new(invitee_id: UserId, sender_id: UserId, is_account_user: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
invitee_id,
|
||||||
|
sender_id,
|
||||||
|
is_account_user,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_account_user(&self) -> bool {
|
||||||
|
self.is_account_user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for Invitation {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
|
f.debug_tuple("Invitation")
|
||||||
|
.field(&self.invitee_id)
|
||||||
|
.field(&self.sender_id)
|
||||||
|
.field(&self.is_account_user)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Room {
|
pub struct Room {
|
||||||
id: RoomId,
|
id: RoomId,
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
spaces: Vec<SpaceId>,
|
|
||||||
|
|
||||||
name: RefCell<Option<String>>,
|
name: RefCell<Option<String>>,
|
||||||
topic: Option<String>,
|
topic: Option<String>,
|
||||||
is_direct: Option<bool>,
|
is_direct: Option<bool>,
|
||||||
state: Option<MatrixRoomState>,
|
state: Option<MatrixRoomState>,
|
||||||
avatar: RefCell<Option<Avatar>>,
|
avatar: RefCell<Option<Avatar>>,
|
||||||
|
|
||||||
|
invitations: RefCell<HashMap<UserId, Invitation>>,
|
||||||
members: RefCell<HashMap<UserId, RoomMember>>,
|
members: RefCell<HashMap<UserId, RoomMember>>,
|
||||||
|
|
||||||
|
spaces: Vec<SpaceId>,
|
||||||
|
|
||||||
messaging_provider: Option<Rc<dyn RoomMessagingProviderInterface>>,
|
messaging_provider: Option<Rc<dyn RoomMessagingProviderInterface>>,
|
||||||
store: RefCell<Option<Rc<dyn RoomStoreProviderInterface>>>,
|
store: RefCell<Option<Rc<dyn RoomStoreProviderInterface>>>,
|
||||||
}
|
}
|
||||||
@@ -44,22 +79,24 @@ impl PartialEq for Room {
|
|||||||
impl Room {
|
impl Room {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
id: RoomId,
|
id: RoomId,
|
||||||
spaces: Vec<SpaceId>,
|
// TODO: move space at the end of the list of params
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
topic: Option<String>,
|
topic: Option<String>,
|
||||||
is_direct: Option<bool>,
|
is_direct: Option<bool>,
|
||||||
state: Option<MatrixRoomState>,
|
state: Option<MatrixRoomState>,
|
||||||
|
spaces: Vec<SpaceId>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
|
|
||||||
spaces,
|
|
||||||
name: RefCell::new(name),
|
name: RefCell::new(name),
|
||||||
topic,
|
topic,
|
||||||
is_direct,
|
is_direct,
|
||||||
state,
|
state,
|
||||||
avatar: RefCell::new(None),
|
avatar: RefCell::new(None),
|
||||||
|
invitations: RefCell::new(HashMap::new()),
|
||||||
members: RefCell::new(HashMap::new()),
|
members: RefCell::new(HashMap::new()),
|
||||||
|
spaces,
|
||||||
|
|
||||||
messaging_provider: None,
|
messaging_provider: None,
|
||||||
store: RefCell::new(None),
|
store: RefCell::new(None),
|
||||||
@@ -81,25 +118,16 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn topic(&self) -> &Option<String> {
|
|
||||||
&self.topic
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn set_topic(&mut self, topic: Option<String>) {
|
pub fn set_topic(&mut self, topic: Option<String>) {
|
||||||
self.topic = topic;
|
self.topic = topic;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn is_direct(&self) -> &Option<bool> {
|
|
||||||
&self.is_direct
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn state(&self) -> &Option<MatrixRoomState> {
|
pub fn state(&self) -> &Option<MatrixRoomState> {
|
||||||
&self.state
|
&self.state
|
||||||
@@ -110,11 +138,41 @@ impl Room {
|
|||||||
self.state.map(|state| state == MatrixRoomState::Invited)
|
self.state.map(|state| state == MatrixRoomState::Invited)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[instrument(name = "Room", skip_all)]
|
||||||
fn add_member(&self, member: RoomMember) {
|
fn add_invitation(&self, invitation: Invitation) {
|
||||||
self.members
|
self.members.borrow_mut().remove(&invitation.invitee_id);
|
||||||
|
|
||||||
|
self.invitations
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.insert(member.id().clone(), member);
|
.insert(invitation.invitee_id.clone(), invitation.clone());
|
||||||
|
|
||||||
|
if let Some(store) = self.store.borrow().as_ref() {
|
||||||
|
store.on_invitation(invitation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(name = "Room", skip_all)]
|
||||||
|
fn add_member(&self, member: RoomMember) {
|
||||||
|
let mut members = self.members.borrow_mut();
|
||||||
|
|
||||||
|
members.insert(member.id().clone(), member.clone());
|
||||||
|
|
||||||
|
// USe the member display name to name the room if it's direct and has no name set.
|
||||||
|
if self.name.borrow().is_none() && members.len() == 1 {
|
||||||
|
if let Some(member_display_name) = member.display_name() {
|
||||||
|
let name = Some(member_display_name.clone());
|
||||||
|
|
||||||
|
self.name.borrow_mut().clone_from(&name);
|
||||||
|
|
||||||
|
if let Some(store) = self.store.borrow().as_ref() {
|
||||||
|
store.on_new_name(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(store) = self.store.borrow().as_ref() {
|
||||||
|
store.on_new_member(member);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_avatar(&self) -> Option<Avatar> {
|
pub async fn get_avatar(&self) -> Option<Avatar> {
|
||||||
@@ -126,7 +184,7 @@ impl Room {
|
|||||||
return Some(avatar);
|
return Some(avatar);
|
||||||
} else {
|
} else {
|
||||||
debug!("The room has no avatar... let's generate one");
|
debug!("The room has no avatar... let's generate one");
|
||||||
match self.render_room_avatar_with_members().await {
|
match self.gen_room_avatar_with_members().await {
|
||||||
Ok(avatar) => {
|
Ok(avatar) => {
|
||||||
if let Some(avatar) = avatar {
|
if let Some(avatar) = avatar {
|
||||||
return Some(avatar);
|
return Some(avatar);
|
||||||
@@ -145,66 +203,89 @@ impl Room {
|
|||||||
self.avatar.borrow().clone()
|
self.avatar.borrow().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render_room_avatar_with_members(&self) -> anyhow::Result<Option<Avatar>> {
|
#[instrument(name = "Room", skip_all)]
|
||||||
if let Some(requester) = &self.messaging_provider {
|
async fn gen_room_avatar_with_members(&self) -> anyhow::Result<Option<Avatar>> {
|
||||||
match requester.get_members(&self.id).await {
|
let mut account_member = None::<&RoomMember>;
|
||||||
Ok(members) => {
|
let mut other_members = Vec::<&RoomMember>::new();
|
||||||
let mut account_member = None::<&RoomMember>;
|
|
||||||
let mut other_members = Vec::<&RoomMember>::new();
|
|
||||||
|
|
||||||
for member in &members {
|
let members = self.members.borrow();
|
||||||
if member.is_account_user() {
|
for member in members.values() {
|
||||||
account_member = Some(member);
|
if member.is_account_user() {
|
||||||
} else {
|
account_member = Some(member);
|
||||||
other_members.push(member);
|
} else {
|
||||||
}
|
other_members.push(member);
|
||||||
}
|
|
||||||
|
|
||||||
let other_avatars_futures =
|
|
||||||
join_all(other_members.iter().map(|member| member.get_avatar()));
|
|
||||||
|
|
||||||
let (other_avatars, account_avatar) = if let Some(account_member) =
|
|
||||||
account_member
|
|
||||||
{
|
|
||||||
join(other_avatars_futures, account_member.get_avatar()).await
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
join_all(other_members.iter().map(|member| member.get_avatar())).await,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let other_avatars: Vec<Vec<u8>> = other_avatars.into_iter().flatten().collect();
|
|
||||||
|
|
||||||
return Ok(Some(create_mozaik(
|
|
||||||
256,
|
|
||||||
256,
|
|
||||||
&other_avatars,
|
|
||||||
&account_avatar,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("err={}", err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None)
|
|
||||||
|
let other_avatars_futures =
|
||||||
|
join_all(other_members.iter().map(|member| member.get_avatar()));
|
||||||
|
|
||||||
|
let (other_avatars, account_avatar) = if let Some(account_member) = account_member {
|
||||||
|
join(other_avatars_futures, account_member.get_avatar()).await
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
join_all(other_members.iter().map(|member| member.get_avatar())).await,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let other_avatars: Vec<Vec<u8>> = other_avatars.into_iter().flatten().collect();
|
||||||
|
|
||||||
|
if account_avatar.is_some() || !other_avatars.is_empty() {
|
||||||
|
let _guard = debug_span!("AvatarRendering").entered();
|
||||||
|
Ok(Some(
|
||||||
|
create_mozaik(256, 256, other_avatars, account_avatar).await,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
impl RoomMessagingConsumerInterface for Room {
|
impl RoomMessagingConsumerInterface for Room {
|
||||||
async fn on_invitation(&self) {
|
#[instrument(name = "Room", skip_all)]
|
||||||
trace!("Room::on_invitation");
|
async fn on_invitation(&self, invitation: Invitation) {
|
||||||
|
trace!("on_invitation");
|
||||||
|
let sender_id = invitation.sender_id.clone();
|
||||||
|
|
||||||
|
self.add_invitation(invitation);
|
||||||
|
|
||||||
|
if self.is_direct.unwrap_or(false) {
|
||||||
|
debug!("1to1 conversation, using the {} avatar", &sender_id);
|
||||||
|
if let Ok(avatar) = self.gen_room_avatar_with_members().await {
|
||||||
|
debug!("Avatar successfully generated");
|
||||||
|
self.avatar.borrow_mut().clone_from(&avatar);
|
||||||
|
if let Some(store) = self.store.borrow().as_ref() {
|
||||||
|
store.on_new_avatar(avatar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(name = "Room", skip_all)]
|
||||||
async fn on_membership(&self, member: RoomMember) {
|
async fn on_membership(&self, member: RoomMember) {
|
||||||
trace!("Room::on_membership({:?})", member);
|
trace!("on_membership");
|
||||||
|
self.add_member(member);
|
||||||
}
|
}
|
||||||
async fn on_new_topic(&self, topic: Option<String>) {
|
|
||||||
trace!("Room::on_new_topic({:?})", topic);
|
#[instrument(name = "Room", skip_all)]
|
||||||
|
async fn on_new_topic(&self, _topic: Option<String>) {
|
||||||
|
trace!("on_new_topic");
|
||||||
}
|
}
|
||||||
async fn on_new_name(&self, name: Option<String>) {
|
|
||||||
trace!("Room::on_new_name({:?})", name);
|
#[instrument(name = "Room", skip_all)]
|
||||||
|
async fn on_new_name(&self, _name: Option<String>) {
|
||||||
|
trace!("on_new_name");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(name = "Room", skip_all)]
|
||||||
|
async fn on_new_avatar(&self, avatar: Option<Avatar>) {
|
||||||
|
trace!("on_new_avatar");
|
||||||
|
self.avatar.borrow_mut().clone_from(&avatar);
|
||||||
|
if let Some(store) = self.store.borrow().as_ref() {
|
||||||
|
store.on_new_avatar(avatar);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +303,21 @@ impl RoomStoreConsumerInterface for Room {
|
|||||||
self.name.borrow().clone()
|
self.name.borrow().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn topic(&self) -> Option<String> {
|
||||||
|
self.topic.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn avatar(&self) -> Option<Avatar> {
|
||||||
|
self.get_avatar().await
|
||||||
|
}
|
||||||
|
|
||||||
fn spaces(&self) -> &Vec<SpaceId> {
|
fn spaces(&self) -> &Vec<SpaceId> {
|
||||||
&self.spaces
|
&self.spaces
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn join(&self) {
|
||||||
|
if let Some(messaging_provider) = &self.messaging_provider {
|
||||||
|
let _ = messaging_provider.join(&self.id).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ use std::{
|
|||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use matrix_sdk::{room::RoomMember as MatrixRoomMember, ruma::OwnedRoomId};
|
use matrix_sdk::ruma::OwnedMxcUri;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -13,9 +13,14 @@ use super::{
|
|||||||
room::RoomId,
|
room::RoomId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub type AvatarUrl = OwnedMxcUri;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RoomMember {
|
pub struct RoomMember {
|
||||||
id: UserId,
|
id: UserId,
|
||||||
|
|
||||||
|
display_name: Option<String>,
|
||||||
|
avatar_url: Option<AvatarUrl>,
|
||||||
room_id: RoomId,
|
room_id: RoomId,
|
||||||
is_account_user: bool,
|
is_account_user: bool,
|
||||||
|
|
||||||
@@ -26,14 +31,18 @@ pub struct RoomMember {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RoomMember {
|
impl RoomMember {
|
||||||
fn new(
|
pub fn new(
|
||||||
id: UserId,
|
id: UserId,
|
||||||
|
display_name: Option<String>,
|
||||||
|
avatar_url: Option<AvatarUrl>,
|
||||||
room_id: RoomId,
|
room_id: RoomId,
|
||||||
is_account_user: bool,
|
is_account_user: bool,
|
||||||
messaging_provider: Rc<dyn MemberMessagingProviderInterface>,
|
messaging_provider: Rc<dyn MemberMessagingProviderInterface>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
|
display_name,
|
||||||
|
avatar_url,
|
||||||
room_id,
|
room_id,
|
||||||
is_account_user,
|
is_account_user,
|
||||||
avatar: RefCell::new(None),
|
avatar: RefCell::new(None),
|
||||||
@@ -41,24 +50,14 @@ impl RoomMember {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Use a factory instead...
|
|
||||||
pub async fn from_matrix(
|
|
||||||
matrix_room_member: &MatrixRoomMember,
|
|
||||||
room_id: &OwnedRoomId,
|
|
||||||
messaging_provider: Rc<dyn MemberMessagingProviderInterface>,
|
|
||||||
) -> Self {
|
|
||||||
Self::new(
|
|
||||||
UserId::from(matrix_room_member.user_id()),
|
|
||||||
room_id.clone(),
|
|
||||||
matrix_room_member.is_account_user(),
|
|
||||||
messaging_provider,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> &UserId {
|
pub fn id(&self) -> &UserId {
|
||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn display_name(&self) -> &Option<String> {
|
||||||
|
&self.display_name
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn room_id(&self) -> &RoomId {
|
pub fn room_id(&self) -> &RoomId {
|
||||||
&self.room_id
|
&self.room_id
|
||||||
@@ -71,7 +70,11 @@ impl RoomMember {
|
|||||||
pub async fn get_avatar(&self) -> Option<Avatar> {
|
pub async fn get_avatar(&self) -> Option<Avatar> {
|
||||||
match self
|
match self
|
||||||
.messaging_provider
|
.messaging_provider
|
||||||
.get_avatar(&self.room_id, &self.id)
|
.get_avatar(
|
||||||
|
self.avatar_url.clone(),
|
||||||
|
self.room_id.clone(),
|
||||||
|
self.id.clone(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(avatar) => avatar,
|
Ok(avatar) => avatar,
|
||||||
|
@@ -2,7 +2,7 @@ use std::{cell::RefCell, collections::HashSet, rc::Rc};
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use matrix_sdk::ruma::OwnedRoomId;
|
use matrix_sdk::ruma::OwnedRoomId;
|
||||||
use tracing::error;
|
use tracing::{instrument, trace};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
common::Avatar,
|
common::Avatar,
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -72,16 +73,21 @@ impl Space {
|
|||||||
|
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
impl SpaceMessagingConsumerInterface for Space {
|
impl SpaceMessagingConsumerInterface for Space {
|
||||||
|
#[instrument(name = "Space", skip_all)]
|
||||||
async fn on_child(&self, room_id: RoomId) {
|
async fn on_child(&self, room_id: RoomId) {
|
||||||
error!("Space::on_child({room_id})");
|
trace!("on_child");
|
||||||
self.children.borrow_mut().insert(room_id);
|
self.children.borrow_mut().insert(room_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(name = "Space", skip_all)]
|
||||||
async fn on_new_topic(&self, topic: Option<String>) {
|
async fn on_new_topic(&self, topic: Option<String>) {
|
||||||
error!("Space::on_new_topic({:?})", topic);
|
trace!("on_new_topic");
|
||||||
*self.topic.borrow_mut() = topic;
|
*self.topic.borrow_mut() = topic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(name = "Space", skip_all)]
|
||||||
async fn on_new_name(&self, name: Option<String>) {
|
async fn on_new_name(&self, name: Option<String>) {
|
||||||
error!("Space::on_new_name({:?})", name);
|
trace!("on_new_name");
|
||||||
self.name.borrow_mut().clone_from(&name);
|
self.name.borrow_mut().clone_from(&name);
|
||||||
|
|
||||||
if let Some(store) = self.store.borrow().as_ref() {
|
if let Some(store) = self.store.borrow().as_ref() {
|
||||||
|
@@ -28,13 +28,19 @@ 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>;
|
||||||
async fn avatar(&self) -> Option<Avatar>;
|
fn topic(&self) -> Option<String>;
|
||||||
fn spaces(&self) -> &Vec<SpaceId>;
|
fn spaces(&self) -> &Vec<SpaceId>;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
async fn avatar(&self) -> Option<Avatar>;
|
||||||
|
async fn join(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait RoomStoreProviderInterface {
|
pub trait RoomStoreProviderInterface {
|
||||||
fn on_new_name(&self, name: Option<String>);
|
fn on_new_name(&self, name: Option<String>);
|
||||||
fn on_new_avatar(&self, avatar: Option<Avatar>);
|
fn on_new_avatar(&self, avatar: Option<Avatar>);
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn on_new_topic(&self, topic: Option<String>);
|
||||||
fn on_new_member(&self, member: RoomMember);
|
fn on_new_member(&self, member: RoomMember);
|
||||||
fn on_invitation(&self, invitation: Invitation);
|
fn on_invitation(&self, invitation: Invitation);
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
|
|
||||||
use matrix_sdk::{ruma::OwnedRoomId, RoomState};
|
use matrix_sdk::{ruma::OwnedRoomId, RoomState};
|
||||||
|
use tracing::Span;
|
||||||
|
|
||||||
use super::room_event::RoomEventsReceiver;
|
use super::room_event::RoomEventsReceiver;
|
||||||
use crate::{domain::model::space::SpaceId, utils::Sender};
|
use crate::{domain::model::space::SpaceId, utils::Sender};
|
||||||
@@ -16,6 +17,7 @@ pub enum AccountEvent {
|
|||||||
RoomState,
|
RoomState,
|
||||||
RoomEventsReceiver,
|
RoomEventsReceiver,
|
||||||
Sender<bool>,
|
Sender<bool>,
|
||||||
|
Span,
|
||||||
),
|
),
|
||||||
|
|
||||||
NewSpace(
|
NewSpace(
|
||||||
@@ -24,23 +26,33 @@ pub enum AccountEvent {
|
|||||||
Option<String>,
|
Option<String>,
|
||||||
RoomEventsReceiver,
|
RoomEventsReceiver,
|
||||||
Sender<bool>,
|
Sender<bool>,
|
||||||
|
Span,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for AccountEvent {
|
impl Debug for AccountEvent {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::NewRoom(id, spaces, name, topic, is_direct, state, _events_receiver, _sender) => {
|
Self::NewRoom(
|
||||||
f.debug_tuple("AccountEvent::NewRoom")
|
id,
|
||||||
.field(id)
|
spaces,
|
||||||
.field(spaces)
|
name,
|
||||||
.field(name)
|
topic,
|
||||||
.field(topic)
|
is_direct,
|
||||||
.field(is_direct)
|
state,
|
||||||
.field(state)
|
_events_receiver,
|
||||||
.finish()
|
_sender,
|
||||||
}
|
_span,
|
||||||
Self::NewSpace(id, name, topic, _events_receiver, _sender) => f
|
) => f
|
||||||
|
.debug_tuple("AccountEvent::NewRoom")
|
||||||
|
.field(id)
|
||||||
|
.field(spaces)
|
||||||
|
.field(name)
|
||||||
|
.field(topic)
|
||||||
|
.field(is_direct)
|
||||||
|
.field(state)
|
||||||
|
.finish(),
|
||||||
|
Self::NewSpace(id, name, topic, _events_receiver, _sender, _span) => f
|
||||||
.debug_tuple("AccountEvent::NewSpace")
|
.debug_tuple("AccountEvent::NewSpace")
|
||||||
.field(id)
|
.field(id)
|
||||||
.field(name)
|
.field(name)
|
||||||
|
@@ -9,30 +9,31 @@ use dioxus::prelude::Task;
|
|||||||
use matrix_sdk::{
|
use matrix_sdk::{
|
||||||
config::SyncSettings,
|
config::SyncSettings,
|
||||||
event_handler::Ctx,
|
event_handler::Ctx,
|
||||||
media::{MediaFormat, MediaThumbnailSize},
|
media::{MediaFormat, MediaRequest, MediaThumbnailSettings, MediaThumbnailSize},
|
||||||
room::{Room, RoomMember},
|
room::{ParentSpace, Room},
|
||||||
ruma::{
|
ruma::{
|
||||||
api::client::media::get_content_thumbnail::v3::Method,
|
api::client::media::get_content_thumbnail::v3::Method,
|
||||||
events::{
|
events::{
|
||||||
room::{
|
room::{
|
||||||
member::{
|
avatar::{RoomAvatarEventContent, StrippedRoomAvatarEvent},
|
||||||
OriginalSyncRoomMemberEvent, RoomMemberEventContent, StrippedRoomMemberEvent,
|
create::{RoomCreateEventContent, StrippedRoomCreateEvent},
|
||||||
},
|
member::{MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent},
|
||||||
topic::RoomTopicEventContent,
|
name::{RoomNameEventContent, StrippedRoomNameEvent},
|
||||||
|
topic::{RoomTopicEventContent, StrippedRoomTopicEvent},
|
||||||
|
MediaSource,
|
||||||
},
|
},
|
||||||
SyncStateEvent,
|
SyncStateEvent,
|
||||||
},
|
},
|
||||||
uint, OwnedRoomId, RoomId, UserId,
|
uint, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||||
},
|
},
|
||||||
Client as MatrixClient, RoomMemberships, RoomState,
|
Client as MatrixClient, RoomState,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tokio::sync::{
|
use tokio::sync::{
|
||||||
broadcast,
|
broadcast,
|
||||||
broadcast::{error::SendError, Receiver, Sender},
|
broadcast::{error::SendError, Receiver, Sender},
|
||||||
mpsc::{unbounded_channel, UnboundedReceiver},
|
mpsc::{unbounded_channel, UnboundedReceiver},
|
||||||
};
|
};
|
||||||
use tracing::{debug, error, warn};
|
use tracing::{debug, debug_span, error, instrument, warn, Instrument, Span};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
account_event::AccountEvent,
|
account_event::AccountEvent,
|
||||||
@@ -42,7 +43,7 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::utils::oneshot;
|
use crate::utils::oneshot;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ClientError {
|
pub enum ClientError {
|
||||||
#[error("Matrix client error: {0}")]
|
#[error("Matrix client error: {0}")]
|
||||||
Matrix(#[from] matrix_sdk::Error),
|
Matrix(#[from] matrix_sdk::Error),
|
||||||
@@ -68,21 +69,29 @@ impl Senders {
|
|||||||
room_senders.contains_key(room_id)
|
room_senders.contains_key(room_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send(&self, room_id: &RoomId, event: RoomEvent) -> Result<usize, SendError<RoomEvent>> {
|
fn send(&self, room_id: &RoomId, event: RoomEvent) -> Result<(), SendError<RoomEvent>> {
|
||||||
let room_senders = self.room_events_senders.lock().unwrap();
|
let room_senders = self.room_events_senders.lock().unwrap();
|
||||||
|
|
||||||
if let Some(room_sender) = room_senders.get(room_id) {
|
if let Some(room_sender) = room_senders.get(room_id) {
|
||||||
room_sender.send(event)
|
if let Err(err) = room_sender.send(event) {
|
||||||
|
warn!("Unable to send event to the {room_id} room: {err}");
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
error!("No sender found for \"{}\" room id", room_id);
|
warn!("No sender found for {room_id} room");
|
||||||
Ok(0)
|
// TODO: Return error
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_room(&self, room_id: &OwnedRoomId) -> Option<RoomEventsReceiver> {
|
fn add_room(&self, room_id: &OwnedRoomId) -> Option<RoomEventsReceiver> {
|
||||||
let mut senders = self.room_events_senders.lock().unwrap();
|
let mut senders = self.room_events_senders.lock().unwrap();
|
||||||
if !senders.contains_key(room_id) {
|
if !senders.contains_key(room_id) {
|
||||||
let (room_sender, room_receiver) = broadcast::channel(32);
|
let (room_sender, room_receiver) = broadcast::channel(32);
|
||||||
|
|
||||||
senders.insert(room_id.clone(), room_sender);
|
senders.insert(room_id.clone(), room_sender);
|
||||||
|
debug!("Create sender for {room_id} room");
|
||||||
|
|
||||||
Some(RoomEventsReceiver::new(room_receiver))
|
Some(RoomEventsReceiver::new(room_receiver))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -107,50 +116,55 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
async fn create_space(
|
async fn create_space(
|
||||||
senders: &Ctx<Senders>,
|
senders: &Ctx<Senders>,
|
||||||
room_id: OwnedRoomId,
|
room_id: &OwnedRoomId,
|
||||||
room: Option<&Room>,
|
room: Option<&Room>,
|
||||||
) -> anyhow::Result<(), SendError<AccountEvent>> {
|
) -> anyhow::Result<(), SendError<AccountEvent>> {
|
||||||
let mut name = None;
|
if let Some(receiver) = senders.add_room(room_id) {
|
||||||
let mut topic = None;
|
let current_span = Span::current();
|
||||||
|
|
||||||
if let Some(room) = room {
|
let mut name = None;
|
||||||
name = room.name();
|
let mut topic = None;
|
||||||
topic = room.topic();
|
if let Some(room) = room {
|
||||||
}
|
name = room.name();
|
||||||
|
topic = room.topic();
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(receiver) = senders.add_room(&room_id) {
|
|
||||||
let (reply, mut response) = oneshot::<bool>();
|
let (reply, mut response) = oneshot::<bool>();
|
||||||
|
|
||||||
let event = AccountEvent::NewSpace(room_id.clone(), name, topic, receiver, reply);
|
// We can't use Room instance here, because dyn PaginableRoom is not Sync
|
||||||
|
let event = AccountEvent::NewSpace(
|
||||||
|
room_id.clone(),
|
||||||
|
name.clone(),
|
||||||
|
topic.clone(),
|
||||||
|
receiver,
|
||||||
|
reply,
|
||||||
|
current_span.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
if let Err(err) = senders.account_events_sender.send(event) {
|
senders.account_events_sender.send(event)?;
|
||||||
error!(
|
|
||||||
"Unable to publish the new room with \"{}\" id: {}",
|
|
||||||
room_id, err
|
|
||||||
);
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're expecting a response indicating that the client is able to compute the next RoomEvent
|
// We're expecting a response indicating that the client is able to compute the next RoomEvent
|
||||||
response.recv().await;
|
response.recv().await;
|
||||||
} else {
|
|
||||||
let events = vec![RoomEvent::NewTopic(topic), RoomEvent::NewName(name)];
|
let events = vec![
|
||||||
|
RoomEvent::NewTopic(topic, current_span.clone()),
|
||||||
|
RoomEvent::NewName(name, current_span),
|
||||||
|
];
|
||||||
|
|
||||||
for event in events {
|
for event in events {
|
||||||
if let Err(err) = senders.send(&room_id, event.clone()) {
|
if let Err(_err) = senders.send(room_id, event.clone()) {
|
||||||
error!(
|
// TODO: Return an error
|
||||||
"Unable to publish the {:?} event to the \"{}\" room: {}",
|
|
||||||
event, room_id, err
|
|
||||||
);
|
|
||||||
// return Err(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
async fn create_room(
|
async fn create_room(
|
||||||
senders: &Ctx<Senders>,
|
senders: &Ctx<Senders>,
|
||||||
room: &Room,
|
room: &Room,
|
||||||
@@ -163,28 +177,30 @@ impl Client {
|
|||||||
let is_direct = match room.is_direct().await {
|
let is_direct = match room.is_direct().await {
|
||||||
Ok(is_direct) => Some(is_direct),
|
Ok(is_direct) => Some(is_direct),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Unable to know if the room \"{room_id}\" is direct: {err}");
|
warn!("Unable to know if the {room_id} room is direct: {err}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut parents = vec![];
|
let mut parents = vec![];
|
||||||
// TODO: Remove unwrap
|
|
||||||
let mut spaces = room.parent_spaces().await.unwrap();
|
if let Ok(mut spaces) = room.parent_spaces().await {
|
||||||
while let Some(parent) = spaces.next().await {
|
while let Some(parent) = spaces.next().await {
|
||||||
match parent {
|
match parent {
|
||||||
Ok(parent) => match parent {
|
Ok(parent) => match parent {
|
||||||
matrix_sdk::room::ParentSpace::Reciprocal(parent) => {
|
ParentSpace::Reciprocal(parent) => {
|
||||||
parents.push(parent.room_id().to_owned());
|
parents.push(parent.room_id().to_owned());
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("{err}");
|
||||||
}
|
}
|
||||||
_ => todo!(),
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
error!("{}", err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We can't use Room instance here, because dyn PaginableRoom is not Sync
|
||||||
let event = AccountEvent::NewRoom(
|
let event = AccountEvent::NewRoom(
|
||||||
room_id.clone(),
|
room_id.clone(),
|
||||||
parents.clone(),
|
parents.clone(),
|
||||||
@@ -194,16 +210,10 @@ impl Client {
|
|||||||
room.state(),
|
room.state(),
|
||||||
receiver,
|
receiver,
|
||||||
reply,
|
reply,
|
||||||
|
Span::current(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(err) = senders.account_events_sender.send(event) {
|
senders.account_events_sender.send(event)?;
|
||||||
error!(
|
|
||||||
"Unable to publish the new room with \"{}\" id: {}",
|
|
||||||
room.room_id(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're expecting a response indicating that the client is able to compute the next RoomEvent
|
// We're expecting a response indicating that the client is able to compute the next RoomEvent
|
||||||
response.recv().await;
|
response.recv().await;
|
||||||
@@ -211,6 +221,7 @@ impl Client {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
async fn add_room(
|
async fn add_room(
|
||||||
senders: &Ctx<Senders>,
|
senders: &Ctx<Senders>,
|
||||||
room: &Room,
|
room: &Room,
|
||||||
@@ -218,255 +229,347 @@ impl Client {
|
|||||||
let room_id = room.room_id().to_owned();
|
let room_id = room.room_id().to_owned();
|
||||||
|
|
||||||
if room.is_space() {
|
if room.is_space() {
|
||||||
Self::create_space(senders, room_id, Some(room)).await
|
Self::create_space(senders, &room_id, Some(room)).await
|
||||||
} else {
|
} else {
|
||||||
let ret = Self::create_room(senders, room).await;
|
|
||||||
|
|
||||||
let mut parents = vec![];
|
let mut parents = vec![];
|
||||||
// TODO: Remove unwrap
|
|
||||||
let mut spaces = room.parent_spaces().await.unwrap();
|
if let Ok(mut spaces) = room.parent_spaces().await {
|
||||||
while let Some(parent) = spaces.next().await {
|
while let Some(parent) = spaces.next().await {
|
||||||
match parent {
|
match parent {
|
||||||
Ok(parent) => match parent {
|
Ok(parent) => match parent {
|
||||||
matrix_sdk::room::ParentSpace::Reciprocal(parent) => {
|
ParentSpace::Reciprocal(parent) => {
|
||||||
parents.push(parent.room_id().to_owned());
|
parents.push(parent.room_id().to_owned());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!(
|
||||||
|
"Only ParentSpace::Reciprocal taken into account, skip {:?}",
|
||||||
|
parent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("{err}");
|
||||||
}
|
}
|
||||||
_ => {
|
|
||||||
warn!(
|
|
||||||
"Only ParentSpace::Reciprocal taken into account, skip {:?}",
|
|
||||||
parent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
error!("{}", err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
error!("parents={:?}", &parents);
|
|
||||||
|
|
||||||
for parent in parents {
|
for parent in parents {
|
||||||
// Create a minimal space to make the relation consistent... its content will be sync later.
|
// Create a minimal space to make the relation consistent... its content will be sync later.
|
||||||
if !senders.contains(&parent) {
|
if !senders.contains(&parent) {
|
||||||
let _ = Self::create_space(senders, parent.clone(), None).await;
|
let _ = Self::create_space(senders, &parent, None).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let event = RoomEvent::NewChild(room_id.clone());
|
let event = RoomEvent::NewChild(room_id.clone(), Span::current());
|
||||||
if let Err(err) = senders.send(parent.as_ref(), event.clone()) {
|
if let Err(_err) = senders.send(&parent, event) {
|
||||||
error!(
|
// TODO: Return an error
|
||||||
"Unable to send the {:?} event to the \"{}\": {:?}",
|
|
||||||
event, parent, err
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ret
|
Self::create_room(senders, room).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// async fn on_sync_typing_event(_ev: SyncTypingEvent, room: Room) {
|
async fn on_stripped_room_create_event(
|
||||||
// debug!("== on_sync_typing_event ==");
|
_ev: StrippedRoomCreateEvent,
|
||||||
// let room_id = room.room_id().to_owned();
|
room: Room,
|
||||||
// dbg!(room_id);
|
senders: Ctx<Senders>,
|
||||||
// }
|
) {
|
||||||
|
let span = debug_span!("Matrix::NewRoom", r = ?room.room_id());
|
||||||
|
|
||||||
// async fn on_presence_event(_ev: PresenceEvent) {
|
let _ = Self::add_room(&senders, &room).instrument(span).await;
|
||||||
// debug!("== on_presence_event ==");
|
}
|
||||||
// dbg!(_ev);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async fn on_sync_state_event(ev: SyncStateEvent<RoomNameEventContent>, _room: Room) {
|
// SyncStateEvent: A possibly-redacted state event without a room_id.
|
||||||
// error!("== on_sync_state_event ==");
|
async fn on_sync_room_create_event(
|
||||||
// if let SyncStateEvent::Original(ev) = ev {
|
_ev: SyncStateEvent<RoomCreateEventContent>,
|
||||||
// dbg!(ev);
|
room: Room,
|
||||||
// }
|
senders: Ctx<Senders>,
|
||||||
// }
|
) {
|
||||||
|
let span = debug_span!("Matrix::NewRoom", r = ?room.room_id());
|
||||||
|
|
||||||
// async fn on_original_sync_room_message_event(
|
let _ = Self::add_room(&senders, &room).instrument(span).await;
|
||||||
// ev: OriginalSyncRoomMessageEvent,
|
}
|
||||||
// _room: Room,
|
|
||||||
// _senders: Ctx<Senders>,
|
|
||||||
// ) {
|
|
||||||
// error!("== on_original_sync_room_message_event ==");
|
|
||||||
// error!("ev={:?}", ev.content);
|
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
fn on_invite_room_member_event(
|
||||||
|
user_id: OwnedUserId,
|
||||||
|
inviter_id: OwnedUserId,
|
||||||
|
room: &Room,
|
||||||
|
matrix_client: &MatrixClient,
|
||||||
|
senders: &Ctx<Senders>,
|
||||||
|
) {
|
||||||
|
if let Some(client_user_id) = matrix_client.user_id() {
|
||||||
|
let room_id = room.room_id();
|
||||||
|
let is_account_user = user_id == client_user_id;
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"{} (account user: {is_account_user}) invited by {} to join the {} room",
|
||||||
|
&user_id, &inviter_id, &room_id
|
||||||
|
);
|
||||||
|
|
||||||
|
let event =
|
||||||
|
RoomEvent::Invitation(user_id, inviter_id, is_account_user, Span::current());
|
||||||
|
|
||||||
|
if let Err(_err) = senders.send(room_id, event) {
|
||||||
|
// TODO: Return an error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
fn on_join_room_member_event(
|
||||||
|
user_id: OwnedUserId,
|
||||||
|
displayname: Option<String>,
|
||||||
|
avatar_url: Option<OwnedMxcUri>,
|
||||||
|
room: &Room,
|
||||||
|
matrix_client: &MatrixClient,
|
||||||
|
senders: &Ctx<Senders>,
|
||||||
|
) {
|
||||||
|
if let Some(client_user_id) = matrix_client.user_id() {
|
||||||
|
let is_account_user = user_id == client_user_id;
|
||||||
|
let room_id = room.room_id();
|
||||||
|
|
||||||
|
debug!("{} has joined the {} room", &user_id, &room_id);
|
||||||
|
|
||||||
|
let event = RoomEvent::Join(
|
||||||
|
user_id,
|
||||||
|
displayname,
|
||||||
|
avatar_url,
|
||||||
|
is_account_user,
|
||||||
|
Span::current(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(_err) = senders.send(room_id, event) {
|
||||||
|
// TODO: Return an error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is called on each m.room.member event for an invited room preview (room not already joined).
|
||||||
async fn on_stripped_room_member_event(
|
async fn on_stripped_room_member_event(
|
||||||
ev: StrippedRoomMemberEvent,
|
ev: StrippedRoomMemberEvent,
|
||||||
matrix_client: MatrixClient,
|
matrix_client: MatrixClient,
|
||||||
room: Room,
|
room: Room,
|
||||||
senders: Ctx<Senders>,
|
senders: Ctx<Senders>,
|
||||||
) {
|
) {
|
||||||
error!("*** on_stripped_room_member_event ***");
|
match room.state() {
|
||||||
// error!("ev={:?}", ev);
|
RoomState::Invited => {
|
||||||
|
let user_id = &ev.state_key;
|
||||||
|
|
||||||
if ev.state_key == matrix_client.user_id().unwrap()
|
match ev.content.membership {
|
||||||
&& room.state() == RoomState::Invited
|
MembershipState::Invite => {
|
||||||
&& Self::add_room(&senders, &room).await.is_ok()
|
let span = debug_span!("Matrix::RoomInvitation", r = ?room.room_id());
|
||||||
{
|
|
||||||
let room_id = room.room_id();
|
|
||||||
|
|
||||||
let event = RoomEvent::Invitation();
|
span.in_scope(|| {
|
||||||
if let Err(err) = senders.send(room_id, event) {
|
Self::on_invite_room_member_event(
|
||||||
error!(
|
user_id.clone(),
|
||||||
"Unable to publish the room \"{}\" invitation: {}",
|
ev.sender,
|
||||||
room.room_id(),
|
&room,
|
||||||
err
|
&matrix_client,
|
||||||
);
|
&senders,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
MembershipState::Join => {
|
||||||
|
let span =
|
||||||
|
debug_span!("Matrix::RoomJoin", r = ?room.room_id(), u = ?user_id)
|
||||||
|
.entered();
|
||||||
|
|
||||||
|
span.in_scope(|| {
|
||||||
|
Self::on_join_room_member_event(
|
||||||
|
ev.sender,
|
||||||
|
ev.content.displayname,
|
||||||
|
ev.content.avatar_url,
|
||||||
|
&room,
|
||||||
|
&matrix_client,
|
||||||
|
&senders,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!("TODO: {:?}", ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!("TODO: {:?}", ev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_room_topic_event(
|
// SyncStateEvent: A possibly-redacted state event without a room_id.
|
||||||
|
// RoomMemberEventContent: The content of an m.room.member event.
|
||||||
|
async fn on_sync_room_member_event(
|
||||||
|
ev: SyncStateEvent<RoomMemberEventContent>,
|
||||||
|
matrix_client: MatrixClient,
|
||||||
|
room: Room,
|
||||||
|
senders: Ctx<Senders>,
|
||||||
|
) {
|
||||||
|
if let SyncStateEvent::Original(ev) = ev {
|
||||||
|
match ev.content.membership {
|
||||||
|
MembershipState::Invite => {
|
||||||
|
let span = debug_span!("Matrix::RoomInvitation", r = ?room.room_id());
|
||||||
|
|
||||||
|
span.in_scope(|| {
|
||||||
|
let invitee_id = ev.state_key;
|
||||||
|
|
||||||
|
Self::on_invite_room_member_event(
|
||||||
|
invitee_id,
|
||||||
|
ev.sender,
|
||||||
|
&room,
|
||||||
|
&matrix_client,
|
||||||
|
&senders,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
MembershipState::Join => {
|
||||||
|
let user_id = ev.sender;
|
||||||
|
let span = debug_span!("Matrix::RoomJoin", r = ?room.room_id(), u = ?user_id)
|
||||||
|
.entered();
|
||||||
|
|
||||||
|
span.in_scope(|| {
|
||||||
|
Self::on_join_room_member_event(
|
||||||
|
user_id,
|
||||||
|
ev.content.displayname,
|
||||||
|
ev.content.avatar_url,
|
||||||
|
&room,
|
||||||
|
&matrix_client,
|
||||||
|
&senders,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => error!("TODO"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
async fn on_room_avatar_event(room: &Room, senders: &Ctx<Senders>) {
|
||||||
|
let room_id = room.room_id();
|
||||||
|
let avatar = match room
|
||||||
|
.avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
|
||||||
|
size: MediaThumbnailSize {
|
||||||
|
method: Method::Scale,
|
||||||
|
width: uint!(256),
|
||||||
|
height: uint!(256),
|
||||||
|
},
|
||||||
|
animated: false,
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(avatar) => avatar,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Unable to fetch avatar for {}: {err}", &room_id);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let event = RoomEvent::NewAvatar(avatar, Span::current());
|
||||||
|
|
||||||
|
if let Err(_err) = senders.send(room_id, event) {
|
||||||
|
// TODO: Return an error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_stripped_room_avatar_event(
|
||||||
|
_ev: StrippedRoomAvatarEvent,
|
||||||
|
room: Room,
|
||||||
|
senders: Ctx<Senders>,
|
||||||
|
) {
|
||||||
|
let span = debug_span!("Matrix::RoomAvatar", r = ?room.room_id());
|
||||||
|
|
||||||
|
Self::on_room_avatar_event(&room, &senders)
|
||||||
|
.instrument(span)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_sync_room_avatar_event(
|
||||||
|
ev: SyncStateEvent<RoomAvatarEventContent>,
|
||||||
|
room: Room,
|
||||||
|
senders: Ctx<Senders>,
|
||||||
|
) {
|
||||||
|
if let SyncStateEvent::Original(_ev) = ev {
|
||||||
|
dioxus::prelude::spawn(async move {
|
||||||
|
let span = debug_span!("Matrix::RoomAvatar", r = ?room.room_id());
|
||||||
|
|
||||||
|
Self::on_room_avatar_event(&room, &senders)
|
||||||
|
.instrument(span)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
fn on_room_name_event(name: Option<String>, room: &Room, senders: &Ctx<Senders>) {
|
||||||
|
let event = RoomEvent::NewName(name, Span::current());
|
||||||
|
|
||||||
|
if let Err(_err) = senders.send(room.room_id(), event) {
|
||||||
|
// TODO: Return an error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_stripped_room_name_event(
|
||||||
|
ev: StrippedRoomNameEvent,
|
||||||
|
room: Room,
|
||||||
|
senders: Ctx<Senders>,
|
||||||
|
) {
|
||||||
|
let span = debug_span!("Matrix::RoomName", r = ?room.room_id());
|
||||||
|
|
||||||
|
span.in_scope(|| {
|
||||||
|
Self::on_room_name_event(ev.content.name, &room, &senders);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_sync_room_name_event(
|
||||||
|
ev: SyncStateEvent<RoomNameEventContent>,
|
||||||
|
room: Room,
|
||||||
|
senders: Ctx<Senders>,
|
||||||
|
) {
|
||||||
|
if let SyncStateEvent::Original(ev) = ev {
|
||||||
|
let span = debug_span!("Matrix::RoomName", r = ?room.room_id());
|
||||||
|
|
||||||
|
span.in_scope(|| {
|
||||||
|
Self::on_room_name_event(Some(ev.content.name), &room, &senders);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
fn on_room_topic_event(topic: Option<String>, room: &Room, senders: &Ctx<Senders>) {
|
||||||
|
let event = RoomEvent::NewTopic(topic, Span::current());
|
||||||
|
|
||||||
|
if let Err(_err) = senders.send(room.room_id(), event) {
|
||||||
|
// TODO: Return an error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_stripped_room_topic_event(
|
||||||
|
ev: StrippedRoomTopicEvent,
|
||||||
|
room: Room,
|
||||||
|
senders: Ctx<Senders>,
|
||||||
|
) {
|
||||||
|
let span = debug_span!("Matrix::RoomTopic", r = ?room.room_id());
|
||||||
|
|
||||||
|
span.in_scope(|| {
|
||||||
|
Self::on_room_topic_event(ev.content.topic, &room, &senders);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_sync_room_topic_event(
|
||||||
ev: SyncStateEvent<RoomTopicEventContent>,
|
ev: SyncStateEvent<RoomTopicEventContent>,
|
||||||
room: Room,
|
room: Room,
|
||||||
senders: Ctx<Senders>,
|
senders: Ctx<Senders>,
|
||||||
) {
|
) {
|
||||||
error!("*** on_room_topic_event ***");
|
|
||||||
// error!("ev={:?}", ev);
|
|
||||||
|
|
||||||
if let SyncStateEvent::Original(ev) = ev {
|
if let SyncStateEvent::Original(ev) = ev {
|
||||||
let _ = Self::add_room(&senders, &room).await;
|
let span = debug_span!("Matrix::RoomTopic", r = ?room.room_id());
|
||||||
|
|
||||||
let room_id = room.room_id();
|
span.in_scope(|| {
|
||||||
let event = RoomEvent::NewTopic(Some(ev.content.topic));
|
Self::on_room_topic_event(Some(ev.content.topic), &room, &senders);
|
||||||
if let Err(err) = senders.send(room_id, event) {
|
});
|
||||||
error!(
|
|
||||||
"Unable to publish the room \"{}\" topic: {}",
|
|
||||||
room.room_id(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_room_member_event(
|
|
||||||
ev: SyncStateEvent<RoomMemberEventContent>,
|
|
||||||
room: Room,
|
|
||||||
senders: Ctx<Senders>,
|
|
||||||
) {
|
|
||||||
error!("*** on_room_member_event ***");
|
|
||||||
// error!("ev={:?}", ev);
|
|
||||||
|
|
||||||
if let SyncStateEvent::Original(_ev) = ev {
|
|
||||||
if Self::add_room(&senders, &room).await.is_ok() {
|
|
||||||
// let room_id = room.room_id();
|
|
||||||
// // TODO: Client shall only manage Matrix object... not BG92 ones.
|
|
||||||
// let event = RoomEvent::Membership(RoomMember::new(ev.sender, room_id));
|
|
||||||
// if let Some(result) = senders.send(room_id, event) {
|
|
||||||
// if let Err(err) = result {
|
|
||||||
// error!(
|
|
||||||
// "Unable to publish the room \"{}\" membership: {}",
|
|
||||||
// room.room_id(),
|
|
||||||
// err
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// async fn on_sync_message_like_room_message_event(
|
|
||||||
// ev: SyncMessageLikeEvent<RoomMessageEventContent>,
|
|
||||||
// _room: Room,
|
|
||||||
// _client: MatrixClient,
|
|
||||||
// ) {
|
|
||||||
// debug!("== on_sync_message_like_room_message_event ==");
|
|
||||||
// dbg!(ev);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async fn on_sync_message_like_reaction_event(
|
|
||||||
// ev: SyncMessageLikeEvent<ReactionEventContent>,
|
|
||||||
// _room: Room,
|
|
||||||
// ) {
|
|
||||||
// debug!("== on_sync_message_like_reaction_event ==");
|
|
||||||
// dbg!(ev);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async fn on_original_sync_room_redaction_event(
|
|
||||||
// ev: OriginalSyncRoomRedactionEvent,
|
|
||||||
// _room: Room,
|
|
||||||
// ) {
|
|
||||||
// debug!("== on_original_sync_room_redaction_event ==");
|
|
||||||
// dbg!(ev);
|
|
||||||
// }
|
|
||||||
|
|
||||||
async fn on_original_sync_room_member_event(
|
|
||||||
_ev: OriginalSyncRoomMemberEvent,
|
|
||||||
_room: Room,
|
|
||||||
_client: MatrixClient,
|
|
||||||
) {
|
|
||||||
// debug!("== on_original_sync_room_member_event ==");
|
|
||||||
// error!("room={:?}", room);
|
|
||||||
// let mut store = store_ctx.read().unwrap().to_owned();
|
|
||||||
// dbg!(store.rooms.keys());
|
|
||||||
// let is_direct = room.is_direct().await.ok();
|
|
||||||
// store.rooms.insert(
|
|
||||||
// OwnedRoomId::from(room_id),
|
|
||||||
// Arc::new(RwLock::new(Room::new(Arc::new(room), None, is_direct))),
|
|
||||||
// );
|
|
||||||
// let _ = store_ctx.write(store);
|
|
||||||
}
|
|
||||||
|
|
||||||
// async fn on_original_sync_key_verif_start_event(
|
|
||||||
// ev: OriginalSyncKeyVerificationStartEvent,
|
|
||||||
// _client: MatrixClient,
|
|
||||||
// ) {
|
|
||||||
// debug!("== on_original_sync_key_verif_start_event ==");
|
|
||||||
// dbg!(ev);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async fn on_original_sync_key_verif_key_event(
|
|
||||||
// ev: OriginalSyncKeyVerificationKeyEvent,
|
|
||||||
// _client: MatrixClient,
|
|
||||||
// ) {
|
|
||||||
// debug!("== on_original_sync_key_verif_key_event ==");
|
|
||||||
// dbg!(ev);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async fn on_original_sync_key_verif_done_event(
|
|
||||||
// ev: OriginalSyncKeyVerificationDoneEvent,
|
|
||||||
// _client: MatrixClient,
|
|
||||||
// ) {
|
|
||||||
// debug!("== on_original_sync_key_verif_done_event ==");
|
|
||||||
// dbg!(ev);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async fn on_device_key_verif_req_event(
|
|
||||||
// ev: ToDeviceKeyVerificationRequestEvent,
|
|
||||||
// _client: MatrixClient,
|
|
||||||
// ) {
|
|
||||||
// debug!("== on_device_key_verif_req_event ==");
|
|
||||||
// dbg!(ev);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async fn on_device_key_verif_start_event(
|
|
||||||
// ev: ToDeviceKeyVerificationStartEvent,
|
|
||||||
// _client: MatrixClient,
|
|
||||||
// ) {
|
|
||||||
// debug!("== on_device_key_verif_start_event ==");
|
|
||||||
// dbg!(ev);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async fn on_device_key_verif_key_event(
|
|
||||||
// ev: ToDeviceKeyVerificationKeyEvent,
|
|
||||||
// _client: MatrixClient,
|
|
||||||
// ) {
|
|
||||||
// debug!("== on_device_key_verif_key_event ==");
|
|
||||||
// dbg!(ev);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async fn on_device_key_verif_done_event(
|
|
||||||
// ev: ToDeviceKeyVerificationDoneEvent,
|
|
||||||
// _client: MatrixClient,
|
|
||||||
// ) {
|
|
||||||
// debug!("== on_device_key_verif_done_event ==");
|
|
||||||
// dbg!(ev);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async fn on_room_event(ev: SomeEvent, _senders: Ctx<Senders>) {
|
|
||||||
// debug!("== on_room_event({}) ==", ev.)
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub async fn spawn(homeserver_url: String) -> (Requester, Receiver<AccountEvent>) {
|
pub async fn spawn(homeserver_url: String) -> (Requester, Receiver<AccountEvent>) {
|
||||||
let matrix_client = Arc::new(
|
let matrix_client = Arc::new(
|
||||||
MatrixClient::builder()
|
MatrixClient::builder()
|
||||||
@@ -494,28 +597,20 @@ impl Client {
|
|||||||
// TODO: Remove clone?
|
// TODO: Remove clone?
|
||||||
client.add_event_handler_context(self.senders.clone());
|
client.add_event_handler_context(self.senders.clone());
|
||||||
|
|
||||||
|
let _ = client.add_event_handler(Client::on_stripped_room_create_event);
|
||||||
|
let _ = client.add_event_handler(Client::on_sync_room_create_event);
|
||||||
|
|
||||||
let _ = client.add_event_handler(Client::on_stripped_room_member_event);
|
let _ = client.add_event_handler(Client::on_stripped_room_member_event);
|
||||||
let _ = client.add_event_handler(Client::on_room_topic_event);
|
let _ = client.add_event_handler(Client::on_sync_room_member_event);
|
||||||
let _ = client.add_event_handler(Client::on_room_member_event);
|
|
||||||
|
|
||||||
// let _ = client.add_event_handler(Client::on_sync_typing_event);
|
let _ = client.add_event_handler(Client::on_stripped_room_avatar_event);
|
||||||
// let _ = client.add_event_handler(Client::on_presence_event);
|
let _ = client.add_event_handler(Client::on_sync_room_avatar_event);
|
||||||
// let _ = client.add_event_handler(Client::on_sync_state_event);
|
|
||||||
// let _ = client.add_event_handler(Client::on_original_sync_room_message_event);
|
|
||||||
|
|
||||||
// let _ = client.add_event_handler(Client::on_sync_message_like_room_message_event);
|
let _ = client.add_event_handler(Client::on_stripped_room_name_event);
|
||||||
// let _ = client.add_event_handler(Client::on_sync_message_like_reaction_event);
|
let _ = client.add_event_handler(Client::on_sync_room_name_event);
|
||||||
// let _ = client.add_event_handler(Client::on_original_sync_room_redaction_event);
|
|
||||||
|
|
||||||
let _ = client.add_event_handler(Client::on_original_sync_room_member_event);
|
let _ = client.add_event_handler(Client::on_stripped_room_topic_event);
|
||||||
|
let _ = client.add_event_handler(Client::on_sync_room_topic_event);
|
||||||
// let _ = client.add_event_handler(Client::on_original_sync_key_verif_start_event);
|
|
||||||
// let _ = client.add_event_handler(Client::on_original_sync_key_verif_key_event);
|
|
||||||
// let _ = client.add_event_handler(Client::on_original_sync_key_verif_done_event);
|
|
||||||
// let _ = client.add_event_handler(Client::on_device_key_verif_req_event);
|
|
||||||
// let _ = client.add_event_handler(Client::on_device_key_verif_start_event);
|
|
||||||
// let _ = client.add_event_handler(Client::on_device_key_verif_key_event);
|
|
||||||
// let _ = client.add_event_handler(Client::on_device_key_verif_done_event);
|
|
||||||
|
|
||||||
self.initialized = true;
|
self.initialized = true;
|
||||||
}
|
}
|
||||||
@@ -576,10 +671,13 @@ impl Client {
|
|||||||
|
|
||||||
match client
|
match client
|
||||||
.account()
|
.account()
|
||||||
.get_avatar(MediaFormat::Thumbnail(MediaThumbnailSize {
|
.get_avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
|
||||||
method: Method::Scale,
|
size: MediaThumbnailSize {
|
||||||
width: uint!(256),
|
method: Method::Scale,
|
||||||
height: uint!(256),
|
width: uint!(256),
|
||||||
|
height: uint!(256),
|
||||||
|
},
|
||||||
|
animated: false,
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -593,10 +691,13 @@ impl Client {
|
|||||||
|
|
||||||
if let Some(room) = client.get_room(room_id) {
|
if let Some(room) = client.get_room(room_id) {
|
||||||
match room
|
match room
|
||||||
.avatar(MediaFormat::Thumbnail(MediaThumbnailSize {
|
.avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
|
||||||
method: Method::Scale,
|
size: MediaThumbnailSize {
|
||||||
width: uint!(256),
|
method: Method::Scale,
|
||||||
height: uint!(256),
|
width: uint!(256),
|
||||||
|
height: uint!(256),
|
||||||
|
},
|
||||||
|
animated: false,
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -604,53 +705,90 @@ impl Client {
|
|||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err.into()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!("No room found with the \"{}\" id", room_id.as_str());
|
warn!("No room found with the \"{}\" id", room_id.as_str());
|
||||||
// TODO: Return an error if the room has not been found
|
// TODO: Return an error if the room has not been found
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_room_members(&mut self, room_id: &OwnedRoomId) -> anyhow::Result<Vec<RoomMember>> {
|
// TODO: Share MediaRequest with other media requests
|
||||||
|
async fn get_thumbnail(&self, media_url: OwnedMxcUri) -> anyhow::Result<Vec<u8>> {
|
||||||
let client = self.client.as_ref().unwrap();
|
let client = self.client.as_ref().unwrap();
|
||||||
|
let media = client.media();
|
||||||
|
|
||||||
if let Some(room) = client.get_room(room_id) {
|
let request = MediaRequest {
|
||||||
match room.members(RoomMemberships::ACTIVE).await {
|
source: MediaSource::Plain(media_url),
|
||||||
Ok(room_members) => Ok(room_members),
|
format: MediaFormat::Thumbnail(MediaThumbnailSettings {
|
||||||
Err(err) => Err(err.into()),
|
size: MediaThumbnailSize {
|
||||||
}
|
method: Method::Scale,
|
||||||
} else {
|
width: uint!(256),
|
||||||
error!("No room found with the \"{}\" id", room_id.as_str());
|
height: uint!(256),
|
||||||
// TODO: Return an error if the room has not been found
|
},
|
||||||
Ok(vec![])
|
animated: false,
|
||||||
}
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = media.get_media_content(&request, true).await;
|
||||||
|
|
||||||
|
Ok(res?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_room_member_avatar(
|
async fn get_room_member_avatar(
|
||||||
&self,
|
&self,
|
||||||
|
avatar_url: &Option<OwnedMxcUri>,
|
||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
) -> anyhow::Result<Option<Vec<u8>>> {
|
) -> anyhow::Result<Option<Vec<u8>>> {
|
||||||
let client = self.client.as_ref().unwrap();
|
let client = self.client.as_ref().unwrap();
|
||||||
|
|
||||||
if let Some(room) = client.get_room(room_id) {
|
if let Some(room) = client.get_room(room_id) {
|
||||||
if let Ok(Some(room_member)) = room.get_member(user_id).await {
|
match avatar_url {
|
||||||
let res = match room_member
|
Some(avatar_url) => {
|
||||||
.avatar(MediaFormat::Thumbnail(MediaThumbnailSize {
|
let thumbnail = self.get_thumbnail(avatar_url.clone()).await;
|
||||||
method: Method::Scale,
|
return Ok(Some(thumbnail?));
|
||||||
width: uint!(256),
|
}
|
||||||
height: uint!(256),
|
None => match room.get_member(user_id).await {
|
||||||
}))
|
Ok(room_member) => {
|
||||||
.await
|
if let Some(room_member) = room_member {
|
||||||
{
|
let res = match room_member
|
||||||
Ok(avatar) => Ok(avatar),
|
.avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
|
||||||
Err(err) => Err(err.into()),
|
size: MediaThumbnailSize {
|
||||||
};
|
method: Method::Scale,
|
||||||
return res;
|
width: uint!(256),
|
||||||
|
height: uint!(256),
|
||||||
|
},
|
||||||
|
animated: false,
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(avatar) => Ok(avatar),
|
||||||
|
Err(err) => Err(err.into()),
|
||||||
|
};
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Unable to get room member {user_id}: {err}");
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None)
|
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>) {
|
async fn work(&mut self, mut rx: UnboundedReceiver<WorkerTask>) {
|
||||||
while let Some(task) = rx.recv().await {
|
while let Some(task) = rx.recv().await {
|
||||||
self.run(task).await;
|
self.run(task).await;
|
||||||
@@ -687,15 +825,17 @@ impl Client {
|
|||||||
WorkerTask::GetRoomAvatar(id, reply) => {
|
WorkerTask::GetRoomAvatar(id, reply) => {
|
||||||
reply.send(self.get_room_avatar(&id).await).await;
|
reply.send(self.get_room_avatar(&id).await).await;
|
||||||
}
|
}
|
||||||
WorkerTask::GetRoomMembers(id, reply) => {
|
WorkerTask::GetRoomMemberAvatar(avatar_url, room_id, user_id, reply) => {
|
||||||
reply.send(self.get_room_members(&id).await).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
WorkerTask::GetRoomMemberAvatar(room_id, user_id, reply) => {
|
|
||||||
reply
|
reply
|
||||||
.send(self.get_room_member_avatar(&room_id, &user_id).await)
|
.send(
|
||||||
|
self.get_room_member_avatar(&avatar_url, &room_id, &user_id)
|
||||||
|
.await,
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
WorkerTask::JoinRoom(id, reply) => {
|
||||||
|
reply.send(self.join_room(&id).await).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
use std::{collections::HashMap, rc::Rc};
|
use std::{collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures::future::join_all;
|
use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
select,
|
select,
|
||||||
sync::{broadcast::Receiver, mpsc::UnboundedSender},
|
sync::{broadcast::Receiver, mpsc::UnboundedSender},
|
||||||
};
|
};
|
||||||
use tokio_stream::{wrappers::BroadcastStream, StreamExt, StreamMap};
|
use tokio_stream::{wrappers::BroadcastStream, StreamExt, StreamMap};
|
||||||
use tracing::error;
|
use tracing::{error, instrument, Instrument};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
account_event::AccountEvent,
|
account_event::AccountEvent,
|
||||||
@@ -23,8 +23,8 @@ use crate::{
|
|||||||
RoomMessagingProviderInterface, SpaceMessagingConsumerInterface,
|
RoomMessagingProviderInterface, SpaceMessagingConsumerInterface,
|
||||||
SpaceMessagingProviderInterface,
|
SpaceMessagingProviderInterface,
|
||||||
},
|
},
|
||||||
room::{Room, RoomId},
|
room::{Invitation, Room, RoomId},
|
||||||
room_member::RoomMember,
|
room_member::{AvatarUrl, RoomMember},
|
||||||
space::Space,
|
space::Space,
|
||||||
},
|
},
|
||||||
utils::oneshot,
|
utils::oneshot,
|
||||||
@@ -97,6 +97,96 @@ impl Requester {
|
|||||||
pub async fn login(&self, style: LoginStyle) -> anyhow::Result<()> {
|
pub async fn login(&self, style: LoginStyle) -> anyhow::Result<()> {
|
||||||
request_to_worker!(self, WorkerTask::Login, style)
|
request_to_worker!(self, WorkerTask::Login, style)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
async fn on_room_invitation(
|
||||||
|
consumer: &Rc<dyn RoomMessagingConsumerInterface>,
|
||||||
|
user_id: OwnedUserId,
|
||||||
|
sender_id: OwnedUserId,
|
||||||
|
is_account_user: bool,
|
||||||
|
) {
|
||||||
|
let invitation = Invitation::new(user_id, sender_id, is_account_user);
|
||||||
|
consumer.on_invitation(invitation).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
async fn on_room_join(
|
||||||
|
consumer: &Rc<dyn RoomMessagingConsumerInterface>,
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
user_id: OwnedUserId,
|
||||||
|
user_name: Option<String>,
|
||||||
|
avatar_url: Option<OwnedMxcUri>,
|
||||||
|
is_account_user: bool,
|
||||||
|
messaging_provider: Rc<dyn MemberMessagingProviderInterface>,
|
||||||
|
) {
|
||||||
|
let member = RoomMember::new(
|
||||||
|
UserId::from(user_id),
|
||||||
|
user_name,
|
||||||
|
avatar_url,
|
||||||
|
room_id,
|
||||||
|
is_account_user,
|
||||||
|
messaging_provider,
|
||||||
|
);
|
||||||
|
consumer.on_membership(member).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
async fn on_room_new_topic(
|
||||||
|
consumer: &Rc<dyn RoomMessagingConsumerInterface>,
|
||||||
|
topic: Option<String>,
|
||||||
|
) {
|
||||||
|
consumer.on_new_topic(topic).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
async fn on_room_new_name(
|
||||||
|
consumer: &Rc<dyn RoomMessagingConsumerInterface>,
|
||||||
|
name: Option<String>,
|
||||||
|
) {
|
||||||
|
consumer.on_new_name(name).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
async fn on_room_new_avatar(
|
||||||
|
consumer: &Rc<dyn RoomMessagingConsumerInterface>,
|
||||||
|
avatar: Option<Avatar>,
|
||||||
|
) {
|
||||||
|
consumer.on_new_avatar(avatar).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
async fn on_space_new_child(
|
||||||
|
consumer: &Rc<dyn SpaceMessagingConsumerInterface>,
|
||||||
|
child_id: RoomId,
|
||||||
|
) {
|
||||||
|
// TODO: Make name consistent
|
||||||
|
consumer.on_child(child_id).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
async fn on_space_new_topic(
|
||||||
|
consumer: &Rc<dyn SpaceMessagingConsumerInterface>,
|
||||||
|
topic: Option<String>,
|
||||||
|
) {
|
||||||
|
consumer.on_new_topic(topic).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
async fn on_space_new_name(
|
||||||
|
consumer: &Rc<dyn SpaceMessagingConsumerInterface>,
|
||||||
|
name: Option<String>,
|
||||||
|
) {
|
||||||
|
consumer.on_new_name(name).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[instrument(name="SpaceAvatar", skip_all, fields(s = %space_id, a = avatar.is_some()))]
|
||||||
|
// async fn on_space_new_avatar(
|
||||||
|
// consumer: &Rc<dyn SpaceMessagingConsumerInterface>,
|
||||||
|
// space_id: OwnedRoomId,
|
||||||
|
// avatar: Option<Avatar>,
|
||||||
|
// ) {
|
||||||
|
// consumer.on_new_avatar(avatar).await;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
@@ -141,8 +231,18 @@ impl AccountMessagingProviderInterface for Requester {
|
|||||||
res = account_events_receiver.recv() => {
|
res = account_events_receiver.recv() => {
|
||||||
if let Ok(account_event) = res {
|
if let Ok(account_event) = res {
|
||||||
match account_event {
|
match account_event {
|
||||||
AccountEvent::NewRoom(id, spaces, name, topic, is_direct, state, receiver, new_room_tx) => {
|
AccountEvent::NewRoom(
|
||||||
let mut room = Room::new(id, spaces, name, topic, is_direct, Some(state));
|
id,
|
||||||
|
spaces,
|
||||||
|
name,
|
||||||
|
topic,
|
||||||
|
is_direct,
|
||||||
|
state,
|
||||||
|
receiver,
|
||||||
|
new_room_tx,
|
||||||
|
span
|
||||||
|
) => {
|
||||||
|
let mut room = Room::new(id, name, topic, is_direct, Some(state), spaces);
|
||||||
let room_id = room.id().clone();
|
let room_id = room.id().clone();
|
||||||
|
|
||||||
room.set_messaging_provider(client.clone());
|
room.set_messaging_provider(client.clone());
|
||||||
@@ -152,13 +252,15 @@ impl AccountMessagingProviderInterface for Requester {
|
|||||||
let stream = BroadcastStream::new(receiver.into());
|
let stream = BroadcastStream::new(receiver.into());
|
||||||
rooms_events_streams.insert(room_id.clone(), stream);
|
rooms_events_streams.insert(room_id.clone(), stream);
|
||||||
|
|
||||||
let room_events_consumer = account_events_consumer.on_new_room(room).await;
|
let room_events_consumer = account_events_consumer.on_new_room(room)
|
||||||
|
.instrument(span)
|
||||||
|
.await;
|
||||||
room_events_consumers.insert(room_id, room_events_consumer);
|
room_events_consumers.insert(room_id, room_events_consumer);
|
||||||
|
|
||||||
// We're now ready to recv and compute RoomEvent.
|
// We're now ready to recv and compute RoomEvent.
|
||||||
new_room_tx.send(true).await;
|
new_room_tx.send(true).await;
|
||||||
},
|
},
|
||||||
AccountEvent::NewSpace(id, name, topic, receiver, new_space_tx) => {
|
AccountEvent::NewSpace(id, name, topic, receiver, new_space_tx, span) => {
|
||||||
let mut space = Space::new(id, name, topic);
|
let mut space = Space::new(id, name, topic);
|
||||||
let space_id = space.id().clone();
|
let space_id = space.id().clone();
|
||||||
|
|
||||||
@@ -169,12 +271,14 @@ impl AccountMessagingProviderInterface for Requester {
|
|||||||
let stream = BroadcastStream::new(receiver.into());
|
let stream = BroadcastStream::new(receiver.into());
|
||||||
spaces_events_streams.insert(space_id.clone(), stream);
|
spaces_events_streams.insert(space_id.clone(), stream);
|
||||||
|
|
||||||
let space_events_consumer = account_events_consumer.on_new_space(space).await;
|
let space_events_consumer = account_events_consumer.on_new_space(space)
|
||||||
|
.instrument(span)
|
||||||
|
.await;
|
||||||
space_events_consumers.insert(space_id, space_events_consumer);
|
space_events_consumers.insert(space_id, space_events_consumer);
|
||||||
|
|
||||||
// We're now ready to recv and compute SpaceEvent.
|
// We're now ready to recv and compute SpaceEvent.
|
||||||
new_space_tx.send(true).await;
|
new_space_tx.send(true).await;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -182,23 +286,43 @@ impl AccountMessagingProviderInterface for Requester {
|
|||||||
if let Ok(room_event) = room_event {
|
if let Ok(room_event) = room_event {
|
||||||
if let Some(consumer) = room_events_consumers.get(&room_id) {
|
if let Some(consumer) = room_events_consumers.get(&room_id) {
|
||||||
match room_event {
|
match room_event {
|
||||||
RoomEvent::Invitation() => {
|
RoomEvent::Invitation(user_id, sender_id, is_account_user, span) => {
|
||||||
consumer.on_invitation().await;
|
Self::on_room_invitation(consumer, user_id, sender_id, is_account_user)
|
||||||
|
.instrument(span)
|
||||||
|
.await;
|
||||||
},
|
},
|
||||||
// RoomEvent::Membership(user_id, is_account_user) => {
|
RoomEvent::Join(user_id, user_name, avatar_url, is_account_user, span) => {
|
||||||
// let member = RoomMember::new(UserId::from(user_id), room_id, is_account_user);
|
Self::on_room_join(
|
||||||
// consumer.on_membership(member).await;
|
consumer,
|
||||||
// },
|
room_id,
|
||||||
RoomEvent::NewTopic(topic) => {
|
user_id,
|
||||||
consumer.on_new_topic(topic).await;
|
user_name,
|
||||||
|
avatar_url,
|
||||||
|
is_account_user,
|
||||||
|
client.clone())
|
||||||
|
.instrument(span)
|
||||||
|
.await;
|
||||||
},
|
},
|
||||||
RoomEvent::NewName(name) => {
|
RoomEvent::NewTopic(topic, span) => {
|
||||||
consumer.on_new_name(name).await;
|
Self::on_room_new_topic(consumer, topic)
|
||||||
|
.instrument(span)
|
||||||
|
.await;
|
||||||
},
|
},
|
||||||
_ => {}
|
RoomEvent::NewName(name, span) => {
|
||||||
|
Self::on_room_new_name(consumer, name)
|
||||||
|
.instrument(span)
|
||||||
|
.await;
|
||||||
|
},
|
||||||
|
RoomEvent::NewAvatar(avatar, span) => {
|
||||||
|
Self::on_room_new_avatar(consumer, avatar)
|
||||||
|
.instrument(span)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
// RoomEvent::NewAvatar(avatar) => Self::on_room_new_avatar(consumer, avatar).await,
|
||||||
|
_ => error!("TODO: {:?}", &room_event),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!("No consumer found for \"{}\" room", room_id);
|
error!("No consumer found for {} room", &room_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -206,19 +330,25 @@ impl AccountMessagingProviderInterface for Requester {
|
|||||||
if let Ok(room_event) = room_event {
|
if let Ok(room_event) = room_event {
|
||||||
if let Some(consumer) = space_events_consumers.get(&space_id) {
|
if let Some(consumer) = space_events_consumers.get(&space_id) {
|
||||||
match room_event {
|
match room_event {
|
||||||
RoomEvent::NewTopic(topic) => {
|
RoomEvent::NewTopic(topic, span) => {
|
||||||
consumer.on_new_topic(topic).await;
|
Self::on_space_new_topic(consumer, topic)
|
||||||
|
.instrument(span)
|
||||||
|
.await;
|
||||||
},
|
},
|
||||||
RoomEvent::NewName(name) => {
|
RoomEvent::NewName(name, span) => {
|
||||||
consumer.on_new_name(name).await;
|
Self::on_space_new_name(consumer, name)
|
||||||
|
.instrument(span)
|
||||||
|
.await;
|
||||||
},
|
},
|
||||||
RoomEvent::NewChild(child_id) => {
|
RoomEvent::NewChild(child_id, span) => {
|
||||||
consumer.on_child(child_id).await;
|
Self::on_space_new_child(consumer, child_id)
|
||||||
|
.instrument(span)
|
||||||
|
.await;
|
||||||
},
|
},
|
||||||
_ => {}
|
_ => error!("TODO: {:?}", &room_event),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!("No consumer found for \"{}\" space", space_id);
|
error!("No consumer found for {} space", &space_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,17 +363,8 @@ impl RoomMessagingProviderInterface for Requester {
|
|||||||
request_to_worker!(self, WorkerTask::GetRoomAvatar, room_id.clone())
|
request_to_worker!(self, WorkerTask::GetRoomAvatar, room_id.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Fix return code
|
async fn join(&self, room_id: &RoomId) -> anyhow::Result<bool> {
|
||||||
async fn get_members(&self, room_id: &RoomId) -> anyhow::Result<Vec<RoomMember>> {
|
request_to_worker!(self, WorkerTask::JoinRoom, room_id.clone())
|
||||||
match request_to_worker!(self, WorkerTask::GetRoomMembers, room_id.clone()) {
|
|
||||||
Ok(matrix_room_members) => {
|
|
||||||
Ok(join_all(matrix_room_members.iter().map(|member| async {
|
|
||||||
RoomMember::from_matrix(member, room_id, Rc::new(self.clone())).await
|
|
||||||
}))
|
|
||||||
.await)
|
|
||||||
}
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,14 +375,16 @@ impl SpaceMessagingProviderInterface for Requester {}
|
|||||||
impl MemberMessagingProviderInterface for Requester {
|
impl MemberMessagingProviderInterface for Requester {
|
||||||
async fn get_avatar(
|
async fn get_avatar(
|
||||||
&self,
|
&self,
|
||||||
room_id: &RoomId,
|
avatar_url: Option<AvatarUrl>,
|
||||||
user_id: &UserId,
|
room_id: RoomId,
|
||||||
|
user_id: UserId,
|
||||||
) -> anyhow::Result<Option<Avatar>> {
|
) -> anyhow::Result<Option<Avatar>> {
|
||||||
request_to_worker!(
|
request_to_worker!(
|
||||||
self,
|
self,
|
||||||
WorkerTask::GetRoomMemberAvatar,
|
WorkerTask::GetRoomMemberAvatar,
|
||||||
room_id.clone(),
|
avatar_url,
|
||||||
user_id.clone()
|
room_id,
|
||||||
|
user_id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,35 +1,48 @@
|
|||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
|
|
||||||
use matrix_sdk::ruma::{OwnedRoomId, OwnedUserId};
|
use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId};
|
||||||
use tokio::sync::broadcast::Receiver;
|
use tokio::sync::broadcast::Receiver;
|
||||||
|
use tracing::Span;
|
||||||
|
|
||||||
|
use crate::domain::model::common::Avatar;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum RoomEvent {
|
pub enum RoomEvent {
|
||||||
Invitation(),
|
Invitation(OwnedUserId, OwnedUserId, bool, Span),
|
||||||
|
Join(OwnedUserId, Option<String>, Option<OwnedMxcUri>, bool, Span),
|
||||||
|
|
||||||
#[allow(dead_code)]
|
NewTopic(Option<String>, Span),
|
||||||
Membership(OwnedUserId, bool),
|
NewName(Option<String>, Span),
|
||||||
|
NewAvatar(Option<Avatar>, Span),
|
||||||
NewTopic(Option<String>),
|
NewChild(OwnedRoomId, Span),
|
||||||
NewName(Option<String>),
|
|
||||||
NewChild(OwnedRoomId),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for RoomEvent {
|
impl Debug for RoomEvent {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
match self {
|
match self {
|
||||||
Self::Invitation() => f
|
Self::Invitation(invitee_id, sender_id, is_account_user, _span) => f
|
||||||
.debug_tuple("RoomEvent::Invitation")
|
.debug_tuple("RoomEvent::Invitation")
|
||||||
.field(&format_args!("_"))
|
.field(invitee_id)
|
||||||
.finish(),
|
.field(sender_id)
|
||||||
Self::Membership(user_id, is_account_user) => f
|
|
||||||
.debug_tuple("RoomEvent::Membership")
|
|
||||||
.field(user_id)
|
|
||||||
.field(is_account_user)
|
.field(is_account_user)
|
||||||
.finish(),
|
.finish(),
|
||||||
Self::NewTopic(topic) => f.debug_tuple("RoomEvent::NewTopic").field(topic).finish(),
|
Self::Join(user_id, user_name, avatar_url, is_account_user, _span) => f
|
||||||
Self::NewName(name) => f.debug_tuple("RoomEvent::NewName").field(name).finish(),
|
.debug_tuple("RoomEvent::Join")
|
||||||
Self::NewChild(room_id) => f
|
.field(user_id)
|
||||||
|
.field(user_name)
|
||||||
|
.field(avatar_url)
|
||||||
|
.field(is_account_user)
|
||||||
|
.finish(),
|
||||||
|
Self::NewTopic(topic, _span) => {
|
||||||
|
f.debug_tuple("RoomEvent::NewTopic").field(topic).finish()
|
||||||
|
}
|
||||||
|
Self::NewName(name, _span) => f.debug_tuple("RoomEvent::NewName").field(name).finish(),
|
||||||
|
Self::NewAvatar(avatar, _span) => f
|
||||||
|
// Self::NewAvatar(avatar) => f
|
||||||
|
.debug_tuple("RoomEvent::NewAvatar")
|
||||||
|
.field(&format!("is_some: {}", &avatar.is_some()))
|
||||||
|
.finish(),
|
||||||
|
Self::NewChild(room_id, _span) => f
|
||||||
.debug_tuple("SpaceEvent::NewChild")
|
.debug_tuple("SpaceEvent::NewChild")
|
||||||
.field(room_id)
|
.field(room_id)
|
||||||
.finish(),
|
.finish(),
|
||||||
|
@@ -1,9 +1,6 @@
|
|||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
|
|
||||||
use matrix_sdk::{
|
use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId};
|
||||||
room::RoomMember,
|
|
||||||
ruma::{OwnedRoomId, OwnedUserId},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::utils::Sender;
|
use crate::utils::Sender;
|
||||||
|
|
||||||
@@ -20,13 +17,13 @@ pub enum WorkerTask {
|
|||||||
GetAvatar(Sender<anyhow::Result<Option<Vec<u8>>>>),
|
GetAvatar(Sender<anyhow::Result<Option<Vec<u8>>>>),
|
||||||
|
|
||||||
GetRoomAvatar(OwnedRoomId, Sender<anyhow::Result<Option<Vec<u8>>>>),
|
GetRoomAvatar(OwnedRoomId, Sender<anyhow::Result<Option<Vec<u8>>>>),
|
||||||
GetRoomMembers(OwnedRoomId, Sender<anyhow::Result<Vec<RoomMember>>>),
|
|
||||||
|
|
||||||
GetRoomMemberAvatar(
|
GetRoomMemberAvatar(
|
||||||
|
Option<OwnedMxcUri>,
|
||||||
OwnedRoomId,
|
OwnedRoomId,
|
||||||
OwnedUserId,
|
OwnedUserId,
|
||||||
Sender<anyhow::Result<Option<Vec<u8>>>>,
|
Sender<anyhow::Result<Option<Vec<u8>>>>,
|
||||||
),
|
),
|
||||||
|
JoinRoom(OwnedRoomId, Sender<anyhow::Result<bool>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for WorkerTask {
|
impl Debug for WorkerTask {
|
||||||
@@ -59,16 +56,16 @@ impl Debug for WorkerTask {
|
|||||||
.debug_tuple("WorkerTask::GetRoomAvatar")
|
.debug_tuple("WorkerTask::GetRoomAvatar")
|
||||||
.field(id)
|
.field(id)
|
||||||
.finish(),
|
.finish(),
|
||||||
WorkerTask::GetRoomMembers(id, _) => f
|
WorkerTask::GetRoomMemberAvatar(room_id, user_id, avatar_url, _) => f
|
||||||
.debug_tuple("WorkerTask::GetRoomMembers")
|
|
||||||
.field(id)
|
|
||||||
.finish(),
|
|
||||||
|
|
||||||
WorkerTask::GetRoomMemberAvatar(room_id, user_id, _) => f
|
|
||||||
.debug_tuple("WorkerTask::GetRoomMemberAvatar")
|
.debug_tuple("WorkerTask::GetRoomMemberAvatar")
|
||||||
|
.field(avatar_url)
|
||||||
.field(room_id)
|
.field(room_id)
|
||||||
.field(user_id)
|
.field(user_id)
|
||||||
.finish(),
|
.finish(),
|
||||||
|
WorkerTask::JoinRoom(room_id, _) => f
|
||||||
|
.debug_tuple("WorkerTask::JoinRoom")
|
||||||
|
.field(room_id)
|
||||||
|
.finish(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,18 @@
|
|||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use image::imageops::FilterType;
|
use image::imageops::FilterType;
|
||||||
use image::io::Reader;
|
use image::{DynamicImage, ImageFormat, ImageReader};
|
||||||
use image::{DynamicImage, ImageFormat};
|
|
||||||
use image::{GenericImage, RgbImage};
|
use image::{GenericImage, RgbImage};
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(not(target_family = "wasm"))] {
|
||||||
|
use tokio::task::spawn_blocking;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn from_raw_to_image(raw: &Vec<u8>) -> Option<DynamicImage> {
|
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(reader) => match reader.decode() {
|
||||||
Ok(image) => return Some(image),
|
Ok(image) => return Some(image),
|
||||||
Err(err) => error!("Unable to decode the image: {}", err),
|
Err(err) => error!("Unable to decode the image: {}", err),
|
||||||
@@ -19,7 +24,7 @@ fn from_raw_to_image(raw: &Vec<u8>) -> Option<DynamicImage> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_mozaik(
|
fn create_mozaik_(
|
||||||
width_px: u32,
|
width_px: u32,
|
||||||
height_px: u32,
|
height_px: u32,
|
||||||
images: &[Vec<u8>],
|
images: &[Vec<u8>],
|
||||||
@@ -90,3 +95,21 @@ pub fn create_mozaik(
|
|||||||
|
|
||||||
bytes
|
bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_mozaik(
|
||||||
|
width_px: u32,
|
||||||
|
height_px: u32,
|
||||||
|
images: Vec<Vec<u8>>,
|
||||||
|
padding_image: Option<Vec<u8>>,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(target_family = "wasm")] {
|
||||||
|
create_mozaik_(width_px, height_px, &images, &padding_image)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
spawn_blocking(move || {
|
||||||
|
create_mozaik_(width_px, height_px, &images, &padding_image)
|
||||||
|
}).await.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -3,17 +3,19 @@ use std::future::Future;
|
|||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::{collections::HashMap, future::IntoFuture};
|
use std::{collections::HashMap, future::IntoFuture};
|
||||||
|
|
||||||
use log::error;
|
|
||||||
use rand::distributions::{Alphanumeric, DistString};
|
use rand::distributions::{Alphanumeric, DistString};
|
||||||
use reqwest::Result as RequestResult;
|
use reqwest::Result as RequestResult;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[cfg(feature = "desktop")]
|
cfg_if! {
|
||||||
use tokio::fs::read_to_string;
|
if #[cfg(target_family = "wasm")] {
|
||||||
|
use web_sys;
|
||||||
|
} else {
|
||||||
|
use tokio::fs::read_to_string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
#[derive(Eq, Hash, PartialEq)]
|
||||||
use web_sys;
|
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Hash)]
|
|
||||||
pub enum AvatarFeeling {
|
pub enum AvatarFeeling {
|
||||||
Ok,
|
Ok,
|
||||||
Warning,
|
Warning,
|
||||||
@@ -145,39 +147,38 @@ async fn fetch_dicebear_svg(
|
|||||||
text.unwrap_or("".to_string())
|
text.unwrap_or("".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "desktop")]
|
cfg_if! {
|
||||||
fn gen_placeholder_fetcher(path: &'static str) -> Box<impl Future<Output = Option<String>>> {
|
if #[cfg(target_family = "wasm")] {
|
||||||
let path = format!("./public/{}", &path);
|
fn gen_placeholder_fetcher<'a>(path: &'static str) -> Box<impl Future<Output = Option<String>>> {
|
||||||
Box::new(async move {
|
Box::new(async move {
|
||||||
match read_to_string(&path).await {
|
let url = format!("{}{}", web_sys::window().unwrap().origin(), path);
|
||||||
Ok(content) => Some(content),
|
match fetch_text(url).await {
|
||||||
Err(err) => {
|
Ok(content) => Some(content),
|
||||||
error!(
|
Err(err) => {
|
||||||
"Error during the access to the {path} file: {}",
|
error!("Error during {path} fetching: {}", err.to_string());
|
||||||
err.to_string()
|
None
|
||||||
);
|
}
|
||||||
None
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
else {
|
||||||
#[cfg(feature = "web")]
|
fn gen_placeholder_fetcher(path: &'static str) -> Box<impl Future<Output = Option<String>>> {
|
||||||
fn gen_placeholder_fetcher<'a>(path: &'static str) -> Box<impl Future<Output = Option<String>>> {
|
let path = format!("./public/{}", &path);
|
||||||
Box::new(async move {
|
Box::new(async move {
|
||||||
let url = format!("{}{}", web_sys::window().unwrap().origin(), path);
|
match read_to_string(&path).await {
|
||||||
match fetch_text(url).await {
|
Ok(content) => Some(content),
|
||||||
Ok(content) => Some(content),
|
Err(err) => {
|
||||||
Err(err) => {
|
error!(
|
||||||
error!("Error during {path} fetching: {}", err.to_string());
|
"Error during the access to the {path} file: {}",
|
||||||
None
|
err.to_string()
|
||||||
}
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(feature = "desktop", feature = "web")))]
|
|
||||||
fn gen_placeholder_fetcher<'a>(_path: &'static str) -> Box<impl Future<Output = Option<String>>> {
|
|
||||||
Box::new(async move { None })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate_random_svg_avatar<'a>(config: Option<&'a AvatarConfig<'a>>) -> String {
|
pub async fn generate_random_svg_avatar<'a>(config: Option<&'a AvatarConfig<'a>>) -> String {
|
||||||
|
215
src/main.rs
215
src/main.rs
@@ -1,125 +1,152 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate cfg_if;
|
||||||
|
|
||||||
mod domain;
|
mod domain;
|
||||||
mod infrastructure;
|
mod infrastructure;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use dioxus::document::{Link, Style};
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use futures_util::stream::StreamExt;
|
||||||
|
use tracing::{debug, error, warn};
|
||||||
|
use tracing_subscriber::{prelude::*, EnvFilter};
|
||||||
|
|
||||||
#[cfg(feature = "desktop")]
|
use crate::{
|
||||||
use dioxus::desktop::Config;
|
domain::model::{messaging_interface::AccountMessagingProviderInterface, session::Session},
|
||||||
|
infrastructure::messaging::matrix::{client::Client, worker_tasks::LoginStyle},
|
||||||
|
ui::{
|
||||||
|
layouts::{conversations::Conversations, login::Login},
|
||||||
|
ACCOUNT, SESSION,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
use tracing::debug;
|
cfg_if! {
|
||||||
use tracing_subscriber::prelude::*;
|
if #[cfg(target_family = "wasm")] {
|
||||||
|
use tracing_web::MakeWebConsoleWriter;
|
||||||
|
} else {
|
||||||
|
use std::fs::File;
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
use dioxus::desktop::Config;
|
||||||
use tracing_web::MakeWebConsoleWriter;
|
use time::format_description::well_known::Iso8601;
|
||||||
|
use tracing_subscriber::fmt::time::UtcTime;
|
||||||
|
use tracing_forest::ForestLayer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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::views::login_view::LoginView;
|
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 is_logged = SESSION.read().is_logged;
|
||||||
|
|
||||||
let mut sync_rooms_coro = None;
|
if !is_logged {
|
||||||
|
|
||||||
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 {
|
|
||||||
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 {
|
rsx! {
|
||||||
debug!("Should render the MainWindow component");
|
Link {
|
||||||
rsx! {
|
href: asset!("public/fonts/Geist/Geist-Medium.woff2"), as: "font", type: "font/woff2"
|
||||||
MainWindow {},
|
|
||||||
}
|
}
|
||||||
} else {
|
Style {
|
||||||
rsx! {
|
id: "style-head", "body {{ font-family: 'Geist'; font-weight: normal; }}"
|
||||||
LoginView {},
|
}
|
||||||
|
|
||||||
|
if !is_logged {
|
||||||
|
Login {}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Conversations {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
#[cfg(feature = "desktop")]
|
let mut builder = LaunchBuilder::new();
|
||||||
{
|
|
||||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
|
||||||
.with_filter(tracing::level_filters::LevelFilter::DEBUG);
|
|
||||||
tracing_subscriber::registry().with(fmt_layer).init();
|
|
||||||
|
|
||||||
let config = Config::new().with_menu(None);
|
let mut layers = Vec::new();
|
||||||
let builder = LaunchBuilder::new().with_cfg(config);
|
|
||||||
builder.launch(app);
|
cfg_if! {
|
||||||
|
if #[cfg(target_family = "wasm")] {
|
||||||
|
let console_layer = tracing_subscriber::fmt::layer()
|
||||||
|
.with_ansi(false) // Only partially supported across browsers
|
||||||
|
.without_time() // std::time is not available in browsers, see note below
|
||||||
|
.with_writer(MakeWebConsoleWriter::new()) // write events to the console
|
||||||
|
.boxed();
|
||||||
|
layers.push(console_layer);
|
||||||
|
} else {
|
||||||
|
let config = Config::new().with_menu(None);
|
||||||
|
builder = builder.with_cfg(config);
|
||||||
|
|
||||||
|
let log_file = File::create("/tmp/bg92.log").unwrap();
|
||||||
|
let file_layer = tracing_subscriber::fmt::layer()
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_writer(log_file)
|
||||||
|
.with_timer(UtcTime::new(Iso8601::DATE_TIME))
|
||||||
|
.boxed();
|
||||||
|
layers.push(file_layer);
|
||||||
|
|
||||||
|
let console_layer = ForestLayer::default().boxed();
|
||||||
|
layers.push(console_layer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
tracing_subscriber::registry()
|
||||||
{
|
.with(layers)
|
||||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
.with(EnvFilter::from_default_env())
|
||||||
.with_ansi(false) // Only partially supported across browsers
|
.init();
|
||||||
.without_time() // std::time is not available in browsers, see note below
|
|
||||||
.with_writer(MakeWebConsoleWriter::new()) // write events to the console
|
|
||||||
.with_filter(tracing::level_filters::LevelFilter::INFO);
|
|
||||||
tracing_subscriber::registry().with(fmt_layer).init(); // Install these as subscribers to tracing events
|
|
||||||
|
|
||||||
let builder = LaunchBuilder::new();
|
builder.launch(app);
|
||||||
builder.launch(app);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -174,29 +174,37 @@ $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;
|
||||||
|
|
||||||
$geist-font-path: "../fonts/Geist";
|
|
||||||
|
|
||||||
$transition-duration: 300ms;
|
$transition-duration: 300ms;
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
src: url("#{$geist-font-path}/Geist-Medium.woff2") format("woff2");
|
|
||||||
font-family: "Geist";
|
font-family: "Geist";
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
src: url("#{$geist-font-path}/Geist-Bold.woff2") format("woff2");
|
|
||||||
font-family: "Geist";
|
font-family: "Geist";
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cf. https://css-tricks.com/box-sizing/
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
*, *:before, *:after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
outline: 0px;
|
outline: 0px;
|
||||||
@@ -206,6 +214,11 @@ input {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide the preloader as soon as the application is loaded and ready to be rendered
|
||||||
|
#preloader {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
#main {
|
#main {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
@import "../base.scss";
|
@import "../base.scss";
|
||||||
|
|
||||||
$panel-aspect-ratio: 1/1.618;
|
$aspect-ratio: 0.618; // 1/1.618;
|
||||||
|
|
||||||
@mixin panel($padding-v: 2%, $padding-h: 2%) {
|
@mixin panel($padding-v: 2%, $padding-h: 2%) {
|
||||||
padding: $padding-v $padding-h;
|
padding: $padding-v $padding-h;
|
||||||
|
|
||||||
height: calc(100% - (2 * $padding-v) - (2 * $border-big-width));
|
height: 100%;
|
||||||
width: calc(100% - (2 * $padding-h) - (2 * $border-big-width));
|
width: 100%;
|
||||||
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
border: $border-big;
|
border: $border-big;
|
||||||
|
@@ -3,7 +3,7 @@ use dioxus_free_icons::{Icon, IconShape};
|
|||||||
|
|
||||||
turf::style_sheet!("src/ui/components/button.scss");
|
turf::style_sheet!("src/ui/components/button.scss");
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Props)]
|
#[derive(Clone, PartialEq, Props)]
|
||||||
struct _ButtonProps {
|
struct _ButtonProps {
|
||||||
children: Element,
|
children: Element,
|
||||||
#[props(default = false)]
|
#[props(default = false)]
|
||||||
@@ -15,7 +15,7 @@ struct _ButtonProps {
|
|||||||
|
|
||||||
macro_rules! svg_text_icon {
|
macro_rules! svg_text_icon {
|
||||||
($name:ident,$text:literal) => {
|
($name:ident,$text:literal) => {
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
struct $name;
|
struct $name;
|
||||||
impl IconShape for $name {
|
impl IconShape for $name {
|
||||||
fn view_box(&self) -> &str {
|
fn view_box(&self) -> &str {
|
||||||
@@ -43,7 +43,7 @@ macro_rules! svg_text_button {
|
|||||||
($name:ident,$style:ident,$icon:ident) => {
|
($name:ident,$style:ident,$icon:ident) => {
|
||||||
pub fn $name(props: ButtonProps) -> Element {
|
pub fn $name(props: ButtonProps) -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
style { {STYLE_SHEET} },
|
style { {STYLE_SHEET} }
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
id: props.id,
|
id: props.id,
|
||||||
@@ -67,7 +67,7 @@ macro_rules! svg_text_button {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Props)]
|
#[derive(Clone, PartialEq, Props)]
|
||||||
pub struct ButtonProps {
|
pub struct ButtonProps {
|
||||||
#[props(default = false)]
|
#[props(default = false)]
|
||||||
focus: bool,
|
focus: bool,
|
||||||
@@ -79,7 +79,7 @@ pub struct ButtonProps {
|
|||||||
|
|
||||||
pub fn Button(props: ButtonProps) -> Element {
|
pub fn Button(props: ButtonProps) -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
style { {STYLE_SHEET} },
|
style { {STYLE_SHEET} }
|
||||||
|
|
||||||
button {
|
button {
|
||||||
id: props.id,
|
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_icon!(LoginText, "LOGIN");
|
||||||
svg_text_button!(LoginButton, LOGIN_BUTTON, LoginText);
|
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_icon!(SuccessText, "OK");
|
||||||
svg_text_button!(SuccessButton, SUCCESS_BUTTON, SuccessText);
|
svg_text_button!(SuccessButton, SUCCESS_BUTTON, SuccessText);
|
||||||
|
|
||||||
|
@@ -53,6 +53,14 @@
|
|||||||
@include button(secondary, 90);
|
@include button(secondary, 90);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.join-button {
|
||||||
|
@include button(secondary, 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject-button {
|
||||||
|
@include button(critical, 90);
|
||||||
|
}
|
||||||
|
|
||||||
.success-button {
|
.success-button {
|
||||||
@include button(success, 100);
|
@include button(success, 100);
|
||||||
}
|
}
|
||||||
|
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();
|
||||||
|
}
|
568
src/ui/components/conversations.rs
Normal file
568
src/ui/components/conversations.rs
Normal file
@@ -0,0 +1,568 @@
|
|||||||
|
use std::{rc::Rc, time::Duration};
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
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::{
|
||||||
|
button::{JoinButton, RejectButton},
|
||||||
|
icons::{ChatsIcon, LogoIcon, RoomsIcon, SpacesIcon},
|
||||||
|
},
|
||||||
|
hooks::use_long_press,
|
||||||
|
ACCOUNT, STORE,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
turf::style_sheet!("src/ui/components/conversations.scss");
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn AccountAvatar(content: Option<Vec<u8>>, class_name: Option<String>) -> Element {
|
||||||
|
rsx! {
|
||||||
|
if let Some(content) = content {
|
||||||
|
div {
|
||||||
|
class: class_name,
|
||||||
|
background_image: format!("url(data:image/jpeg;base64,{})", general_purpose::STANDARD.encode(content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 => VNode::empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 => VNode::empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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! {
|
||||||
|
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_selected: Option<EventHandler<RoomId>>,
|
||||||
|
on_pressed: Option<EventHandler<RoomId>>,
|
||||||
|
) -> Element {
|
||||||
|
let long_press_duration = Duration::from_millis(500);
|
||||||
|
|
||||||
|
let rooms = STORE.read().rooms();
|
||||||
|
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_signal(|| None::<RoomId>);
|
||||||
|
|
||||||
|
let invited_badge = if room.is_invited() {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: ClassName::CONVERSATION_AVATAR_INVITED_BADGE,
|
||||||
|
|
||||||
|
p {
|
||||||
|
"Invited"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
VNode::empty()
|
||||||
|
};
|
||||||
|
|
||||||
|
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(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(" ");
|
||||||
|
|
||||||
|
let on_press = {
|
||||||
|
let room_id = room_id.clone();
|
||||||
|
move || {
|
||||||
|
if let Some(c) = on_selected {
|
||||||
|
c.call(room_id.as_ref().clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_long_press = move || {
|
||||||
|
if let Some(c) = on_pressed {
|
||||||
|
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}",
|
||||||
|
|
||||||
|
..long_press_hook.handlers,
|
||||||
|
|
||||||
|
{avatar}
|
||||||
|
{invited_badge}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
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 || {
|
||||||
|
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_selected: on_selected_conversation,
|
||||||
|
on_pressed: on_pressed_conversation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: ClassName::SPACE_CONVERSATIONS_CAROUSEL,
|
||||||
|
// TODO: Needed?
|
||||||
|
onscroll: move |_| {
|
||||||
|
// Catch scrolling events.
|
||||||
|
},
|
||||||
|
{rendered_avatars}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If id is None, the Space will handle all the Conversation which have no parent (Space).
|
||||||
|
#[component]
|
||||||
|
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.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
space.name()
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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));
|
||||||
|
};
|
||||||
|
|
||||||
|
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! {
|
||||||
|
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,
|
||||||
|
on_pressed_conversation,
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: ClassName::SPACE_CONVERSATION_NAME,
|
||||||
|
p {
|
||||||
|
{selected_room_name}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(), on_pressed_conversation }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: ClassName::SPACES,
|
||||||
|
|
||||||
|
{rendered_spaces}
|
||||||
|
|
||||||
|
Space { on_pressed_conversation }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Search() -> Element {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: ClassName::SEARCH,
|
||||||
|
|
||||||
|
div {
|
||||||
|
class: ClassName::SEARCH_TEXT,
|
||||||
|
|
||||||
|
TextInput {}
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
class: ClassName::SEARCH_BUTTON,
|
||||||
|
Button {
|
||||||
|
SearchIcon {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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! {
|
||||||
|
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 {}
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
class: ClassName::CONVERSATIONS_SPACES,
|
||||||
|
Spaces { on_pressed_conversation }
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
class: ClassName::CONVERSATIONS_SEARCH,
|
||||||
|
Search {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{menu}
|
||||||
|
}
|
||||||
|
}
|
440
src/ui/components/conversations.scss
Normal file
440
src/ui/components/conversations.scss
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
@import "../base.scss";
|
||||||
|
@import "./_panel.scss";
|
||||||
|
@import "./button.scss";
|
||||||
|
|
||||||
|
@mixin button-class {
|
||||||
|
button {
|
||||||
|
@include button(secondary, 90);
|
||||||
|
width: 100%;
|
||||||
|
max-height: 128px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin extra-marged-button() {
|
||||||
|
@include button-class();
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__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%;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
@include button-class();
|
||||||
|
|
||||||
|
width: 20%;
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversations {
|
||||||
|
@include panel();
|
||||||
|
|
||||||
|
$gap: 1%;
|
||||||
|
$account-height: 15%;
|
||||||
|
$search-height: 5%;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
grid-area: account;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__spaces {
|
||||||
|
grid-area: spaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__search {
|
||||||
|
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");
|
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};
|
use style::{COLOR_PRIMARY_100, COLOR_TERNARY_100};
|
||||||
|
|
||||||
@@ -15,7 +15,8 @@ macro_rules! transparent_icon {
|
|||||||
($name:ident, $icon:ident) => {
|
($name:ident, $icon:ident) => {
|
||||||
pub fn $name() -> Element {
|
pub fn $name() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
style { {STYLE_SHEET} },
|
style { {STYLE_SHEET} }
|
||||||
|
|
||||||
Icon {
|
Icon {
|
||||||
class: ClassName::TRANSPARENT_ICON,
|
class: ClassName::TRANSPARENT_ICON,
|
||||||
icon: $icon,
|
icon: $icon,
|
||||||
@@ -52,7 +53,7 @@ impl IconShape for LogoShape {
|
|||||||
|
|
||||||
pub fn LogoIcon() -> Element {
|
pub fn LogoIcon() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
style { {STYLE_SHEET} },
|
style { {STYLE_SHEET} }
|
||||||
|
|
||||||
Icon {
|
Icon {
|
||||||
icon: LogoShape,
|
icon: LogoShape,
|
||||||
@@ -86,7 +87,7 @@ const _PYRAMID_VIEWBOX_HEIGHT: i64 = (_PYRAMID_CENTRAL_EDGE_E2_Y + _PYRAMID_STRO
|
|||||||
const _PYRAMID_VIEWBOX_WIDTH: i64 = (_PYRAMID_RIGHT_EDGE_E2_X + _PYRAMID_STROKE_WIDTH) as i64;
|
const _PYRAMID_VIEWBOX_WIDTH: i64 = (_PYRAMID_RIGHT_EDGE_E2_X + _PYRAMID_STROKE_WIDTH) as i64;
|
||||||
const _PYRAMID_VIEWBOX: &str = formatcp!("0 0 {_PYRAMID_VIEWBOX_WIDTH} {_PYRAMID_VIEWBOX_HEIGHT}");
|
const _PYRAMID_VIEWBOX: &str = formatcp!("0 0 {_PYRAMID_VIEWBOX_WIDTH} {_PYRAMID_VIEWBOX_HEIGHT}");
|
||||||
|
|
||||||
#[derive(PartialEq, Clone)]
|
#[derive(Clone, PartialEq)]
|
||||||
struct PyramidShape {
|
struct PyramidShape {
|
||||||
color: String,
|
color: String,
|
||||||
ratio: f64,
|
ratio: f64,
|
||||||
@@ -133,14 +134,14 @@ impl IconShape for PyramidShape {
|
|||||||
L {_PYRAMID_EDGES_E1_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
|
L {_PYRAMID_EDGES_E1_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
|
||||||
M {_PYRAMID_EDGES_E1_X} {_PYRAMID_EDGES_E1_Y} \
|
M {_PYRAMID_EDGES_E1_X} {_PYRAMID_EDGES_E1_Y} \
|
||||||
V {_PYRAMID_CENTRAL_EDGE_Y_LEN}",
|
V {_PYRAMID_CENTRAL_EDGE_Y_LEN}",
|
||||||
},
|
}
|
||||||
path {
|
path {
|
||||||
d: "\
|
d: "\
|
||||||
M {_PYRAMID_CENTRAL_EDGE_E2_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
|
M {_PYRAMID_CENTRAL_EDGE_E2_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
|
||||||
V {central_edge_ratio_e2_y} \
|
V {central_edge_ratio_e2_y} \
|
||||||
L {left_edge_ratio_e1_x} {no_central_edge_ratio_e1_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",
|
L {_PYRAMID_LEFT_EDGE_E2_X} {_PYRAMID_LEFT_EDGE_E2_Y} Z",
|
||||||
},
|
}
|
||||||
path {
|
path {
|
||||||
d: "\
|
d: "\
|
||||||
M {_PYRAMID_CENTRAL_EDGE_E2_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
|
M {_PYRAMID_CENTRAL_EDGE_E2_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
|
||||||
@@ -153,7 +154,7 @@ impl IconShape for PyramidShape {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Props)]
|
#[derive(Clone, PartialEq, Props)]
|
||||||
pub struct PyramidProps {
|
pub struct PyramidProps {
|
||||||
color: Option<String>,
|
color: Option<String>,
|
||||||
#[props(default = 0.5)]
|
#[props(default = 0.5)]
|
||||||
@@ -168,10 +169,11 @@ pub fn Pyramid(props: PyramidProps) -> Element {
|
|||||||
.unwrap_or(COLOR_TERNARY_100.to_string());
|
.unwrap_or(COLOR_TERNARY_100.to_string());
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
style { {STYLE_SHEET} },
|
style { {STYLE_SHEET} }
|
||||||
|
|
||||||
Icon {
|
Icon {
|
||||||
class: ClassName::PYRAMID_ICON,
|
class: ClassName::PYRAMID_ICON,
|
||||||
|
|
||||||
icon: PyramidShape { ratio: props.ratio, color, progress_color },
|
icon: PyramidShape { ratio: props.ratio, color, progress_color },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,26 +1,25 @@
|
|||||||
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::*;
|
||||||
use log::{debug, error, warn};
|
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_tokens.rs"));
|
||||||
|
|
||||||
use style::{
|
use style::{
|
||||||
COLOR_PRIMARY_100, COLOR_PRIMARY_110, COLOR_PRIMARY_120, COLOR_PRIMARY_140, COLOR_PRIMARY_150,
|
COLOR_PRIMARY_100, COLOR_PRIMARY_110, COLOR_PRIMARY_120, COLOR_PRIMARY_140, COLOR_PRIMARY_150,
|
||||||
@@ -98,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)]
|
#[derive(Clone)]
|
||||||
struct UrlInputHandler {
|
struct UrlInputHandler {
|
||||||
state: Signal<TextInputState>,
|
state: Signal<TextInputState>,
|
||||||
@@ -620,7 +598,6 @@ pub fn Login() -> Element {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if *spinner_animated.read() && SESSION.read().is_logged {
|
if *spinner_animated.read() && SESSION.read().is_logged {
|
||||||
debug!("Stop spinner");
|
|
||||||
spinner_animated.set(false);
|
spinner_animated.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -746,7 +723,7 @@ pub fn Login() -> Element {
|
|||||||
let confirm_password_classes_str = confirm_password_classes.join(" ");
|
let confirm_password_classes_str = confirm_password_classes.join(" ");
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
style { {STYLE_SHEET} },
|
style { {STYLE_SHEET} }
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: "{classes_str}",
|
class: "{classes_str}",
|
||||||
@@ -758,73 +735,79 @@ pub fn Login() -> Element {
|
|||||||
random_avatar_future.restart()
|
random_avatar_future.restart()
|
||||||
},
|
},
|
||||||
|
|
||||||
{avatar},
|
{avatar}
|
||||||
},
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::LOGIN_HOMESERVER,
|
class: ClassName::LOGIN_HOMESERVER,
|
||||||
|
|
||||||
TextInput {
|
TextInput {
|
||||||
placeholder: "Homeserver URL",
|
placeholder: "Homeserver URL",
|
||||||
value: "{homeserver_url}",
|
value: "{homeserver_url}",
|
||||||
state: homeserver_url_state,
|
state: homeserver_url_state,
|
||||||
oninput: on_input![data, homeserver_url],
|
oninput: on_input![data, homeserver_url],
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::LOGIN_ID,
|
class: ClassName::LOGIN_ID,
|
||||||
|
|
||||||
TextInput {
|
TextInput {
|
||||||
placeholder: "{id_placeholder}",
|
placeholder: "{id_placeholder}",
|
||||||
value: "{id}",
|
value: "{id}",
|
||||||
state: id_state,
|
state: id_state,
|
||||||
oninput: on_input![data, id],
|
oninput: on_input![data, id],
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: "{password_classes_str}",
|
class: "{password_classes_str}",
|
||||||
|
|
||||||
PasswordTextInput {
|
PasswordTextInput {
|
||||||
placeholder: "Password",
|
placeholder: "Password",
|
||||||
value: "{password}",
|
value: "{password}",
|
||||||
state: password_state,
|
state: password_state,
|
||||||
oninput: on_input![data, password],
|
oninput: on_input![data, password],
|
||||||
},
|
}
|
||||||
|
}
|
||||||
},
|
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: "{confirm_password_classes_str}",
|
class: "{confirm_password_classes_str}",
|
||||||
|
|
||||||
PasswordTextInput {
|
PasswordTextInput {
|
||||||
placeholder: "Confirm Password",
|
placeholder: "Confirm Password",
|
||||||
value: "{confirm_password}",
|
value: "{confirm_password}",
|
||||||
state: confirm_password_state,
|
state: confirm_password_state,
|
||||||
oninput: on_input![data, confirm_password],
|
oninput: on_input![data, confirm_password],
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::LOGIN_SPINNER,
|
class: ClassName::LOGIN_SPINNER,
|
||||||
|
|
||||||
Spinner {
|
Spinner {
|
||||||
animate: *spinner_animated.read(),
|
animate: *spinner_animated.read(),
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::LOGIN_REGISTER_BUTTON,
|
class: ClassName::LOGIN_REGISTER_BUTTON,
|
||||||
|
|
||||||
RegisterButton {
|
RegisterButton {
|
||||||
onclick: on_clicked_register,
|
onclick: on_clicked_register,
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::LOGIN_LOGIN_BUTTON,
|
class: ClassName::LOGIN_LOGIN_BUTTON,
|
||||||
|
|
||||||
LoginButton {
|
LoginButton {
|
||||||
focus: true,
|
focus: true,
|
||||||
onclick: on_clicked_login,
|
onclick: on_clicked_login,
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
{rendered_modal},
|
{rendered_modal}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -73,7 +73,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
height: calc(100% + (2 * $border-normal-width));
|
height: 100%;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,13 +9,13 @@ use crate::infrastructure::services::random_svg_generators::{
|
|||||||
generate_random_svg_avatar, AvatarConfig, AvatarFeeling,
|
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};
|
use style::{COLOR_CRITICAL_100, COLOR_SUCCESS_100, COLOR_WARNING_100};
|
||||||
|
|
||||||
turf::style_sheet!("src/ui/components/modal.scss");
|
turf::style_sheet!("src/ui/components/modal.scss");
|
||||||
|
|
||||||
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
|
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||||
pub enum Severity {
|
pub enum Severity {
|
||||||
Ok,
|
Ok,
|
||||||
Warning,
|
Warning,
|
||||||
@@ -42,7 +42,7 @@ fn avatar_configs() -> &'static HashMap<Severity, AvatarConfig<'static>> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Props, Clone, PartialEq)]
|
#[derive(Clone, PartialEq, Props)]
|
||||||
pub struct ModalProps {
|
pub struct ModalProps {
|
||||||
pub severity: Severity,
|
pub severity: Severity,
|
||||||
#[props(optional)]
|
#[props(optional)]
|
||||||
@@ -76,10 +76,10 @@ pub fn Modal(props: ModalProps) -> Element {
|
|||||||
Severity::Critical => ErrorButton,
|
Severity::Critical => ErrorButton,
|
||||||
};
|
};
|
||||||
|
|
||||||
icon.as_ref()?;
|
let _ = icon.as_ref().ok_or(VNode::empty());
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
style { {STYLE_SHEET} },
|
style { {STYLE_SHEET} }
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::MODAL,
|
class: ClassName::MODAL,
|
||||||
@@ -90,17 +90,17 @@ pub fn Modal(props: ModalProps) -> Element {
|
|||||||
div {
|
div {
|
||||||
class: ClassName::MODAL_CONTENT_ICON,
|
class: ClassName::MODAL_CONTENT_ICON,
|
||||||
{icon}
|
{icon}
|
||||||
},
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::MODAL_CONTENT_TITLE,
|
class: ClassName::MODAL_CONTENT_TITLE,
|
||||||
{props.title},
|
{props.title}
|
||||||
},
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::MODAL_CONTENT_MSG,
|
class: ClassName::MODAL_CONTENT_MSG,
|
||||||
{props.children},
|
{props.children}
|
||||||
},
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::MODAL_CONTENT_BUTTONS,
|
class: ClassName::MODAL_CONTENT_BUTTONS,
|
||||||
@@ -109,10 +109,10 @@ pub fn Modal(props: ModalProps) -> Element {
|
|||||||
if let Some(cb) = &props.on_confirm {
|
if let Some(cb) = &props.on_confirm {
|
||||||
cb.call(evt);
|
cb.call(evt);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -67,7 +67,7 @@ $modal-max-height: 55vh;
|
|||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
|
|
||||||
&__placeholder {
|
&__placeholder {
|
||||||
width: calc(100% + (2 * $border-normal-width));
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ use crate::ui::components::icons::LogoShape;
|
|||||||
|
|
||||||
turf::style_sheet!("src/ui/components/spinner.scss");
|
turf::style_sheet!("src/ui/components/spinner.scss");
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Props)]
|
#[derive(Clone, PartialEq, Props)]
|
||||||
pub struct SpinnerProps {
|
pub struct SpinnerProps {
|
||||||
#[props(default = true)]
|
#[props(default = true)]
|
||||||
animate: bool,
|
animate: bool,
|
||||||
@@ -13,13 +13,14 @@ pub struct SpinnerProps {
|
|||||||
|
|
||||||
pub fn Spinner(props: SpinnerProps) -> Element {
|
pub fn Spinner(props: SpinnerProps) -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
style { {STYLE_SHEET} },
|
style { {STYLE_SHEET} }
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::SPINNER,
|
class: ClassName::SPINNER,
|
||||||
|
|
||||||
Icon {
|
Icon {
|
||||||
class: if props.animate { "" } else { ClassName::PAUSED },
|
class: if props.animate { "" } else { ClassName::PAUSED },
|
||||||
|
|
||||||
icon: LogoShape,
|
icon: LogoShape,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,8 +9,8 @@ turf::style_sheet!("src/ui/components/text_input.scss");
|
|||||||
|
|
||||||
pub trait InputPropsData {}
|
pub trait InputPropsData {}
|
||||||
|
|
||||||
#[derive(Props, Clone, PartialEq)]
|
#[derive(Clone, PartialEq, Props)]
|
||||||
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>>>,
|
||||||
@@ -67,7 +67,7 @@ pub fn TextInput(props: InputProps<TextInputState>) -> Element {
|
|||||||
let input_classes_str = [ClassName::TEXT_INPUT_INPUT, criticity_class].join(" ");
|
let input_classes_str = [ClassName::TEXT_INPUT_INPUT, criticity_class].join(" ");
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
style { {STYLE_SHEET} },
|
style { {STYLE_SHEET} }
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::TEXT_INPUT,
|
class: ClassName::TEXT_INPUT,
|
||||||
@@ -83,7 +83,7 @@ pub fn TextInput(props: InputProps<TextInputState>) -> Element {
|
|||||||
cb.call(evt);
|
cb.call(evt);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::TEXT_INPUT_HELPER_TEXT,
|
class: ClassName::TEXT_INPUT_HELPER_TEXT,
|
||||||
@@ -97,7 +97,7 @@ pub fn TextInput(props: InputProps<TextInputState>) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Props, Clone, PartialEq)]
|
#[derive(Clone, PartialEq, Props)]
|
||||||
pub struct PasswordInputState {
|
pub struct PasswordInputState {
|
||||||
text_input_state: TextInputState,
|
text_input_state: TextInputState,
|
||||||
#[props(default = 0.0)]
|
#[props(default = 0.0)]
|
||||||
@@ -159,7 +159,7 @@ pub fn PasswordTextInput(props: InputProps<PasswordInputState>) -> Element {
|
|||||||
let input_classes = [ClassName::PASSWORD_TEXT_INPUT_INPUT, criticity_class].join(" ");
|
let input_classes = [ClassName::PASSWORD_TEXT_INPUT_INPUT, criticity_class].join(" ");
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
style { {STYLE_SHEET} },
|
style { {STYLE_SHEET} }
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: "{text_input_classes}",
|
class: "{text_input_classes}",
|
||||||
@@ -175,7 +175,7 @@ pub fn PasswordTextInput(props: InputProps<PasswordInputState>) -> Element {
|
|||||||
cb.call(evt);
|
cb.call(evt);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
|
|
||||||
if let Some(score) = score {
|
if let Some(score) = score {
|
||||||
div {
|
div {
|
||||||
@@ -184,7 +184,7 @@ pub fn PasswordTextInput(props: InputProps<PasswordInputState>) -> Element {
|
|||||||
ratio: score,
|
ratio: score,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::PASSWORD_TEXT_INPUT_SHOW_TOGGLE,
|
class: ClassName::PASSWORD_TEXT_INPUT_SHOW_TOGGLE,
|
||||||
@@ -203,7 +203,7 @@ pub fn PasswordTextInput(props: InputProps<PasswordInputState>) -> Element {
|
|||||||
icon: IoEye,
|
icon: IoEye,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::PASSWORD_TEXT_INPUT_HELPER_TEXT,
|
class: ClassName::PASSWORD_TEXT_INPUT_HELPER_TEXT,
|
||||||
@@ -212,7 +212,7 @@ pub fn PasswordTextInput(props: InputProps<PasswordInputState>) -> Element {
|
|||||||
class: criticity_class,
|
class: criticity_class,
|
||||||
{helper_text}
|
{helper_text}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,19 +1,16 @@
|
|||||||
@import "../_base.scss"
|
@import "../_base.scss";
|
||||||
|
|
||||||
%base-text-input {
|
%base-text-input {
|
||||||
$horizontal-padding: 1vw;
|
$horizontal-padding: 1vw;
|
||||||
|
|
||||||
height: calc(100% - (2 * $border-normal-width));
|
height: 100%;
|
||||||
width: calc(100% - (2 * $horizontal-padding));
|
width: 100%;
|
||||||
|
|
||||||
border: $border-normal;
|
border: $border-normal;
|
||||||
border-color: get-color(primary, 90);
|
border-color: get-color(primary, 90);
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
|
|
||||||
padding-left: $horizontal-padding;
|
padding: 0px $horizontal-padding;
|
||||||
padding-right: $horizontal-padding;
|
|
||||||
padding-top: 0px;
|
|
||||||
padding-bottom: 0px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
%base-input {
|
%base-input {
|
||||||
|
@@ -7,20 +7,26 @@ turf::style_sheet!("src/ui/components/wallpaper.scss");
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Wallpaper(display_version: Option<bool>) -> Element {
|
pub fn Wallpaper(display_version: Option<bool>) -> Element {
|
||||||
|
let background_image_asset = asset!("/public/images/wallpaper-pattern.svg");
|
||||||
|
let background_image = format!("url({})", background_image_asset.to_string());
|
||||||
|
|
||||||
let version = display_version.map(|flag| if flag { Some(GIT_VERSION) } else { None });
|
let version = display_version.map(|flag| if flag { Some(GIT_VERSION) } else { None });
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
style { {STYLE_SHEET} },
|
style { {STYLE_SHEET} }
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::WALLPAPER,
|
class: ClassName::WALLPAPER,
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::WALLPAPER_CONTENT,
|
class: ClassName::WALLPAPER_CONTENT,
|
||||||
|
background_image: "{background_image}",
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::WALLPAPER_VERSION,
|
class: ClassName::WALLPAPER_VERSION,
|
||||||
{version},
|
|
||||||
|
{version}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
background-image: url("./images/wallpaper-pattern.svg");
|
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
|
||||||
width: 150%;
|
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,
|
||||||
|
}
|
||||||
|
}
|
355
src/ui/layouts/conversations.rs
Normal file
355
src/ui/layouts/conversations.rs
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
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");
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/style_component_panel.rs"));
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/style_layout_conversations.rs"));
|
||||||
|
|
||||||
|
use conversations::INNER_PANEL_HEIGHT_RATIO;
|
||||||
|
use panel::ASPECT_RATIO;
|
||||||
|
|
||||||
|
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 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 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 {room_name_repr}") }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If this is the last iteration
|
||||||
|
let panel = if displayed_room_ids_it.peek().is_none() {
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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} }
|
||||||
|
|
||||||
|
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.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::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 {
|
||||||
|
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 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_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: room_name_repr }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if displayed_room_ids_it.peek().is_none() {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_PANELS_PANEL,
|
||||||
|
onmounted: move |cx: Event<MountedData>| last_div.set(Some(cx.data())),
|
||||||
|
ChatPanel { name: room_name_repr }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
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} }
|
||||||
|
|
||||||
|
div {
|
||||||
|
class: ClassName::CONVERSATIONS_VIEW_BIG,
|
||||||
|
|
||||||
|
div {
|
||||||
|
class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_PANEL,
|
||||||
|
|
||||||
|
ConversationsComponent {}
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
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();
|
||||||
|
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.iter()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn Conversations() -> Element {
|
||||||
|
let mut use_big_layout = use_signal(|| false);
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
style { {STYLE_SHEET} }
|
||||||
|
|
||||||
|
Wallpaper {
|
||||||
|
display_version: true
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
class: ClassName::CONVERSATIONS_VIEW,
|
||||||
|
|
||||||
|
onresize: move |cx| {
|
||||||
|
let data = cx.data();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
if use_big_layout() {
|
||||||
|
LayoutBig {}
|
||||||
|
} else {
|
||||||
|
LayoutSmall {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
215
src/ui/layouts/conversations.scss
Normal file
215
src/ui/layouts/conversations.scss
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$inner-panel-height-ratio: 0.95;
|
||||||
|
|
||||||
|
.conversations-view {
|
||||||
|
$height: 100vh;
|
||||||
|
$width: 100vw;
|
||||||
|
$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;
|
||||||
|
|
||||||
|
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: $aspect-ratio) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
@media (min-aspect-ratio: $aspect-ratio) {
|
||||||
|
aspect-ratio: $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: $aspect-ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,13 +1,13 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::ui::components::login::Login;
|
use crate::ui::components::login::Login as LoginComponent;
|
||||||
use crate::ui::components::wallpaper::Wallpaper;
|
use crate::ui::components::wallpaper::Wallpaper;
|
||||||
|
|
||||||
turf::style_sheet!("src/ui/views/login_view.scss");
|
turf::style_sheet!("src/ui/layouts/login.scss");
|
||||||
|
|
||||||
pub fn LoginView() -> Element {
|
pub fn Login() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
style { {STYLE_SHEET} },
|
style { {STYLE_SHEET} }
|
||||||
|
|
||||||
Wallpaper {
|
Wallpaper {
|
||||||
display_version: true
|
display_version: true
|
||||||
@@ -18,7 +18,7 @@ pub fn LoginView() -> Element {
|
|||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::LOGIN_VIEW_LOGIN_PANEL,
|
class: ClassName::LOGIN_VIEW_LOGIN_PANEL,
|
||||||
Login {}
|
LoginComponent {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -18,14 +18,14 @@
|
|||||||
align-items: safe center;
|
align-items: safe center;
|
||||||
|
|
||||||
&__login-panel {
|
&__login-panel {
|
||||||
@media (max-aspect-ratio: $panel-aspect-ratio) {
|
@media (max-aspect-ratio: $aspect-ratio) {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
}
|
}
|
||||||
@media (min-aspect-ratio: $panel-aspect-ratio) {
|
@media (min-aspect-ratio: $aspect-ratio) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
aspect-ratio: $panel-aspect-ratio;
|
aspect-ratio: $aspect-ratio;
|
||||||
max-height: $panel-max-height;
|
max-height: $panel-max-height;
|
||||||
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -36,6 +36,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
// Variables inherited by children
|
// Variables inherited by children
|
||||||
--aspect-ratio: #{$panel-aspect-ratio};
|
--aspect-ratio: #{$aspect-ratio};
|
||||||
}
|
}
|
||||||
}
|
}
|
2
src/ui/layouts/mod.rs
Normal file
2
src/ui/layouts/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub(crate) mod conversations;
|
||||||
|
pub(crate) mod login;
|
@@ -1,3 +1,14 @@
|
|||||||
pub(crate) mod components;
|
pub(crate) mod components;
|
||||||
|
pub(crate) mod hooks;
|
||||||
|
pub(crate) mod layouts;
|
||||||
pub(crate) mod store;
|
pub(crate) mod store;
|
||||||
pub(crate) mod views;
|
|
||||||
|
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);
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
pub(crate) mod room;
|
pub(crate) mod room;
|
||||||
pub(crate) mod space;
|
pub(crate) mod space;
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::{collections::HashMap, rc::Rc};
|
use std::{collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -21,6 +22,17 @@ use space::Space;
|
|||||||
pub struct Store {
|
pub struct Store {
|
||||||
rooms: HashMap<RoomId, Rc<Room>>,
|
rooms: HashMap<RoomId, Rc<Room>>,
|
||||||
spaces: HashMap<SpaceId, Rc<Space>>,
|
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)]
|
#[async_trait(?Send)]
|
||||||
|
@@ -12,13 +12,15 @@ use crate::domain::model::{
|
|||||||
store_interface::{RoomStoreConsumerInterface, RoomStoreProviderInterface},
|
store_interface::{RoomStoreConsumerInterface, RoomStoreProviderInterface},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[modx::props(id, is_direct, name, spaces)]
|
#[modx::props(id, is_direct, name, topic, spaces)]
|
||||||
#[modx::store]
|
#[modx::store]
|
||||||
pub struct Store {
|
pub struct Store {
|
||||||
id: RoomId,
|
id: RoomId,
|
||||||
|
|
||||||
is_direct: Option<bool>,
|
is_direct: Option<bool>,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
topic: Option<String>,
|
||||||
|
|
||||||
avatar: Option<Avatar>,
|
avatar: Option<Avatar>,
|
||||||
members: Vec<RoomMember>,
|
members: Vec<RoomMember>,
|
||||||
invitations: Vec<Invitation>,
|
invitations: Vec<Invitation>,
|
||||||
@@ -30,12 +32,13 @@ pub struct Store {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Room {
|
pub struct Room {
|
||||||
store: RefCell<Store>,
|
store: RefCell<Store>,
|
||||||
|
|
||||||
domain: Rc<dyn RoomStoreConsumerInterface>,
|
domain: Rc<dyn RoomStoreConsumerInterface>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Room {
|
impl Room {
|
||||||
pub fn signal(&self) -> Store {
|
pub fn signal(&self) -> Store {
|
||||||
self.store.borrow().clone()
|
*self.store.borrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_domain(room: Rc<dyn RoomStoreConsumerInterface>) -> Self {
|
pub fn from_domain(room: Rc<dyn RoomStoreConsumerInterface>) -> Self {
|
||||||
@@ -43,6 +46,7 @@ impl Room {
|
|||||||
room.id().clone(),
|
room.id().clone(),
|
||||||
room.is_direct(),
|
room.is_direct(),
|
||||||
room.name(),
|
room.name(),
|
||||||
|
room.topic(),
|
||||||
room.spaces().clone(),
|
room.spaces().clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -52,6 +56,11 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn join(&self) {
|
||||||
|
self.domain.join().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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
|
||||||
}
|
}
|
||||||
@@ -68,8 +77,18 @@ impl RoomStoreProviderInterface for Room {
|
|||||||
store.avatar.set(avatar);
|
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) {
|
fn on_new_member(&self, member: RoomMember) {
|
||||||
let mut store = self.store.borrow_mut();
|
let mut store = self.store.borrow_mut();
|
||||||
|
|
||||||
|
if member.is_account_user() {
|
||||||
|
store.is_invited.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
store.members.write().push(member);
|
store.members.write().push(member);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,12 +20,14 @@ 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Space {
|
impl Space {
|
||||||
pub fn signal(&self) -> Store {
|
pub fn signal(&self) -> Store {
|
||||||
self.store.borrow().clone()
|
*self.store.borrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_domain(space: Rc<dyn SpaceStoreConsumerInterface>) -> Self {
|
pub fn from_domain(space: Rc<dyn SpaceStoreConsumerInterface>) -> Self {
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
pub(crate) mod login_view;
|
|
Reference in New Issue
Block a user