226 Commits

Author SHA1 Message Date
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
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
dfe2761a3a Merge branch 'fix/login-invalid-width-on-mobile' into develop
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-04-22 15:55:58 +02:00
58e12c991d 🐛 Fix the height and radius of the inputs 2024-04-22 15:51:01 +02:00
badd541424 Merge branch 'fix/login-invalid-width-on-mobile' into develop
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-04-22 15:05:52 +02:00
fcf3d92cf9 🐛 Fix a view in charge to set the Login dimesions according to the screen aspect-ratio 2024-04-22 15:04:26 +02:00
6172167ea8 Add a parameter to the Wallpaper widget to show the app version 2024-04-22 14:44:51 +02:00
7170332205 Merge branch 'fix/loading-invalid-spinner-position' into develop
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-04-22 09:02:38 +02:00
a3775e35d3 🐛 Update to match to the dioxus_free_icons view_box and xmlns definition 2024-04-22 08:58:29 +02:00
724d04c592 Merge branch 'fix/loading-invalid-spinner-position' into develop
Some checks failed
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-04-21 23:12:30 +02:00
b3330abecc 🐛 The loading spinner isn't aligned with the wallpaper on mobile 2024-04-21 23:08:34 +02:00
eecb46e4b8 🐛 Displayed versions are always computed as dirty
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-04-16 23:21:19 +02:00
de9d2b3a8a 👷 Run dockerize and deploy CI steps on commits on default branch
Some checks failed
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-04-16 07:29:09 +02:00
f0d3b91084 🧑‍💻 Add version to the Wallpaper
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
This information should be moved to a version panel, when it'll be available...
2024-04-16 07:01:49 +02:00
cae7a1e244 👷 Optimize building process using the homemade dioxus-cli docker image
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-04-15 23:18:40 +02:00
6f95e0f57b 👷 Use of the CI to deliver a new image on each commit (temp)
All checks were successful
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-04-15 22:04:25 +02:00
d4af06d687 🐛 Fix wallpaper-pattern misalignment 2024-04-12 16:59:45 +02:00
53fff64537 🐛 Fix invalid font used in login form on (web platform) 2024-04-12 16:24:28 +02:00
fb4554aa71 🚀 Update Dockerfile to take the new code organization into account 2024-04-12 14:44:44 +02:00
741124e47e ️ Configure cargo to Optimize the size of the generated wasm bundle 2024-04-12 14:42:41 +02:00
9951c2fea6 🔥 Remove the use of LoadingPage component
For now, the LoadingPage component is kept, just in case.
2024-04-12 14:41:31 +02:00
df33e94a12 Add web page to display during the wasm bundle 2024-04-12 14:33:10 +02:00
78cb65e054 ♻️ Move fonts and images directories in a common public on 2024-04-12 12:49:07 +02:00
77fa0c5fd5 🚀 Add the Dockerfile for the web app 2024-04-11 08:32:57 +02:00
d3a35cd81f ️ Configure turf to minify the generated CSS files 2024-04-11 08:31:33 +02:00
b524048563 ⬆️ Bump turf release (0.7.0 -> 0.8.*) 2024-04-11 08:30:29 +02:00
4ab4ac5fee ⬆️ Use of the latest matrix-sdk 0.7.* version (master branch before) 2024-04-11 08:30:17 +02:00
438416bec1 Merge branch 'web-client' into develop 2024-04-10 17:23:24 +02:00
c580fba315 ♻️ Add Room domain entity 2024-04-10 17:14:26 +02:00
a7bccfa779 ♻️ Add Session domain entity 2024-04-10 12:50:15 +02:00
eb81b3252c Enable on tokio rt and sync features (disable default ones) 2024-04-10 12:41:30 +02:00
880195109d ⬆️ Bump dioxus version 2024-04-10 12:41:29 +02:00
11e239714b Reuse tracing library to be able to display matrix SDK logs 2024-04-10 12:41:05 +02:00
4261e24cd2 ♻️ Clean Cargo.toml file and add target specific dependencies 2024-04-06 12:18:48 +02:00
9cfc0841df 💄 Fix some rendering inconsistencies 2024-04-06 12:16:18 +02:00
39ff4122c9 🐛 Svg generated using dicebear shall use unique ids 2024-04-06 12:13:10 +02:00
46ce890718 ♻️ Make random_svg_generators able to get placeholder according to the target 2024-04-06 12:07:29 +02:00
82b15a5509 💄 Manage config per target and remove menu bar from the desktop one 2024-04-06 12:02:43 +02:00
912b67ed23 🐛 Remove unused tokio::time import 2024-04-06 11:55:32 +02:00
0ec1187fc3 ♻️ Replace tracing dependency with dioxus-logger
tracing package doesn't support web platform when dioxus-logger `will eventually support every target that Dioxus
does. Currently only web and desktop platforms are supported.`
2024-04-06 11:51:46 +02:00
f78765e553 ⬆️ Bump dioxus-sdk version (0.5.0)
The dioxus-std has been renamed to dioxus-sdk
2024-04-06 11:37:43 +02:00
b26cb1d982 Use async-std to sleep asynchronously (previously done with tokio) 2024-04-05 17:23:48 +02:00
fc9411376c Remove dioxus-desktop dependency 2024-04-05 17:14:37 +02:00
df465d99c0 Disable matrix-sdk unused and default features 2024-04-05 17:13:22 +02:00
491e34903f 🔧 Add Dioxus.toml file 2024-04-05 17:06:28 +02:00
0c1df908f2 Merge branch 'dioxus-0.5.0' into develop 2024-04-05 16:26:32 +02:00
d245169345 🚸 Make desktop the default target 2024-04-05 16:16:19 +02:00
9eaf79208e Merge branch 'clean-redesign' into develop 2024-04-04 14:39:04 +02:00
0ce0764204 🎨 Isolate infra and ui components
The src/base.rs is still to be reworked.
2024-04-04 14:27:58 +02:00
92bf860101 Merge branch 'dioxus-0.5.0' into develop 2024-04-01 23:47:13 +02:00
014a0c2c57 🎨 Put svg image generation in a dedicated datasource 2024-04-01 19:32:35 +02:00
9071b0073c ⬆️ Update the components to take the dioxus 0.5 rework into account 2024-03-31 23:26:10 +02:00
aad0064a0c Merge branch 'redesign-login-form' into develop 2024-03-30 18:31:15 +01:00
83fe388e8d 🚨 Fix clippy warnings 2024-03-30 18:24:04 +01:00
448b81b65d Add Login component 2024-03-30 17:40:17 +01:00
4e963ce063 🎨 Factorize the definition of the Button components 2024-03-30 14:37:44 +01:00
cf9737fc76 Add Modal component 2024-03-30 13:46:53 +01:00
5c91df206c 💄 Store colors in a nested map to make them reachable using criteria 2024-03-30 08:23:52 +01:00
0ab6aaac1c Add PasswordTextInput component
The TextInput component has been reworked to factorize some pieces of code with PasswordTextInput.
2024-03-21 21:12:49 +01:00
89b1f10b6e Add Pyramid icon 2024-03-21 21:05:04 +01:00
570a969cee 💄 Fix conflicts regarding the generated CSS class names 2024-03-21 18:32:40 +01:00
ceeda1a771 Redesign Login component and add fields validation 2024-03-15 14:58:58 +01:00
fc0d3b1212 ♻️ Replace constcat with const_format 2024-03-15 12:33:25 +01:00
01f589e789 Add helper_text to TextInput (previously TextEdit) 2024-03-15 12:24:42 +01:00
c746fb6552 💄 Rework Login component 2024-03-10 12:02:18 +01:00
1073a592ed 🔨 Make the Design System color tokens reachable from the rust code
The design system tokens are specified in the _base.scss file. To avoid to duplicate their value in a rust file, a new
step has been added to the building process to generate the `style_vars.rs` file which make the color tokens reachable
to the app.
2024-03-10 11:40:00 +01:00
dd0754073c Add TextField component 2024-03-10 11:35:25 +01:00
b05e3efce4 🐛 Add SVG pattern file used to render the Wallpaper 2024-03-10 11:01:33 +01:00
0a4969e079 💄 Center and uppercase the content of the Register and Login buttons 2024-03-10 10:42:02 +01:00
f52733d9a6 💄 Add buttons which will be used by the Login component 2024-03-09 22:46:00 +01:00
bb56d24f02 💄 Add border variables to the base SCSS file 2024-03-09 22:44:04 +01:00
043a721429 Make Spinner animation suspendable 2024-03-09 13:04:01 +01:00
46c251ef90 ♻️ Make Spinner reusable (not only by loading view) 2024-03-03 23:35:09 +01:00
257b36eae1 ✏️ Fix typo in color names 2024-03-03 23:31:00 +01:00
ff430edffe 💄 Use Geist font everywhere. 2024-03-03 23:29:49 +01:00
5e05b75bde 💄 Add Wallpaper, Spinner and LoadingPage widgets 2024-02-29 23:47:32 +01:00
5719cb8254 💄 Add DS colors 2024-02-29 23:46:20 +01:00
921003aeac Merge branch 'readme' into develop 2024-01-07 13:30:04 +01:00
8ffc977846 📝 Add the reference to the MSN messenger client 2024-01-07 13:28:27 +01:00
6e64eb4d97 📝 Add the description of the technical stack to the README 2024-01-07 13:25:52 +01:00
a7cf0f681a 🎨 Split ChatsWindow component
Creation of the Navbar and Conversation components.
2024-01-01 21:52:31 +01:00
a8d343ce3a ️ Use of the token returned by the first sync with the homeserver 2024-01-01 21:32:32 +01:00
5fe13335a1 🎨 Factorize Room creation from MatrixRoom instances 2023-12-31 15:21:49 +01:00
04628ae10d ️ Remove the periodic pooling to get the rooms (joined or not) 2023-12-31 15:04:18 +01:00
66f4ba6a7e 🐛 Remove piece of code used for testing 2023-12-30 23:38:06 +01:00
116bbcb247 🚧 Add an interface to the ChatsWindows to drive its behavior
For now, only the ChatsWindow tabs are toggled on clicks on room names (from ContactsSection).
2023-12-30 23:31:51 +01:00
2fed770f62 📝 Change the presentation video chroma subsampling
Cf. https://bugzilla.mozilla.org/show_bug.cgi?id=1368063
2023-12-26 21:44:46 +01:00
227a07dd9e 📝 Add main readme file 2023-12-26 21:40:12 +01:00
7498638ac1 🚨 Fix clippy warnings 2023-12-26 21:04:57 +01:00
ddeb94e887 ♻️ Replace flume with tokio and share Matrix client infos to chats
- Remove of the flume dependency.
- Add the capability to share data provided by the Matrix client to the ChatsWindow. Indeed, until the 0.6 Dioxus
  release, each window runs in a separate virtual DOM so the context and Fermi states are completely seperate
  (cf. https://discord.com/channels/899851952891002890/1188206938215948378).
2023-12-25 23:14:43 +01:00
d7ba8130d3 🙈 Add dist directory to gitignore 2023-12-23 17:42:42 +01:00
8679a23692 Add rooms topics management 2023-12-23 14:54:21 +01:00
c9292fd613 🏗️ Remove data handling from components
The data sent by matrix_interface senders is now handled by the App.
2023-12-21 22:07:08 +01:00
513b05ddb3 🏗️ Split matrix_client.rs to create the matrix_interface module 2023-12-17 11:54:21 +01:00
ae8dba86f6 🏗️ Rearchitecting the interface with the MatrixClient
- Replace RwStore with channels.
- Use of fermi to handle application data.
- Use of tracing.
2023-12-10 22:04:07 +01:00
4988054dae 💄 Display ChatsWindow only once the user logged 2023-08-21 21:43:42 +02:00
f79bf329a5 🐛 Fix AvatarSelector shift (Login) 2023-08-21 21:29:09 +02:00
5120f1e74f 🐛 Fix not-centered AvatarSelector rendering issue 2023-08-20 20:06:16 +02:00
ff95dcade8 Add first static (no reactivity, static data) implementation of ChatsWindow 2023-08-20 20:04:09 +02:00
3b89cd1769 🎨 Move DownArrowIcon to the new icons file 2023-08-20 19:53:28 +02:00
136 changed files with 8287 additions and 1640 deletions

8
.cargo/config.toml Normal file
View File

@@ -0,0 +1,8 @@
[profile.release]
opt-level = "z"
debug = false
lto = true
codegen-units = 1
panic = "abort"
strip = true
incremental = false

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
# .git directory is not filtered here: we need to copy the .git directory in the builder image to compute the version.
# media directory, README.md and Dockerfile files are not filtered to avoid the dockerized building env to get dirty
# and append the "-modified" suffix to the displayed version.
dist
target

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
medias/presentation.png filter=lfs diff=lfs merge=lfs -text
medias/presentation.mp4 filter=lfs diff=lfs merge=lfs -text

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/target
Cargo.lock
/dist
Cargo.lock

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

25
.woodpecker/.deploy.yaml Normal file
View File

@@ -0,0 +1,25 @@
steps:
deploy:
image: euryecetelecom/woodpeckerci-kubernetes
settings:
kubernetes_server:
from_secret: kubernetes_server
kubernetes_token:
from_secret: kubernetes_token
kubernetes_cert:
from_secret: kubernetes_cert
namespace: bg92
wait: true
wait_timeout: 60s
force: true
deployment: beau-gosse-du-92-web
repo: rg.fr-par.scw.cloud/asr-projects/beau-gosse-du-92-web
container: beau-gosse-du-92-web
tag: ${CI_COMMIT_SHA}
when:
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
depends_on:
- dockerize

View File

@@ -0,0 +1,19 @@
steps:
dockerize:
image: woodpeckerci/plugin-kaniko
settings:
registry: rg.fr-par.scw.cloud
repo: asr-projects/beau-gosse-du-92-web
tags: ${CI_COMMIT_SHA}
auto_tag: true
cache: false
username: nologin
password:
from_secret: registry-password
when:
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
# depends_on:
# - validate

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,24 +2,96 @@
name = "beau-gosse-du-92"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = []
[package.metadata.spellcheck]
config = "./spellcheck.toml"
[dependencies]
dioxus = "0.4.0"
dioxus-desktop = "0.4.0"
# matrix-sdk = { version = "0.6.2", features = ["js"] }
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", branch = "main" , features = ["js"]}
anyhow = "1.0.72"
url = "2.4.0"
tokio = "1.29.1"
dirs = "5.0.1"
ctrlc-async = "3.2.2"
tracing-subscriber = "0.3.17"
dioxus-free-icons = { version = "0.7.0", features = ["material-design-icons-navigation", "ionicons"] }
thiserror = "1.0.44"
turf = "0.5.0"
dioxus-std = { version = "0.4.0", features = ["utils"] }
# Errors
anyhow = "1.0.75"
thiserror = "1.0.50"
[build]
target = "x86_64-unknown-linux-gnu"
# Async
async-std = "1.12.0"
async-trait = "0.1.80"
futures = "0.3.29"
futures-util = "0.3.29"
tokio = { version = "1.34.0", default-features = false, features = ["rt", "sync"] }
tokio-stream = "0.1.15"
# Utils
base64 = "0.22.0"
const_format = "0.2.32"
rand = "0.8.5"
validator = { version = "0.17.0", features = ["derive"] }
# Http client
reqwest = "0.11.24"
# Password strength estimation
zxcvbn = "2.2.2"
# Image processing/conversion
image = "0.25.1"
# Get the application version
git-version = "0.3.9"
# Conditional compilation
cfg-if = "1.0.0"
# Logging/tracing
tracing = "0.1.40"
tracing-forest = "0.1.6"
# SCSS -> CSS + usage in rust code
turf = "0.9.3"
# Dioxus
dioxus-free-icons = { version = "0.9", features = ["ionicons", "font-awesome-solid"] }
modx = "0.1.4"
[target.'cfg(target_family = "wasm")'.dependencies]
# Logging/tracing
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing-web = "0.1.3"
# Dioxus
dioxus = { version = "0.6.3", features = ["web"] }
web-sys = "0.3.69"
# Matrix
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", rev = "fa6066b8", default-features = false, features = ["rustls-tls", "js"] }
[target.'cfg(not(target_family = "wasm"))'.dependencies]
# Utils
time = "0.3.36"
# Logging/tracing
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "time"] }
# Dioxus
dioxus = { version = "0.6.3", features = ["desktop"] }
# Matrix
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", rev = "fa6066b8", default-features = false, features = ["rustls-tls"] }
[build-dependencies]
regex = "1.10.3"
[profile]
[profile.wasm-dev]
inherits = "dev"
opt-level = 1
[profile.server-dev]
inherits = "dev"
[profile.android-dev]
inherits = "dev"
[package.metadata.turf]
minify = true
[package.metadata.turf.class_names]
template = "<original_name>--<id>"

13
Dioxus.toml Normal file
View File

@@ -0,0 +1,13 @@
[application]
name = "beau-gosse-du-92"
default_platform = "desktop"
[web.app]
title = "BG92"
[web.watcher]
reload_html = true
watch_path = ["Dioxus.toml", "public/index.html", "src"]
[[web.proxy]]
backend = "http://localhost:8000/api/"

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM rg.fr-par.scw.cloud/asr-projects/dioxus-cli-0.6.3:latest AS builder
ARG JOBS_NB=${JOBS_NB:-default}
# Disable incremental compilation
# Cf. https://doc.rust-lang.org/cargo/reference/profiles.html#incremental
ARG CARGO_INCREMENTAL=0
WORKDIR /usr/src/beau-gosse-du-92
# git is required by the git-version crate
RUN apt update \
&& apt install -y --no-install-recommends git git-lfs \
&& apt clean
COPY . .
RUN dx build -r --platform web -- -j ${JOBS_NB}
FROM nginx:mainline-alpine-slim
WORKDIR /usr/share/nginx/html
COPY --from=builder /usr/src/beau-gosse-du-92/target/dx/beau-gosse-du-92/release/web/public .

43
README.md Normal file
View File

@@ -0,0 +1,43 @@
# 🚧 Beau-gosse-du-92 🚧
The goal of this project is to propose a new open-source implementation of the famous MSN messenger instant-messaging client.
[![Presentation](medias/presentation.png)](medias/presentation.mp4)
# Technical stack
## Back-end
This project is based on the [Matrix.org](https://matrix.org/) building blocks (back-end and front-end SDK) to avoid to
reinvent the wheel. This solution provides:
- [Open-source protocol](https://spec.matrix.org/v1.9/).
- Features expected for a messaging solution in 2024 (multi-devices management, emojis, integrations, redaction,
spaces, ...).
- Multi-platforms clients (Android, iOS and web-client).
- SDK available for each platform and a new Rust SDK supporting all the previously listed platforms.
- Conference stack ([Element Call](https://github.com/element-hq/element-call)).
- End-to-end encryption.
- Federation management.
- Capability to host all the back-end infrastructure by ourself.
## Front-end
First, the project involves writing a client compatible with the [Matrix.org (client-server
API)](https://spec.matrix.org/v1.9/client-server-api/) protocol.
Even if the Rust SDK is still in beta, it seems to be the future one (cf. [Element X - experience the future of
Element!](https://element.io/blog/element-x-experience-the-future-of-element/)) and a good choice for someone starting a
new client... from my point of view.
The SDK chosen, a Rust (to avoid to use the bindings provided by the matrix-rust-sdk and mostly because I want to
learn Rust) graphical library should be selected. The [Dioxus](https://dioxuslabs.com/) one seems to do the job:
- React-inspired library for Rust.
- Multi-platforms (use of Web-view or WGPU-enabled renderers).
# TODO
- [ ] Test dioxus-radio.
- [ ] Design system ?
- [ ] Implement MSN messenger features using Matrix.org SDK...

153
build.rs
View File

@@ -1,5 +1,156 @@
use std::env;
use std::fmt::Display;
use std::fs::File;
use std::io::Write;
use std::io::{self, BufRead};
use std::path::Path;
use std::path::PathBuf;
use regex::Regex;
fn main() {
// Tell Cargo to rerun this build script if any SCSS file
// in the 'src' directory or its subdirectories changes.
println!("cargo:rerun-if-changed=src/**/*.scss");
println!("cargo:rerun-if-changed=src/ui/**/*.scss");
let out_dir = env::var("OUT_DIR").unwrap();
// let mut tasks = Vec::new();
let tasks = vec![
// Global tokens
Task::new(
PathBuf::from("src/ui/_base.scss"),
Path::new(&out_dir).join("style_tokens.rs"),
"style".to_string(),
),
// variables defined by the Panel component
Task::new(
PathBuf::from("src/ui/components/_panel.scss"),
Path::new(&out_dir).join("style_component_panel.rs"),
"panel".to_string(),
),
// Variables set by the Conversations layout
Task::new(
PathBuf::from("src/ui/layouts/conversations.scss"),
Path::new(&out_dir).join("style_layout_conversations.rs"),
"conversations".to_string(),
),
];
export_variables(tasks)
}
// From https://doc.rust-lang.org/rust-by-example/std_misc/file/read_lines.html
// The output is wrapped in a Result to allow matching on errors.
// Returns an Iterator to the Reader of the lines of the file.
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where
P: AsRef<Path>,
{
let file = File::open(filename)?;
Ok(io::BufReader::new(file).lines())
}
#[derive(Debug)]
struct ColorVariable {
name: String,
value: String,
}
impl ColorVariable {
pub fn new(name: String, value: String) -> Self {
Self { name, value }
}
}
impl Display for ColorVariable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"const {name}: &str = \"{value}\";",
name = self.name.replace('-', "_").to_uppercase(),
value = self.value
)
}
}
#[derive(Debug)]
struct FloatVariable {
name: String,
value: f64,
}
impl FloatVariable {
pub fn new(name: String, value: f64) -> Self {
Self { name, value }
}
}
impl Display for FloatVariable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"const {name}: f64 = {value};",
name = self.name.replace('-', "_").to_uppercase(),
value = self.value
)
}
}
struct Task {
src_path: PathBuf,
dst_path: PathBuf,
module_name: String,
}
impl Task {
pub fn new(src_path: PathBuf, dst_path: PathBuf, module_name: String) -> Self {
Self {
src_path,
dst_path,
module_name,
}
}
}
// fn export_variables(src_path: &PathBuf, dst_path: &PathBuf) {
fn export_variables(tasks: Vec<Task>) {
let color_re = Regex::new(r"^\$([^:]+):[[:space:]]*#([^$]+);[[:space:]]*$").unwrap();
let variable_re = Regex::new(r"^\$([^:]+):[[:space:]]*([^;]+)[[:space:]]*;").unwrap();
for task in tasks {
let mut dst_file = File::create(task.dst_path).unwrap();
if let Err(err) = dst_file.write_fmt(format_args!(
"#[allow(dead_code)]\nmod {} {{\n",
task.module_name
)) {
println!("{}", err);
return;
};
let mut variables = Vec::<Box<dyn Display>>::new();
if let Ok(lines) = read_lines(task.src_path) {
for line in lines.map_while(Result::ok) {
if let Some(groups) = color_re.captures(&line) {
let var = ColorVariable::new(groups[1].to_string(), groups[2].to_string());
variables.push(Box::new(var));
} else if let Some(groups) = variable_re.captures(&line) {
if let Ok(value) = groups[2].parse::<f64>() {
variables.push(Box::new(FloatVariable::new(groups[1].to_string(), value)));
}
}
}
}
for variable in variables {
if let Err(err) = dst_file.write_fmt(format_args!(" pub {}\n", variable)) {
println!("{}", err);
break;
}
}
if let Err(err) = dst_file.write(b"}\n") {
println!("{}", err);
};
}
}

298
deny.toml Normal file
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.86 AS builder
RUN apt update \
&& apt install -y --no-install-recommends libclang-dev hunspell \
&& apt clean
RUN rustup default nightly \
&& rustup component add rustfmt clippy
RUN cargo install cargo-binstall
RUN cargo binstall cargo-sort-derives cargo-spellcheck cargo-udeps cargo-deny
FROM debian:trixie-slim
RUN apt update \
&& apt install -y --no-install-recommends ca-certificates git rustup build-essential \
libssl-dev pkg-config libglib2.0-0 libpango-1.0-0 libatk1.0-dev libgdk-pixbuf-2.0-dev \
libcairo2-dev libgtk-3-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev \
&& apt clean
COPY --from=builder /usr/local/rustup/toolchains/nightly-x86_64-unknown-linux-gnu/ /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/
COPY --from=builder /usr/local/cargo/bin /root/.cargo/bin/
RUN rustup default nightly

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 B

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: 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: 306 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 430 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

1
index.html Symbolic link
View File

@@ -0,0 +1 @@
public/index.html

BIN
medias/presentation.mp4 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
medias/presentation.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,92 @@
Geist Sans and Geist Mono Font
(C) 2023 Vercel, made in collaboration with basement.studio
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is available with a FAQ at: http://scripts.sil.org/OFL and copied below
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none" shape-rendering="auto"><desc>"Shapes" by "Florian Körner", licensed under "CC0 1.0". / Remix of the original. - Created with dicebear.com</desc><metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:rdf><cc:work><dc:title>Shapes</dc:title><dc:creator><cc:agent rdf:about="https://www.dicebear.com"><dc:title>Florian Körner</dc:title></cc:agent></dc:creator><dc:source>https://www.dicebear.com</dc:source><cc:license rdf:resource="https://creativecommons.org/publicdomain/zero/1.0/"></cc:license></cc:work></rdf:rdf></metadata><mask id="w6sj6i8m"><rect width="100" height="100" rx="0" ry="0" x="0" y="0" fill="#fff"></rect></mask><g mask="url(#w6sj6i8m)"><rect fill="#E2F2F7" width="100" height="100" x="0" y="0"></rect><g transform="matrix(1.2 0 0 1.2 -10 -10)"><g transform="translate(51, -23) rotate(-38 50 50)"><path d="M0 0h100v100H0V0Z" fill="#83CADE"></path></g></g><g transform="matrix(.8 0 0 .8 10 10)"><g transform="translate(-2, 35) rotate(99 50 50)"><path d="M100 50A50 50 0 1 1 0 50a50 50 0 0 1 100 0Z" fill="#6957A0"></path></g></g><g transform="matrix(.4 0 0 .4 30 30)"><g transform="translate(-18, -6) rotate(-97 50 50)"><path d="m50 7 50 86.6H0L50 7Z" fill="#D53583"></path></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" height="128" width="384" viewBox="0 0 384 128">
<pattern id="p" width="384" height="128" patternUnits="userSpaceOnUse" stroke="#1B1B1B" stroke-linejoin="round" stroke-width="4">
<path fill="#1DB2CF" d="M 9.736 -15 L -30 3.337 l 23.642 -0.088 L -10.212 15 L 30 -3.425 H 6.834 L 9.736 -15 Z"/>
<path fill="#D53583" d="M 201.736 -15 L 162 3.337 l 23.642 -0.088 L 181.788 15 L 222 -3.425 H 198.834 L 201.736 -15 Z"/>
<path fill="#1DB2CF" d="M 393.736 -15 L 354 3.337 l 23.642 -0.088 L 373.788 15 L 414 -3.425 H 390.834 L 393.736 -15 Z"/>
<path fill="#7E6BB6" d="M 105.736 50 L 66 68.337 l 23.642 -0.088 L 85.788 80 L 126 61.575 H 102.834 L 105.736 50 Z"/>
<path fill="#7E6BB6" d="M 297.736 50 L 258 68.337 l 23.642 -0.088 L 277.788 80 L 318 61.575 H 294.834 L 297.736 50 Z"/>
<path fill="#1DB2CF" d="M 9.736 113 L -30 131.337 l 23.642 -0.088 L -10.212 143 L 30 124.575 H 6.834 L 9.736 113 Z"/>
<path fill="#D53583" d="M 201.736 113 L 162 131.337 l 23.642 -0.088 L 181.788 143 L 222 124.575 H 198.834 L 201.736 113 Z"/>
<path fill="#1DB2CF" d="M 393.736 113 L 354 131.337 l 23.642 -0.088 L 373.788 143 L 414 124.575 H 390.834 L 393.736 113 Z"/>
</pattern>
<rect fill="url(#p)" width="100%" height="100%"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

224
public/index.html Normal file
View File

@@ -0,0 +1,224 @@
<!DOCTYPE html>
<html>
<head>
<title>{app_name}</title>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8" />
<style>
html, body {
height: 100%;
width: 100%;
margin: 0;
font-family: "Geist";
font-weight: normal;
}
#main {
height: 100%;
width: 100%;
}
@keyframes multicolor {
0% { fill: #1DB2CF; /* color-primary-100 */ }
33% { fill: #7E6BB6; /* color-secondary-100 */ }
66% { fill: #D53583; /* color-ternary-100 */ }
}
:root {
--wallpaper-pattern-height: 128px;
--spinner-height: 5%;
--window-center-pos: calc(50% + (var(--wallpaper-pattern-height) / 2) - (var(--spinner-height) / 2));
}
/* @media (0px < height <= calc(var(--wallpaper-pattern-height) * 5)) { */
@media (min-height: 0px) and (max-height: 640px) {
:root {
--spinner-top: var(--window-center-pos);
}
}
/* @media (calc($wallpaper-pattern-height * 5) < height <= calc($wallpaper-pattern-height * 6)) { */
@media (min-height: 641px) and (max-height: 768px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 2));
}
}
/* @media (calc($wallpaper-pattern-height * 6) < height <= calc($wallpaper-pattern-height * 8)) { */
@media (min-height: 769px) and (max-height: 1024px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 2));
}
}
/* @media (calc($wallpaper-pattern-height * 8) < height <= calc($wallpaper-pattern-height * 10)) { */
@media (min-height: 1025px) and (max-height: 1280px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 3));
}
}
/* @media (calc($wallpaper-pattern-height * 10) < height <= calc($wallpaper-pattern-height * 12)) { */
@media (min-height: 1281px) and (max-height: 1536px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 4));
}
}
/* @media (calc($wallpaper-pattern-height * 12) < height <= calc($wallpaper-pattern-height * 14)) { */
@media (min-height: 1537px) and (max-height: 1792px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 5));
}
}
/* @media (calc($wallpaper-pattern-height * 14) < height <= calc($wallpaper-pattern-height * 16)) { */
@media (min-height: 1793px) and (max-height: 2048px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 6));
}
}
/* @media (calc($wallpaper-pattern-height * 16) < height <= calc($wallpaper-pattern-height * 18)) { */
@media (min-height: 2049px) and (max-height: 2304px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 7));
}
}
/* @media (calc($wallpaper-pattern-height * 18) < height <= calc($wallpaper-pattern-height * 20)) { */
@media (min-height: 2305px) and (max-height: 2560px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 8));
}
}
/* @media (calc($wallpaper-pattern-height * 20) < height <= calc($wallpaper-pattern-height * 22)) { */
@media (min-height: 2561px) and (max-height: 2816px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 9));
}
}
/* @media (calc($wallpaper-pattern-height * 22) < height <= calc($wallpaper-pattern-height * 24)) { */
@media (min-height: 2817px) and (max-height: 3072px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 10));
}
}
/* @media (calc($wallpaper-pattern-height * 24) < height <= calc($wallpaper-pattern-height * 26)) { */
@media (min-height: 3073px) and (max-height: 3328px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 11));
}
}
/* @media (calc($wallpaper-pattern-height * 26) < height <= calc($wallpaper-pattern-height * 28)) { */
@media (min-height: 3329px) and (max-height: 3584px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 12));
}
}
/* @media (calc($wallpaper-pattern-height * 28) < height <= calc($wallpaper-pattern-height * 30)) { */
@media (min-height: 3585px) and (max-height: 3840px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 13));
}
}
/* @media (calc($wallpaper-pattern-height * 30) < height <= calc($wallpaper-pattern-height * 32)) { */
@media (min-height: 3841px) and (max-height: 4096px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 14));
}
}
/* @media (calc($wallpaper-pattern-height * 32) < height <= calc($wallpaper-pattern-height * 34)) { */
@media (min-height: 4097px) and (max-height: 4352px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 15));
}
}
/* @media (calc($wallpaper-pattern-height * 34) < height <= calc($wallpaper-pattern-height * 36)) { */
@media (min-height: 4353px) and (max-height: 4608px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 16));
}
}
/* @media (calc($wallpaper-pattern-height * 36) < height <= calc($wallpaper-pattern-height * 38)) { */
@media (min-height: 4609px) and (max-height: 4864px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 17));
}
}
/* @media (calc($wallpaper-pattern-height * 38) < height <= calc($wallpaper-pattern-height * 40)) { */
@media (min-height: 4865px) and (max-height: 5120px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 18));
}
}
.loader {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
.wallpaper {
height: 100%;
width: 100%;
z-index: -1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.content {
background-image: url("data:image/svg+xml, <svg xmlns='http://www.w3.org/2000/svg' height='128' width='384' viewBox='0 0 384 128'><pattern id='p' width='384' height='128' patternUnits='userSpaceOnUse' stroke='%231B1B1B' stroke-linejoin='round' stroke-width='4'><path fill='%231DB2CF' d='M 9.736 -15 L -30 3.337 l 23.642 -0.088 L -10.212 15 L 30 -3.425 H 6.834 L 9.736 -15 Z'/><path fill='%23D53583' d='M 201.736 -15 L 162 3.337 l 23.642 -0.088 L 181.788 15 L 222 -3.425 H 198.834 L 201.736 -15 Z'/><path fill='%231DB2CF' d='M 393.736 -15 L 354 3.337 l 23.642 -0.088 L 373.788 15 L 414 -3.425 H 390.834 L 393.736 -15 Z'/><path fill='%237E6BB6' d='M 105.736 50 L 66 68.337 l 23.642 -0.088 L 85.788 80 L 126 61.575 H 102.834 L 105.736 50 Z'/><path fill='%237E6BB6' d='M 297.736 50 L 258 68.337 l 23.642 -0.088 L 277.788 80 L 318 61.575 H 294.834 L 297.736 50 Z'/><path fill='%231DB2CF' d='M 9.736 113 L -30 131.337 l 23.642 -0.088 L -10.212 143 L 30 124.575 H 6.834 L 9.736 113 Z'/><path fill='%23D53583' d='M 201.736 113 L 162 131.337 l 23.642 -0.088 L 181.788 143 L 222 124.575 H 198.834 L 201.736 113 Z'/><path fill='%231DB2CF' d='M 393.736 113 L 354 131.337 l 23.642 -0.088 L 373.788 143 L 414 124.575 H 390.834 L 393.736 113 Z'/></pattern><rect fill='url(%23p)' width='100%' height='100%'/></svg>");
background-position: center;
backgrond-size: var(--wallpaper-pattern-height);
width: 150%;
height: 150%;
}
}
.spinner {
height: var(--spinner-height);
aspect-ratio: 2;
position: absolute;
top: var(--spinner-top);
svg {
--fps: 4;
--duration_sec: 3;
--steps: calc(var(--duration_sec) * var(--fps));
height: 100%;
width: 100%;
fill: #1DB2CF; /* color-primary-100 */
stroke: #1B1B1B; /* greyscale-90 */
animation: 3s multicolor linear infinite;
animation-timing-function: steps(var(--steps), end);
}
}
}
</style>
</head>
<body>
<div id="main">
<div id="preloader" class="loader">
<div class="wallpaper">
<div class="content"></div>
</div>
<div class="spinner">
<svg xmlns="http://www.w3.org/2000/svg" width='184' height='94' viewBox="0 0 184 94">
<path
stroke-linejoin="round"
stroke-width="6"
d="M121.208 2 2 57.011l70.927-.265L61.363 92 182 36.724h-69.498L121.208 2Z"
/>
</svg>
</div>
</div>
</div>
</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,48 +0,0 @@
$font-size: 100vh * 0.01;
$icon-size: $font-size * 2;
body {
height: 100vh;
width: 100vw;
margin: 0px;
padding: 0px;
outline: 0px;
font-family: Tahoma, sans-serif;
}
#main {
height: 100%;
width: 100%;
}
.aeroButton {
height: 50%;
min-height: 16px;
aspect-ratio: 1;
background-color: transparent;
border: 2px solid transparent;
background-size: contain !important;
margin-right: 1%;
}
.aeroButton:hover {
border-image: url(./images/aerobutton_border.png) 2 round;
}
.aeroButton:active {
border-image: url(./images/aerobutton_border_down.png) 2 round;
}
.button {
height: 50%;
min-height: 16px;
aspect-ratio: 1;
background-color: transparent;
border: 2px solid transparent;
background-size: contain !important;
margin-right: 1%;
}
.button:hover {
border-image: url(./images/button_border.png) 2 round;
}
.button:active {
border-image: url(./images/button_border_down.png) 2 round;
}

View File

@@ -1,121 +0,0 @@
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use dioxus_std::utils::rw::UseRw;
use matrix_sdk::room::Room as MatrixRoom;
use matrix_sdk::{
room::RoomMember,
ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId},
};
use crate::matrix_client::Requester;
#[derive(Clone, Debug)]
pub struct UserInfo {
pub avatar_url: Option<OwnedMxcUri>,
pub display_name: Option<String>,
pub blurhash: Option<String>,
}
impl UserInfo {
pub fn new(
avatar_url: Option<OwnedMxcUri>,
display_name: Option<String>,
blurhash: Option<String>,
) -> Self {
Self {
avatar_url,
display_name,
blurhash,
}
}
}
#[derive(Clone, Debug)]
pub struct Room {
pub matrix_room: Arc<MatrixRoom>,
pub topic: Option<String>,
pub members: HashMap<OwnedUserId, RoomMember>,
pub is_direct: Option<bool>,
}
impl Room {
pub fn new(
matrix_room: Arc<MatrixRoom>,
topic: Option<String>,
is_direct: Option<bool>,
) -> Self {
Self {
matrix_room,
topic,
members: HashMap::new(),
is_direct,
}
}
pub fn name(&self) -> Option<String> {
self.matrix_room.name()
}
}
impl PartialEq for Room {
fn eq(&self, other: &Self) -> bool {
// TODO: Look for a better way to compare Matrix rooms
self.matrix_room.room_id() == other.matrix_room.room_id()
&& self.topic == other.topic
&& self.is_direct == other.is_direct
}
}
pub type ByIdRooms = HashMap<OwnedRoomId, Arc<RwLock<Room>>>;
pub type ByIdUserInfos = HashMap<OwnedUserId, UserInfo>;
#[derive(Clone, Debug)]
pub struct Store {
pub is_logged: bool,
pub rooms: ByIdRooms,
pub user_infos: ByIdUserInfos,
pub user_id: Option<OwnedUserId>,
}
impl<'a> Store {
pub fn new() -> Self {
Self {
is_logged: false,
rooms: HashMap::new(),
user_infos: HashMap::new(),
user_id: None,
}
}
}
impl PartialEq for Store {
fn eq(&self, other: &Self) -> bool {
self.is_logged == other.is_logged
&& self.user_id == other.user_id
&& self.user_infos.len() == other.user_infos.len()
&& self
.user_infos
.keys()
.all(|k| other.user_infos.contains_key(k))
&& self.rooms.len() == other.rooms.len()
&& self.rooms.keys().all(|k| other.rooms.contains_key(k))
}
}
impl Eq for Store {}
pub type ReactiveStore = Arc<UseRw<Store>>;
#[derive(Clone)]
pub struct AppSettings {
pub requester: Option<Arc<Requester>>,
}
impl AppSettings {
pub fn new() -> Self {
Self { requester: None }
}
}

View File

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

View File

@@ -1,14 +0,0 @@
.selector {
position: relative;
height: 100%;
aspect-ratio: 1;
.picture {
position: absolute;
height: 75%;
aspect-ratio: 1;
bottom: 17.5%;
right: 18%;
}
}

View File

@@ -1,8 +0,0 @@
use dioxus::prelude::*;
pub fn ChatsWindow(cx: Scope) -> Element {
cx.render(rsx! {
div {
}
})
}

View File

@@ -1 +0,0 @@
pub mod chats_window;

View File

@@ -1,47 +0,0 @@
use std::cell::RefCell;
use dioxus::prelude::*;
use dioxus_std::utils::rw::UseRw;
use crate::base::Room;
use crate::base::Store;
use crate::components::contacts_window::contacts_section::ContactsSection;
turf::style_sheet!("src/components/contacts_window/contacts.scss");
#[inline_props]
pub fn Contacts<'a>(cx: Scope, rw_store: &'a UseRw<Store>) -> Element {
println!("Contacts rendering");
let store = rw_store.read().unwrap();
let rooms = &store.rooms;
let rooms_len = rooms.len();
let mut groups = Vec::<Room>::with_capacity(rooms_len);
let mut directs = Vec::<Room>::with_capacity(rooms_len);
for arc_room in rooms.values() {
let room = arc_room.read().unwrap().to_owned();
let is_direct = room.is_direct.unwrap();
if is_direct {
directs.push(room);
} else {
groups.push(room);
}
}
// TODO: Test overflow
// TODO: Add offline users ?
cx.render(rsx! {
style { STYLE_SHEET },
div {
class: ClassName::CONTACTS,
ContactsSection {name: "Groups", contacts: RefCell::new(groups)},
ContactsSection {name: "Available", contacts: RefCell::new(directs)},
},
})
}

View File

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

View File

@@ -1,87 +0,0 @@
use std::cell::RefCell;
use dioxus::prelude::*;
use dioxus_free_icons::icons::io_icons::IoChevronDown;
use dioxus_free_icons::Icon;
use matrix_sdk::RoomState;
use crate::base::Room;
turf::style_sheet!("src/components/contacts_window/contacts_section.scss");
fn ContactsArrow(cx: Scope) -> Element {
cx.render(rsx! {
style { STYLE_SHEET },
Icon {
icon: IoChevronDown,
},
})
}
static NO_SUBJECT_REPR: &str = "No subject";
#[inline_props]
pub fn ContactsSection<'a>(cx: Scope, name: &'a str, contacts: RefCell<Vec<Room>>) -> Element {
println!("ContactsSection rendering");
let show = use_state(cx, || false);
let classes = vec![
ClassName::SECTION,
if **show { ClassName::ACTIVE } else { "" },
]
.join(" ");
let contacts_len = contacts.borrow().len();
let rendered_contacts = contacts.borrow_mut().clone().into_iter().map(|room| {
let room_name = room.name().unwrap();
let is_invited = room.matrix_room.state() == RoomState::Invited;
let formatted = format!(
"{room_name} - {}",
if is_invited {
format!("Invited - ")
} else {
"".to_string()
}
);
let room_topic = room.topic.unwrap_or(NO_SUBJECT_REPR.to_string()).to_owned();
rsx!(li {
img {
src: "./images/status_online.png",
},
p {
formatted,
},
p {
style: "color: darkgrey;",
room_topic,
},
},
)
});
cx.render(rsx! {
style { STYLE_SHEET },
div {
class: "{classes}",
p {
class: ClassName::HEADER,
onclick: move |_| show.set(!show),
ContactsArrow {},
format!("{name} ({contacts_len})"),
},
ul {
rendered_contacts.into_iter(),
},
},
})
}

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,94 +0,0 @@
use dioxus::prelude::*;
use dioxus_std::utils::rw::UseRw;
use crate::base::Store;
use crate::components::contacts_window::contacts::Contacts;
use crate::components::contacts_window::user_infos::UserInfos;
turf::style_sheet!("src/components/contacts_window/contacts_window.scss");
#[inline_props]
pub fn ContactsWindow<'a>(cx: Scope, rw_store: &'a UseRw<Store>) -> Element {
cx.render(rsx! {
style { STYLE_SHEET },
div {
class: ClassName::CONTACTS_WINDOW,
div {
class: ClassName::HEADER,
div {
class: ClassName::TITLE_BAR,
},
div {
class: ClassName::USER_INFO,
},
UserInfos {rw_store: rw_store},
},
div {
class: ClassName::CONTACTS_NAV,
div {
class: ClassName::INNER,
button {
class: ClassName::AERO_BUTTON,
style: "background: url(./images/letter.png) center no-repeat",
},
button {
class: ClassName::AERO_BUTTON,
style: "background: url(./images/directory.png) no-repeat center",
},
button {
class: ClassName::AERO_BUTTON,
style: "background: url(./images/news.png) no-repeat center",
},
button {
class: ClassName::FLEX_RIGHT_AERO_BUTTON,
style: "background: url(./images/brush.png) no-repeat center",
},
button {
class: ClassName::AERO_BUTTON,
style: "background: url(./images/settings.png) no-repeat center",
},
},
},
div {
class: ClassName::SEARCH,
div {
class: ClassName::INNER,
input {
class: ClassName::SEARCH_INPUT,
placeholder: "Find a contact...",
r#type: "text",
},
button {
class: ClassName::BUTTON,
style: "background: url(./images/add_user.png) no-repeat center",
},
button {
class: ClassName::BUTTON,
style: "background: url(./images/tbc_transfert.png) no-repeat center",
},
},
},
Contacts {rw_store: rw_store},
div {
class: ClassName::FOOTER,
},
},
})
}

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,5 +0,0 @@
pub mod contacts_window;
mod contacts;
mod contacts_section;
mod user_infos;

View File

@@ -1,72 +0,0 @@
use dioxus::prelude::*;
use dioxus_free_icons::icons::md_navigation_icons::MdArrowDropDown;
use dioxus_free_icons::Icon;
use dioxus_std::utils::rw::UseRw;
use crate::base::Store;
use crate::components::avatar_selector::AvatarSelector;
turf::style_sheet!("src/components/contacts_window/user_infos.scss");
fn DownArrowIcon(cx: Scope) -> Element {
cx.render(rsx! {
style { STYLE_SHEET },
Icon {
class: ClassName::DOWN_ARROW_ICON,
icon: MdArrowDropDown,
}
})
}
static MESSAGE_PLACEHOLDER: &str = "<Enter a personal message>";
#[inline_props]
pub fn UserInfos<'a>(cx: Scope, rw_store: &'a UseRw<Store>) -> Element {
println!("UserInfos rendering");
let store = rw_store.read().unwrap().clone();
let user_id = store.user_id.unwrap();
let user_info = store.user_infos.get(&user_id).unwrap();
let user_display_name = user_info.display_name.as_ref().unwrap();
cx.render(rsx! {
style { STYLE_SHEET },
div {
class: ClassName::USER_INFO,
div {
class: ClassName::AVATAR_SELECTOR,
AvatarSelector {},
},
div {
class: ClassName::INFO_CONTAINER,
div {
class: ClassName::USER_ID,
p {
class: ClassName::USER_NAME,
"{user_display_name}",
},
p {
class: ClassName::USER_STATUS,
"(Busy)",
},
DownArrowIcon {},
},
div {
class: ClassName::USER_MESSAGE,
p {
// TODO: Handle user message
MESSAGE_PLACEHOLDER,
}
DownArrowIcon {},
},
},
},
})
}

View File

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

View File

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

View File

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

View File

@@ -1,148 +0,0 @@
use std::str::FromStr;
use std::sync::Arc;
use dioxus::prelude::*;
use dioxus_std::utils::rw::UseRw;
use crate::base::{AppSettings, Store};
use crate::components::avatar_selector::AvatarSelector;
use crate::components::header::Header;
use crate::matrix_client::{LoginStyle, MatrixClient};
turf::style_sheet!("src/components/login.scss");
static EMPTY_PLACEHOLDER: &str = "Tmp placeholder";
#[inline_props]
pub fn Login<'a>(cx: Scope, rw_store: &'a UseRw<Store>) -> Element {
let app_context = use_shared_state::<AppSettings>(cx).unwrap();
let invalid_login = use_state(cx, || false);
let login = use_ref(cx, || Login::new());
let arc_store = Arc::new(rw_store.to_owned().clone());
let password_class = if **invalid_login {
ClassName::INVALID_INPUT
} else {
""
};
let run_matrix_client = move |_| {
cx.spawn({
to_owned![app_context, invalid_login, login, arc_store];
let login_ref = login.read();
let homeserver_url = login_ref.homeserver_url.clone().unwrap();
let username = login_ref.email.clone().unwrap();
let password = login_ref.password.clone().unwrap();
async move {
let requester = MatrixClient::spawn(homeserver_url, arc_store.clone()).await;
requester.init();
match requester.login(LoginStyle::Password(username, password)) {
Ok(_) => {
println!("successfully logged");
}
Err(err) => {
println!("Error during login: {err}");
invalid_login.modify(|_| true);
}
}
app_context.write().requester = Some(Arc::new(requester));
}
});
};
let login_ref = login.read();
let placeholder = EMPTY_PLACEHOLDER.to_string();
let homeserver_url_value = login_ref.homeserver_url.as_ref().unwrap_or(&placeholder);
let email_value = login_ref.email.as_ref().unwrap_or(&placeholder);
let password_value = login_ref.password.as_ref().unwrap_or(&placeholder);
cx.render(rsx! {
style { STYLE_SHEET },
div {
class: ClassName::ROOT,
div {
class: ClassName::HEADER,
Header {},
},
div {
class: ClassName::BODY,
div {
class: ClassName::AVATAR_SELECTOR,
AvatarSelector {},
},
p {
"Matrix homeserver:"
},
input {
id: "input-homeserver-url",
r#type: "text",
name: "homeserver URL",
value: "{homeserver_url_value}",
oninput: move |evt| login.write().homeserver_url = Some(evt.value.clone()),
},
p {
"E-mail address:"
},
input {
id: "login-input-email",
r#type: "text",
name: "email",
value: "{email_value}",
oninput: move |evt| login.write().email = Some(evt.value.clone()),
},
p {
"Password:"
},
input {
class: "{password_class}",
id: "login-input-password",
r#type: "password",
name: "Password",
value: "{password_value}",
oninput: move |evt| {
login.write().password = Some(evt.value.clone());
invalid_login.set(false);
},
},
div {
class: ClassName::FOOTER_BUTTONS,
input {
class: ClassName::BUTTON,
onclick: run_matrix_client,
r#type: "submit",
value: "sign in",
},
},
},
},
})
}
#[derive(Debug)]
struct Login {
homeserver_url: Option<String>,
email: Option<String>,
password: Option<String>,
}
impl Login {
fn new() -> Self {
let login = Self {
homeserver_url: None,
email: None,
password: None,
};
login
}
}

View File

@@ -1,51 +0,0 @@
@import "../_base.scss";
.root {
width: 90%;
height: 98%;
display: flex;
flex-direction: column;
align-items: center;
padding: 5%;
padding-top: 2%;
background: linear-gradient(rgb(138, 191, 209), rgb(236, 246, 249) 10%);
.header {
height: 5%;
width: 100%;
}
.body {
height: 50%;
width: 50%;
max-width: 400px;
display: flex;
flex-direction: column;
justify-content: center;
padding-bottom: 3%;
.invalidInput {
border-color: red;
}
.avatarSelector {
height: 30%;
width: 100%;
}
.footerButtons {
width: 100%;
padding-top: 5%;
display: flex;
justify-content: center;
}
}
}

View File

@@ -1,22 +0,0 @@
use dioxus::prelude::*;
use dioxus_std::utils::rw::use_rw;
use crate::base::Store;
use crate::components::contacts_window::contacts_window::ContactsWindow;
use crate::components::login::Login;
#[inline_props]
pub fn MainWindow(cx: Scope) -> Element {
let rw_store = use_rw(cx, || Store::new());
let is_logged = rw_store.read().unwrap().is_logged;
cx.render(rsx! {
if is_logged {
rsx!(ContactsWindow {rw_store: rw_store})
}
else {
rsx!(Login {rw_store: rw_store})
}
})
}

View File

@@ -1,6 +0,0 @@
pub mod avatar_selector;
pub mod chats_window;
pub mod contacts_window;
pub mod header;
pub mod login;
pub mod main_window;

1
src/domain/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub(crate) mod model;

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

8
src/domain/model/mod.rs Normal file
View File

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

323
src/domain/model/room.rs Normal file
View File

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

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

View File

@@ -0,0 +1,26 @@
pub struct Session {
pub homeserver_url: Option<String>,
pub username: Option<String>,
pub password: Option<String>,
pub is_logged: bool,
}
impl Session {
pub fn new() -> Self {
Self {
homeserver_url: None,
username: None,
password: None,
is_logged: false,
}
}
pub fn update(
&mut self,
homeserver_url: Option<String>,
username: Option<String>,
password: Option<String>,
) {
self.homeserver_url = homeserver_url;
self.username = username;
self.password = password;
}
}

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

View File

@@ -0,0 +1,841 @@
use std::{
borrow::Borrow,
collections::HashMap,
sync::{Arc, Mutex},
};
use async_std::stream::StreamExt;
use dioxus::prelude::Task;
use matrix_sdk::{
config::SyncSettings,
event_handler::Ctx,
media::{MediaFormat, MediaRequest, MediaThumbnailSettings, MediaThumbnailSize},
room::{ParentSpace, Room},
ruma::{
api::client::media::get_content_thumbnail::v3::Method,
events::{
room::{
avatar::{RoomAvatarEventContent, StrippedRoomAvatarEvent},
create::{RoomCreateEventContent, StrippedRoomCreateEvent},
member::{MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent},
name::{RoomNameEventContent, StrippedRoomNameEvent},
topic::{RoomTopicEventContent, StrippedRoomTopicEvent},
MediaSource,
},
SyncStateEvent,
},
uint, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UserId,
},
Client as MatrixClient, RoomState,
};
use tokio::sync::{
broadcast,
broadcast::{error::SendError, Receiver, Sender},
mpsc::{unbounded_channel, UnboundedReceiver},
};
use tracing::{debug, debug_span, error, instrument, warn, Instrument, Span};
use super::{
account_event::AccountEvent,
requester::Requester,
room_event::{RoomEvent, RoomEventsReceiver},
worker_tasks::{LoginStyle, WorkerTask},
};
use crate::utils::oneshot;
#[derive(Debug, thiserror::Error)]
pub enum ClientError {
#[error("Matrix client error: {0}")]
Matrix(#[from] matrix_sdk::Error),
}
#[derive(Clone)]
struct Senders {
account_events_sender: Sender<AccountEvent>,
room_events_senders: Arc<Mutex<HashMap<OwnedRoomId, Sender<RoomEvent>>>>,
}
impl Senders {
fn new(account_events_sender: Sender<AccountEvent>) -> Self {
Self {
account_events_sender,
room_events_senders: Arc::new(Mutex::new(HashMap::new())),
}
}
fn contains(&self, room_id: &RoomId) -> bool {
let room_senders = self.room_events_senders.lock().unwrap();
room_senders.contains_key(room_id)
}
fn send(&self, room_id: &RoomId, event: RoomEvent) -> Result<(), SendError<RoomEvent>> {
let room_senders = self.room_events_senders.lock().unwrap();
if let Some(room_sender) = room_senders.get(room_id) {
if let Err(err) = room_sender.send(event) {
warn!("Unable to send event to the {room_id} room: {err}");
return Err(err);
}
} else {
warn!("No sender found for {room_id} room");
// TODO: Return error
}
Ok(())
}
fn add_room(&self, room_id: &OwnedRoomId) -> Option<RoomEventsReceiver> {
let mut senders = self.room_events_senders.lock().unwrap();
if !senders.contains_key(room_id) {
let (room_sender, room_receiver) = broadcast::channel(32);
senders.insert(room_id.clone(), room_sender);
debug!("Create sender for {room_id} room");
Some(RoomEventsReceiver::new(room_receiver))
} else {
None
}
}
}
pub struct Client {
initialized: bool,
client: Option<Arc<MatrixClient>>,
sync_task: Option<Task>,
senders: Senders,
}
impl Client {
pub fn new(client: Arc<MatrixClient>, account_events_sender: Sender<AccountEvent>) -> Self {
Self {
initialized: false,
client: Some(client),
sync_task: None,
senders: Senders::new(account_events_sender),
}
}
#[instrument(skip_all)]
async fn create_space(
senders: &Ctx<Senders>,
room_id: &OwnedRoomId,
room: Option<&Room>,
) -> anyhow::Result<(), SendError<AccountEvent>> {
if let Some(receiver) = senders.add_room(room_id) {
let current_span = Span::current();
let mut name = None;
let mut topic = None;
if let Some(room) = room {
name = room.name();
topic = room.topic();
}
let (reply, mut response) = oneshot::<bool>();
// We can't use Room instance here, because dyn PaginableRoom is not Sync
let event = AccountEvent::NewSpace(
room_id.clone(),
name.clone(),
topic.clone(),
receiver,
reply,
current_span.clone(),
);
senders.account_events_sender.send(event)?;
// We're expecting a response indicating that the client is able to compute the next RoomEvent
response.recv().await;
let events = vec![
RoomEvent::NewTopic(topic, current_span.clone()),
RoomEvent::NewName(name, current_span),
];
for event in events {
if let Err(_err) = senders.send(room_id, event.clone()) {
// TODO: Return an error
}
}
}
Ok(())
}
#[instrument(skip_all)]
async fn create_room(
senders: &Ctx<Senders>,
room: &Room,
) -> anyhow::Result<(), SendError<AccountEvent>> {
let room_id = room.room_id().to_owned();
if let Some(receiver) = senders.add_room(&room_id) {
let (reply, mut response) = oneshot::<bool>();
let is_direct = match room.is_direct().await {
Ok(is_direct) => Some(is_direct),
Err(err) => {
warn!("Unable to know if the {room_id} room is direct: {err}");
None
}
};
let mut parents = vec![];
if let Ok(mut spaces) = room.parent_spaces().await {
while let Some(parent) = spaces.next().await {
match parent {
Ok(parent) => match parent {
ParentSpace::Reciprocal(parent) => {
parents.push(parent.room_id().to_owned());
}
_ => todo!(),
},
Err(err) => {
error!("{err}");
}
}
}
}
// We can't use Room instance here, because dyn PaginableRoom is not Sync
let event = AccountEvent::NewRoom(
room_id.clone(),
parents.clone(),
room.name(),
room.topic(),
is_direct,
room.state(),
receiver,
reply,
Span::current(),
);
senders.account_events_sender.send(event)?;
// We're expecting a response indicating that the client is able to compute the next RoomEvent
response.recv().await;
}
Ok(())
}
#[instrument(skip_all)]
async fn add_room(
senders: &Ctx<Senders>,
room: &Room,
) -> anyhow::Result<(), SendError<AccountEvent>> {
let room_id = room.room_id().to_owned();
if room.is_space() {
Self::create_space(senders, &room_id, Some(room)).await
} else {
let mut parents = vec![];
if let Ok(mut spaces) = room.parent_spaces().await {
while let Some(parent) = spaces.next().await {
match parent {
Ok(parent) => match parent {
ParentSpace::Reciprocal(parent) => {
parents.push(parent.room_id().to_owned());
}
_ => {
warn!(
"Only ParentSpace::Reciprocal taken into account, skip {:?}",
parent
);
}
},
Err(err) => {
error!("{err}");
}
}
}
}
for parent in parents {
// Create a minimal space to make the relation consistent... its content will be sync later.
if !senders.contains(&parent) {
let _ = Self::create_space(senders, &parent, None).await;
}
let event = RoomEvent::NewChild(room_id.clone(), Span::current());
if let Err(_err) = senders.send(&parent, event) {
// TODO: Return an error
}
}
Self::create_room(senders, room).await
}
}
async fn on_stripped_room_create_event(
_ev: StrippedRoomCreateEvent,
room: Room,
senders: Ctx<Senders>,
) {
let span = debug_span!("Matrix::NewRoom", r = ?room.room_id());
let _ = Self::add_room(&senders, &room).instrument(span).await;
}
// SyncStateEvent: A possibly-redacted state event without a room_id.
async fn on_sync_room_create_event(
_ev: SyncStateEvent<RoomCreateEventContent>,
room: Room,
senders: Ctx<Senders>,
) {
let span = debug_span!("Matrix::NewRoom", r = ?room.room_id());
let _ = Self::add_room(&senders, &room).instrument(span).await;
}
#[instrument(skip_all)]
fn on_invite_room_member_event(
user_id: OwnedUserId,
inviter_id: OwnedUserId,
room: &Room,
matrix_client: &MatrixClient,
senders: &Ctx<Senders>,
) {
if let Some(client_user_id) = matrix_client.user_id() {
let room_id = room.room_id();
let is_account_user = user_id == client_user_id;
debug!(
"{} (account user: {is_account_user}) invited by {} to join the {} room",
&user_id, &inviter_id, &room_id
);
let event =
RoomEvent::Invitation(user_id, inviter_id, is_account_user, Span::current());
if let Err(_err) = senders.send(room_id, event) {
// TODO: Return an error
}
}
}
#[instrument(skip_all)]
fn on_join_room_member_event(
user_id: OwnedUserId,
displayname: Option<String>,
avatar_url: Option<OwnedMxcUri>,
room: &Room,
matrix_client: &MatrixClient,
senders: &Ctx<Senders>,
) {
if let Some(client_user_id) = matrix_client.user_id() {
let is_account_user = user_id == client_user_id;
let room_id = room.room_id();
debug!("{} has joined the {} room", &user_id, &room_id);
let event = RoomEvent::Join(
user_id,
displayname,
avatar_url,
is_account_user,
Span::current(),
);
if let Err(_err) = senders.send(room_id, event) {
// TODO: Return an error
}
}
}
// This function is called on each m.room.member event for an invited room preview (room not already joined).
async fn on_stripped_room_member_event(
ev: StrippedRoomMemberEvent,
matrix_client: MatrixClient,
room: Room,
senders: Ctx<Senders>,
) {
match room.state() {
RoomState::Invited => {
let user_id = &ev.state_key;
match ev.content.membership {
MembershipState::Invite => {
let span = debug_span!("Matrix::RoomInvitation", r = ?room.room_id());
span.in_scope(|| {
Self::on_invite_room_member_event(
user_id.clone(),
ev.sender,
&room,
&matrix_client,
&senders,
)
});
}
MembershipState::Join => {
let span =
debug_span!("Matrix::RoomJoin", r = ?room.room_id(), u = ?user_id)
.entered();
span.in_scope(|| {
Self::on_join_room_member_event(
ev.sender,
ev.content.displayname,
ev.content.avatar_url,
&room,
&matrix_client,
&senders,
)
});
}
_ => {
error!("TODO: {:?}", ev);
}
}
}
_ => {
error!("TODO: {:?}", ev);
}
}
}
// SyncStateEvent: A possibly-redacted state event without a room_id.
// RoomMemberEventContent: The content of an m.room.member event.
async fn on_sync_room_member_event(
ev: SyncStateEvent<RoomMemberEventContent>,
matrix_client: MatrixClient,
room: Room,
senders: Ctx<Senders>,
) {
if let SyncStateEvent::Original(ev) = ev {
match ev.content.membership {
MembershipState::Invite => {
let span = debug_span!("Matrix::RoomInvitation", r = ?room.room_id());
span.in_scope(|| {
let invitee_id = ev.state_key;
Self::on_invite_room_member_event(
invitee_id,
ev.sender,
&room,
&matrix_client,
&senders,
)
});
}
MembershipState::Join => {
let user_id = ev.sender;
let span = debug_span!("Matrix::RoomJoin", r = ?room.room_id(), u = ?user_id)
.entered();
span.in_scope(|| {
Self::on_join_room_member_event(
user_id,
ev.content.displayname,
ev.content.avatar_url,
&room,
&matrix_client,
&senders,
)
});
}
_ => error!("TODO"),
}
}
}
#[instrument(skip_all)]
async fn on_room_avatar_event(room: &Room, senders: &Ctx<Senders>) {
let room_id = room.room_id();
let avatar = match room
.avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
size: MediaThumbnailSize {
method: Method::Scale,
width: uint!(256),
height: uint!(256),
},
animated: false,
}))
.await
{
Ok(avatar) => avatar,
Err(err) => {
warn!("Unable to fetch avatar for {}: {err}", &room_id);
None
}
};
let event = RoomEvent::NewAvatar(avatar, Span::current());
if let Err(_err) = senders.send(room_id, event) {
// TODO: Return an error
}
}
async fn on_stripped_room_avatar_event(
_ev: StrippedRoomAvatarEvent,
room: Room,
senders: Ctx<Senders>,
) {
let span = debug_span!("Matrix::RoomAvatar", r = ?room.room_id());
Self::on_room_avatar_event(&room, &senders)
.instrument(span)
.await;
}
async fn on_sync_room_avatar_event(
ev: SyncStateEvent<RoomAvatarEventContent>,
room: Room,
senders: Ctx<Senders>,
) {
if let SyncStateEvent::Original(_ev) = ev {
dioxus::prelude::spawn(async move {
let span = debug_span!("Matrix::RoomAvatar", r = ?room.room_id());
Self::on_room_avatar_event(&room, &senders)
.instrument(span)
.await;
});
}
}
#[instrument(skip_all)]
fn on_room_name_event(name: Option<String>, room: &Room, senders: &Ctx<Senders>) {
let event = RoomEvent::NewName(name, Span::current());
if let Err(_err) = senders.send(room.room_id(), event) {
// TODO: Return an error
}
}
async fn on_stripped_room_name_event(
ev: StrippedRoomNameEvent,
room: Room,
senders: Ctx<Senders>,
) {
let span = debug_span!("Matrix::RoomName", r = ?room.room_id());
span.in_scope(|| {
Self::on_room_name_event(ev.content.name, &room, &senders);
});
}
async fn on_sync_room_name_event(
ev: SyncStateEvent<RoomNameEventContent>,
room: Room,
senders: Ctx<Senders>,
) {
if let SyncStateEvent::Original(ev) = ev {
let span = debug_span!("Matrix::RoomName", r = ?room.room_id());
span.in_scope(|| {
Self::on_room_name_event(Some(ev.content.name), &room, &senders);
});
}
}
#[instrument(skip_all)]
fn on_room_topic_event(topic: Option<String>, room: &Room, senders: &Ctx<Senders>) {
let event = RoomEvent::NewTopic(topic, Span::current());
if let Err(_err) = senders.send(room.room_id(), event) {
// TODO: Return an error
}
}
async fn on_stripped_room_topic_event(
ev: StrippedRoomTopicEvent,
room: Room,
senders: Ctx<Senders>,
) {
let span = debug_span!("Matrix::RoomTopic", r = ?room.room_id());
span.in_scope(|| {
Self::on_room_topic_event(ev.content.topic, &room, &senders);
});
}
async fn on_sync_room_topic_event(
ev: SyncStateEvent<RoomTopicEventContent>,
room: Room,
senders: Ctx<Senders>,
) {
if let SyncStateEvent::Original(ev) = ev {
let span = debug_span!("Matrix::RoomTopic", r = ?room.room_id());
span.in_scope(|| {
Self::on_room_topic_event(Some(ev.content.topic), &room, &senders);
});
}
}
pub async fn spawn(homeserver_url: String) -> (Requester, Receiver<AccountEvent>) {
let matrix_client = Arc::new(
MatrixClient::builder()
.homeserver_url(&homeserver_url)
.build()
.await
.unwrap(),
);
let (worker_tasks_sender, worker_tasks_receiver) = unbounded_channel::<WorkerTask>();
let (account_events_sender, account_events_receiver) =
broadcast::channel::<AccountEvent>(32);
let mut client = Client::new(matrix_client, account_events_sender);
dioxus::prelude::spawn(async move {
client.work(worker_tasks_receiver).await;
});
(Requester::new(worker_tasks_sender), account_events_receiver)
}
fn init(&mut self) {
if let Some(client) = self.client.borrow() {
// TODO: Remove clone?
client.add_event_handler_context(self.senders.clone());
let _ = client.add_event_handler(Client::on_stripped_room_create_event);
let _ = client.add_event_handler(Client::on_sync_room_create_event);
let _ = client.add_event_handler(Client::on_stripped_room_member_event);
let _ = client.add_event_handler(Client::on_sync_room_member_event);
let _ = client.add_event_handler(Client::on_stripped_room_avatar_event);
let _ = client.add_event_handler(Client::on_sync_room_avatar_event);
let _ = client.add_event_handler(Client::on_stripped_room_name_event);
let _ = client.add_event_handler(Client::on_sync_room_name_event);
let _ = client.add_event_handler(Client::on_stripped_room_topic_event);
let _ = client.add_event_handler(Client::on_sync_room_topic_event);
self.initialized = true;
}
}
async fn login(&mut self, style: LoginStyle) -> anyhow::Result<()> {
let client = self.client.as_ref().unwrap();
match style {
LoginStyle::Password(username, password) => {
client
.matrix_auth()
.login_username(&username, &password)
.initial_device_display_name("TODO")
.send()
.await
.map_err(ClientError::from)?;
}
}
Ok(())
}
async fn run_forever(&mut self) {
let client = self.client.clone().unwrap();
let task = dioxus::prelude::spawn(async move {
// Sync once so we receive the client state and old messages
let sync_token_option = match client.sync_once(SyncSettings::default()).await {
Ok(sync_response) => Some(sync_response.next_batch),
Err(err) => {
error!("Error during sync one: {}", err);
None
}
};
if let Some(sync_token) = sync_token_option {
debug!("User connected to the homeserver, start syncing");
let settings = SyncSettings::default().token(sync_token);
let _ = client.sync(settings).await;
}
});
self.sync_task = Some(task);
}
async fn get_display_name(&mut self) -> anyhow::Result<Option<String>> {
let client = self.client.as_ref().unwrap();
match client.account().get_display_name().await {
Ok(display_name) => Ok(display_name),
Err(err) => Err(err.into()),
}
}
async fn get_avatar(&mut self) -> anyhow::Result<Option<Vec<u8>>> {
let client = self.client.as_ref().unwrap();
match client
.account()
.get_avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
size: MediaThumbnailSize {
method: Method::Scale,
width: uint!(256),
height: uint!(256),
},
animated: false,
}))
.await
{
Ok(avatar) => Ok(avatar),
Err(err) => Err(err.into()),
}
}
async fn get_room_avatar(&mut self, room_id: &OwnedRoomId) -> anyhow::Result<Option<Vec<u8>>> {
let client = self.client.as_ref().unwrap();
if let Some(room) = client.get_room(room_id) {
match room
.avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
size: MediaThumbnailSize {
method: Method::Scale,
width: uint!(256),
height: uint!(256),
},
animated: false,
}))
.await
{
Ok(avatar) => Ok(avatar),
Err(err) => Err(err.into()),
}
} else {
warn!("No room found with the \"{}\" id", room_id.as_str());
// TODO: Return an error if the room has not been found
Ok(None)
}
}
// TODO: Share MediaRequest with other media requests
async fn get_thumbnail(&self, media_url: OwnedMxcUri) -> anyhow::Result<Vec<u8>> {
let client = self.client.as_ref().unwrap();
let media = client.media();
let request = MediaRequest {
source: MediaSource::Plain(media_url),
format: MediaFormat::Thumbnail(MediaThumbnailSettings {
size: MediaThumbnailSize {
method: Method::Scale,
width: uint!(256),
height: uint!(256),
},
animated: false,
}),
};
let res = media.get_media_content(&request, true).await;
Ok(res?)
}
async fn get_room_member_avatar(
&self,
avatar_url: &Option<OwnedMxcUri>,
room_id: &RoomId,
user_id: &UserId,
) -> anyhow::Result<Option<Vec<u8>>> {
let client = self.client.as_ref().unwrap();
if let Some(room) = client.get_room(room_id) {
match avatar_url {
Some(avatar_url) => {
let thumbnail = self.get_thumbnail(avatar_url.clone()).await;
return Ok(Some(thumbnail?));
}
None => match room.get_member(user_id).await {
Ok(room_member) => {
if let Some(room_member) = room_member {
let res = match room_member
.avatar(MediaFormat::Thumbnail(MediaThumbnailSettings {
size: MediaThumbnailSize {
method: Method::Scale,
width: uint!(256),
height: uint!(256),
},
animated: false,
}))
.await
{
Ok(avatar) => Ok(avatar),
Err(err) => Err(err.into()),
};
return res;
}
}
Err(err) => {
warn!("Unable to get room member {user_id}: {err}");
}
},
}
}
Ok(None)
}
async fn join_room(&self, room_id: &RoomId) -> anyhow::Result<bool> {
let client = self.client.as_ref().unwrap();
if let Some(room) = client.get_room(room_id) {
return match room.join().await {
Ok(_) => Ok(true),
Err(err) => Err(err.into()),
};
}
Ok(false)
}
async fn work(&mut self, mut rx: UnboundedReceiver<WorkerTask>) {
while let Some(task) = rx.recv().await {
self.run(task).await;
}
if let Some(task) = self.sync_task.take() {
task.cancel()
}
}
async fn run(&mut self, task: WorkerTask) {
match task {
WorkerTask::Init(reply) => {
self.init();
reply.send(Ok(())).await;
}
WorkerTask::RunForever(reply) => {
{
self.run_forever().await;
reply.send(())
}
.await
}
WorkerTask::Login(style, reply) => {
reply.send(self.login(style).await).await;
}
WorkerTask::GetDisplayName(reply) => {
reply.send(self.get_display_name().await).await;
}
WorkerTask::GetAvatar(reply) => {
reply.send(self.get_avatar().await).await;
}
WorkerTask::GetRoomAvatar(id, reply) => {
reply.send(self.get_room_avatar(&id).await).await;
}
WorkerTask::GetRoomMemberAvatar(avatar_url, room_id, user_id, reply) => {
reply
.send(
self.get_room_member_avatar(&avatar_url, &room_id, &user_id)
.await,
)
.await;
}
WorkerTask::JoinRoom(id, reply) => {
reply.send(self.join_room(&id).await).await;
}
}
}
}

View File

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

View File

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

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

@@ -0,0 +1,71 @@
use std::fmt::{Debug, Formatter};
use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId};
use crate::utils::Sender;
#[derive(Debug)]
pub enum LoginStyle {
Password(String, String),
}
pub enum WorkerTask {
Init(Sender<anyhow::Result<()>>),
Login(LoginStyle, Sender<anyhow::Result<()>>),
RunForever(Sender<()>),
GetDisplayName(Sender<anyhow::Result<Option<String>>>),
GetAvatar(Sender<anyhow::Result<Option<Vec<u8>>>>),
GetRoomAvatar(OwnedRoomId, Sender<anyhow::Result<Option<Vec<u8>>>>),
GetRoomMemberAvatar(
Option<OwnedMxcUri>,
OwnedRoomId,
OwnedUserId,
Sender<anyhow::Result<Option<Vec<u8>>>>,
),
JoinRoom(OwnedRoomId, Sender<anyhow::Result<bool>>),
}
impl Debug for WorkerTask {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
WorkerTask::Init(_) => f
.debug_tuple("WorkerTask::Init")
.field(&format_args!("_"))
// .field(&format_args!("_"))
.finish(),
WorkerTask::RunForever(_) => f
.debug_tuple("WorkerTask::RunForever")
.field(&format_args!("_"))
.finish(),
WorkerTask::Login(style, _) => f
.debug_tuple("WorkerTask::Login")
.field(style)
// .field(&format_args!("_"))
.finish(),
WorkerTask::GetDisplayName(_) => f
.debug_tuple("WorkerTask::GetDisplayName")
.field(&format_args!("_"))
.finish(),
WorkerTask::GetAvatar(_) => f
.debug_tuple("WorkerTask::GetAvatar")
.field(&format_args!("_"))
.finish(),
WorkerTask::GetRoomAvatar(id, _) => f
.debug_tuple("WorkerTask::GetRoomAvatar")
.field(id)
.finish(),
WorkerTask::GetRoomMemberAvatar(room_id, user_id, avatar_url, _) => f
.debug_tuple("WorkerTask::GetRoomMemberAvatar")
.field(avatar_url)
.field(room_id)
.field(user_id)
.finish(),
WorkerTask::JoinRoom(room_id, _) => f
.debug_tuple("WorkerTask::JoinRoom")
.field(room_id)
.finish(),
}
}
}

View File

@@ -0,0 +1 @@
pub(crate) mod matrix;

View File

@@ -0,0 +1,2 @@
pub(crate) mod messaging;
pub(crate) mod services;

View File

@@ -0,0 +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

@@ -0,0 +1,268 @@
use std::fmt;
use std::future::Future;
use std::sync::OnceLock;
use std::{collections::HashMap, future::IntoFuture};
use rand::distributions::{Alphanumeric, DistString};
use reqwest::Result as RequestResult;
use tracing::error;
cfg_if! {
if #[cfg(target_family = "wasm")] {
use web_sys;
} else {
use tokio::fs::read_to_string;
}
}
#[derive(Eq, Hash, PartialEq)]
pub enum AvatarFeeling {
Ok,
Warning,
Alerting,
}
impl fmt::Display for AvatarFeeling {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let repr = match self {
Self::Ok => "Ok",
Self::Warning => "Warning",
Self::Alerting => "Alerting",
};
write!(f, "{repr}")
}
}
pub struct AvatarConfig<'a> {
feeling: AvatarFeeling,
background_color: &'a str,
}
impl<'a> AvatarConfig<'a> {
pub fn new(feeling: AvatarFeeling, background_color: &'a str) -> Self {
Self {
feeling,
background_color,
}
}
}
enum DicebearType {
Notionists,
Shapes,
}
impl fmt::Display for DicebearType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let repr = match self {
Self::Notionists => "notionists",
Self::Shapes => "shapes",
};
write!(f, "{repr}")
}
}
struct DicebearConfig<'a> {
gesture: &'a str,
browns: Vec<u32>,
eyes: Vec<u32>,
lips: Vec<u32>,
}
fn avatar_variants() -> &'static HashMap<AvatarFeeling, DicebearConfig<'static>> {
static VARIANTS: OnceLock<HashMap<AvatarFeeling, DicebearConfig>> = OnceLock::new();
VARIANTS.get_or_init(|| {
let mut variants = HashMap::new();
variants.insert(
AvatarFeeling::Alerting,
DicebearConfig {
gesture: "wavePointLongArms",
browns: vec![2, 6, 11, 13],
eyes: vec![2, 4],
lips: vec![1, 2, 7, 11, 19, 20, 24, 27],
},
);
variants.insert(
AvatarFeeling::Warning,
DicebearConfig {
gesture: "pointLongArm",
browns: vec![2, 5, 10, 13],
eyes: vec![1, 3],
lips: vec![1, 2, 4, 8, 10, 13, 18, 21, 29],
},
);
variants.insert(
AvatarFeeling::Ok,
DicebearConfig {
gesture: "okLongArm",
browns: vec![1, 3, 4, 7, 8, 9, 12],
eyes: vec![5],
lips: vec![3, 5, 9, 14, 17, 22, 23, 25, 30],
},
);
variants
})
}
fn render_dicebear_variants(values: &[u32]) -> String {
values
.iter()
.map(|l| format!("variant{:02}", l))
.collect::<Vec<String>>()
.join(",")
}
async fn fetch_text(req: String) -> RequestResult<String> {
match reqwest::get(req).await?.error_for_status() {
Ok(res) => res.text().await,
Err(err) => Err(err),
}
}
async fn fetch_dicebear_svg(
r#type: &DicebearType,
req_fields: &[String],
placeholder_fetcher: Option<Box<impl Future<Output = Option<String>>>>,
) -> String {
// TODO: Use configuration file
let url = "dicebear.tools.adrien.run";
let seed = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
let type_str = r#type.to_string();
let url = format!(
"https://{url}/8.x/{type_str}/svg?seed={seed}&randomizeIds=true{}{}",
if !req_fields.is_empty() { "&" } else { " " },
req_fields.join("&")
);
let text = match fetch_text(url).await {
Ok(text) => Some(text),
Err(err) => {
error!("Error during placeholder loading: {}", err);
if let Some(placeholder_fetcher) = placeholder_fetcher {
placeholder_fetcher.into_future().await
} else {
None
}
}
};
text.unwrap_or("".to_string())
}
cfg_if! {
if #[cfg(target_family = "wasm")] {
fn gen_placeholder_fetcher<'a>(path: &'static str) -> Box<impl Future<Output = Option<String>>> {
Box::new(async move {
let url = format!("{}{}", web_sys::window().unwrap().origin(), path);
match fetch_text(url).await {
Ok(content) => Some(content),
Err(err) => {
error!("Error during {path} fetching: {}", err.to_string());
None
}
}
})
}
}
else {
fn gen_placeholder_fetcher(path: &'static str) -> Box<impl Future<Output = Option<String>>> {
let path = format!("./public/{}", &path);
Box::new(async move {
match read_to_string(&path).await {
Ok(content) => Some(content),
Err(err) => {
error!(
"Error during the access to the {path} file: {}",
err.to_string()
);
None
}
}
})
}
}
}
pub async fn generate_random_svg_avatar<'a>(config: Option<&'a AvatarConfig<'a>>) -> String {
let (variant, feeling) = match config {
Some(config) => (avatar_variants().get(&config.feeling), &config.feeling),
None => (None, &AvatarFeeling::Alerting),
};
let mut req_fields = Vec::<String>::new();
if let Some(config) = config {
req_fields.push(format!("backgroundColor={}", config.background_color));
}
if let Some(variant) = variant {
req_fields.push(format!(
"gestureProbability=100&gesture={}",
&variant.gesture
));
req_fields.push(format!(
"&browsProbability=100&brows={}",
render_dicebear_variants(&variant.browns)
));
req_fields.push(format!(
"&eyesProbability=100&eyes={}",
render_dicebear_variants(&variant.eyes)
));
req_fields.push(format!(
"&lipsProbability=100&lips={}",
render_dicebear_variants(&variant.lips)
));
}
let placeholder_path = match feeling {
AvatarFeeling::Ok => "/images/modal-default-ok-icon.svg",
AvatarFeeling::Warning => "/images/modal-default-warning-icon.svg",
AvatarFeeling::Alerting => "/images/modal-default-critical-icon.svg",
};
fetch_dicebear_svg(
&DicebearType::Notionists,
&req_fields,
Some(gen_placeholder_fetcher(placeholder_path)),
)
.await
}
pub struct ShapeConfig<'a> {
background_color: &'a str,
shape_1_color: &'a str,
shape_2_color: &'a str,
shape_3_color: &'a str,
}
impl<'a> ShapeConfig<'a> {
pub fn new(
background_color: &'a str,
shape_1_color: &'a str,
shape_2_color: &'a str,
shape_3_color: &'a str,
) -> Self {
Self {
background_color,
shape_1_color,
shape_2_color,
shape_3_color,
}
}
}
pub async fn generate_random_svg_shape<'a>(config: Option<&'a ShapeConfig<'a>>) -> String {
let mut req_fields = Vec::<String>::new();
if let Some(config) = config {
req_fields.push(format!("backgroundColor={}", config.background_color));
req_fields.push(format!("shape1Color={}", config.shape_1_color));
req_fields.push(format!("shape2Color={}", config.shape_2_color));
req_fields.push(format!("shape3Color={}", config.shape_3_color));
}
let placeholder_path = "/images/login-profile-placeholder.svg";
fetch_dicebear_svg(
&DicebearType::Shapes,
&req_fields,
Some(gen_placeholder_fetcher(placeholder_path)),
)
.await
}

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