Compare commits
215 Commits
94d0ea1cc8
...
ci/add-che
Author | SHA1 | Date | |
---|---|---|---|
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
|
|||
cbe32c250e
|
|||
d77c2a9d12
|
|||
bc30670f6e
|
|||
18a797bc3f
|
|||
bc6b02bc34
|
|||
0a936dd12b
|
|||
ef41c0bd48
|
|||
e3a6ec9858
|
|||
692a71faef
|
|||
c2918fbc78
|
|||
bfa1539d23
|
|||
0190cf9165
|
|||
4f9e5c538e
|
|||
79e8dea622
|
|||
0a0d6e745b
|
|||
32b633aad6
|
|||
4ea4416165
|
|||
c4dcb0f87d
|
|||
f79ebb0b03
|
|||
7078f86cd8
|
|||
894f32e177
|
|||
3afed02aa8
|
|||
7b6781a007
|
|||
dfe2761a3a
|
|||
58e12c991d
|
|||
badd541424
|
|||
fcf3d92cf9
|
|||
6172167ea8
|
|||
7170332205
|
|||
a3775e35d3
|
|||
724d04c592
|
|||
b3330abecc
|
|||
eecb46e4b8
|
|||
de9d2b3a8a
|
|||
f0d3b91084
|
|||
cae7a1e244
|
|||
6f95e0f57b
|
|||
d4af06d687
|
|||
53fff64537
|
|||
fb4554aa71
|
|||
741124e47e
|
|||
9951c2fea6
|
|||
df33e94a12
|
|||
78cb65e054
|
|||
77fa0c5fd5
|
|||
d3a35cd81f
|
|||
b524048563
|
|||
4ab4ac5fee
|
|||
438416bec1
|
|||
c580fba315
|
|||
a7bccfa779
|
|||
eb81b3252c
|
|||
880195109d
|
|||
11e239714b
|
|||
4261e24cd2
|
|||
9cfc0841df
|
|||
39ff4122c9
|
|||
46ce890718
|
|||
82b15a5509
|
|||
912b67ed23
|
|||
0ec1187fc3
|
|||
f78765e553
|
|||
b26cb1d982
|
|||
fc9411376c
|
|||
df465d99c0
|
|||
491e34903f
|
|||
0c1df908f2
|
|||
d245169345
|
|||
9eaf79208e
|
|||
0ce0764204
|
|||
92bf860101
|
|||
014a0c2c57
|
|||
9071b0073c
|
|||
aad0064a0c
|
|||
83fe388e8d
|
|||
448b81b65d
|
|||
4e963ce063
|
|||
cf9737fc76
|
|||
5c91df206c
|
|||
0ab6aaac1c
|
|||
89b1f10b6e
|
|||
570a969cee
|
|||
ceeda1a771
|
|||
fc0d3b1212
|
|||
01f589e789
|
|||
c746fb6552
|
|||
1073a592ed
|
|||
dd0754073c
|
|||
b05e3efce4
|
|||
0a4969e079
|
|||
f52733d9a6
|
|||
bb56d24f02
|
|||
043a721429
|
|||
46c251ef90
|
|||
257b36eae1
|
|||
ff430edffe
|
|||
5e05b75bde
|
|||
5719cb8254
|
|||
921003aeac
|
|||
8ffc977846
|
|||
6e64eb4d97
|
|||
a7cf0f681a
|
|||
a8d343ce3a
|
|||
5fe13335a1
|
|||
04628ae10d
|
|||
66f4ba6a7e
|
|||
116bbcb247
|
|||
2fed770f62
|
|||
227a07dd9e
|
|||
7498638ac1
|
|||
ddeb94e887
|
|||
d7ba8130d3
|
|||
8679a23692
|
|||
c9292fd613
|
|||
513b05ddb3
|
|||
ae8dba86f6
|
|||
4988054dae
|
|||
f79bf329a5
|
|||
5120f1e74f
|
|||
ff95dcade8
|
|||
3b89cd1769
|
8
.cargo/config.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
debug = false
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
incremental = false
|
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
# .git directory is not filtered here: we need to copy the .git directory in the builder image to compute the version.
|
||||
# media directory, README.md and Dockerfile files are not filtered to avoid the dockerized building env to get dirty
|
||||
# and append the "-modified" suffix to the displayed version.
|
||||
dist
|
||||
target
|
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
medias/presentation.png filter=lfs diff=lfs merge=lfs -text
|
||||
medias/presentation.mp4 filter=lfs diff=lfs merge=lfs -text
|
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
/dist
|
||||
Cargo.lock
|
||||
|
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
@@ -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
|
25
.woodpecker/.deploy.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
steps:
|
||||
deploy:
|
||||
image: euryecetelecom/woodpeckerci-kubernetes
|
||||
settings:
|
||||
kubernetes_server:
|
||||
from_secret: kubernetes_server
|
||||
kubernetes_token:
|
||||
from_secret: kubernetes_token
|
||||
kubernetes_cert:
|
||||
from_secret: kubernetes_cert
|
||||
namespace: bg92
|
||||
wait: true
|
||||
wait_timeout: 60s
|
||||
force: true
|
||||
deployment: beau-gosse-du-92-web
|
||||
repo: rg.fr-par.scw.cloud/asr-projects/beau-gosse-du-92-web
|
||||
container: beau-gosse-du-92-web
|
||||
tag: ${CI_COMMIT_SHA}
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
|
||||
depends_on:
|
||||
- dockerize
|
19
.woodpecker/.dockerize.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
steps:
|
||||
dockerize:
|
||||
image: woodpeckerci/plugin-kaniko
|
||||
settings:
|
||||
registry: rg.fr-par.scw.cloud
|
||||
repo: asr-projects/beau-gosse-du-92-web
|
||||
tags: ${CI_COMMIT_SHA}
|
||||
auto_tag: true
|
||||
cache: false
|
||||
username: nologin
|
||||
password:
|
||||
from_secret: registry-password
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
|
||||
# depends_on:
|
||||
# - validate
|
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
@@ -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
|
94
Cargo.toml
@@ -2,24 +2,84 @@
|
||||
name = "beau-gosse-du-92"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[package.metadata.spellcheck]
|
||||
config = "./spellcheck.toml"
|
||||
|
||||
[dependencies]
|
||||
dioxus = "0.4.0"
|
||||
dioxus-desktop = "0.4.0"
|
||||
# matrix-sdk = { version = "0.6.2", features = ["js"] }
|
||||
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", branch = "main" , features = ["js"]}
|
||||
anyhow = "1.0.72"
|
||||
url = "2.4.0"
|
||||
tokio = "1.29.1"
|
||||
dirs = "5.0.1"
|
||||
ctrlc-async = "3.2.2"
|
||||
tracing-subscriber = "0.3.17"
|
||||
dioxus-free-icons = { version = "0.7.0", features = ["material-design-icons-navigation", "ionicons"] }
|
||||
thiserror = "1.0.44"
|
||||
turf = "0.5.0"
|
||||
dioxus-std = { version = "0.4.0", features = ["utils"] }
|
||||
# Errors
|
||||
anyhow = "1.0.75"
|
||||
thiserror = "1.0.50"
|
||||
|
||||
[build]
|
||||
target = "x86_64-unknown-linux-gnu"
|
||||
# Async
|
||||
async-std = "1.12.0"
|
||||
async-trait = "0.1.80"
|
||||
futures = "0.3.29"
|
||||
futures-util = "0.3.29"
|
||||
tokio = { version = "1.34.0", default-features = false, features = ["rt", "sync"] }
|
||||
tokio-stream = "0.1.15"
|
||||
|
||||
# Utils
|
||||
base64 = "0.22.0"
|
||||
const_format = "0.2.32"
|
||||
rand = "0.8.5"
|
||||
validator = { version = "0.17.0", features = ["derive"] }
|
||||
# Http client
|
||||
reqwest = "0.11.24"
|
||||
# Password strength estimation
|
||||
zxcvbn = "2.2.2"
|
||||
# Image processing/conversion
|
||||
image = "0.25.1"
|
||||
# Get the application version
|
||||
git-version = "0.3.9"
|
||||
# Conditional compilation
|
||||
cfg-if = "1.0.0"
|
||||
|
||||
# Logging/tracing
|
||||
tracing = "0.1.40"
|
||||
tracing-forest = "0.1.6"
|
||||
|
||||
# SCSS -> CSS + usage in rust code
|
||||
turf = "0.9.3"
|
||||
|
||||
# Dioxus
|
||||
dioxus-free-icons = { version = "0.9", features = ["ionicons", "font-awesome-solid"] }
|
||||
modx = "0.1.4"
|
||||
|
||||
[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"
|
||||
|
||||
# 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]
|
||||
regex = "1.10.3"
|
||||
|
||||
[package.metadata.turf]
|
||||
minify = true
|
||||
|
||||
[package.metadata.turf.class_names]
|
||||
template = "<original_name>--<id>"
|
||||
|
13
Dioxus.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[application]
|
||||
name = "beau-gosse-du-92"
|
||||
default_platform = "desktop"
|
||||
|
||||
[web.app]
|
||||
title = "BG92"
|
||||
|
||||
[web.watcher]
|
||||
reload_html = true
|
||||
watch_path = ["Dioxus.toml", "public/index.html", "src"]
|
||||
|
||||
[[web.proxy]]
|
||||
backend = "http://localhost:8000/api/"
|
24
Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
FROM rg.fr-par.scw.cloud/asr-projects/dioxus-cli-0.6.3:latest AS builder
|
||||
|
||||
ARG JOBS_NB=${JOBS_NB:-default}
|
||||
# Disable incremental compilation
|
||||
# Cf. https://doc.rust-lang.org/cargo/reference/profiles.html#incremental
|
||||
ARG CARGO_INCREMENTAL=0
|
||||
|
||||
WORKDIR /usr/src/beau-gosse-du-92
|
||||
|
||||
# git is required by the git-version crate
|
||||
RUN apt update \
|
||||
&& apt install -y --no-install-recommends git \
|
||||
&& apt clean
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN dx build -r --platform web -- -j ${JOBS_NB}
|
||||
|
||||
|
||||
FROM nginx:mainline-alpine-slim
|
||||
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
COPY --from=builder /usr/src/beau-gosse-du-92/target/dx/beau-gosse-du-92/release/web/public .
|
43
README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 🚧 Beau-gosse-du-92 🚧
|
||||
|
||||
The goal of this project is to propose a new open-source implementation of the famous MSN messenger instant-messaging client.
|
||||
|
||||
[](medias/presentation.mp4)
|
||||
|
||||
# Technical stack
|
||||
|
||||
## Back-end
|
||||
|
||||
This project is based on the [Matrix.org](https://matrix.org/) building blocks (back-end and front-end SDK) to avoid to
|
||||
reinvent the wheel. This solution provides:
|
||||
|
||||
- [Open-source protocol](https://spec.matrix.org/v1.9/).
|
||||
- Features expected for a messaging solution in 2024 (multi-devices management, emojis, integrations, redaction,
|
||||
spaces, ...).
|
||||
- Multi-platforms clients (Android, iOS and web-client).
|
||||
- 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)).
|
||||
- End-to-end encryption.
|
||||
- Federation management.
|
||||
- Capability to host all the back-end infrastructure by ourself.
|
||||
|
||||
## Front-end
|
||||
|
||||
First, the project involves writing a client compatible with the [Matrix.org (client-server
|
||||
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
|
||||
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 view.
|
||||
|
||||
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:
|
||||
|
||||
- React-inspired library for Rust.
|
||||
- Multi-platforms (use of Web-view or WGPU-enabled renderers).
|
||||
|
||||
# TODO
|
||||
|
||||
- [ ] Test dioxus-radio.
|
||||
- [ ] Design system ?
|
||||
- [ ] Implement MSN messenger features using Matrix.org SDK...
|
153
build.rs
@@ -1,5 +1,156 @@
|
||||
use std::env;
|
||||
use std::fmt::Display;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::io::{self, BufRead};
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
fn main() {
|
||||
// Tell Cargo to rerun this build script if any SCSS file
|
||||
// in the 'src' directory or its subdirectories changes.
|
||||
println!("cargo:rerun-if-changed=src/**/*.scss");
|
||||
println!("cargo:rerun-if-changed=src/ui/**/*.scss");
|
||||
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
|
||||
// let mut tasks = Vec::new();
|
||||
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_variables(tasks)
|
||||
}
|
||||
|
||||
// From https://doc.rust-lang.org/rust-by-example/std_misc/file/read_lines.html
|
||||
// The output is wrapped in a Result to allow matching on errors.
|
||||
// Returns an Iterator to the Reader of the lines of the file.
|
||||
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file = File::open(filename)?;
|
||||
Ok(io::BufReader::new(file).lines())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ColorVariable {
|
||||
name: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl ColorVariable {
|
||||
pub fn new(name: String, value: String) -> Self {
|
||||
Self { name, value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ColorVariable {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"const {name}: &str = \"{value}\";",
|
||||
name = self.name.replace('-', "_").to_uppercase(),
|
||||
value = self.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FloatVariable {
|
||||
name: String,
|
||||
value: f64,
|
||||
}
|
||||
|
||||
impl FloatVariable {
|
||||
pub fn new(name: String, value: f64) -> Self {
|
||||
Self { name, value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FloatVariable {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"const {name}: f64 = {value};",
|
||||
name = self.name.replace('-', "_").to_uppercase(),
|
||||
value = self.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Task {
|
||||
src_path: PathBuf,
|
||||
dst_path: PathBuf,
|
||||
module_name: String,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub fn new(src_path: PathBuf, dst_path: PathBuf, module_name: String) -> Self {
|
||||
Self {
|
||||
src_path,
|
||||
dst_path,
|
||||
module_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fn export_variables(src_path: &PathBuf, dst_path: &PathBuf) {
|
||||
fn export_variables(tasks: Vec<Task>) {
|
||||
let color_re = Regex::new(r"^\$([^:]+):[[:space:]]*#([^$]+);[[:space:]]*$").unwrap();
|
||||
let variable_re = Regex::new(r"^\$([^:]+):[[:space:]]*([^;]+)[[:space:]]*;").unwrap();
|
||||
|
||||
for task in tasks {
|
||||
let mut dst_file = File::create(task.dst_path).unwrap();
|
||||
if let Err(err) = dst_file.write_fmt(format_args!(
|
||||
"#[allow(dead_code)]\nmod {} {{\n",
|
||||
task.module_name
|
||||
)) {
|
||||
println!("{}", err);
|
||||
return;
|
||||
};
|
||||
|
||||
let 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);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = dst_file.write(b"}\n") {
|
||||
println!("{}", err);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
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
@@ -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
|
Before Width: | Height: | Size: 698 B |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.9 KiB |
BIN
images/brush.png
Before Width: | Height: | Size: 494 B |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 221 B |
Before Width: | Height: | Size: 223 B |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 755 B |
Before Width: | Height: | Size: 306 B |
Before Width: | Height: | Size: 7.2 KiB |
BIN
images/news.png
Before Width: | Height: | Size: 430 B |
Before Width: | Height: | Size: 388 B |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 323 B |
1
index.html
Symbolic link
@@ -0,0 +1 @@
|
||||
public/index.html
|
BIN
medias/presentation.mp4
(Stored with Git LFS)
Normal file
BIN
medias/presentation.png
(Stored with Git LFS)
Normal file
BIN
public/fonts/Geist/Geist-Black.woff2
Normal file
BIN
public/fonts/Geist/Geist-Bold.woff2
Normal file
BIN
public/fonts/Geist/Geist-Light.woff2
Normal file
BIN
public/fonts/Geist/Geist-Medium.woff2
Normal file
BIN
public/fonts/Geist/Geist-Regular.woff2
Normal file
BIN
public/fonts/Geist/Geist-SemiBold.woff2
Normal file
BIN
public/fonts/Geist/Geist-Thin.woff2
Normal file
BIN
public/fonts/Geist/Geist-UltraBlack.woff2
Normal file
BIN
public/fonts/Geist/Geist-UltraLight.woff2
Normal file
BIN
public/fonts/Geist/GeistVariableVF.ttf
Normal file
BIN
public/fonts/Geist/GeistVariableVF.woff2
Normal file
92
public/fonts/Geist/LICENSE.TXT
Normal file
@@ -0,0 +1,92 @@
|
||||
Geist Sans and Geist Mono Font
|
||||
(C) 2023 Vercel, made in collaboration with basement.studio
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is available with a FAQ at: http://scripts.sil.org/OFL and copied below
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION AND CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
1
public/images/login-profile-placeholder.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none" shape-rendering="auto"><desc>"Shapes" by "Florian Körner", licensed under "CC0 1.0". / Remix of the original. - Created with dicebear.com</desc><metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:rdf><cc:work><dc:title>Shapes</dc:title><dc:creator><cc:agent rdf:about="https://www.dicebear.com"><dc:title>Florian Körner</dc:title></cc:agent></dc:creator><dc:source>https://www.dicebear.com</dc:source><cc:license rdf:resource="https://creativecommons.org/publicdomain/zero/1.0/"></cc:license></cc:work></rdf:rdf></metadata><mask id="w6sj6i8m"><rect width="100" height="100" rx="0" ry="0" x="0" y="0" fill="#fff"></rect></mask><g mask="url(#w6sj6i8m)"><rect fill="#E2F2F7" width="100" height="100" x="0" y="0"></rect><g transform="matrix(1.2 0 0 1.2 -10 -10)"><g transform="translate(51, -23) rotate(-38 50 50)"><path d="M0 0h100v100H0V0Z" fill="#83CADE"></path></g></g><g transform="matrix(.8 0 0 .8 10 10)"><g transform="translate(-2, 35) rotate(99 50 50)"><path d="M100 50A50 50 0 1 1 0 50a50 50 0 0 1 100 0Z" fill="#6957A0"></path></g></g><g transform="matrix(.4 0 0 .4 30 30)"><g transform="translate(-18, -6) rotate(-97 50 50)"><path d="m50 7 50 86.6H0L50 7Z" fill="#D53583"></path></g></g></g></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
public/images/modal-default-critical-icon.svg
Normal file
After Width: | Height: | Size: 20 KiB |
1
public/images/modal-default-ok-icon.svg
Normal file
After Width: | Height: | Size: 17 KiB |
1
public/images/modal-default-warning-icon.svg
Normal file
After Width: | Height: | Size: 17 KiB |
13
public/images/wallpaper-pattern.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<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">
|
||||
<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="#D53583" 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="#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 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 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 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 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(#p)" width="100%" height="100%"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
244
public/index.html
Normal file
@@ -0,0 +1,244 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{app_name}</title>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="UTF-8" />
|
||||
<style>
|
||||
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>
|
||||
<body>
|
||||
<div id="main">
|
||||
<div id="preloader" class="loader">
|
||||
<div class="wallpaper">
|
||||
<div class="content"></div>
|
||||
</div>
|
||||
<div class="spinner">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width='184' height='94' viewBox="0 0 184 94">
|
||||
<path
|
||||
stroke-linejoin="round"
|
||||
stroke-width="6"
|
||||
d="M121.208 2 2 57.011l70.927-.265L61.363 92 182 36.724h-69.498L121.208 2Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" async>
|
||||
import init from "{base_path}/wasm/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}/wasm/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>
|
||||
</html>
|
3
renovate.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||
}
|
6
spellcheck.dic
Normal file
@@ -0,0 +1,6 @@
|
||||
5
|
||||
Dioxus
|
||||
MSN
|
||||
renderers
|
||||
SDK
|
||||
TODO
|
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" ]
|
@@ -1,48 +0,0 @@
|
||||
$font-size: 100vh * 0.01;
|
||||
$icon-size: $font-size * 2;
|
||||
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
outline: 0px;
|
||||
font-family: Tahoma, sans-serif;
|
||||
}
|
||||
|
||||
#main {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aeroButton {
|
||||
height: 50%;
|
||||
min-height: 16px;
|
||||
aspect-ratio: 1;
|
||||
background-color: transparent;
|
||||
border: 2px solid transparent;
|
||||
background-size: contain !important;
|
||||
margin-right: 1%;
|
||||
}
|
||||
.aeroButton:hover {
|
||||
border-image: url(./images/aerobutton_border.png) 2 round;
|
||||
}
|
||||
.aeroButton:active {
|
||||
border-image: url(./images/aerobutton_border_down.png) 2 round;
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 50%;
|
||||
min-height: 16px;
|
||||
aspect-ratio: 1;
|
||||
background-color: transparent;
|
||||
border: 2px solid transparent;
|
||||
background-size: contain !important;
|
||||
margin-right: 1%;
|
||||
}
|
||||
.button:hover {
|
||||
border-image: url(./images/button_border.png) 2 round;
|
||||
}
|
||||
.button:active {
|
||||
border-image: url(./images/button_border_down.png) 2 round;
|
||||
}
|
121
src/base.rs
@@ -1,121 +0,0 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use dioxus_std::utils::rw::UseRw;
|
||||
use matrix_sdk::room::Room as MatrixRoom;
|
||||
use matrix_sdk::{
|
||||
room::RoomMember,
|
||||
ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId},
|
||||
};
|
||||
|
||||
use crate::matrix_client::Requester;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UserInfo {
|
||||
pub avatar_url: Option<OwnedMxcUri>,
|
||||
pub display_name: Option<String>,
|
||||
pub blurhash: Option<String>,
|
||||
}
|
||||
|
||||
impl UserInfo {
|
||||
pub fn new(
|
||||
avatar_url: Option<OwnedMxcUri>,
|
||||
display_name: Option<String>,
|
||||
blurhash: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
avatar_url,
|
||||
display_name,
|
||||
blurhash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Room {
|
||||
pub matrix_room: Arc<MatrixRoom>,
|
||||
pub topic: Option<String>,
|
||||
pub members: HashMap<OwnedUserId, RoomMember>,
|
||||
pub is_direct: Option<bool>,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
pub fn new(
|
||||
matrix_room: Arc<MatrixRoom>,
|
||||
topic: Option<String>,
|
||||
is_direct: Option<bool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
matrix_room,
|
||||
topic,
|
||||
members: HashMap::new(),
|
||||
is_direct,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<String> {
|
||||
self.matrix_room.name()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Room {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
// TODO: Look for a better way to compare Matrix rooms
|
||||
self.matrix_room.room_id() == other.matrix_room.room_id()
|
||||
&& self.topic == other.topic
|
||||
&& self.is_direct == other.is_direct
|
||||
}
|
||||
}
|
||||
|
||||
pub type ByIdRooms = HashMap<OwnedRoomId, Arc<RwLock<Room>>>;
|
||||
pub type ByIdUserInfos = HashMap<OwnedUserId, UserInfo>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Store {
|
||||
pub is_logged: bool,
|
||||
pub rooms: ByIdRooms,
|
||||
pub user_infos: ByIdUserInfos,
|
||||
pub user_id: Option<OwnedUserId>,
|
||||
}
|
||||
|
||||
impl<'a> Store {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_logged: false,
|
||||
rooms: HashMap::new(),
|
||||
user_infos: HashMap::new(),
|
||||
user_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Store {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.is_logged == other.is_logged
|
||||
&& self.user_id == other.user_id
|
||||
&& self.user_infos.len() == other.user_infos.len()
|
||||
&& self
|
||||
.user_infos
|
||||
.keys()
|
||||
.all(|k| other.user_infos.contains_key(k))
|
||||
&& self.rooms.len() == other.rooms.len()
|
||||
&& self.rooms.keys().all(|k| other.rooms.contains_key(k))
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Store {}
|
||||
|
||||
pub type ReactiveStore = Arc<UseRw<Store>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppSettings {
|
||||
pub requester: Option<Arc<Requester>>,
|
||||
}
|
||||
|
||||
impl AppSettings {
|
||||
pub fn new() -> Self {
|
||||
Self { requester: None }
|
||||
}
|
||||
}
|
@@ -1,60 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
turf::style_sheet!("src/components/avatar_selector.scss");
|
||||
|
||||
pub fn AvatarSelector(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
style { STYLE_SHEET },
|
||||
|
||||
div {
|
||||
class: ClassName::SELECTOR,
|
||||
svg {
|
||||
view_box: "0 0 100 100",
|
||||
linearGradient {
|
||||
id: "avatar-gradient",
|
||||
x1: 1,
|
||||
y1: 1,
|
||||
x2: 0,
|
||||
y2: 0,
|
||||
stop {
|
||||
offset: "0%",
|
||||
stop_color: "rgb(138, 191, 209)",
|
||||
}
|
||||
stop {
|
||||
offset: "60%",
|
||||
stop_color: "rgb(236, 246, 249)",
|
||||
}
|
||||
},
|
||||
filter {
|
||||
id: "avatar-shadow",
|
||||
feDropShadow {
|
||||
dx: 2,
|
||||
dy: 2,
|
||||
std_deviation: 3,
|
||||
flood_opacity: 0.5,
|
||||
},
|
||||
},
|
||||
rect {
|
||||
width: "90",
|
||||
height: "90",
|
||||
rx: "12",
|
||||
fill: "url('#avatar-gradient')",
|
||||
filter: "url('#avatar-shadow')",
|
||||
stroke: "grey",
|
||||
},
|
||||
// rect {
|
||||
// x: "7.5",
|
||||
// y: "7.5",
|
||||
// width: "75",
|
||||
// height: "75",
|
||||
// fill: "transparent",
|
||||
// stroke: "grey",
|
||||
// },
|
||||
},
|
||||
img {
|
||||
class: ClassName::PICTURE,
|
||||
src: "./images/default-avatar.png",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
.selector {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
aspect-ratio: 1;
|
||||
|
||||
.picture {
|
||||
position: absolute;
|
||||
height: 75%;
|
||||
aspect-ratio: 1;
|
||||
|
||||
bottom: 17.5%;
|
||||
right: 18%;
|
||||
}
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
pub fn ChatsWindow(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
}
|
||||
})
|
||||
}
|
@@ -1 +0,0 @@
|
||||
pub mod chats_window;
|
@@ -1,47 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_std::utils::rw::UseRw;
|
||||
|
||||
use crate::base::Room;
|
||||
use crate::base::Store;
|
||||
use crate::components::contacts_window::contacts_section::ContactsSection;
|
||||
|
||||
turf::style_sheet!("src/components/contacts_window/contacts.scss");
|
||||
|
||||
#[inline_props]
|
||||
pub fn Contacts<'a>(cx: Scope, rw_store: &'a UseRw<Store>) -> Element {
|
||||
println!("Contacts rendering");
|
||||
|
||||
let store = rw_store.read().unwrap();
|
||||
|
||||
let rooms = &store.rooms;
|
||||
|
||||
let rooms_len = rooms.len();
|
||||
let mut groups = Vec::<Room>::with_capacity(rooms_len);
|
||||
let mut directs = Vec::<Room>::with_capacity(rooms_len);
|
||||
|
||||
for arc_room in rooms.values() {
|
||||
let room = arc_room.read().unwrap().to_owned();
|
||||
|
||||
let is_direct = room.is_direct.unwrap();
|
||||
if is_direct {
|
||||
directs.push(room);
|
||||
} else {
|
||||
groups.push(room);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Test overflow
|
||||
// TODO: Add offline users ?
|
||||
cx.render(rsx! {
|
||||
style { STYLE_SHEET },
|
||||
|
||||
div {
|
||||
class: ClassName::CONTACTS,
|
||||
|
||||
ContactsSection {name: "Groups", contacts: RefCell::new(groups)},
|
||||
ContactsSection {name: "Available", contacts: RefCell::new(directs)},
|
||||
},
|
||||
})
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
@import "../../_base.scss"
|
||||
|
||||
.contacts {
|
||||
height: 72%;
|
||||
background-color: white;
|
||||
}
|
@@ -1,87 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_free_icons::icons::io_icons::IoChevronDown;
|
||||
use dioxus_free_icons::Icon;
|
||||
use matrix_sdk::RoomState;
|
||||
|
||||
use crate::base::Room;
|
||||
|
||||
turf::style_sheet!("src/components/contacts_window/contacts_section.scss");
|
||||
|
||||
fn ContactsArrow(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
style { STYLE_SHEET },
|
||||
|
||||
Icon {
|
||||
icon: IoChevronDown,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
static NO_SUBJECT_REPR: &str = "No subject";
|
||||
|
||||
#[inline_props]
|
||||
pub fn ContactsSection<'a>(cx: Scope, name: &'a str, contacts: RefCell<Vec<Room>>) -> Element {
|
||||
println!("ContactsSection rendering");
|
||||
|
||||
let show = use_state(cx, || false);
|
||||
|
||||
let classes = vec![
|
||||
ClassName::SECTION,
|
||||
if **show { ClassName::ACTIVE } else { "" },
|
||||
]
|
||||
.join(" ");
|
||||
|
||||
let contacts_len = contacts.borrow().len();
|
||||
|
||||
let rendered_contacts = contacts.borrow_mut().clone().into_iter().map(|room| {
|
||||
let room_name = room.name().unwrap();
|
||||
let is_invited = room.matrix_room.state() == RoomState::Invited;
|
||||
let formatted = format!(
|
||||
"{room_name} - {}",
|
||||
if is_invited {
|
||||
format!("Invited - ")
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
let room_topic = room.topic.unwrap_or(NO_SUBJECT_REPR.to_string()).to_owned();
|
||||
|
||||
rsx!(li {
|
||||
img {
|
||||
src: "./images/status_online.png",
|
||||
},
|
||||
p {
|
||||
formatted,
|
||||
},
|
||||
p {
|
||||
style: "color: darkgrey;",
|
||||
room_topic,
|
||||
},
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
cx.render(rsx! {
|
||||
style { STYLE_SHEET },
|
||||
|
||||
div {
|
||||
class: "{classes}",
|
||||
|
||||
p {
|
||||
class: ClassName::HEADER,
|
||||
onclick: move |_| show.set(!show),
|
||||
|
||||
ContactsArrow {},
|
||||
|
||||
format!("{name} ({contacts_len})"),
|
||||
},
|
||||
|
||||
ul {
|
||||
rendered_contacts.into_iter(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
@@ -1,67 +0,0 @@
|
||||
@import "../../_base.scss"
|
||||
|
||||
.section {
|
||||
width: 100%;
|
||||
font-size: $font-size;
|
||||
|
||||
&.active {
|
||||
ul {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 2%;
|
||||
width: 98%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
margin-left: 1%;
|
||||
padding-top: 1%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ul {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
transition: 0.4s ease;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
img {
|
||||
height: $icon-size;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
transition: 0.4s ease;
|
||||
}
|
||||
|
||||
.contact {
|
||||
list-style-type: none;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
cursor: pointer
|
||||
}
|
||||
}
|
@@ -1,94 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_std::utils::rw::UseRw;
|
||||
|
||||
use crate::base::Store;
|
||||
use crate::components::contacts_window::contacts::Contacts;
|
||||
use crate::components::contacts_window::user_infos::UserInfos;
|
||||
|
||||
turf::style_sheet!("src/components/contacts_window/contacts_window.scss");
|
||||
|
||||
#[inline_props]
|
||||
pub fn ContactsWindow<'a>(cx: Scope, rw_store: &'a UseRw<Store>) -> Element {
|
||||
cx.render(rsx! {
|
||||
style { STYLE_SHEET },
|
||||
|
||||
div {
|
||||
class: ClassName::CONTACTS_WINDOW,
|
||||
|
||||
div {
|
||||
class: ClassName::HEADER,
|
||||
|
||||
div {
|
||||
class: ClassName::TITLE_BAR,
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::USER_INFO,
|
||||
},
|
||||
|
||||
UserInfos {rw_store: rw_store},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::CONTACTS_NAV,
|
||||
div {
|
||||
class: ClassName::INNER,
|
||||
|
||||
button {
|
||||
class: ClassName::AERO_BUTTON,
|
||||
style: "background: url(./images/letter.png) center no-repeat",
|
||||
},
|
||||
button {
|
||||
class: ClassName::AERO_BUTTON,
|
||||
style: "background: url(./images/directory.png) no-repeat center",
|
||||
},
|
||||
button {
|
||||
class: ClassName::AERO_BUTTON,
|
||||
style: "background: url(./images/news.png) no-repeat center",
|
||||
},
|
||||
|
||||
button {
|
||||
class: ClassName::FLEX_RIGHT_AERO_BUTTON,
|
||||
style: "background: url(./images/brush.png) no-repeat center",
|
||||
},
|
||||
button {
|
||||
class: ClassName::AERO_BUTTON,
|
||||
style: "background: url(./images/settings.png) no-repeat center",
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::SEARCH,
|
||||
|
||||
div {
|
||||
class: ClassName::INNER,
|
||||
|
||||
input {
|
||||
class: ClassName::SEARCH_INPUT,
|
||||
placeholder: "Find a contact...",
|
||||
r#type: "text",
|
||||
},
|
||||
|
||||
button {
|
||||
class: ClassName::BUTTON,
|
||||
style: "background: url(./images/add_user.png) no-repeat center",
|
||||
},
|
||||
|
||||
button {
|
||||
class: ClassName::BUTTON,
|
||||
style: "background: url(./images/tbc_transfert.png) no-repeat center",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Contacts {rw_store: rw_store},
|
||||
|
||||
div {
|
||||
class: ClassName::FOOTER,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
@@ -1,84 +0,0 @@
|
||||
@import "../../_base.scss";
|
||||
|
||||
.contactsWindow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: #ECF6F9;
|
||||
font-family: "Tahoma", sans-serif;
|
||||
|
||||
border: thin solid #707070;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 5px #00000050;
|
||||
|
||||
|
||||
.header {
|
||||
height: 10%;
|
||||
width: 100%;
|
||||
|
||||
.titleBar {
|
||||
height: 60%;
|
||||
width: 100%;
|
||||
background:
|
||||
linear-gradient(180deg, #7DC5E3, #3883A3);
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
height: 40%;
|
||||
width: 100%;
|
||||
background:
|
||||
linear-gradient(180deg, #00658B, #0077A6);
|
||||
}
|
||||
}
|
||||
|
||||
.contactsNav {
|
||||
height: calc(31/1080*100%);
|
||||
background:
|
||||
linear-gradient(180deg, #00658B, #0077A6);
|
||||
|
||||
.inner {
|
||||
margin-left: 1%;
|
||||
margin-right: 1%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.flexRightAeroButton {
|
||||
@extend .aeroButton;
|
||||
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
height: calc(38/1080*100%);
|
||||
width: 100%;
|
||||
|
||||
border-bottom: thin solid #e2eaf3;
|
||||
|
||||
.inner {
|
||||
height: 100%;
|
||||
width: 98%;
|
||||
padding-left: 1%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.searchInput {
|
||||
height: calc(23/38*100%);
|
||||
width: 100%;
|
||||
margin-right: 1%;
|
||||
border: thin solid #c7c7c7;
|
||||
box-shadow: inset 0 0 calc(3/1080*100%) #0000002a;
|
||||
font-size: 8pt;
|
||||
|
||||
padding-left: 1%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 10%;
|
||||
}
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
pub mod contacts_window;
|
||||
|
||||
mod contacts;
|
||||
mod contacts_section;
|
||||
mod user_infos;
|
@@ -1,72 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_free_icons::icons::md_navigation_icons::MdArrowDropDown;
|
||||
use dioxus_free_icons::Icon;
|
||||
use dioxus_std::utils::rw::UseRw;
|
||||
|
||||
use crate::base::Store;
|
||||
use crate::components::avatar_selector::AvatarSelector;
|
||||
|
||||
turf::style_sheet!("src/components/contacts_window/user_infos.scss");
|
||||
|
||||
fn DownArrowIcon(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
style { STYLE_SHEET },
|
||||
|
||||
Icon {
|
||||
class: ClassName::DOWN_ARROW_ICON,
|
||||
icon: MdArrowDropDown,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static MESSAGE_PLACEHOLDER: &str = "<Enter a personal message>";
|
||||
|
||||
#[inline_props]
|
||||
pub fn UserInfos<'a>(cx: Scope, rw_store: &'a UseRw<Store>) -> Element {
|
||||
println!("UserInfos rendering");
|
||||
|
||||
let store = rw_store.read().unwrap().clone();
|
||||
|
||||
let user_id = store.user_id.unwrap();
|
||||
let user_info = store.user_infos.get(&user_id).unwrap();
|
||||
let user_display_name = user_info.display_name.as_ref().unwrap();
|
||||
|
||||
cx.render(rsx! {
|
||||
style { STYLE_SHEET },
|
||||
|
||||
div {
|
||||
class: ClassName::USER_INFO,
|
||||
|
||||
div {
|
||||
class: ClassName::AVATAR_SELECTOR,
|
||||
AvatarSelector {},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::INFO_CONTAINER,
|
||||
|
||||
div {
|
||||
class: ClassName::USER_ID,
|
||||
p {
|
||||
class: ClassName::USER_NAME,
|
||||
"{user_display_name}",
|
||||
},
|
||||
p {
|
||||
class: ClassName::USER_STATUS,
|
||||
"(Busy)",
|
||||
},
|
||||
DownArrowIcon {},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::USER_MESSAGE,
|
||||
p {
|
||||
// TODO: Handle user message
|
||||
MESSAGE_PLACEHOLDER,
|
||||
}
|
||||
DownArrowIcon {},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
@@ -1,71 +0,0 @@
|
||||
@import "../../_base.scss"
|
||||
|
||||
.userInfo {
|
||||
position: relative;
|
||||
height: 75%;
|
||||
width: 99%;
|
||||
top: -75%;
|
||||
left: 1%;
|
||||
aspect-ratio: 1;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.avatarSelector {
|
||||
height: 100%;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.infoContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.userId {
|
||||
@extend .aeroButton;
|
||||
|
||||
height: 30%;
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
text-align: begin;
|
||||
align-items: center;
|
||||
|
||||
.userName {
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.userStatus {
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
color: #B9DDE7;
|
||||
}
|
||||
}
|
||||
|
||||
.userMessage {
|
||||
@extend .aeroButton;
|
||||
|
||||
width: fit-content;
|
||||
height: 30%;
|
||||
display: flex;
|
||||
text-align: begin;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.downArrowIcon {
|
||||
color: transparent;
|
||||
|
||||
path:last-child {
|
||||
fill: white;
|
||||
}
|
||||
}
|
@@ -1,26 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
turf::style_sheet!("src/components/header.scss");
|
||||
|
||||
pub fn Header(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
style { STYLE_SHEET },
|
||||
|
||||
div {
|
||||
class: ClassName::ROOT,
|
||||
img {
|
||||
// src: "./assets/live-logo2.png"
|
||||
src: "./images/logo-msn.png"
|
||||
}
|
||||
svg {
|
||||
view_box: "0 0 100 10",
|
||||
text {
|
||||
y: "55%",
|
||||
dominant_baseline: "middle",
|
||||
font_size: "5",
|
||||
"Windows Live Messenger",
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
.root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: white;
|
||||
}
|
||||
}
|
@@ -1,148 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_std::utils::rw::UseRw;
|
||||
|
||||
use crate::base::{AppSettings, Store};
|
||||
use crate::components::avatar_selector::AvatarSelector;
|
||||
use crate::components::header::Header;
|
||||
use crate::matrix_client::{LoginStyle, MatrixClient};
|
||||
|
||||
turf::style_sheet!("src/components/login.scss");
|
||||
|
||||
static EMPTY_PLACEHOLDER: &str = "Tmp placeholder";
|
||||
|
||||
#[inline_props]
|
||||
pub fn Login<'a>(cx: Scope, rw_store: &'a UseRw<Store>) -> Element {
|
||||
let app_context = use_shared_state::<AppSettings>(cx).unwrap();
|
||||
let invalid_login = use_state(cx, || false);
|
||||
let login = use_ref(cx, || Login::new());
|
||||
|
||||
let arc_store = Arc::new(rw_store.to_owned().clone());
|
||||
|
||||
let password_class = if **invalid_login {
|
||||
ClassName::INVALID_INPUT
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let run_matrix_client = move |_| {
|
||||
cx.spawn({
|
||||
to_owned![app_context, invalid_login, login, arc_store];
|
||||
|
||||
let login_ref = login.read();
|
||||
let homeserver_url = login_ref.homeserver_url.clone().unwrap();
|
||||
let username = login_ref.email.clone().unwrap();
|
||||
let password = login_ref.password.clone().unwrap();
|
||||
|
||||
async move {
|
||||
let requester = MatrixClient::spawn(homeserver_url, arc_store.clone()).await;
|
||||
requester.init();
|
||||
|
||||
match requester.login(LoginStyle::Password(username, password)) {
|
||||
Ok(_) => {
|
||||
println!("successfully logged");
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error during login: {err}");
|
||||
invalid_login.modify(|_| true);
|
||||
}
|
||||
}
|
||||
|
||||
app_context.write().requester = Some(Arc::new(requester));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let login_ref = login.read();
|
||||
let placeholder = EMPTY_PLACEHOLDER.to_string();
|
||||
let homeserver_url_value = login_ref.homeserver_url.as_ref().unwrap_or(&placeholder);
|
||||
let email_value = login_ref.email.as_ref().unwrap_or(&placeholder);
|
||||
let password_value = login_ref.password.as_ref().unwrap_or(&placeholder);
|
||||
|
||||
cx.render(rsx! {
|
||||
style { STYLE_SHEET },
|
||||
|
||||
div {
|
||||
class: ClassName::ROOT,
|
||||
|
||||
div {
|
||||
class: ClassName::HEADER,
|
||||
Header {},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::BODY,
|
||||
div {
|
||||
class: ClassName::AVATAR_SELECTOR,
|
||||
AvatarSelector {},
|
||||
},
|
||||
|
||||
p {
|
||||
"Matrix homeserver:"
|
||||
},
|
||||
input {
|
||||
id: "input-homeserver-url",
|
||||
r#type: "text",
|
||||
name: "homeserver URL",
|
||||
value: "{homeserver_url_value}",
|
||||
oninput: move |evt| login.write().homeserver_url = Some(evt.value.clone()),
|
||||
},
|
||||
|
||||
p {
|
||||
"E-mail address:"
|
||||
},
|
||||
input {
|
||||
id: "login-input-email",
|
||||
r#type: "text",
|
||||
name: "email",
|
||||
value: "{email_value}",
|
||||
oninput: move |evt| login.write().email = Some(evt.value.clone()),
|
||||
},
|
||||
p {
|
||||
"Password:"
|
||||
},
|
||||
input {
|
||||
class: "{password_class}",
|
||||
id: "login-input-password",
|
||||
r#type: "password",
|
||||
name: "Password",
|
||||
value: "{password_value}",
|
||||
oninput: move |evt| {
|
||||
login.write().password = Some(evt.value.clone());
|
||||
invalid_login.set(false);
|
||||
},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::FOOTER_BUTTONS,
|
||||
input {
|
||||
class: ClassName::BUTTON,
|
||||
onclick: run_matrix_client,
|
||||
r#type: "submit",
|
||||
value: "sign in",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Login {
|
||||
homeserver_url: Option<String>,
|
||||
email: Option<String>,
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
impl Login {
|
||||
fn new() -> Self {
|
||||
let login = Self {
|
||||
homeserver_url: None,
|
||||
email: None,
|
||||
password: None,
|
||||
};
|
||||
login
|
||||
}
|
||||
}
|
@@ -1,51 +0,0 @@
|
||||
@import "../_base.scss";
|
||||
|
||||
.root {
|
||||
width: 90%;
|
||||
height: 98%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
padding: 5%;
|
||||
padding-top: 2%;
|
||||
|
||||
background: linear-gradient(rgb(138, 191, 209), rgb(236, 246, 249) 10%);
|
||||
|
||||
.header {
|
||||
height: 5%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.body {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
max-width: 400px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
padding-bottom: 3%;
|
||||
|
||||
.invalidInput {
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
.avatarSelector {
|
||||
height: 30%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.footerButtons {
|
||||
width: 100%;
|
||||
|
||||
padding-top: 5%;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_std::utils::rw::use_rw;
|
||||
|
||||
use crate::base::Store;
|
||||
use crate::components::contacts_window::contacts_window::ContactsWindow;
|
||||
use crate::components::login::Login;
|
||||
|
||||
#[inline_props]
|
||||
pub fn MainWindow(cx: Scope) -> Element {
|
||||
let rw_store = use_rw(cx, || Store::new());
|
||||
|
||||
let is_logged = rw_store.read().unwrap().is_logged;
|
||||
|
||||
cx.render(rsx! {
|
||||
if is_logged {
|
||||
rsx!(ContactsWindow {rw_store: rw_store})
|
||||
}
|
||||
else {
|
||||
rsx!(Login {rw_store: rw_store})
|
||||
}
|
||||
})
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
pub mod avatar_selector;
|
||||
pub mod chats_window;
|
||||
pub mod contacts_window;
|
||||
pub mod header;
|
||||
pub mod login;
|
||||
pub mod main_window;
|
1
src/domain/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub(crate) mod model;
|
136
src/domain/model/account.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tracing::{error, instrument, trace};
|
||||
|
||||
use super::{
|
||||
common::PresenceState,
|
||||
messaging_interface::{
|
||||
AccountMessagingConsumerInterface, AccountMessagingProviderInterface,
|
||||
RoomMessagingConsumerInterface, SpaceMessagingConsumerInterface,
|
||||
},
|
||||
room::{Room, RoomId},
|
||||
space::{Space, SpaceId},
|
||||
store_interface::{
|
||||
AccountStoreProviderInterface, RoomStoreConsumerInterface, SpaceStoreConsumerInterface,
|
||||
},
|
||||
};
|
||||
|
||||
type Rooms = HashMap<RoomId, Rc<Room>>;
|
||||
type Spaces = HashMap<SpaceId, Rc<Space>>;
|
||||
|
||||
pub struct Account {
|
||||
display_name: RefCell<Option<String>>,
|
||||
avatar: RefCell<Option<Vec<u8>>>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
presence_state: RefCell<Option<PresenceState>>,
|
||||
|
||||
by_id_rooms: RefCell<Rooms>,
|
||||
by_id_spaces: RefCell<Spaces>,
|
||||
|
||||
messaging_provider: Option<Rc<dyn AccountMessagingProviderInterface>>,
|
||||
store: &'static dyn AccountStoreProviderInterface,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn new(store: &'static dyn AccountStoreProviderInterface) -> Self {
|
||||
Self {
|
||||
display_name: RefCell::new(None),
|
||||
avatar: RefCell::new(None),
|
||||
presence_state: RefCell::new(None),
|
||||
|
||||
by_id_rooms: RefCell::new(Rooms::new()),
|
||||
by_id_spaces: RefCell::new(Spaces::new()),
|
||||
|
||||
messaging_provider: None,
|
||||
store,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_messaging_provider(&mut self, provider: Rc<dyn AccountMessagingProviderInterface>) {
|
||||
self.messaging_provider = Some(provider.clone());
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_room(&self, room_id: &RoomId) -> Option<Rc<Room>> {
|
||||
self.by_id_rooms.borrow().get(room_id).cloned()
|
||||
}
|
||||
|
||||
pub async fn get_display_name(&self) -> &RefCell<Option<String>> {
|
||||
if self.display_name.borrow().is_none() {
|
||||
if let Some(requester) = &self.messaging_provider {
|
||||
let resp = requester.get_display_name().await;
|
||||
if let Ok(display_name) = resp {
|
||||
if let Some(display_name) = display_name {
|
||||
self.display_name.borrow_mut().replace(display_name);
|
||||
} else {
|
||||
self.display_name.borrow_mut().take();
|
||||
}
|
||||
} else {
|
||||
error!("err={:?}", resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
&self.display_name
|
||||
}
|
||||
|
||||
pub async fn get_avatar(&self) -> &RefCell<Option<Vec<u8>>> {
|
||||
if self.avatar.borrow().is_none() {
|
||||
if let Some(requester) = &self.messaging_provider {
|
||||
let resp = requester.get_avatar().await;
|
||||
if let Ok(avatar) = resp {
|
||||
if let Some(avatar) = avatar {
|
||||
self.avatar.borrow_mut().replace(avatar);
|
||||
} else {
|
||||
self.avatar.borrow_mut().take();
|
||||
}
|
||||
} else {
|
||||
error!("err={:?}", resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
&self.avatar
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl AccountMessagingConsumerInterface for Account {
|
||||
#[instrument(name = "Account", skip_all)]
|
||||
async fn on_new_room(&self, room: Rc<Room>) -> Rc<dyn RoomMessagingConsumerInterface> {
|
||||
trace!("on_new_room");
|
||||
|
||||
let room_id = room.id().clone();
|
||||
|
||||
self.by_id_rooms
|
||||
.borrow_mut()
|
||||
.insert(room_id, Rc::clone(&room));
|
||||
|
||||
let room_store = self
|
||||
.store
|
||||
.on_new_room(Rc::clone(&room) as Rc<dyn RoomStoreConsumerInterface>);
|
||||
|
||||
room.set_store(room_store);
|
||||
|
||||
room
|
||||
}
|
||||
|
||||
#[instrument(name = "Account", skip_all)]
|
||||
async fn on_new_space(&self, space: Rc<Space>) -> Rc<dyn SpaceMessagingConsumerInterface> {
|
||||
trace!("on_new_space");
|
||||
|
||||
let space_id = space.id().clone();
|
||||
|
||||
self.by_id_spaces
|
||||
.borrow_mut()
|
||||
.insert(space_id, Rc::clone(&space));
|
||||
|
||||
let space_store = self
|
||||
.store
|
||||
.on_new_space(Rc::clone(&space) as Rc<dyn SpaceStoreConsumerInterface>);
|
||||
|
||||
space.set_store(space_store);
|
||||
|
||||
space
|
||||
}
|
||||
}
|
7
src/domain/model/common.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use matrix_sdk::ruma::{presence::PresenceState as MatrixPresenceState, OwnedUserId};
|
||||
|
||||
pub type Avatar = Vec<u8>;
|
||||
|
||||
pub type PresenceState = MatrixPresenceState;
|
||||
|
||||
pub type UserId = OwnedUserId;
|
69
src/domain/model/messaging_interface.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::broadcast::Receiver;
|
||||
|
||||
use super::{
|
||||
common::{Avatar, UserId},
|
||||
room::{Invitation, Room, RoomId},
|
||||
room_member::{AvatarUrl, RoomMember},
|
||||
space::Space,
|
||||
};
|
||||
use crate::infrastructure::messaging::matrix::account_event::AccountEvent;
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait AccountMessagingConsumerInterface {
|
||||
async fn on_new_room(&self, room: Rc<Room>) -> Rc<dyn RoomMessagingConsumerInterface>;
|
||||
async fn on_new_space(&self, space: Rc<Space>) -> Rc<dyn SpaceMessagingConsumerInterface>;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait AccountMessagingProviderInterface {
|
||||
async fn get_display_name(&self) -> anyhow::Result<Option<String>>;
|
||||
async fn get_avatar(&self) -> anyhow::Result<Option<Vec<u8>>>;
|
||||
|
||||
async fn run_forever(
|
||||
&self,
|
||||
account_events_consumer: &dyn AccountMessagingConsumerInterface,
|
||||
account_events_receiver: Receiver<AccountEvent>,
|
||||
) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait RoomMessagingConsumerInterface {
|
||||
async fn on_invitation(&self, _invitation: Invitation) {}
|
||||
|
||||
async fn on_new_topic(&self, _topic: Option<String>) {}
|
||||
async fn on_new_name(&self, _name: Option<String>) {}
|
||||
async fn on_new_avatar(&self, _url: Option<Avatar>) {}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn on_membership(&self, _member: RoomMember) {}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait RoomMessagingProviderInterface {
|
||||
async fn get_avatar(&self, id: &RoomId) -> anyhow::Result<Option<Avatar>>;
|
||||
async fn join(&self, room_id: &RoomId) -> anyhow::Result<bool>;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait SpaceMessagingConsumerInterface {
|
||||
async fn on_child(&self, _room_id: RoomId) {}
|
||||
async fn on_new_topic(&self, _topic: Option<String>) {}
|
||||
async fn on_new_name(&self, _name: Option<String>) {}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait SpaceMessagingProviderInterface {}
|
||||
|
||||
// TODO: Rework
|
||||
#[async_trait(?Send)]
|
||||
pub trait MemberMessagingProviderInterface {
|
||||
async fn get_avatar(
|
||||
&self,
|
||||
avatar_url: Option<AvatarUrl>,
|
||||
room_id: RoomId,
|
||||
user_id: UserId,
|
||||
) -> anyhow::Result<Option<Avatar>>;
|
||||
}
|
8
src/domain/model/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
pub(crate) mod account;
|
||||
pub(crate) mod common;
|
||||
pub(crate) mod messaging_interface;
|
||||
pub(crate) mod room;
|
||||
pub(crate) mod room_member;
|
||||
pub(crate) mod session;
|
||||
pub(crate) mod space;
|
||||
pub(crate) mod store_interface;
|
323
src/domain/model/room.rs
Normal file
@@ -0,0 +1,323 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
fmt::{Debug, Formatter},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::future::{join, join_all};
|
||||
use matrix_sdk::{ruma::OwnedRoomId, RoomState as MatrixRoomState};
|
||||
use tracing::{debug, debug_span, error, instrument, trace};
|
||||
|
||||
use super::{
|
||||
common::{Avatar, UserId},
|
||||
messaging_interface::{RoomMessagingConsumerInterface, RoomMessagingProviderInterface},
|
||||
room_member::RoomMember,
|
||||
space::SpaceId,
|
||||
store_interface::{RoomStoreConsumerInterface, RoomStoreProviderInterface},
|
||||
};
|
||||
use crate::infrastructure::services::mozaik_builder::create_mozaik;
|
||||
|
||||
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 {
|
||||
id: RoomId,
|
||||
|
||||
name: RefCell<Option<String>>,
|
||||
topic: Option<String>,
|
||||
is_direct: Option<bool>,
|
||||
state: Option<MatrixRoomState>,
|
||||
avatar: RefCell<Option<Avatar>>,
|
||||
|
||||
invitations: RefCell<HashMap<UserId, Invitation>>,
|
||||
members: RefCell<HashMap<UserId, RoomMember>>,
|
||||
|
||||
spaces: Vec<SpaceId>,
|
||||
|
||||
messaging_provider: Option<Rc<dyn RoomMessagingProviderInterface>>,
|
||||
store: RefCell<Option<Rc<dyn RoomStoreProviderInterface>>>,
|
||||
}
|
||||
|
||||
impl PartialEq for Room {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Room {
|
||||
pub fn new(
|
||||
id: RoomId,
|
||||
// TODO: move space at the end of the list of params
|
||||
name: Option<String>,
|
||||
topic: Option<String>,
|
||||
is_direct: Option<bool>,
|
||||
state: Option<MatrixRoomState>,
|
||||
spaces: Vec<SpaceId>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
|
||||
name: RefCell::new(name),
|
||||
topic,
|
||||
is_direct,
|
||||
state,
|
||||
avatar: RefCell::new(None),
|
||||
invitations: RefCell::new(HashMap::new()),
|
||||
members: RefCell::new(HashMap::new()),
|
||||
spaces,
|
||||
|
||||
messaging_provider: None,
|
||||
store: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_messaging_provider(
|
||||
&mut self,
|
||||
messaging_provider: Rc<dyn RoomMessagingProviderInterface>,
|
||||
) {
|
||||
self.messaging_provider = Some(messaging_provider);
|
||||
}
|
||||
|
||||
pub fn set_store(&self, store: Rc<dyn RoomStoreProviderInterface>) {
|
||||
*self.store.borrow_mut() = Some(store);
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &RoomId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn name(&self) -> Option<String> {
|
||||
self.name.borrow().clone()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_topic(&mut self, topic: Option<String>) {
|
||||
self.topic = topic;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn state(&self) -> &Option<MatrixRoomState> {
|
||||
&self.state
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_invited(&self) -> Option<bool> {
|
||||
self.state.map(|state| state == MatrixRoomState::Invited)
|
||||
}
|
||||
|
||||
#[instrument(name = "Room", skip_all)]
|
||||
fn add_invitation(&self, invitation: Invitation) {
|
||||
self.members.borrow_mut().remove(&invitation.invitee_id);
|
||||
|
||||
self.invitations
|
||||
.borrow_mut()
|
||||
.insert(invitation.invitee_id.clone(), invitation.clone());
|
||||
|
||||
if let Some(store) = self.store.borrow().as_ref() {
|
||||
store.on_invitation(invitation);
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(name = "Room", skip_all)]
|
||||
fn add_member(&self, member: RoomMember) {
|
||||
let mut members = self.members.borrow_mut();
|
||||
|
||||
members.insert(member.id().clone(), member.clone());
|
||||
|
||||
// USe the member display name to name the room if it's direct and has no name set.
|
||||
if self.name.borrow().is_none() && members.len() == 1 {
|
||||
if let Some(member_display_name) = member.display_name() {
|
||||
let name = Some(member_display_name.clone());
|
||||
|
||||
self.name.borrow_mut().clone_from(&name);
|
||||
|
||||
if let Some(store) = self.store.borrow().as_ref() {
|
||||
store.on_new_name(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(store) = self.store.borrow().as_ref() {
|
||||
store.on_new_member(member);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_avatar(&self) -> Option<Avatar> {
|
||||
if self.avatar.borrow().is_none() {
|
||||
if let Some(requester) = &self.messaging_provider {
|
||||
let resp = requester.get_avatar(&self.id).await;
|
||||
if let Ok(avatar) = resp {
|
||||
if let Some(avatar) = avatar {
|
||||
return Some(avatar);
|
||||
} else {
|
||||
debug!("The room has no avatar... let's generate one");
|
||||
match self.gen_room_avatar_with_members().await {
|
||||
Ok(avatar) => {
|
||||
if let Some(avatar) = avatar {
|
||||
return Some(avatar);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("err={}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("err={:?}", resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.avatar.borrow().clone()
|
||||
}
|
||||
|
||||
#[instrument(name = "Room", skip_all)]
|
||||
async fn gen_room_avatar_with_members(&self) -> anyhow::Result<Option<Avatar>> {
|
||||
let mut account_member = None::<&RoomMember>;
|
||||
let mut other_members = Vec::<&RoomMember>::new();
|
||||
|
||||
let members = self.members.borrow();
|
||||
for member in members.values() {
|
||||
if member.is_account_user() {
|
||||
account_member = Some(member);
|
||||
} else {
|
||||
other_members.push(member);
|
||||
}
|
||||
}
|
||||
|
||||
let other_avatars_futures =
|
||||
join_all(other_members.iter().map(|member| member.get_avatar()));
|
||||
|
||||
let (other_avatars, account_avatar) = if let Some(account_member) = account_member {
|
||||
join(other_avatars_futures, account_member.get_avatar()).await
|
||||
} else {
|
||||
(
|
||||
join_all(other_members.iter().map(|member| member.get_avatar())).await,
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
let other_avatars: Vec<Vec<u8>> = other_avatars.into_iter().flatten().collect();
|
||||
|
||||
if account_avatar.is_some() || !other_avatars.is_empty() {
|
||||
let _guard = debug_span!("AvatarRendering").entered();
|
||||
Ok(Some(
|
||||
create_mozaik(256, 256, other_avatars, account_avatar).await,
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl RoomMessagingConsumerInterface for Room {
|
||||
#[instrument(name = "Room", skip_all)]
|
||||
async fn on_invitation(&self, invitation: Invitation) {
|
||||
trace!("on_invitation");
|
||||
let sender_id = invitation.sender_id.clone();
|
||||
|
||||
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) {
|
||||
trace!("on_membership");
|
||||
self.add_member(member);
|
||||
}
|
||||
|
||||
#[instrument(name = "Room", skip_all)]
|
||||
async fn on_new_topic(&self, _topic: Option<String>) {
|
||||
trace!("on_new_topic");
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl RoomStoreConsumerInterface for Room {
|
||||
fn id(&self) -> &RoomId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn is_direct(&self) -> Option<bool> {
|
||||
self.is_direct
|
||||
}
|
||||
|
||||
fn name(&self) -> Option<String> {
|
||||
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> {
|
||||
&self.spaces
|
||||
}
|
||||
|
||||
async fn join(&self) {
|
||||
if let Some(messaging_provider) = &self.messaging_provider {
|
||||
let _ = messaging_provider.join(&self.id).await;
|
||||
}
|
||||
}
|
||||
}
|
93
src/domain/model/room_member.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
fmt::{Debug, Formatter},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use matrix_sdk::ruma::OwnedMxcUri;
|
||||
use tracing::error;
|
||||
|
||||
use super::{
|
||||
common::{Avatar, UserId},
|
||||
messaging_interface::MemberMessagingProviderInterface,
|
||||
room::RoomId,
|
||||
};
|
||||
|
||||
pub type AvatarUrl = OwnedMxcUri;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RoomMember {
|
||||
id: UserId,
|
||||
|
||||
display_name: Option<String>,
|
||||
avatar_url: Option<AvatarUrl>,
|
||||
room_id: RoomId,
|
||||
is_account_user: bool,
|
||||
|
||||
#[allow(dead_code)]
|
||||
avatar: RefCell<Option<Avatar>>,
|
||||
|
||||
messaging_provider: Rc<dyn MemberMessagingProviderInterface>,
|
||||
}
|
||||
|
||||
impl RoomMember {
|
||||
pub fn new(
|
||||
id: UserId,
|
||||
display_name: Option<String>,
|
||||
avatar_url: Option<AvatarUrl>,
|
||||
room_id: RoomId,
|
||||
is_account_user: bool,
|
||||
messaging_provider: Rc<dyn MemberMessagingProviderInterface>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
display_name,
|
||||
avatar_url,
|
||||
room_id,
|
||||
is_account_user,
|
||||
avatar: RefCell::new(None),
|
||||
messaging_provider,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &UserId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> &Option<String> {
|
||||
&self.display_name
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn room_id(&self) -> &RoomId {
|
||||
&self.room_id
|
||||
}
|
||||
|
||||
pub fn is_account_user(&self) -> bool {
|
||||
self.is_account_user
|
||||
}
|
||||
|
||||
pub async fn get_avatar(&self) -> Option<Avatar> {
|
||||
match self
|
||||
.messaging_provider
|
||||
.get_avatar(
|
||||
self.avatar_url.clone(),
|
||||
self.room_id.clone(),
|
||||
self.id.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(avatar) => avatar,
|
||||
Err(err) => {
|
||||
error!("err={}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for RoomMember {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
f.debug_struct("RoomMember").field("id", &self.id).finish()
|
||||
}
|
||||
}
|
26
src/domain/model/session.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
pub struct Session {
|
||||
pub homeserver_url: Option<String>,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub is_logged: bool,
|
||||
}
|
||||
impl Session {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
homeserver_url: None,
|
||||
username: None,
|
||||
password: None,
|
||||
is_logged: false,
|
||||
}
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
homeserver_url: Option<String>,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
) {
|
||||
self.homeserver_url = homeserver_url;
|
||||
self.username = username;
|
||||
self.password = password;
|
||||
}
|
||||
}
|
107
src/domain/model/space.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use std::{cell::RefCell, collections::HashSet, rc::Rc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk::ruma::OwnedRoomId;
|
||||
use tracing::{instrument, trace};
|
||||
|
||||
use super::{
|
||||
common::Avatar,
|
||||
messaging_interface::{SpaceMessagingConsumerInterface, SpaceMessagingProviderInterface},
|
||||
room::RoomId,
|
||||
store_interface::{SpaceStoreConsumerInterface, SpaceStoreProviderInterface},
|
||||
};
|
||||
|
||||
pub type SpaceId = OwnedRoomId;
|
||||
|
||||
// TODO: Add membership?
|
||||
pub struct Space {
|
||||
id: SpaceId,
|
||||
|
||||
name: RefCell<Option<String>>,
|
||||
topic: RefCell<Option<String>>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
avatar: RefCell<Option<Avatar>>,
|
||||
|
||||
children: RefCell<HashSet<RoomId>>, // We don´t expect to manage nested spaces
|
||||
|
||||
messaging_provider: Option<Rc<dyn SpaceMessagingProviderInterface>>,
|
||||
store: RefCell<Option<Rc<dyn SpaceStoreProviderInterface>>>,
|
||||
}
|
||||
|
||||
impl PartialEq for Space {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Space {
|
||||
pub fn new(id: SpaceId, name: Option<String>, topic: Option<String>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
|
||||
name: RefCell::new(name),
|
||||
topic: RefCell::new(topic),
|
||||
|
||||
#[allow(dead_code)]
|
||||
avatar: RefCell::new(None),
|
||||
|
||||
children: RefCell::new(HashSet::new()),
|
||||
|
||||
messaging_provider: None,
|
||||
store: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_messaging_provider(&mut self, provider: Rc<dyn SpaceMessagingProviderInterface>) {
|
||||
self.messaging_provider = Some(provider);
|
||||
}
|
||||
|
||||
pub fn set_store(&self, store: Rc<dyn SpaceStoreProviderInterface>) {
|
||||
*self.store.borrow_mut() = Some(store);
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &SpaceId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn name(&self) -> Option<String> {
|
||||
self.name.borrow().clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl SpaceMessagingConsumerInterface for Space {
|
||||
#[instrument(name = "Space", skip_all)]
|
||||
async fn on_child(&self, room_id: RoomId) {
|
||||
trace!("on_child");
|
||||
self.children.borrow_mut().insert(room_id);
|
||||
}
|
||||
|
||||
#[instrument(name = "Space", skip_all)]
|
||||
async fn on_new_topic(&self, topic: Option<String>) {
|
||||
trace!("on_new_topic");
|
||||
*self.topic.borrow_mut() = topic;
|
||||
}
|
||||
|
||||
#[instrument(name = "Space", skip_all)]
|
||||
async fn on_new_name(&self, name: Option<String>) {
|
||||
trace!("on_new_name");
|
||||
self.name.borrow_mut().clone_from(&name);
|
||||
|
||||
if let Some(store) = self.store.borrow().as_ref() {
|
||||
store.set_name(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SpaceStoreConsumerInterface for Space {
|
||||
fn id(&self) -> &SpaceId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn name(&self) -> Option<String> {
|
||||
self.name.borrow().clone()
|
||||
}
|
||||
}
|
56
src/domain/model/store_interface.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::{
|
||||
common::Avatar,
|
||||
room::{Invitation, RoomId},
|
||||
room_member::RoomMember,
|
||||
space::SpaceId,
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub trait AccountStoreConsumerInterface {}
|
||||
|
||||
pub trait AccountStoreProviderInterface {
|
||||
fn on_new_room(
|
||||
&self,
|
||||
room: Rc<dyn RoomStoreConsumerInterface>,
|
||||
) -> Rc<dyn RoomStoreProviderInterface>;
|
||||
fn on_new_space(
|
||||
&self,
|
||||
space: Rc<dyn SpaceStoreConsumerInterface>,
|
||||
) -> Rc<dyn SpaceStoreProviderInterface>;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait RoomStoreConsumerInterface {
|
||||
fn id(&self) -> &RoomId;
|
||||
fn is_direct(&self) -> Option<bool>;
|
||||
fn name(&self) -> Option<String>;
|
||||
fn topic(&self) -> Option<String>;
|
||||
fn spaces(&self) -> &Vec<SpaceId>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn avatar(&self) -> Option<Avatar>;
|
||||
async fn join(&self);
|
||||
}
|
||||
|
||||
pub trait RoomStoreProviderInterface {
|
||||
fn on_new_name(&self, name: Option<String>);
|
||||
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_invitation(&self, invitation: Invitation);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub trait SpaceStoreConsumerInterface {
|
||||
fn id(&self) -> &SpaceId;
|
||||
fn name(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
pub trait SpaceStoreProviderInterface {
|
||||
fn set_name(&self, _name: Option<String>) {}
|
||||
}
|
63
src/infrastructure/messaging/matrix/account_event.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
use matrix_sdk::{ruma::OwnedRoomId, RoomState};
|
||||
use tracing::Span;
|
||||
|
||||
use super::room_event::RoomEventsReceiver;
|
||||
use crate::{domain::model::space::SpaceId, utils::Sender};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AccountEvent {
|
||||
NewRoom(
|
||||
OwnedRoomId,
|
||||
Vec<SpaceId>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<bool>,
|
||||
RoomState,
|
||||
RoomEventsReceiver,
|
||||
Sender<bool>,
|
||||
Span,
|
||||
),
|
||||
|
||||
NewSpace(
|
||||
OwnedRoomId,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
RoomEventsReceiver,
|
||||
Sender<bool>,
|
||||
Span,
|
||||
),
|
||||
}
|
||||
|
||||
impl Debug for AccountEvent {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NewRoom(
|
||||
id,
|
||||
spaces,
|
||||
name,
|
||||
topic,
|
||||
is_direct,
|
||||
state,
|
||||
_events_receiver,
|
||||
_sender,
|
||||
_span,
|
||||
) => f
|
||||
.debug_tuple("AccountEvent::NewRoom")
|
||||
.field(id)
|
||||
.field(spaces)
|
||||
.field(name)
|
||||
.field(topic)
|
||||
.field(is_direct)
|
||||
.field(state)
|
||||
.finish(),
|
||||
Self::NewSpace(id, name, topic, _events_receiver, _sender, _span) => f
|
||||
.debug_tuple("AccountEvent::NewSpace")
|
||||
.field(id)
|
||||
.field(name)
|
||||
.field(topic)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
841
src/infrastructure/messaging/matrix/client.rs
Normal file
@@ -0,0 +1,841 @@
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use async_std::stream::StreamExt;
|
||||
use dioxus::prelude::Task;
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
event_handler::Ctx,
|
||||
media::{MediaFormat, MediaRequest, MediaThumbnailSettings, MediaThumbnailSize},
|
||||
room::{ParentSpace, Room},
|
||||
ruma::{
|
||||
api::client::media::get_content_thumbnail::v3::Method,
|
||||
events::{
|
||||
room::{
|
||||
avatar::{RoomAvatarEventContent, StrippedRoomAvatarEvent},
|
||||
create::{RoomCreateEventContent, StrippedRoomCreateEvent},
|
||||
member::{MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent},
|
||||
name::{RoomNameEventContent, StrippedRoomNameEvent},
|
||||
topic::{RoomTopicEventContent, StrippedRoomTopicEvent},
|
||||
MediaSource,
|
||||
},
|
||||
SyncStateEvent,
|
||||
},
|
||||
uint, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
},
|
||||
Client as MatrixClient, RoomState,
|
||||
};
|
||||
use tokio::sync::{
|
||||
broadcast,
|
||||
broadcast::{error::SendError, Receiver, Sender},
|
||||
mpsc::{unbounded_channel, UnboundedReceiver},
|
||||
};
|
||||
use tracing::{debug, debug_span, error, instrument, warn, Instrument, Span};
|
||||
|
||||
use super::{
|
||||
account_event::AccountEvent,
|
||||
requester::Requester,
|
||||
room_event::{RoomEvent, RoomEventsReceiver},
|
||||
worker_tasks::{LoginStyle, WorkerTask},
|
||||
};
|
||||
use crate::utils::oneshot;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ClientError {
|
||||
#[error("Matrix client error: {0}")]
|
||||
Matrix(#[from] matrix_sdk::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Senders {
|
||||
account_events_sender: Sender<AccountEvent>,
|
||||
room_events_senders: Arc<Mutex<HashMap<OwnedRoomId, Sender<RoomEvent>>>>,
|
||||
}
|
||||
|
||||
impl Senders {
|
||||
fn new(account_events_sender: Sender<AccountEvent>) -> Self {
|
||||
Self {
|
||||
account_events_sender,
|
||||
room_events_senders: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn contains(&self, room_id: &RoomId) -> bool {
|
||||
let room_senders = self.room_events_senders.lock().unwrap();
|
||||
|
||||
room_senders.contains_key(room_id)
|
||||
}
|
||||
|
||||
fn send(&self, room_id: &RoomId, event: RoomEvent) -> Result<(), SendError<RoomEvent>> {
|
||||
let room_senders = self.room_events_senders.lock().unwrap();
|
||||
|
||||
if let Some(room_sender) = room_senders.get(room_id) {
|
||||
if let Err(err) = room_sender.send(event) {
|
||||
warn!("Unable to send event to the {room_id} room: {err}");
|
||||
return Err(err);
|
||||
}
|
||||
} else {
|
||||
warn!("No sender found for {room_id} room");
|
||||
// TODO: Return error
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_room(&self, room_id: &OwnedRoomId) -> Option<RoomEventsReceiver> {
|
||||
let mut senders = self.room_events_senders.lock().unwrap();
|
||||
if !senders.contains_key(room_id) {
|
||||
let (room_sender, room_receiver) = broadcast::channel(32);
|
||||
|
||||
senders.insert(room_id.clone(), room_sender);
|
||||
debug!("Create sender for {room_id} room");
|
||||
|
||||
Some(RoomEventsReceiver::new(room_receiver))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
initialized: bool,
|
||||
client: Option<Arc<MatrixClient>>,
|
||||
sync_task: Option<Task>,
|
||||
senders: Senders,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(client: Arc<MatrixClient>, account_events_sender: Sender<AccountEvent>) -> Self {
|
||||
Self {
|
||||
initialized: false,
|
||||
client: Some(client),
|
||||
sync_task: None,
|
||||
senders: Senders::new(account_events_sender),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn create_space(
|
||||
senders: &Ctx<Senders>,
|
||||
room_id: &OwnedRoomId,
|
||||
room: Option<&Room>,
|
||||
) -> anyhow::Result<(), SendError<AccountEvent>> {
|
||||
if let Some(receiver) = senders.add_room(room_id) {
|
||||
let current_span = Span::current();
|
||||
|
||||
let mut name = None;
|
||||
let mut topic = None;
|
||||
if let Some(room) = room {
|
||||
name = room.name();
|
||||
topic = room.topic();
|
||||
}
|
||||
|
||||
let (reply, mut response) = oneshot::<bool>();
|
||||
|
||||
// We can't use Room instance here, because dyn PaginableRoom is not Sync
|
||||
let event = AccountEvent::NewSpace(
|
||||
room_id.clone(),
|
||||
name.clone(),
|
||||
topic.clone(),
|
||||
receiver,
|
||||
reply,
|
||||
current_span.clone(),
|
||||
);
|
||||
|
||||
senders.account_events_sender.send(event)?;
|
||||
|
||||
// We're expecting a response indicating that the client is able to compute the next RoomEvent
|
||||
response.recv().await;
|
||||
|
||||
let events = vec![
|
||||
RoomEvent::NewTopic(topic, current_span.clone()),
|
||||
RoomEvent::NewName(name, current_span),
|
||||
];
|
||||
|
||||
for event in events {
|
||||
if let Err(_err) = senders.send(room_id, event.clone()) {
|
||||
// TODO: Return an error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn create_room(
|
||||
senders: &Ctx<Senders>,
|
||||
room: &Room,
|
||||
) -> anyhow::Result<(), SendError<AccountEvent>> {
|
||||
let room_id = room.room_id().to_owned();
|
||||
|
||||
if let Some(receiver) = senders.add_room(&room_id) {
|
||||
let (reply, mut response) = oneshot::<bool>();
|
||||
|
||||
let is_direct = match room.is_direct().await {
|
||||
Ok(is_direct) => Some(is_direct),
|
||||
Err(err) => {
|
||||
warn!("Unable to know if the {room_id} room is direct: {err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let mut parents = vec![];
|
||||
|
||||
if let Ok(mut spaces) = room.parent_spaces().await {
|
||||
while let Some(parent) = spaces.next().await {
|
||||
match parent {
|
||||
Ok(parent) => match parent {
|
||||
ParentSpace::Reciprocal(parent) => {
|
||||
parents.push(parent.room_id().to_owned());
|
||||
}
|
||||
_ => todo!(),
|
||||
},
|
||||
Err(err) => {
|
||||
error!("{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We can't use Room instance here, because dyn PaginableRoom is not Sync
|
||||
let event = AccountEvent::NewRoom(
|
||||
room_id.clone(),
|
||||
parents.clone(),
|
||||
room.name(),
|
||||
room.topic(),
|
||||
is_direct,
|
||||
room.state(),
|
||||
receiver,
|
||||
reply,
|
||||
Span::current(),
|
||||
);
|
||||
|
||||
senders.account_events_sender.send(event)?;
|
||||
|
||||
// We're expecting a response indicating that the client is able to compute the next RoomEvent
|
||||
response.recv().await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn add_room(
|
||||
senders: &Ctx<Senders>,
|
||||
room: &Room,
|
||||
) -> anyhow::Result<(), SendError<AccountEvent>> {
|
||||
let room_id = room.room_id().to_owned();
|
||||
|
||||
if room.is_space() {
|
||||
Self::create_space(senders, &room_id, Some(room)).await
|
||||
} else {
|
||||
let mut parents = vec![];
|
||||
|
||||
if let Ok(mut spaces) = room.parent_spaces().await {
|
||||
while let Some(parent) = spaces.next().await {
|
||||
match parent {
|
||||
Ok(parent) => match parent {
|
||||
ParentSpace::Reciprocal(parent) => {
|
||||
parents.push(parent.room_id().to_owned());
|
||||
}
|
||||
_ => {
|
||||
warn!(
|
||||
"Only ParentSpace::Reciprocal taken into account, skip {:?}",
|
||||
parent
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!("{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for parent in parents {
|
||||
// Create a minimal space to make the relation consistent... its content will be sync later.
|
||||
if !senders.contains(&parent) {
|
||||
let _ = Self::create_space(senders, &parent, None).await;
|
||||
}
|
||||
|
||||
let event = RoomEvent::NewChild(room_id.clone(), Span::current());
|
||||
if let Err(_err) = senders.send(&parent, event) {
|
||||
// TODO: Return an error
|
||||
}
|
||||
}
|
||||
|
||||
Self::create_room(senders, room).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_stripped_room_create_event(
|
||||
_ev: StrippedRoomCreateEvent,
|
||||
room: Room,
|
||||
senders: Ctx<Senders>,
|
||||
) {
|
||||
let span = debug_span!("Matrix::NewRoom", r = ?room.room_id());
|
||||
|
||||
let _ = Self::add_room(&senders, &room).instrument(span).await;
|
||||
}
|
||||
|
||||
// SyncStateEvent: A possibly-redacted state event without a room_id.
|
||||
async fn on_sync_room_create_event(
|
||||
_ev: SyncStateEvent<RoomCreateEventContent>,
|
||||
room: Room,
|
||||
senders: Ctx<Senders>,
|
||||
) {
|
||||
let span = debug_span!("Matrix::NewRoom", r = ?room.room_id());
|
||||
|
||||
let _ = Self::add_room(&senders, &room).instrument(span).await;
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
fn on_invite_room_member_event(
|
||||
user_id: OwnedUserId,
|
||||
inviter_id: OwnedUserId,
|
||||
room: &Room,
|
||||
matrix_client: &MatrixClient,
|
||||
senders: &Ctx<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(
|
||||
ev: StrippedRoomMemberEvent,
|
||||
matrix_client: MatrixClient,
|
||||
room: Room,
|
||||
senders: Ctx<Senders>,
|
||||
) {
|
||||
match room.state() {
|
||||
RoomState::Invited => {
|
||||
let user_id = &ev.state_key;
|
||||
|
||||
match ev.content.membership {
|
||||
MembershipState::Invite => {
|
||||
let span = debug_span!("Matrix::RoomInvitation", r = ?room.room_id());
|
||||
|
||||
span.in_scope(|| {
|
||||
Self::on_invite_room_member_event(
|
||||
user_id.clone(),
|
||||
ev.sender,
|
||||
&room,
|
||||
&matrix_client,
|
||||
&senders,
|
||||
)
|
||||
});
|
||||
}
|
||||
MembershipState::Join => {
|
||||
let span =
|
||||
debug_span!("Matrix::RoomJoin", r = ?room.room_id(), u = ?user_id)
|
||||
.entered();
|
||||
|
||||
span.in_scope(|| {
|
||||
Self::on_join_room_member_event(
|
||||
ev.sender,
|
||||
ev.content.displayname,
|
||||
ev.content.avatar_url,
|
||||
&room,
|
||||
&matrix_client,
|
||||
&senders,
|
||||
)
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
error!("TODO: {:?}", ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
error!("TODO: {:?}", ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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>,
|
||||
room: Room,
|
||||
senders: Ctx<Senders>,
|
||||
) {
|
||||
if let SyncStateEvent::Original(ev) = ev {
|
||||
let span = debug_span!("Matrix::RoomTopic", r = ?room.room_id());
|
||||
|
||||
span.in_scope(|| {
|
||||
Self::on_room_topic_event(Some(ev.content.topic), &room, &senders);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn spawn(homeserver_url: String) -> (Requester, Receiver<AccountEvent>) {
|
||||
let matrix_client = Arc::new(
|
||||
MatrixClient::builder()
|
||||
.homeserver_url(&homeserver_url)
|
||||
.build()
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let (worker_tasks_sender, worker_tasks_receiver) = unbounded_channel::<WorkerTask>();
|
||||
let (account_events_sender, account_events_receiver) =
|
||||
broadcast::channel::<AccountEvent>(32);
|
||||
|
||||
let mut client = Client::new(matrix_client, account_events_sender);
|
||||
|
||||
dioxus::prelude::spawn(async move {
|
||||
client.work(worker_tasks_receiver).await;
|
||||
});
|
||||
|
||||
(Requester::new(worker_tasks_sender), account_events_receiver)
|
||||
}
|
||||
|
||||
fn init(&mut self) {
|
||||
if let Some(client) = self.client.borrow() {
|
||||
// TODO: Remove clone?
|
||||
client.add_event_handler_context(self.senders.clone());
|
||||
|
||||
let _ = client.add_event_handler(Client::on_stripped_room_create_event);
|
||||
let _ = client.add_event_handler(Client::on_sync_room_create_event);
|
||||
|
||||
let _ = client.add_event_handler(Client::on_stripped_room_member_event);
|
||||
let _ = client.add_event_handler(Client::on_sync_room_member_event);
|
||||
|
||||
let _ = client.add_event_handler(Client::on_stripped_room_avatar_event);
|
||||
let _ = client.add_event_handler(Client::on_sync_room_avatar_event);
|
||||
|
||||
let _ = client.add_event_handler(Client::on_stripped_room_name_event);
|
||||
let _ = client.add_event_handler(Client::on_sync_room_name_event);
|
||||
|
||||
let _ = client.add_event_handler(Client::on_stripped_room_topic_event);
|
||||
let _ = client.add_event_handler(Client::on_sync_room_topic_event);
|
||||
|
||||
self.initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
async fn login(&mut self, style: LoginStyle) -> anyhow::Result<()> {
|
||||
let client = self.client.as_ref().unwrap();
|
||||
|
||||
match style {
|
||||
LoginStyle::Password(username, password) => {
|
||||
client
|
||||
.matrix_auth()
|
||||
.login_username(&username, &password)
|
||||
.initial_device_display_name("TODO")
|
||||
.send()
|
||||
.await
|
||||
.map_err(ClientError::from)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_forever(&mut self) {
|
||||
let client = self.client.clone().unwrap();
|
||||
|
||||
let task = dioxus::prelude::spawn(async move {
|
||||
// Sync once so we receive the client state and old messages
|
||||
let sync_token_option = match client.sync_once(SyncSettings::default()).await {
|
||||
Ok(sync_response) => Some(sync_response.next_batch),
|
||||
Err(err) => {
|
||||
error!("Error during sync one: {}", err);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(sync_token) = sync_token_option {
|
||||
debug!("User connected to the homeserver, start syncing");
|
||||
|
||||
let settings = SyncSettings::default().token(sync_token);
|
||||
let _ = client.sync(settings).await;
|
||||
}
|
||||
});
|
||||
self.sync_task = Some(task);
|
||||
}
|
||||
|
||||
async fn get_display_name(&mut self) -> anyhow::Result<Option<String>> {
|
||||
let client = self.client.as_ref().unwrap();
|
||||
|
||||
match client.account().get_display_name().await {
|
||||
Ok(display_name) => Ok(display_name),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_avatar(&mut self) -> anyhow::Result<Option<Vec<u8>>> {
|
||||
let client = self.client.as_ref().unwrap();
|
||||
|
||||
match client
|
||||
.account()
|
||||
.get_avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
|
||||
size: MediaThumbnailSize {
|
||||
method: Method::Scale,
|
||||
width: uint!(256),
|
||||
height: uint!(256),
|
||||
},
|
||||
animated: false,
|
||||
}))
|
||||
.await
|
||||
{
|
||||
Ok(avatar) => Ok(avatar),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_room_avatar(&mut self, room_id: &OwnedRoomId) -> anyhow::Result<Option<Vec<u8>>> {
|
||||
let client = self.client.as_ref().unwrap();
|
||||
|
||||
if let Some(room) = client.get_room(room_id) {
|
||||
match room
|
||||
.avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
|
||||
size: MediaThumbnailSize {
|
||||
method: Method::Scale,
|
||||
width: uint!(256),
|
||||
height: uint!(256),
|
||||
},
|
||||
animated: false,
|
||||
}))
|
||||
.await
|
||||
{
|
||||
Ok(avatar) => Ok(avatar),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
} else {
|
||||
warn!("No room found with the \"{}\" id", room_id.as_str());
|
||||
// TODO: Return an error if the room has not been found
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 media = client.media();
|
||||
|
||||
let request = MediaRequest {
|
||||
source: MediaSource::Plain(media_url),
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSettings {
|
||||
size: MediaThumbnailSize {
|
||||
method: Method::Scale,
|
||||
width: uint!(256),
|
||||
height: uint!(256),
|
||||
},
|
||||
animated: false,
|
||||
}),
|
||||
};
|
||||
|
||||
let res = media.get_media_content(&request, true).await;
|
||||
|
||||
Ok(res?)
|
||||
}
|
||||
|
||||
async fn get_room_member_avatar(
|
||||
&self,
|
||||
avatar_url: &Option<OwnedMxcUri>,
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
) -> anyhow::Result<Option<Vec<u8>>> {
|
||||
let client = self.client.as_ref().unwrap();
|
||||
|
||||
if let Some(room) = client.get_room(room_id) {
|
||||
match avatar_url {
|
||||
Some(avatar_url) => {
|
||||
let thumbnail = self.get_thumbnail(avatar_url.clone()).await;
|
||||
return Ok(Some(thumbnail?));
|
||||
}
|
||||
None => match room.get_member(user_id).await {
|
||||
Ok(room_member) => {
|
||||
if let Some(room_member) = room_member {
|
||||
let res = match room_member
|
||||
.avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
|
||||
size: MediaThumbnailSize {
|
||||
method: Method::Scale,
|
||||
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)
|
||||
}
|
||||
|
||||
async fn join_room(&self, room_id: &RoomId) -> anyhow::Result<bool> {
|
||||
let client = self.client.as_ref().unwrap();
|
||||
|
||||
if let Some(room) = client.get_room(room_id) {
|
||||
return match room.join().await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(err) => Err(err.into()),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn work(&mut self, mut rx: UnboundedReceiver<WorkerTask>) {
|
||||
while let Some(task) = rx.recv().await {
|
||||
self.run(task).await;
|
||||
}
|
||||
|
||||
if let Some(task) = self.sync_task.take() {
|
||||
task.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(&mut self, task: WorkerTask) {
|
||||
match task {
|
||||
WorkerTask::Init(reply) => {
|
||||
self.init();
|
||||
reply.send(Ok(())).await;
|
||||
}
|
||||
WorkerTask::RunForever(reply) => {
|
||||
{
|
||||
self.run_forever().await;
|
||||
reply.send(())
|
||||
}
|
||||
.await
|
||||
}
|
||||
WorkerTask::Login(style, reply) => {
|
||||
reply.send(self.login(style).await).await;
|
||||
}
|
||||
WorkerTask::GetDisplayName(reply) => {
|
||||
reply.send(self.get_display_name().await).await;
|
||||
}
|
||||
WorkerTask::GetAvatar(reply) => {
|
||||
reply.send(self.get_avatar().await).await;
|
||||
}
|
||||
|
||||
WorkerTask::GetRoomAvatar(id, reply) => {
|
||||
reply.send(self.get_room_avatar(&id).await).await;
|
||||
}
|
||||
WorkerTask::GetRoomMemberAvatar(avatar_url, room_id, user_id, reply) => {
|
||||
reply
|
||||
.send(
|
||||
self.get_room_member_avatar(&avatar_url, &room_id, &user_id)
|
||||
.await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
WorkerTask::JoinRoom(id, reply) => {
|
||||
reply.send(self.join_room(&id).await).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
src/infrastructure/messaging/matrix/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub(crate) mod account_event;
|
||||
pub(crate) mod client;
|
||||
pub(crate) mod requester;
|
||||
pub(crate) mod room_event;
|
||||
pub(crate) mod worker_tasks;
|
390
src/infrastructure/messaging/matrix/requester.rs
Normal file
@@ -0,0 +1,390 @@
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId};
|
||||
use tokio::{
|
||||
select,
|
||||
sync::{broadcast::Receiver, mpsc::UnboundedSender},
|
||||
};
|
||||
use tokio_stream::{wrappers::BroadcastStream, StreamExt, StreamMap};
|
||||
use tracing::{error, instrument, Instrument};
|
||||
|
||||
use super::{
|
||||
account_event::AccountEvent,
|
||||
room_event::RoomEvent,
|
||||
worker_tasks::{LoginStyle, WorkerTask},
|
||||
};
|
||||
use crate::{
|
||||
domain::model::{
|
||||
common::{Avatar, UserId},
|
||||
messaging_interface::{
|
||||
AccountMessagingConsumerInterface, AccountMessagingProviderInterface,
|
||||
MemberMessagingProviderInterface, RoomMessagingConsumerInterface,
|
||||
RoomMessagingProviderInterface, SpaceMessagingConsumerInterface,
|
||||
SpaceMessagingProviderInterface,
|
||||
},
|
||||
room::{Invitation, Room, RoomId},
|
||||
room_member::{AvatarUrl, RoomMember},
|
||||
space::Space,
|
||||
},
|
||||
utils::oneshot,
|
||||
};
|
||||
|
||||
pub struct Requester {
|
||||
worker_tasks_sender: UnboundedSender<WorkerTask>,
|
||||
}
|
||||
|
||||
impl Clone for Requester {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
worker_tasks_sender: self.worker_tasks_sender.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Requester {
|
||||
pub fn new(worker_tasks_sender: UnboundedSender<WorkerTask>) -> Self {
|
||||
Self {
|
||||
worker_tasks_sender,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Is there a way to avoid this duplication?
|
||||
macro_rules! request_to_worker {
|
||||
($self:ident, $task:expr) => {
|
||||
{
|
||||
let (reply, mut response) = oneshot();
|
||||
|
||||
let task = $task(reply);
|
||||
|
||||
if let Err(err) = $self.worker_tasks_sender.send(task) {
|
||||
let msg = format!("Unable to request to the Matrix client: {err}");
|
||||
return Err(anyhow::Error::msg(msg));
|
||||
}
|
||||
|
||||
match response.recv().await {
|
||||
Some(result) => result,
|
||||
None => Err(anyhow::Error::msg("TBD")),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($self:ident, $task:expr $(, $arg:expr)+) => {
|
||||
{
|
||||
let (reply, mut response) = oneshot();
|
||||
|
||||
let task = $task($($arg),*, reply);
|
||||
|
||||
if let Err(err) = $self.worker_tasks_sender.send(task) {
|
||||
let msg = format!("Unable to request to the Matrix client: {err}");
|
||||
return Err(anyhow::Error::msg(msg));
|
||||
}
|
||||
|
||||
match response.recv().await {
|
||||
Some(result) => result,
|
||||
None => Err(anyhow::Error::msg("TBD")),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Requester {
|
||||
pub async fn init(&self) -> anyhow::Result<()> {
|
||||
request_to_worker!(self, WorkerTask::Init)
|
||||
}
|
||||
|
||||
pub async fn login(&self, style: LoginStyle) -> anyhow::Result<()> {
|
||||
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)]
|
||||
impl AccountMessagingProviderInterface for Requester {
|
||||
async fn get_display_name(&self) -> anyhow::Result<Option<String>> {
|
||||
request_to_worker!(self, WorkerTask::GetDisplayName)
|
||||
}
|
||||
|
||||
async fn get_avatar(&self) -> anyhow::Result<Option<Avatar>> {
|
||||
request_to_worker!(self, WorkerTask::GetAvatar)
|
||||
}
|
||||
|
||||
async fn run_forever(
|
||||
&self,
|
||||
account_events_consumer: &dyn AccountMessagingConsumerInterface,
|
||||
mut account_events_receiver: Receiver<AccountEvent>,
|
||||
) -> anyhow::Result<()> {
|
||||
// TODO: manage the result provided by response
|
||||
let (run_forever_tx, _run_forever_rx) = oneshot();
|
||||
|
||||
if let Err(err) = self
|
||||
.worker_tasks_sender
|
||||
.send(WorkerTask::RunForever(run_forever_tx))
|
||||
{
|
||||
let msg = format!("Unable to request login to the Matrix client: {err}");
|
||||
return Err(anyhow::Error::msg(msg));
|
||||
}
|
||||
|
||||
let mut rooms_events_streams = StreamMap::new();
|
||||
let mut spaces_events_streams = StreamMap::new();
|
||||
|
||||
let mut room_events_consumers =
|
||||
HashMap::<RoomId, Rc<dyn RoomMessagingConsumerInterface>>::new();
|
||||
let mut space_events_consumers =
|
||||
HashMap::<RoomId, Rc<dyn SpaceMessagingConsumerInterface>>::new();
|
||||
|
||||
// TODO: Fix this...
|
||||
let client = Rc::new(self.clone());
|
||||
|
||||
loop {
|
||||
select! {
|
||||
res = account_events_receiver.recv() => {
|
||||
if let Ok(account_event) = res {
|
||||
match account_event {
|
||||
AccountEvent::NewRoom(
|
||||
id,
|
||||
spaces,
|
||||
name,
|
||||
topic,
|
||||
is_direct,
|
||||
state,
|
||||
receiver,
|
||||
new_room_tx,
|
||||
span
|
||||
) => {
|
||||
let mut room = Room::new(id, name, topic, is_direct, Some(state), spaces);
|
||||
let room_id = room.id().clone();
|
||||
|
||||
room.set_messaging_provider(client.clone());
|
||||
|
||||
let room = Rc::new(room);
|
||||
|
||||
let stream = BroadcastStream::new(receiver.into());
|
||||
rooms_events_streams.insert(room_id.clone(), stream);
|
||||
|
||||
let room_events_consumer = account_events_consumer.on_new_room(room)
|
||||
.instrument(span)
|
||||
.await;
|
||||
room_events_consumers.insert(room_id, room_events_consumer);
|
||||
|
||||
// We're now ready to recv and compute RoomEvent.
|
||||
new_room_tx.send(true).await;
|
||||
},
|
||||
AccountEvent::NewSpace(id, name, topic, receiver, new_space_tx, span) => {
|
||||
let mut space = Space::new(id, name, topic);
|
||||
let space_id = space.id().clone();
|
||||
|
||||
space.set_messaging_provider(client.clone());
|
||||
|
||||
let space = Rc::new(space);
|
||||
|
||||
let stream = BroadcastStream::new(receiver.into());
|
||||
spaces_events_streams.insert(space_id.clone(), stream);
|
||||
|
||||
let space_events_consumer = account_events_consumer.on_new_space(space)
|
||||
.instrument(span)
|
||||
.await;
|
||||
space_events_consumers.insert(space_id, space_events_consumer);
|
||||
|
||||
// We're now ready to recv and compute SpaceEvent.
|
||||
new_space_tx.send(true).await;
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
Some((room_id, room_event)) = rooms_events_streams.next() => {
|
||||
if let Ok(room_event) = room_event {
|
||||
if let Some(consumer) = room_events_consumers.get(&room_id) {
|
||||
match room_event {
|
||||
RoomEvent::Invitation(user_id, sender_id, is_account_user, span) => {
|
||||
Self::on_room_invitation(consumer, user_id, sender_id, is_account_user)
|
||||
.instrument(span)
|
||||
.await;
|
||||
},
|
||||
RoomEvent::Join(user_id, user_name, avatar_url, is_account_user, span) => {
|
||||
Self::on_room_join(
|
||||
consumer,
|
||||
room_id,
|
||||
user_id,
|
||||
user_name,
|
||||
avatar_url,
|
||||
is_account_user,
|
||||
client.clone())
|
||||
.instrument(span)
|
||||
.await;
|
||||
},
|
||||
RoomEvent::NewTopic(topic, span) => {
|
||||
Self::on_room_new_topic(consumer, topic)
|
||||
.instrument(span)
|
||||
.await;
|
||||
},
|
||||
RoomEvent::NewName(name, span) => {
|
||||
Self::on_room_new_name(consumer, name)
|
||||
.instrument(span)
|
||||
.await;
|
||||
},
|
||||
RoomEvent::NewAvatar(avatar, span) => {
|
||||
Self::on_room_new_avatar(consumer, avatar)
|
||||
.instrument(span)
|
||||
.await;
|
||||
}
|
||||
// RoomEvent::NewAvatar(avatar) => Self::on_room_new_avatar(consumer, avatar).await,
|
||||
_ => error!("TODO: {:?}", &room_event),
|
||||
}
|
||||
} else {
|
||||
error!("No consumer found for {} room", &room_id);
|
||||
}
|
||||
}
|
||||
},
|
||||
Some((space_id, room_event)) = spaces_events_streams.next() => {
|
||||
if let Ok(room_event) = room_event {
|
||||
if let Some(consumer) = space_events_consumers.get(&space_id) {
|
||||
match room_event {
|
||||
RoomEvent::NewTopic(topic, span) => {
|
||||
Self::on_space_new_topic(consumer, topic)
|
||||
.instrument(span)
|
||||
.await;
|
||||
},
|
||||
RoomEvent::NewName(name, span) => {
|
||||
Self::on_space_new_name(consumer, name)
|
||||
.instrument(span)
|
||||
.await;
|
||||
},
|
||||
RoomEvent::NewChild(child_id, span) => {
|
||||
Self::on_space_new_child(consumer, child_id)
|
||||
.instrument(span)
|
||||
.await;
|
||||
},
|
||||
_ => error!("TODO: {:?}", &room_event),
|
||||
}
|
||||
} else {
|
||||
error!("No consumer found for {} space", &space_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl RoomMessagingProviderInterface for Requester {
|
||||
async fn get_avatar(&self, room_id: &RoomId) -> anyhow::Result<Option<Avatar>> {
|
||||
request_to_worker!(self, WorkerTask::GetRoomAvatar, room_id.clone())
|
||||
}
|
||||
|
||||
async fn join(&self, room_id: &RoomId) -> anyhow::Result<bool> {
|
||||
request_to_worker!(self, WorkerTask::JoinRoom, room_id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl SpaceMessagingProviderInterface for Requester {}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl MemberMessagingProviderInterface for Requester {
|
||||
async fn get_avatar(
|
||||
&self,
|
||||
avatar_url: Option<AvatarUrl>,
|
||||
room_id: RoomId,
|
||||
user_id: UserId,
|
||||
) -> anyhow::Result<Option<Avatar>> {
|
||||
request_to_worker!(
|
||||
self,
|
||||
WorkerTask::GetRoomMemberAvatar,
|
||||
avatar_url,
|
||||
room_id,
|
||||
user_id
|
||||
)
|
||||
}
|
||||
}
|
71
src/infrastructure/messaging/matrix/room_event.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId};
|
||||
use tokio::sync::broadcast::Receiver;
|
||||
use tracing::Span;
|
||||
|
||||
use crate::domain::model::common::Avatar;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum RoomEvent {
|
||||
Invitation(OwnedUserId, OwnedUserId, bool, Span),
|
||||
Join(OwnedUserId, Option<String>, Option<OwnedMxcUri>, bool, Span),
|
||||
|
||||
NewTopic(Option<String>, Span),
|
||||
NewName(Option<String>, Span),
|
||||
NewAvatar(Option<Avatar>, Span),
|
||||
NewChild(OwnedRoomId, Span),
|
||||
}
|
||||
|
||||
impl Debug for RoomEvent {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
Self::Invitation(invitee_id, sender_id, is_account_user, _span) => f
|
||||
.debug_tuple("RoomEvent::Invitation")
|
||||
.field(invitee_id)
|
||||
.field(sender_id)
|
||||
.field(is_account_user)
|
||||
.finish(),
|
||||
Self::Join(user_id, user_name, avatar_url, is_account_user, _span) => f
|
||||
.debug_tuple("RoomEvent::Join")
|
||||
.field(user_id)
|
||||
.field(user_name)
|
||||
.field(avatar_url)
|
||||
.field(is_account_user)
|
||||
.finish(),
|
||||
Self::NewTopic(topic, _span) => {
|
||||
f.debug_tuple("RoomEvent::NewTopic").field(topic).finish()
|
||||
}
|
||||
Self::NewName(name, _span) => f.debug_tuple("RoomEvent::NewName").field(name).finish(),
|
||||
Self::NewAvatar(avatar, _span) => f
|
||||
// Self::NewAvatar(avatar) => f
|
||||
.debug_tuple("RoomEvent::NewAvatar")
|
||||
.field(&format!("is_some: {}", &avatar.is_some()))
|
||||
.finish(),
|
||||
Self::NewChild(room_id, _span) => f
|
||||
.debug_tuple("SpaceEvent::NewChild")
|
||||
.field(room_id)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RoomEventsReceiver(Receiver<RoomEvent>);
|
||||
|
||||
impl Clone for RoomEventsReceiver {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.resubscribe())
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomEventsReceiver {
|
||||
pub fn new(inner: Receiver<RoomEvent>) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomEventsReceiver> for Receiver<RoomEvent> {
|
||||
fn from(val: RoomEventsReceiver) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
71
src/infrastructure/messaging/matrix/worker_tasks.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId};
|
||||
|
||||
use crate::utils::Sender;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoginStyle {
|
||||
Password(String, String),
|
||||
}
|
||||
|
||||
pub enum WorkerTask {
|
||||
Init(Sender<anyhow::Result<()>>),
|
||||
Login(LoginStyle, Sender<anyhow::Result<()>>),
|
||||
RunForever(Sender<()>),
|
||||
GetDisplayName(Sender<anyhow::Result<Option<String>>>),
|
||||
GetAvatar(Sender<anyhow::Result<Option<Vec<u8>>>>),
|
||||
|
||||
GetRoomAvatar(OwnedRoomId, Sender<anyhow::Result<Option<Vec<u8>>>>),
|
||||
GetRoomMemberAvatar(
|
||||
Option<OwnedMxcUri>,
|
||||
OwnedRoomId,
|
||||
OwnedUserId,
|
||||
Sender<anyhow::Result<Option<Vec<u8>>>>,
|
||||
),
|
||||
JoinRoom(OwnedRoomId, Sender<anyhow::Result<bool>>),
|
||||
}
|
||||
|
||||
impl Debug for WorkerTask {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
WorkerTask::Init(_) => f
|
||||
.debug_tuple("WorkerTask::Init")
|
||||
.field(&format_args!("_"))
|
||||
// .field(&format_args!("_"))
|
||||
.finish(),
|
||||
WorkerTask::RunForever(_) => f
|
||||
.debug_tuple("WorkerTask::RunForever")
|
||||
.field(&format_args!("_"))
|
||||
.finish(),
|
||||
WorkerTask::Login(style, _) => f
|
||||
.debug_tuple("WorkerTask::Login")
|
||||
.field(style)
|
||||
// .field(&format_args!("_"))
|
||||
.finish(),
|
||||
WorkerTask::GetDisplayName(_) => f
|
||||
.debug_tuple("WorkerTask::GetDisplayName")
|
||||
.field(&format_args!("_"))
|
||||
.finish(),
|
||||
WorkerTask::GetAvatar(_) => f
|
||||
.debug_tuple("WorkerTask::GetAvatar")
|
||||
.field(&format_args!("_"))
|
||||
.finish(),
|
||||
|
||||
WorkerTask::GetRoomAvatar(id, _) => f
|
||||
.debug_tuple("WorkerTask::GetRoomAvatar")
|
||||
.field(id)
|
||||
.finish(),
|
||||
WorkerTask::GetRoomMemberAvatar(room_id, user_id, avatar_url, _) => f
|
||||
.debug_tuple("WorkerTask::GetRoomMemberAvatar")
|
||||
.field(avatar_url)
|
||||
.field(room_id)
|
||||
.field(user_id)
|
||||
.finish(),
|
||||
WorkerTask::JoinRoom(room_id, _) => f
|
||||
.debug_tuple("WorkerTask::JoinRoom")
|
||||
.field(room_id)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
1
src/infrastructure/messaging/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub(crate) mod matrix;
|
2
src/infrastructure/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod messaging;
|
||||
pub(crate) mod services;
|
2
src/infrastructure/services/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod mozaik_builder;
|
||||
pub(crate) mod random_svg_generators;
|
115
src/infrastructure/services/mozaik_builder.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use image::imageops::FilterType;
|
||||
use image::{DynamicImage, ImageFormat, ImageReader};
|
||||
use image::{GenericImage, RgbImage};
|
||||
use tracing::{error, warn};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(not(target_family = "wasm"))] {
|
||||
use tokio::task::spawn_blocking;
|
||||
}
|
||||
}
|
||||
|
||||
fn from_raw_to_image(raw: &Vec<u8>) -> Option<DynamicImage> {
|
||||
match ImageReader::new(Cursor::new(raw)).with_guessed_format() {
|
||||
Ok(reader) => match reader.decode() {
|
||||
Ok(image) => return Some(image),
|
||||
Err(err) => error!("Unable to decode the image: {}", err),
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Unable to read the image: {}", err)
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn create_mozaik_(
|
||||
width_px: u32,
|
||||
height_px: u32,
|
||||
images: &[Vec<u8>],
|
||||
padding_image: &Option<Vec<u8>>,
|
||||
) -> Vec<u8> {
|
||||
let placeholder = DynamicImage::new_rgb8(128, 128);
|
||||
|
||||
let images: Vec<Option<DynamicImage>> = images.iter().map(from_raw_to_image).collect();
|
||||
let padding_image = if let Some(padding_image) = padding_image {
|
||||
from_raw_to_image(padding_image)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
|
||||
let mut allocations: Vec<&Option<DynamicImage>> = vec![];
|
||||
let mut images_per_row = 1;
|
||||
let mut images_per_col = 1;
|
||||
|
||||
match images.len() {
|
||||
0 => {
|
||||
allocations.push(&padding_image);
|
||||
}
|
||||
1 => {
|
||||
allocations.push(&images[0]);
|
||||
}
|
||||
2 => {
|
||||
allocations.extend_from_slice(&[&images[0], &images[1], &images[1], &images[0]]);
|
||||
images_per_row = 2;
|
||||
images_per_col = 2;
|
||||
}
|
||||
_ => {
|
||||
// TODO: Manage other cases
|
||||
warn!("For now, we only manage the rendering of mozaic with less than 3 images");
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
let image_width_px = width_px / images_per_row;
|
||||
let image_height_px = height_px / images_per_col;
|
||||
|
||||
let mut output = RgbImage::new(width_px, height_px);
|
||||
|
||||
let mut row_pos = 0;
|
||||
for (index, image) in allocations.iter().enumerate() {
|
||||
if index > 0 && index % images_per_row as usize == 0 {
|
||||
row_pos += 1;
|
||||
}
|
||||
|
||||
let col_pos = index - (images_per_row as usize * row_pos);
|
||||
|
||||
let image = *image;
|
||||
|
||||
let scaled = image
|
||||
.as_ref()
|
||||
.unwrap_or(&placeholder)
|
||||
.resize_to_fill(image_width_px, image_height_px, FilterType::Nearest)
|
||||
.into_rgb8();
|
||||
|
||||
let output_image_pos_x = col_pos as u32 * image_width_px;
|
||||
let output_image_pos_y = row_pos as u32 * image_height_px;
|
||||
|
||||
let _ = output.copy_from(&scaled, output_image_pos_x, output_image_pos_y);
|
||||
}
|
||||
|
||||
let _ = output.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Jpeg);
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub async fn create_mozaik(
|
||||
width_px: u32,
|
||||
height_px: u32,
|
||||
images: Vec<Vec<u8>>,
|
||||
padding_image: Option<Vec<u8>>,
|
||||
) -> Vec<u8> {
|
||||
cfg_if! {
|
||||
if #[cfg(target_family = "wasm")] {
|
||||
create_mozaik_(width_px, height_px, &images, &padding_image)
|
||||
}
|
||||
else {
|
||||
spawn_blocking(move || {
|
||||
create_mozaik_(width_px, height_px, &images, &padding_image)
|
||||
}).await.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
268
src/infrastructure/services/random_svg_generators.rs
Normal file
@@ -0,0 +1,268 @@
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::sync::OnceLock;
|
||||
use std::{collections::HashMap, future::IntoFuture};
|
||||
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use reqwest::Result as RequestResult;
|
||||
use tracing::error;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(target_family = "wasm")] {
|
||||
use web_sys;
|
||||
} else {
|
||||
use tokio::fs::read_to_string;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, Hash, PartialEq)]
|
||||
pub enum AvatarFeeling {
|
||||
Ok,
|
||||
Warning,
|
||||
Alerting,
|
||||
}
|
||||
impl fmt::Display for AvatarFeeling {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let repr = match self {
|
||||
Self::Ok => "Ok",
|
||||
Self::Warning => "Warning",
|
||||
Self::Alerting => "Alerting",
|
||||
};
|
||||
write!(f, "{repr}")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AvatarConfig<'a> {
|
||||
feeling: AvatarFeeling,
|
||||
background_color: &'a str,
|
||||
}
|
||||
impl<'a> AvatarConfig<'a> {
|
||||
pub fn new(feeling: AvatarFeeling, background_color: &'a str) -> Self {
|
||||
Self {
|
||||
feeling,
|
||||
background_color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DicebearType {
|
||||
Notionists,
|
||||
Shapes,
|
||||
}
|
||||
impl fmt::Display for DicebearType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let repr = match self {
|
||||
Self::Notionists => "notionists",
|
||||
Self::Shapes => "shapes",
|
||||
};
|
||||
write!(f, "{repr}")
|
||||
}
|
||||
}
|
||||
|
||||
struct DicebearConfig<'a> {
|
||||
gesture: &'a str,
|
||||
browns: Vec<u32>,
|
||||
eyes: Vec<u32>,
|
||||
lips: Vec<u32>,
|
||||
}
|
||||
|
||||
fn avatar_variants() -> &'static HashMap<AvatarFeeling, DicebearConfig<'static>> {
|
||||
static VARIANTS: OnceLock<HashMap<AvatarFeeling, DicebearConfig>> = OnceLock::new();
|
||||
VARIANTS.get_or_init(|| {
|
||||
let mut variants = HashMap::new();
|
||||
variants.insert(
|
||||
AvatarFeeling::Alerting,
|
||||
DicebearConfig {
|
||||
gesture: "wavePointLongArms",
|
||||
browns: vec![2, 6, 11, 13],
|
||||
eyes: vec![2, 4],
|
||||
lips: vec![1, 2, 7, 11, 19, 20, 24, 27],
|
||||
},
|
||||
);
|
||||
variants.insert(
|
||||
AvatarFeeling::Warning,
|
||||
DicebearConfig {
|
||||
gesture: "pointLongArm",
|
||||
browns: vec![2, 5, 10, 13],
|
||||
eyes: vec![1, 3],
|
||||
lips: vec![1, 2, 4, 8, 10, 13, 18, 21, 29],
|
||||
},
|
||||
);
|
||||
variants.insert(
|
||||
AvatarFeeling::Ok,
|
||||
DicebearConfig {
|
||||
gesture: "okLongArm",
|
||||
browns: vec![1, 3, 4, 7, 8, 9, 12],
|
||||
eyes: vec![5],
|
||||
lips: vec![3, 5, 9, 14, 17, 22, 23, 25, 30],
|
||||
},
|
||||
);
|
||||
variants
|
||||
})
|
||||
}
|
||||
|
||||
fn render_dicebear_variants(values: &[u32]) -> String {
|
||||
values
|
||||
.iter()
|
||||
.map(|l| format!("variant{:02}", l))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
}
|
||||
|
||||
async fn fetch_text(req: String) -> RequestResult<String> {
|
||||
match reqwest::get(req).await?.error_for_status() {
|
||||
Ok(res) => res.text().await,
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_dicebear_svg(
|
||||
r#type: &DicebearType,
|
||||
req_fields: &[String],
|
||||
placeholder_fetcher: Option<Box<impl Future<Output = Option<String>>>>,
|
||||
) -> String {
|
||||
// TODO: Use configuration file
|
||||
let url = "dicebear.tools.adrien.run";
|
||||
|
||||
let seed = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
|
||||
let type_str = r#type.to_string();
|
||||
let url = format!(
|
||||
"https://{url}/8.x/{type_str}/svg?seed={seed}&randomizeIds=true{}{}",
|
||||
if !req_fields.is_empty() { "&" } else { " " },
|
||||
req_fields.join("&")
|
||||
);
|
||||
|
||||
let text = match fetch_text(url).await {
|
||||
Ok(text) => Some(text),
|
||||
Err(err) => {
|
||||
error!("Error during placeholder loading: {}", err);
|
||||
if let Some(placeholder_fetcher) = placeholder_fetcher {
|
||||
placeholder_fetcher.into_future().await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
text.unwrap_or("".to_string())
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(target_family = "wasm")] {
|
||||
fn gen_placeholder_fetcher<'a>(path: &'static str) -> Box<impl Future<Output = Option<String>>> {
|
||||
Box::new(async move {
|
||||
let url = format!("{}{}", web_sys::window().unwrap().origin(), path);
|
||||
match fetch_text(url).await {
|
||||
Ok(content) => Some(content),
|
||||
Err(err) => {
|
||||
error!("Error during {path} fetching: {}", err.to_string());
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
fn gen_placeholder_fetcher(path: &'static str) -> Box<impl Future<Output = Option<String>>> {
|
||||
let path = format!("./public/{}", &path);
|
||||
Box::new(async move {
|
||||
match read_to_string(&path).await {
|
||||
Ok(content) => Some(content),
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Error during the access to the {path} file: {}",
|
||||
err.to_string()
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_random_svg_avatar<'a>(config: Option<&'a AvatarConfig<'a>>) -> String {
|
||||
let (variant, feeling) = match config {
|
||||
Some(config) => (avatar_variants().get(&config.feeling), &config.feeling),
|
||||
None => (None, &AvatarFeeling::Alerting),
|
||||
};
|
||||
|
||||
let mut req_fields = Vec::<String>::new();
|
||||
|
||||
if let Some(config) = config {
|
||||
req_fields.push(format!("backgroundColor={}", config.background_color));
|
||||
}
|
||||
|
||||
if let Some(variant) = variant {
|
||||
req_fields.push(format!(
|
||||
"gestureProbability=100&gesture={}",
|
||||
&variant.gesture
|
||||
));
|
||||
req_fields.push(format!(
|
||||
"&browsProbability=100&brows={}",
|
||||
render_dicebear_variants(&variant.browns)
|
||||
));
|
||||
req_fields.push(format!(
|
||||
"&eyesProbability=100&eyes={}",
|
||||
render_dicebear_variants(&variant.eyes)
|
||||
));
|
||||
req_fields.push(format!(
|
||||
"&lipsProbability=100&lips={}",
|
||||
render_dicebear_variants(&variant.lips)
|
||||
));
|
||||
}
|
||||
|
||||
let placeholder_path = match feeling {
|
||||
AvatarFeeling::Ok => "/images/modal-default-ok-icon.svg",
|
||||
AvatarFeeling::Warning => "/images/modal-default-warning-icon.svg",
|
||||
AvatarFeeling::Alerting => "/images/modal-default-critical-icon.svg",
|
||||
};
|
||||
|
||||
fetch_dicebear_svg(
|
||||
&DicebearType::Notionists,
|
||||
&req_fields,
|
||||
Some(gen_placeholder_fetcher(placeholder_path)),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub struct ShapeConfig<'a> {
|
||||
background_color: &'a str,
|
||||
shape_1_color: &'a str,
|
||||
shape_2_color: &'a str,
|
||||
shape_3_color: &'a str,
|
||||
}
|
||||
impl<'a> ShapeConfig<'a> {
|
||||
pub fn new(
|
||||
background_color: &'a str,
|
||||
shape_1_color: &'a str,
|
||||
shape_2_color: &'a str,
|
||||
shape_3_color: &'a str,
|
||||
) -> Self {
|
||||
Self {
|
||||
background_color,
|
||||
shape_1_color,
|
||||
shape_2_color,
|
||||
shape_3_color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_random_svg_shape<'a>(config: Option<&'a ShapeConfig<'a>>) -> String {
|
||||
let mut req_fields = Vec::<String>::new();
|
||||
|
||||
if let Some(config) = config {
|
||||
req_fields.push(format!("backgroundColor={}", config.background_color));
|
||||
req_fields.push(format!("shape1Color={}", config.shape_1_color));
|
||||
req_fields.push(format!("shape2Color={}", config.shape_2_color));
|
||||
req_fields.push(format!("shape3Color={}", config.shape_3_color));
|
||||
}
|
||||
|
||||
let placeholder_path = "/images/login-profile-placeholder.svg";
|
||||
fetch_dicebear_svg(
|
||||
&DicebearType::Shapes,
|
||||
&req_fields,
|
||||
Some(gen_placeholder_fetcher(placeholder_path)),
|
||||
)
|
||||
.await
|
||||
}
|