146 Commits

Author SHA1 Message Date
8dfc4f2694 Update Rust crate validator to 0.20.0
Some checks failed
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/audit unknown status
2025-07-19 10:59:04 +00:00
b2da9b5dc5 Merge pull request 'Update Rust crate zxcvbn to v3' (#12) from renovate/zxcvbn-3.x into develop
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
ci/woodpecker/cron/dependencies Pipeline was successful
Reviewed-on: #12
2025-07-19 10:48:44 +00:00
ff578ab849 🐛 Use of the zxcvbn::Score enum
All checks were successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/audit Pipeline was successful
2025-07-19 12:31:39 +02:00
0cffafcd77 Update Rust crate zxcvbn to v3
Some checks failed
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/audit unknown status
2025-07-15 06:07:18 +00:00
e88ad47af9 Merge pull request 'Update Rust crate rand to 0.9.0' (#8) from renovate/rand-0.x into develop
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
ci/woodpecker/cron/dependencies Pipeline was successful
Reviewed-on: #8
2025-07-13 19:10:20 +00:00
bf6c3d5cb0 🐛 Apply getrandom requirements to support wasm-unknown-unknown
All checks were successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/audit Pipeline was successful
See https://docs.rs/getrandom/latest/getrandom/#webassembly-support
2025-07-13 20:43:23 +02:00
2f101bedc3 Merge pull request 'Update rust Docker tag to v1.88' (#17) from renovate/rust-1.x into develop
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
ci/woodpecker/cron/dependencies Pipeline was successful
Reviewed-on: #17
2025-06-29 11:46:22 +00:00
30b118a621 Update rust Docker tag to v1.88
All checks were successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/audit Pipeline was successful
2025-06-27 00:01:39 +00:00
cf359b4950 🐛 Fix issues raised by clippy
Some checks failed
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/audit unknown status
2025-06-09 14:18:40 +02:00
c101fba07c Merge pull request 'Update Rust crate thiserror to v2' (#11) from renovate/thiserror-2.x into develop
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
ci/woodpecker/cron/dependencies Pipeline was successful
Reviewed-on: #11
2025-06-09 09:38:30 +00:00
01778866e0 Merge pull request 'Update Rust crate turf to 0.10.0' (#9) from renovate/turf-0.x into develop
All checks were successful
ci/woodpecker/cron/dependencies Pipeline was successful
Reviewed-on: #9
2025-06-08 20:37:54 +00:00
2bfa5161ee Merge pull request 'Update Rust crate reqwest to 0.12.0' (#5) from renovate/reqwest-0.x into develop
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
ci/woodpecker/cron/dependencies Pipeline was successful
Reviewed-on: #5
2025-05-03 10:18:57 +00:00
ae57282dda Update Rust crate thiserror to v2
All checks were successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/audit Pipeline was successful
2025-05-03 00:01:52 +00:00
0e022fabef Update Rust crate turf to 0.10.0
All checks were successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/audit Pipeline was successful
2025-05-03 00:01:45 +00:00
b0623a3f1b Update Rust crate reqwest to 0.12.0
All checks were successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/audit Pipeline was successful
2025-05-03 00:01:41 +00:00
56afe2688d Update Rust crate rand to 0.9.0
Some checks failed
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/audit unknown status
2025-05-03 00:01:39 +00:00
66179ba858 Merge pull request '🐛 Versions are always displayed with the "-modified" suffix (step 2)' (#16) from fix/versions-always-modified into develop
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
ci/woodpecker/cron/dependencies Pipeline was successful
Reviewed-on: #16
2025-05-02 19:37:03 +00:00
63f82eab07 🔇 Undo temporary logs
All checks were successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/audit Pipeline was successful
2025-05-02 21:08:36 +02:00
f00adf9276 🔨 Add profile section to the Cargo.toml file 2025-05-02 20:56:18 +02:00
216b4cee80 🔊 Add another logs to the Dockerfile 2025-05-02 20:23:57 +02:00
cdc02a601d 🔊 Deploy web app on commit on this branch (TEMP !!!)
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/audit Pipeline was successful
2025-05-02 20:04:50 +02:00
8bcb479b57 Add logs
All checks were successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/audit Pipeline was successful
2025-05-02 19:47:01 +02:00
b9d5c25137 Merge pull request '🐛 Versions are always displayed with the "-modified" suffix' (#15) from fix/versions-always-modified into develop
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
Reviewed-on: #15
2025-05-02 16:20:32 +00:00
e9fb20ad6e 🐛 Install git-lfs
All checks were successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/audit Pipeline was successful
2025-05-02 17:46:35 +02:00
d98222cd4a Merge pull request '🐛 Hide the preloader once the app loaded' (#14) from fix/hide-preloaded-once-the-app-loaded into develop
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
Reviewed-on: #14
2025-05-02 13:05:49 +00:00
c6effdfa15 🐛 Hide preloader once the app loaded
All checks were successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/audit Pipeline was successful
2025-05-02 14:51:57 +02:00
f17986fa16 Merge pull request '🐛 404 error returned on js loading' (#13) from fix/404-on-js-loading into develop
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
Reviewed-on: #13
2025-05-02 12:02:34 +00:00
cc65e7d5ff 🐛 Let dioxus add the preload script during the building process
All checks were successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/audit Pipeline was successful
2025-05-02 13:29:52 +02:00
1f42eaa37c Merge pull request 'Ensure that the linters and builds pass before merging a PR' (#7) from ci/add-checks-on-mr into develop
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
Reviewed-on: #7
2025-05-02 07:41:57 +00:00
acbe15ed69 👷 Split lint and audit jobs and fix woodpecker linter warns
All checks were successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/audit Pipeline was successful
2025-05-02 09:18:42 +02:00
4671a5ee51 👷 Use of the ci-lint-audit docker image
All checks were successful
ci/woodpecker/pr/validate Pipeline was successful
2025-05-01 22:32:17 +02:00
9e7ba84576 👷 Add Dockerfile for the ci-lint-audit image
All checks were successful
ci/woodpecker/pr/validate Pipeline was successful
ci/woodpecker/manual/dependencies Pipeline was successful
ci/woodpecker/manual/lint-audit-image Pipeline was successful
2025-04-28 07:25:17 +02:00
a8a7b16e9f 👷 Add cargo sort-derives tool
All checks were successful
ci/woodpecker/pr/validate Pipeline was successful
2025-04-27 22:10:27 +02:00
8684086c74 👷 Add cargo spellcheck tool
All checks were successful
ci/woodpecker/pr/validate Pipeline was successful
2025-04-27 21:12:32 +02:00
cd0a763c0a 👷 Fix lint - dependencies CI job
All checks were successful
ci/woodpecker/pr/validate Pipeline was successful
2025-04-27 20:34:40 +02:00
4d6d6d3515 👷 Add cargo udeps tool
Some checks failed
ci/woodpecker/pr/validate Pipeline failed
2025-04-27 17:34:34 +02:00
2bdd0b6a6d 👷 Add the cargo-deny configuration file 2025-04-27 13:27:50 +02:00
a9996d448c 👷 Run cargo deny only if we're able to build the web docker image
All checks were successful
ci/woodpecker/pr/validate Pipeline was successful
2025-04-27 11:52:15 +02:00
d5e92f282a 👷 For now, Don't block PRs on clippy or cargo deny errors
Some checks failed
ci/woodpecker/pr/validate Pipeline failed
2025-04-27 11:50:14 +02:00
285d4ba590 📄 Set AGPL-3.0-or-later license
Some checks failed
ci/woodpecker/pr/validate Pipeline failed
2025-04-27 11:36:45 +02:00
6b8cef176f ✏️ Fix typo 2025-04-27 11:36:27 +02:00
60756b7e72 👷 Add cargo deny tool
All checks were successful
ci/woodpecker/pr/validate Pipeline was successful
2025-04-27 11:08:34 +02:00
18ee33d512 💚 Add credential to pull images from our private Docker registry
All checks were successful
ci/woodpecker/manual/dependencies Pipeline was successful
ci/woodpecker/pr/validate Pipeline was successful
2025-04-27 04:27:03 +02:00
e2c20e4c64 💚 Dry run to ensure that we're able to build the web Docker image
Some checks failed
ci/woodpecker/pr/validate Pipeline failed
2025-04-27 04:23:29 +02:00
28aa250f58 💚 Use the custom dioxus-cli image to build the webclient dockerfile
Some checks failed
ci/woodpecker/pr/validate Pipeline failed
2025-04-27 03:46:57 +02:00
d29a3a0821 👷 Restore validation trigger
Some checks failed
ci/woodpecker/pr/validate Pipeline failed
2025-04-21 18:20:15 +02:00
570c792bf4 👷 Test the build for web on PR
All checks were successful
ci/woodpecker/manual/dependencies Pipeline was successful
2025-04-21 17:40:06 +02:00
fb37125740 Merge pull request 'Configure Renovate' (#3) from renovate/configure into develop
Some checks failed
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
ci/woodpecker/cron/dependencies Pipeline was successful
Reviewed-on: #3
2025-04-21 13:19:25 +00:00
6ce395cf7c Add renovate.json
All checks were successful
ci/woodpecker/pr/validate Pipeline was successful
2025-04-21 13:11:42 +00:00
a4eae624d3 👷 Retry to fix renovate git auth issue by setting RENOVATE_GIT_AUTHOR
Some checks failed
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
ci/woodpecker/cron/dependencies Pipeline was successful
2025-04-21 15:10:57 +02:00
f53e4fbadf 👷 Try to fix renovate git auth issue by setting RENOVATE_GIT_AUTHOR
Some checks failed
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
ci/woodpecker/cron/dependencies Pipeline failed
2025-04-21 15:06:59 +02:00
b4bc48d576 👷 Remove the dry run option for renovate
Some checks failed
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
ci/woodpecker/cron/dependencies Pipeline failed
2025-04-21 14:46:36 +02:00
b435bc73a7 👷 Try to fix renovate token issue (fix RENOVATE_ENDPOINT)
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/cron/dependencies Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2025-04-21 14:02:36 +02:00
a741c6ea8e 👷 Try to fix renovate token issue (move RENOVATE_TOKEN to env
Some checks failed
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/cron/dependencies Pipeline failed
ci/woodpecker/push/deploy Pipeline was successful
2025-04-21 13:44:01 +02:00
c761b203cc 👷 Try to fix renovate token issue (Upercase renovate_token)
Some checks failed
ci/woodpecker/cron/dependencies Pipeline failed
2025-04-21 13:39:16 +02:00
d85a2a97b7 👷 Remove command from the dependency scanning job
Some checks failed
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/cron/dependencies Pipeline failed
ci/woodpecker/push/deploy Pipeline was successful
2025-04-21 12:40:42 +02:00
403d238463 👷 First try adding renovate to the CI 2025-04-21 12:37:51 +02:00
581a3d159a ⬆️ Bump modx version (0.1.2 -> 0.1.4)
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2025-04-21 11:03:34 +02:00
4bbe863a56 ⬆️ Bump Dioxus version (0.6.1 -> 0.6.3)
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2025-04-21 10:14:54 +02:00
722d98f5d1 ♻️ Use of the assets management introduced by Dioxus 0.6.0
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
Cf. https://dioxuslabs.com/blog/release-060#stabilizing-manganis-asset-system
2025-04-21 09:43:25 +02:00
219fac87b1 ⬆️ Bump rust version (1.83 -> 1.86)
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2025-04-20 23:45:48 +02:00
c9deeea36f 🚧 Try to fix CI after the bump of the woodpecker version
Some checks failed
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2025-04-20 23:28:55 +02:00
d19a8a7f7d 🚧 Try to fix CI dockerize job 2025-04-20 23:11:59 +02:00
5bc8ac409e ⬆️ Force the commit used for matrix-sdk (fa6066b8)
Some checks failed
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-12-29 16:53:12 +01:00
6586edf287 ⬆️ Bump dioxus version (main -> 0.6.1)
Some checks failed
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-12-29 16:25:30 +01:00
a533f1869d 🐛 Disable caching for dockerize
Some checks failed
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-09-25 23:35:01 +02:00
6b9ef5dc90 🐛 Don't run validation on commit into default branch
Some checks failed
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-09-25 22:59:24 +02:00
93b6027c73 🐛 Use dioxus-cli:6ff7a54 image to dockerize
Some checks failed
ci/woodpecker/push/validate Pipeline failed
ci/woodpecker/push/dockerize unknown status
ci/woodpecker/push/deploy unknown status
2024-09-25 22:17:59 +02:00
8026b6fa32 🐛 Use stable dioxus-cli
Some checks failed
ci/woodpecker/push/validate Pipeline was successful
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-09-25 21:04:35 +02:00
8652d56f51 🐛 Fix wasm-bindgen-cli version in Dockerfile
Some checks failed
ci/woodpecker/push/validate Pipeline was successful
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-09-25 00:11:12 +02:00
c5045c328c Merge pull request 'Run validation step on push into default branch (2)' (#2) from fix/ci-add-first-checks into develop
Some checks failed
ci/woodpecker/push/validate Pipeline was successful
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
Reviewed-on: #2
2024-09-24 21:37:11 +00:00
ba96ad77d3 👷 Run validation step on push into default branch
All checks were successful
ci/woodpecker/pr/validate Pipeline was successful
2024-09-24 23:18:44 +02:00
5ff06c93f4 Merge pull request 'Run tests on PR' (#1) from fix/ci-add-first-checks into develop
Reviewed-on: #1
2024-09-24 21:12:34 +00:00
831085e8b6 👷 Disable build step on MR
All checks were successful
ci/woodpecker/pr/validate Pipeline was successful
2024-09-24 22:56:04 +02:00
691dc7572a 🚨 Fix some clippy warnings
All checks were successful
ci/woodpecker/pr/validate Pipeline was successful
2024-09-23 22:16:55 +02:00
b728f6efcd 👷 Install deps
Some checks failed
ci/woodpecker/pr/validate Pipeline failed
2024-09-23 21:58:07 +02:00
d8e3d49d95 👷 Install clippy
Some checks failed
ci/woodpecker/pr/validate Pipeline failed
2024-09-23 20:52:08 +02:00
9b2ab337b2 👷 Install rustfmt
Some checks failed
ci/woodpecker/pr/validate Pipeline failed
2024-09-23 20:50:51 +02:00
deb3e273f4 👷 Add format and clippy steps
Some checks failed
ci/woodpecker/pr/validate Pipeline failed
2024-09-23 20:47:23 +02:00
abea905feb 👷 Check that no error occurs during the build of the webapp
All checks were successful
ci/woodpecker/pr/validate Pipeline was successful
2024-09-22 23:40:22 +02:00
c7955d5571 Merge branch 'fix/invalid-wysiwyg-dep' into develop
Some checks failed
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-09-22 22:50:16 +02:00
f99296bdce 🐛 Fix Cargo.toml 2024-09-22 22:49:09 +02:00
7989d86af1 Merge branch 'conversations-panel' into develop
Some checks failed
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-09-22 21:19:37 +02:00
d2108fa6fc ♻️ Remove duplicated if statement 2024-09-22 19:39:06 +02:00
44ba3d4d23 🐛 script_include is not used anymore (since Dioxus #2258) 2024-09-22 19:36:43 +02:00
5206fb13c8 ♻️ Use of manganis for wallpaper pattern 2024-09-22 18:51:48 +02:00
27934c7fc9 🚨 Fix some clippy warnings 2024-09-08 16:12:33 +02:00
9d95bd4481 Add the capability to join a conversation 2024-09-08 16:07:13 +02:00
648be8ba72 🐛 Add tail div to dynamic conversation_panels to fix some side effects 2024-09-07 13:01:26 +02:00
b7b98dff15 Add TabsBar to the LayoutBig (Conversations layout) 2024-09-07 12:58:39 +02:00
aaafa91cbe ⬆️ Bump turf version (0.8.0 -> 0.9.3) 2024-08-21 23:29:35 +02:00
9a5f7ae504 ⬆️ Use of Dioxus main branch instead of 0.5 release 2024-08-21 23:29:33 +02:00
d5d996eec3 Add topic to the UI store Room 2024-06-27 08:25:11 +02:00
73c5b70ba8 🚧 First design template for ConversationOptionsMenu 2024-06-22 20:57:44 +02:00
f0463213cf 🚧 Use of the use_long_press to open a future room configuration menu 2024-06-22 20:55:19 +02:00
e55992aed5 💄 Add Reject and Join buttons 2024-06-22 20:52:08 +02:00
cea60ce695 Add ui use_long_press hook 2024-06-15 16:19:20 +02:00
271e865d40 💄 Open/close ChatPanel on click (Conversations layout/big breakpoint) 2024-06-09 12:31:18 +02:00
ffe759e749 ♻️ Merge Space and HomeSpace components 2024-06-09 11:22:48 +02:00
9baa7f290a 💄 Open/close ChatPanel on click 2024-06-09 09:43:46 +02:00
d566a4927f ♻️ Use of the SCSS variables to compute the width of the inner panels 2024-06-08 13:04:17 +02:00
1ad4d444fb 💄 Add first breakpoint management by Conversations layout 2024-06-07 22:18:15 +02:00
204e11b8b1 🐛 tracing_forest crate isn't used for wasm 2024-05-26 18:09:05 +02:00
f566d88df2 Merge branch 'conversations-panel' into develop
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-05-26 12:00:55 +02:00
89473cfd61 🚨 Fix some clippy warnings 2024-05-26 11:53:47 +02:00
62015f8d13 Use of Conversations layout 2024-05-26 11:39:18 +02:00
c8e8e2da67 Add a first Conversations layout 2024-05-26 11:17:37 +02:00
df32faa8e6 🐛 Dioxus Props shall implement PartialEq and Clone traits 2024-05-26 10:58:51 +02:00
5194899de0 Add a first Conversations component 2024-05-26 10:45:09 +02:00
ff0ac7f982 🚧 Add a ChatPanel placeholder component 2024-05-25 14:06:40 +02:00
8ed4ff3f2a ♻️ Rename views -> layouts 2024-05-24 22:35:10 +02:00
e7e1a4d663 💄 Use of border-box box sizing
This makes calculating the size of the UI components easier, especially those which have padding, margin or border.
2024-05-23 08:45:51 +02:00
19d64d7ac5 ♻️ Render Room avatar using the RoomMember ones, if not set 2024-05-22 16:44:07 +02:00
35e191eb62 🗑️ WorkerTask::GetRoomMembers isn't used, remove it 2024-05-22 16:21:13 +02:00
8c244ce4a7 ⬆️ Use dioxus main branch for now 2024-05-22 15:32:25 +02:00
a1fe74f53e ♻️ Use of "target_family" instead of feature to manage wasm platform 2024-05-21 12:40:19 +02:00
f43f54c120 🚧 Use of local Dioxus repos waiting the #2338 PR merge
Cf https://github.com/DioxusLabs/dioxus/pull/2338
2024-05-21 12:35:00 +02:00
cd6506fb03 🔊 Trace events from Matrix client callbacks to domain methods 2024-05-21 12:26:40 +02:00
b5da0ee992 ♻️ Use of cfg_if to manage how to logging according to the platform 2024-05-18 22:03:21 +02:00
df2d924c65 Cleanup dependencies 2024-05-18 22:00:28 +02:00
54c7073b98 💡 Add comments to keep in mind why we can't send Matrix sdk Room 2024-05-18 09:52:40 +02:00
fdae149c4a 🚧 Add Avatar management and refresh the Matrix client part 2024-05-17 22:41:35 +02:00
0b898dce52 Add Invitation value object 2024-05-17 09:31:59 +02:00
cbe32c250e 🚧 Add relations between store::Room and store::Area 2024-05-16 22:46:59 +02:00
d77c2a9d12 ♻️ Use of Store interfaces 2024-05-15 19:08:52 +02:00
bc30670f6e ⬆️ Bumps modx version (git -> 0.1.2) 2024-05-15 19:08:49 +02:00
18a797bc3f ♻️ Add Account, Room and Space UI store structs 2024-05-11 18:23:48 +02:00
bc6b02bc34 ♻️ Rework the Matrix messaging Client 2024-05-10 22:32:35 +02:00
0a936dd12b ♻️ Rework the Matrix messaging Requester 2024-05-10 22:20:32 +02:00
ef41c0bd48 Add events shared by Matrix client and Requester 2024-05-10 22:20:11 +02:00
e3a6ec9858 Add new messaging WorkerTask 2024-05-10 22:16:49 +02:00
692a71faef 🚨 Fix clippy warnings 2024-05-10 20:11:48 +02:00
c2918fbc78 🚧 Add RoomMember value object 2024-05-10 19:56:39 +02:00
bfa1539d23 🚧 Add Space identity 2024-05-10 19:29:42 +02:00
0190cf9165 ♻️ Rework the Room entity 2024-05-10 19:13:46 +02:00
4f9e5c538e 🚧 Add a first version of the mozaik builder service 2024-05-10 18:56:54 +02:00
79e8dea622 🚧 Add Account identity and messaging and store interfaces 2024-05-10 18:56:37 +02:00
0a0d6e745b 🚧 Remove AvatarSelector component 2024-05-09 22:12:32 +02:00
32b633aad6 🚧 Remove Header component 2024-05-08 10:27:01 +02:00
4ea4416165 🚧 Remove Loading component 2024-05-08 10:23:18 +02:00
c4dcb0f87d 🚧 Remove ChatsWindow and ContactsWindow components 2024-05-05 22:20:25 +02:00
f79ebb0b03 Add SearchIcon, SpacesIcon, ChatsIcon and RoomsIcon elements 2024-04-26 19:42:49 +02:00
7078f86cd8 💄 Make the "logo" shape reusable 2024-04-26 19:31:05 +02:00
894f32e177 💄 Adjust Login padding 2024-04-26 19:27:11 +02:00
3afed02aa8 🚧 Make Button usable outside of the button.rs file 2024-04-26 19:23:34 +02:00
7b6781a007 🐛 Fix gap in wallpaper pattern
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-04-23 11:35:31 +02:00
118 changed files with 5229 additions and 2778 deletions

18
.woodpecker/.audit.yaml Normal file
View 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

View 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

View File

@@ -16,10 +16,6 @@ steps:
repo: rg.fr-par.scw.cloud/asr-projects/beau-gosse-du-92-web
container: beau-gosse-du-92-web
tag: ${CI_COMMIT_SHA}
secrets:
- kubernetes_cert
- kubernetes_server
- kubernetes_token
when:
- event: push

View File

@@ -6,6 +6,7 @@ steps:
repo: asr-projects/beau-gosse-du-92-web
tags: ${CI_COMMIT_SHA}
auto_tag: true
cache: false
username: nologin
password:
from_secret: registry-password
@@ -13,3 +14,6 @@ steps:
when:
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
# depends_on:
# - validate

View 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
View 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

View File

@@ -2,46 +2,101 @@
name = "beau-gosse-du-92"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-or-later"
[features]
default = []
desktop = ["dioxus/desktop"]
web = ["dioxus/web"]
[package.metadata.spellcheck]
config = "./spellcheck.toml"
[dependencies]
dioxus = "0.5.*"
dioxus-free-icons = { version = "0.8", features = ["material-design-icons-navigation", "ionicons"] }
dioxus-sdk = { version = "0.5.*", features = ["utils"] }
# matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", branch = "main", default-features = false, features = ["js", "rustls-tls"] }
matrix-sdk = { version = "0.7.*", default-features = false, features = ["js", "rustls-tls"] }
# Errors
anyhow = "1.0.75"
url = "2.5.0"
dirs = "5.0.1"
ctrlc-async = "3.2.2"
thiserror = "1.0.50"
turf = "0.8.*"
tokio = { version = "1.34.0", default-features = false, features = ["rt", "sync"] }
log = "0.4.20"
futures-util = "0.3.29"
futures = "0.3.29"
rand = "0.8.5"
reqwest = "0.11.24"
validator = { version = "0.17.0", features = ["derive"] }
const_format = "0.2.32"
zxcvbn = "2.2.2"
thiserror = "2.0.0"
# Async
async-std = "1.12.0"
tracing = "0.1.40"
tracing-web = "0.1.3"
tracing-subscriber = "0.3.18"
async-trait = "0.1.80"
futures = "0.3.29"
futures-util = "0.3.29"
tokio = { version = "1.34.0", default-features = false, features = ["rt", "sync"] }
tokio-stream = "0.1.15"
# Utils
base64 = "0.22.0"
const_format = "0.2.32"
rand = "0.9.1"
validator = { version = "0.20.0", features = ["derive"] }
# Http client
reqwest = "0.12.0"
# Password strength estimation
zxcvbn = { version = "3.0.0", features = ["ser"] }
# 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.10.0"
# Dioxus
dioxus-free-icons = { version = "0.9", features = ["ionicons", "font-awesome-solid"] }
modx = "0.1.4"
[target.'cfg(target_family = "wasm")'.dependencies]
# Utils
getrandom = { version = "0.3.2", features = ["wasm_js"] }
# 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"
getrandom = { version = "0.3.2" }
# 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"] }
[target.wasm32-unknown-unknown]
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
[build-dependencies]
regex = "1.10.3"
[profile]
[profile.wasm-dev]
inherits = "dev"
opt-level = 1
[profile.server-dev]
inherits = "dev"
[profile.android-dev]
inherits = "dev"
[package.metadata.turf]
minify = true

View File

@@ -7,14 +7,7 @@ title = "BG92"
[web.watcher]
reload_html = true
watch_path = ["public/index.html", "public/assets/index.css", "src"]
[web.resource]
style = ["assets/index.css"]
[web.resource.dev]
style = []
script = []
watch_path = ["Dioxus.toml", "public/index.html", "src"]
[[web.proxy]]
backend = "http://localhost:8000/api/"

View File

@@ -1,7 +1,4 @@
FROM rust:latest AS builder
# Homemade docker image providing the dioxus-cli
COPY --from=rg.fr-par.scw.cloud/asr-projects/dioxus-cli:asr-0.5.2 /usr/local/bin/dx /usr/local/bin/dx
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
@@ -10,16 +7,20 @@ 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 git-lfs
&& apt install -y --no-install-recommends git git-lfs \
&& apt clean
COPY . .
RUN /usr/local/bin/dx build -r --platform web -- -j ${JOBS_NB}
# Workaround waiting for the dioxus 0.7 release (cf. https://github.com/DioxusLabs/dioxus/issues/4238)
ENV RUSTFLAGS="--cfg getrandom_backend=\"wasm_js\""
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/dist .
COPY --from=builder /usr/src/beau-gosse-du-92/target/dx/beau-gosse-du-92/release/web/public .

View File

@@ -14,8 +14,8 @@ 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 Webclient).
- SDK available for each platform and a new Rust sdk supporting all the previously listed platforms.
- 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.
@@ -28,16 +28,16 @@ API)](https://spec.matrix.org/v1.9/client-server-api/) protocol.
Even if the Rust SDK is still in beta, it seems to be the future one (cf. [Element X - experience the future of
Element!](https://element.io/blog/element-x-experience-the-future-of-element/)) and a good choice for someone starting a
new client... from my point of vue.
new client... from my point of view.
The SDK choosen, a Rust (to avoid to use the bindings provided by the matrix-rust-sdk and mostly because I want to
The SDK chosen, a Rust (to avoid to use the bindings provided by the matrix-rust-sdk and mostly because I want to
learn Rust) graphical library should be selected. The [Dioxus](https://dioxuslabs.com/) one seems to do the job:
- React-inspired library for Rust.
- Multi-platforms (use of Webview or WGPU-enabled renderers).
- Multi-platforms (use of Web-view or WGPU-enabled renderers).
# TODO
- [ ] Test dioxus-radio.
- [ ] Design system ?
- [ ] Implement MSN messenger features using Matrix.org sdk...
- [ ] Implement MSN messenger features using Matrix.org SDK...

141
build.rs
View File

@@ -1,4 +1,5 @@
use std::env;
use std::fmt::Display;
use std::fs::File;
use std::io::Write;
use std::io::{self, BufRead};
@@ -10,14 +11,33 @@ use regex::Regex;
fn main() {
// Tell Cargo to rerun this build script if any SCSS file
// in the 'src' directory or its subdirectories changes.
println!("cargo:rerun-if-changed=src/**/*.scss");
println!("cargo:rerun-if-changed=src/ui/**/*.scss");
let out_dir = env::var("OUT_DIR").unwrap();
let style_src_path = PathBuf::from("src/ui/_base.scss");
let style_dst_path = Path::new(&out_dir).join("style_vars.rs");
// let mut tasks = Vec::new();
let tasks = vec![
// Global tokens
Task::new(
PathBuf::from("src/ui/_base.scss"),
Path::new(&out_dir).join("style_tokens.rs"),
"style".to_string(),
),
// variables defined by the Panel component
Task::new(
PathBuf::from("src/ui/components/_panel.scss"),
Path::new(&out_dir).join("style_component_panel.rs"),
"panel".to_string(),
),
// Variables set by the Conversations layout
Task::new(
PathBuf::from("src/ui/layouts/conversations.scss"),
Path::new(&out_dir).join("style_layout_conversations.rs"),
"conversations".to_string(),
),
];
export_color_variables(&style_src_path, &style_dst_path)
export_variables(tasks)
}
// From https://doc.rust-lang.org/rust-by-example/std_misc/file/read_lines.html
@@ -32,14 +52,21 @@ where
}
#[derive(Debug)]
struct CssColorVariable<'a> {
name: &'a str,
value: &'a str,
struct ColorVariable {
name: String,
value: String,
}
impl<'a> CssColorVariable<'a> {
pub fn to_rust(&self) -> String {
format!(
impl ColorVariable {
pub fn new(name: String, value: String) -> Self {
Self { name, value }
}
}
impl Display for ColorVariable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"const {name}: &str = \"{value}\";",
name = self.name.replace('-', "_").to_uppercase(),
value = self.value
@@ -47,35 +74,83 @@ impl<'a> CssColorVariable<'a> {
}
}
fn export_color_variables(src_path: &PathBuf, dst_path: &PathBuf) {
let mut dst_file = File::create(dst_path).unwrap();
if let Err(err) = dst_file.write(b"#[allow(dead_code)]\nmod style {") {
println!("{}", err);
return;
};
#[derive(Debug)]
struct FloatVariable {
name: String,
value: f64,
}
let re = Regex::new(r"^\$([^:]+):[[:space:]]*#([^$]+);[[:space:]]*$").unwrap();
impl FloatVariable {
pub fn new(name: String, value: f64) -> Self {
Self { name, value }
}
}
if let Ok(lines) = read_lines(src_path) {
for line in lines.map_while(Result::ok) {
let Some(groups) = re.captures(&line) else {
continue;
};
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
)
}
}
let var = CssColorVariable {
name: &groups[1],
value: &groups[2],
};
struct Task {
src_path: PathBuf,
dst_path: PathBuf,
module_name: String,
}
let rust_export = var.to_rust();
if let Err(err) = dst_file.write_fmt(format_args!(" pub {}\n", rust_export)) {
println!("{}", err);
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 {variable}\n")) {
println!("{err}");
break;
}
}
}
if let Err(err) = dst_file.write(b"}\n") {
println!("{}", err);
};
if let Err(err) = dst_file.write(b"}\n") {
println!("{err}");
};
}
}

298
deny.toml Normal file
View 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 = []

View File

@@ -0,0 +1,26 @@
FROM rust:1.88 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

View File

@@ -1,207 +0,0 @@
@font-face {
src: url("../fonts/Geist/Geist-Medium.woff2") format("woff2");
font-family: "Geist";
font-weight: normal;
}
@font-face {
src: url("../fonts/Geist/Geist-Bold.woff2") format("woff2");
font-family: "Geist";
font-weight: bold;
}
html, body {
height: 100%;
width: 100%;
margin: 0;
font-family: "Geist";
font-weight: normal;
}
#main {
height: 100%;
width: 100%;
}
@keyframes multicolor {
0% { fill: #1DB2CF; /* color-primary-100 */ }
33% { fill: #7E6BB6; /* color-secondary-100 */ }
66% { fill: #D53583; /* color-ternary-100 */ }
}
:root {
--wallpaper-pattern-height: 128px;
--spinner-height: 5%;
--window-center-pos: calc(50% + (var(--wallpaper-pattern-height) / 2) - (var(--spinner-height) / 2));
}
/* @media (0px < height <= calc(var(--wallpaper-pattern-height) * 5)) { */
@media (min-height: 0px) and (max-height: 640px) {
:root {
--spinner-top: var(--window-center-pos);
}
}
/* @media (calc($wallpaper-pattern-height * 5) < height <= calc($wallpaper-pattern-height * 6)) { */
@media (min-height: 641px) and (max-height: 768px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 2));
}
}
/* @media (calc($wallpaper-pattern-height * 6) < height <= calc($wallpaper-pattern-height * 8)) { */
@media (min-height: 769px) and (max-height: 1024px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 2));
}
}
/* @media (calc($wallpaper-pattern-height * 8) < height <= calc($wallpaper-pattern-height * 10)) { */
@media (min-height: 1025px) and (max-height: 1280px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 3));
}
}
/* @media (calc($wallpaper-pattern-height * 10) < height <= calc($wallpaper-pattern-height * 12)) { */
@media (min-height: 1281px) and (max-height: 1536px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 4));
}
}
/* @media (calc($wallpaper-pattern-height * 12) < height <= calc($wallpaper-pattern-height * 14)) { */
@media (min-height: 1537px) and (max-height: 1792px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 5));
}
}
/* @media (calc($wallpaper-pattern-height * 14) < height <= calc($wallpaper-pattern-height * 16)) { */
@media (min-height: 1793px) and (max-height: 2048px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 6));
}
}
/* @media (calc($wallpaper-pattern-height * 16) < height <= calc($wallpaper-pattern-height * 18)) { */
@media (min-height: 2049px) and (max-height: 2304px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 7));
}
}
/* @media (calc($wallpaper-pattern-height * 18) < height <= calc($wallpaper-pattern-height * 20)) { */
@media (min-height: 2305px) and (max-height: 2560px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 8));
}
}
/* @media (calc($wallpaper-pattern-height * 20) < height <= calc($wallpaper-pattern-height * 22)) { */
@media (min-height: 2561px) and (max-height: 2816px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 9));
}
}
/* @media (calc($wallpaper-pattern-height * 22) < height <= calc($wallpaper-pattern-height * 24)) { */
@media (min-height: 2817px) and (max-height: 3072px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 10));
}
}
/* @media (calc($wallpaper-pattern-height * 24) < height <= calc($wallpaper-pattern-height * 26)) { */
@media (min-height: 3073px) and (max-height: 3328px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 11));
}
}
/* @media (calc($wallpaper-pattern-height * 26) < height <= calc($wallpaper-pattern-height * 28)) { */
@media (min-height: 3329px) and (max-height: 3584px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 12));
}
}
/* @media (calc($wallpaper-pattern-height * 28) < height <= calc($wallpaper-pattern-height * 30)) { */
@media (min-height: 3585px) and (max-height: 3840px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 13));
}
}
/* @media (calc($wallpaper-pattern-height * 30) < height <= calc($wallpaper-pattern-height * 32)) { */
@media (min-height: 3841px) and (max-height: 4096px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 14));
}
}
/* @media (calc($wallpaper-pattern-height * 32) < height <= calc($wallpaper-pattern-height * 34)) { */
@media (min-height: 4097px) and (max-height: 4352px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 15));
}
}
/* @media (calc($wallpaper-pattern-height * 34) < height <= calc($wallpaper-pattern-height * 36)) { */
@media (min-height: 4353px) and (max-height: 4608px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 16));
}
}
/* @media (calc($wallpaper-pattern-height * 36) < height <= calc($wallpaper-pattern-height * 38)) { */
@media (min-height: 4609px) and (max-height: 4864px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 17));
}
}
/* @media (calc($wallpaper-pattern-height * 38) < height <= calc($wallpaper-pattern-height * 40)) { */
@media (min-height: 4865px) and (max-height: 5120px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 18));
}
}
.loader {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
.wallpaper {
height: 100%;
width: 100%;
z-index: -1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.content {
background-image: url("../images/wallpaper-pattern.svg");
background-position: center;
width: 150%;
height: 150%;
}
}
.spinner {
height: var(--spinner-height);
aspect-ratio: 2;
position: absolute;
top: var(--spinner-top);
svg {
--fps: 4;
--duration_sec: 3;
--steps: calc(var(--duration_sec) * var(--fps));
height: 100%;
width: 100%;
fill: #1DB2CF; /* color-primary-100 */
stroke: #1B1B1B; /* greyscale-90 */
animation: 3s multicolor linear infinite;
animation-timing-function: steps(var(--steps), end);
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 B

View File

@@ -1,4 +1,3 @@
<!-- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1152 128"> -->
<svg xmlns="http://www.w3.org/2000/svg" height="128" width="384" viewBox="0 0 384 128">
<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"/>
@@ -6,9 +5,9 @@
<path fill="#1DB2CF" d="M 393.736 -15 L 354 3.337 l 23.642 -0.088 L 373.788 15 L 414 -3.425 H 390.834 L 393.736 -15 Z"/>
<path fill="#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 114 L -30 132.337 l 23.642 -0.088 L -10.212 144 L 30 125.575 H 6.834 L 9.736 114 Z"/>
<path fill="#D53583" d="M 201.736 114 L 162 132.337 l 23.642 -0.088 L 181.788 144 L 222 125.575 H 198.834 L 201.736 114 Z"/>
<path fill="#1DB2CF" d="M 393.736 114 L 354 132.337 l 23.642 -0.088 L 373.788 144 L 414 125.575 H 390.834 L 393.736 114 Z"/>
</pattern>
<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>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +0,0 @@
<svg enable-background="new 0 0 48 48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path d="m36.5 44h-25c-1.1 0-1.8-1.2-1.3-2.2l2.8-4.8h22l2.7 4.8c.6 1-.1 2.2-1.2 2.2z" fill="#455a64"/><circle cx="24" cy="23" fill="#78909c" r="18"/><path d="m24 35c-6.6 0-12-5.4-12-12s5.4-12 12-12 12 5.4 12 12-5.4 12-12 12z" fill="#455a64"/><circle cx="24" cy="23" fill="#42a5f5" r="9"/><path d="m28.8 20c-1.2-1.4-3-2.2-4.8-2.2s-3.6.8-4.8 2.2c-.5.5-.4 1.3.1 1.8s1.3.4 1.8-.1c1.5-1.7 4.3-1.7 5.8 0 .3.3.6.4 1 .4.3 0 .6-.1.9-.3.4-.4.5-1.3 0-1.8z" fill="#90caf9"/></svg>

Before

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 719 B

View File

@@ -1,16 +1,207 @@
<!DOCTYPE html>
<html>
<head>
<!-- Hack to avoid the addition of the script part by dioxus-cli -->
<!-- <title>{base_path}</title> -->
<!-- <title>{app_name}</title> -->
<title></title>
<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_include}
{script_include}
<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">
@@ -19,7 +210,7 @@
<div class="content"></div>
</div>
<div class="spinner">
<svg viewBox="0 0 184 94" xmlns="http://www.w3.org/2000/svg">
<svg xmlns="http://www.w3.org/2000/svg" width='184' height='94' viewBox="0 0 184 94">
<path
stroke-linejoin="round"
stroke-width="6"
@@ -29,25 +220,5 @@
</div>
</div>
</div>
<script type="module" async>
import init from "{base_path}/assets/dioxus/beau-gosse-du-92.js";
// Ensure that everything needed to render preloader has been downloaded
// before fetching the wasm bundle.
window.onload=function() {
init("{base_path}/assets/dioxus/beau-gosse-du-92_bg.wasm").then(
wasm => {
const preloader = document.getElementById("preloader");
if (preloader !== undefined) {
preloader.style.display = 'none';
}
if (wasm.__wbindgen_start == undefined) {
wasm.main();
}
}
);
}
</script>
</body>
</html>

3
renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

6
spellcheck.dic Normal file
View File

@@ -0,0 +1,6 @@
5
Dioxus
MSN
renderers
SDK
TODO

10
spellcheck.toml Normal file
View 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" ]

View File

@@ -1,197 +0,0 @@
// Cf. https://dioxuslabs.com/learn/0.4/reference/use_coroutine
// In order to use/run the rx.next().await statement you will need to extend the [Stream] trait
// (used by [UnboundedReceiver]) by adding 'futures_util' as a dependency to your project
// and adding the use futures_util::stream::StreamExt;
use std::cell::RefCell;
use dioxus::prelude::*;
use futures_util::stream::StreamExt;
use log::{debug, error, warn};
use matrix_sdk::ruma::OwnedRoomId;
use tokio::select;
use crate::domain::model::room::{ByIdRooms, Room};
use crate::domain::model::session::Session;
use crate::infrastructure::messaging::matrix::client::{Client, RoomEvent};
use crate::infrastructure::messaging::matrix::requester::{Receivers, Requester};
use crate::infrastructure::messaging::matrix::worker_tasks::LoginStyle;
use crate::ui::components::chats_window::interface::Interface as ChatsWinInterface;
// #[derive(Clone, Debug)]
// pub struct UserInfo {
// pub avatar_url: Option<OwnedMxcUri>,
// pub display_name: Option<String>,
// pub blurhash: Option<String>,
// }
// impl UserInfo {
// pub fn new(
// avatar_url: Option<OwnedMxcUri>,
// display_name: Option<String>,
// blurhash: Option<String>,
// ) -> Self {
// Self {
// avatar_url,
// display_name,
// blurhash,
// }
// }
// }
// pub type ByIdUserInfos = HashMap<OwnedUserId, UserInfo>;
// #[derive(Clone)]
// pub struct Store {
// pub is_logged: bool,
// pub rooms: ByIdRooms,
// pub user_infos: ByIdUserInfos,
// pub user_id: Option<OwnedUserId>,
// }
// impl Store {
// pub fn new() -> Self {
// Self {
// is_logged: false,
// rooms: HashMap::new(),
// user_infos: HashMap::new(),
// user_id: None,
// }
// }
// }
// impl PartialEq for Store {
// fn eq(&self, other: &Self) -> bool {
// self.is_logged == other.is_logged
// && self.user_id == other.user_id
// && self.user_infos.len() == other.user_infos.len()
// && self
// .user_infos
// .keys()
// .all(|k| other.user_infos.contains_key(k))
// && self.rooms.len() == other.rooms.len()
// && self.rooms.keys().all(|k| other.rooms.contains_key(k))
// }
// }
// impl Eq for Store {}
pub struct AppSettings {
pub requester: Option<RefCell<Requester>>,
}
impl AppSettings {
pub fn new() -> Self {
Self { requester: None }
}
pub fn set_requester(&mut self, requester: RefCell<Requester>) {
self.requester = Some(requester);
}
}
async fn on_room(room_id: OwnedRoomId, room: Room, by_id_rooms: &GlobalSignal<ByIdRooms>) {
// TODO: Update rooms
by_id_rooms
.write()
.insert(room_id, RefCell::<Room>::new(room));
}
async fn on_joining_invitation(
room_id: OwnedRoomId,
room: Room,
by_id_rooms: &GlobalSignal<ByIdRooms>,
) {
debug!("You're invited to join the \"{}\" room", room.id());
// TODO: Update rooms
by_id_rooms
.write()
.insert(room_id, RefCell::<Room>::new(room));
}
async fn on_room_topic(room_id: OwnedRoomId, topic: String, by_id_rooms: &GlobalSignal<ByIdRooms>) {
if let Some(room) = by_id_rooms.read().get(&room_id) {
let mut room = room.borrow_mut();
room.set_topic(Some(topic));
} else {
warn!("No room found with the \"{}\" id", room_id);
}
}
pub async fn sync_messages(by_id_rooms: &GlobalSignal<ByIdRooms>, room_id: OwnedRoomId) {
error!("== sync_messages ==");
}
pub async fn sync_rooms(
mut rx: UnboundedReceiver<bool>,
receivers: Receivers,
by_id_rooms: &GlobalSignal<ByIdRooms>,
) {
if let Some(_is_logged) = rx.next().await {
let mut rooms_receiver = receivers.room_receiver.borrow_mut();
loop {
// TODO: Remove select if no more receivers will be used.
select! {
res = rooms_receiver.recv() => {
if let Ok(room_event) = res {
match room_event {
RoomEvent::MemberEvent(room_id, room) => on_room(room_id, room, &by_id_rooms).await,
RoomEvent::InviteEvent(room_id, room) => on_joining_invitation(room_id, room, &by_id_rooms).await,
RoomEvent::TopicEvent(room_id, topic) => on_room_topic(room_id, topic, &by_id_rooms).await,
};
}
},
}
}
}
}
pub async fn login(
mut rx: UnboundedReceiver<bool>,
app_settings: &GlobalSignal<AppSettings>,
session: &GlobalSignal<Session>,
) {
while let Some(is_logged) = rx.next().await {
if !is_logged {
let homeserver_url = session.read().homeserver_url.clone();
let username = session.read().username.clone();
let password = session.read().password.clone();
if homeserver_url.is_some() && username.is_some() && password.is_some() {
let client = Client::spawn(homeserver_url.unwrap()).await;
if let Err(err) = client.init().await {
error!("Following error occureds during client init: {}", err);
}
match client
.login(LoginStyle::Password(username.unwrap(), password.unwrap()))
.await
{
Ok(_) => {
debug!("successfully logged");
session.write().is_logged = true;
}
Err(err) => {
error!("Error during login: {err}");
// TODO: Handle invalid login
// invalid_login.modify(|_| true);
}
}
app_settings.write().set_requester(RefCell::new(client));
} else {
warn!("At least one of the following values is/are invalid: homeserver, username or password");
}
} else {
warn!("already logged... skip login");
}
}
error!("=== LOGIN END ===");
}
pub static APP_SETTINGS: GlobalSignal<AppSettings> = Signal::global(AppSettings::new);
pub static ROOMS: GlobalSignal<ByIdRooms> = Signal::global(ByIdRooms::new);
pub static SESSION: GlobalSignal<Session> = Signal::global(Session::new);
pub static CHATS_WIN_INTERFACE: GlobalSignal<ChatsWinInterface> =
Signal::global(ChatsWinInterface::new);

136
src/domain/model/account.rs Normal file
View 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
}
}

View 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;

View 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>>;
}

View File

@@ -1,2 +1,8 @@
pub(crate) mod account;
pub(crate) mod common;
pub(crate) mod messaging_interface;
pub(crate) mod room;
pub(crate) mod room_member;
pub(crate) mod session;
pub(crate) mod space;
pub(crate) mod store_interface;

View File

@@ -1,145 +1,323 @@
use std::cell::RefCell;
use std::{collections::HashMap, sync::Arc};
use std::{
cell::RefCell,
collections::HashMap,
fmt::{Debug, Formatter},
rc::Rc,
};
use matrix_sdk::ruma::OwnedRoomId;
use matrix_sdk::{Room as MatrixRoom, RoomState as MatrixRoomState};
use tracing::error;
use async_trait::async_trait;
use futures::future::{join, join_all};
use matrix_sdk::{ruma::OwnedRoomId, RoomState as MatrixRoomState};
use tracing::{debug, debug_span, error, instrument, trace};
pub(crate) type RoomId = OwnedRoomId;
use super::{
common::{Avatar, UserId},
messaging_interface::{RoomMessagingConsumerInterface, RoomMessagingProviderInterface},
room_member::RoomMember,
space::SpaceId,
store_interface::{RoomStoreConsumerInterface, RoomStoreProviderInterface},
};
use crate::infrastructure::services::mozaik_builder::create_mozaik;
#[derive(Clone, Debug)]
pub(crate) struct Room {
pub type RoomId = OwnedRoomId;
#[derive(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: Option<String>,
name: RefCell<Option<String>>,
topic: Option<String>,
is_direct: Option<bool>,
state: Option<MatrixRoomState>,
avatar: RefCell<Option<Avatar>>,
invitations: RefCell<HashMap<UserId, Invitation>>,
members: RefCell<HashMap<UserId, RoomMember>>,
spaces: Vec<SpaceId>,
messaging_provider: Option<Rc<dyn RoomMessagingProviderInterface>>,
store: RefCell<Option<Rc<dyn RoomStoreProviderInterface>>>,
}
impl PartialEq for Room {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Room {
fn new(
pub fn new(
id: RoomId,
// TODO: move space at the end of the list of params
name: Option<String>,
topic: Option<String>,
is_direct: Option<bool>,
state: Option<MatrixRoomState>,
spaces: Vec<SpaceId>,
) -> Self {
Self {
id,
name,
name: RefCell::new(name),
topic,
is_direct,
state,
avatar: RefCell::new(None),
invitations: RefCell::new(HashMap::new()),
members: RefCell::new(HashMap::new()),
spaces,
messaging_provider: None,
store: RefCell::new(None),
}
}
// TODO: Use a factory instead...
pub async fn from_matrix_room(matrix_room: &MatrixRoom) -> Self {
// let room_topic = matrix_room.topic().map(RefCell::new);
let id = RoomId::from(matrix_room.room_id());
let name = matrix_room.name();
let room_topic = matrix_room.topic();
let is_direct = match matrix_room.is_direct().await {
Ok(is_direct) => Some(is_direct),
Err(err) => {
error!("Unable to know if the room \"{id}\" is direct: {err}");
None
}
};
let state = Some(matrix_room.state());
Self::new(id, name, room_topic, is_direct, state)
// room.timeline.subscribe().await
// Arc::new(matrix_room.to_owned()),
pub fn set_messaging_provider(
&mut self,
messaging_provider: Rc<dyn RoomMessagingProviderInterface>,
) {
self.messaging_provider = Some(messaging_provider);
}
pub fn id(&self) -> &OwnedRoomId {
pub fn set_store(&self, store: Rc<dyn RoomStoreProviderInterface>) {
*self.store.borrow_mut() = Some(store);
}
pub fn id(&self) -> &RoomId {
&self.id
}
pub fn name(&self) -> &Option<String> {
&self.name
#[allow(dead_code)]
pub fn name(&self) -> Option<String> {
self.name.borrow().clone()
}
pub fn topic(&self) -> &Option<String> {
&self.topic
}
#[allow(dead_code)]
pub fn set_topic(&mut self, topic: Option<String>) {
self.topic = topic;
}
pub fn is_direct(&self) -> &Option<bool> {
&self.is_direct
}
#[allow(dead_code)]
pub fn state(&self) -> &Option<MatrixRoomState> {
&self.state
}
#[allow(dead_code)]
pub fn is_invited(&self) -> Option<bool> {
match self.state {
Some(state) => Some(state == MatrixRoomState::Invited),
None => None,
self.state.map(|state| state == MatrixRoomState::Invited)
}
#[instrument(name = "Room", skip_all)]
fn add_invitation(&self, invitation: Invitation) {
self.members.borrow_mut().remove(&invitation.invitee_id);
self.invitations
.borrow_mut()
.insert(invitation.invitee_id.clone(), invitation.clone());
if let Some(store) = self.store.borrow().as_ref() {
store.on_invitation(invitation);
}
}
#[instrument(name = "Room", skip_all)]
fn add_member(&self, member: RoomMember) {
let mut members = self.members.borrow_mut();
members.insert(member.id().clone(), member.clone());
// USe the member display name to name the room if it's direct and has no name set.
if self.name.borrow().is_none() && members.len() == 1 {
if let Some(member_display_name) = member.display_name() {
let name = Some(member_display_name.clone());
self.name.borrow_mut().clone_from(&name);
if let Some(store) = self.store.borrow().as_ref() {
store.on_new_name(name);
}
}
}
if let Some(store) = self.store.borrow().as_ref() {
store.on_new_member(member);
}
}
pub async fn get_avatar(&self) -> Option<Avatar> {
if self.avatar.borrow().is_none() {
if let Some(requester) = &self.messaging_provider {
let resp = requester.get_avatar(&self.id).await;
if let Ok(avatar) = resp {
if let Some(avatar) = avatar {
return Some(avatar);
} else {
debug!("The room has no avatar... let's generate one");
match self.gen_room_avatar_with_members().await {
Ok(avatar) => {
if let Some(avatar) = avatar {
return Some(avatar);
}
}
Err(err) => {
error!("err={}", err);
}
}
}
} else {
error!("err={:?}", resp);
}
}
}
self.avatar.borrow().clone()
}
#[instrument(name = "Room", skip_all)]
async fn gen_room_avatar_with_members(&self) -> anyhow::Result<Option<Avatar>> {
let mut account_member = None::<&RoomMember>;
let mut other_members = Vec::<&RoomMember>::new();
let members = self.members.borrow();
for member in members.values() {
if member.is_account_user() {
account_member = Some(member);
} else {
other_members.push(member);
}
}
let other_avatars_futures =
join_all(other_members.iter().map(|member| member.get_avatar()));
let (other_avatars, account_avatar) = if let Some(account_member) = account_member {
join(other_avatars_futures, account_member.get_avatar()).await
} else {
(
join_all(other_members.iter().map(|member| member.get_avatar())).await,
None,
)
};
let other_avatars: Vec<Vec<u8>> = other_avatars.into_iter().flatten().collect();
if account_avatar.is_some() || !other_avatars.is_empty() {
let _guard = debug_span!("AvatarRendering").entered();
Ok(Some(
create_mozaik(256, 256, other_avatars, account_avatar).await,
))
} else {
Ok(None)
}
}
}
pub type ByIdRooms = HashMap<OwnedRoomId, RefCell<Room>>;
#[async_trait(?Send)]
impl RoomMessagingConsumerInterface for Room {
#[instrument(name = "Room", skip_all)]
async fn on_invitation(&self, invitation: Invitation) {
trace!("on_invitation");
let sender_id = invitation.sender_id.clone();
// pub type ByIdRooms = HashMap<OwnedRoomId, RefCell<Room>>;
self.add_invitation(invitation);
// #[derive(Clone)]
// pub struct Room {
// // pub matrix_room: Arc<MatrixRoom>,
// pub topic: Option<RefCell<String>>,
// pub members: HashMap<OwnedUserId, RoomMember>,
// pub is_direct: Option<bool>,
// // pub timeline: Arc<Timeline>,
// }
if self.is_direct.unwrap_or(false) {
debug!("1to1 conversation, using the {} avatar", &sender_id);
if let Ok(avatar) = self.gen_room_avatar_with_members().await {
debug!("Avatar successfully generated");
self.avatar.borrow_mut().clone_from(&avatar);
if let Some(store) = self.store.borrow().as_ref() {
store.on_new_avatar(avatar);
}
}
}
}
// impl Room {
// pub async fn new(
// matrix_room: Arc<MatrixRoom>,
// topic: Option<RefCell<String>>,
// is_direct: Option<bool>,
// ) -> Self {
// // TODO: Filter events
// // let timeline = Arc::new(matrix_room.timeline_builder().build().await.ok().unwrap());
// Self {
// matrix_room,
// topic,
// members: HashMap::new(),
// is_direct,
// // timeline,
// }
// }
#[instrument(name = "Room", skip_all)]
async fn on_membership(&self, member: RoomMember) {
trace!("on_membership");
self.add_member(member);
}
// pub async fn from_matrix_room(matrix_room: &MatrixRoom) -> Self {
// let room_topic = matrix_room.topic().map(RefCell::new);
#[instrument(name = "Room", skip_all)]
async fn on_new_topic(&self, _topic: Option<String>) {
trace!("on_new_topic");
}
// Self::new(
// Arc::new(matrix_room.to_owned()),
// room_topic,
// matrix_room.is_direct().await.ok(),
// )
// .await
// // room.timeline.subscribe().await
// }
#[instrument(name = "Room", skip_all)]
async fn on_new_name(&self, _name: Option<String>) {
trace!("on_new_name");
}
// pub fn name(&self) -> Option<String> {
// self.matrix_room.name()
// }
#[instrument(name = "Room", skip_all)]
async fn on_new_avatar(&self, avatar: Option<Avatar>) {
trace!("on_new_avatar");
self.avatar.borrow_mut().clone_from(&avatar);
if let Some(store) = self.store.borrow().as_ref() {
store.on_new_avatar(avatar);
}
}
}
// pub fn id(&self) -> OwnedRoomId {
// OwnedRoomId::from(self.matrix_room.room_id())
// }
// }
#[async_trait(?Send)]
impl RoomStoreConsumerInterface for Room {
fn id(&self) -> &RoomId {
&self.id
}
// impl PartialEq for Room {
// fn eq(&self, other: &Self) -> bool {
// // TODO: Look for a better way to compare Matrix rooms
// self.matrix_room.room_id() == other.matrix_room.room_id()
// }
// }
fn is_direct(&self) -> Option<bool> {
self.is_direct
}
fn name(&self) -> Option<String> {
self.name.borrow().clone()
}
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;
}
}
}

View File

@@ -0,0 +1,93 @@
use std::{
cell::RefCell,
fmt::{Debug, Formatter},
rc::Rc,
};
use matrix_sdk::ruma::OwnedMxcUri;
use tracing::error;
use super::{
common::{Avatar, UserId},
messaging_interface::MemberMessagingProviderInterface,
room::RoomId,
};
pub type AvatarUrl = OwnedMxcUri;
#[derive(Clone)]
pub struct RoomMember {
id: UserId,
display_name: Option<String>,
avatar_url: Option<AvatarUrl>,
room_id: RoomId,
is_account_user: bool,
#[allow(dead_code)]
avatar: RefCell<Option<Avatar>>,
messaging_provider: Rc<dyn MemberMessagingProviderInterface>,
}
impl RoomMember {
pub fn new(
id: UserId,
display_name: Option<String>,
avatar_url: Option<AvatarUrl>,
room_id: RoomId,
is_account_user: bool,
messaging_provider: Rc<dyn MemberMessagingProviderInterface>,
) -> Self {
Self {
id,
display_name,
avatar_url,
room_id,
is_account_user,
avatar: RefCell::new(None),
messaging_provider,
}
}
pub fn id(&self) -> &UserId {
&self.id
}
pub fn display_name(&self) -> &Option<String> {
&self.display_name
}
#[allow(dead_code)]
pub fn room_id(&self) -> &RoomId {
&self.room_id
}
pub fn is_account_user(&self) -> bool {
self.is_account_user
}
pub async fn get_avatar(&self) -> Option<Avatar> {
match self
.messaging_provider
.get_avatar(
self.avatar_url.clone(),
self.room_id.clone(),
self.id.clone(),
)
.await
{
Ok(avatar) => avatar,
Err(err) => {
error!("err={}", err);
None
}
}
}
}
impl Debug for RoomMember {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
f.debug_struct("RoomMember").field("id", &self.id).finish()
}
}

107
src/domain/model/space.rs Normal file
View 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()
}
}

View 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>) {}
}

View 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(),
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
pub(crate) mod account_event;
pub(crate) mod client;
pub(crate) mod requester;
pub(crate) mod room_event;
pub(crate) mod worker_tasks;

View File

@@ -1,64 +1,390 @@
use std::cell::RefCell;
use std::sync::Arc;
use std::{collections::HashMap, rc::Rc};
use matrix_sdk::Client as MatrixClient;
use tokio::sync::broadcast::Receiver;
use tokio::sync::mpsc::UnboundedSender;
use async_trait::async_trait;
use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId};
use tokio::{
select,
sync::{broadcast::Receiver, mpsc::UnboundedSender},
};
use tokio_stream::{wrappers::BroadcastStream, StreamExt, StreamMap};
use tracing::{error, instrument, Instrument};
use super::client::RoomEvent;
use super::worker_tasks::{LoginStyle, WorkerTask};
use crate::utils::oneshot;
use super::{
account_event::AccountEvent,
room_event::RoomEvent,
worker_tasks::{LoginStyle, WorkerTask},
};
use crate::{
domain::model::{
common::{Avatar, UserId},
messaging_interface::{
AccountMessagingConsumerInterface, AccountMessagingProviderInterface,
MemberMessagingProviderInterface, RoomMessagingConsumerInterface,
RoomMessagingProviderInterface, SpaceMessagingConsumerInterface,
SpaceMessagingProviderInterface,
},
room::{Invitation, Room, RoomId},
room_member::{AvatarUrl, RoomMember},
space::Space,
},
utils::oneshot,
};
pub struct Receivers {
pub room_receiver: RefCell<Receiver<RoomEvent>>,
pub struct Requester {
worker_tasks_sender: UnboundedSender<WorkerTask>,
}
impl Clone for Receivers {
impl Clone for Requester {
fn clone(&self) -> Self {
Self {
room_receiver: RefCell::new(self.room_receiver.borrow().resubscribe()),
worker_tasks_sender: self.worker_tasks_sender.clone(),
}
}
}
impl PartialEq for Receivers {
fn eq(&self, other: &Self) -> bool {
self.room_receiver
.borrow()
.same_channel(&other.room_receiver.borrow())
impl Requester {
pub fn new(worker_tasks_sender: UnboundedSender<WorkerTask>) -> Self {
Self {
worker_tasks_sender,
}
}
}
pub struct Requester {
pub matrix_client: Arc<MatrixClient>,
pub tx: UnboundedSender<WorkerTask>,
pub receivers: Receivers,
// TODO: Is there a way to avoid this duplication?
macro_rules! request_to_worker {
($self:ident, $task:expr) => {
{
let (reply, mut response) = oneshot();
let task = $task(reply);
if let Err(err) = $self.worker_tasks_sender.send(task) {
let msg = format!("Unable to request to the Matrix client: {err}");
return Err(anyhow::Error::msg(msg));
}
match response.recv().await {
Some(result) => result,
None => Err(anyhow::Error::msg("TBD")),
}
}
};
($self:ident, $task:expr $(, $arg:expr)+) => {
{
let (reply, mut response) = oneshot();
let task = $task($($arg),*, reply);
if let Err(err) = $self.worker_tasks_sender.send(task) {
let msg = format!("Unable to request to the Matrix client: {err}");
return Err(anyhow::Error::msg(msg));
}
match response.recv().await {
Some(result) => result,
None => Err(anyhow::Error::msg("TBD")),
}
}
};
}
impl Requester {
pub async fn init(&self) -> anyhow::Result<()> {
let (reply, mut response) = oneshot();
if let Err(err) = self.tx.send(WorkerTask::Init(reply)) {
let msg = format!("Unable to request the init of the Matrix client: {err}");
return Err(anyhow::Error::msg(msg));
}
match response.recv().await {
Some(result) => Ok(result),
None => Err(anyhow::Error::msg("TBD")),
}
request_to_worker!(self, WorkerTask::Init)
}
pub async fn login(&self, style: LoginStyle) -> anyhow::Result<()> {
let (reply, mut response) = oneshot();
request_to_worker!(self, WorkerTask::Login, style)
}
if let Err(err) = self.tx.send(WorkerTask::Login(style, reply)) {
#[instrument(skip_all)]
async fn on_room_invitation(
consumer: &Rc<dyn RoomMessagingConsumerInterface>,
user_id: OwnedUserId,
sender_id: OwnedUserId,
is_account_user: bool,
) {
let invitation = Invitation::new(user_id, sender_id, is_account_user);
consumer.on_invitation(invitation).await;
}
#[instrument(skip_all)]
async fn on_room_join(
consumer: &Rc<dyn RoomMessagingConsumerInterface>,
room_id: OwnedRoomId,
user_id: OwnedUserId,
user_name: Option<String>,
avatar_url: Option<OwnedMxcUri>,
is_account_user: bool,
messaging_provider: Rc<dyn MemberMessagingProviderInterface>,
) {
let member = RoomMember::new(
UserId::from(user_id),
user_name,
avatar_url,
room_id,
is_account_user,
messaging_provider,
);
consumer.on_membership(member).await;
}
#[instrument(skip_all)]
async fn on_room_new_topic(
consumer: &Rc<dyn RoomMessagingConsumerInterface>,
topic: Option<String>,
) {
consumer.on_new_topic(topic).await;
}
#[instrument(skip_all)]
async fn on_room_new_name(
consumer: &Rc<dyn RoomMessagingConsumerInterface>,
name: Option<String>,
) {
consumer.on_new_name(name).await;
}
#[instrument(skip_all)]
async fn on_room_new_avatar(
consumer: &Rc<dyn RoomMessagingConsumerInterface>,
avatar: Option<Avatar>,
) {
consumer.on_new_avatar(avatar).await;
}
#[instrument(skip_all)]
async fn on_space_new_child(
consumer: &Rc<dyn SpaceMessagingConsumerInterface>,
child_id: RoomId,
) {
// TODO: Make name consistent
consumer.on_child(child_id).await;
}
#[instrument(skip_all)]
async fn on_space_new_topic(
consumer: &Rc<dyn SpaceMessagingConsumerInterface>,
topic: Option<String>,
) {
consumer.on_new_topic(topic).await;
}
#[instrument(skip_all)]
async fn on_space_new_name(
consumer: &Rc<dyn SpaceMessagingConsumerInterface>,
name: Option<String>,
) {
consumer.on_new_name(name).await;
}
// #[instrument(name="SpaceAvatar", skip_all, fields(s = %space_id, a = avatar.is_some()))]
// async fn on_space_new_avatar(
// consumer: &Rc<dyn SpaceMessagingConsumerInterface>,
// space_id: OwnedRoomId,
// avatar: Option<Avatar>,
// ) {
// consumer.on_new_avatar(avatar).await;
// }
}
#[async_trait(?Send)]
impl AccountMessagingProviderInterface for Requester {
async fn get_display_name(&self) -> anyhow::Result<Option<String>> {
request_to_worker!(self, WorkerTask::GetDisplayName)
}
async fn get_avatar(&self) -> anyhow::Result<Option<Avatar>> {
request_to_worker!(self, WorkerTask::GetAvatar)
}
async fn run_forever(
&self,
account_events_consumer: &dyn AccountMessagingConsumerInterface,
mut account_events_receiver: Receiver<AccountEvent>,
) -> anyhow::Result<()> {
// TODO: manage the result provided by response
let (run_forever_tx, _run_forever_rx) = oneshot();
if let Err(err) = self
.worker_tasks_sender
.send(WorkerTask::RunForever(run_forever_tx))
{
let msg = format!("Unable to request login to the Matrix client: {err}");
return Err(anyhow::Error::msg(msg));
}
match response.recv().await {
Some(result) => result,
None => Err(anyhow::Error::msg("TBD")),
let mut rooms_events_streams = StreamMap::new();
let mut spaces_events_streams = StreamMap::new();
let mut room_events_consumers =
HashMap::<RoomId, Rc<dyn RoomMessagingConsumerInterface>>::new();
let mut space_events_consumers =
HashMap::<RoomId, Rc<dyn SpaceMessagingConsumerInterface>>::new();
// TODO: Fix this...
let client = Rc::new(self.clone());
loop {
select! {
res = account_events_receiver.recv() => {
if let Ok(account_event) = res {
match account_event {
AccountEvent::NewRoom(
id,
spaces,
name,
topic,
is_direct,
state,
receiver,
new_room_tx,
span
) => {
let mut room = Room::new(id, name, topic, is_direct, Some(state), spaces);
let room_id = room.id().clone();
room.set_messaging_provider(client.clone());
let room = Rc::new(room);
let stream = BroadcastStream::new(receiver.into());
rooms_events_streams.insert(room_id.clone(), stream);
let room_events_consumer = account_events_consumer.on_new_room(room)
.instrument(span)
.await;
room_events_consumers.insert(room_id, room_events_consumer);
// We're now ready to recv and compute RoomEvent.
new_room_tx.send(true).await;
},
AccountEvent::NewSpace(id, name, topic, receiver, new_space_tx, span) => {
let mut space = Space::new(id, name, topic);
let space_id = space.id().clone();
space.set_messaging_provider(client.clone());
let space = Rc::new(space);
let stream = BroadcastStream::new(receiver.into());
spaces_events_streams.insert(space_id.clone(), stream);
let space_events_consumer = account_events_consumer.on_new_space(space)
.instrument(span)
.await;
space_events_consumers.insert(space_id, space_events_consumer);
// We're now ready to recv and compute SpaceEvent.
new_space_tx.send(true).await;
},
};
}
},
Some((room_id, room_event)) = rooms_events_streams.next() => {
if let Ok(room_event) = room_event {
if let Some(consumer) = room_events_consumers.get(&room_id) {
match room_event {
RoomEvent::Invitation(user_id, sender_id, is_account_user, span) => {
Self::on_room_invitation(consumer, user_id, sender_id, is_account_user)
.instrument(span)
.await;
},
RoomEvent::Join(user_id, user_name, avatar_url, is_account_user, span) => {
Self::on_room_join(
consumer,
room_id,
user_id,
user_name,
avatar_url,
is_account_user,
client.clone())
.instrument(span)
.await;
},
RoomEvent::NewTopic(topic, span) => {
Self::on_room_new_topic(consumer, topic)
.instrument(span)
.await;
},
RoomEvent::NewName(name, span) => {
Self::on_room_new_name(consumer, name)
.instrument(span)
.await;
},
RoomEvent::NewAvatar(avatar, span) => {
Self::on_room_new_avatar(consumer, avatar)
.instrument(span)
.await;
}
// RoomEvent::NewAvatar(avatar) => Self::on_room_new_avatar(consumer, avatar).await,
_ => error!("TODO: {:?}", &room_event),
}
} else {
error!("No consumer found for {} room", &room_id);
}
}
},
Some((space_id, room_event)) = spaces_events_streams.next() => {
if let Ok(room_event) = room_event {
if let Some(consumer) = space_events_consumers.get(&space_id) {
match room_event {
RoomEvent::NewTopic(topic, span) => {
Self::on_space_new_topic(consumer, topic)
.instrument(span)
.await;
},
RoomEvent::NewName(name, span) => {
Self::on_space_new_name(consumer, name)
.instrument(span)
.await;
},
RoomEvent::NewChild(child_id, span) => {
Self::on_space_new_child(consumer, child_id)
.instrument(span)
.await;
},
_ => error!("TODO: {:?}", &room_event),
}
} else {
error!("No consumer found for {} space", &space_id);
}
}
}
}
}
}
}
#[async_trait(?Send)]
impl RoomMessagingProviderInterface for Requester {
async fn get_avatar(&self, room_id: &RoomId) -> anyhow::Result<Option<Avatar>> {
request_to_worker!(self, WorkerTask::GetRoomAvatar, room_id.clone())
}
async 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
)
}
}

View 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
}
}

View File

@@ -1,19 +1,29 @@
use std::fmt::{Debug, Formatter};
use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId};
use crate::utils::Sender;
#[derive(Debug)]
pub enum LoginStyle {
// SessionRestore(Session),
Password(String, String),
}
pub enum WorkerTask {
// Init(AsyncProgramStore, ClientReply<()>),
// Init(ClientReply<()>),
Init(Sender<()>),
//Login(LoginStyle, ClientReply<EditInfo>),
Init(Sender<anyhow::Result<()>>),
Login(LoginStyle, Sender<anyhow::Result<()>>),
RunForever(Sender<()>),
GetDisplayName(Sender<anyhow::Result<Option<String>>>),
GetAvatar(Sender<anyhow::Result<Option<Vec<u8>>>>),
GetRoomAvatar(OwnedRoomId, Sender<anyhow::Result<Option<Vec<u8>>>>),
GetRoomMemberAvatar(
Option<OwnedMxcUri>,
OwnedRoomId,
OwnedUserId,
Sender<anyhow::Result<Option<Vec<u8>>>>,
),
JoinRoom(OwnedRoomId, Sender<anyhow::Result<bool>>),
}
impl Debug for WorkerTask {
@@ -24,11 +34,38 @@ impl Debug for WorkerTask {
.field(&format_args!("_"))
// .field(&format_args!("_"))
.finish(),
WorkerTask::RunForever(_) => f
.debug_tuple("WorkerTask::RunForever")
.field(&format_args!("_"))
.finish(),
WorkerTask::Login(style, _) => f
.debug_tuple("WorkerTask::Login")
.field(style)
// .field(&format_args!("_"))
.finish(),
WorkerTask::GetDisplayName(_) => f
.debug_tuple("WorkerTask::GetDisplayName")
.field(&format_args!("_"))
.finish(),
WorkerTask::GetAvatar(_) => f
.debug_tuple("WorkerTask::GetAvatar")
.field(&format_args!("_"))
.finish(),
WorkerTask::GetRoomAvatar(id, _) => f
.debug_tuple("WorkerTask::GetRoomAvatar")
.field(id)
.finish(),
WorkerTask::GetRoomMemberAvatar(room_id, user_id, avatar_url, _) => f
.debug_tuple("WorkerTask::GetRoomMemberAvatar")
.field(avatar_url)
.field(room_id)
.field(user_id)
.finish(),
WorkerTask::JoinRoom(room_id, _) => f
.debug_tuple("WorkerTask::JoinRoom")
.field(room_id)
.finish(),
}
}
}

View File

@@ -1 +1,2 @@
pub(crate) mod mozaik_builder;
pub(crate) mod random_svg_generators;

View 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()
}
}
}

View File

@@ -3,17 +3,19 @@ use std::future::Future;
use std::sync::OnceLock;
use std::{collections::HashMap, future::IntoFuture};
use log::error;
use rand::distributions::{Alphanumeric, DistString};
use rand::distr::{Alphanumeric, SampleString};
use reqwest::Result as RequestResult;
use tracing::error;
#[cfg(feature = "desktop")]
use tokio::fs::read_to_string;
cfg_if! {
if #[cfg(target_family = "wasm")] {
use web_sys;
} else {
use tokio::fs::read_to_string;
}
}
#[cfg(feature = "web")]
use web_sys;
#[derive(Eq, PartialEq, Hash)]
#[derive(Eq, Hash, PartialEq)]
pub enum AvatarFeeling {
Ok,
Warning,
@@ -116,13 +118,13 @@ async fn fetch_text(req: String) -> RequestResult<String> {
async fn fetch_dicebear_svg(
r#type: &DicebearType,
req_fields: &Vec<String>,
req_fields: &[String],
placeholder_fetcher: Option<Box<impl Future<Output = Option<String>>>>,
) -> String {
// TODO: Use configuration file
let url = "dicebear.tools.adrien.run";
let seed = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
let seed = Alphanumeric.sample_string(&mut rand::rng(), 16);
let type_str = r#type.to_string();
let url = format!(
"https://{url}/8.x/{type_str}/svg?seed={seed}&randomizeIds=true{}{}",
@@ -145,39 +147,38 @@ async fn fetch_dicebear_svg(
text.unwrap_or("".to_string())
}
#[cfg(feature = "desktop")]
fn gen_placeholder_fetcher<'a>(path: &'static str) -> Box<impl Future<Output = Option<String>>> {
let path = format!("./public/{}", &path);
Box::new(async move {
match read_to_string(&path).await {
Ok(content) => Some(content),
Err(err) => {
error!(
"Error during the access to the {path} file: {}",
err.to_string()
);
None
}
cfg_if! {
if #[cfg(target_family = "wasm")] {
fn gen_placeholder_fetcher<'a>(path: &'static str) -> Box<impl Future<Output = Option<String>>> {
Box::new(async move {
let url = format!("{}{}", web_sys::window().unwrap().origin(), path);
match fetch_text(url).await {
Ok(content) => Some(content),
Err(err) => {
error!("Error during {path} fetching: {}", err.to_string());
None
}
}
})
}
})
}
#[cfg(feature = "web")]
fn gen_placeholder_fetcher<'a>(path: &'static str) -> Box<impl Future<Output = Option<String>>> {
Box::new(async move {
let url = format!("{}{}", web_sys::window().unwrap().origin(), path);
match fetch_text(url).await {
Ok(content) => Some(content),
Err(err) => {
error!("Error during {path} fetching: {}", err.to_string());
None
}
}
else {
fn gen_placeholder_fetcher(path: &'static str) -> Box<impl Future<Output = Option<String>>> {
let path = format!("./public/{}", &path);
Box::new(async move {
match read_to_string(&path).await {
Ok(content) => Some(content),
Err(err) => {
error!(
"Error during the access to the {path} file: {}",
err.to_string()
);
None
}
}
})
}
})
}
#[cfg(not(any(feature = "desktop", feature = "web")))]
fn gen_placeholder_fetcher<'a>(_path: &'static str) -> Box<impl Future<Output = Option<String>>> {
Box::new(async move { None })
}
}
pub async fn generate_random_svg_avatar<'a>(config: Option<&'a AvatarConfig<'a>>) -> String {

View File

@@ -1,125 +1,152 @@
#![allow(non_snake_case)]
#[macro_use]
extern crate cfg_if;
mod domain;
mod infrastructure;
mod ui;
mod utils;
use std::rc::Rc;
use dioxus::document::{Link, Style};
use dioxus::prelude::*;
use futures_util::stream::StreamExt;
use tracing::{debug, error, warn};
use tracing_subscriber::{prelude::*, EnvFilter};
#[cfg(feature = "desktop")]
use dioxus::desktop::Config;
use crate::{
domain::model::{messaging_interface::AccountMessagingProviderInterface, session::Session},
infrastructure::messaging::matrix::{client::Client, worker_tasks::LoginStyle},
ui::{
layouts::{conversations::Conversations, login::Login},
ACCOUNT, SESSION,
},
};
use tracing::debug;
use tracing_subscriber::prelude::*;
cfg_if! {
if #[cfg(target_family = "wasm")] {
use tracing_web::MakeWebConsoleWriter;
} else {
use std::fs::File;
#[cfg(feature = "web")]
use tracing_web::MakeWebConsoleWriter;
use dioxus::desktop::Config;
use time::format_description::well_known::Iso8601;
use tracing_subscriber::fmt::time::UtcTime;
use tracing_forest::ForestLayer;
}
}
use crate::base::{login, sync_rooms};
use crate::base::{APP_SETTINGS, ROOMS, SESSION};
use crate::ui::components::login::Login;
use crate::ui::components::main_window::MainWindow;
use crate::ui::views::login_view::LoginView;
async fn login(mut rx: UnboundedReceiver<bool>, session: &GlobalSignal<Session>) {
while let Some(is_logged) = rx.next().await {
if !is_logged {
let homeserver_url = session.read().homeserver_url.clone();
let username = session.read().username.clone();
let password = session.read().password.clone();
mod base;
if homeserver_url.is_some() && username.is_some() && password.is_some() {
let (requester, account_events_receiver) =
Client::spawn(homeserver_url.unwrap()).await;
if let Err(err) = requester.init().await {
warn!("Unable to login: {}", err);
}
match requester
.login(LoginStyle::Password(username.unwrap(), password.unwrap()))
.await
{
Ok(_) => {
debug!("successfully logged");
session.write().is_logged = true;
let requester = Rc::new(requester);
dioxus::prelude::spawn(async move {
ACCOUNT.write().set_messaging_provider(requester.clone());
let _ = requester
.run_forever(&*ACCOUNT.read(), account_events_receiver)
.await;
});
}
Err(err) => {
error!("Error during login: {err}");
// TODO: Handle invalid login
// invalid_login.modify(|_| true);
return;
}
}
} else {
warn!("At least one of the following values is/are invalid: homeserver, username or password");
}
} else {
warn!("already logged... skip login");
}
}
}
fn app() -> Element {
debug!("*** App rendering ***");
let login_coro = use_coroutine(|rx| login(rx, &SESSION));
let login_coro = use_coroutine(|rx| login(rx, &APP_SETTINGS, &SESSION));
let is_logged = SESSION.read().is_logged;
let mut sync_rooms_coro = None;
if let Some(requester) = &APP_SETTINGS.read().requester {
sync_rooms_coro = Some(use_coroutine(|rx| {
sync_rooms(rx, requester.borrow().receivers.clone(), &ROOMS)
}));
}
if !SESSION.read().is_logged {
if !is_logged {
login_coro.send(false);
} else {
if let Some(coro) = sync_rooms_coro {
coro.send(true);
}
// if chats_win_state.read().is_none() {
// let chats_window = dioxus_desktop::use_window(cx);
// let receivers = app_settings
// .read()
// .requester
// .as_ref()
// .unwrap()
// .borrow()
// .receivers
// .clone();
// let chats_props = ChatsWindowProps {
// receivers,
// interface: chats_win_interface_ref.clone(),
// };
// let chats_dom = VirtualDom::new_with_props(ChatsWindow, chats_props);
// let window_cfg = Config::default().with_custom_head(
// r#"
// <style type="text/css">
// html, body {
// height: 100%;
// width: 100%;
// margin: 0;
// }
// #main, #bodywrap {
// height: 100%;
// width: 100%;
// }
// </style>
// "#
// .to_owned(),
// );
// let chats_window_desktop_service = chats_window.new_window(chats_dom, window_cfg);
// chats_win_state.set(Some(chats_window_desktop_service));
// }
}
if SESSION.read().is_logged {
debug!("Should render the MainWindow component");
rsx! {
MainWindow {},
rsx! {
Link {
href: asset!("public/fonts/Geist/Geist-Medium.woff2"), as: "font", type: "font/woff2"
}
} else {
rsx! {
LoginView {},
Style {
id: "style-head", "body {{ font-family: 'Geist'; font-weight: normal; }}"
}
if !is_logged {
Login {}
}
else {
Conversations {}
}
}
}
fn main() {
#[cfg(feature = "desktop")]
{
let fmt_layer = tracing_subscriber::fmt::layer()
.with_filter(tracing::level_filters::LevelFilter::DEBUG);
tracing_subscriber::registry().with(fmt_layer).init();
let mut builder = LaunchBuilder::new();
let config = Config::new().with_menu(None);
let builder = LaunchBuilder::new().with_cfg(config);
builder.launch(app);
let mut layers = Vec::new();
cfg_if! {
if #[cfg(target_family = "wasm")] {
let console_layer = tracing_subscriber::fmt::layer()
.with_ansi(false) // Only partially supported across browsers
.without_time() // std::time is not available in browsers, see note below
.with_writer(MakeWebConsoleWriter::new()) // write events to the console
.boxed();
layers.push(console_layer);
} else {
let config = Config::new().with_menu(None);
builder = builder.with_cfg(config);
let log_file = File::create("/tmp/bg92.log").unwrap();
let file_layer = tracing_subscriber::fmt::layer()
.with_ansi(false)
.with_writer(log_file)
.with_timer(UtcTime::new(Iso8601::DATE_TIME))
.boxed();
layers.push(file_layer);
let console_layer = ForestLayer::default().boxed();
layers.push(console_layer);
}
}
#[cfg(feature = "web")]
{
let fmt_layer = tracing_subscriber::fmt::layer()
.with_ansi(false) // Only partially supported across browsers
.without_time() // std::time is not available in browsers, see note below
.with_writer(MakeWebConsoleWriter::new()) // write events to the console
.with_filter(tracing::level_filters::LevelFilter::INFO);
tracing_subscriber::registry().with(fmt_layer).init(); // Install these as subscribers to tracing events
tracing_subscriber::registry()
.with(layers)
.with(EnvFilter::from_default_env())
.init();
let builder = LaunchBuilder::new();
builder.launch(app);
}
builder.launch(app);
}

View File

@@ -174,29 +174,37 @@ $border-big-width: 4px;
$border-big: solid $border-big-width $border-default-color;
$border-normal-width: 2px;
$border-normal: solid $border-normal-width $border-default-color;
$border-thin-width: 1px;
$border-thin: solid $border-thin-width $border-default-color;
// TODO: Radius should be a percentage(eg: 1024/16px).
$border-radius: 16px;
$geist-font-path: "../fonts/Geist";
$transition-duration: 300ms;
@font-face {
src: url("#{$geist-font-path}/Geist-Medium.woff2") format("woff2");
font-family: "Geist";
font-weight: normal;
}
@font-face {
src: url("#{$geist-font-path}/Geist-Bold.woff2") format("woff2");
font-family: "Geist";
font-weight: bold;
}
// Cf. https://css-tricks.com/box-sizing/
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
height: 100vh;
width: 100vw;
margin: 0px;
padding: 0px;
outline: 0px;
@@ -206,6 +214,11 @@ input {
font-family: inherit;
}
// Hide the preloader as soon as the application is loaded and ready to be rendered
#preloader {
display: none;
}
#main {
height: 100%;
width: 100%;
@@ -217,37 +230,3 @@ input {
::selection {
background-color: transparent;
}
// TODO: To remove once the design updated.
.aeroButton {
height: 50%;
min-height: 16px;
aspect-ratio: 1;
background-color: transparent;
border: 2px solid transparent;
background-size: contain !important;
margin-right: 1%;
}
.aeroButton:hover {
border-image: url(/public/images/aerobutton_border.png) 2 round;
}
.aeroButton:active {
border-image: url(/public/images/aerobutton_border_down.png) 2 round;
}
.button {
height: 50%;
min-height: 16px;
aspect-ratio: 1;
background-color: transparent;
border: 2px solid transparent;
background-size: contain !important;
margin-right: 1%;
}
.button:hover {
border-image: url(/public/images/button_border.png) 2 round;
}
.button:active {
border-image: url(/public/images/button_border_down.png) 2 round;
}

View File

@@ -1,14 +1,13 @@
@import "../base.scss";
$panel-aspect-ratio: 1/1.618;
$panel-padding-v: 5%;
$panel-padding-h: 5%;
$aspect-ratio: 0.618; // 1/1.618;
%panel {
padding: $panel-padding-v $panel-padding-h;
@mixin panel($padding-v: 2%, $padding-h: 2%) {
padding: $padding-v $padding-h;
height: 100%;
width: 100%;
height: calc(100% - $panel-padding-v - (2 * $border-big-width));
width: calc(100% - $panel-padding-h - (2 * $border-big-width));
flex-shrink: 0;
border: $border-big;

View File

@@ -1,54 +0,0 @@
use dioxus::prelude::*;
turf::style_sheet!("src/ui/components/avatar_selector.scss");
pub fn AvatarSelector() -> Element {
rsx! {
style { {STYLE_SHEET} },
div {
class: ClassName::AVATAR_SELECTOR,
svg {
view_box: "0 0 100 100",
linearGradient {
id: "avatar-gradient",
x1: 1,
y1: 1,
x2: 0,
y2: 0,
stop {
offset: "0%",
stop_color: "rgb(138, 191, 209)",
}
stop {
offset: "60%",
stop_color: "rgb(236, 246, 249)",
}
},
filter {
id: "avatar-shadow",
feDropShadow {
dx: 2,
dy: 2,
std_deviation: 3,
flood_opacity: 0.5,
},
},
rect {
x: "10",
y: "10",
width: "80",
height: "80",
rx: "12",
fill: "url('#avatar-gradient')",
filter: "url('#avatar-shadow')",
stroke: "grey",
},
},
img {
class: ClassName::AVATAR_SELECTOR_PICTURE,
src: "/public/images/default-avatar.png",
},
},
}
}

View File

@@ -1,17 +0,0 @@
.avatar-selector {
position: relative;
height: 100%;
aspect-ratio: 1;
&__picture {
$height: 65%;
$margin: calc(100% - $height) / 2;
position: absolute;
height: $height;
aspect-ratio: 1;
top: $margin;
right: $margin;
}
}

View File

@@ -3,7 +3,7 @@ use dioxus_free_icons::{Icon, IconShape};
turf::style_sheet!("src/ui/components/button.scss");
#[derive(PartialEq, Clone, Props)]
#[derive(Clone, PartialEq, Props)]
struct _ButtonProps {
children: Element,
#[props(default = false)]
@@ -15,7 +15,7 @@ struct _ButtonProps {
macro_rules! svg_text_icon {
($name:ident,$text:literal) => {
#[derive(Copy, Clone, PartialEq)]
#[derive(Clone, Copy, PartialEq)]
struct $name;
impl IconShape for $name {
fn view_box(&self) -> &str {
@@ -43,7 +43,7 @@ macro_rules! svg_text_button {
($name:ident,$style:ident,$icon:ident) => {
pub fn $name(props: ButtonProps) -> Element {
rsx! {
style { {STYLE_SHEET} },
style { {STYLE_SHEET} }
Button {
id: props.id,
@@ -67,7 +67,7 @@ macro_rules! svg_text_button {
};
}
#[derive(PartialEq, Clone, Props)]
#[derive(Clone, PartialEq, Props)]
pub struct ButtonProps {
#[props(default = false)]
focus: bool,
@@ -77,9 +77,9 @@ pub struct ButtonProps {
children: Element,
}
fn Button(props: ButtonProps) -> Element {
pub fn Button(props: ButtonProps) -> Element {
rsx! {
style { {STYLE_SHEET} },
style { {STYLE_SHEET} }
button {
id: props.id,
@@ -96,8 +96,8 @@ fn Button(props: ButtonProps) -> Element {
}
},
{props.children},
},
{props.children}
}
}
}
@@ -107,6 +107,12 @@ svg_text_button!(RegisterButton, REGISTER_BUTTON, RegisterText);
svg_text_icon!(LoginText, "LOGIN");
svg_text_button!(LoginButton, LOGIN_BUTTON, LoginText);
svg_text_icon!(JoinText, "JOIN");
svg_text_button!(JoinButton, JOIN_BUTTON, JoinText);
svg_text_icon!(RejectText, "REJECT");
svg_text_button!(RejectButton, REJECT_BUTTON, RejectText);
svg_text_icon!(SuccessText, "OK");
svg_text_button!(SuccessButton, SUCCESS_BUTTON, SuccessText);

View File

@@ -5,16 +5,22 @@
aspect-ratio: 3.5;
border: $border-normal;
border-radius: $border-radius;
border-radius: 5%;
color: get-color(greyscale, 0);
font-family: "Geist";
font-weight: bold;
// To center the inner svg
// TODO: Find a more efficient way to center
display: flex;
align-items: center;
justify-content: center;
svg {
height: 100%;
width: 100%;
height: 85%;
width: 85%;
text {
font-size: 50;
@@ -47,6 +53,14 @@
@include button(secondary, 90);
}
.join-button {
@include button(secondary, 90);
}
.reject-button {
@include button(critical, 90);
}
.success-button {
@include button(success, 100);
}

View File

@@ -0,0 +1,18 @@
use dioxus::prelude::*;
turf::style_sheet!("src/ui/components/chat_panel.scss");
#[component]
pub fn ChatPanel(name: String) -> Element {
rsx! {
style { {STYLE_SHEET} }
div {
class: ClassName::CHAT_PANEL,
div {
{name}
}
}
}
}

View File

@@ -0,0 +1,6 @@
@import "../_base.scss";
@import "./_panel.scss";
.chat-panel {
@include panel();
}

View File

@@ -1,94 +0,0 @@
@import "../../_base.scss"
.chats-window {
height: 100%;
width: 100%;
$horizontal-padding-margin: calc((2*100%)/1980);
.tabs {
height: 2%;
width: 100%;
display: flex;
flex-flow: row;
overflow-x: scroll;
&::-webkit-scrollbar {
height: 0px;
}
.tab {
height: 100%;
flex-grow: 1;
padding: 0 $horizontal-padding-margin;
display: flex;
button {
height: 100%;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
$clamped-horizontal-padding-margin: clamp(5px, $horizontal-padding-margin, $horizontal-padding-margin);
margin: 0 $clamped-horizontal-padding-margin;
padding: 0 $clamped-horizontal-padding-margin;
white-space: nowrap;
background-color: #EFF9F9;
border: $border-style;
$radius: calc((6*100%)/1980);
$clamped-radius: clamp(6px, $radius, $radius);
border-radius: $clamped-radius $clamped-radius 0 0;
font-size: $font-size;
img {
height: $icon-size;
aspect-ratio: 1;
}
}
}
}
.chat {
height: 98%;
width: 100%;
background-color: #ECF6F9;
.header {
height: 7%;
border: $border-style;
.info {
height: 45%;
display: flex;
flex-direction: column;
padding-left: 2%;
background: linear-gradient(180deg, #BFE3EB, #DEFBFE);
font-size: $font-size;
.room-name {
margin: 0;
margin-top: 1%;
font-weight: bold;
}
.room-topic {
margin: 0;
color: darkgrey;
}
}
}
}
}

View File

@@ -1,87 +0,0 @@
use dioxus::prelude::*;
use log::error;
use matrix_sdk::ruma::OwnedRoomId;
use super::edit_section::EditSection;
use crate::base::{sync_messages, ROOMS};
use crate::ui::components::avatar_selector::AvatarSelector;
use crate::ui::components::icons::DownArrowIcon;
turf::style_sheet!("src/ui/components/chats_window/conversation.scss");
#[component]
pub(super) fn Conversation(room_id: OwnedRoomId) -> Element {
error!("Conversation {} rendering", room_id);
let _sync_message_coro: Coroutine<()> =
use_coroutine(|_: UnboundedReceiver<_>| sync_messages(&ROOMS, room_id));
rsx! {
style { {STYLE_SHEET} },
div {
class: ClassName::CONVERSATION,
div {
class: ClassName::ROOM_EVENTS,
ul {
li {
class: ClassName::ROOM_EVENT,
div {
p {
class: ClassName::TITLE,
"MON POTE says:"
},
p {
class: ClassName::CONTENT,
"Coucou mon pote",
},
},
},
},
},
div {
class: ClassName::OTHER_AVATAR_SELECTOR_CONTAINER,
div {
class: ClassName::AVATAR_SELECTOR,
AvatarSelector {},
},
div {
class: ClassName::WEBCAM,
img {
src: "images/webcam.svg"
},
},
div {
class: ClassName::ARROW_ICON,
DownArrowIcon {}
},
},
div {
class: ClassName::HOLDER,
"••••••"
},
div {
class: ClassName::EDIT_SECTION,
EditSection {},
},
div {
class: ClassName::MY_AVATAR_SELECTOR_CONTAINER,
div {
class: ClassName::AVATAR_SELECTOR,
AvatarSelector {},
},
div {
class: ClassName::WEBCAM,
img {
src: "images/webcam.svg"
},
},
div {
class: ClassName::ARROW_ICON,
DownArrowIcon {}
},
},
},
}
}

View File

@@ -1,113 +0,0 @@
@import "../../_base.scss"
.conversation {
$padding-top: 2%;
height: calc(93% - $padding-top);
padding-left: 2%;
padding-top: $padding-top;
display: grid;
grid-template-columns: 75% 25%;
grid-template-rows: 70% 1% 29%;
cursor: pointer;
.holder {
display: flex;
justify-content: center;
align-items: center;
grid-column: 1;
grid-row: 2;
color: darkgrey;
}
.room-events {
display: flex;
flex-flow: column;
justify-content: flex-start;
border: $border-style;
background-color: #FFFFFF;
ul {
margin: 0;
padding-left: 0;
}
li {
list-style-type: none;
}
.room-event {
display: flex;
flex-flow: column;
justify-content: space-between;
padding-top: 1%;
font-size: $font-size;
.title {
margin: 0;
}
.content {
margin: 0;
padding-left: 2%;
}
}
}
%selector-container {
aspect-ratio: 1;
grid-column: 2;
display: grid;
grid-template-columns: 10% 15% 50% 15% 10%;
grid-template-rows: 80% 20%;
.avatar-selector {
grid-column-start: 1;
grid-column-end: 6;
aspect-ratio: 1;
}
.webcam {
grid-column: 2;
grid-row: 2;
aspect-ratio: 1;
}
.arrow-icon {
grid-column: 4;
grid-row: 2;
svg {
path:last-child {
fill: black;
}
}
}
}
.other-avatar-selector-container {
@extend %selector-container;
grid-row: 1;
}
.my-avatar-selector-container {
@extend %selector-container;
grid-row: 3;
}
.edit-section {
grid-row: 3;
}
}

View File

@@ -1,52 +0,0 @@
use dioxus::prelude::*;
turf::style_sheet!("src/ui/components/chats_window/edit_section.scss");
pub fn EditSection() -> Element {
rsx! {
style { {STYLE_SHEET} },
div {
class: ClassName::INPUT_AREA,
div {
class: ClassName::BUTTONS,
button {
"😀"
},
button {
"😉"
},
button {
"😴"
},
button {
"🔊"
},
},
textarea {
class: ClassName::EDIT,
placeholder: "Type your message here...",
},
div {
class: ClassName::CMD_BUTTONS,
button {
class: ClassName::SEND_BUTTON,
"Send"
},
button {
"🔎"
},
},
},
}
}

View File

@@ -1,55 +0,0 @@
@import "../../_base.scss"
.input-area {
height: 100%;
width: 100%;
margin-bottom: 2%;
.buttons {
$padding-top-bottom: 0.5%;
height: calc(10% - ($padding-top-bottom * 2));
padding-left: 2%;
padding-top: $padding-top-bottom;
padding-bottom: $padding-top-bottom;
display: flex;
flex-direction: row;
align-items: center;
border: $border-style;
background: linear-gradient(180deg, #F5FDFF, #E3ECF0, #F5FDFF);
button {
@extend .aeroButton;
height: $icon-size;
padding: 0;
margin: 0;
margin-right: 2%;
font-size: larger;
}
}
.edit {
height: 80%;
// Remove border from width
width: calc(100% - 2px);
padding: 0;
margin: 0;
}
.cmd-buttons {
height: 7%;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.send-button {
width: 15%;
}
}

View File

@@ -1,39 +0,0 @@
use std::cell::RefCell;
use matrix_sdk::ruma::OwnedRoomId;
use tokio::sync::broadcast::error::SendError;
use tokio::sync::broadcast::{channel, Receiver, Sender};
#[derive(Clone)]
pub enum Tasks {
ToggleRoom(OwnedRoomId),
}
pub struct Interface {
sender: Sender<Tasks>,
receiver: RefCell<Receiver<Tasks>>,
}
impl Interface {
pub fn new() -> Self {
let (sender, receiver) = channel::<Tasks>(32);
Self {
sender,
receiver: RefCell::new(receiver),
}
}
pub(super) fn receiver(&self) -> &RefCell<Receiver<Tasks>> {
&self.receiver
}
pub fn toggle_room(&self, room_id: OwnedRoomId) -> Result<usize, SendError<Tasks>> {
self.sender.send(Tasks::ToggleRoom(room_id))
}
}
impl Default for Interface {
fn default() -> Self {
Self::new()
}
}

View File

@@ -1,171 +0,0 @@
mod conversation;
mod edit_section;
mod navbar;
pub(crate) mod interface;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use dioxus::prelude::*;
use log::{debug, error};
use matrix_sdk::ruma::OwnedRoomId;
use tokio::sync::broadcast::Receiver;
use crate::base::{sync_rooms, ROOMS};
use crate::domain::model::room::Room;
use crate::infrastructure::messaging::matrix::requester::Receivers;
use conversation::Conversation;
use navbar::Navbar;
use interface::{Interface, Tasks};
turf::style_sheet!("src/ui/components/chats_window/chats_window.scss");
#[derive(Props, Clone, PartialEq)]
pub struct ChatsWindowProps {
pub receivers: Receivers,
pub interface: Signal<Interface>,
}
fn render_rooms_tabs(
by_id_rooms: &GlobalSignal<HashMap<OwnedRoomId, RefCell<Room>>>,
displayed_room_ids: Signal<HashSet<OwnedRoomId>>,
) -> Vec<Element> {
let rooms_ref = by_id_rooms.read();
let displayed_room_ids = displayed_room_ids.read();
rooms_ref
.values()
.filter(|room| displayed_room_ids.contains(room.borrow().id()))
.map(|room| {
let room = room.borrow();
let room_name = match room.name() {
Some(room_name) => room_name.clone(),
None => room.id().to_string(),
};
rsx!(
div {
class: ClassName::TAB,
button {
img {
src: "/public/images/status_online.png",
},
"{room_name}",
},
},
)
})
.collect()
}
fn render_rooms_conversations(
by_id_rooms: &GlobalSignal<HashMap<OwnedRoomId, RefCell<Room>>>,
displayed_room_ids: Signal<HashSet<OwnedRoomId>>,
) -> Vec<Element> {
let rooms_ref = by_id_rooms.read();
let displayed_room_ids = displayed_room_ids.read();
rooms_ref
.values()
.filter(|room| displayed_room_ids.contains(room.borrow().id()))
.map(|room| {
let room = room.borrow();
let room_id = room.id();
rsx!(Conversation {
room_id: room_id.clone()
},)
})
.collect()
}
async fn handle_controls(
receiver_ref: &RefCell<Receiver<Tasks>>,
mut displayed_room_ids: Signal<HashSet<OwnedRoomId>>,
) {
loop {
let result = receiver_ref.borrow_mut().recv().await;
match result {
Ok(task) => match task {
Tasks::ToggleRoom(room_id) => {
error!("ON TOGGLE ROOM {}", room_id);
let mut displayed_room_ids = displayed_room_ids.write();
match displayed_room_ids.take(&room_id) {
Some(_) => {
error!("{} room already dispayed... close it", room_id);
}
None => {
error!("{} room isn't dispayed... open it", room_id);
displayed_room_ids.insert(room_id);
}
}
}
},
Err(err) => error!("{}", err),
}
}
}
pub fn ChatsWindow(props: ChatsWindowProps) -> Element {
debug!("ChatsWindow rendering");
let receivers = &props.receivers;
let interface_ref = &props.interface;
let displayed_room_ids = use_signal(HashSet::<OwnedRoomId>::new);
let sync_rooms_coro = use_coroutine(|rx| {
to_owned![receivers];
sync_rooms(rx, receivers, &ROOMS)
});
sync_rooms_coro.send(true);
let _: Coroutine<()> = use_coroutine(|_: UnboundedReceiver<_>| {
to_owned![interface_ref, displayed_room_ids];
async move {
let interface = interface_ref.read();
let receiver = &interface.receiver();
handle_controls(receiver, displayed_room_ids).await
}
});
let rendered_rooms_tabs = render_rooms_tabs(&ROOMS, displayed_room_ids);
let rendered_rooms_conversations = render_rooms_conversations(&ROOMS, displayed_room_ids);
rsx! {
style { {STYLE_SHEET} },
div {
class: ClassName::CHATS_WINDOW,
div {
class: ClassName::TABS,
{rendered_rooms_tabs.into_iter()},
},
div {
class: ClassName::CHAT,
div {
class: ClassName::HEADER,
div {
class: ClassName::INFO,
p {
class: ClassName::ROOM_NAME,
"MON POTE",
},
p {
class: ClassName::ROOM_TOPIC,
"LE STATUT A MON POTE",
},
},
Navbar {},
},
{rendered_rooms_conversations.into_iter()},
},
},
}
}

View File

@@ -1,50 +0,0 @@
use dioxus::prelude::*;
use log::debug;
turf::style_sheet!("src/ui/components/chats_window/navbar.scss");
pub fn Navbar() -> Element {
debug!("Navbar rendering");
rsx! {
style { {STYLE_SHEET} },
div {
class: ClassName::NAVBAR,
button {
style: "background: url(/public/images/add_user2.png) center no-repeat",
},
button {
style: "background: url(/public/images/directory.png) center no-repeat",
},
button {
style: "background: url(/public/images/phone.png) center no-repeat",
},
button {
style: "background: url(/public/images/medias.png) center no-repeat",
},
button {
style: "background: url(/public/images/games.png) center no-repeat",
},
button {
style: "background: url(/public/images/ban_user.png) center no-repeat",
},
button {
class: ClassName::FLEX_RIGHT_AERO_BUTTON,
style: "background: url(/public/images/brush.png) center no-repeat",
},
button {
class: ClassName::FLEX_LAST_BUTTON,
style: "background: url(/public/images/settings.png) center no-repeat",
},
},
}
}

View File

@@ -1,26 +0,0 @@
@import "../../_base.scss"
.navbar {
height: 55%;
padding-left: 2%;
padding-right: 2%;
background: linear-gradient(180deg, #A9D3E0, #F0F9FA);
display: flex;
flex-direction: row;
align-items: center;
button {
@extend .aeroButton;
padding-right: 2%;
}
.flex-right-aero-button {
margin-left: auto;
}
.flex-last-button {
margin: 0;
}
}

View File

@@ -1,26 +0,0 @@
use std::rc::Rc;
use dioxus::prelude::*;
use log::debug;
use crate::ui::components::contacts_window::contacts_section::{
filter_people_conversations, filter_room_conversations, ContactsSection,
};
turf::style_sheet!("src/ui/components/contacts_window/contacts.scss");
pub fn Contacts() -> Element {
debug!("Contacts rendering");
// TODO: Test overflow
// TODO: Add offline users ?
rsx! {
style { {STYLE_SHEET} },
div {
class: ClassName::CONTACTS,
ContactsSection {name: "Groups", filter: Rc::new(filter_room_conversations)},
ContactsSection {name: "Available", filter: Rc::new(filter_people_conversations)},
},
}
}

View File

@@ -1,6 +0,0 @@
@import "../../_base.scss"
.contacts {
height: 72%;
background-color: white;
}

View File

@@ -1,149 +0,0 @@
use std::cell::RefCell;
use std::rc::Rc;
use dioxus::prelude::*;
use dioxus_free_icons::icons::io_icons::IoChevronDown;
use dioxus_free_icons::Icon;
use log::debug;
use crate::base::{CHATS_WIN_INTERFACE, ROOMS};
use crate::domain::model::room::{ByIdRooms, Room, RoomId};
use crate::ui::components::chats_window::interface::Interface as ChatsWindowInterface;
turf::style_sheet!("src/ui/components/contacts_window/contacts_section.scss");
fn ContactsArrow() -> Element {
rsx! {
style { {STYLE_SHEET} },
Icon {
icon: IoChevronDown,
},
}
}
static NO_NAME_REPR: &str = "No name";
static NO_SUBJECT_REPR: &str = "No subject";
pub(super) fn filter_people_conversations(
by_id_rooms: &GlobalSignal<ByIdRooms>,
) -> Vec<RefCell<Room>> {
let by_id_rooms = by_id_rooms.read();
let mut filtered_rooms = Vec::<RefCell<Room>>::with_capacity(by_id_rooms.len());
for room in by_id_rooms.values() {
let is_direct = room.borrow().is_direct().unwrap();
if !is_direct {
filtered_rooms.push(room.to_owned());
}
}
filtered_rooms
}
pub(super) fn filter_room_conversations(
by_id_rooms: &GlobalSignal<ByIdRooms>,
) -> Vec<RefCell<Room>> {
let by_id_rooms = by_id_rooms.read();
let mut filtered_rooms = Vec::<RefCell<Room>>::with_capacity(by_id_rooms.len());
for room in by_id_rooms.values() {
let is_direct = room.borrow().is_direct().unwrap();
if is_direct {
filtered_rooms.push(room.to_owned());
}
}
filtered_rooms
}
// TODO: Handle errors
fn on_clicked_room(room_id: &RoomId, chats_window_interface: &GlobalSignal<ChatsWindowInterface>) {
let _ = chats_window_interface.read().toggle_room(room_id.clone());
}
#[derive(Props, Clone)]
pub struct ContactsSectionProps {
name: String,
filter: Rc<dyn Fn(&GlobalSignal<ByIdRooms>) -> Vec<RefCell<Room>>>,
}
impl PartialEq for ContactsSectionProps {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && Rc::ptr_eq(&self.filter, &other.filter)
}
}
pub fn ContactsSection(props: ContactsSectionProps) -> Element {
debug!("ContactsSection rendering");
let contacts = props.filter.to_owned()(&ROOMS);
let contacts_len = contacts.len();
let mut show = use_signal(|| false);
let classes = [
ClassName::SECTION,
if *show.read() { ClassName::ACTIVE } else { "" },
]
.join(" ");
let rendered_contacts = contacts.into_iter().map(|room| {
let room = room.borrow();
let topic = room.topic().clone().unwrap_or("".to_string());
let name = match room.name() {
Some(name) => name.clone(),
None => NO_NAME_REPR.to_string(),
};
let id = room.id().clone();
let is_invited = room.is_invited().unwrap_or(false);
let formatted = format!(
"{name} - {}",
if is_invited {
"Invited - ".to_string()
} else {
"".to_string()
}
);
rsx! {
li {
onclick: move |_| on_clicked_room(&id, &CHATS_WIN_INTERFACE),
img {
src: "/public/images/status_online.png",
},
p {
{formatted},
},
p {
style: "color: darkgrey;",
{topic},
},
}
}
});
rsx! {
style { {STYLE_SHEET} },
div {
class: "{classes}",
p {
class: ClassName::HEADER,
onclick: move |_| {
let state = *show.read();
show.set(!state)
},
ContactsArrow {},
{format!("{} ({contacts_len})", props.name)},
},
ul {
{rendered_contacts.into_iter()},
},
},
}
}

View File

@@ -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
}
}

View File

@@ -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%;
}
}

View File

@@ -1,98 +0,0 @@
mod contacts;
mod contacts_section;
mod user_infos;
use dioxus::prelude::*;
use log::debug;
use crate::ui::components::contacts_window::contacts::Contacts;
use crate::ui::components::contacts_window::user_infos::UserInfos;
turf::style_sheet!("src/ui/components/contacts_window/contacts_window.scss");
pub fn ContactsWindow() -> Element {
debug!("ContactsWindow rendering");
rsx! {
style { {STYLE_SHEET} },
div {
class: ClassName::CONTACTS_WINDOW,
div {
class: ClassName::HEADER,
div {
class: ClassName::TITLE_BAR,
},
div {
class: ClassName::USER_INFO,
},
UserInfos {},
},
div {
class: ClassName::CONTACTS_NAV,
div {
class: ClassName::INNER,
button {
class: ClassName::AERO_BUTTON,
style: "background: url(/public/images/letter.png) center no-repeat",
},
button {
class: ClassName::AERO_BUTTON,
style: "background: url(/public/images/directory.png) no-repeat center",
},
button {
class: ClassName::AERO_BUTTON,
style: "background: url(/public/images/news.png) no-repeat center",
},
button {
class: ClassName::FLEX_RIGHT_AERO_BUTTON,
style: "background: url(/public/images/brush.png) no-repeat center",
},
button {
class: ClassName::AERO_BUTTON,
style: "background: url(/public/images/settings.png) no-repeat center",
},
},
},
div {
class: ClassName::SEARCH,
div {
class: ClassName::INNER,
input {
class: ClassName::SEARCH_INPUT,
placeholder: "Find a contact...",
r#type: "text",
},
button {
class: ClassName::BUTTON,
style: "background: url(/public/images/add_user.png) no-repeat center",
},
button {
class: ClassName::BUTTON,
style: "background: url(/public/images/tbc_transfert.png) no-repeat center",
},
},
},
Contacts {},
div {
class: ClassName::FOOTER,
},
},
}
}

View File

@@ -1,76 +0,0 @@
use dioxus::prelude::*;
use log::debug;
use crate::ui::components::avatar_selector::AvatarSelector;
use crate::ui::components::icons::DownArrowIcon;
turf::style_sheet!("src/ui/components/contacts_window/user_infos.scss");
static MESSAGE_PLACEHOLDER: &str = "<Enter a personal message>";
pub fn UserInfos() -> Element {
debug!("UserInfos rendering");
// let app_settings = use_atom_ref(cx, &APP_SETTINGS);
// let store = &app_settings.read().store;
// println!("----------------------------------");
// println!("UserInfos rendering");
// // println!("store={:?}", &store);
// dbg!(&store.user_id);
// println!("----------------------------------");
// let user_id = store.user_id..as_ref().unwrap();
// let mut user_info_option = None;
let user_display_name_option: Option<bool> = None;
let user_display_name = "AIE";
// let user_id_option = &store.user_id;
// if user_id_option.is_some() {
// let user_id = user_id_option.as_ref().unwrap();
// let user_info_option = store.user_infos.get(user_id);
// if user_info_option.is_some() {
// user_display_name_option = user_info_option.unwrap().display_name.as_ref();
// }
// }
rsx! {
style { {STYLE_SHEET} },
div {
class: ClassName::USER_INFO,
div {
class: ClassName::AVATAR_SELECTOR,
AvatarSelector {},
},
div {
class: ClassName::INFO_CONTAINER,
div {
class: ClassName::USER_ID,
p {
class: ClassName::USER_NAME,
if user_display_name_option.is_some() { "{user_display_name}" } else { "AIE" },
},
p {
class: ClassName::USER_STATUS,
"(Busy)",
},
DownArrowIcon {},
},
div {
class: ClassName::USER_MESSAGE,
p {
// TODO: Handle user message
{MESSAGE_PLACEHOLDER},
}
DownArrowIcon {},
},
},
},
}
}

View File

@@ -1,63 +0,0 @@
@import "../../_base.scss"
.userInfo {
position: relative;
height: 75%;
width: 99%;
top: -75%;
left: 1%;
aspect-ratio: 1;
z-index: 1;
display: flex;
flex-direction: row;
align-items: center;
.avatarSelector {
height: 100%;
aspect-ratio: 1;
}
.infoContainer {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
height: 100%;
width: 100%;
.userId {
@extend .aeroButton;
height: 30%;
width: fit-content;
display: flex;
text-align: begin;
align-items: center;
.userName {
display: inline-block;
width: fit-content;
color: white;
margin: 0;
}
.userStatus {
display: inline-block;
width: fit-content;
color: #B9DDE7;
}
}
.userMessage {
@extend .aeroButton;
width: fit-content;
height: 30%;
display: flex;
text-align: begin;
align-items: center;
margin: 0;
color: white;
}
}
}

View File

@@ -0,0 +1,568 @@
use std::{rc::Rc, time::Duration};
use base64::{engine::general_purpose, Engine as _};
use dioxus::prelude::*;
use futures_util::StreamExt;
use tracing::{debug, warn};
use super::{button::Button, icons::SearchIcon, text_input::TextInput};
use crate::{
domain::model::{common::PresenceState as DomainPresenceState, room::RoomId, space::SpaceId},
ui::{
components::{
button::{JoinButton, RejectButton},
icons::{ChatsIcon, LogoIcon, RoomsIcon, SpacesIcon},
},
hooks::use_long_press,
ACCOUNT, STORE,
},
};
turf::style_sheet!("src/ui/components/conversations.scss");
#[component]
fn AccountAvatar(content: Option<Vec<u8>>, class_name: Option<String>) -> Element {
rsx! {
if let Some(content) = content {
div {
class: class_name,
background_image: format!("url(data:image/jpeg;base64,{})", general_purpose::STANDARD.encode(content))
}
}
}
}
#[component]
fn PresenceState(state: Option<DomainPresenceState>, class_name: Option<String>) -> Element {
let class_name = class_name.unwrap_or("".to_string());
match state {
Some(state) => {
let state_class = match state {
DomainPresenceState::Online => ClassName::ONLINE,
DomainPresenceState::Offline => ClassName::OFFLINE,
DomainPresenceState::Unavailable => ClassName::UNAVAILABLE,
_ => ClassName::UNAVAILABLE,
};
let classes = [class_name.as_str(), state_class].join(" ");
rsx! {
div {
class: classes,
LogoIcon {}
}
}
}
None => VNode::empty(),
}
}
#[component]
fn DisplayName(display_name: Option<String>, class_name: Option<String>) -> Element {
match display_name {
Some(display_name) => {
rsx! {
div {
class: class_name,
p {
{display_name}
}
}
}
}
None => VNode::empty(),
}
}
#[component]
fn Status(status: Option<String>, class_name: Option<String>) -> Element {
let status = status.unwrap_or("Type your status".to_string());
rsx! {
div {
class: class_name,
TextInput {
placeholder: status,
}
}
}
}
pub fn Account() -> Element {
let avatar = use_resource(move || async move {
let account = ACCOUNT.read();
let avatar = account.get_avatar().await;
rsx! {
AccountAvatar {
class_name: ClassName::ACCOUNT_AVATAR,
content: avatar.borrow().clone(),
}
}
});
let presence_state = use_resource(move || async move {
// TODO: Fetch the state from the domain
rsx! {
PresenceState {
state: DomainPresenceState::Online,
class_name: ClassName::ACCOUNT_PRESENCE_STATE,
}
}
});
let display_name = use_resource(move || async move {
let account = ACCOUNT.read();
let display_name = account.get_display_name().await;
rsx! {
DisplayName {
class_name: ClassName::ACCOUNT_DISPLAY_NAME,
display_name: display_name.borrow().clone(),
}
}
});
let status = use_resource(move || async move {
// TODO: Fetch the status from the domain
rsx! {
Status {
class_name: ClassName::ACCOUNT_STATUS,
status: "Coucou, Je suis BG92".to_string(),
}
}
});
rsx! {
div {
class: ClassName::ACCOUNT,
{avatar}
{presence_state}
{display_name}
{status}
div {
class: ClassName::ACCOUNT_SPACES,
Button {
SpacesIcon {}
}
}
div {
class: ClassName::ACCOUNT_CHAT,
Button {
ChatsIcon {}
}
}
div {
class: ClassName::ACCOUNT_ROOM,
Button {
RoomsIcon {}
}
}
}
}
}
#[component]
pub fn ConversationAvatar(
room_id: RoomId,
on_selected: Option<EventHandler<RoomId>>,
on_pressed: Option<EventHandler<RoomId>>,
) -> Element {
let long_press_duration = Duration::from_millis(500);
let rooms = STORE.read().rooms();
let room = rooms.get(&room_id).unwrap().signal();
let room_id = Rc::new(room_id);
let room_name = room.name();
let selected_room_id = use_signal(|| None::<RoomId>);
let invited_badge = if room.is_invited() {
rsx! {
div {
class: ClassName::CONVERSATION_AVATAR_INVITED_BADGE,
p {
"Invited"
}
}
}
} else {
VNode::empty()
};
let is_selected = match selected_room_id.read().as_ref() {
Some(selected_room_id) => *selected_room_id == *room_id,
None => false,
};
let avatar = if let Some(content) = room.avatar() {
let encoded = general_purpose::STANDARD.encode(content);
rsx! {
div {
class: ClassName::CONVERSATION_AVATAR_IMAGE,
background_image: format!("url(data:image/jpeg;base64,{encoded})"),
}
}
} else {
let placeholder = room_name
.unwrap_or("?".to_string())
.to_uppercase()
.chars()
.next()
.unwrap_or('?')
.to_string();
debug!("Use of {} placeholder for {}", placeholder, room_id);
rsx! {
div {
class: ClassName::CONVERSATION_AVATAR_IMAGE,
{placeholder}
}
}
};
let classes = [
ClassName::CONVERSATION_AVATAR,
if is_selected { ClassName::SELECTED } else { "" },
];
let classes_str = classes.join(" ");
let on_press = {
let room_id = room_id.clone();
move || {
if let Some(c) = on_selected {
c.call(room_id.as_ref().clone())
}
}
};
let on_long_press = move || {
if let Some(c) = on_pressed {
c.call(room_id.as_ref().clone())
}
};
let long_press_hook = use_long_press(long_press_duration, on_press, on_long_press);
rsx! {
div {
class: "{classes_str}",
..long_press_hook.handlers,
{avatar}
{invited_badge}
}
}
}
#[component]
pub fn ConversationsCarousel(
on_selected_conversation: EventHandler<RoomId>,
on_pressed_conversation: EventHandler<RoomId>,
) -> Element {
let mut ordered_rooms = use_signal(Vec::<RoomId>::new);
use_effect(move || {
let rooms = use_context::<Signal<Vec<RoomId>>>();
let rooms = rooms.read();
for room in rooms.iter() {
if !ordered_rooms.peek().contains(room) {
ordered_rooms.push(room.clone());
}
}
ordered_rooms.retain(|room| rooms.contains(room));
});
let ordered_rooms = ordered_rooms.read();
let rendered_avatars = ordered_rooms.iter().map(|room| {
rsx! {
ConversationAvatar {
room_id: room.clone(),
on_selected: on_selected_conversation,
on_pressed: on_pressed_conversation,
}
}
});
rsx! {
div {
class: ClassName::SPACE_CONVERSATIONS_CAROUSEL,
// TODO: Needed?
onscroll: move |_| {
// Catch scrolling events.
},
{rendered_avatars}
}
}
}
// If id is None, the Space will handle all the Conversation which have no parent (Space).
#[component]
pub fn Space(id: Option<SpaceId>, on_pressed_conversation: EventHandler<RoomId>) -> Element {
let mut selected_room_id = use_context_provider(|| Signal::new(None::<RoomId>));
let mut displayed_rooms = use_context_provider(|| Signal::new(Vec::<RoomId>::new()));
let name = if let Some(id) = id {
let space = STORE.read().spaces().get(&id).unwrap().signal();
use_effect(move || {
let rooms = STORE.peek().rooms();
let room_ids = space.room_ids();
for room_id in room_ids {
if rooms.contains_key(&room_id) {
displayed_rooms.write().push(room_id);
}
}
});
space.name()
} else {
use_effect(move || {
let rooms = STORE.read().rooms();
for room in rooms.values() {
if room.signal().spaces().is_empty() {
let room_id = room.signal().id();
displayed_rooms.write().push(room_id);
}
}
});
Some("Home".to_string())
};
let on_selected_conversation = move |room_id: RoomId| {
STORE.write().on_selected_room(room_id.clone());
selected_room_id.set(Some(room_id));
};
let mut space_classes: [&str; 2] = [ClassName::SPACE, ""];
let mut selected_room_name = "".to_string();
if let Some(room_id) = selected_room_id.read().as_ref() {
space_classes[1] = ClassName::DISPLAY_CONVERSATION_NAME;
if let Some(room) = STORE.read().rooms().get(room_id) {
let room = room.signal();
if let Some(name) = room.name() {
selected_room_name = name;
} else {
debug!("No name set for {} room", &room_id);
selected_room_name = room_id.to_string();
}
} else {
warn!("No room found for the {} id", &room_id);
}
}
let classes_str = space_classes.join(" ");
rsx! {
div {
class: "{classes_str}",
// Deselect the conversation on clicks outside of the ConversationAvatar
onclick: move |_| {
selected_room_id.set(None);
},
div {
class: ClassName::SPACE_NAME,
p {
{name}
}
}
ConversationsCarousel {
on_selected_conversation,
on_pressed_conversation,
}
div {
class: ClassName::SPACE_CONVERSATION_NAME,
p {
{selected_room_name}
}
}
}
}
}
#[component]
pub fn Spaces(on_pressed_conversation: EventHandler<RoomId>) -> Element {
let spaces = STORE.read().spaces();
let space_ids = spaces.keys().clone().last();
let rendered_spaces = space_ids.map(|id| {
rsx! {
Space { id: id.clone(), on_pressed_conversation }
}
});
rsx! {
div {
class: ClassName::SPACES,
{rendered_spaces}
Space { on_pressed_conversation }
}
}
}
#[component]
pub fn Search() -> Element {
rsx! {
div {
class: ClassName::SEARCH,
div {
class: ClassName::SEARCH_TEXT,
TextInput {}
}
div {
class: ClassName::SEARCH_BUTTON,
Button {
SearchIcon {}
}
}
}
}
}
#[derive(PartialEq)]
enum ConversationOptionsMenuActions {
Join(RoomId),
Close,
}
#[component]
fn ConversationOptionsMenu(
room_id: RoomId,
callbacks: Coroutine<ConversationOptionsMenuActions>,
) -> Element {
let room = STORE.read().rooms().get(&room_id).unwrap().signal();
let topic = room.topic().unwrap_or("<No topic set>".to_string());
rsx! {
div {
class: ClassName::CONVERSATION_OPTIONS_MENU,
div {
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER,
div {
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_AVATAR,
ConversationAvatar { room_id: room_id.clone() }
}
div {
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_NAME,
p {
{room.name()}
}
}
div {
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_TOPIC,
p {
{topic}
}
}
div {
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_CONFIG,
p {
"Coming soon..."
}
}
div {
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_CLOSE_BUTTON,
RejectButton {
onclick: move |_| {
callbacks.send(ConversationOptionsMenuActions::Close);
}
}
}
div {
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_JOIN_BUTTON,
JoinButton {
onclick: move |_| {
callbacks.send(ConversationOptionsMenuActions::Join(room_id.clone()));
callbacks.send(ConversationOptionsMenuActions::Close);
}
}
}
}
}
}
}
pub fn Conversations() -> Element {
let mut room_id = use_signal(|| None::<RoomId>);
let on_pressed_conversation = move |id: RoomId| {
room_id.set(Some(id));
};
let callbacks = use_coroutine(
move |mut rx: UnboundedReceiver<ConversationOptionsMenuActions>| async move {
while let Some(action) = rx.next().await {
match action {
ConversationOptionsMenuActions::Join(room_id) => {
let rooms = STORE.read().rooms();
if let Some(room) = rooms.get(&room_id) {
room.join().await;
}
}
ConversationOptionsMenuActions::Close => {
room_id.set(None);
}
}
}
},
);
let menu = match room_id.read().as_ref() {
Some(room_id) => {
let room_id = room_id.clone();
rsx! {
ConversationOptionsMenu { room_id, callbacks }
}
}
None => VNode::empty(),
};
rsx! {
style { {STYLE_SHEET} }
div {
class: ClassName::CONVERSATIONS,
div {
class: ClassName::CONVERSATIONS_ACCOUNT,
Account {}
}
div {
class: ClassName::CONVERSATIONS_SPACES,
Spaces { on_pressed_conversation }
}
div {
class: ClassName::CONVERSATIONS_SEARCH,
Search {}
}
}
{menu}
}
}

View File

@@ -0,0 +1,440 @@
@import "../base.scss";
@import "./_panel.scss";
@import "./button.scss";
@mixin button-class {
button {
@include button(secondary, 90);
width: 100%;
max-height: 128px;
}
}
@mixin extra-marged-button() {
@include button-class();
}
.account {
$colum-spacing: 5%;
$col-width: 8.75%;
$button-width: 20%;
$button-height: calc(100% / 3);
$buttons-row-margin-top: 10%;
height: 100%;
width: 100%;
display: grid;
grid-template-columns: auto $colum-spacing repeat(2, calc($button-width/2)) $colum-spacing $button-width $colum-spacing $button-width;
grid-template-rows: 30% auto $button-height;
row-gap: 5%;
grid-template-areas:
"avatar . state name name name name name"
"avatar . status status status status status status"
"avatar . spaces spaces . chat . room"
;
&__avatar {
grid-area: avatar;
display: flex;
align-items: center;
justify-content: center;
border: $border-normal;
border-radius: $border-radius;
background-size: cover;
background-position: center;
}
&__presence-state {
grid-area: state;
svg {
height: 100%;
width: 100%;
stroke: get-color(greyscale, 90);
}
&.online {
svg {
fill: get-color(primary, 100);
}
}
&.offline {
svg {
fill: get-color(ternary, 100);
}
}
&.unavailable {
svg {
fill: get-color(greyscale, 80);
}
}
}
&__display-name {
grid-area: name;
display: flex;
align-items: center;
justify-content: center;
p {
font-size: 2.5vh;
margin: 0;
text-align: center;
}
}
&__status {
grid-area: status;
}
&__spaces {
grid-area: spaces;
@include extra-marged-button();
}
&__chat {
grid-area: chat;
@include extra-marged-button();
}
&__room {
grid-area: room;
@include extra-marged-button();
}
}
.spaces {
$gap: 1%;
$spaces-to-display: 5;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
gap: $gap;
overflow-y: scroll;
// TODO: Manage android, Safari, ...
scrollbar-width: none;
$space-height: calc((100% - (($spaces-to-display - 1) * (1%))) / $spaces-to-display);
--space-height: #{$space-height};
}
.space {
$gap: 5%;
$vertical-padding: 1%;
$horizontal-padding: 1%;
height: var(--space-height);
width: 100%;
flex-shrink: 0;
border: $border-normal;
border-color: get-color(primary, 100);
border-radius: $border-radius;
padding: $vertical-padding $horizontal-padding;
$name-height: 15%;
$conversation-name-height: 15%;
display: grid;
grid-template-columns: 100%;
grid-template-rows: $name-height $gap auto 0% 0%;
grid-template-areas:
"name"
"."
"conversations-carousel"
"."
"conversation-name"
;
transition: $transition-duration;
&.display-conversation-name {
grid-template-rows: $name-height $gap auto $gap $conversation-name-height;
}
cursor: default;
p {
margin: 0;
}
&__name {
grid-area: name;
display: flex;
align-items: center;
justify-content: left;
font-size: 2vh;
}
&__conversations-carousel {
grid-area: conversations-carousel;
display: flex;
flex-flow: row;
gap: 1%;
overflow-x: scroll;
// TODO: Manage android, Safari, ...
scrollbar-width: none;
}
&__conversation-name {
grid-area: conversation-name;
display: flex;
align-items: center;
justify-content: center;
font-size: 2vh;
}
}
.conversation-avatar {
height: 100%;
aspect-ratio: 1;
flex-shrink: 0;
border: $border-thin;
border-radius: $border-radius;
overflow: hidden;
filter: brightness(90%);
&.selected {
filter: brightness(120%);
}
&__image {
height: 100%;
width: 100%;
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
font-size: 6vh;
color: get-color(primary, 80);
}
&__invited-badge {
$height: 20%;
height: $height;
width: 100%;
position: relative;
top: calc($height * -1);
color: get-color(greyscale, 0);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5vh;
background-color: get-color(ternary, 100);
}
}
.search {
height: 100%;
width: 100%;
display: flex;
gap: 5%;
&__text {
height: 100%;
width: 100%;
}
&__button {
@include button-class();
width: 20%;
flex-shrink: 0;
}
}
.conversations {
@include panel();
$gap: 1%;
$account-height: 15%;
$search-height: 5%;
display: grid;
grid-template-columns: auto;
grid-template-rows: min($account-height, 384px) $gap auto $gap min($search-height, 128px);
grid-template-areas:
"account"
"."
"spaces"
"."
"search"
;
&__account {
grid-area: account;
}
&__spaces {
grid-area: spaces;
}
&__search {
grid-area: search;
}
&__menu {
grid-area: spaces;
z-index: 1;
}
}
%base-helper-text {
margin: 0;
margin-top: 0.3vh;
font-size: 1.2vh;
// TODO: Set color used for text in _base.scss file
color: get-color(greyscale, 90);
p {
margin: 0;
&.invalid {
color: get-color(critical, 100);
}
}
}
.conversation-options-menu {
width: 100%;
height: 100%;
position: relative;
top: -100%;
margin-bottom: calc(-100% / $aspect-ratio);
border-radius: $border-radius;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
&__inner {
$padding: 5%;
// TODO: Thin border
@include panel($padding, $padding);
width: 95%;
height: 60%;
display: grid;
grid-template-columns: 10% 10% 5% 15% 20% 10% 20% 10%;
grid-template-rows: 7.5% 7.5% 5% 5% auto 5% 10%;
grid-template-areas:
"avatar avatar . name name name name name"
"avatar avatar . topic topic topic topic topic"
"avatar avatar . . . . . ."
". . . . . . . ."
"config config config config config config config config"
". . . . . . . ."
". close close close . join join ."
;
&__avatar {
grid-area: avatar;
width: 100%;
aspect-ratio: 1;
}
&__name {
grid-area: name;
// TODO: Merge with &__display-name
display: flex;
align-items: center;
justify-content: center;
p {
font-size: 2.5vh;
margin: 0;
text-align: center;
}
}
&__topic {
grid-area: topic;
// TODO: Merge with &__display-name
display: flex;
align-items: center;
justify-content: center;
@extend %base-helper-text;
p {
font-size: 2vh;
margin: 0;
text-align: center;
}
}
&__config {
grid-area: config;
// TODO: Merge with &__display-name
display: flex;
align-items: center;
justify-content: center;
border: $border-thin;
border-color: get-color(ternary, 90);
border-radius: $border-radius;
}
button {
height: 100%;
width: 100%;
}
&__close-button {
grid-area: close;
}
&__join-button {
grid-area: join;
}
}
}

View File

@@ -1,25 +0,0 @@
use dioxus::prelude::*;
turf::style_sheet!("src/ui/components/header.scss");
pub fn Header() -> Element {
rsx! {
style { {STYLE_SHEET} },
div {
class: ClassName::ROOT,
img {
src: "/public/images/logo-msn.png"
}
svg {
view_box: "0 0 100 10",
text {
y: "55%",
dominant_baseline: "middle",
font_size: "5",
"Windows Live Messenger",
},
},
}
}
}

View File

@@ -1,13 +0,0 @@
.root {
height: 100%;
width: 100%;
display: flex;
img {
height: 100%;
}
svg {
fill: white;
}
}

View File

@@ -1,21 +1,62 @@
use const_format::formatcp;
use dioxus::prelude::*;
use dioxus_free_icons::icons::md_navigation_icons::MdArrowDropDown;
use dioxus_free_icons::icons::fa_solid_icons::{
FaComments, FaLayerGroup, FaMagnifyingGlass, FaPeopleGroup,
};
use dioxus_free_icons::{Icon, IconShape};
turf::style_sheet!("src/ui/components/icons.scss");
include!(concat!(env!("OUT_DIR"), "/style_vars.rs"));
include!(concat!(env!("OUT_DIR"), "/style_tokens.rs"));
use style::{COLOR_PRIMARY_100, COLOR_TERNARY_100};
pub fn DownArrowIcon() -> Element {
macro_rules! transparent_icon {
($name:ident, $icon:ident) => {
pub fn $name() -> Element {
rsx! {
style { {STYLE_SHEET} }
Icon {
class: ClassName::TRANSPARENT_ICON,
icon: $icon,
}
}
}
};
}
transparent_icon!(SearchIcon, FaMagnifyingGlass);
transparent_icon!(SpacesIcon, FaLayerGroup);
transparent_icon!(ChatsIcon, FaComments);
transparent_icon!(RoomsIcon, FaPeopleGroup);
#[derive(Clone, PartialEq)]
pub(crate) struct LogoShape;
impl IconShape for LogoShape {
fn view_box(&self) -> &str {
"0 0 184 94"
}
fn xmlns(&self) -> &str {
"http://www.w3.org/2000/svg"
}
fn child_elements(&self) -> Element {
rsx! {
path {
"stroke-linejoin": "round",
"stroke-width": "6",
d: "M121.208 2 2 57.011l70.927-.265L61.363 92 182 36.724h-69.498L121.208 2Z"
}
}
}
}
pub fn LogoIcon() -> Element {
rsx! {
style { {STYLE_SHEET} },
style { {STYLE_SHEET} }
Icon {
class: ClassName::DOWN_ARROW_ICON,
icon: MdArrowDropDown,
icon: LogoShape,
}
}
}
@@ -46,7 +87,7 @@ const _PYRAMID_VIEWBOX_HEIGHT: i64 = (_PYRAMID_CENTRAL_EDGE_E2_Y + _PYRAMID_STRO
const _PYRAMID_VIEWBOX_WIDTH: i64 = (_PYRAMID_RIGHT_EDGE_E2_X + _PYRAMID_STROKE_WIDTH) as i64;
const _PYRAMID_VIEWBOX: &str = formatcp!("0 0 {_PYRAMID_VIEWBOX_WIDTH} {_PYRAMID_VIEWBOX_HEIGHT}");
#[derive(PartialEq, Clone)]
#[derive(Clone, PartialEq)]
struct PyramidShape {
color: String,
ratio: f64,
@@ -93,14 +134,14 @@ impl IconShape for PyramidShape {
L {_PYRAMID_EDGES_E1_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
M {_PYRAMID_EDGES_E1_X} {_PYRAMID_EDGES_E1_Y} \
V {_PYRAMID_CENTRAL_EDGE_Y_LEN}",
},
}
path {
d: "\
M {_PYRAMID_CENTRAL_EDGE_E2_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
V {central_edge_ratio_e2_y} \
L {left_edge_ratio_e1_x} {no_central_edge_ratio_e1_y} \
L {_PYRAMID_LEFT_EDGE_E2_X} {_PYRAMID_LEFT_EDGE_E2_Y} Z",
},
}
path {
d: "\
M {_PYRAMID_CENTRAL_EDGE_E2_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
@@ -113,7 +154,7 @@ impl IconShape for PyramidShape {
}
}
#[derive(PartialEq, Clone, Props)]
#[derive(Clone, PartialEq, Props)]
pub struct PyramidProps {
color: Option<String>,
#[props(default = 0.5)]
@@ -128,10 +169,11 @@ pub fn Pyramid(props: PyramidProps) -> Element {
.unwrap_or(COLOR_TERNARY_100.to_string());
rsx! {
style { {STYLE_SHEET} },
style { {STYLE_SHEET} }
Icon {
class: ClassName::PYRAMID_ICON,
icon: PyramidShape { ratio: props.ratio, color, progress_color },
}
}

View File

@@ -1,4 +1,4 @@
.down-arrow-icon {
.transparent-icon {
color: transparent;
path:last-child {

View File

@@ -1,28 +0,0 @@
use dioxus::prelude::*;
use log::debug;
use super::spinner::Spinner;
use super::wallpaper::Wallpaper;
turf::style_sheet!("src/ui/components/loading.scss");
pub fn LoadingPage() -> Element {
debug!("LoadingPage rendering");
rsx! {
style { {STYLE_SHEET} },
div {
class: ClassName::LOADING,
Wallpaper {
display_version: true
},
div {
class: ClassName::LOADING_SPINNER,
Spinner {},
}
}
}
}

View File

@@ -1,80 +0,0 @@
@import "../_base.scss"
@import "./spinner.scss"
.loading {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
&__spinner {
height: 5%;
aspect-ratio: $logo-aspect-ratio;
position: absolute;
$logo-center-pos: calc(50% + ($background-height / 2) - ($logo-height / 2));
@media (0px < height <= calc($background-height * 5)) {
top: $logo-center-pos;
}
@media (calc($background-height * 5) < height <= calc($background-height * 6)) {
top: calc($logo-center-pos + ($background-height * 1));
}
@media (calc($background-height * 6) < height <= calc($background-height * 8)) {
top: calc($logo-center-pos + ($background-height * 2));
}
@media (calc($background-height * 8) < height <= calc($background-height * 10)) {
top: calc($logo-center-pos + ($background-height * 3));
}
@media (calc($background-height * 10) < height <= calc($background-height * 12)) {
top: calc($logo-center-pos + ($background-height * 4));
}
@media (calc($background-height * 12) < height <= calc($background-height * 14)) {
top: calc($logo-center-pos + ($background-height * 5));
}
@media (calc($background-height * 14) < height <= calc($background-height * 16)) {
top: calc($logo-center-pos + ($background-height * 6));
}
@media (calc($background-height * 16) < height <= calc($background-height * 18)) {
top: calc($logo-center-pos + ($background-height * 7));
}
@media (calc($background-height * 18) < height <= calc($background-height * 20)) {
top: calc($logo-center-pos + ($background-height * 8));
}
@media (calc($background-height * 20) < height <= calc($background-height * 22)) {
top: calc($logo-center-pos + ($background-height * 9));
}
@media (calc($background-height * 22) < height <= calc($background-height * 24)) {
top: calc($logo-center-pos + ($background-height * 10));
}
@media (calc($background-height * 24) < height <= calc($background-height * 26)) {
top: calc($logo-center-pos + ($background-height * 11));
}
@media (calc($background-height * 26) < height <= calc($background-height * 28)) {
top: calc($logo-center-pos + ($background-height * 12));
}
@media (calc($background-height * 28) < height <= calc($background-height * 30)) {
top: calc($logo-center-pos + ($background-height * 13));
}
@media (calc($background-height * 30) < height <= calc($background-height * 32)) {
top: calc($logo-center-pos + ($background-height * 14));
}
@media (calc($background-height * 32) < height <= calc($background-height * 34)) {
top: calc($logo-center-pos + ($background-height * 15));
}
@media (calc($background-height * 34) < height <= calc($background-height * 36)) {
top: calc($logo-center-pos + ($background-height * 16));
}
@media (calc($background-height * 36) < height <= calc($background-height * 38)) {
top: calc($logo-center-pos + ($background-height * 17));
}
@media (calc($background-height * 38) < height <= calc($background-height * 40)) {
top: calc($logo-center-pos + ($background-height * 18));
}
background-color: get-color(greyscale, 0);
}
}

View File

@@ -1,26 +1,25 @@
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::{borrow::Cow, cell::RefCell, collections::HashMap, rc::Rc};
use const_format::formatcp;
use dioxus::prelude::*;
use log::{debug, error, warn};
use tracing::{debug, error, warn};
use validator::{Validate, ValidateArgs, ValidateEmail, ValidationError, ValidationErrors};
use zxcvbn::zxcvbn;
use zxcvbn::{zxcvbn, Score};
use crate::base::SESSION;
use crate::domain::model::session::Session;
use crate::infrastructure::services::random_svg_generators::{
generate_random_svg_shape, ShapeConfig,
use crate::{
domain::model::session::Session,
infrastructure::services::random_svg_generators::{generate_random_svg_shape, ShapeConfig},
ui::SESSION,
};
use super::button::{LoginButton, RegisterButton};
use super::modal::{Modal, Severity};
use super::spinner::Spinner;
use super::text_input::{PasswordInputState, PasswordTextInput, TextInput, TextInputState};
use super::{
button::{LoginButton, RegisterButton},
modal::{Modal, Severity},
spinner::Spinner,
text_input::{PasswordInputState, PasswordTextInput, TextInput, TextInputState},
};
include!(concat!(env!("OUT_DIR"), "/style_vars.rs"));
include!(concat!(env!("OUT_DIR"), "/style_tokens.rs"));
use style::{
COLOR_PRIMARY_100, COLOR_PRIMARY_110, COLOR_PRIMARY_120, COLOR_PRIMARY_140, COLOR_PRIMARY_150,
@@ -98,27 +97,6 @@ impl Clone for Box<dyn OnValidationError> {
}
}
#[derive(Clone)]
struct TextInputHandler {
state: Signal<TextInputState>,
}
impl TextInputHandler {}
impl OnValidationError for TextInputHandler {
fn reset(&mut self) {
self.state.write().reset();
}
fn invalidate(&mut self, helper_text: String) {
self.state.write().invalidate(helper_text);
}
fn box_clone(&self) -> Box<dyn OnValidationError> {
Box::new(self.clone())
}
}
#[derive(Clone)]
struct UrlInputHandler {
state: Signal<TextInputState>,
@@ -370,14 +348,14 @@ fn validate_id(id: &Option<String>, process: &Process) -> Result<(), ValidationE
}
struct PasswordValidationResult {
score: u8,
score: Score,
rating: f64, // 0 <= rating <= 1
suggestions: Vec<String>,
}
impl PasswordValidationResult {
pub fn new() -> Self {
PasswordValidationResult {
score: 0,
score: Score::Zero,
rating: 0.0,
suggestions: Vec::<String>::new(),
}
@@ -388,9 +366,7 @@ fn compute_password_score(
password: &str,
with_suggestions: Option<bool>,
) -> Option<PasswordValidationResult> {
let Ok(estimate) = zxcvbn(password, &[]) else {
return None;
};
let estimate = zxcvbn(password, &[]);
let mut result = PasswordValidationResult::new();
result.score = estimate.score();
@@ -418,7 +394,7 @@ fn validate_password(password: &Option<String>, process: &Process) -> Result<(),
if let Some(password) = password {
if let Some(result) = compute_password_score(password, Some(true)) {
// TODO: To configuration?
if result.score <= 2 {
if [Score::Zero, Score::One, Score::Two].contains(&result.score) {
let mut err = ValidationError::new(TOO_WEAK_PASSWORD_ERROR_NAME);
err.add_param(Cow::from("score"), &result.score);
err.add_param(Cow::from("rating"), &result.rating);
@@ -538,7 +514,7 @@ fn generate_modal(
on_confirm: on_confirm,
div {
{rendered_suggestions.into_iter()}
{rendered_suggestions.iter()}
}
}
}
@@ -608,18 +584,18 @@ pub fn Login() -> Element {
generate_random_svg_shape(Some(&shape_config)).await
});
let avatar = match &*random_avatar_future.read_unchecked() {
Some(svg) => Some(rsx! {
div {
class: ClassName::LOGIN_AVATAR_CONTENT,
dangerous_inner_html: svg.as_str(),
let avatar = (*random_avatar_future.read_unchecked())
.as_ref()
.map(|svg| {
rsx! {
div {
class: ClassName::LOGIN_AVATAR_CONTENT,
dangerous_inner_html: svg.as_str(),
}
}
}),
None => None,
};
});
if *spinner_animated.read() && SESSION.read().is_logged {
debug!("Stop spinner");
spinner_animated.set(false);
}
@@ -745,7 +721,7 @@ pub fn Login() -> Element {
let confirm_password_classes_str = confirm_password_classes.join(" ");
rsx! {
style { {STYLE_SHEET} },
style { {STYLE_SHEET} }
div {
class: "{classes_str}",
@@ -757,73 +733,79 @@ pub fn Login() -> Element {
random_avatar_future.restart()
},
{avatar},
},
{avatar}
}
div {
class: ClassName::LOGIN_HOMESERVER,
TextInput {
placeholder: "Homeserver URL",
value: "{homeserver_url}",
state: homeserver_url_state,
oninput: on_input![data, homeserver_url],
},
},
}
}
div {
class: ClassName::LOGIN_ID,
TextInput {
placeholder: "{id_placeholder}",
value: "{id}",
state: id_state,
oninput: on_input![data, id],
},
},
}
}
div {
class: "{password_classes_str}",
PasswordTextInput {
placeholder: "Password",
value: "{password}",
state: password_state,
oninput: on_input![data, password],
},
},
}
}
div {
class: "{confirm_password_classes_str}",
PasswordTextInput {
placeholder: "Confirm Password",
value: "{confirm_password}",
state: confirm_password_state,
oninput: on_input![data, confirm_password],
}
},
}
div {
class: ClassName::LOGIN_SPINNER,
Spinner {
animate: *spinner_animated.read(),
},
},
}
}
div {
class: ClassName::LOGIN_REGISTER_BUTTON,
RegisterButton {
onclick: on_clicked_register,
},
},
}
}
div {
class: ClassName::LOGIN_LOGIN_BUTTON,
LoginButton {
focus: true,
onclick: on_clicked_login,
},
},
},
}
}
}
{rendered_modal},
{rendered_modal}
}
}

View File

@@ -3,7 +3,8 @@
@import "./spinner.scss";
.login {
@extend %panel;
$padding: 5%;
@include panel($padding, $padding);
$aspect-ratio: var(--aspect-ratio);
@@ -72,7 +73,7 @@
overflow: hidden;
&__content {
height: calc(100% + (2 * $border-normal-width));
height: 100%;
aspect-ratio: 1;
}
}

View File

@@ -1,12 +1,8 @@
pub(crate) mod avatar_selector;
pub(crate) mod button;
pub(crate) mod chats_window;
pub(crate) mod contacts_window;
pub(crate) mod header;
pub(crate) mod chat_panel;
pub(crate) mod conversations;
pub(crate) mod icons;
pub(crate) mod loading;
pub(crate) mod login;
pub(crate) mod main_window;
pub(crate) mod modal;
pub(crate) mod spinner;
pub(crate) mod text_input;

View File

@@ -9,13 +9,13 @@ use crate::infrastructure::services::random_svg_generators::{
generate_random_svg_avatar, AvatarConfig, AvatarFeeling,
};
include!(concat!(env!("OUT_DIR"), "/style_vars.rs"));
include!(concat!(env!("OUT_DIR"), "/style_tokens.rs"));
use style::{COLOR_CRITICAL_100, COLOR_SUCCESS_100, COLOR_WARNING_100};
turf::style_sheet!("src/ui/components/modal.scss");
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub enum Severity {
Ok,
Warning,
@@ -42,7 +42,7 @@ fn avatar_configs() -> &'static HashMap<Severity, AvatarConfig<'static>> {
})
}
#[derive(Props, Clone, PartialEq)]
#[derive(Clone, PartialEq, Props)]
pub struct ModalProps {
pub severity: Severity,
#[props(optional)]
@@ -59,15 +59,16 @@ pub fn Modal(props: ModalProps) -> Element {
let random_figure_future =
use_resource(move || async move { generate_random_svg_avatar(avatar_config).await });
let icon = match &*random_figure_future.read_unchecked() {
Some(svg) => Some(rsx! {
div {
class: ClassName::MODAL_CONTENT_ICON_PLACEHOLDER,
dangerous_inner_html: svg.as_str(),
let icon = (*random_figure_future.read_unchecked())
.as_ref()
.map(|svg| {
rsx! {
div {
class: ClassName::MODAL_CONTENT_ICON_PLACEHOLDER,
dangerous_inner_html: svg.as_str(),
}
}
}),
None => None,
};
});
let button_class = match &props.severity {
Severity::Ok => SuccessButton,
@@ -75,10 +76,10 @@ pub fn Modal(props: ModalProps) -> Element {
Severity::Critical => ErrorButton,
};
icon.as_ref()?;
let _ = icon.as_ref().ok_or(VNode::empty());
rsx! {
style { {STYLE_SHEET} },
style { {STYLE_SHEET} }
div {
class: ClassName::MODAL,
@@ -89,17 +90,17 @@ pub fn Modal(props: ModalProps) -> Element {
div {
class: ClassName::MODAL_CONTENT_ICON,
{icon}
},
}
div {
class: ClassName::MODAL_CONTENT_TITLE,
{props.title},
},
{props.title}
}
div {
class: ClassName::MODAL_CONTENT_MSG,
{props.children},
},
{props.children}
}
div {
class: ClassName::MODAL_CONTENT_BUTTONS,
@@ -108,10 +109,10 @@ pub fn Modal(props: ModalProps) -> Element {
if let Some(cb) = &props.on_confirm {
cb.call(evt);
}
},
},
},
},
},
}
}
}
}
}
}
}

View File

@@ -67,7 +67,7 @@ $modal-max-height: 55vh;
border-radius: $border-radius;
&__placeholder {
width: calc(100% + (2 * $border-normal-width));
width: 100%;
height: 100%;
}

Some files were not shown because too many files have changed in this diff Show More