Author SHA1 Message Date
Adrien acbe15ed69 👷 Split lint and audit jobs and fix woodpecker linter warns
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/audit Pipeline was successful
2025-05-02 09:18:42 +02:00
Adrien 4671a5ee51 👷 Use of the ci-lint-audit docker image
ci/woodpecker/pr/validate Pipeline was successful
2025-05-01 22:32:17 +02:00
Adrien 9e7ba84576 👷 Add Dockerfile for the ci-lint-audit image
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
Adrien a8a7b16e9f 👷 Add cargo sort-derives tool
ci/woodpecker/pr/validate Pipeline was successful
2025-04-27 22:10:27 +02:00
Adrien 8684086c74 👷 Add cargo spellcheck tool
ci/woodpecker/pr/validate Pipeline was successful
2025-04-27 21:12:32 +02:00
Adrien cd0a763c0a 👷 Fix lint - dependencies CI job
ci/woodpecker/pr/validate Pipeline was successful
2025-04-27 20:34:40 +02:00
Adrien 4d6d6d3515 👷 Add cargo udeps tool
ci/woodpecker/pr/validate Pipeline failed
2025-04-27 17:34:34 +02:00
Adrien 2bdd0b6a6d 👷 Add the cargo-deny configuration file 2025-04-27 13:27:50 +02:00
Adrien a9996d448c 👷 Run cargo deny only if we're able to build the web docker image
ci/woodpecker/pr/validate Pipeline was successful
2025-04-27 11:52:15 +02:00
Adrien d5e92f282a 👷 For now, Don't block PRs on clippy or cargo deny errors
ci/woodpecker/pr/validate Pipeline failed
2025-04-27 11:50:14 +02:00
Adrien 285d4ba590 📄 Set AGPL-3.0-or-later license
ci/woodpecker/pr/validate Pipeline failed
2025-04-27 11:36:45 +02:00
Adrien 6b8cef176f ✏️ Fix typo 2025-04-27 11:36:27 +02:00
Adrien 60756b7e72 👷 Add cargo deny tool
ci/woodpecker/pr/validate Pipeline was successful
2025-04-27 11:08:34 +02:00
Adrien 18ee33d512 💚 Add credential to pull images from our private Docker registry
ci/woodpecker/manual/dependencies Pipeline was successful
ci/woodpecker/pr/validate Pipeline was successful
2025-04-27 04:27:03 +02:00
Adrien e2c20e4c64 💚 Dry run to ensure that we're able to build the web Docker image
ci/woodpecker/pr/validate Pipeline failed
2025-04-27 04:23:29 +02:00
Adrien 28aa250f58 💚 Use the custom dioxus-cli image to build the webclient dockerfile
ci/woodpecker/pr/validate Pipeline failed
2025-04-27 03:46:57 +02:00
Adrien d29a3a0821 👷 Restore validation trigger
ci/woodpecker/pr/validate Pipeline failed
2025-04-21 18:20:15 +02:00
Adrien 570c792bf4 👷 Test the build for web on PR
ci/woodpecker/manual/dependencies Pipeline was successful
2025-04-21 17:40:06 +02:00
Adrien fb37125740 Merge pull request 'Configure Renovate' (#3) from renovate/configure into develop
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
renovate-bot 6ce395cf7c Add renovate.json
ci/woodpecker/pr/validate Pipeline was successful
2025-04-21 13:11:42 +00:00
Adrien a4eae624d3 👷 Retry to fix renovate git auth issue by setting RENOVATE_GIT_AUTHOR
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
Adrien f53e4fbadf 👷 Try to fix renovate git auth issue by setting RENOVATE_GIT_AUTHOR
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
Adrien b4bc48d576 👷 Remove the dry run option for renovate
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
Adrien b435bc73a7 👷 Try to fix renovate token issue (fix RENOVATE_ENDPOINT)
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
Adrien a741c6ea8e 👷 Try to fix renovate token issue (move RENOVATE_TOKEN to env
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
Adrien c761b203cc 👷 Try to fix renovate token issue (Upercase renovate_token)
ci/woodpecker/cron/dependencies Pipeline failed
2025-04-21 13:39:16 +02:00
Adrien d85a2a97b7 👷 Remove command from the dependency scanning job
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
Adrien 403d238463 👷 First try adding renovate to the CI 2025-04-21 12:37:51 +02:00
Adrien 581a3d159a ⬆️ Bump modx version (0.1.2 -> 0.1.4)
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2025-04-21 11:03:34 +02:00
Adrien 4bbe863a56 ⬆️ Bump Dioxus version (0.6.1 -> 0.6.3)
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2025-04-21 10:14:54 +02:00
Adrien 722d98f5d1 ♻️ Use of the assets management introduced by Dioxus 0.6.0
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
Adrien 219fac87b1 ⬆️ Bump rust version (1.83 -> 1.86)
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2025-04-20 23:45:48 +02:00
Adrien c9deeea36f 🚧 Try to fix CI after the bump of the woodpecker version
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2025-04-20 23:28:55 +02:00
Adrien d19a8a7f7d 🚧 Try to fix CI dockerize job 2025-04-20 23:11:59 +02:00
Adrien 5bc8ac409e ⬆️ Force the commit used for matrix-sdk (fa6066b8)
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-12-29 16:53:12 +01:00
Adrien 6586edf287 ⬆️ Bump dioxus version (main -> 0.6.1)
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-12-29 16:25:30 +01:00
Adrien a533f1869d 🐛 Disable caching for dockerize
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-09-25 23:35:01 +02:00
Adrien 6b9ef5dc90 🐛 Don't run validation on commit into default branch
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-09-25 22:59:24 +02:00
Adrien 93b6027c73 🐛 Use dioxus-cli:6ff7a54 image to dockerize
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
Adrien 8026b6fa32 🐛 Use stable dioxus-cli
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
Adrien 8652d56f51 🐛 Fix wasm-bindgen-cli version in Dockerfile
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
Adrien c5045c328c Merge pull request 'Run validation step on push into default branch (2)' (#2) from fix/ci-add-first-checks into develop
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
Adrien ba96ad77d3 👷 Run validation step on push into default branch
ci/woodpecker/pr/validate Pipeline was successful
2024-09-24 23:18:44 +02:00
Adrien 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
Adrien 831085e8b6 👷 Disable build step on MR
ci/woodpecker/pr/validate Pipeline was successful
2024-09-24 22:56:04 +02:00
Adrien 691dc7572a 🚨 Fix some clippy warnings
ci/woodpecker/pr/validate Pipeline was successful
2024-09-23 22:16:55 +02:00
Adrien b728f6efcd 👷 Install deps
ci/woodpecker/pr/validate Pipeline failed
2024-09-23 21:58:07 +02:00
Adrien d8e3d49d95 👷 Install clippy
ci/woodpecker/pr/validate Pipeline failed
2024-09-23 20:52:08 +02:00
Adrien 9b2ab337b2 👷 Install rustfmt
ci/woodpecker/pr/validate Pipeline failed
2024-09-23 20:50:51 +02:00
Adrien deb3e273f4 👷 Add format and clippy steps
ci/woodpecker/pr/validate Pipeline failed
2024-09-23 20:47:23 +02:00
Adrien abea905feb 👷 Check that no error occurs during the build of the webapp
ci/woodpecker/pr/validate Pipeline was successful
2024-09-22 23:40:22 +02:00
Adrien c7955d5571 Merge branch 'fix/invalid-wysiwyg-dep' into develop
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-09-22 22:50:16 +02:00
Adrien f99296bdce 🐛 Fix Cargo.toml 2024-09-22 22:49:09 +02:00
Adrien 7989d86af1 Merge branch 'conversations-panel' into develop
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-09-22 21:19:37 +02:00
Adrien d2108fa6fc ♻️ Remove duplicated if statement 2024-09-22 19:39:06 +02:00
Adrien 44ba3d4d23 🐛 script_include is not used anymore (since Dioxus #2258) 2024-09-22 19:36:43 +02:00
Adrien 5206fb13c8 ♻️ Use of manganis for wallpaper pattern 2024-09-22 18:51:48 +02:00
Adrien 27934c7fc9 🚨 Fix some clippy warnings 2024-09-08 16:12:33 +02:00
Adrien 9d95bd4481 Add the capability to join a conversation 2024-09-08 16:07:13 +02:00
Adrien 648be8ba72 🐛 Add tail div to dynamic conversation_panels to fix some side effects 2024-09-07 13:01:26 +02:00
Adrien b7b98dff15 Add TabsBar to the LayoutBig (Conversations layout) 2024-09-07 12:58:39 +02:00
Adrien aaafa91cbe ⬆️ Bump turf version (0.8.0 -> 0.9.3) 2024-08-21 23:29:35 +02:00
Adrien 9a5f7ae504 ⬆️ Use of Dioxus main branch instead of 0.5 release 2024-08-21 23:29:33 +02:00
Adrien d5d996eec3 Add topic to the UI store Room 2024-06-27 08:25:11 +02:00
Adrien 73c5b70ba8 🚧 First design template for ConversationOptionsMenu 2024-06-22 20:57:44 +02:00
Adrien f0463213cf 🚧 Use of the use_long_press to open a future room configuration menu 2024-06-22 20:55:19 +02:00
Adrien e55992aed5 💄 Add Reject and Join buttons 2024-06-22 20:52:08 +02:00
Adrien cea60ce695 Add ui use_long_press hook 2024-06-15 16:19:20 +02:00
Adrien 271e865d40 💄 Open/close ChatPanel on click (Conversations layout/big breakpoint) 2024-06-09 12:31:18 +02:00
Adrien ffe759e749 ♻️ Merge Space and HomeSpace components 2024-06-09 11:22:48 +02:00
Adrien 9baa7f290a 💄 Open/close ChatPanel on click 2024-06-09 09:43:46 +02:00
Adrien d566a4927f ♻️ Use of the SCSS variables to compute the width of the inner panels 2024-06-08 13:04:17 +02:00
Adrien 1ad4d444fb 💄 Add first breakpoint management by Conversations layout 2024-06-07 22:18:15 +02:00
Adrien 204e11b8b1 🐛 tracing_forest crate isn't used for wasm 2024-05-26 18:09:05 +02:00
Adrien f566d88df2 Merge branch 'conversations-panel' into develop
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-05-26 12:00:55 +02:00
Adrien 89473cfd61 🚨 Fix some clippy warnings 2024-05-26 11:53:47 +02:00
Adrien 62015f8d13 Use of Conversations layout 2024-05-26 11:39:18 +02:00
Adrien c8e8e2da67 Add a first Conversations layout 2024-05-26 11:17:37 +02:00
Adrien df32faa8e6 🐛 Dioxus Props shall implement PartialEq and Clone traits 2024-05-26 10:58:51 +02:00
Adrien 5194899de0 Add a first Conversations component 2024-05-26 10:45:09 +02:00
Adrien ff0ac7f982 🚧 Add a ChatPanel placeholder component 2024-05-25 14:06:40 +02:00
Adrien 8ed4ff3f2a ♻️ Rename views -> layouts 2024-05-24 22:35:10 +02:00
Adrien 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
Adrien 19d64d7ac5 ♻️ Render Room avatar using the RoomMember ones, if not set 2024-05-22 16:44:07 +02:00
Adrien 35e191eb62 🗑️ WorkerTask::GetRoomMembers isn't used, remove it 2024-05-22 16:21:13 +02:00
Adrien 8c244ce4a7 ⬆️ Use dioxus main branch for now 2024-05-22 15:32:25 +02:00
Adrien a1fe74f53e ♻️ Use of "target_family" instead of feature to manage wasm platform 2024-05-21 12:40:19 +02:00
Adrien 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
Adrien cd6506fb03 🔊 Trace events from Matrix client callbacks to domain methods 2024-05-21 12:26:40 +02:00
Adrien b5da0ee992 ♻️ Use of cfg_if to manage how to logging according to the platform 2024-05-18 22:03:21 +02:00
Adrien df2d924c65 Cleanup dependencies 2024-05-18 22:00:28 +02:00
Adrien 54c7073b98 💡 Add comments to keep in mind why we can't send Matrix sdk Room 2024-05-18 09:52:40 +02:00
Adrien fdae149c4a 🚧 Add Avatar management and refresh the Matrix client part 2024-05-17 22:41:35 +02:00
Adrien 0b898dce52 Add Invitation value object 2024-05-17 09:31:59 +02:00
Adrien cbe32c250e 🚧 Add relations between store::Room and store::Area 2024-05-16 22:46:59 +02:00
Adrien d77c2a9d12 ♻️ Use of Store interfaces 2024-05-15 19:08:52 +02:00
Adrien bc30670f6e ⬆️ Bumps modx version (git -> 0.1.2) 2024-05-15 19:08:49 +02:00
Adrien 18a797bc3f ♻️ Add Account, Room and Space UI store structs 2024-05-11 18:23:48 +02:00
Adrien bc6b02bc34 ♻️ Rework the Matrix messaging Client 2024-05-10 22:32:35 +02:00
Adrien 0a936dd12b ♻️ Rework the Matrix messaging Requester 2024-05-10 22:20:32 +02:00
Adrien ef41c0bd48 Add events shared by Matrix client and Requester 2024-05-10 22:20:11 +02:00
Adrien e3a6ec9858 Add new messaging WorkerTask 2024-05-10 22:16:49 +02:00
Adrien 692a71faef 🚨 Fix clippy warnings 2024-05-10 20:11:48 +02:00
Adrien c2918fbc78 🚧 Add RoomMember value object 2024-05-10 19:56:39 +02:00
Adrien bfa1539d23 🚧 Add Space identity 2024-05-10 19:29:42 +02:00
Adrien 0190cf9165 ♻️ Rework the Room entity 2024-05-10 19:13:46 +02:00
Adrien 4f9e5c538e 🚧 Add a first version of the mozaik builder service 2024-05-10 18:56:54 +02:00
Adrien 79e8dea622 🚧 Add Account identity and messaging and store interfaces 2024-05-10 18:56:37 +02:00
Adrien 0a0d6e745b 🚧 Remove AvatarSelector component 2024-05-09 22:12:32 +02:00
Adrien 32b633aad6 🚧 Remove Header component 2024-05-08 10:27:01 +02:00
Adrien 4ea4416165 🚧 Remove Loading component 2024-05-08 10:23:18 +02:00
Adrien c4dcb0f87d 🚧 Remove ChatsWindow and ContactsWindow components 2024-05-05 22:20:25 +02:00
Adrien f79ebb0b03 Add SearchIcon, SpacesIcon, ChatsIcon and RoomsIcon elements 2024-04-26 19:42:49 +02:00
Adrien 7078f86cd8 💄 Make the "logo" shape reusable 2024-04-26 19:31:05 +02:00
Adrien 894f32e177 💄 Adjust Login padding 2024-04-26 19:27:11 +02:00
Adrien 3afed02aa8 🚧 Make Button usable outside of the button.rs file 2024-04-26 19:23:34 +02:00
Adrien 7b6781a007 🐛 Fix gap in wallpaper pattern
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-04-23 11:35:31 +02:00
Adrien dfe2761a3a Merge branch 'fix/login-invalid-width-on-mobile' into develop
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-04-22 15:55:58 +02:00
Adrien 58e12c991d 🐛 Fix the height and radius of the inputs 2024-04-22 15:51:01 +02:00
Adrien badd541424 Merge branch 'fix/login-invalid-width-on-mobile' into develop
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-04-22 15:05:52 +02:00
Adrien 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
Adrien 6172167ea8 Add a parameter to the Wallpaper widget to show the app version 2024-04-22 14:44:51 +02:00
Adrien 7170332205 Merge branch 'fix/loading-invalid-spinner-position' into develop
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-04-22 09:02:38 +02:00
Adrien a3775e35d3 🐛 Update to match to the dioxus_free_icons view_box and xmlns definition 2024-04-22 08:58:29 +02:00
Adrien 724d04c592 Merge branch 'fix/loading-invalid-spinner-position' into develop
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-04-21 23:12:30 +02:00
Adrien b3330abecc 🐛 The loading spinner isn't aligned with the wallpaper on mobile 2024-04-21 23:08:34 +02:00
Adrien eecb46e4b8 🐛 Displayed versions are always computed as dirty
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-04-16 23:21:19 +02:00
Adrien de9d2b3a8a 👷 Run dockerize and deploy CI steps on commits on default branch
ci/woodpecker/push/dockerize Pipeline failed
ci/woodpecker/push/deploy unknown status
2024-04-16 07:29:09 +02:00
Adrien f0d3b91084 🧑‍💻 Add version to the Wallpaper
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
Adrien cae7a1e244 👷 Optimize building process using the homemade dioxus-cli docker image
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-04-15 23:18:40 +02:00
Adrien 6f95e0f57b 👷 Use of the CI to deliver a new image on each commit (temp)
ci/woodpecker/push/dockerize Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2024-04-15 22:04:25 +02:00
Adrien d4af06d687 🐛 Fix wallpaper-pattern misalignment 2024-04-12 16:59:45 +02:00
Adrien 53fff64537 🐛 Fix invalid font used in login form on (web platform) 2024-04-12 16:24:28 +02:00
Adrien fb4554aa71 🚀 Update Dockerfile to take the new code organization into account 2024-04-12 14:44:44 +02:00
Adrien 741124e47e ️ Configure cargo to Optimize the size of the generated wasm bundle 2024-04-12 14:42:41 +02:00
Adrien 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
Adrien df33e94a12 Add web page to display during the wasm bundle 2024-04-12 14:33:10 +02:00
Adrien 78cb65e054 ♻️ Move fonts and images directories in a common public on 2024-04-12 12:49:07 +02:00
Adrien 77fa0c5fd5 🚀 Add the Dockerfile for the web app 2024-04-11 08:32:57 +02:00
Adrien d3a35cd81f ️ Configure turf to minify the generated CSS files 2024-04-11 08:31:33 +02:00
Adrien b524048563 ⬆️ Bump turf release (0.7.0 -> 0.8.*) 2024-04-11 08:30:29 +02:00
Adrien 4ab4ac5fee ⬆️ Use of the latest matrix-sdk 0.7.* version (master branch before) 2024-04-11 08:30:17 +02:00
Adrien 438416bec1 Merge branch 'web-client' into develop 2024-04-10 17:23:24 +02:00
Adrien c580fba315 ♻️ Add Room domain entity 2024-04-10 17:14:26 +02:00
Adrien a7bccfa779 ♻️ Add Session domain entity 2024-04-10 12:50:15 +02:00
Adrien eb81b3252c Enable on tokio rt and sync features (disable default ones) 2024-04-10 12:41:30 +02:00
Adrien 880195109d ⬆️ Bump dioxus version 2024-04-10 12:41:29 +02:00
Adrien 11e239714b Reuse tracing library to be able to display matrix SDK logs 2024-04-10 12:41:05 +02:00
Adrien 4261e24cd2 ♻️ Clean Cargo.toml file and add target specific dependencies 2024-04-06 12:18:48 +02:00
Adrien 9cfc0841df 💄 Fix some rendering inconsistencies 2024-04-06 12:16:18 +02:00
Adrien 39ff4122c9 🐛 Svg generated using dicebear shall use unique ids 2024-04-06 12:13:10 +02:00
Adrien 46ce890718 ♻️ Make random_svg_generators able to get placeholder according to the target 2024-04-06 12:07:29 +02:00
Adrien 82b15a5509 💄 Manage config per target and remove menu bar from the desktop one 2024-04-06 12:02:43 +02:00
Adrien 912b67ed23 🐛 Remove unused tokio::time import 2024-04-06 11:55:32 +02:00
Adrien 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
Adrien 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
Adrien b26cb1d982 Use async-std to sleep asynchronously (previously done with tokio) 2024-04-05 17:23:48 +02:00
Adrien fc9411376c Remove dioxus-desktop dependency 2024-04-05 17:14:37 +02:00
Adrien df465d99c0 Disable matrix-sdk unused and default features 2024-04-05 17:13:22 +02:00
Adrien 491e34903f 🔧 Add Dioxus.toml file 2024-04-05 17:06:28 +02:00
Adrien 0c1df908f2 Merge branch 'dioxus-0.5.0' into develop 2024-04-05 16:26:32 +02:00
Adrien d245169345 🚸 Make desktop the default target 2024-04-05 16:16:19 +02:00
Adrien 9eaf79208e Merge branch 'clean-redesign' into develop 2024-04-04 14:39:04 +02:00
Adrien 0ce0764204 🎨 Isolate infra and ui components
The src/base.rs is still to be reworked.
2024-04-04 14:27:58 +02:00
Adrien 92bf860101 Merge branch 'dioxus-0.5.0' into develop 2024-04-01 23:47:13 +02:00
Adrien 014a0c2c57 🎨 Put svg image generation in a dedicated datasource 2024-04-01 19:32:35 +02:00
Adrien 9071b0073c ⬆️ Update the components to take the dioxus 0.5 rework into account 2024-03-31 23:26:10 +02:00
Adrien aad0064a0c Merge branch 'redesign-login-form' into develop 2024-03-30 18:31:15 +01:00
Adrien 83fe388e8d 🚨 Fix clippy warnings 2024-03-30 18:24:04 +01:00
Adrien 448b81b65d Add Login component 2024-03-30 17:40:17 +01:00
Adrien 4e963ce063 🎨 Factorize the definition of the Button components 2024-03-30 14:37:44 +01:00
Adrien cf9737fc76 Add Modal component 2024-03-30 13:46:53 +01:00
Adrien 5c91df206c 💄 Store colors in a nested map to make them reachable using criteria 2024-03-30 08:23:52 +01:00
Adrien 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
Adrien 89b1f10b6e Add Pyramid icon 2024-03-21 21:05:04 +01:00
Adrien 570a969cee 💄 Fix conflicts regarding the generated CSS class names 2024-03-21 18:32:40 +01:00
Adrien ceeda1a771 Redesign Login component and add fields validation 2024-03-15 14:58:58 +01:00
Adrien fc0d3b1212 ♻️ Replace constcat with const_format 2024-03-15 12:33:25 +01:00
Adrien 01f589e789 Add helper_text to TextInput (previously TextEdit) 2024-03-15 12:24:42 +01:00
Adrien c746fb6552 💄 Rework Login component 2024-03-10 12:02:18 +01:00
Adrien 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
Adrien dd0754073c Add TextField component 2024-03-10 11:35:25 +01:00
Adrien b05e3efce4 🐛 Add SVG pattern file used to render the Wallpaper 2024-03-10 11:01:33 +01:00
Adrien 0a4969e079 💄 Center and uppercase the content of the Register and Login buttons 2024-03-10 10:42:02 +01:00
Adrien f52733d9a6 💄 Add buttons which will be used by the Login component 2024-03-09 22:46:00 +01:00
Adrien bb56d24f02 💄 Add border variables to the base SCSS file 2024-03-09 22:44:04 +01:00
Adrien 043a721429 Make Spinner animation suspendable 2024-03-09 13:04:01 +01:00
Adrien 46c251ef90 ♻️ Make Spinner reusable (not only by loading view) 2024-03-03 23:35:09 +01:00
Adrien 257b36eae1 ✏️ Fix typo in color names 2024-03-03 23:31:00 +01:00
Adrien ff430edffe 💄 Use Geist font everywhere. 2024-03-03 23:29:49 +01:00
Adrien 5e05b75bde 💄 Add Wallpaper, Spinner and LoadingPage widgets 2024-02-29 23:47:32 +01:00
Adrien 5719cb8254 💄 Add DS colors 2024-02-29 23:46:20 +01:00
Adrien 921003aeac Merge branch 'readme' into develop 2024-01-07 13:30:04 +01:00
Adrien 8ffc977846 📝 Add the reference to the MSN messenger client 2024-01-07 13:28:27 +01:00
Adrien 6e64eb4d97 📝 Add the description of the technical stack to the README 2024-01-07 13:25:52 +01:00
Adrien a7cf0f681a 🎨 Split ChatsWindow component
Creation of the Navbar and Conversation components.
2024-01-01 21:52:31 +01:00
Adrien a8d343ce3a ️ Use of the token returned by the first sync with the homeserver 2024-01-01 21:32:32 +01:00
Adrien 5fe13335a1 🎨 Factorize Room creation from MatrixRoom instances 2023-12-31 15:21:49 +01:00
Adrien 04628ae10d ️ Remove the periodic pooling to get the rooms (joined or not) 2023-12-31 15:04:18 +01:00
Adrien 66f4ba6a7e 🐛 Remove piece of code used for testing 2023-12-30 23:38:06 +01:00
Adrien 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
Adrien 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
Adrien 227a07dd9e 📝 Add main readme file 2023-12-26 21:40:12 +01:00
Adrien 7498638ac1 🚨 Fix clippy warnings 2023-12-26 21:04:57 +01:00
Adrien 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
Adrien d7ba8130d3 🙈 Add dist directory to gitignore 2023-12-23 17:42:42 +01:00
Adrien 8679a23692 Add rooms topics management 2023-12-23 14:54:21 +01:00
Adrien 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
Adrien 513b05ddb3 🏗️ Split matrix_client.rs to create the matrix_interface module 2023-12-17 11:54:21 +01:00
Adrien 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
Adrien 4988054dae 💄 Display ChatsWindow only once the user logged 2023-08-21 21:43:42 +02:00
Adrien f79bf329a5 🐛 Fix AvatarSelector shift (Login) 2023-08-21 21:29:09 +02:00
Adrien 5120f1e74f 🐛 Fix not-centered AvatarSelector rendering issue 2023-08-20 20:06:16 +02:00
Adrien ff95dcade8 Add first static (no reactivity, static data) implementation of ChatsWindow 2023-08-20 20:04:09 +02:00
Adrien 3b89cd1769 🎨 Move DownArrowIcon to the new icons file 2023-08-20 19:53:28 +02:00
136 changed files with 8290 additions and 1640 deletions
+8
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
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
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
+2 -1
View File
@@ -1,2 +1,3 @@
/target
Cargo.lock
/dist
Cargo.lock
+18
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
+18
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
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
+19
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
+17
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
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
+77 -17
View File
@@ -2,24 +2,84 @@
name = "beau-gosse-du-92"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = []
[package.metadata.spellcheck]
config = "./spellcheck.toml"
[dependencies]
dioxus = "0.4.0"
dioxus-desktop = "0.4.0"
# matrix-sdk = { version = "0.6.2", features = ["js"] }
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", branch = "main" , features = ["js"]}
anyhow = "1.0.72"
url = "2.4.0"
tokio = "1.29.1"
dirs = "5.0.1"
ctrlc-async = "3.2.2"
tracing-subscriber = "0.3.17"
dioxus-free-icons = { version = "0.7.0", features = ["material-design-icons-navigation", "ionicons"] }
thiserror = "1.0.44"
turf = "0.5.0"
dioxus-std = { version = "0.4.0", features = ["utils"] }
# Errors
anyhow = "1.0.75"
thiserror = "1.0.50"
[build]
target = "x86_64-unknown-linux-gnu"
# Async
async-std = "1.12.0"
async-trait = "0.1.80"
futures = "0.3.29"
futures-util = "0.3.29"
tokio = { version = "1.34.0", default-features = false, features = ["rt", "sync"] }
tokio-stream = "0.1.15"
# Utils
base64 = "0.22.0"
const_format = "0.2.32"
rand = "0.8.5"
validator = { version = "0.17.0", features = ["derive"] }
# Http client
reqwest = "0.11.24"
# Password strength estimation
zxcvbn = "2.2.2"
# Image processing/conversion
image = "0.25.1"
# Get the application version
git-version = "0.3.9"
# Conditional compilation
cfg-if = "1.0.0"
# Logging/tracing
tracing = "0.1.40"
tracing-forest = "0.1.6"
# SCSS -> CSS + usage in rust code
turf = "0.9.3"
# Dioxus
dioxus-free-icons = { version = "0.9", features = ["ionicons", "font-awesome-solid"] }
modx = "0.1.4"
[target.'cfg(target_family = "wasm")'.dependencies]
# Logging/tracing
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing-web = "0.1.3"
# Dioxus
dioxus = { version = "0.6.3", features = ["web"] }
web-sys = "0.3.69"
# Matrix
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", rev = "fa6066b8", default-features = false, features = ["rustls-tls", "js"] }
[target.'cfg(not(target_family = "wasm"))'.dependencies]
# Utils
time = "0.3.36"
# Logging/tracing
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "time"] }
# Dioxus
dioxus = { version = "0.6.3", features = ["desktop"] }
# Matrix
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", rev = "fa6066b8", default-features = false, features = ["rustls-tls"] }
[build-dependencies]
regex = "1.10.3"
[package.metadata.turf]
minify = true
[package.metadata.turf.class_names]
template = "<original_name>--<id>"
+13
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
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 \
&& 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
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...
+152 -1
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
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 = []
+26
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

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

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

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

Symlink
+1
View File
@@ -0,0 +1 @@
public/index.html
BIN
View File
Binary file not shown.
BIN
View 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.
+92
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.
@@ -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

+13
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

+244
View File
@@ -0,0 +1,244 @@
<!DOCTYPE html>
<html>
<head>
<title>{app_name}</title>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8" />
<style>
html, body {
height: 100%;
width: 100%;
margin: 0;
font-family: "Geist";
font-weight: normal;
}
#main {
height: 100%;
width: 100%;
}
@keyframes multicolor {
0% { fill: #1DB2CF; /* color-primary-100 */ }
33% { fill: #7E6BB6; /* color-secondary-100 */ }
66% { fill: #D53583; /* color-ternary-100 */ }
}
:root {
--wallpaper-pattern-height: 128px;
--spinner-height: 5%;
--window-center-pos: calc(50% + (var(--wallpaper-pattern-height) / 2) - (var(--spinner-height) / 2));
}
/* @media (0px < height <= calc(var(--wallpaper-pattern-height) * 5)) { */
@media (min-height: 0px) and (max-height: 640px) {
:root {
--spinner-top: var(--window-center-pos);
}
}
/* @media (calc($wallpaper-pattern-height * 5) < height <= calc($wallpaper-pattern-height * 6)) { */
@media (min-height: 641px) and (max-height: 768px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 2));
}
}
/* @media (calc($wallpaper-pattern-height * 6) < height <= calc($wallpaper-pattern-height * 8)) { */
@media (min-height: 769px) and (max-height: 1024px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 2));
}
}
/* @media (calc($wallpaper-pattern-height * 8) < height <= calc($wallpaper-pattern-height * 10)) { */
@media (min-height: 1025px) and (max-height: 1280px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 3));
}
}
/* @media (calc($wallpaper-pattern-height * 10) < height <= calc($wallpaper-pattern-height * 12)) { */
@media (min-height: 1281px) and (max-height: 1536px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 4));
}
}
/* @media (calc($wallpaper-pattern-height * 12) < height <= calc($wallpaper-pattern-height * 14)) { */
@media (min-height: 1537px) and (max-height: 1792px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 5));
}
}
/* @media (calc($wallpaper-pattern-height * 14) < height <= calc($wallpaper-pattern-height * 16)) { */
@media (min-height: 1793px) and (max-height: 2048px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 6));
}
}
/* @media (calc($wallpaper-pattern-height * 16) < height <= calc($wallpaper-pattern-height * 18)) { */
@media (min-height: 2049px) and (max-height: 2304px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 7));
}
}
/* @media (calc($wallpaper-pattern-height * 18) < height <= calc($wallpaper-pattern-height * 20)) { */
@media (min-height: 2305px) and (max-height: 2560px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 8));
}
}
/* @media (calc($wallpaper-pattern-height * 20) < height <= calc($wallpaper-pattern-height * 22)) { */
@media (min-height: 2561px) and (max-height: 2816px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 9));
}
}
/* @media (calc($wallpaper-pattern-height * 22) < height <= calc($wallpaper-pattern-height * 24)) { */
@media (min-height: 2817px) and (max-height: 3072px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 10));
}
}
/* @media (calc($wallpaper-pattern-height * 24) < height <= calc($wallpaper-pattern-height * 26)) { */
@media (min-height: 3073px) and (max-height: 3328px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 11));
}
}
/* @media (calc($wallpaper-pattern-height * 26) < height <= calc($wallpaper-pattern-height * 28)) { */
@media (min-height: 3329px) and (max-height: 3584px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 12));
}
}
/* @media (calc($wallpaper-pattern-height * 28) < height <= calc($wallpaper-pattern-height * 30)) { */
@media (min-height: 3585px) and (max-height: 3840px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 13));
}
}
/* @media (calc($wallpaper-pattern-height * 30) < height <= calc($wallpaper-pattern-height * 32)) { */
@media (min-height: 3841px) and (max-height: 4096px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 14));
}
}
/* @media (calc($wallpaper-pattern-height * 32) < height <= calc($wallpaper-pattern-height * 34)) { */
@media (min-height: 4097px) and (max-height: 4352px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 15));
}
}
/* @media (calc($wallpaper-pattern-height * 34) < height <= calc($wallpaper-pattern-height * 36)) { */
@media (min-height: 4353px) and (max-height: 4608px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 16));
}
}
/* @media (calc($wallpaper-pattern-height * 36) < height <= calc($wallpaper-pattern-height * 38)) { */
@media (min-height: 4609px) and (max-height: 4864px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 17));
}
}
/* @media (calc($wallpaper-pattern-height * 38) < height <= calc($wallpaper-pattern-height * 40)) { */
@media (min-height: 4865px) and (max-height: 5120px) {
:root {
--spinner-top: calc(var(--window-center-pos) + (var(--wallpaper-pattern-height) * 18));
}
}
.loader {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
.wallpaper {
height: 100%;
width: 100%;
z-index: -1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.content {
background-image: url("data:image/svg+xml, <svg xmlns='http://www.w3.org/2000/svg' height='128' width='384' viewBox='0 0 384 128'><pattern id='p' width='384' height='128' patternUnits='userSpaceOnUse' stroke='%231B1B1B' stroke-linejoin='round' stroke-width='4'><path fill='%231DB2CF' d='M 9.736 -15 L -30 3.337 l 23.642 -0.088 L -10.212 15 L 30 -3.425 H 6.834 L 9.736 -15 Z'/><path fill='%23D53583' d='M 201.736 -15 L 162 3.337 l 23.642 -0.088 L 181.788 15 L 222 -3.425 H 198.834 L 201.736 -15 Z'/><path fill='%231DB2CF' d='M 393.736 -15 L 354 3.337 l 23.642 -0.088 L 373.788 15 L 414 -3.425 H 390.834 L 393.736 -15 Z'/><path fill='%237E6BB6' d='M 105.736 50 L 66 68.337 l 23.642 -0.088 L 85.788 80 L 126 61.575 H 102.834 L 105.736 50 Z'/><path fill='%237E6BB6' d='M 297.736 50 L 258 68.337 l 23.642 -0.088 L 277.788 80 L 318 61.575 H 294.834 L 297.736 50 Z'/><path fill='%231DB2CF' d='M 9.736 113 L -30 131.337 l 23.642 -0.088 L -10.212 143 L 30 124.575 H 6.834 L 9.736 113 Z'/><path fill='%23D53583' d='M 201.736 113 L 162 131.337 l 23.642 -0.088 L 181.788 143 L 222 124.575 H 198.834 L 201.736 113 Z'/><path fill='%231DB2CF' d='M 393.736 113 L 354 131.337 l 23.642 -0.088 L 373.788 143 L 414 124.575 H 390.834 L 393.736 113 Z'/></pattern><rect fill='url(%23p)' width='100%' height='100%'/></svg>");
background-position: center;
backgrond-size: var(--wallpaper-pattern-height);
width: 150%;
height: 150%;
}
}
.spinner {
height: var(--spinner-height);
aspect-ratio: 2;
position: absolute;
top: var(--spinner-top);
svg {
--fps: 4;
--duration_sec: 3;
--steps: calc(var(--duration_sec) * var(--fps));
height: 100%;
width: 100%;
fill: #1DB2CF; /* color-primary-100 */
stroke: #1B1B1B; /* greyscale-90 */
animation: 3s multicolor linear infinite;
animation-timing-function: steps(var(--steps), end);
}
}
}
</style>
</head>
<body>
<div id="main">
<div id="preloader" class="loader">
<div class="wallpaper">
<div class="content"></div>
</div>
<div class="spinner">
<svg xmlns="http://www.w3.org/2000/svg" width='184' height='94' viewBox="0 0 184 94">
<path
stroke-linejoin="round"
stroke-width="6"
d="M121.208 2 2 57.011l70.927-.265L61.363 92 182 36.724h-69.498L121.208 2Z"
/>
</svg>
</div>
</div>
</div>
<script type="module" async>
import init from "{base_path}/wasm/beau-gosse-du-92.js";
// Ensure that everything needed to render preloader has been downloaded
// before fetching the wasm bundle.
window.onload=function() {
init("{base_path}/wasm/beau-gosse-du-92_bg.wasm").then(
wasm => {
const preloader = document.getElementById("preloader");
if (preloader !== undefined) {
preloader.style.display = 'none';
}
if (wasm.__wbindgen_start == undefined) {
wasm.main();
}
}
);
}
</script>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}
+6
View File
@@ -0,0 +1,6 @@
5
Dioxus
MSN
renderers
SDK
TODO
+10
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" ]
-48
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;
}
-121
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 }
}
}
-60
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",
},
},
})
}
-14
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%;
}
}
@@ -1,8 +0,0 @@
use dioxus::prelude::*;
pub fn ChatsWindow(cx: Scope) -> Element {
cx.render(rsx! {
div {
}
})
}
-1
View File
@@ -1 +0,0 @@
pub mod chats_window;
@@ -1,47 +0,0 @@
use std::cell::RefCell;
use dioxus::prelude::*;
use dioxus_std::utils::rw::UseRw;
use crate::base::Room;
use crate::base::Store;
use crate::components::contacts_window::contacts_section::ContactsSection;
turf::style_sheet!("src/components/contacts_window/contacts.scss");
#[inline_props]
pub fn Contacts<'a>(cx: Scope, rw_store: &'a UseRw<Store>) -> Element {
println!("Contacts rendering");
let store = rw_store.read().unwrap();
let rooms = &store.rooms;
let rooms_len = rooms.len();
let mut groups = Vec::<Room>::with_capacity(rooms_len);
let mut directs = Vec::<Room>::with_capacity(rooms_len);
for arc_room in rooms.values() {
let room = arc_room.read().unwrap().to_owned();
let is_direct = room.is_direct.unwrap();
if is_direct {
directs.push(room);
} else {
groups.push(room);
}
}
// TODO: Test overflow
// TODO: Add offline users ?
cx.render(rsx! {
style { STYLE_SHEET },
div {
class: ClassName::CONTACTS,
ContactsSection {name: "Groups", contacts: RefCell::new(groups)},
ContactsSection {name: "Available", contacts: RefCell::new(directs)},
},
})
}
@@ -1,6 +0,0 @@
@import "../../_base.scss"
.contacts {
height: 72%;
background-color: white;
}
@@ -1,87 +0,0 @@
use std::cell::RefCell;
use dioxus::prelude::*;
use dioxus_free_icons::icons::io_icons::IoChevronDown;
use dioxus_free_icons::Icon;
use matrix_sdk::RoomState;
use crate::base::Room;
turf::style_sheet!("src/components/contacts_window/contacts_section.scss");
fn ContactsArrow(cx: Scope) -> Element {
cx.render(rsx! {
style { STYLE_SHEET },
Icon {
icon: IoChevronDown,
},
})
}
static NO_SUBJECT_REPR: &str = "No subject";
#[inline_props]
pub fn ContactsSection<'a>(cx: Scope, name: &'a str, contacts: RefCell<Vec<Room>>) -> Element {
println!("ContactsSection rendering");
let show = use_state(cx, || false);
let classes = vec![
ClassName::SECTION,
if **show { ClassName::ACTIVE } else { "" },
]
.join(" ");
let contacts_len = contacts.borrow().len();
let rendered_contacts = contacts.borrow_mut().clone().into_iter().map(|room| {
let room_name = room.name().unwrap();
let is_invited = room.matrix_room.state() == RoomState::Invited;
let formatted = format!(
"{room_name} - {}",
if is_invited {
format!("Invited - ")
} else {
"".to_string()
}
);
let room_topic = room.topic.unwrap_or(NO_SUBJECT_REPR.to_string()).to_owned();
rsx!(li {
img {
src: "./images/status_online.png",
},
p {
formatted,
},
p {
style: "color: darkgrey;",
room_topic,
},
},
)
});
cx.render(rsx! {
style { STYLE_SHEET },
div {
class: "{classes}",
p {
class: ClassName::HEADER,
onclick: move |_| show.set(!show),
ContactsArrow {},
format!("{name} ({contacts_len})"),
},
ul {
rendered_contacts.into_iter(),
},
},
})
}
@@ -1,67 +0,0 @@
@import "../../_base.scss"
.section {
width: 100%;
font-size: $font-size;
&.active {
ul {
height: 0;
opacity: 0;
}
svg {
transform: rotate(180deg);
}
}
.header {
height: 2%;
width: 98%;
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
margin: 0;
margin-left: 1%;
padding-top: 1%;
font-weight: bold;
}
ul {
height: 100%;
margin: 0;
overflow: hidden;
opacity: 1;
transition: 0.4s ease;
}
li {
list-style-type: none;
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
img {
height: $icon-size;
aspect-ratio: 1;
}
p {
margin: 0;
}
}
svg {
transition: 0.4s ease;
}
.contact {
list-style-type: none;
margin: 0 auto;
text-align: left;
cursor: pointer
}
}
@@ -1,94 +0,0 @@
use dioxus::prelude::*;
use dioxus_std::utils::rw::UseRw;
use crate::base::Store;
use crate::components::contacts_window::contacts::Contacts;
use crate::components::contacts_window::user_infos::UserInfos;
turf::style_sheet!("src/components/contacts_window/contacts_window.scss");
#[inline_props]
pub fn ContactsWindow<'a>(cx: Scope, rw_store: &'a UseRw<Store>) -> Element {
cx.render(rsx! {
style { STYLE_SHEET },
div {
class: ClassName::CONTACTS_WINDOW,
div {
class: ClassName::HEADER,
div {
class: ClassName::TITLE_BAR,
},
div {
class: ClassName::USER_INFO,
},
UserInfos {rw_store: rw_store},
},
div {
class: ClassName::CONTACTS_NAV,
div {
class: ClassName::INNER,
button {
class: ClassName::AERO_BUTTON,
style: "background: url(./images/letter.png) center no-repeat",
},
button {
class: ClassName::AERO_BUTTON,
style: "background: url(./images/directory.png) no-repeat center",
},
button {
class: ClassName::AERO_BUTTON,
style: "background: url(./images/news.png) no-repeat center",
},
button {
class: ClassName::FLEX_RIGHT_AERO_BUTTON,
style: "background: url(./images/brush.png) no-repeat center",
},
button {
class: ClassName::AERO_BUTTON,
style: "background: url(./images/settings.png) no-repeat center",
},
},
},
div {
class: ClassName::SEARCH,
div {
class: ClassName::INNER,
input {
class: ClassName::SEARCH_INPUT,
placeholder: "Find a contact...",
r#type: "text",
},
button {
class: ClassName::BUTTON,
style: "background: url(./images/add_user.png) no-repeat center",
},
button {
class: ClassName::BUTTON,
style: "background: url(./images/tbc_transfert.png) no-repeat center",
},
},
},
Contacts {rw_store: rw_store},
div {
class: ClassName::FOOTER,
},
},
})
}
@@ -1,84 +0,0 @@
@import "../../_base.scss";
.contactsWindow {
width: 100%;
height: 100%;
background-color: #ECF6F9;
font-family: "Tahoma", sans-serif;
border: thin solid #707070;
border-radius: 8px;
box-shadow: 0 0 5px #00000050;
.header {
height: 10%;
width: 100%;
.titleBar {
height: 60%;
width: 100%;
background:
linear-gradient(180deg, #7DC5E3, #3883A3);
}
.userInfo {
height: 40%;
width: 100%;
background:
linear-gradient(180deg, #00658B, #0077A6);
}
}
.contactsNav {
height: calc(31/1080*100%);
background:
linear-gradient(180deg, #00658B, #0077A6);
.inner {
margin-left: 1%;
margin-right: 1%;
height: 100%;
display: flex;
align-items: center;
.flexRightAeroButton {
@extend .aeroButton;
margin-left: auto;
}
}
}
.search {
height: calc(38/1080*100%);
width: 100%;
border-bottom: thin solid #e2eaf3;
.inner {
height: 100%;
width: 98%;
padding-left: 1%;
display: flex;
flex-direction: row;
align-items: center;
.searchInput {
height: calc(23/38*100%);
width: 100%;
margin-right: 1%;
border: thin solid #c7c7c7;
box-shadow: inset 0 0 calc(3/1080*100%) #0000002a;
font-size: 8pt;
padding-left: 1%;
}
}
}
.footer {
height: 10%;
}
}
-5
View File
@@ -1,5 +0,0 @@
pub mod contacts_window;
mod contacts;
mod contacts_section;
mod user_infos;
@@ -1,72 +0,0 @@
use dioxus::prelude::*;
use dioxus_free_icons::icons::md_navigation_icons::MdArrowDropDown;
use dioxus_free_icons::Icon;
use dioxus_std::utils::rw::UseRw;
use crate::base::Store;
use crate::components::avatar_selector::AvatarSelector;
turf::style_sheet!("src/components/contacts_window/user_infos.scss");
fn DownArrowIcon(cx: Scope) -> Element {
cx.render(rsx! {
style { STYLE_SHEET },
Icon {
class: ClassName::DOWN_ARROW_ICON,
icon: MdArrowDropDown,
}
})
}
static MESSAGE_PLACEHOLDER: &str = "<Enter a personal message>";
#[inline_props]
pub fn UserInfos<'a>(cx: Scope, rw_store: &'a UseRw<Store>) -> Element {
println!("UserInfos rendering");
let store = rw_store.read().unwrap().clone();
let user_id = store.user_id.unwrap();
let user_info = store.user_infos.get(&user_id).unwrap();
let user_display_name = user_info.display_name.as_ref().unwrap();
cx.render(rsx! {
style { STYLE_SHEET },
div {
class: ClassName::USER_INFO,
div {
class: ClassName::AVATAR_SELECTOR,
AvatarSelector {},
},
div {
class: ClassName::INFO_CONTAINER,
div {
class: ClassName::USER_ID,
p {
class: ClassName::USER_NAME,
"{user_display_name}",
},
p {
class: ClassName::USER_STATUS,
"(Busy)",
},
DownArrowIcon {},
},
div {
class: ClassName::USER_MESSAGE,
p {
// TODO: Handle user message
MESSAGE_PLACEHOLDER,
}
DownArrowIcon {},
},
},
},
})
}
@@ -1,71 +0,0 @@
@import "../../_base.scss"
.userInfo {
position: relative;
height: 75%;
width: 99%;
top: -75%;
left: 1%;
aspect-ratio: 1;
z-index: 1;
display: flex;
flex-direction: row;
align-items: center;
.avatarSelector {
height: 100%;
aspect-ratio: 1;
}
.infoContainer {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
height: 100%;
width: 100%;
.userId {
@extend .aeroButton;
height: 30%;
width: fit-content;
display: flex;
text-align: begin;
align-items: center;
.userName {
display: inline-block;
width: fit-content;
color: white;
margin: 0;
}
.userStatus {
display: inline-block;
width: fit-content;
color: #B9DDE7;
}
}
.userMessage {
@extend .aeroButton;
width: fit-content;
height: 30%;
display: flex;
text-align: begin;
align-items: center;
margin: 0;
color: white;
}
}
}
.downArrowIcon {
color: transparent;
path:last-child {
fill: white;
}
}
-26
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",
},
},
}
})
}
-13
View File
@@ -1,13 +0,0 @@
.root {
height: 100%;
width: 100%;
display: flex;
img {
height: 100%;
}
svg {
fill: white;
}
}
-148
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
}
}
-51
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;
}
}
}
-22
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})
}
})
}
-6
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
View File
@@ -0,0 +1 @@
pub(crate) mod model;
+136
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
}
}
+7
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;
+69
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
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
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;
}
}
}
+93
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()
}
}
+26
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
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()
}
}
+56
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>) {}
}
@@ -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(),
}
}
}
@@ -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;
}
}
}
}
@@ -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;
@@ -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
)
}
}
@@ -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
}
}
@@ -0,0 +1,71 @@
use std::fmt::{Debug, Formatter};
use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId};
use crate::utils::Sender;
#[derive(Debug)]
pub enum LoginStyle {
Password(String, String),
}
pub enum WorkerTask {
Init(Sender<anyhow::Result<()>>),
Login(LoginStyle, Sender<anyhow::Result<()>>),
RunForever(Sender<()>),
GetDisplayName(Sender<anyhow::Result<Option<String>>>),
GetAvatar(Sender<anyhow::Result<Option<Vec<u8>>>>),
GetRoomAvatar(OwnedRoomId, Sender<anyhow::Result<Option<Vec<u8>>>>),
GetRoomMemberAvatar(
Option<OwnedMxcUri>,
OwnedRoomId,
OwnedUserId,
Sender<anyhow::Result<Option<Vec<u8>>>>,
),
JoinRoom(OwnedRoomId, Sender<anyhow::Result<bool>>),
}
impl Debug for WorkerTask {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
WorkerTask::Init(_) => f
.debug_tuple("WorkerTask::Init")
.field(&format_args!("_"))
// .field(&format_args!("_"))
.finish(),
WorkerTask::RunForever(_) => f
.debug_tuple("WorkerTask::RunForever")
.field(&format_args!("_"))
.finish(),
WorkerTask::Login(style, _) => f
.debug_tuple("WorkerTask::Login")
.field(style)
// .field(&format_args!("_"))
.finish(),
WorkerTask::GetDisplayName(_) => f
.debug_tuple("WorkerTask::GetDisplayName")
.field(&format_args!("_"))
.finish(),
WorkerTask::GetAvatar(_) => f
.debug_tuple("WorkerTask::GetAvatar")
.field(&format_args!("_"))
.finish(),
WorkerTask::GetRoomAvatar(id, _) => f
.debug_tuple("WorkerTask::GetRoomAvatar")
.field(id)
.finish(),
WorkerTask::GetRoomMemberAvatar(room_id, user_id, avatar_url, _) => f
.debug_tuple("WorkerTask::GetRoomMemberAvatar")
.field(avatar_url)
.field(room_id)
.field(user_id)
.finish(),
WorkerTask::JoinRoom(room_id, _) => f
.debug_tuple("WorkerTask::JoinRoom")
.field(room_id)
.finish(),
}
}
}
+1
View File
@@ -0,0 +1 @@
pub(crate) mod matrix;
+2
View File
@@ -0,0 +1,2 @@
pub(crate) mod messaging;
pub(crate) mod services;
+2
View File
@@ -0,0 +1,2 @@
pub(crate) mod mozaik_builder;
pub(crate) mod random_svg_generators;
@@ -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()
}
}
}
@@ -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