summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.cargo/config.toml30
-rw-r--r--.gitignore8
-rw-r--r--.helix/languages.toml7
-rw-r--r--Cargo.lock4771
-rw-r--r--Cargo.lock.license3
-rw-r--r--Cargo.toml59
-rw-r--r--LICENSES/AGPL-3.0-or-later.txt235
-rw-r--r--Trunk.toml67
-rw-r--r--aaaa.md11
-rw-r--r--assets/bubble.pngbin0 -> 232874 bytes
-rw-r--r--assets/caw.pngbin0 -> 1106705 bytes
-rw-r--r--assets/fonts/Diolce-Regular.otfbin37600 -> 0 bytes
-rw-r--r--assets/fonts/Diolce-Regular.ttfbin49224 -> 0 bytes
-rw-r--r--assets/fonts/Diolce-Regular.woff2bin0 -> 20584 bytes
-rw-r--r--assets/fonts/K2D-Bold.ttfbin92876 -> 0 bytes
-rw-r--r--assets/fonts/K2D-BoldItalic.ttfbin97072 -> 0 bytes
-rw-r--r--assets/fonts/K2D-ExtraBold.ttfbin92920 -> 0 bytes
-rw-r--r--assets/fonts/K2D-ExtraBoldItalic.ttfbin97228 -> 0 bytes
-rw-r--r--assets/fonts/K2D-ExtraLight.ttfbin92112 -> 0 bytes
-rw-r--r--assets/fonts/K2D-ExtraLightItalic.ttfbin96264 -> 0 bytes
-rw-r--r--assets/fonts/K2D-Italic.ttfbin96444 -> 0 bytes
-rw-r--r--assets/fonts/K2D-Light.ttfbin92148 -> 0 bytes
-rw-r--r--assets/fonts/K2D-LightItalic.ttfbin96240 -> 0 bytes
-rw-r--r--assets/fonts/K2D-Medium.ttfbin92772 -> 0 bytes
-rw-r--r--assets/fonts/K2D-MediumItalic.ttfbin96888 -> 0 bytes
-rw-r--r--assets/fonts/K2D-Regular.ttfbin92352 -> 0 bytes
-rw-r--r--assets/fonts/K2D-SemiBold.ttfbin92912 -> 0 bytes
-rw-r--r--assets/fonts/K2D-SemiBoldItalic.ttfbin97032 -> 0 bytes
-rw-r--r--assets/fonts/K2D-Thin.ttfbin92276 -> 0 bytes
-rw-r--r--assets/fonts/K2D-ThinItalic.ttfbin96192 -> 0 bytes
-rw-r--r--assets/fonts/Millimetre-Regular.otfbin0 -> 56956 bytes
-rw-r--r--assets/fonts/k2d-1.woff2bin0 -> 12100 bytes
-rw-r--r--assets/fonts/k2d-10.woff2bin0 -> 5020 bytes
-rw-r--r--assets/fonts/k2d-11.woff2bin0 -> 12824 bytes
-rw-r--r--assets/fonts/k2d-12.woff2bin0 -> 15600 bytes
-rw-r--r--assets/fonts/k2d-13.woff2bin0 -> 12196 bytes
-rw-r--r--assets/fonts/k2d-14.woff2bin0 -> 4984 bytes
-rw-r--r--assets/fonts/k2d-15.woff2bin0 -> 12532 bytes
-rw-r--r--assets/fonts/k2d-16.woff2bin0 -> 15060 bytes
-rw-r--r--assets/fonts/k2d-17.woff2bin0 -> 12536 bytes
-rw-r--r--assets/fonts/k2d-18.woff2bin0 -> 5040 bytes
-rw-r--r--assets/fonts/k2d-19.woff2bin0 -> 12980 bytes
-rw-r--r--assets/fonts/k2d-2.woff2bin0 -> 4864 bytes
-rw-r--r--assets/fonts/k2d-20.woff2bin0 -> 15608 bytes
-rw-r--r--assets/fonts/k2d-21.woff2bin0 -> 12524 bytes
-rw-r--r--assets/fonts/k2d-22.woff2bin0 -> 5100 bytes
-rw-r--r--assets/fonts/k2d-23.woff2bin0 -> 12912 bytes
-rw-r--r--assets/fonts/k2d-24.woff2bin0 -> 15592 bytes
-rw-r--r--assets/fonts/k2d-25.woff2bin0 -> 12504 bytes
-rw-r--r--assets/fonts/k2d-26.woff2bin0 -> 5084 bytes
-rw-r--r--assets/fonts/k2d-27.woff2bin0 -> 13016 bytes
-rw-r--r--assets/fonts/k2d-28.woff2bin0 -> 15728 bytes
-rw-r--r--assets/fonts/k2d-29.woff2bin0 -> 11892 bytes
-rw-r--r--assets/fonts/k2d-3.woff2bin0 -> 12468 bytes
-rw-r--r--assets/fonts/k2d-30.woff2bin0 -> 5076 bytes
-rw-r--r--assets/fonts/k2d-31.woff2bin0 -> 12772 bytes
-rw-r--r--assets/fonts/k2d-32.woff2bin0 -> 15156 bytes
-rw-r--r--assets/fonts/k2d-33.woff2bin0 -> 10876 bytes
-rw-r--r--assets/fonts/k2d-34.woff2bin0 -> 4544 bytes
-rw-r--r--assets/fonts/k2d-35.woff2bin0 -> 11552 bytes
-rw-r--r--assets/fonts/k2d-36.woff2bin0 -> 13840 bytes
-rw-r--r--assets/fonts/k2d-37.woff2bin0 -> 10968 bytes
-rw-r--r--assets/fonts/k2d-38.woff2bin0 -> 4656 bytes
-rw-r--r--assets/fonts/k2d-39.woff2bin0 -> 11872 bytes
-rw-r--r--assets/fonts/k2d-4.woff2bin0 -> 15068 bytes
-rw-r--r--assets/fonts/k2d-40.woff2bin0 -> 14216 bytes
-rw-r--r--assets/fonts/k2d-41.woff2bin0 -> 10944 bytes
-rw-r--r--assets/fonts/k2d-42.woff2bin0 -> 4744 bytes
-rw-r--r--assets/fonts/k2d-43.woff2bin0 -> 11976 bytes
-rw-r--r--assets/fonts/k2d-44.woff2bin0 -> 14344 bytes
-rw-r--r--assets/fonts/k2d-45.woff2bin0 -> 10484 bytes
-rw-r--r--assets/fonts/k2d-46.woff2bin0 -> 4676 bytes
-rw-r--r--assets/fonts/k2d-47.woff2bin0 -> 11768 bytes
-rw-r--r--assets/fonts/k2d-48.woff2bin0 -> 13812 bytes
-rw-r--r--assets/fonts/k2d-49.woff2bin0 -> 10872 bytes
-rw-r--r--assets/fonts/k2d-5.woff2bin0 -> 12504 bytes
-rw-r--r--assets/fonts/k2d-50.woff2bin0 -> 4720 bytes
-rw-r--r--assets/fonts/k2d-51.woff2bin0 -> 12116 bytes
-rw-r--r--assets/fonts/k2d-52.woff2bin0 -> 14488 bytes
-rw-r--r--assets/fonts/k2d-53.woff2bin0 -> 10760 bytes
-rw-r--r--assets/fonts/k2d-54.woff2bin0 -> 4740 bytes
-rw-r--r--assets/fonts/k2d-55.woff2bin0 -> 12016 bytes
-rw-r--r--assets/fonts/k2d-56.woff2bin0 -> 14348 bytes
-rw-r--r--assets/fonts/k2d-57.woff2bin0 -> 10804 bytes
-rw-r--r--assets/fonts/k2d-58.woff2bin0 -> 4732 bytes
-rw-r--r--assets/fonts/k2d-59.woff2bin0 -> 12140 bytes
-rw-r--r--assets/fonts/k2d-6.woff2bin0 -> 4936 bytes
-rw-r--r--assets/fonts/k2d-60.woff2bin0 -> 14564 bytes
-rw-r--r--assets/fonts/k2d-61.woff2bin0 -> 10116 bytes
-rw-r--r--assets/fonts/k2d-62.woff2bin0 -> 4616 bytes
-rw-r--r--assets/fonts/k2d-63.woff2bin0 -> 11860 bytes
-rw-r--r--assets/fonts/k2d-64.woff2bin0 -> 13736 bytes
-rw-r--r--assets/fonts/k2d-7.woff2bin0 -> 12764 bytes
-rw-r--r--assets/fonts/k2d-8.woff2bin0 -> 15540 bytes
-rw-r--r--assets/fonts/k2d-9.woff2bin0 -> 12584 bytes
-rw-r--r--assets/icon.pngbin0 -> 33964 bytes
-rw-r--r--assets/icons/available16color.svg3
-rw-r--r--assets/icons/away16color.svg2
-rw-r--r--assets/icons/bubble24.svg9
-rw-r--r--assets/icons/chat16color.svg10
-rw-r--r--assets/icons/close24.svg4
-rw-r--r--assets/icons/dnd16color.svg2
-rw-r--r--assets/icons/newbubble24.svg4
-rw-r--r--assets/icons/xa16color.svg10
-rw-r--r--assets/lovely.svg3
-rw-r--r--assets/macaw-icon.pngbin33964 -> 1300928 bytes
-rw-r--r--assets/no-avatar.pngbin0 -> 34768 bytes
-rw-r--r--assets/overlayscrollbars.browser.es6.min.js10
-rw-r--r--assets/overlayscrollbars.min.css9
-rw-r--r--assets/style.scss1605
-rw-r--r--index.html33
-rw-r--r--notes/README.md (renamed from README.md)0
-rw-r--r--notes/components.md (renamed from components.md)2
-rw-r--r--notes/icons.md (renamed from icons.md)1
-rw-r--r--notes/ideas.md (renamed from ideas.md)31
-rw-r--r--old_code.md460
-rw-r--r--rust-toolchain.toml6
-rw-r--r--src/chat.rs90
-rw-r--r--src/client.rs36
-rw-r--r--src/components/avatar.rs53
-rw-r--r--src/components/chat_header.rs26
-rw-r--r--src/components/chats_list.rs156
-rw-r--r--src/components/chats_list/chats_list_item.rs93
-rw-r--r--src/components/icon.rs64
-rw-r--r--src/components/message.rs98
-rw-r--r--src/components/message_composer.rs134
-rw-r--r--src/components/message_history_buffer.rs158
-rw-r--r--src/components/mod.rs17
-rw-r--r--src/components/modal.rs25
-rw-r--r--src/components/new_chat.rs147
-rw-r--r--src/components/overlay.rs22
-rw-r--r--src/components/personal_status.rs249
-rw-r--r--src/components/roster_list.rs133
-rw-r--r--src/components/roster_list/contact_request_manager.rs284
-rw-r--r--src/components/roster_list/roster_list_item.rs122
-rw-r--r--src/components/sidebar.rs233
-rw-r--r--src/contact.rs67
-rw-r--r--src/context.rs3
-rw-r--r--src/files.rs53
-rw-r--r--src/icon.rs114
-rw-r--r--src/icons.rs127
-rw-r--r--src/lib.rs20
-rw-r--r--src/login_modal.rs112
-rw-r--r--src/main.rs1482
-rw-r--r--src/message.rs80
-rw-r--r--src/message_subscriptions.rs89
-rw-r--r--src/message_view.rs280
-rw-r--r--src/open_chats.rs72
-rw-r--r--src/roster.rs24
-rw-r--r--src/state_store.rs270
-rw-r--r--src/user.rs153
-rw-r--r--src/user_presences.rs173
-rw-r--r--src/views/login_page.rs202
-rw-r--r--src/views/macaw.rs199
-rw-r--r--src/views/macaw/open_chats_panel.rs81
-rw-r--r--src/views/macaw/settings.rs391
-rw-r--r--src/views/mod.rs39
157 files changed, 7434 insertions, 6132 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml
index c91c3f3..e2f8b2d 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,2 +1,32 @@
+# SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+[build]
+rustflags = [
+ # LLD (shipped with the Rust toolchain) is used as the default linker
+ # "-C", "link-arg=-Tlink.x",
+
+ # if you run into problems with LLD switch to the GNU linker by commenting out
+ # this line
+ # "-C", "linker=arm-none-eabi-ld",
+
+ # if you need to link to pre-compiled C libraries provided by a C toolchain
+ # use GCC as the linker by commenting out both lines above and then
+ # uncommenting the three lines below
+ "-C",
+ "target-feature=+atomics,+bulk-memory,+mutable-globals",
+ "-C", "link-arg=--shared-memory",
+ "-C", "link-arg=--max-memory=1073741824",
+ "-C", "link-arg=--import-memory",
+ "-C", "link-arg=--export=__wasm_init_tls",
+ "-C", "link-arg=--export=__tls_size",
+ "-C", "link-arg=--export=__tls_align",
+ "-C", "link-arg=--export=__tls_base",
+]
+
+[unstable]
+build-std = ["std", "panic_abort"]
+
[net]
git-fetch-with-cli = true
diff --git a/.gitignore b/.gitignore
index e13b707..7714fca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
+# SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
/target
-.vscode
-macaw.db
-filamento.db
+/dist
diff --git a/.helix/languages.toml b/.helix/languages.toml
index e828879..c06a8b0 100644
--- a/.helix/languages.toml
+++ b/.helix/languages.toml
@@ -1,4 +1,9 @@
+# SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
[language-server.rust-analyzer]
command = "rust-analyzer"
-environment = { "DATABASE_URL" = "sqlite://filamento.db" }
+config = { rustfmt.overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"], cargo.features = ["filamento/reactive_stores"], cargo.target = "wasm32-unknown-unknown" }
+# environment = { "DATABASE_URL" = "sqlite://filamento.db" }
# config = { cargo.features = ["stanza/rfc_6121", "stanza/xep_0203", "stanza/xep_0030", "stanza/xep_0060", "stanza/xep_0172", "stanza/xep_0390", "stanza/xep_0128", "stanza/xep_0115", "stanza/xep_0084", "sqlx/sqlite", "sqlx/runtime-tokio", "sqlx/uuid", "sqlx/chrono", "jid/sqlx", "uuid/v4", "tokio/full", "rsasl/provider_base64", "rsasl/plain", "rsasl/config_builder", "rsasl/scram-sha-1"] }
diff --git a/Cargo.lock b/Cargo.lock
index c8b13ff..17e8e4a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,22 +3,18 @@
version = 4
[[package]]
-name = "ab_glyph"
-version = "0.2.29"
+name = "accessory"
+version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0"
+checksum = "28e416a3ab45838bac2ab2d81b1088d738d7b2d2c5272a54d39366565a29bd80"
dependencies = [
- "ab_glyph_rasterizer",
- "owned_ttf_parser",
+ "macroific",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
]
[[package]]
-name = "ab_glyph_rasterizer"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
-
-[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -29,74 +25,29 @@ dependencies = [
[[package]]
name = "adler2"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
-
-[[package]]
-name = "ahash"
-version = "0.7.8"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
-dependencies = [
- "getrandom 0.2.15",
- "once_cell",
- "version_check",
-]
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
-name = "ahash"
-version = "0.8.11"
+name = "aho-corasick"
+version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
- "cfg-if",
- "getrandom 0.2.15",
- "once_cell",
- "version_check",
- "zerocopy 0.7.35",
+ "memchr",
]
[[package]]
name = "aligned-vec"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
-
-[[package]]
-name = "allocator-api2"
-version = "0.2.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
-
-[[package]]
-name = "android-activity"
-version = "0.6.0"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
+checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
dependencies = [
- "android-properties",
- "bitflags 2.9.0",
- "cc",
- "cesu8",
- "jni",
- "jni-sys",
- "libc",
- "log",
- "ndk",
- "ndk-context",
- "ndk-sys 0.6.0+11769913",
- "num_enum",
- "thiserror 1.0.69",
+ "equator",
]
[[package]]
-name = "android-properties"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
-
-[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -112,19 +63,21 @@ dependencies = [
]
[[package]]
-name = "anyhow"
-version = "1.0.97"
+name = "any_spawner"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
+checksum = "1384d3fe1eecb464229fcf6eebb72306591c56bf27b373561489458a7c73027d"
+dependencies = [
+ "futures",
+ "thiserror 2.0.12",
+ "wasm-bindgen-futures",
+]
[[package]]
-name = "approx"
-version = "0.5.1"
+name = "anyhow"
+version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
-dependencies = [
- "num-traits",
-]
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "arbitrary"
@@ -140,104 +93,16 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
-name = "arrayref"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
-
-[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
-name = "as-raw-xcb-connection"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
-
-[[package]]
-name = "ash"
-version = "0.37.3+1.3.251"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a"
-dependencies = [
- "libloading 0.7.4",
-]
-
-[[package]]
-name = "async-broadcast"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
-dependencies = [
- "event-listener",
- "event-listener-strategy",
- "futures-core",
- "pin-project-lite",
-]
-
-[[package]]
-name = "async-channel"
-version = "2.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
-dependencies = [
- "concurrent-queue",
- "event-listener-strategy",
- "futures-core",
- "pin-project-lite",
-]
-
-[[package]]
-name = "async-executor"
-version = "1.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec"
-dependencies = [
- "async-task",
- "concurrent-queue",
- "fastrand",
- "futures-lite",
- "slab",
-]
-
-[[package]]
-name = "async-fs"
-version = "2.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a"
-dependencies = [
- "async-lock",
- "blocking",
- "futures-lite",
-]
-
-[[package]]
-name = "async-io"
-version = "2.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
-dependencies = [
- "async-lock",
- "cfg-if",
- "concurrent-queue",
- "futures-io",
- "futures-lite",
- "parking",
- "polling",
- "rustix 0.38.44",
- "slab",
- "tracing",
- "windows-sys 0.59.0",
-]
-
-[[package]]
name = "async-lock"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -249,25 +114,6 @@ dependencies = [
]
[[package]]
-name = "async-process"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
-dependencies = [
- "async-channel",
- "async-io",
- "async-lock",
- "async-signal",
- "async-task",
- "blocking",
- "cfg-if",
- "event-listener",
- "futures-lite",
- "rustix 0.38.44",
- "tracing",
-]
-
-[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -275,34 +121,10 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
-name = "async-signal"
-version = "0.2.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
-dependencies = [
- "async-io",
- "async-lock",
- "atomic-waker",
- "cfg-if",
- "futures-core",
- "futures-io",
- "rustix 0.38.44",
- "signal-hook-registry",
- "slab",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "async-task"
-version = "4.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
-
-[[package]]
name = "async-trait"
version = "0.1.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -310,23 +132,38 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
-name = "atoi"
-version = "2.0.0"
+name = "attribute-derive"
+version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
+checksum = "0053e96dd3bec5b4879c23a138d6ef26f2cb936c9cdc96274ac2b9ed44b5bb54"
dependencies = [
- "num-traits",
+ "attribute-derive-macro",
+ "derive-where",
+ "manyhow",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
]
[[package]]
-name = "atomic-waker"
-version = "1.1.2"
+name = "attribute-derive-macro"
+version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+checksum = "463b53ad0fd5b460af4b1915fe045ff4d946d025fb6c4dc3337752eaa980f71b"
+dependencies = [
+ "collection_literals",
+ "interpolator",
+ "manyhow",
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+ "quote-use",
+ "syn 2.0.102",
+]
[[package]]
name = "autocfg"
@@ -336,9 +173,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "av1-grain"
-version = "0.2.3"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf"
+checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8"
dependencies = [
"anyhow",
"arrayvec",
@@ -359,9 +196,9 @@ dependencies = [
[[package]]
name = "backtrace"
-version = "0.3.74"
+version = "0.3.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
dependencies = [
"addr2line",
"cfg-if",
@@ -379,27 +216,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
-name = "base64ct"
-version = "1.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
-
-[[package]]
-name = "bit-set"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
-dependencies = [
- "bit-vec",
-]
-
-[[package]]
-name = "bit-vec"
-version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
-
-[[package]]
name = "bit_field"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -413,12 +229,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
-version = "2.9.0"
+version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
-dependencies = [
- "serde",
-]
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "bitstream-io"
@@ -427,12 +240,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
[[package]]
-name = "block"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
-
-[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -442,28 +249,6 @@ dependencies = [
]
[[package]]
-name = "block2"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
-dependencies = [
- "objc2",
-]
-
-[[package]]
-name = "blocking"
-version = "1.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
-dependencies = [
- "async-channel",
- "async-task",
- "futures-io",
- "futures-lite",
- "piper",
-]
-
-[[package]]
name = "built"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -471,41 +256,15 @@ checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b"
[[package]]
name = "bumpalo"
-version = "3.17.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
-
-[[package]]
-name = "by_address"
-version = "1.2.1"
+version = "3.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
+checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
[[package]]
name = "bytemuck"
-version = "1.22.0"
+version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
-dependencies = [
- "bytemuck_derive",
-]
-
-[[package]]
-name = "bytemuck_derive"
-version = "1.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "byteorder"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
[[package]]
name = "byteorder-lite"
@@ -520,36 +279,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
-name = "calloop"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
-dependencies = [
- "bitflags 2.9.0",
- "log",
- "polling",
- "rustix 0.38.44",
- "slab",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "calloop-wayland-source"
-version = "0.3.0"
+name = "camino"
+version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
-dependencies = [
- "calloop",
- "rustix 0.38.44",
- "wayland-backend",
- "wayland-client",
-]
+checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
[[package]]
name = "cc"
-version = "1.2.18"
+version = "1.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
+checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
dependencies = [
"jobserver",
"libc",
@@ -557,12 +296,6 @@ dependencies = [
]
[[package]]
-name = "cesu8"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
-
-[[package]]
name = "cfg-expr"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -574,27 +307,15 @@ dependencies = [
[[package]]
name = "cfg-if"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
-
-[[package]]
-name = "cfg_aliases"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
-
-[[package]]
-name = "cfg_aliases"
-version = "0.2.1"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "chrono"
-version = "0.4.40"
+version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -622,129 +343,111 @@ dependencies = [
]
[[package]]
-name = "clipboard-win"
-version = "5.4.0"
+name = "codee"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892"
+checksum = "0f18d705321923b1a9358e3fc3c57c3b50171196827fc7f5f10b053242aca627"
dependencies = [
- "error-code",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.12",
]
[[package]]
-name = "clipboard_macos"
-version = "0.1.1"
+name = "collection_literals"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f"
-dependencies = [
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
-]
+checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271"
[[package]]
-name = "clipboard_wayland"
-version = "0.2.2"
+name = "color_quant"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "003f886bc4e2987729d10c1db3424e7f80809f3fc22dbc16c685738887cb37b8"
-dependencies = [
- "smithay-clipboard",
-]
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
-name = "clipboard_x11"
-version = "0.4.2"
+name = "concurrent-queue"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4274ea815e013e0f9f04a2633423e14194e408a0576c943ce3d14ca56c50031c"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
- "thiserror 1.0.69",
- "x11rb",
+ "crossbeam-utils",
]
[[package]]
-name = "codespan-reporting"
-version = "0.11.1"
+name = "config"
+version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+checksum = "595aae20e65c3be792d05818e8c63025294ac3cb7e200f11459063a352a6ef80"
dependencies = [
- "termcolor",
- "unicode-width",
+ "convert_case 0.6.0",
+ "pathdiff",
+ "serde",
+ "toml",
+ "winnow 0.7.11",
]
[[package]]
-name = "color_quant"
-version = "1.1.0"
+name = "console_error_panic_hook"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
[[package]]
-name = "com"
-version = "0.6.0"
+name = "const-str"
+version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6"
-dependencies = [
- "com_macros",
-]
+checksum = "9e991226a70654b49d34de5ed064885f0bef0348a8e70018b8ff1ac80aa984a2"
[[package]]
-name = "com_macros"
-version = "0.6.0"
+name = "const_format"
+version = "0.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5"
+checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd"
dependencies = [
- "com_macros_support",
- "proc-macro2",
- "syn 1.0.109",
+ "const_format_proc_macros",
]
[[package]]
-name = "com_macros_support"
-version = "0.6.0"
+name = "const_format_proc_macros"
+version = "0.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c"
+checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "unicode-xid",
]
[[package]]
-name = "combine"
-version = "4.6.7"
+name = "const_str_slice_concat"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
-dependencies = [
- "bytes",
- "memchr",
-]
+checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b"
[[package]]
-name = "concurrent-queue"
-version = "2.5.0"
+name = "convert_case"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
- "crossbeam-utils",
+ "unicode-segmentation",
]
[[package]]
-name = "confy"
-version = "0.6.1"
+name = "convert_case"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45b1f4c00870f07dc34adcac82bb6a72cc5aabca8536ba1797e01df51d2ce9a0"
+checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
dependencies = [
- "directories",
- "serde",
- "thiserror 1.0.69",
- "toml",
+ "unicode-segmentation",
]
[[package]]
-name = "const-oid"
-version = "0.9.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
-
-[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -755,70 +458,12 @@ dependencies = [
]
[[package]]
-name = "core-foundation"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
-[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
-name = "core-graphics"
-version = "0.23.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
-dependencies = [
- "bitflags 1.3.2",
- "core-foundation 0.9.4",
- "core-graphics-types 0.1.3",
- "foreign-types 0.5.0",
- "libc",
-]
-
-[[package]]
-name = "core-graphics"
-version = "0.24.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
-dependencies = [
- "bitflags 2.9.0",
- "core-foundation 0.10.0",
- "core-graphics-types 0.2.0",
- "foreign-types 0.5.0",
- "libc",
-]
-
-[[package]]
-name = "core-graphics-types"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
-dependencies = [
- "bitflags 1.3.2",
- "core-foundation 0.9.4",
- "libc",
-]
-
-[[package]]
-name = "core-graphics-types"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
-dependencies = [
- "bitflags 2.9.0",
- "core-foundation 0.10.0",
- "libc",
-]
-
-[[package]]
name = "core2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -828,29 +473,6 @@ dependencies = [
]
[[package]]
-name = "cosmic-text"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2"
-dependencies = [
- "bitflags 2.9.0",
- "fontdb 0.16.2",
- "log",
- "rangemap",
- "rayon",
- "rustc-hash 1.1.0",
- "rustybuzz",
- "self_cell",
- "swash",
- "sys-locale",
- "ttf-parser 0.21.1",
- "unicode-bidi",
- "unicode-linebreak",
- "unicode-script",
- "unicode-segmentation",
-]
-
-[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -860,21 +482,6 @@ dependencies = [
]
[[package]]
-name = "crc"
-version = "3.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
-dependencies = [
- "crc-catalog",
-]
-
-[[package]]
-name = "crc-catalog"
-version = "2.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
-
-[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -903,15 +510,6 @@ dependencies = [
]
[[package]]
-name = "crossbeam-queue"
-version = "0.3.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
-dependencies = [
- "crossbeam-utils",
-]
-
-[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -934,102 +532,70 @@ dependencies = [
]
[[package]]
-name = "ctor-lite"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b"
-
-[[package]]
-name = "cursor-icon"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
-
-[[package]]
-name = "d3d12"
-version = "0.19.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307"
-dependencies = [
- "bitflags 2.9.0",
- "libloading 0.8.6",
- "winapi",
-]
-
-[[package]]
-name = "dark-light"
-version = "1.1.1"
+name = "dashmap"
+version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a76fa97167fa740dcdbfe18e8895601e1bc36525f09b044e00916e717c03a3c"
+checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [
- "dconf_rs",
- "detect-desktop-environment",
- "dirs 4.0.0",
- "objc",
- "rust-ini",
- "web-sys",
- "winreg 0.10.1",
- "zbus",
+ "cfg-if",
+ "crossbeam-utils",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
]
[[package]]
name = "data-encoding"
-version = "2.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
-
-[[package]]
-name = "data-url"
-version = "0.3.1"
+version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
+checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
-name = "dbus"
-version = "0.9.7"
+name = "delegate-display"
+version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b"
+checksum = "9926686c832494164c33a36bf65118f4bd6e704000b58c94681bf62e9ad67a74"
dependencies = [
- "libc",
- "libdbus-sys",
- "winapi",
+ "impartial-ord",
+ "itoa",
+ "macroific",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
]
[[package]]
-name = "dbus-secret-service"
-version = "4.0.3"
+name = "derive-where"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b42a16374481d92aed73ae45b1f120207d8e71d24fb89f357fadbd8f946fd84b"
+checksum = "e73f2692d4bd3cac41dca28934a39894200c9fabf49586d77d0e5954af1d7902"
dependencies = [
- "dbus",
- "futures-util",
- "num",
- "once_cell",
- "rand",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
]
[[package]]
-name = "dconf_rs"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b"
-
-[[package]]
-name = "der"
-version = "0.7.9"
+name = "derive_more"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
+checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
- "const-oid",
- "pem-rfc7468",
- "zeroize",
+ "derive_more-impl",
]
[[package]]
-name = "detect-desktop-environment"
-version = "0.2.0"
+name = "derive_more-impl"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810"
+checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
+ "unicode-xid",
+]
[[package]]
name = "digest"
@@ -1038,80 +604,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
- "const-oid",
"crypto-common",
"subtle",
]
[[package]]
-name = "directories"
-version = "5.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
-dependencies = [
- "dirs-sys 0.4.1",
-]
-
-[[package]]
-name = "dirs"
-version = "4.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
-dependencies = [
- "dirs-sys 0.3.7",
-]
-
-[[package]]
-name = "dirs"
-version = "6.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
-dependencies = [
- "dirs-sys 0.5.0",
-]
-
-[[package]]
-name = "dirs-sys"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
-dependencies = [
- "libc",
- "redox_users 0.4.6",
- "winapi",
-]
-
-[[package]]
-name = "dirs-sys"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
-dependencies = [
- "libc",
- "option-ext",
- "redox_users 0.4.6",
- "windows-sys 0.48.0",
-]
-
-[[package]]
-name = "dirs-sys"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
-dependencies = [
- "libc",
- "option-ext",
- "redox_users 0.5.0",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "dispatch"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
-
-[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1119,95 +616,30 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "dlib"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
-dependencies = [
- "libloading 0.8.6",
-]
-
-[[package]]
-name = "dlv-list"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
-
-[[package]]
-name = "dotenvy"
-version = "0.15.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
-
-[[package]]
-name = "downcast-rs"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
-
-[[package]]
-name = "dpi"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53"
-
-[[package]]
-name = "drm"
-version = "0.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1"
-dependencies = [
- "bitflags 2.9.0",
- "bytemuck",
- "drm-ffi",
- "drm-fourcc",
- "rustix 0.38.44",
-]
-
-[[package]]
-name = "drm-ffi"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53"
-dependencies = [
- "drm-sys",
- "rustix 0.38.44",
+ "syn 2.0.102",
]
[[package]]
-name = "drm-fourcc"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4"
-
-[[package]]
-name = "drm-sys"
-version = "0.7.0"
+name = "drain_filter_polyfill"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986"
-dependencies = [
- "libc",
- "linux-raw-sys 0.6.5",
-]
+checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
-dependencies = [
- "serde",
-]
[[package]]
-name = "endi"
-version = "1.1.0"
+name = "either_of"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
+checksum = "216d23e0ec69759a17f05e1c553f3a6870e5ec73420fbb07807a6f34d5d1d5a4"
+dependencies = [
+ "paste",
+ "pin-project-lite",
+]
[[package]]
name = "enum-as-inner"
@@ -1222,24 +654,23 @@ dependencies = [
]
[[package]]
-name = "enumflags2"
-version = "0.7.11"
+name = "equator"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147"
+checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
dependencies = [
- "enumflags2_derive",
- "serde",
+ "equator-macro",
]
[[package]]
-name = "enumflags2_derive"
-version = "0.7.11"
+name = "equator-macro"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79"
+checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
@@ -1249,49 +680,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
-name = "errno"
-version = "0.3.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
-dependencies = [
- "libc",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "error-code"
-version = "3.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f"
-
-[[package]]
-name = "etagere"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342"
-dependencies = [
- "euclid",
- "svg_fmt",
-]
-
-[[package]]
-name = "etcetera"
-version = "0.8.0"
+name = "erased"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
-dependencies = [
- "cfg-if",
- "home",
- "windows-sys 0.48.0",
-]
+checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472"
[[package]]
-name = "euclid"
-version = "0.22.11"
+name = "errno"
+version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48"
+checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [
- "num-traits",
+ "libc",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -1331,10 +732,28 @@ dependencies = [
]
[[package]]
-name = "fast-srgb8"
-version = "1.0.0"
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
+[[package]]
+name = "fancy_constructor"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28a27643a5d05f3a22f5afd6e0d0e6e354f92d37907006f97b84b9cb79082198"
+dependencies = [
+ "macroific",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
+]
[[package]]
name = "fastrand"
@@ -1359,46 +778,41 @@ dependencies = [
"chrono",
"futures",
"hex",
- "image 0.25.6",
+ "image",
"jid",
+ "js-sys",
"lampada",
+ "reactive_stores",
+ "rusqlite",
"sha1",
"sha2",
"sha3",
- "sqlx",
"stanza",
"thiserror 2.0.12",
"tokio",
+ "tokio_with_wasm",
"tracing",
"uuid",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
]
[[package]]
name = "flate2"
-version = "1.1.1"
+version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
+checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
-name = "float-cmp"
-version = "0.9.0"
+name = "fnv"
+version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
-
-[[package]]
-name = "flume"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
-dependencies = [
- "futures-core",
- "futures-sink",
- "spin",
-]
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
@@ -1407,79 +821,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
-name = "font-types"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492"
-dependencies = [
- "bytemuck",
-]
-
-[[package]]
-name = "fontconfig-parser"
-version = "0.5.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1fcfcd44ca6e90c921fee9fa665d530b21ef1327a4c1a6c5250ea44b776ada7"
-dependencies = [
- "roxmltree",
-]
-
-[[package]]
-name = "fontdb"
-version = "0.16.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3"
-dependencies = [
- "fontconfig-parser",
- "log",
- "memmap2",
- "slotmap",
- "tinyvec",
- "ttf-parser 0.20.0",
-]
-
-[[package]]
-name = "fontdb"
-version = "0.18.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770"
-dependencies = [
- "fontconfig-parser",
- "log",
- "memmap2",
- "slotmap",
- "tinyvec",
- "ttf-parser 0.21.1",
-]
-
-[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
- "foreign-types-shared 0.1.1",
-]
-
-[[package]]
-name = "foreign-types"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
-dependencies = [
- "foreign-types-macros",
- "foreign-types-shared 0.3.1",
-]
-
-[[package]]
-name = "foreign-types-macros"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "foreign-types-shared",
]
[[package]]
@@ -1489,12 +836,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
-name = "foreign-types-shared"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
-
-[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1504,6 +845,12 @@ dependencies = [
]
[[package]]
+name = "fragile"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
+
+[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1547,36 +894,12 @@ dependencies = [
]
[[package]]
-name = "futures-intrusive"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
-dependencies = [
- "futures-core",
- "lock_api",
- "parking_lot 0.12.3",
-]
-
-[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
-name = "futures-lite"
-version = "2.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
-dependencies = [
- "fastrand",
- "futures-core",
- "futures-io",
- "parking",
- "pin-project-lite",
-]
-
-[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1584,7 +907,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
@@ -1628,31 +951,23 @@ dependencies = [
]
[[package]]
-name = "gethostname"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818"
-dependencies = [
- "libc",
- "windows-targets 0.48.5",
-]
-
-[[package]]
name = "getrandom"
-version = "0.2.15"
+version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "wasm-bindgen",
]
[[package]]
name = "getrandom"
-version = "0.3.2"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
@@ -1677,104 +992,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
-name = "gl_generator"
-version = "0.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
-dependencies = [
- "khronos_api",
- "log",
- "xml-rs",
-]
-
-[[package]]
-name = "glam"
-version = "0.25.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3"
-
-[[package]]
-name = "glow"
-version = "0.13.1"
+name = "gloo-net"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1"
+checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580"
dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-sink",
+ "gloo-utils",
+ "http",
"js-sys",
- "slotmap",
+ "pin-project",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
"wasm-bindgen",
+ "wasm-bindgen-futures",
"web-sys",
]
[[package]]
-name = "glutin_wgl_sys"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead"
-dependencies = [
- "gl_generator",
-]
-
-[[package]]
-name = "gpu-alloc"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
-dependencies = [
- "bitflags 2.9.0",
- "gpu-alloc-types",
-]
-
-[[package]]
-name = "gpu-alloc-types"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
-dependencies = [
- "bitflags 2.9.0",
-]
-
-[[package]]
-name = "gpu-allocator"
-version = "0.25.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884"
-dependencies = [
- "log",
- "presser",
- "thiserror 1.0.69",
- "winapi",
- "windows",
-]
-
-[[package]]
-name = "gpu-descriptor"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c"
-dependencies = [
- "bitflags 2.9.0",
- "gpu-descriptor-types",
- "hashbrown 0.14.5",
-]
-
-[[package]]
-name = "gpu-descriptor-types"
-version = "0.1.2"
+name = "gloo-utils"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c"
+checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [
- "bitflags 2.9.0",
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
]
[[package]]
-name = "guillotiere"
-version = "0.6.2"
+name = "guardian"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782"
-dependencies = [
- "euclid",
- "svg_fmt",
-]
+checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f"
[[package]]
name = "half"
@@ -1788,31 +1043,16 @@ dependencies = [
[[package]]
name = "hashbrown"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
-dependencies = [
- "ahash 0.7.8",
-]
-
-[[package]]
-name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
-dependencies = [
- "ahash 0.8.11",
- "allocator-api2",
-]
[[package]]
name = "hashbrown"
-version = "0.15.2"
+version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
dependencies = [
- "allocator-api2",
- "equivalent",
"foldhash",
]
@@ -1822,22 +1062,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
- "hashbrown 0.15.2",
-]
-
-[[package]]
-name = "hassle-rs"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890"
-dependencies = [
- "bitflags 2.9.0",
- "com",
- "libc",
- "libloading 0.8.6",
- "thiserror 1.0.69",
- "widestring",
- "winapi",
+ "hashbrown 0.15.4",
]
[[package]]
@@ -1854,15 +1079,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
-
-[[package]]
-name = "hermit-abi"
-version = "0.4.0"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hex"
@@ -1871,47 +1090,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
-name = "hexf-parse"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
-
-[[package]]
-name = "hkdf"
-version = "0.12.4"
+name = "hmac"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
- "hmac",
+ "digest",
]
[[package]]
-name = "hmac"
-version = "0.12.1"
+name = "html-escape"
+version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
dependencies = [
- "digest",
+ "utf8-width",
]
[[package]]
-name = "home"
-version = "0.5.11"
+name = "http"
+version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
dependencies = [
- "windows-sys 0.59.0",
+ "bytes",
+ "fnv",
+ "itoa",
]
[[package]]
-name = "hostname"
-version = "0.4.1"
+name = "hydration_context"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
+checksum = "e8714ae4adeaa846d838f380fbd72f049197de629948f91bf045329e0cf0a283"
dependencies = [
- "cfg-if",
- "libc",
- "windows-link",
+ "futures",
+ "once_cell",
+ "or_poisoned",
+ "pin-project-lite",
+ "serde",
+ "throw_error",
]
[[package]]
@@ -1926,7 +1144,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
- "windows-core 0.61.0",
+ "windows-core",
]
[[package]]
@@ -1939,206 +1157,23 @@ dependencies = [
]
[[package]]
-name = "iced"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88acfabc84ec077eaf9ede3457ffa3a104626d79022a9bf7f296093b1d60c73f"
-dependencies = [
- "iced_core",
- "iced_futures",
- "iced_renderer",
- "iced_widget",
- "iced_winit",
- "image 0.24.9",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "iced_core"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0013a238275494641bf8f1732a23a808196540dc67b22ff97099c044ae4c8a1c"
-dependencies = [
- "bitflags 2.9.0",
- "bytes",
- "dark-light",
- "glam",
- "log",
- "num-traits",
- "once_cell",
- "palette",
- "rustc-hash 2.1.1",
- "smol_str",
- "thiserror 1.0.69",
- "web-time",
-]
-
-[[package]]
-name = "iced_futures"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c04a6745ba2e80f32cf01e034fd00d853aa4f4cd8b91888099cb7aaee0d5d7c"
-dependencies = [
- "futures",
- "iced_core",
- "log",
- "rustc-hash 2.1.1",
- "tokio",
- "wasm-bindgen-futures",
- "wasm-timer",
-]
-
-[[package]]
-name = "iced_glyphon"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41c3bb56f1820ca252bc1d0994ece33d233a55657c0c263ea7cb16895adbde82"
-dependencies = [
- "cosmic-text",
- "etagere",
- "lru",
- "rustc-hash 2.1.1",
- "wgpu",
-]
-
-[[package]]
-name = "iced_graphics"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba25a18cfa6d5cc160aca7e1b34f73ccdff21680fa8702168c09739767b6c66f"
-dependencies = [
- "bitflags 2.9.0",
- "bytemuck",
- "cosmic-text",
- "half",
- "iced_core",
- "iced_futures",
- "image 0.24.9",
- "kamadak-exif",
- "log",
- "once_cell",
- "raw-window-handle",
- "rustc-hash 2.1.1",
- "thiserror 1.0.69",
- "unicode-segmentation",
-]
-
-[[package]]
-name = "iced_renderer"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73558208059f9e622df2bf434e044ee2f838ce75201a023cf0ca3e1244f46c2a"
-dependencies = [
- "iced_graphics",
- "iced_tiny_skia",
- "iced_wgpu",
- "log",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "iced_runtime"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "348b5b2c61c934d88ca3b0ed1ed913291e923d086a66fa288ce9669da9ef62b5"
-dependencies = [
- "bytes",
- "iced_core",
- "iced_futures",
- "raw-window-handle",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "iced_tiny_skia"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c625d368284fcc43b0b36b176f76eff1abebe7959dd58bd8ce6897d641962a50"
-dependencies = [
- "bytemuck",
- "cosmic-text",
- "iced_graphics",
- "kurbo 0.10.4",
- "log",
- "resvg",
- "rustc-hash 2.1.1",
- "softbuffer",
- "tiny-skia",
-]
-
-[[package]]
-name = "iced_wgpu"
-version = "0.13.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15708887133671d2bcc6c1d01d1f176f43a64d6cdc3b2bf893396c3ee498295f"
-dependencies = [
- "bitflags 2.9.0",
- "bytemuck",
- "futures",
- "glam",
- "guillotiere",
- "iced_glyphon",
- "iced_graphics",
- "log",
- "once_cell",
- "resvg",
- "rustc-hash 2.1.1",
- "thiserror 1.0.69",
- "wgpu",
-]
-
-[[package]]
-name = "iced_widget"
-version = "0.13.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81429e1b950b0e4bca65be4c4278fea6678ea782030a411778f26fa9f8983e1d"
-dependencies = [
- "iced_renderer",
- "iced_runtime",
- "num-traits",
- "once_cell",
- "rustc-hash 2.1.1",
- "thiserror 1.0.69",
- "unicode-segmentation",
-]
-
-[[package]]
-name = "iced_winit"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f44cd4e1c594b6334f409282937bf972ba14d31fedf03c23aa595d982a2fda28"
-dependencies = [
- "iced_futures",
- "iced_graphics",
- "iced_runtime",
- "log",
- "rustc-hash 2.1.1",
- "thiserror 1.0.69",
- "tracing",
- "wasm-bindgen-futures",
- "web-sys",
- "winapi",
- "window_clipboard",
- "winit",
-]
-
-[[package]]
name = "icu_collections"
-version = "1.5.0"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
dependencies = [
"displaydoc",
+ "potential_utf",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
-name = "icu_locid"
-version = "1.5.0"
+name = "icu_locale_core"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
dependencies = [
"displaydoc",
"litemap",
@@ -2148,30 +1183,10 @@ dependencies = [
]
[[package]]
-name = "icu_locid_transform"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
-dependencies = [
- "displaydoc",
- "icu_locid",
- "icu_locid_transform_data",
- "icu_provider",
- "tinystr",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid_transform_data"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d"
-
-[[package]]
name = "icu_normalizer"
-version = "1.5.0"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
dependencies = [
"displaydoc",
"icu_collections",
@@ -2179,68 +1194,55 @@ dependencies = [
"icu_properties",
"icu_provider",
"smallvec",
- "utf16_iter",
- "utf8_iter",
- "write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
-version = "1.5.1"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]]
name = "icu_properties"
-version = "1.5.1"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
dependencies = [
"displaydoc",
"icu_collections",
- "icu_locid_transform",
+ "icu_locale_core",
"icu_properties_data",
"icu_provider",
- "tinystr",
+ "potential_utf",
+ "zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
-version = "1.5.1"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
[[package]]
name = "icu_provider"
-version = "1.5.0"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
dependencies = [
"displaydoc",
- "icu_locid",
- "icu_provider_macros",
+ "icu_locale_core",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
+ "zerotrie",
"zerovec",
]
[[package]]
-name = "icu_provider_macros"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
-]
-
-[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2264,9 +1266,9 @@ dependencies = [
[[package]]
name = "idna_adapter"
-version = "1.2.0"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
dependencies = [
"icu_normalizer",
"icu_properties",
@@ -2274,24 +1276,6 @@ dependencies = [
[[package]]
name = "image"
-version = "0.24.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
-dependencies = [
- "bytemuck",
- "byteorder",
- "color_quant",
- "exr",
- "gif",
- "jpeg-decoder",
- "num-traits",
- "png",
- "qoi",
- "tiff",
-]
-
-[[package]]
-name = "image"
version = "0.25.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a"
@@ -2315,43 +1299,73 @@ dependencies = [
[[package]]
name = "image-webp"
-version = "0.2.1"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f"
+checksum = "14d75c7014ddab93c232bc6bb9f64790d3dfd1d605199acd4b40b6d69e691e9f"
dependencies = [
"byteorder-lite",
"quick-error",
]
[[package]]
-name = "imagesize"
-version = "0.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
-
-[[package]]
name = "imgref"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
[[package]]
-name = "indexmap"
-version = "2.9.0"
+name = "impartial-ord"
+version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+checksum = "0ab604ee7085efba6efc65e4ebca0e9533e3aff6cb501d7d77b211e3a781c6d5"
dependencies = [
- "equivalent",
- "hashbrown 0.15.2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
]
[[package]]
-name = "instant"
-version = "0.1.13"
+name = "indexed_db_futures"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+checksum = "69ff41758cbd104e91033bb53bc449bec7eea65652960c81eddf3fc146ecea19"
dependencies = [
+ "accessory",
"cfg-if",
+ "delegate-display",
+ "derive_more",
+ "fancy_constructor",
+ "indexed_db_futures_macros_internal",
+ "js-sys",
+ "sealed",
+ "smallvec",
+ "thiserror 2.0.12",
+ "tokio",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "indexed_db_futures_macros_internal"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caeba94923b68f254abef921cea7e7698bf4675fdd89d7c58bf1ed885b49a27d"
+dependencies = [
+ "macroific",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.4",
]
[[package]]
@@ -2362,10 +1376,16 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
+name = "interpolator"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8"
+
+[[package]]
name = "ipconfig"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2374,7 +1394,7 @@ dependencies = [
"socket2",
"widestring",
"windows-sys 0.48.0",
- "winreg 0.50.0",
+ "winreg",
]
[[package]]
@@ -2393,6 +1413,15 @@ dependencies = [
]
[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2402,38 +1431,16 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
name = "jid"
version = "0.1.0"
dependencies = [
- "sqlx",
-]
-
-[[package]]
-name = "jni"
-version = "0.21.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
-dependencies = [
- "cesu8",
- "cfg-if",
- "combine",
- "jni-sys",
- "log",
- "thiserror 1.0.69",
- "walkdir",
- "windows-sys 0.45.0",
+ "rusqlite",
]
[[package]]
-name = "jni-sys"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
-
-[[package]]
name = "jobserver"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [
- "getrandom 0.3.2",
+ "getrandom 0.3.3",
"libc",
]
@@ -2442,9 +1449,6 @@ name = "jpeg-decoder"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
-dependencies = [
- "rayon",
-]
[[package]]
name = "js-sys"
@@ -2457,15 +1461,6 @@ dependencies = [
]
[[package]]
-name = "kamadak-exif"
-version = "0.5.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077"
-dependencies = [
- "mutate_once",
-]
-
-[[package]]
name = "keccak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2475,57 +1470,6 @@ dependencies = [
]
[[package]]
-name = "keyring"
-version = "3.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1961983669d57bdfe6c0f3ef8e4c229b5ef751afcc7d87e4271d2f71f6ccfa8b"
-dependencies = [
- "byteorder",
- "dbus-secret-service",
- "log",
- "security-framework 2.11.1",
- "security-framework 3.2.0",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "khronos-egl"
-version = "6.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
-dependencies = [
- "libc",
- "libloading 0.8.6",
- "pkg-config",
-]
-
-[[package]]
-name = "khronos_api"
-version = "3.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
-
-[[package]]
-name = "kurbo"
-version = "0.10.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440"
-dependencies = [
- "arrayvec",
- "smallvec",
-]
-
-[[package]]
-name = "kurbo"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f"
-dependencies = [
- "arrayvec",
- "smallvec",
-]
-
-[[package]]
name = "lampada"
version = "0.1.0"
dependencies = [
@@ -2536,6 +1480,7 @@ dependencies = [
"stanza",
"thiserror 2.0.12",
"tokio",
+ "tokio_with_wasm",
"tracing",
]
@@ -2544,9 +1489,6 @@ name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
-dependencies = [
- "spin",
-]
[[package]]
name = "lebe"
@@ -2555,95 +1497,167 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
-name = "libc"
-version = "0.2.171"
+name = "leptos"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
+checksum = "2ceaf7d86820125c57dcd380edac4b972debf480ee4c7eea6dd7cea212615978"
+dependencies = [
+ "any_spawner",
+ "cfg-if",
+ "either_of",
+ "futures",
+ "getrandom 0.2.16",
+ "hydration_context",
+ "leptos_config",
+ "leptos_dom",
+ "leptos_hot_reload",
+ "leptos_macro",
+ "leptos_server",
+ "oco_ref",
+ "or_poisoned",
+ "paste",
+ "reactive_graph",
+ "rustc-hash",
+ "rustc_version",
+ "send_wrapper",
+ "serde",
+ "serde_qs",
+ "server_fn",
+ "slotmap",
+ "tachys",
+ "thiserror 2.0.12",
+ "throw_error",
+ "typed-builder",
+ "typed-builder-macro",
+ "wasm-bindgen",
+ "web-sys",
+]
[[package]]
-name = "libdbus-sys"
-version = "0.2.5"
+name = "leptos_config"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72"
+checksum = "cf4100ad54455f82b686c9d0500a45c909eb50ce68ccb2ed51439ff2596f54fd"
dependencies = [
- "pkg-config",
+ "config",
+ "regex",
+ "serde",
+ "thiserror 2.0.12",
+ "typed-builder",
]
[[package]]
-name = "libfuzzer-sys"
-version = "0.4.9"
+name = "leptos_dom"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75"
+checksum = "adaca2ec1d6215a7c43dc6353d487e4e34faf325b8e4df2ca3df488964d403be"
dependencies = [
- "arbitrary",
- "cc",
+ "js-sys",
+ "or_poisoned",
+ "reactive_graph",
+ "send_wrapper",
+ "tachys",
+ "wasm-bindgen",
+ "web-sys",
]
[[package]]
-name = "libloading"
-version = "0.7.4"
+name = "leptos_hot_reload"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+checksum = "597f84532609518092960ac241741963c90c216ee11f752e1b238b846f043640"
dependencies = [
- "cfg-if",
- "winapi",
+ "anyhow",
+ "camino",
+ "indexmap",
+ "parking_lot",
+ "proc-macro2",
+ "quote",
+ "rstml",
+ "serde",
+ "syn 2.0.102",
+ "walkdir",
]
[[package]]
-name = "libloading"
-version = "0.8.6"
+name = "leptos_macro"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
+checksum = "8a2ec91579e9a1344adc1eee637cb774a01354a3d25857cbd028b0289efe131d"
dependencies = [
+ "attribute-derive",
"cfg-if",
- "windows-targets 0.52.6",
+ "convert_case 0.8.0",
+ "html-escape",
+ "itertools 0.14.0",
+ "leptos_hot_reload",
+ "prettyplease",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "rstml",
+ "rustc_version",
+ "server_fn_macro",
+ "syn 2.0.102",
+ "uuid",
]
[[package]]
-name = "libm"
-version = "0.2.11"
+name = "leptos_server"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
+checksum = "5af59932aa8a640da4d3d20650cf07084433e25db0ee690203d893b81773db29"
+dependencies = [
+ "any_spawner",
+ "base64",
+ "codee",
+ "futures",
+ "hydration_context",
+ "or_poisoned",
+ "reactive_graph",
+ "send_wrapper",
+ "serde",
+ "serde_json",
+ "server_fn",
+ "tachys",
+]
[[package]]
-name = "libredox"
-version = "0.1.3"
+name = "libc"
+version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75"
dependencies = [
- "bitflags 2.9.0",
- "libc",
- "redox_syscall 0.5.11",
+ "arbitrary",
+ "cc",
]
[[package]]
name = "libsqlite3-sys"
-version = "0.30.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
+version = "0.32.0"
+source = "git+https://github.com/Spxg/rusqlite.git?branch=wasm-demo#4820de79e45d0604e0596641cbb5dd86aa53f0ed"
dependencies = [
- "cc",
"pkg-config",
"vcpkg",
]
[[package]]
-name = "linked-hash-map"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
-
-[[package]]
-name = "linux-raw-sys"
-version = "0.4.15"
+name = "linear-map"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee"
[[package]]
-name = "linux-raw-sys"
-version = "0.6.5"
+name = "linked-hash-map"
+version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
@@ -2653,15 +1667,15 @@ checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "litemap"
-version = "0.7.5"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "lock_api"
-version = "0.4.12"
+version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [
"autocfg",
"scopeguard",
@@ -2683,12 +1697,6 @@ dependencies = [
]
[[package]]
-name = "lru"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
-
-[[package]]
name = "lru-cache"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2701,124 +1709,143 @@ dependencies = [
name = "luz"
version = "0.1.0"
dependencies = [
- "async-recursion",
- "async-trait",
- "futures",
+ "getrandom 0.2.16",
"jid",
- "lazy_static",
- "nanoid",
+ "js-sys",
"peanuts",
- "pin-project",
- "pin-project-lite",
"rsasl",
"stanza",
- "take_mut",
"thiserror 2.0.12",
"tokio",
"tokio-native-tls",
"tracing",
"trust-dns-resolver",
- "try_map",
+ "uuid",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
]
[[package]]
-name = "macaw"
+name = "macaw-web"
version = "0.1.0"
dependencies = [
+ "base64",
"chrono",
"chrono-humanize",
- "confy",
- "dirs 6.0.0",
+ "console_error_panic_hook",
"filamento",
- "iced",
+ "futures",
"indexmap",
"jid",
- "keyring",
+ "js-sys",
+ "leptos",
+ "overlay-scrollbars",
+ "reactive_stores",
"serde",
+ "stylance",
"thiserror 2.0.12",
"tokio",
- "tokio-stream",
- "toml",
"tracing",
- "tracing-subscriber",
+ "tracing-wasm",
"uuid",
+ "web-sys",
]
[[package]]
-name = "malloc_buf"
-version = "0.0.6"
+name = "macroific"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+checksum = "89f276537b4b8f981bf1c13d79470980f71134b7bdcc5e6e911e910e556b0285"
dependencies = [
- "libc",
+ "macroific_attr_parse",
+ "macroific_core",
+ "macroific_macro",
]
[[package]]
-name = "matches"
-version = "0.1.10"
+name = "macroific_attr_parse"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+checksum = "ad4023761b45fcd36abed8fb7ae6a80456b0a38102d55e89a57d9a594a236be9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sealed",
+ "syn 2.0.102",
+]
[[package]]
-name = "maybe-rayon"
-version = "0.1.1"
+name = "macroific_core"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
+checksum = "d0a7594d3c14916fa55bef7e9d18c5daa9ed410dd37504251e4b75bbdeec33e3"
dependencies = [
- "cfg-if",
- "rayon",
+ "proc-macro2",
+ "quote",
+ "sealed",
+ "syn 2.0.102",
]
[[package]]
-name = "md-5"
-version = "0.10.6"
+name = "macroific_macro"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+checksum = "4da6f2ed796261b0a74e2b52b42c693bb6dee1effba3a482c49592659f824b3b"
dependencies = [
- "cfg-if",
- "digest",
+ "macroific_attr_parse",
+ "macroific_core",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
]
[[package]]
-name = "memchr"
-version = "2.7.4"
+name = "manyhow"
+version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587"
+dependencies = [
+ "manyhow-macros",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
+]
[[package]]
-name = "memmap2"
-version = "0.9.5"
+name = "manyhow-macros"
+version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
+checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495"
dependencies = [
- "libc",
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
]
[[package]]
-name = "memoffset"
-version = "0.9.1"
+name = "matches"
+version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
-dependencies = [
- "autocfg",
-]
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
-name = "metal"
-version = "0.27.0"
+name = "maybe-rayon"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25"
+checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
dependencies = [
- "bitflags 2.9.0",
- "block",
- "core-graphics-types 0.1.3",
- "foreign-types 0.5.0",
- "log",
- "objc",
- "paste",
+ "cfg-if",
+ "rayon",
]
[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2826,9 +1853,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
-version = "0.8.8"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
@@ -2836,48 +1863,13 @@ dependencies = [
[[package]]
name = "mio"
-version = "1.0.3"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "mutate_once"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b"
-
-[[package]]
-name = "naga"
-version = "0.19.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843"
-dependencies = [
- "bit-set",
- "bitflags 2.9.0",
- "codespan-reporting",
- "hexf-parse",
- "indexmap",
- "log",
- "num-traits",
- "rustc-hash 1.1.0",
- "spirv",
- "termcolor",
- "thiserror 1.0.69",
- "unicode-xid",
-]
-
-[[package]]
-name = "nanoid"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
-dependencies = [
- "rand",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -2892,68 +1884,22 @@ dependencies = [
"openssl-probe",
"openssl-sys",
"schannel",
- "security-framework 2.11.1",
+ "security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
-name = "ndk"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
-dependencies = [
- "bitflags 2.9.0",
- "jni-sys",
- "log",
- "ndk-sys 0.6.0+11769913",
- "num_enum",
- "raw-window-handle",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "ndk-context"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
-
-[[package]]
-name = "ndk-sys"
-version = "0.5.0+25.2.9519653"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
-dependencies = [
- "jni-sys",
-]
-
-[[package]]
-name = "ndk-sys"
-version = "0.6.0+11769913"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
-dependencies = [
- "jni-sys",
-]
-
-[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
-name = "nix"
-version = "0.29.0"
+name = "next_tuple"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
-dependencies = [
- "bitflags 2.9.0",
- "cfg-if",
- "cfg_aliases 0.2.1",
- "libc",
- "memoffset",
-]
+checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28"
[[package]]
name = "nom"
@@ -2972,30 +1918,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
-name = "nu-ansi-term"
-version = "0.46.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
-dependencies = [
- "overload",
- "winapi",
-]
-
-[[package]]
-name = "num"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
-dependencies = [
- "num-bigint",
- "num-complex",
- "num-integer",
- "num-iter",
- "num-rational",
- "num-traits",
-]
-
-[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3006,32 +1928,6 @@ dependencies = [
]
[[package]]
-name = "num-bigint-dig"
-version = "0.8.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
-dependencies = [
- "byteorder",
- "lazy_static",
- "libm",
- "num-integer",
- "num-iter",
- "num-traits",
- "rand",
- "smallvec",
- "zeroize",
-]
-
-[[package]]
-name = "num-complex"
-version = "0.4.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
-dependencies = [
- "num-traits",
-]
-
-[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3039,7 +1935,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
@@ -3052,17 +1948,6 @@ dependencies = [
]
[[package]]
-name = "num-iter"
-version = "0.1.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
-dependencies = [
- "autocfg",
- "num-integer",
- "num-traits",
-]
-
-[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3080,269 +1965,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
- "libm",
]
[[package]]
name = "num_cpus"
-version = "1.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
-dependencies = [
- "hermit-abi 0.3.9",
- "libc",
-]
-
-[[package]]
-name = "num_enum"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
-dependencies = [
- "num_enum_derive",
-]
-
-[[package]]
-name = "num_enum_derive"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
-dependencies = [
- "proc-macro-crate",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "objc"
-version = "0.2.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
-dependencies = [
- "malloc_buf",
- "objc_exception",
-]
-
-[[package]]
-name = "objc-sys"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
-
-[[package]]
-name = "objc2"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
-dependencies = [
- "objc-sys",
- "objc2-encode",
-]
-
-[[package]]
-name = "objc2-app-kit"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
-dependencies = [
- "bitflags 2.9.0",
- "block2",
- "libc",
- "objc2",
- "objc2-core-data",
- "objc2-core-image",
- "objc2-foundation",
- "objc2-quartz-core",
-]
-
-[[package]]
-name = "objc2-cloud-kit"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
-dependencies = [
- "bitflags 2.9.0",
- "block2",
- "objc2",
- "objc2-core-location",
- "objc2-foundation",
-]
-
-[[package]]
-name = "objc2-contacts"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
-dependencies = [
- "block2",
- "objc2",
- "objc2-foundation",
-]
-
-[[package]]
-name = "objc2-core-data"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
-dependencies = [
- "bitflags 2.9.0",
- "block2",
- "objc2",
- "objc2-foundation",
-]
-
-[[package]]
-name = "objc2-core-image"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
-dependencies = [
- "block2",
- "objc2",
- "objc2-foundation",
- "objc2-metal",
-]
-
-[[package]]
-name = "objc2-core-location"
-version = "0.2.2"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
dependencies = [
- "block2",
- "objc2",
- "objc2-contacts",
- "objc2-foundation",
-]
-
-[[package]]
-name = "objc2-encode"
-version = "4.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
-
-[[package]]
-name = "objc2-foundation"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
-dependencies = [
- "bitflags 2.9.0",
- "block2",
- "dispatch",
+ "hermit-abi",
"libc",
- "objc2",
-]
-
-[[package]]
-name = "objc2-link-presentation"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
-dependencies = [
- "block2",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
]
[[package]]
-name = "objc2-metal"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
-dependencies = [
- "bitflags 2.9.0",
- "block2",
- "objc2",
- "objc2-foundation",
-]
-
-[[package]]
-name = "objc2-quartz-core"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
-dependencies = [
- "bitflags 2.9.0",
- "block2",
- "objc2",
- "objc2-foundation",
- "objc2-metal",
-]
-
-[[package]]
-name = "objc2-symbols"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
-dependencies = [
- "objc2",
- "objc2-foundation",
-]
-
-[[package]]
-name = "objc2-ui-kit"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
-dependencies = [
- "bitflags 2.9.0",
- "block2",
- "objc2",
- "objc2-cloud-kit",
- "objc2-core-data",
- "objc2-core-image",
- "objc2-core-location",
- "objc2-foundation",
- "objc2-link-presentation",
- "objc2-quartz-core",
- "objc2-symbols",
- "objc2-uniform-type-identifiers",
- "objc2-user-notifications",
-]
-
-[[package]]
-name = "objc2-uniform-type-identifiers"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
-dependencies = [
- "block2",
- "objc2",
- "objc2-foundation",
-]
-
-[[package]]
-name = "objc2-user-notifications"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
-dependencies = [
- "bitflags 2.9.0",
- "block2",
- "objc2",
- "objc2-core-location",
- "objc2-foundation",
-]
-
-[[package]]
-name = "objc_exception"
-version = "0.1.2"
+name = "object"
+version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
- "cc",
+ "memchr",
]
[[package]]
-name = "object"
-version = "0.36.7"
+name = "oco_ref"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+checksum = "64b94982fe39a861561cf67ff17a7849f2cedadbbad960a797634032b7abb998"
dependencies = [
- "memchr",
+ "serde",
+ "thiserror 1.0.69",
]
[[package]]
@@ -3353,13 +2004,13 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl"
-version = "0.10.72"
+version = "0.10.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
"cfg-if",
- "foreign-types 0.3.2",
+ "foreign-types",
"libc",
"once_cell",
"openssl-macros",
@@ -3374,7 +2025,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
@@ -3385,9 +2036,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
-version = "0.9.107"
+version = "0.9.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
dependencies = [
"cc",
"libc",
@@ -3396,77 +2047,19 @@ dependencies = [
]
[[package]]
-name = "option-ext"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
-
-[[package]]
-name = "orbclient"
-version = "0.3.48"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43"
-dependencies = [
- "libredox",
-]
-
-[[package]]
-name = "ordered-multimap"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a"
-dependencies = [
- "dlv-list",
- "hashbrown 0.12.3",
-]
-
-[[package]]
-name = "ordered-stream"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
-dependencies = [
- "futures-core",
- "pin-project-lite",
-]
-
-[[package]]
-name = "overload"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
-
-[[package]]
-name = "owned_ttf_parser"
-version = "0.25.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4"
-dependencies = [
- "ttf-parser 0.25.1",
-]
-
-[[package]]
-name = "palette"
-version = "0.7.6"
+name = "or_poisoned"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6"
-dependencies = [
- "approx",
- "fast-srgb8",
- "palette_derive",
- "phf",
-]
+checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd"
[[package]]
-name = "palette_derive"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30"
+name = "overlay-scrollbars"
+version = "0.1.0"
+source = "git+https://bunny.garden/forks/overlay-scrollbars#5ff8a16e3f411a253bdf5f157bacf48f3a7da93b"
dependencies = [
- "by_address",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
]
[[package]]
@@ -3477,48 +2070,23 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
-version = "0.11.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
-dependencies = [
- "instant",
- "lock_api",
- "parking_lot_core 0.8.6",
-]
-
-[[package]]
-name = "parking_lot"
-version = "0.12.3"
+version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [
"lock_api",
- "parking_lot_core 0.9.10",
+ "parking_lot_core",
]
[[package]]
name = "parking_lot_core"
-version = "0.8.6"
+version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
- "instant",
"libc",
- "redox_syscall 0.2.16",
- "smallvec",
- "winapi",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall 0.5.11",
+ "redox_syscall",
"smallvec",
"windows-targets 0.52.6",
]
@@ -3530,6 +2098,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3541,26 +2115,20 @@ dependencies = [
[[package]]
name = "peanuts"
version = "0.1.0"
-source = "git+https://bunny.garden/peanuts#92f4fd88295cb39bf865e10b0a28cc36acb5276a"
+source = "git+https://bunny.garden/peanuts#e1f4fbb65724fdfbce2068d3ee334eb4f60af37a"
dependencies = [
"async-recursion",
"circular",
"futures",
"futures-util",
+ "js-sys",
"nom",
"pin-project",
"thiserror 2.0.12",
"tokio",
"tracing",
-]
-
-[[package]]
-name = "pem-rfc7468"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
-dependencies = [
- "base64ct",
+ "wasm-bindgen",
+ "web-sys",
]
[[package]]
@@ -3570,54 +2138,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
-name = "phf"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
-dependencies = [
- "phf_macros",
- "phf_shared",
-]
-
-[[package]]
-name = "phf_generator"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
-dependencies = [
- "phf_shared",
- "rand",
-]
-
-[[package]]
-name = "phf_macros"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
-dependencies = [
- "phf_generator",
- "phf_shared",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "phf_shared"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
-dependencies = [
- "siphasher",
-]
-
-[[package]]
-name = "pico-args"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
-
-[[package]]
name = "pin-project"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3634,7 +2154,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
@@ -3650,38 +2170,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
-name = "piper"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
-dependencies = [
- "atomic-waker",
- "fastrand",
- "futures-io",
-]
-
-[[package]]
-name = "pkcs1"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
-dependencies = [
- "der",
- "pkcs8",
- "spki",
-]
-
-[[package]]
-name = "pkcs8"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
-dependencies = [
- "der",
- "spki",
-]
-
-[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3701,18 +2189,12 @@ dependencies = [
]
[[package]]
-name = "polling"
-version = "3.7.4"
+name = "potential_utf"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
dependencies = [
- "cfg-if",
- "concurrent-queue",
- "hermit-abi 0.4.0",
- "pin-project-lite",
- "rustix 0.38.44",
- "tracing",
- "windows-sys 0.59.0",
+ "zerovec",
]
[[package]]
@@ -3721,34 +2203,75 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
- "zerocopy 0.8.24",
+ "zerocopy",
]
[[package]]
-name = "presser"
-version = "0.3.1"
+name = "prettyplease"
+version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
+checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.102",
+]
[[package]]
-name = "proc-macro-crate"
-version = "3.3.0"
+name = "proc-macro-error-attr2"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
- "toml_edit",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
+]
+
+[[package]]
+name = "proc-macro-utils"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "smallvec",
]
[[package]]
name = "proc-macro2"
-version = "1.0.94"
+version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
name = "profiling"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3764,7 +2287,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
dependencies = [
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
@@ -3783,21 +2306,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
-name = "quick-xml"
-version = "0.37.4"
+name = "quote"
+version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
- "memchr",
+ "proc-macro2",
]
[[package]]
-name = "quote"
-version = "1.0.40"
+name = "quote-use"
+version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e"
+dependencies = [
+ "quote",
+ "quote-use-macros",
+]
+
+[[package]]
+name = "quote-use-macros"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35"
dependencies = [
+ "proc-macro-utils",
"proc-macro2",
+ "quote",
+ "syn 2.0.102",
]
[[package]]
@@ -3833,22 +2369,10 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
- "getrandom 0.2.15",
+ "getrandom 0.2.16",
]
[[package]]
-name = "range-alloc"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde"
-
-[[package]]
-name = "rangemap"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684"
-
-[[package]]
name = "rav1e"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3862,7 +2386,7 @@ dependencies = [
"built",
"cfg-if",
"interpolate_name",
- "itertools",
+ "itertools 0.12.1",
"libc",
"libfuzzer-sys",
"log",
@@ -3885,9 +2409,9 @@ dependencies = [
[[package]]
name = "ravif"
-version = "0.11.11"
+version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6"
+checksum = "d6a5f31fcf7500f9401fea858ea4ab5525c99f2322cfcee732c0e6c74208c0c6"
dependencies = [
"avif-serialize",
"imgref",
@@ -3899,12 +2423,6 @@ dependencies = [
]
[[package]]
-name = "raw-window-handle"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
-
-[[package]]
name = "rayon"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3925,129 +2443,106 @@ dependencies = [
]
[[package]]
-name = "read-fonts"
-version = "0.22.7"
+name = "reactive_graph"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69aacb76b5c29acfb7f90155d39759a29496aebb49395830e928a9703d2eec2f"
+checksum = "ac68cd988635779e6f378871257cbccfd51d7eeb7bc0bf6184835842aed51cc1"
dependencies = [
- "bytemuck",
- "font-types",
+ "any_spawner",
+ "async-lock",
+ "futures",
+ "guardian",
+ "hydration_context",
+ "or_poisoned",
+ "pin-project-lite",
+ "rustc-hash",
+ "rustc_version",
+ "send_wrapper",
+ "serde",
+ "slotmap",
+ "thiserror 2.0.12",
+ "web-sys",
]
[[package]]
-name = "redox_syscall"
-version = "0.2.16"
+name = "reactive_stores"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+checksum = "23e02f30b9cc6645e330e926dd778d4bcbd0e5770bdf4ec3d422dc0fe3c52a41"
dependencies = [
- "bitflags 1.3.2",
+ "dashmap",
+ "guardian",
+ "itertools 0.14.0",
+ "or_poisoned",
+ "paste",
+ "reactive_graph",
+ "reactive_stores_macro",
+ "rustc-hash",
+ "send_wrapper",
]
[[package]]
-name = "redox_syscall"
-version = "0.4.1"
+name = "reactive_stores_macro"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+checksum = "2f2bfb3b29c0b93d2d58a157b2a6783957bb592b296ab0b98a18fc3cdc574b07"
dependencies = [
- "bitflags 1.3.2",
+ "convert_case 0.8.0",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
]
[[package]]
name = "redox_syscall"
-version = "0.5.11"
+version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
+checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
]
[[package]]
-name = "redox_users"
-version = "0.4.6"
+name = "regex"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
- "getrandom 0.2.15",
- "libredox",
- "thiserror 1.0.69",
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
]
[[package]]
-name = "redox_users"
-version = "0.5.0"
+name = "regex-automata"
+version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
- "getrandom 0.2.15",
- "libredox",
- "thiserror 2.0.12",
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
]
[[package]]
-name = "renderdoc-sys"
-version = "1.1.0"
+name = "regex-syntax"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "resolv-conf"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48375394603e3dd4b2d64371f7148fd8c7baa2680e28741f2cb8d23b59e3d4c4"
-dependencies = [
- "hostname",
-]
-
-[[package]]
-name = "resvg"
-version = "0.42.0"
+version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051"
-dependencies = [
- "gif",
- "jpeg-decoder",
- "log",
- "pico-args",
- "rgb",
- "svgtypes",
- "tiny-skia",
- "usvg",
-]
+checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3"
[[package]]
name = "rgb"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
-dependencies = [
- "bytemuck",
-]
-
-[[package]]
-name = "roxmltree"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
-
-[[package]]
-name = "rsa"
-version = "0.9.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
-dependencies = [
- "const-oid",
- "digest",
- "num-bigint-dig",
- "num-integer",
- "num-traits",
- "pkcs1",
- "pkcs8",
- "rand_core",
- "signature",
- "spki",
- "subtle",
- "zeroize",
-]
[[package]]
name = "rsasl"
@@ -4068,26 +2563,41 @@ dependencies = [
]
[[package]]
-name = "rust-ini"
-version = "0.18.0"
+name = "rstml"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df"
+checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56"
dependencies = [
- "cfg-if",
- "ordered-multimap",
+ "derive-where",
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn 2.0.102",
+ "syn_derive",
+ "thiserror 2.0.12",
]
[[package]]
-name = "rustc-demangle"
-version = "0.1.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+name = "rusqlite"
+version = "0.34.0"
+source = "git+https://github.com/Spxg/rusqlite.git?branch=wasm-demo#4820de79e45d0604e0596641cbb5dd86aa53f0ed"
+dependencies = [
+ "bitflags 2.9.1",
+ "chrono",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "hashlink",
+ "libsqlite3-sys",
+ "smallvec",
+ "sqlite-wasm-rs",
+ "uuid",
+]
[[package]]
-name = "rustc-hash"
-version = "1.1.0"
+name = "rustc-demangle"
+version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
[[package]]
name = "rustc-hash"
@@ -4096,53 +2606,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
-name = "rustix"
-version = "0.38.44"
+name = "rustc_version"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
- "bitflags 2.9.0",
- "errno",
- "libc",
- "linux-raw-sys 0.4.15",
- "windows-sys 0.59.0",
+ "semver",
]
[[package]]
name = "rustix"
-version = "1.0.5"
+version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
"errno",
"libc",
- "linux-raw-sys 0.9.4",
+ "linux-raw-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "rustversion"
-version = "1.0.20"
+version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
-
-[[package]]
-name = "rustybuzz"
-version = "0.14.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c"
-dependencies = [
- "bitflags 2.9.0",
- "bytemuck",
- "libm",
- "smallvec",
- "ttf-parser 0.21.1",
- "unicode-bidi-mirroring",
- "unicode-ccc",
- "unicode-properties",
- "unicode-script",
-]
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "ryu"
@@ -4169,28 +2658,20 @@ dependencies = [
]
[[package]]
-name = "scoped-tls"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
-
-[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
-name = "sctk-adwaita"
-version = "0.10.1"
+name = "sealed"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec"
+checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107"
dependencies = [
- "ab_glyph",
- "log",
- "memmap2",
- "smithay-client-toolkit",
- "tiny-skia",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
]
[[package]]
@@ -4199,21 +2680,8 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
- "bitflags 2.9.0",
- "core-foundation 0.9.4",
- "core-foundation-sys",
- "libc",
- "security-framework-sys",
-]
-
-[[package]]
-name = "security-framework"
-version = "3.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
-dependencies = [
- "bitflags 2.9.0",
- "core-foundation 0.10.0",
+ "bitflags 2.9.1",
+ "core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
@@ -4230,10 +2698,19 @@ dependencies = [
]
[[package]]
-name = "self_cell"
-version = "1.1.0"
+name = "semver"
+version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+
+[[package]]
+name = "send_wrapper"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
+dependencies = [
+ "futures-core",
+]
[[package]]
name = "serde"
@@ -4252,7 +2729,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
@@ -4268,35 +2745,82 @@ dependencies = [
]
[[package]]
-name = "serde_repr"
-version = "0.1.20"
+name = "serde_qs"
+version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+checksum = "8b417bedc008acbdf6d6b4bc482d29859924114bbe2650b7921fb68a261d0aa6"
dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "percent-encoding",
+ "serde",
+ "thiserror 2.0.12",
]
[[package]]
name = "serde_spanned"
-version = "0.6.8"
+version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
+name = "server_fn"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+checksum = "09b0f92b9d3a62c73f238ac21f7a09f15bad335a9d1651514d9da80d2eaf8d4c"
dependencies = [
- "form_urlencoded",
- "itoa",
- "ryu",
+ "base64",
+ "bytes",
+ "const-str",
+ "const_format",
+ "dashmap",
+ "futures",
+ "gloo-net",
+ "http",
+ "js-sys",
+ "once_cell",
+ "pin-project-lite",
+ "rustc_version",
+ "rustversion",
+ "send_wrapper",
"serde",
+ "serde_json",
+ "serde_qs",
+ "server_fn_macro_default",
+ "thiserror 2.0.12",
+ "throw_error",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "server_fn_macro"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "341dd1087afe9f3e546c5979a4f0b6d55ac072e1201313f86e7fe364223835ac"
+dependencies = [
+ "const_format",
+ "convert_case 0.8.0",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.102",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "server_fn_macro_default"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc5ab934f581482a66da82f2b57b15390ad67c9ab85bd9a6c54bb65060fb1380"
+dependencies = [
+ "server_fn_macro",
+ "syn 2.0.102",
]
[[package]]
@@ -4312,9 +2836,9 @@ dependencies = [
[[package]]
name = "sha2"
-version = "0.10.8"
+version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
@@ -4347,25 +2871,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
-name = "signal-hook-registry"
-version = "1.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "signature"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
-dependencies = [
- "digest",
- "rand_core",
-]
-
-[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4381,31 +2886,12 @@ dependencies = [
]
[[package]]
-name = "simplecss"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
-dependencies = [
- "log",
-]
-
-[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
-name = "skrifa"
-version = "0.22.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe"
-dependencies = [
- "bytemuck",
- "read-fonts",
-]
-
-[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4425,321 +2911,37 @@ dependencies = [
[[package]]
name = "smallvec"
-version = "1.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "smithay-client-toolkit"
-version = "0.19.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
-dependencies = [
- "bitflags 2.9.0",
- "calloop",
- "calloop-wayland-source",
- "cursor-icon",
- "libc",
- "log",
- "memmap2",
- "rustix 0.38.44",
- "thiserror 1.0.69",
- "wayland-backend",
- "wayland-client",
- "wayland-csd-frame",
- "wayland-cursor",
- "wayland-protocols",
- "wayland-protocols-wlr",
- "wayland-scanner",
- "xkeysym",
-]
-
-[[package]]
-name = "smithay-clipboard"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846"
-dependencies = [
- "libc",
- "smithay-client-toolkit",
- "wayland-backend",
-]
-
-[[package]]
-name = "smol_str"
-version = "0.2.2"
+version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
-dependencies = [
- "serde",
-]
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
-version = "0.5.9"
+version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
-name = "softbuffer"
-version = "0.4.6"
+name = "sqlite-wasm-rs"
+version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
+checksum = "c3154cbfc5c60b307a5ce91ee19314571b22f4549628f3c1d731c24696dfa3af"
dependencies = [
- "as-raw-xcb-connection",
- "bytemuck",
- "cfg_aliases 0.2.1",
- "core-graphics 0.24.0",
- "drm",
- "fastrand",
- "foreign-types 0.5.0",
+ "fragile",
+ "indexed_db_futures",
"js-sys",
- "log",
- "memmap2",
- "objc2",
- "objc2-foundation",
- "objc2-quartz-core",
- "raw-window-handle",
- "redox_syscall 0.5.11",
- "rustix 0.38.44",
- "tiny-xlib",
- "wasm-bindgen",
- "wayland-backend",
- "wayland-client",
- "wayland-sys",
- "web-sys",
- "windows-sys 0.59.0",
- "x11rb",
-]
-
-[[package]]
-name = "spin"
-version = "0.9.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
-dependencies = [
- "lock_api",
-]
-
-[[package]]
-name = "spirv"
-version = "0.3.0+sdk-1.3.268.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
-dependencies = [
- "bitflags 2.9.0",
-]
-
-[[package]]
-name = "spki"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
-dependencies = [
- "base64ct",
- "der",
-]
-
-[[package]]
-name = "sqlx"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
-dependencies = [
- "sqlx-core",
- "sqlx-macros",
- "sqlx-mysql",
- "sqlx-postgres",
- "sqlx-sqlite",
-]
-
-[[package]]
-name = "sqlx-core"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
-dependencies = [
- "bytes",
- "chrono",
- "crc",
- "crossbeam-queue",
- "either",
- "event-listener",
- "futures-core",
- "futures-intrusive",
- "futures-io",
- "futures-util",
- "hashbrown 0.15.2",
- "hashlink",
- "indexmap",
- "log",
- "memchr",
"once_cell",
- "percent-encoding",
- "serde",
- "serde_json",
- "sha2",
- "smallvec",
+ "parking_lot",
"thiserror 2.0.12",
"tokio",
- "tokio-stream",
- "tracing",
- "url",
- "uuid",
-]
-
-[[package]]
-name = "sqlx-macros"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
-dependencies = [
- "proc-macro2",
- "quote",
- "sqlx-core",
- "sqlx-macros-core",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "sqlx-macros-core"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
-dependencies = [
- "dotenvy",
- "either",
- "heck 0.5.0",
- "hex",
- "once_cell",
- "proc-macro2",
- "quote",
- "serde",
- "serde_json",
- "sha2",
- "sqlx-core",
- "sqlx-mysql",
- "sqlx-postgres",
- "sqlx-sqlite",
- "syn 2.0.100",
- "tempfile",
- "tokio",
- "url",
-]
-
-[[package]]
-name = "sqlx-mysql"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
-dependencies = [
- "atoi",
- "base64",
- "bitflags 2.9.0",
- "byteorder",
- "bytes",
- "chrono",
- "crc",
- "digest",
- "dotenvy",
- "either",
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-util",
- "generic-array",
- "hex",
- "hkdf",
- "hmac",
- "itoa",
- "log",
- "md-5",
- "memchr",
- "once_cell",
- "percent-encoding",
- "rand",
- "rsa",
- "serde",
- "sha1",
- "sha2",
- "smallvec",
- "sqlx-core",
- "stringprep",
- "thiserror 2.0.12",
- "tracing",
- "uuid",
- "whoami",
-]
-
-[[package]]
-name = "sqlx-postgres"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
-dependencies = [
- "atoi",
- "base64",
- "bitflags 2.9.0",
- "byteorder",
- "chrono",
- "crc",
- "dotenvy",
- "etcetera",
- "futures-channel",
- "futures-core",
- "futures-util",
- "hex",
- "hkdf",
- "hmac",
- "home",
- "itoa",
- "log",
- "md-5",
- "memchr",
- "once_cell",
- "rand",
- "serde",
- "serde_json",
- "sha2",
- "smallvec",
- "sqlx-core",
- "stringprep",
- "thiserror 2.0.12",
- "tracing",
- "uuid",
- "whoami",
-]
-
-[[package]]
-name = "sqlx-sqlite"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
-dependencies = [
- "atoi",
- "chrono",
- "flume",
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-intrusive",
- "futures-util",
- "libsqlite3-sys",
- "log",
- "percent-encoding",
- "serde",
- "serde_urlencoded",
- "sqlx-core",
- "tracing",
- "url",
- "uuid",
+ "wasm-array-cp",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
]
[[package]]
@@ -4759,21 +2961,6 @@ dependencies = [
]
[[package]]
-name = "static_assertions"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
-
-[[package]]
-name = "strict-num"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
-dependencies = [
- "float-cmp",
-]
-
-[[package]]
name = "stringprep"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4785,39 +2972,47 @@ dependencies = [
]
[[package]]
-name = "subtle"
-version = "2.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
-
-[[package]]
-name = "svg_fmt"
-version = "0.4.4"
+name = "stylance"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce5d813d71d82c4cbc1742135004e4a79fd870214c155443451c139c9470a0aa"
+checksum = "cb98ec3b593f005791976cb36947af5bc64c1d8b5c37ae3a909076287f1be20e"
+dependencies = [
+ "stylance-macros",
+]
[[package]]
-name = "svgtypes"
-version = "0.15.3"
+name = "stylance-core"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc"
+checksum = "e4e6b833937b4a01d1f67db7a1dbd220d35271cee8f8581086467a25e16f7946"
dependencies = [
- "kurbo 0.11.1",
+ "anyhow",
+ "serde",
"siphasher",
+ "toml",
+ "winnow 0.5.40",
]
[[package]]
-name = "swash"
-version = "0.1.19"
+name = "stylance-macros"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbd59f3f359ddd2c95af4758c18270eddd9c730dde98598023cdabff472c2ca2"
+checksum = "138509d238319b5fdd75df17b9d3aab696c56bb2dbb9641cbf87647a68bb3361"
dependencies = [
- "skrifa",
- "yazi",
- "zeno",
+ "anyhow",
+ "proc-macro2",
+ "quote",
+ "stylance-core",
+ "syn 2.0.102",
]
[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4830,9 +3025,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.100"
+version = "2.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
+checksum = "f6397daf94fa90f058bd0fd88429dd9e5738999cca8d701813c80723add80462"
dependencies = [
"proc-macro2",
"quote",
@@ -4840,23 +3035,26 @@ dependencies = [
]
[[package]]
-name = "synstructure"
-version = "0.13.1"
+name = "syn_derive"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
+checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219"
dependencies = [
+ "proc-macro-error2",
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
-name = "sys-locale"
-version = "0.3.2"
+name = "synstructure"
+version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
- "libc",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
]
[[package]]
@@ -4873,10 +3071,39 @@ dependencies = [
]
[[package]]
-name = "take_mut"
-version = "0.2.2"
+name = "tachys"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
+checksum = "51a9a5d6436e532fd27b49bcca005a038bf510fc369687de830121a74811ccf4"
+dependencies = [
+ "any_spawner",
+ "async-trait",
+ "const_str_slice_concat",
+ "drain_filter_polyfill",
+ "either_of",
+ "erased",
+ "futures",
+ "html-escape",
+ "indexmap",
+ "itertools 0.14.0",
+ "js-sys",
+ "linear-map",
+ "next_tuple",
+ "oco_ref",
+ "once_cell",
+ "or_poisoned",
+ "parking_lot",
+ "paste",
+ "reactive_graph",
+ "reactive_stores",
+ "rustc-hash",
+ "rustc_version",
+ "send_wrapper",
+ "slotmap",
+ "throw_error",
+ "wasm-bindgen",
+ "web-sys",
+]
[[package]]
name = "target-lexicon"
@@ -4886,27 +3113,18 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tempfile"
-version = "3.19.1"
+version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
+checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [
"fastrand",
- "getrandom 0.3.2",
+ "getrandom 0.3.3",
"once_cell",
- "rustix 1.0.5",
+ "rustix",
"windows-sys 0.59.0",
]
[[package]]
-name = "termcolor"
-version = "1.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
-dependencies = [
- "winapi-util",
-]
-
-[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4932,7 +3150,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
@@ -4943,7 +3161,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
@@ -4957,6 +3175,15 @@ dependencies = [
]
[[package]]
+name = "throw_error"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41e42a6afdde94f3e656fae18f837cb9bbe500a5ac5de325b09f3ec05b9c28e3"
+dependencies = [
+ "pin-project-lite",
+]
+
+[[package]]
name = "tiff"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4968,49 +3195,10 @@ dependencies = [
]
[[package]]
-name = "tiny-skia"
-version = "0.11.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
-dependencies = [
- "arrayref",
- "arrayvec",
- "bytemuck",
- "cfg-if",
- "log",
- "png",
- "tiny-skia-path",
-]
-
-[[package]]
-name = "tiny-skia-path"
-version = "0.11.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
-dependencies = [
- "arrayref",
- "bytemuck",
- "strict-num",
-]
-
-[[package]]
-name = "tiny-xlib"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e"
-dependencies = [
- "as-raw-xcb-connection",
- "ctor-lite",
- "libloading 0.8.6",
- "pkg-config",
- "tracing",
-]
-
-[[package]]
name = "tinystr"
-version = "0.7.6"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
dependencies = [
"displaydoc",
"zerovec",
@@ -5033,17 +3221,15 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.44.2"
+version = "1.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
+checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
- "parking_lot 0.12.3",
"pin-project-lite",
- "signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
@@ -5057,7 +3243,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
@@ -5071,21 +3257,32 @@ dependencies = [
]
[[package]]
-name = "tokio-stream"
-version = "0.1.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+name = "tokio_with_wasm"
+version = "0.8.2"
dependencies = [
- "futures-core",
- "pin-project-lite",
+ "js-sys",
"tokio",
+ "tokio_with_wasm_proc",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "tokio_with_wasm_proc"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c77ab10ef5cedbe0955cc210ffa852567f9c4bcfd16fd8fb7b7870eef9b7dcec"
+dependencies = [
+ "quote",
+ "syn 2.0.102",
]
[[package]]
name = "toml"
-version = "0.8.20"
+version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
@@ -5095,33 +3292,39 @@ dependencies = [
[[package]]
name = "toml_datetime"
-version = "0.6.8"
+version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
-version = "0.22.24"
+version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
- "winnow",
+ "toml_write",
+ "winnow 0.7.11",
]
[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
- "log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@@ -5129,34 +3332,22 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.28"
+version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
name = "tracing-core"
-version = "0.1.33"
+version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
- "valuable",
-]
-
-[[package]]
-name = "tracing-log"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
-dependencies = [
- "log",
- "once_cell",
- "tracing-core",
]
[[package]]
@@ -5165,12 +3356,20 @@ version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
- "nu-ansi-term",
"sharded-slab",
- "smallvec",
"thread_local",
"tracing-core",
- "tracing-log",
+]
+
+[[package]]
+name = "tracing-wasm"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07"
+dependencies = [
+ "tracing",
+ "tracing-subscriber",
+ "wasm-bindgen",
]
[[package]]
@@ -5209,7 +3408,7 @@ dependencies = [
"ipconfig",
"lazy_static",
"lru-cache",
- "parking_lot 0.12.3",
+ "parking_lot",
"resolv-conf",
"smallvec",
"thiserror 1.0.69",
@@ -5219,28 +3418,24 @@ dependencies = [
]
[[package]]
-name = "try_map"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb1626d07cb5c1bb2cf17d94c0be4852e8a7c02b041acec9a8c5bdda99f9d580"
-
-[[package]]
-name = "ttf-parser"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4"
-
-[[package]]
-name = "ttf-parser"
-version = "0.21.1"
+name = "typed-builder"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
+checksum = "ce63bcaf7e9806c206f7d7b9c1f38e0dce8bb165a80af0898161058b19248534"
+dependencies = [
+ "typed-builder-macro",
+]
[[package]]
-name = "ttf-parser"
-version = "0.25.1"
+name = "typed-builder-macro"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
+checksum = "60d8d828da2a3d759d3519cdf29a5bac49c77d039ad36d0782edadbf9cd5415b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.102",
+]
[[package]]
name = "typenum"
@@ -5249,47 +3444,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
-name = "uds_windows"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
-dependencies = [
- "memoffset",
- "tempfile",
- "winapi",
-]
-
-[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
-name = "unicode-bidi-mirroring"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86"
-
-[[package]]
-name = "unicode-ccc"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656"
-
-[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
-name = "unicode-linebreak"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
-
-[[package]]
name = "unicode-normalization"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5305,30 +3471,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
[[package]]
-name = "unicode-script"
-version = "0.5.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f"
-
-[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
-name = "unicode-vo"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
-
-[[package]]
-name = "unicode-width"
-version = "0.1.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
-
-[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5346,37 +3494,10 @@ dependencies = [
]
[[package]]
-name = "usvg"
-version = "0.42.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032"
-dependencies = [
- "base64",
- "data-url",
- "flate2",
- "fontdb 0.18.0",
- "imagesize",
- "kurbo 0.11.1",
- "log",
- "pico-args",
- "roxmltree",
- "rustybuzz",
- "simplecss",
- "siphasher",
- "strict-num",
- "svgtypes",
- "tiny-skia-path",
- "unicode-bidi",
- "unicode-script",
- "unicode-vo",
- "xmlwriter",
-]
-
-[[package]]
-name = "utf16_iter"
-version = "1.0.5"
+name = "utf8-width"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
+checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
[[package]]
name = "utf8_iter"
@@ -5386,18 +3507,20 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
-version = "1.16.0"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
+checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
- "getrandom 0.3.2",
+ "getrandom 0.3.3",
+ "js-sys",
+ "wasm-bindgen",
]
[[package]]
name = "v_frame"
-version = "0.3.8"
+version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b"
+checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
dependencies = [
"aligned-vec",
"num-traits",
@@ -5405,12 +3528,6 @@ dependencies = [
]
[[package]]
-name = "valuable"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
-
-[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5440,9 +3557,9 @@ dependencies = [
[[package]]
name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
+version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
@@ -5454,10 +3571,14 @@ dependencies = [
]
[[package]]
-name = "wasite"
-version = "0.1.0"
+name = "wasm-array-cp"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+checksum = "bb633b3e235f0ebe0a35162adc1e0293fc4b7e3f3a6fc7b5374d80464267ff84"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
[[package]]
name = "wasm-bindgen"
@@ -5481,7 +3602,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
"wasm-bindgen-shared",
]
@@ -5516,7 +3637,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -5531,130 +3652,19 @@ dependencies = [
]
[[package]]
-name = "wasm-timer"
-version = "0.2.5"
+name = "wasm-streams"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
dependencies = [
- "futures",
+ "futures-util",
"js-sys",
- "parking_lot 0.11.2",
- "pin-utils",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
-name = "wayland-backend"
-version = "0.3.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf"
-dependencies = [
- "cc",
- "downcast-rs",
- "rustix 0.38.44",
- "scoped-tls",
- "smallvec",
- "wayland-sys",
-]
-
-[[package]]
-name = "wayland-client"
-version = "0.31.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f"
-dependencies = [
- "bitflags 2.9.0",
- "rustix 0.38.44",
- "wayland-backend",
- "wayland-scanner",
-]
-
-[[package]]
-name = "wayland-csd-frame"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
-dependencies = [
- "bitflags 2.9.0",
- "cursor-icon",
- "wayland-backend",
-]
-
-[[package]]
-name = "wayland-cursor"
-version = "0.31.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d"
-dependencies = [
- "rustix 0.38.44",
- "wayland-client",
- "xcursor",
-]
-
-[[package]]
-name = "wayland-protocols"
-version = "0.32.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc"
-dependencies = [
- "bitflags 2.9.0",
- "wayland-backend",
- "wayland-client",
- "wayland-scanner",
-]
-
-[[package]]
-name = "wayland-protocols-plasma"
-version = "0.3.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ccaacc76703fefd6763022ac565b590fcade92202492381c95b2edfdf7d46b3"
-dependencies = [
- "bitflags 2.9.0",
- "wayland-backend",
- "wayland-client",
- "wayland-protocols",
- "wayland-scanner",
-]
-
-[[package]]
-name = "wayland-protocols-wlr"
-version = "0.3.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2"
-dependencies = [
- "bitflags 2.9.0",
- "wayland-backend",
- "wayland-client",
- "wayland-protocols",
- "wayland-scanner",
-]
-
-[[package]]
-name = "wayland-scanner"
-version = "0.31.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484"
-dependencies = [
- "proc-macro2",
- "quick-xml",
- "quote",
-]
-
-[[package]]
-name = "wayland-sys"
-version = "0.31.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615"
-dependencies = [
- "dlib",
- "log",
- "once_cell",
- "pkg-config",
-]
-
-[[package]]
name = "web-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5665,137 +3675,10 @@ dependencies = [
]
[[package]]
-name = "web-time"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
name = "weezl"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
-
-[[package]]
-name = "wgpu"
-version = "0.19.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbd7311dbd2abcfebaabf1841a2824ed7c8be443a0f29166e5d3c6a53a762c01"
-dependencies = [
- "arrayvec",
- "cfg-if",
- "cfg_aliases 0.1.1",
- "js-sys",
- "log",
- "naga",
- "parking_lot 0.12.3",
- "profiling",
- "raw-window-handle",
- "smallvec",
- "static_assertions",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
- "wgpu-core",
- "wgpu-hal",
- "wgpu-types",
-]
-
-[[package]]
-name = "wgpu-core"
-version = "0.19.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a"
-dependencies = [
- "arrayvec",
- "bit-vec",
- "bitflags 2.9.0",
- "cfg_aliases 0.1.1",
- "codespan-reporting",
- "indexmap",
- "log",
- "naga",
- "once_cell",
- "parking_lot 0.12.3",
- "profiling",
- "raw-window-handle",
- "rustc-hash 1.1.0",
- "smallvec",
- "thiserror 1.0.69",
- "web-sys",
- "wgpu-hal",
- "wgpu-types",
-]
-
-[[package]]
-name = "wgpu-hal"
-version = "0.19.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfabcfc55fd86611a855816326b2d54c3b2fd7972c27ce414291562650552703"
-dependencies = [
- "android_system_properties",
- "arrayvec",
- "ash",
- "bit-set",
- "bitflags 2.9.0",
- "block",
- "cfg_aliases 0.1.1",
- "core-graphics-types 0.1.3",
- "d3d12",
- "glow",
- "glutin_wgl_sys",
- "gpu-alloc",
- "gpu-allocator",
- "gpu-descriptor",
- "hassle-rs",
- "js-sys",
- "khronos-egl",
- "libc",
- "libloading 0.8.6",
- "log",
- "metal",
- "naga",
- "ndk-sys 0.5.0+25.2.9519653",
- "objc",
- "once_cell",
- "parking_lot 0.12.3",
- "profiling",
- "range-alloc",
- "raw-window-handle",
- "renderdoc-sys",
- "rustc-hash 1.1.0",
- "smallvec",
- "thiserror 1.0.69",
- "wasm-bindgen",
- "web-sys",
- "wgpu-types",
- "winapi",
-]
-
-[[package]]
-name = "wgpu-types"
-version = "0.19.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805"
-dependencies = [
- "bitflags 2.9.0",
- "js-sys",
- "web-sys",
-]
-
-[[package]]
-name = "whoami"
-version = "1.6.0"
+version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7"
-dependencies = [
- "redox_syscall 0.5.11",
- "wasite",
-]
+checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3"
[[package]]
name = "widestring"
@@ -5804,22 +3687,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5829,49 +3696,10 @@ dependencies = [
]
[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
-[[package]]
-name = "window_clipboard"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6d692d46038c433f9daee7ad8757e002a4248c20b0a3fbc991d99521d3bcb6d"
-dependencies = [
- "clipboard-win",
- "clipboard_macos",
- "clipboard_wayland",
- "clipboard_x11",
- "raw-window-handle",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "windows"
-version = "0.52.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
-dependencies = [
- "windows-core 0.52.0",
- "windows-targets 0.52.6",
-]
-
-[[package]]
name = "windows-core"
-version = "0.52.0"
+version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
-dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-core"
-version = "0.61.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
@@ -5888,7 +3716,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
@@ -5899,44 +3727,35 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
name = "windows-link"
-version = "0.1.1"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
+checksum = "d3bfe459f85da17560875b8bf1423d6f113b7a87a5d942e7da0ac71be7c61f8b"
[[package]]
name = "windows-result"
-version = "0.3.2"
+version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
-version = "0.4.0"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
-version = "0.45.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
-dependencies = [
- "windows-targets 0.42.2",
-]
-
-[[package]]
-name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
@@ -5964,21 +3783,6 @@ dependencies = [
[[package]]
name = "windows-targets"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
-dependencies = [
- "windows_aarch64_gnullvm 0.42.2",
- "windows_aarch64_msvc 0.42.2",
- "windows_i686_gnu 0.42.2",
- "windows_i686_msvc 0.42.2",
- "windows_x86_64_gnu 0.42.2",
- "windows_x86_64_gnullvm 0.42.2",
- "windows_x86_64_msvc 0.42.2",
-]
-
-[[package]]
-name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
@@ -6010,12 +3814,6 @@ dependencies = [
[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
-
-[[package]]
-name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
@@ -6028,12 +3826,6 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
-
-[[package]]
-name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
@@ -6046,12 +3838,6 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
-
-[[package]]
-name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
@@ -6070,12 +3856,6 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
-
-[[package]]
-name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
@@ -6088,12 +3868,6 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
-
-[[package]]
-name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
@@ -6106,12 +3880,6 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
@@ -6124,12 +3892,6 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
-
-[[package]]
-name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
@@ -6141,73 +3903,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
-name = "winit"
-version = "0.30.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a809eacf18c8eca8b6635091543f02a5a06ddf3dad846398795460e6e0ae3cc0"
-dependencies = [
- "ahash 0.8.11",
- "android-activity",
- "atomic-waker",
- "bitflags 2.9.0",
- "block2",
- "bytemuck",
- "calloop",
- "cfg_aliases 0.2.1",
- "concurrent-queue",
- "core-foundation 0.9.4",
- "core-graphics 0.23.2",
- "cursor-icon",
- "dpi",
- "js-sys",
- "libc",
- "memmap2",
- "ndk",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
- "objc2-ui-kit",
- "orbclient",
- "percent-encoding",
- "pin-project",
- "raw-window-handle",
- "redox_syscall 0.4.1",
- "rustix 0.38.44",
- "sctk-adwaita",
- "smithay-client-toolkit",
- "smol_str",
- "tracing",
- "unicode-segmentation",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "wayland-backend",
- "wayland-client",
- "wayland-protocols",
- "wayland-protocols-plasma",
- "web-sys",
- "web-time",
- "windows-sys 0.52.0",
- "x11-dl",
- "x11rb",
- "xkbcommon-dl",
-]
-
-[[package]]
name = "winnow"
-version = "0.7.6"
+version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]]
-name = "winreg"
-version = "0.10.1"
+name = "winnow"
+version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
dependencies = [
- "winapi",
+ "memchr",
]
[[package]]
@@ -6226,111 +3936,32 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
]
[[package]]
-name = "write16"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
-
-[[package]]
name = "writeable"
-version = "0.5.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
-
-[[package]]
-name = "x11-dl"
-version = "2.21.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
-dependencies = [
- "libc",
- "once_cell",
- "pkg-config",
-]
-
-[[package]]
-name = "x11rb"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12"
-dependencies = [
- "as-raw-xcb-connection",
- "gethostname",
- "libc",
- "libloading 0.8.6",
- "once_cell",
- "rustix 0.38.44",
- "x11rb-protocol",
-]
-
-[[package]]
-name = "x11rb-protocol"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
-
-[[package]]
-name = "xcursor"
-version = "0.3.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61"
-
-[[package]]
-name = "xdg-home"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6"
-dependencies = [
- "libc",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "xkbcommon-dl"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
-dependencies = [
- "bitflags 2.9.0",
- "dlib",
- "log",
- "once_cell",
- "xkeysym",
-]
-
-[[package]]
-name = "xkeysym"
-version = "0.2.1"
+version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
-name = "xml-rs"
-version = "0.8.26"
+name = "xxhash-rust"
+version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda"
+checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
[[package]]
-name = "xmlwriter"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
-
-[[package]]
-name = "yazi"
-version = "0.1.6"
+name = "yansi"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yoke"
-version = "0.7.5"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
dependencies = [
"serde",
"stable_deref_trait",
@@ -6340,122 +3971,34 @@ dependencies = [
[[package]]
name = "yoke-derive"
-version = "0.7.5"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
"synstructure",
]
[[package]]
-name = "zbus"
-version = "4.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
-dependencies = [
- "async-broadcast",
- "async-executor",
- "async-fs",
- "async-io",
- "async-lock",
- "async-process",
- "async-recursion",
- "async-task",
- "async-trait",
- "blocking",
- "enumflags2",
- "event-listener",
- "futures-core",
- "futures-sink",
- "futures-util",
- "hex",
- "nix",
- "ordered-stream",
- "rand",
- "serde",
- "serde_repr",
- "sha1",
- "static_assertions",
- "tracing",
- "uds_windows",
- "windows-sys 0.52.0",
- "xdg-home",
- "zbus_macros",
- "zbus_names",
- "zvariant",
-]
-
-[[package]]
-name = "zbus_macros"
-version = "4.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
-dependencies = [
- "proc-macro-crate",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
- "zvariant_utils",
-]
-
-[[package]]
-name = "zbus_names"
-version = "3.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
-dependencies = [
- "serde",
- "static_assertions",
- "zvariant",
-]
-
-[[package]]
-name = "zeno"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
-
-[[package]]
name = "zerocopy"
-version = "0.7.35"
+version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
dependencies = [
- "zerocopy-derive 0.7.35",
-]
-
-[[package]]
-name = "zerocopy"
-version = "0.8.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879"
-dependencies = [
- "zerocopy-derive 0.8.24",
+ "zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.7.35"
+version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
-]
-
-[[package]]
-name = "zerocopy-derive"
-version = "0.8.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
@@ -6475,21 +4018,26 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
"synstructure",
]
[[package]]
-name = "zeroize"
-version = "1.8.1"
+name = "zerotrie"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
[[package]]
name = "zerovec"
-version = "0.10.4"
+version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
+checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
dependencies = [
"yoke",
"zerofrom",
@@ -6498,13 +4046,13 @@ dependencies = [
[[package]]
name = "zerovec-derive"
-version = "0.10.3"
+version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.100",
+ "syn 2.0.102",
]
[[package]]
@@ -6524,46 +4072,9 @@ dependencies = [
[[package]]
name = "zune-jpeg"
-version = "0.4.14"
+version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028"
+checksum = "0f6fe2e33d02a98ee64423802e16df3de99c43e5cf5ff983767e1128b394c8ac"
dependencies = [
"zune-core",
]
-
-[[package]]
-name = "zvariant"
-version = "4.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe"
-dependencies = [
- "endi",
- "enumflags2",
- "serde",
- "static_assertions",
- "zvariant_derive",
-]
-
-[[package]]
-name = "zvariant_derive"
-version = "4.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
-dependencies = [
- "proc-macro-crate",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
- "zvariant_utils",
-]
-
-[[package]]
-name = "zvariant_utils"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
-]
diff --git a/Cargo.lock.license b/Cargo.lock.license
new file mode 100644
index 0000000..e06abbf
--- /dev/null
+++ b/Cargo.lock.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+
+SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/Cargo.toml b/Cargo.toml
index 85f4378..86f202c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,26 +1,45 @@
-[workspace]
-resolver = "2"
+# SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
[package]
-name = "macaw"
+name = "macaw-web"
version = "0.1.0"
-edition = "2021"
+edition = "2024"
+
+# [lib]
+# crate-type = ["cdylib", "rlib"]
[dependencies]
-iced = { version = "0.13.0", features = ["tokio", "image", "svg"] }
-filamento = { version = "0.1.0", path = "../luz/filamento" }
-jid = { version = "0.1.0", path = "../luz/jid" }
-tokio = "1.43.0"
-tokio-stream = "0.1.17"
-tracing-subscriber = "0.3.19"
-tracing = "0.1.41"
-confy = "0.6.1"
-keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] }
-uuid = { version = "1.13.1", features = ["v4"] }
-indexmap = "2.7.1"
-serde = { version = "1.0.218", features = ["derive"] }
-thiserror = "2.0.11"
-toml = "0.8"
-dirs = "6.0.0"
+base64 = "0.22.1"
+chrono = "0.4.41"
chrono-humanize = "0.2.3"
-chrono = "0.4.40"
+console_error_panic_hook = "0.1.7"
+filamento = { path = "../luz/filamento", features = [
+ "reactive_stores",
+ "opfs",
+ # "serde"
+] }
+futures = "0.3.31"
+indexmap = "2.9.0"
+jid = { path = "../luz/jid" }
+leptos = { version = "0.8.2", features = ["csr"] }
+# leptos-use = "0.15.7"
+# leptos_meta = "0.8.2"
+# reactive_graph = "0.2.2"
+# leptos_reactive = { version = "0.6.15", features = ["csr"] }
+reactive_stores = "0.2.2"
+serde = "1.0.219"
+stylance = "0.6.0"
+thiserror = "2.0.12"
+tokio = { version = "1.44.2", features = ["sync", "rt"] }
+tracing = "0.1.41"
+tracing-wasm = "0.2.1"
+uuid = { version = "1.16.0", features = ["v4"] }
+# wasm-bindgen = "0.2.100"
+overlay-scrollbars = { git = "https://bunny.garden/forks/overlay-scrollbars" }
+web-sys = { version = "0.3.77", features = ["HtmlInputElement", "FileList", "Url"] }
+js-sys = { version = "0.3.77" }
+
+[patch.crates-io]
+tokio_with_wasm = { path = "../tokio-with-wasm/tokio_with_wasm" }
diff --git a/LICENSES/AGPL-3.0-or-later.txt b/LICENSES/AGPL-3.0-or-later.txt
new file mode 100644
index 0000000..0c97efd
--- /dev/null
+++ b/LICENSES/AGPL-3.0-or-later.txt
@@ -0,0 +1,235 @@
+GNU AFFERO GENERAL PUBLIC LICENSE
+Version 3, 19 November 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+
+ Preamble
+
+The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
+
+The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.
+
+When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
+
+Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
+
+The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
+
+An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
+
+The precise terms and conditions for copying, distribution and modification follow.
+
+ TERMS AND CONDITIONS
+
+0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based on the Program.
+
+To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
+
+1. Source Code.
+The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
+
+A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same work.
+
+2. Basic Permissions.
+All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
+
+3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
+
+When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
+
+4. Conveying Verbatim Copies.
+You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
+
+5. Conveying Modified Source Versions.
+You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
+
+6. Conveying Non-Source Forms.
+You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
+
+ d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
+
+The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
+
+7. Additional Terms.
+"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
+
+8. Termination.
+
+You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
+
+9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
+
+10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
+
+11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
+
+12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
+
+13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
+
+Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.
+
+14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
+
+Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
+
+15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements.
+
+You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see <http://www.gnu.org/licenses/>.
diff --git a/Trunk.toml b/Trunk.toml
new file mode 100644
index 0000000..f5b0b6a
--- /dev/null
+++ b/Trunk.toml
@@ -0,0 +1,67 @@
+# SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+[build]
+# The index HTML file to drive the bundling process.
+target = "index.html"
+# Build in release mode.
+release = false
+# Use a custom cargo profile. Overrides the default chosen by cargo. Ignored if the 'index.html' has one configured.
+# cargo_profile = ""
+# The output dir for all final assets.
+dist = "dist"
+# The public URL from which assets are to be served.
+public_url = "/"
+# Whether to include hash values in the output file names.
+filehash = true
+# Whether to inject scripts (and module preloads) into the finalized output.
+inject_scripts = true
+cargo_flags = ["-Z build-std=std,panic_abort"]
+# Run without network access
+# offline = false
+# Require Cargo.lock and cache are up to date
+# frozen = false
+# Require Cargo.lock is up to date
+# locked = false
+# Control minification
+# minify = "never" # can be one of: never, on_release, always
+# Allow disabling sub-resource integrity (SRI)
+# no_sri = false
+# An optional cargo profile to use
+# cargo_profile = "release-trunk"
+
+[watch]
+# Paths to watch. The `build.target`'s parent folder is watched by default.
+watch = []
+# Paths to ignore.
+ignore = []
+
+[serve]
+# The address to serve on.
+addresses = ["127.0.0.1"]
+# The port to serve on.
+port = 3000
+# Aliases to serve, typically found in an /etc/hosts file.
+# aliases = ["http://localhost.mywebsite.com"]
+# Disable the reverse DNS lookup during startup
+# disable_address_lookup = false
+# Open a browser tab once the initial build is complete.
+open = false
+headers = { "Cross-Origin-Embedder-Policy" = "require-corp", "Cross-Origin-Opener-Policy" = "same-origin" }
+
+# Whether to disable fallback to index.html for missing files.
+# no_spa = false
+# Disable auto-reload of the web app.
+# no_autoreload = false
+# Disable error reporting
+# no_error_reporting = false
+# Additional headers set for responses.
+# headers = { "test-header" = "header value", "test-header2" = "header value 2" }
+# Protocol used for autoreload WebSockets connection.
+# ws_protocol = "ws"
+# The certificate/private key pair to use for TLS, which is enabled if both are set.
+# tls_key_path = "self_signed_certs/key.pem"
+# tls_cert_path = "self_signed_certs/cert.pem"
+# Additional headers to send. NOTE: header names must be valid HTTP headers.
+# headers = { "X-Foo" = "bar" }
diff --git a/aaaa.md b/aaaa.md
deleted file mode 100644
index 699f9f3..0000000
--- a/aaaa.md
+++ /dev/null
@@ -1,11 +0,0 @@
-- each thing that references something holds a reference to it, guaranteeing that it is stored in the cache.
-- if all the references are dropped, it is removed from the cache.
-- if you query the cache with an id you should be able to get a reference.
-- but the issue is that the underlying data can change when the value in the cache changes. therefore, the reference must be a reference to a mutex.
- - so then use generational thing
-- the cache also needs to be in a mutex, because the cache must be modifiable through the drop function and the main logic.
- - the drop function doesn't have to be able to modify the cache, it can just let the program know it's time to delete something from the cache.
- - the generational thing means that it will be auto-dropped.
-
-
-arena that has a hashmap along with it. the hashmap has the id and the generation index. if the generation thing is fucked, then it falls back to the hashmap to change the index. changing the hashmap unfortunately requires mutex. the key is self referential to the macaw struct.
diff --git a/assets/bubble.png b/assets/bubble.png
new file mode 100644
index 0000000..003a141
--- /dev/null
+++ b/assets/bubble.png
Binary files differ
diff --git a/assets/caw.png b/assets/caw.png
new file mode 100644
index 0000000..0e26169
--- /dev/null
+++ b/assets/caw.png
Binary files differ
diff --git a/assets/fonts/Diolce-Regular.otf b/assets/fonts/Diolce-Regular.otf
deleted file mode 100644
index 562e749..0000000
--- a/assets/fonts/Diolce-Regular.otf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/Diolce-Regular.ttf b/assets/fonts/Diolce-Regular.ttf
deleted file mode 100644
index a7f0fd6..0000000
--- a/assets/fonts/Diolce-Regular.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/Diolce-Regular.woff2 b/assets/fonts/Diolce-Regular.woff2
new file mode 100644
index 0000000..e38e949
--- /dev/null
+++ b/assets/fonts/Diolce-Regular.woff2
Binary files differ
diff --git a/assets/fonts/K2D-Bold.ttf b/assets/fonts/K2D-Bold.ttf
deleted file mode 100644
index 6c07a9a..0000000
--- a/assets/fonts/K2D-Bold.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-BoldItalic.ttf b/assets/fonts/K2D-BoldItalic.ttf
deleted file mode 100644
index be68508..0000000
--- a/assets/fonts/K2D-BoldItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-ExtraBold.ttf b/assets/fonts/K2D-ExtraBold.ttf
deleted file mode 100644
index 1065611..0000000
--- a/assets/fonts/K2D-ExtraBold.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-ExtraBoldItalic.ttf b/assets/fonts/K2D-ExtraBoldItalic.ttf
deleted file mode 100644
index fbd4804..0000000
--- a/assets/fonts/K2D-ExtraBoldItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-ExtraLight.ttf b/assets/fonts/K2D-ExtraLight.ttf
deleted file mode 100644
index 21273c3..0000000
--- a/assets/fonts/K2D-ExtraLight.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-ExtraLightItalic.ttf b/assets/fonts/K2D-ExtraLightItalic.ttf
deleted file mode 100644
index 75a3ca3..0000000
--- a/assets/fonts/K2D-ExtraLightItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-Italic.ttf b/assets/fonts/K2D-Italic.ttf
deleted file mode 100644
index ae77b56..0000000
--- a/assets/fonts/K2D-Italic.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-Light.ttf b/assets/fonts/K2D-Light.ttf
deleted file mode 100644
index 48bf67d..0000000
--- a/assets/fonts/K2D-Light.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-LightItalic.ttf b/assets/fonts/K2D-LightItalic.ttf
deleted file mode 100644
index 8086fd7..0000000
--- a/assets/fonts/K2D-LightItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-Medium.ttf b/assets/fonts/K2D-Medium.ttf
deleted file mode 100644
index 667fefe..0000000
--- a/assets/fonts/K2D-Medium.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-MediumItalic.ttf b/assets/fonts/K2D-MediumItalic.ttf
deleted file mode 100644
index 177f863..0000000
--- a/assets/fonts/K2D-MediumItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-Regular.ttf b/assets/fonts/K2D-Regular.ttf
deleted file mode 100644
index dce2033..0000000
--- a/assets/fonts/K2D-Regular.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-SemiBold.ttf b/assets/fonts/K2D-SemiBold.ttf
deleted file mode 100644
index d448629..0000000
--- a/assets/fonts/K2D-SemiBold.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-SemiBoldItalic.ttf b/assets/fonts/K2D-SemiBoldItalic.ttf
deleted file mode 100644
index 8ec3713..0000000
--- a/assets/fonts/K2D-SemiBoldItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-Thin.ttf b/assets/fonts/K2D-Thin.ttf
deleted file mode 100644
index 4d68740..0000000
--- a/assets/fonts/K2D-Thin.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/K2D-ThinItalic.ttf b/assets/fonts/K2D-ThinItalic.ttf
deleted file mode 100644
index 64191d8..0000000
--- a/assets/fonts/K2D-ThinItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/assets/fonts/Millimetre-Regular.otf b/assets/fonts/Millimetre-Regular.otf
new file mode 100644
index 0000000..71f31b7
--- /dev/null
+++ b/assets/fonts/Millimetre-Regular.otf
Binary files differ
diff --git a/assets/fonts/k2d-1.woff2 b/assets/fonts/k2d-1.woff2
new file mode 100644
index 0000000..0762015
--- /dev/null
+++ b/assets/fonts/k2d-1.woff2
Binary files differ
diff --git a/assets/fonts/k2d-10.woff2 b/assets/fonts/k2d-10.woff2
new file mode 100644
index 0000000..715af48
--- /dev/null
+++ b/assets/fonts/k2d-10.woff2
Binary files differ
diff --git a/assets/fonts/k2d-11.woff2 b/assets/fonts/k2d-11.woff2
new file mode 100644
index 0000000..dc5570f
--- /dev/null
+++ b/assets/fonts/k2d-11.woff2
Binary files differ
diff --git a/assets/fonts/k2d-12.woff2 b/assets/fonts/k2d-12.woff2
new file mode 100644
index 0000000..a690e54
--- /dev/null
+++ b/assets/fonts/k2d-12.woff2
Binary files differ
diff --git a/assets/fonts/k2d-13.woff2 b/assets/fonts/k2d-13.woff2
new file mode 100644
index 0000000..1b52974
--- /dev/null
+++ b/assets/fonts/k2d-13.woff2
Binary files differ
diff --git a/assets/fonts/k2d-14.woff2 b/assets/fonts/k2d-14.woff2
new file mode 100644
index 0000000..4eb449b
--- /dev/null
+++ b/assets/fonts/k2d-14.woff2
Binary files differ
diff --git a/assets/fonts/k2d-15.woff2 b/assets/fonts/k2d-15.woff2
new file mode 100644
index 0000000..714a104
--- /dev/null
+++ b/assets/fonts/k2d-15.woff2
Binary files differ
diff --git a/assets/fonts/k2d-16.woff2 b/assets/fonts/k2d-16.woff2
new file mode 100644
index 0000000..14c3b72
--- /dev/null
+++ b/assets/fonts/k2d-16.woff2
Binary files differ
diff --git a/assets/fonts/k2d-17.woff2 b/assets/fonts/k2d-17.woff2
new file mode 100644
index 0000000..229e9a5
--- /dev/null
+++ b/assets/fonts/k2d-17.woff2
Binary files differ
diff --git a/assets/fonts/k2d-18.woff2 b/assets/fonts/k2d-18.woff2
new file mode 100644
index 0000000..3ddcb33
--- /dev/null
+++ b/assets/fonts/k2d-18.woff2
Binary files differ
diff --git a/assets/fonts/k2d-19.woff2 b/assets/fonts/k2d-19.woff2
new file mode 100644
index 0000000..945aeb8
--- /dev/null
+++ b/assets/fonts/k2d-19.woff2
Binary files differ
diff --git a/assets/fonts/k2d-2.woff2 b/assets/fonts/k2d-2.woff2
new file mode 100644
index 0000000..530d179
--- /dev/null
+++ b/assets/fonts/k2d-2.woff2
Binary files differ
diff --git a/assets/fonts/k2d-20.woff2 b/assets/fonts/k2d-20.woff2
new file mode 100644
index 0000000..16602ee
--- /dev/null
+++ b/assets/fonts/k2d-20.woff2
Binary files differ
diff --git a/assets/fonts/k2d-21.woff2 b/assets/fonts/k2d-21.woff2
new file mode 100644
index 0000000..117a373
--- /dev/null
+++ b/assets/fonts/k2d-21.woff2
Binary files differ
diff --git a/assets/fonts/k2d-22.woff2 b/assets/fonts/k2d-22.woff2
new file mode 100644
index 0000000..1375154
--- /dev/null
+++ b/assets/fonts/k2d-22.woff2
Binary files differ
diff --git a/assets/fonts/k2d-23.woff2 b/assets/fonts/k2d-23.woff2
new file mode 100644
index 0000000..4b803e3
--- /dev/null
+++ b/assets/fonts/k2d-23.woff2
Binary files differ
diff --git a/assets/fonts/k2d-24.woff2 b/assets/fonts/k2d-24.woff2
new file mode 100644
index 0000000..67922b6
--- /dev/null
+++ b/assets/fonts/k2d-24.woff2
Binary files differ
diff --git a/assets/fonts/k2d-25.woff2 b/assets/fonts/k2d-25.woff2
new file mode 100644
index 0000000..4a3f885
--- /dev/null
+++ b/assets/fonts/k2d-25.woff2
Binary files differ
diff --git a/assets/fonts/k2d-26.woff2 b/assets/fonts/k2d-26.woff2
new file mode 100644
index 0000000..14354c8
--- /dev/null
+++ b/assets/fonts/k2d-26.woff2
Binary files differ
diff --git a/assets/fonts/k2d-27.woff2 b/assets/fonts/k2d-27.woff2
new file mode 100644
index 0000000..516392f
--- /dev/null
+++ b/assets/fonts/k2d-27.woff2
Binary files differ
diff --git a/assets/fonts/k2d-28.woff2 b/assets/fonts/k2d-28.woff2
new file mode 100644
index 0000000..5ae329f
--- /dev/null
+++ b/assets/fonts/k2d-28.woff2
Binary files differ
diff --git a/assets/fonts/k2d-29.woff2 b/assets/fonts/k2d-29.woff2
new file mode 100644
index 0000000..4d013f6
--- /dev/null
+++ b/assets/fonts/k2d-29.woff2
Binary files differ
diff --git a/assets/fonts/k2d-3.woff2 b/assets/fonts/k2d-3.woff2
new file mode 100644
index 0000000..6ac5214
--- /dev/null
+++ b/assets/fonts/k2d-3.woff2
Binary files differ
diff --git a/assets/fonts/k2d-30.woff2 b/assets/fonts/k2d-30.woff2
new file mode 100644
index 0000000..5d2f74b
--- /dev/null
+++ b/assets/fonts/k2d-30.woff2
Binary files differ
diff --git a/assets/fonts/k2d-31.woff2 b/assets/fonts/k2d-31.woff2
new file mode 100644
index 0000000..400134e
--- /dev/null
+++ b/assets/fonts/k2d-31.woff2
Binary files differ
diff --git a/assets/fonts/k2d-32.woff2 b/assets/fonts/k2d-32.woff2
new file mode 100644
index 0000000..635281e
--- /dev/null
+++ b/assets/fonts/k2d-32.woff2
Binary files differ
diff --git a/assets/fonts/k2d-33.woff2 b/assets/fonts/k2d-33.woff2
new file mode 100644
index 0000000..a4d4a59
--- /dev/null
+++ b/assets/fonts/k2d-33.woff2
Binary files differ
diff --git a/assets/fonts/k2d-34.woff2 b/assets/fonts/k2d-34.woff2
new file mode 100644
index 0000000..eb8c874
--- /dev/null
+++ b/assets/fonts/k2d-34.woff2
Binary files differ
diff --git a/assets/fonts/k2d-35.woff2 b/assets/fonts/k2d-35.woff2
new file mode 100644
index 0000000..e294b67
--- /dev/null
+++ b/assets/fonts/k2d-35.woff2
Binary files differ
diff --git a/assets/fonts/k2d-36.woff2 b/assets/fonts/k2d-36.woff2
new file mode 100644
index 0000000..da5fb33
--- /dev/null
+++ b/assets/fonts/k2d-36.woff2
Binary files differ
diff --git a/assets/fonts/k2d-37.woff2 b/assets/fonts/k2d-37.woff2
new file mode 100644
index 0000000..072e115
--- /dev/null
+++ b/assets/fonts/k2d-37.woff2
Binary files differ
diff --git a/assets/fonts/k2d-38.woff2 b/assets/fonts/k2d-38.woff2
new file mode 100644
index 0000000..85de65e
--- /dev/null
+++ b/assets/fonts/k2d-38.woff2
Binary files differ
diff --git a/assets/fonts/k2d-39.woff2 b/assets/fonts/k2d-39.woff2
new file mode 100644
index 0000000..3c216ca
--- /dev/null
+++ b/assets/fonts/k2d-39.woff2
Binary files differ
diff --git a/assets/fonts/k2d-4.woff2 b/assets/fonts/k2d-4.woff2
new file mode 100644
index 0000000..149837d
--- /dev/null
+++ b/assets/fonts/k2d-4.woff2
Binary files differ
diff --git a/assets/fonts/k2d-40.woff2 b/assets/fonts/k2d-40.woff2
new file mode 100644
index 0000000..2a50843
--- /dev/null
+++ b/assets/fonts/k2d-40.woff2
Binary files differ
diff --git a/assets/fonts/k2d-41.woff2 b/assets/fonts/k2d-41.woff2
new file mode 100644
index 0000000..1db6313
--- /dev/null
+++ b/assets/fonts/k2d-41.woff2
Binary files differ
diff --git a/assets/fonts/k2d-42.woff2 b/assets/fonts/k2d-42.woff2
new file mode 100644
index 0000000..365ab35
--- /dev/null
+++ b/assets/fonts/k2d-42.woff2
Binary files differ
diff --git a/assets/fonts/k2d-43.woff2 b/assets/fonts/k2d-43.woff2
new file mode 100644
index 0000000..49de7b4
--- /dev/null
+++ b/assets/fonts/k2d-43.woff2
Binary files differ
diff --git a/assets/fonts/k2d-44.woff2 b/assets/fonts/k2d-44.woff2
new file mode 100644
index 0000000..586639d
--- /dev/null
+++ b/assets/fonts/k2d-44.woff2
Binary files differ
diff --git a/assets/fonts/k2d-45.woff2 b/assets/fonts/k2d-45.woff2
new file mode 100644
index 0000000..9b0e17d
--- /dev/null
+++ b/assets/fonts/k2d-45.woff2
Binary files differ
diff --git a/assets/fonts/k2d-46.woff2 b/assets/fonts/k2d-46.woff2
new file mode 100644
index 0000000..a1ff3d3
--- /dev/null
+++ b/assets/fonts/k2d-46.woff2
Binary files differ
diff --git a/assets/fonts/k2d-47.woff2 b/assets/fonts/k2d-47.woff2
new file mode 100644
index 0000000..60c7aab
--- /dev/null
+++ b/assets/fonts/k2d-47.woff2
Binary files differ
diff --git a/assets/fonts/k2d-48.woff2 b/assets/fonts/k2d-48.woff2
new file mode 100644
index 0000000..5c7db90
--- /dev/null
+++ b/assets/fonts/k2d-48.woff2
Binary files differ
diff --git a/assets/fonts/k2d-49.woff2 b/assets/fonts/k2d-49.woff2
new file mode 100644
index 0000000..52fa1f3
--- /dev/null
+++ b/assets/fonts/k2d-49.woff2
Binary files differ
diff --git a/assets/fonts/k2d-5.woff2 b/assets/fonts/k2d-5.woff2
new file mode 100644
index 0000000..fa98a3f
--- /dev/null
+++ b/assets/fonts/k2d-5.woff2
Binary files differ
diff --git a/assets/fonts/k2d-50.woff2 b/assets/fonts/k2d-50.woff2
new file mode 100644
index 0000000..25167d2
--- /dev/null
+++ b/assets/fonts/k2d-50.woff2
Binary files differ
diff --git a/assets/fonts/k2d-51.woff2 b/assets/fonts/k2d-51.woff2
new file mode 100644
index 0000000..08c1e97
--- /dev/null
+++ b/assets/fonts/k2d-51.woff2
Binary files differ
diff --git a/assets/fonts/k2d-52.woff2 b/assets/fonts/k2d-52.woff2
new file mode 100644
index 0000000..a137c1e
--- /dev/null
+++ b/assets/fonts/k2d-52.woff2
Binary files differ
diff --git a/assets/fonts/k2d-53.woff2 b/assets/fonts/k2d-53.woff2
new file mode 100644
index 0000000..da5174f
--- /dev/null
+++ b/assets/fonts/k2d-53.woff2
Binary files differ
diff --git a/assets/fonts/k2d-54.woff2 b/assets/fonts/k2d-54.woff2
new file mode 100644
index 0000000..dda2732
--- /dev/null
+++ b/assets/fonts/k2d-54.woff2
Binary files differ
diff --git a/assets/fonts/k2d-55.woff2 b/assets/fonts/k2d-55.woff2
new file mode 100644
index 0000000..d942dbe
--- /dev/null
+++ b/assets/fonts/k2d-55.woff2
Binary files differ
diff --git a/assets/fonts/k2d-56.woff2 b/assets/fonts/k2d-56.woff2
new file mode 100644
index 0000000..95cf728
--- /dev/null
+++ b/assets/fonts/k2d-56.woff2
Binary files differ
diff --git a/assets/fonts/k2d-57.woff2 b/assets/fonts/k2d-57.woff2
new file mode 100644
index 0000000..cbd1b64
--- /dev/null
+++ b/assets/fonts/k2d-57.woff2
Binary files differ
diff --git a/assets/fonts/k2d-58.woff2 b/assets/fonts/k2d-58.woff2
new file mode 100644
index 0000000..bf9a06d
--- /dev/null
+++ b/assets/fonts/k2d-58.woff2
Binary files differ
diff --git a/assets/fonts/k2d-59.woff2 b/assets/fonts/k2d-59.woff2
new file mode 100644
index 0000000..77b4fec
--- /dev/null
+++ b/assets/fonts/k2d-59.woff2
Binary files differ
diff --git a/assets/fonts/k2d-6.woff2 b/assets/fonts/k2d-6.woff2
new file mode 100644
index 0000000..224fe51
--- /dev/null
+++ b/assets/fonts/k2d-6.woff2
Binary files differ
diff --git a/assets/fonts/k2d-60.woff2 b/assets/fonts/k2d-60.woff2
new file mode 100644
index 0000000..97924a5
--- /dev/null
+++ b/assets/fonts/k2d-60.woff2
Binary files differ
diff --git a/assets/fonts/k2d-61.woff2 b/assets/fonts/k2d-61.woff2
new file mode 100644
index 0000000..231ce99
--- /dev/null
+++ b/assets/fonts/k2d-61.woff2
Binary files differ
diff --git a/assets/fonts/k2d-62.woff2 b/assets/fonts/k2d-62.woff2
new file mode 100644
index 0000000..651d107
--- /dev/null
+++ b/assets/fonts/k2d-62.woff2
Binary files differ
diff --git a/assets/fonts/k2d-63.woff2 b/assets/fonts/k2d-63.woff2
new file mode 100644
index 0000000..f1761bb
--- /dev/null
+++ b/assets/fonts/k2d-63.woff2
Binary files differ
diff --git a/assets/fonts/k2d-64.woff2 b/assets/fonts/k2d-64.woff2
new file mode 100644
index 0000000..6b8b201
--- /dev/null
+++ b/assets/fonts/k2d-64.woff2
Binary files differ
diff --git a/assets/fonts/k2d-7.woff2 b/assets/fonts/k2d-7.woff2
new file mode 100644
index 0000000..c3ed121
--- /dev/null
+++ b/assets/fonts/k2d-7.woff2
Binary files differ
diff --git a/assets/fonts/k2d-8.woff2 b/assets/fonts/k2d-8.woff2
new file mode 100644
index 0000000..edf39af
--- /dev/null
+++ b/assets/fonts/k2d-8.woff2
Binary files differ
diff --git a/assets/fonts/k2d-9.woff2 b/assets/fonts/k2d-9.woff2
new file mode 100644
index 0000000..fe8117b
--- /dev/null
+++ b/assets/fonts/k2d-9.woff2
Binary files differ
diff --git a/assets/icon.png b/assets/icon.png
new file mode 100644
index 0000000..18536a8
--- /dev/null
+++ b/assets/icon.png
Binary files differ
diff --git a/assets/icons/available16color.svg b/assets/icons/available16color.svg
new file mode 100644
index 0000000..e54c2f7
--- /dev/null
+++ b/assets/icons/available16color.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="8" cy="8" r="6" fill="#87EE23" stroke="#66BF10" stroke-width="2"/>
+</svg>
diff --git a/assets/icons/away16color.svg b/assets/icons/away16color.svg
index e35da57..8803c51 100644
--- a/assets/icons/away16color.svg
+++ b/assets/icons/away16color.svg
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_204_583)">
-<path d="M12.3949 3.68876C14.4905 5.78437 14.3985 9.52573 11.8703 12.0539C9.3422 14.582 5.60084 14.674 3.50523 12.5784C2.7029 11.7761 2.22277 10.747 2.07449 9.63325C2.10824 9.63849 2.14189 9.64327 2.17541 9.64762C2.83698 9.73336 3.54898 9.66529 4.21793 9.51478C5.52363 9.22102 6.93329 8.5485 7.73555 7.67748C8.50721 6.83967 9.11302 5.50754 9.35486 4.24412C9.47772 3.60222 9.51846 2.91991 9.40702 2.28141C9.4053 2.27156 9.40354 2.2617 9.40175 2.25184C10.5339 2.39262 11.5812 2.87506 12.3949 3.68876Z" fill="#FFCE07" stroke="black" stroke-width="2"/>
+<path d="M3.50535 12.5786C2.70294 11.7762 2.22417 10.7466 2.07594 9.63274C2.10924 9.6379 2.1423 9.64365 2.17538 9.64793C2.83692 9.73367 3.54906 9.66584 4.21798 9.51535C5.52368 9.22158 6.9333 8.54817 7.73556 7.67715C8.50707 6.83941 9.11299 5.50773 9.35486 4.2445C9.47773 3.60261 9.51808 2.91981 9.40665 2.28132C9.40504 2.27208 9.40281 2.26294 9.40112 2.25369C10.5335 2.39442 11.5815 2.87482 12.3953 3.68862C14.4907 5.78422 14.3984 9.52565 11.8705 12.0538C9.34237 14.5819 5.60098 14.674 3.50535 12.5786Z" fill="#FFCE07" stroke="#D0A700" stroke-width="2"/>
</g>
<defs>
<clipPath id="clip0_204_583">
diff --git a/assets/icons/bubble24.svg b/assets/icons/bubble24.svg
index b9e9414..19decc2 100644
--- a/assets/icons/bubble24.svg
+++ b/assets/icons/bubble24.svg
@@ -1,3 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13.1661 18.7474L12.6411 17.1101C13.0131 17.0644 13.376 16.9992 13.7259 16.9192C15.3587 16.5462 16.8089 15.8307 17.7144 15.1369C20.8619 12.7253 21.8527 8.91393 19.7675 6.19249C18.5016 4.54019 16.4353 3.89603 14.2909 4.02335C12.1395 4.15109 9.79196 5.04995 7.71792 6.63908C5.64387 8.2282 4.16516 10.261 3.48204 12.305C2.80113 14.3425 2.88548 16.5052 4.15146 18.1575C5.3258 19.6902 8.44768 21.1066 13.1661 18.7474Z" stroke="black" stroke-width="2" stroke-miterlimit="1.41421"/>
+<g clip-path="url(#clip0_218_10)">
+<path d="M3.39687 10.1647C3.96179 8.56349 5.17184 6.84 7.48298 5.27824L7.95995 4.96762C9.13922 4.22996 11.2635 3.42663 13.4985 3.29304C15.5828 3.16849 17.6272 3.62867 19.1355 5.10846L19.4299 5.41748C21.0253 7.21643 21.253 9.34884 20.5361 11.3095C19.8528 13.1777 18.3016 14.8937 16.2258 15.9073L15.8035 16.1003C13.2542 17.1912 11.1923 17.1182 9.42282 17.1818L8.35117 17.2206L8.46464 18.2874C8.49948 18.6139 8.64191 18.9983 8.79409 19.3362C8.95987 19.7043 9.1863 20.124 9.46151 20.5462C9.49056 20.5908 9.52181 20.635 9.55236 20.6802C8.51755 20.3814 7.5274 19.8863 6.64017 19.245C5.1113 18.1398 3.95211 16.6527 3.40236 15.1427L3.30015 14.8405C2.89914 13.5433 2.79033 11.884 3.39687 10.1647Z" stroke="black" stroke-width="2"/>
+</g>
+<defs>
+<clipPath id="clip0_218_10">
+<rect width="24" height="24" fill="white"/>
+</clipPath>
+</defs>
</svg>
diff --git a/assets/icons/chat16color.svg b/assets/icons/chat16color.svg
new file mode 100644
index 0000000..d4a2479
--- /dev/null
+++ b/assets/icons/chat16color.svg
@@ -0,0 +1,10 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_214_198)">
+<path d="M2.32171 6.75399C2.68825 5.71497 3.48076 4.5717 5.03364 3.5225L5.3547 3.31455C6.12876 2.83032 7.54957 2.29008 9.04407 2.20066C10.4342 2.11758 11.7523 2.42625 12.7153 3.37165L12.9029 3.56876C13.9254 4.72163 14.072 6.07691 13.6105 7.33885C13.1668 8.55207 12.1506 9.68103 10.7797 10.3505L10.5014 10.4785C8.79928 11.2069 7.43339 11.1579 6.19304 11.2025L5.12138 11.2413L5.23485 12.3081C5.2644 12.5851 5.38057 12.8882 5.48855 13.128C5.52044 13.1988 5.5553 13.2722 5.59296 13.3471C5.19706 13.1688 4.81599 12.9476 4.45863 12.6893C3.44189 11.9544 2.68078 10.9727 2.32417 9.99369L2.25848 9.79823C1.99751 8.95405 1.92678 7.87353 2.32171 6.75399Z" fill="#87EE23" stroke="#66BF10" stroke-width="2"/>
+</g>
+<defs>
+<clipPath id="clip0_214_198">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/assets/icons/close24.svg b/assets/icons/close24.svg
new file mode 100644
index 0000000..d7e82ba
--- /dev/null
+++ b/assets/icons/close24.svg
@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3 3L21 21" stroke="black" stroke-width="2"/>
+<path d="M3 21L21 3" stroke="black" stroke-width="2"/>
+</svg>
diff --git a/assets/icons/dnd16color.svg b/assets/icons/dnd16color.svg
index e69cbe3..8e18fae 100644
--- a/assets/icons/dnd16color.svg
+++ b/assets/icons/dnd16color.svg
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6 14L2 10V6L6 2H10L14 6V10L10 14H6Z" fill="#C1173C" stroke="black" stroke-width="2"/>
+<path d="M6 14L2 10V6L6 2H10L14 6V10L10 14H6Z" fill="#C1173C" stroke="#8B0C28" stroke-width="2"/>
</svg>
diff --git a/assets/icons/newbubble24.svg b/assets/icons/newbubble24.svg
index b1d72fd..5a43f46 100644
--- a/assets/icons/newbubble24.svg
+++ b/assets/icons/newbubble24.svg
@@ -1,4 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13.1661 18.7474L12.6411 17.1101C13.0131 17.0644 13.376 16.9992 13.7259 16.9192C15.3587 16.5462 16.8089 15.8307 17.7144 15.1369C20.8619 12.7253 21.8527 8.91393 19.7675 6.19249C18.5016 4.54019 16.4353 3.89603 14.2909 4.02335C12.1395 4.15109 9.79196 5.04995 7.71792 6.63908C5.64387 8.2282 4.16516 10.261 3.48204 12.305C2.80113 14.3425 2.88548 16.5052 4.15146 18.1575C5.3258 19.6902 8.44768 21.1066 13.1661 18.7474Z" stroke="black" stroke-width="2" stroke-miterlimit="1.41421"/>
-<path d="M12 8.05864V14.0586M15 11.0586H9" stroke="black" stroke-width="2"/>
+<path d="M7.42972 4.12007C10.0635 2.47254 16.5712 0.68783 20.1778 4.75386C23.0466 7.98855 22.1245 12.2329 19.2481 15H19V12.1699C19.2458 11.7798 19.4469 11.3763 19.5967 10.9668C20.1624 9.42006 20.0308 7.79602 18.918 6.36519L18.6817 6.08101C17.4247 4.66391 15.5976 4.16921 13.5586 4.29097C11.4955 4.41429 9.53189 5.16479 8.49026 5.81636C6.03172 7.35447 4.85726 9.03081 4.33987 10.497C3.84746 11.8928 3.89933 13.2434 4.19339 14.331L4.25589 14.5449C4.68025 15.9173 5.7417 17.3611 7.22659 18.4345C7.31876 18.5011 7.41171 18.567 7.50589 18.6298C7.49082 18.5514 7.47911 18.472 7.47073 18.3935L7.24222 16.2597L9.38675 16.1826C10.2341 16.1521 11.0902 16.1483 12 16.0537V18.0664C11.0537 18.1544 10.2051 18.1548 9.45901 18.1816C9.52823 18.8267 10.6974 21.119 12.2637 21.9834C7.92689 22.2587 3.75854 19.0513 2.46097 15.4824L2.34476 15.1357C1.43441 12.1908 1.85165 7.60958 7.42972 4.12007Z" fill="black"/>
+<path d="M17 13V17M17 17V21M17 17H21M17 17H13" stroke="black" stroke-width="2"/>
</svg>
diff --git a/assets/icons/xa16color.svg b/assets/icons/xa16color.svg
new file mode 100644
index 0000000..1e3643e
--- /dev/null
+++ b/assets/icons/xa16color.svg
@@ -0,0 +1,10 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_214_392)">
+<path d="M3.50535 12.5786C2.70294 11.7762 2.22417 10.7466 2.07594 9.63274C2.10924 9.6379 2.1423 9.64365 2.17538 9.64793C2.83692 9.73367 3.54906 9.66584 4.21798 9.51535C5.52368 9.22158 6.9333 8.54817 7.73556 7.67715C8.50707 6.83941 9.11299 5.50773 9.35486 4.2445C9.47773 3.60261 9.51808 2.91981 9.40665 2.28132C9.40504 2.27208 9.40281 2.26294 9.40112 2.25369C10.5335 2.39442 11.5815 2.87482 12.3953 3.68862C14.4907 5.78422 14.3984 9.52565 11.8705 12.0538C9.34237 14.5819 5.60098 14.674 3.50535 12.5786Z" fill="#F99E36" stroke="#D06F00" stroke-width="2"/>
+</g>
+<defs>
+<clipPath id="clip0_214_392">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/assets/lovely.svg b/assets/lovely.svg
new file mode 100644
index 0000000..789b98d
--- /dev/null
+++ b/assets/lovely.svg
@@ -0,0 +1,3 @@
+<svg width="444" height="165" viewBox="0 0 444 165" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.5 91.3333C40.181 77.7175 56.2407 66.921 89.7778 45C96.4349 40.6487 119.631 25.0708 125.333 14.1111C125.944 12.9373 123.922 11.0434 122.611 11.2222C115.382 12.208 102.897 23.9096 99.4444 27.2222C89.0964 37.1514 81.8461 46.1667 74.2222 58.2778C71.762 62.1861 57.4043 84.0505 63.5556 91C67.4795 95.4331 82.8889 88.0805 83.3333 87.8889C99.4875 80.9249 114.556 71.4351 129.722 62.6111C134.628 59.7566 128.427 64.1201 126.667 65.7778C122.525 69.6778 117.699 74.248 115.5 79.7222C113.861 83.8017 113.906 87.3252 119.222 87.6667C127.813 88.2183 137.823 81.3961 143.222 75.3333C147.246 70.8147 148.348 64.0202 142.556 60.6667C138.782 58.4819 143.208 61.9764 144.778 62.6667C161.165 69.8724 182.305 79.2827 200 82.5C202.907 83.0286 188.841 72.8929 188 72C187.01 70.9486 179.218 66.7409 179 64.5C178.924 63.7222 196 54.2467 199.5 52.3333C212 45.5 219.492 40.8 234 38.5C237.702 37.9131 247 38.5 247.833 42.7222C249.028 48.7761 243.554 51.1204 240 53.5C235.5 56 231.38 56.758 225.556 59.2222C219.574 61.7529 223.169 61.263 228.167 62.5555C235.779 64.5243 258.608 72.2347 251.222 84.6667C248.815 88.7191 221.183 98.2441 220.722 91.3333C220.394 86.4044 233.492 81.3894 236.333 80C259.726 68.5615 281.069 56.1032 300.944 38.9444C312.002 29.3986 325.559 17.7447 331.833 3.99999C332.526 2.4822 332.807 1.85607 331 2.88888C324.285 6.72592 318.224 13.6692 313.556 19.5555C300.664 35.8096 289.999 55.3187 286 75.8333C283.206 90.1703 295.684 84.7776 304.667 80.6667C317.201 74.9301 327.055 65.9382 338.778 59.2222C340.109 58.4593 338.62 62.33 338.111 63.7778C336.445 68.517 329.745 82.2511 339.389 84.2222C355.578 87.5309 374.583 70.5925 386.667 62C387.864 61.1487 385.025 64.4363 384.222 65.6667C378.957 73.7399 373.851 81.8891 369.389 90.4444C360.746 107.017 354.469 124.681 345.778 141.222C343.489 145.578 336.876 160.448 330.667 162C329.349 162.329 330 158.109 331.5 155.5C343 135.5 354.064 118.488 373 106.5C391.936 94.5115 408 83.5 442 72" stroke="black" stroke-width="4" stroke-linecap="round"/>
+</svg>
diff --git a/assets/macaw-icon.png b/assets/macaw-icon.png
index 18536a8..b352ac0 100644
--- a/assets/macaw-icon.png
+++ b/assets/macaw-icon.png
Binary files differ
diff --git a/assets/no-avatar.png b/assets/no-avatar.png
new file mode 100644
index 0000000..953b263
--- /dev/null
+++ b/assets/no-avatar.png
Binary files differ
diff --git a/assets/overlayscrollbars.browser.es6.min.js b/assets/overlayscrollbars.browser.es6.min.js
new file mode 100644
index 0000000..7f44ae6
--- /dev/null
+++ b/assets/overlayscrollbars.browser.es6.min.js
@@ -0,0 +1,10 @@
+/*!
+ * OverlayScrollbars
+ * Version: 2.11.0
+ *
+ * Copyright (c) Rene Haas | KingSora.
+ * https://github.com/KingSora
+ *
+ * Released under the MIT license.
+ */
+var OverlayScrollbarsGlobal=function(t){"use strict";const e=(t,e)=>{const{o:n,i:r,u:o}=t;let s,i=n;const l=(t,e)=>{const n=i,l=t,c=e||(r?!r(n,l):n!==l);return(c||o)&&(i=l,s=n),[i,c,s]};return[e?t=>l(e(i,s),t):l,t=>[i,!!t,s]]},n="undefined"!=typeof window&&"undefined"!=typeof HTMLElement&&window.document?window:{},r=Math.max,o=Math.min,s=Math.round,i=Math.abs,l=Math.sign,c=n.cancelAnimationFrame,a=n.requestAnimationFrame,u=n.setTimeout,d=n.clearTimeout,p=t=>void 0!==n[t]?n[t]:void 0,y=p("MutationObserver"),h=p("IntersectionObserver"),f=p("ResizeObserver"),v=p("ScrollTimeline"),x=t=>void 0===t,b=t=>null===t,g=t=>"number"==typeof t,w=t=>"string"==typeof t,m=t=>"boolean"==typeof t,$=t=>"function"==typeof t,S=t=>Array.isArray(t),O=t=>"object"==typeof t&&!S(t)&&!b(t),M=t=>{const e=!!t&&t.length,n=g(e)&&e>-1&&e%1==0;return!(!(S(t)||!$(t)&&n)||e>0&&O(t)&&!(e-1 in t))},C=t=>!!t&&t.constructor===Object,D=t=>t instanceof HTMLElement,T=t=>t instanceof Element,k=()=>performance.now(),P=(t,e,n,o,s)=>{let i=0;const l=k(),u=r(0,n),d=n=>{const c=k(),p=c-l>=u,y=n?1:1-(r(0,l+u-c)/u||0),h=(e-t)*($(s)?s(y,y*u,0,1,u):y)+t,f=p||1===y;o&&o(h,y,f),i=f?0:a((()=>d()))};return d(),t=>{c(i),t&&d(t)}};function H(t,e){if(M(t))for(let n=0;n<t.length&&!1!==e(t[n],n,t);n++);else t&&H(Object.keys(t),(n=>e(t[n],n,t)));return t}const A=(t,e)=>t.indexOf(e)>=0,L=(t,e)=>t.concat(e),E=(t,e,n)=>(!w(e)&&M(e)?Array.prototype.push.apply(t,e):t.push(e),t),R=t=>Array.from(t||[]),U=t=>S(t)?t:!w(t)&&M(t)?R(t):[t],z=t=>!!t&&!t.length,I=t=>R(new Set(t)),_=(t,e,n)=>{H(t,(t=>!t||t.apply(void 0,e||[]))),!n&&(t.length=0)},j="paddingTop",N="paddingRight",V="paddingLeft",B="paddingBottom",F="marginLeft",Z="marginRight",W="marginBottom",q="overflowX",Y="overflowY",X="width",G="height",J="visible",K="hidden",Q="scroll",tt=(t,e,n,r)=>{if(t&&e){let r=!0;return H(n,(n=>{t[n]!==e[n]&&(r=!1)})),r}return!1},et=(t,e)=>tt(t,e,["w","h"]),nt=(t,e)=>tt(t,e,["x","y"]),rt=(t,e)=>tt(t,e,["t","r","b","l"]),ot=()=>{},st=(t,...e)=>t.bind(0,...e),it=t=>{let e;const n=t?u:a,r=t?d:c;return[o=>{r(e),e=n((()=>o()),$(t)?t():t)},()=>r(e)]},lt=(t,e)=>{const{_:n,v:r,p:o,S:s}=e||{};let i,l,p,y,h=ot;const f=function(e){h(),d(i),y=i=l=void 0,h=ot,t.apply(this,e)},v=t=>s&&l?s(l,t):t,x=()=>{h!==ot&&f(v(p)||p)},b=function(){const t=R(arguments),e=$(n)?n():n;if(g(e)&&e>=0){const n=$(r)?r():r,s=g(n)&&n>=0,b=e>0?u:a,w=e>0?d:c,m=v(t)||t,S=f.bind(0,m);let O;h(),o&&!y?(S(),y=!0,O=b((()=>y=void 0),e)):(O=b(S,e),s&&!i&&(i=u(x,n))),h=()=>w(O),l=p=m}else f(t)};return b.m=x,b},ct=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),at=t=>t?Object.keys(t):[],ut=(t,e,n,r,o,s,i)=>{const l=[e,n,r,o,s,i];return"object"==typeof t&&!b(t)||$(t)||(t={}),H(l,(e=>{H(e,((n,r)=>{const o=e[r];if(t===o)return!0;const s=S(o);if(o&&C(o)){const e=t[r];let n=e;s&&!S(e)?n=[]:s||C(e)||(n={}),t[r]=ut(n,o)}else t[r]=s?o.slice():o}))})),t},dt=(t,e)=>H(ut({},t),((t,e,n)=>{void 0===t?delete n[e]:t&&C(t)&&(n[e]=dt(t))})),pt=t=>!at(t).length,yt=(t,e,n)=>r(t,o(e,n)),ht=t=>I((S(t)?t:(t||"").split(" ")).filter((t=>t))),ft=(t,e)=>t&&t.getAttribute(e),vt=(t,e)=>t&&t.hasAttribute(e),xt=(t,e,n)=>{H(ht(e),(e=>{t&&t.setAttribute(e,String(n||""))}))},bt=(t,e)=>{H(ht(e),(e=>t&&t.removeAttribute(e)))},gt=(t,e)=>{const n=ht(ft(t,e)),r=st(xt,t,e),o=(t,e)=>{const r=new Set(n);return H(ht(t),(t=>{r[e](t)})),R(r).join(" ")};return{O:t=>r(o(t,"delete")),$:t=>r(o(t,"add")),C:t=>{const e=ht(t);return e.reduce(((t,e)=>t&&n.includes(e)),e.length>0)}}},wt=(t,e,n)=>(gt(t,e).O(n),st(mt,t,e,n)),mt=(t,e,n)=>(gt(t,e).$(n),st(wt,t,e,n)),$t=(t,e,n,r)=>(r?mt:wt)(t,e,n),St=(t,e,n)=>gt(t,e).C(n),Ot=t=>gt(t,"class"),Mt=(t,e)=>{Ot(t).O(e)},Ct=(t,e)=>(Ot(t).$(e),st(Mt,t,e)),Dt=(t,e)=>{const n=e?T(e)&&e:document;return n?R(n.querySelectorAll(t)):[]},Tt=(t,e)=>T(t)&&t.matches(e),kt=t=>Tt(t,"body"),Pt=t=>t?R(t.childNodes):[],Ht=t=>t&&t.parentElement,At=(t,e)=>T(t)&&t.closest(e),Lt=t=>document.activeElement,Et=t=>{H(U(t),(t=>{const e=Ht(t);t&&e&&e.removeChild(t)}))},Rt=(t,e)=>st(Et,t&&e&&H(U(e),(e=>{e&&t.appendChild(e)})));let Ut;const zt=t=>{const e=document.createElement("div");return xt(e,"class",t),e},It=t=>{const e=zt(),n=Ut,r=t.trim();return e.innerHTML=n?n.createHTML(r):r,H(Pt(e),(t=>Et(t)))},_t=(t,e)=>t.getPropertyValue(e)||t[e]||"",jt=t=>{const e=t||0;return isFinite(e)?e:0},Nt=t=>jt(parseFloat(t||"")),Vt=t=>Math.round(1e4*t)/1e4,Bt=t=>`${Vt(jt(t))}px`;function Ft(t,e){t&&e&&H(e,((e,n)=>{try{const r=t.style,o=b(e)||m(e)?"":g(e)?Bt(e):e;0===n.indexOf("--")?r.setProperty(n,o):r[n]=o}catch(r){}}))}function Zt(t,e,r){const o=w(e);let s=o?"":{};if(t){const i=n.getComputedStyle(t,r)||t.style;s=o?_t(i,e):R(e).reduce(((t,e)=>(t[e]=_t(i,e),t)),s)}return s}const Wt=(t,e,n)=>{const r=e?`${e}-`:"",o=n?`-${n}`:"",s=`${r}top${o}`,i=`${r}right${o}`,l=`${r}bottom${o}`,c=`${r}left${o}`,a=Zt(t,[s,i,l,c]);return{t:Nt(a[s]),r:Nt(a[i]),b:Nt(a[l]),l:Nt(a[c])}},qt=(t,e)=>`translate${O(t)?`(${t.x},${t.y})`:`${e?"X":"Y"}(${t})`}`,Yt={w:0,h:0},Xt=(t,e)=>e?{w:e[`${t}Width`],h:e[`${t}Height`]}:Yt,Gt=t=>Xt("inner",t||n),Jt=st(Xt,"offset"),Kt=st(Xt,"client"),Qt=st(Xt,"scroll"),te=t=>{const e=parseFloat(Zt(t,X))||0,n=parseFloat(Zt(t,G))||0;return{w:e-s(e),h:n-s(n)}},ee=t=>t.getBoundingClientRect(),ne=t=>!(!t||!t[G]&&!t[X]),re=(t,e)=>{const n=ne(t);return!ne(e)&&n},oe=(t,e,n,r)=>{H(ht(e),(e=>{t&&t.removeEventListener(e,n,r)}))},se=(t,e,n,r)=>{var o;const s=null==(o=r&&r.H)||o,i=r&&r.I||!1,l=r&&r.A||!1,c={passive:s,capture:i};return st(_,ht(e).map((e=>{const r=l?o=>{oe(t,e,r,i),n&&n(o)}:n;return t&&t.addEventListener(e,r,c),st(oe,t,e,r,i)})))},ie=t=>t.stopPropagation(),le=t=>t.preventDefault(),ce=t=>ie(t)||le(t),ae=(t,e)=>{const{x:n,y:r}=g(e)?{x:e,y:e}:e||{};g(n)&&(t.scrollLeft=n),g(r)&&(t.scrollTop=r)},ue=t=>({x:t.scrollLeft,y:t.scrollTop}),de=(t,e)=>{const{D:n,M:r}=t,{w:o,h:s}=e,c=(t,e,n)=>{let r=l(t)*n,o=l(e)*n;if(r===o){const n=i(t),s=i(e);o=n>s?0:o,r=n<s?0:r}return r=r===o?0:r,[r+0,o+0]},[a,u]=c(n.x,r.x,o),[d,p]=c(n.y,r.y,s);return{D:{x:a,y:d},M:{x:u,y:p}}},pe=({D:t,M:e})=>{const n=(t,e)=>0===t&&t<=e;return{x:n(t.x,e.x),y:n(t.y,e.y)}},ye=({D:t,M:e},n)=>{const r=(t,e,n)=>yt(0,1,(t-n)/(t-e)||0);return{x:r(t.x,e.x,n.x),y:r(t.y,e.y,n.y)}},he=t=>{t&&t.focus&&t.focus({preventScroll:!0})},fe=(t,e)=>{H(U(e),t)},ve=t=>{const e=new Map,n=(t,n)=>{if(t){const r=e.get(t);fe((t=>{r&&r[t?"delete":"clear"](t)}),n)}else e.forEach((t=>{t.clear()})),e.clear()},r=(t,o)=>{if(w(t)){const r=e.get(t)||new Set;return e.set(t,r),fe((t=>{$(t)&&r.add(t)}),o),st(n,t,o)}m(o)&&o&&n();const s=at(t),i=[];return H(s,(e=>{const n=t[e];n&&E(i,r(e,n))})),st(_,i)};return r(t||{}),[r,n,(t,n)=>{H(R(e.get(t)),(t=>{n&&!z(n)?t.apply(0,n):t()}))}]},xe={},be={},ge=(t,e,n)=>at(t).map((r=>{const{static:o,instance:s}=t[r],[i,l,c]=n||[],a=n?s:o;if(a){const t=n?a(i,l,e):a(e);return(c||be)[r]=t}})),we=t=>be[t],me="__osOptionsValidationPlugin",$e=`data-overlayscrollbars`,Se="os-environment",Oe=`${Se}-scrollbar-hidden`,Me=`${$e}-initialize`,Ce="noClipping",De=`${$e}-body`,Te=$e,ke="host",Pe=`${$e}-viewport`,He=q,Ae=Y,Le="arrange",Ee="measuring",Re="scrolling",Ue="scrollbarHidden",ze="noContent",Ie=`${$e}-padding`,_e=`${$e}-content`,je="os-size-observer",Ne=`${je}-appear`,Ve=`${je}-listener`,Be=`${Ve}-scroll`,Fe=`${Ve}-item`,Ze=`${Fe}-final`,We="os-trinsic-observer",qe="os-theme-none",Ye="os-scrollbar",Xe=`${Ye}-rtl`,Ge=`${Ye}-horizontal`,Je=`${Ye}-vertical`,Ke=`${Ye}-track`,Qe=`${Ye}-handle`,tn=`${Ye}-visible`,en=`${Ye}-cornerless`,nn=`${Ye}-interaction`,rn=`${Ye}-unusable`,on=`${Ye}-auto-hide`,sn=`${on}-hidden`,ln=`${Ye}-wheel`,cn=`${Ke}-interactive`,an=`${Qe}-interactive`,un="__osSizeObserverPlugin",dn=(()=>({[un]:{static:()=>(t,e,n)=>{const r=3333333,o="scroll",s=It(`<div class="${Fe}" dir="ltr"><div class="${Fe}"><div class="${Ze}"></div></div><div class="${Fe}"><div class="${Ze}" style="width: 200%; height: 200%"></div></div></div>`),i=s[0],l=i.lastChild,u=i.firstChild,d=null==u?void 0:u.firstChild;let p,y=Jt(i),h=y,f=!1;const v=()=>{ae(u,r),ae(l,r)},x=t=>{p=0,f&&(y=h,e(!0===t))},b=t=>{h=Jt(i),f=!t||!et(h,y),t?(ie(t),f&&!p&&(c(p),p=a(x))):x(!1===t),v()},g=[Rt(t,s),se(u,o,b),se(l,o,b)];return Ct(t,Be),Ft(d,{[X]:r,[G]:r}),a(v),[n?st(b,!1):v,g]}}}))(),pn=(t,e)=>{const{T:n}=e,[r,o]=t("showNativeOverlaidScrollbars");return[r&&n.x&&n.y,o]},yn=t=>0===t.indexOf(J),hn=(t,e)=>{const n=(t,e,n,r)=>{const o=t===J?K:t.replace(`${J}-`,""),s=yn(t),i=yn(n);return e||r?s&&i?J:s?e&&r?o:e?J:K:e?o:i&&r?J:K:K},r={x:n(e.x,t.x,e.y,t.y),y:n(e.y,t.y,e.x,t.x)};return{k:r,R:{x:r.x===Q,y:r.y===Q}}},fn="__osScrollbarsHidingPlugin",vn=(()=>({[fn]:{static:()=>({V:(t,e,n,r,o)=>{const{L:s,U:i}=t,{P:l,T:c,N:a}=r,u=!s&&!l&&(c.x||c.y),[d]=pn(o,r),p=t=>{const{R:e}=t,n=l||d?0:42,r=(t,e,r)=>[e&&!l?t?n:r:0,t&&!!n],[o,s]=r(c.x,e.x,a.x),[i,u]=r(c.y,e.y,a.y);return{q:{x:o,y:i},j:{x:s,y:u}}},y=(t,{B:n},r)=>{if(!s){const o=ut({},{[Z]:0,[W]:0,[F]:0}),{q:s,j:i}=p(t),{x:l,y:c}=i,{x:a,y:u}=s,{F:d}=e,y=n?F:Z,h=n?V:N,f=d[y],v=d[W],x=d[h],b=d[B];return o[X]=`calc(100% + ${u+-1*f}px)`,o[y]=-u+f,o[W]=-a+v,r&&(o[h]=x+(c?u:0),o[B]=b+(l?a:0)),o}};return{X:p,Y:(t,r,o)=>{if(u){const{F:s}=e,{q:l,j:c}=p(t),{x:a,y:u}=c,{x:d,y}=l,{B:h}=n,f=s[h?N:V],v=s.paddingTop,x=r.w+o.w,b=r.h+o.h,g={w:y&&u?`${y+x-f}px`:"",h:d&&a?`${d+b-v}px`:""};Ft(i,{"--os-vaw":g.w,"--os-vah":g.h})}return u},W:t=>{if(u){const r=t||(()=>{const t=t=>{const e=Zt(i,t);return[e,e===Q]},[e,n]=t(q),[r,o]=t(Y);return{k:{x:e,y:r},R:{x:n,y:o}}})(),{F:o}=e,{j:s}=p(r),{x:l,y:c}=s,a={},d=t=>H(t,(t=>{a[t]=o[t]}));l&&d([W,j,B]),c&&d([F,Z,V,N]);const h=Zt(i,at(a)),f=wt(i,Pe,Le);return Ft(i,a),[()=>{Ft(i,ut({},h,y(r,n,u))),f()},r]}return[ot]},G:y}}})}}))(),xn="__osClickScrollPlugin",bn=(()=>({[xn]:{static:()=>(t,e,n,r)=>{let o=!1,s=ot;const i=133,l=222,[c,a]=it(i),u=Math.sign(e),d=n*u,p=d/2,y=t=>1-(1-t)*(1-t),h=(e,n)=>P(e,n,l,t,y),f=(n,r)=>P(n,e-d,i*r,((n,r,o)=>{t(n),o&&(s=h(n,e))})),v=P(0,d,l,((i,l,a)=>{if(t(i),a&&(r(o),!o)){const t=e-i;Math.sign(t-p)===u&&c((()=>{const r=t-d,o=Math.sign(r)===u;s=o?f(i,Math.abs(r)/n):h(i,e)}))}}),y);return t=>{o=!0,t&&v(),a(),s()}}}}))(),gn=t=>JSON.stringify(t,((t,e)=>{if($(e))throw 0;return e})),wn=(t,e)=>t?`${e}`.split(".").reduce(((t,e)=>t&&ct(t,e)?t[e]:void 0),t):void 0,mn={paddingAbsolute:!1,showNativeOverlaidScrollbars:!1,update:{elementEvents:[["img","load"]],debounce:[0,33],attributes:null,ignoreMutation:null},overflow:{x:"scroll",y:"scroll"},scrollbars:{theme:"os-theme-dark",visibility:"auto",autoHide:"never",autoHideDelay:1300,autoHideSuspend:!1,dragScroll:!0,clickScroll:!1,pointers:["mouse","touch","pen"]}},$n=(t,e)=>{const n={};return H(L(at(e),at(t)),(r=>{const o=t[r],s=e[r];if(O(o)&&O(s))ut(n[r]={},$n(o,s)),pt(n[r])&&delete n[r];else if(ct(e,r)&&s!==o){let t=!0;if(S(o)||S(s))try{gn(o)===gn(s)&&(t=!1)}catch(i){}t&&(n[r]=s)}})),n},Sn=(t,e,n)=>r=>[wn(t,r),n||void 0!==wn(e,r)];let On,Mn;const Cn=()=>(Mn||(Mn=(()=>{const t=(t,e,n)=>{Rt(document.body,t),Rt(document.body,t);const r=Kt(t),o=Jt(t),s=te(e);return n&&Et(t),{x:o.h-r.h+s.h,y:o.w-r.w+s.w}},r=It(`<div class="${Se}"><div></div><style>${`.${Se}{scroll-behavior:auto!important;position:fixed;opacity:0;visibility:hidden;overflow:scroll;height:200px;width:200px;z-index:-1}.${Se} div{width:200%;height:200%;margin:10px 0}.${Oe}{scrollbar-width:none!important}.${Oe}::-webkit-scrollbar,.${Oe}::-webkit-scrollbar-corner{appearance:none!important;display:none!important;width:0!important;height:0!important}`}</style></div>`)[0],o=r.firstChild,s=r.lastChild,i=On;i&&(s.nonce=i);const[l,,c]=ve(),[a,u]=e({o:t(r,o),i:nt},st(t,r,o,!0)),[d]=u(),p=(t=>{let e=!1;const n=Ct(t,Oe);try{e="none"===Zt(t,"scrollbar-width")||"none"===Zt(t,"display","::-webkit-scrollbar")}catch(r){}return n(),e})(r),y={x:0===d.x,y:0===d.y},h={elements:{host:null,padding:!p,viewport:t=>p&&kt(t)&&t,content:!1},scrollbars:{slot:!0},cancel:{nativeScrollbarsOverlaid:!1,body:null}},f=ut({},mn),x=st(ut,{},f),b=st(ut,{},h),g={N:d,T:y,P:p,J:!!v,K:st(l,"r"),Z:b,tt:t=>ut(h,t)&&b(),nt:x,ot:t=>ut(f,t)&&x(),st:ut({},h),et:ut({},f)};if(bt(r,"style"),Et(r),se(n,"resize",(()=>{c("r",[])})),$(n.matchMedia)&&!p&&(!y.x||!y.y)){const t=e=>{const r=n.matchMedia(`(resolution: ${n.devicePixelRatio}dppx)`);se(r,"change",(()=>{e(),t(e)}),{A:!0})};t((()=>{const[t,e]=a();ut(g.N,t),c("r",[e])}))}return g})()),Mn),Dn=(t,e,n,r)=>{let o=!1;const{ct:s,rt:i,lt:l,it:c,ut:a,_t:u}=r||{},d=lt((()=>o&&n(!0)),{_:33,v:99}),[p,h]=((t,e,n)=>{let r=!1;const o=!!n&&new WeakMap,s=s=>{if(o&&n){const i=n.map((e=>{const[n,r]=e||[];return[r&&n?(s||Dt)(n,t):[],r]}));H(i,(n=>H(n[0],(s=>{const i=n[1],l=o.get(s)||[];if(t.contains(s)&&i){const t=se(s,i,(n=>{r?(t(),o.delete(s)):e(n)}));o.set(s,E(l,t))}else _(l),o.delete(s)}))))}};return s(),[()=>{r=!0},s]})(t,d,l),f=i||[],v=L(s||[],f),x=(o,s)=>{if(!z(s)){const i=a||ot,l=u||ot,d=[],p=[];let y=!1,v=!1;if(H(s,(n=>{const{attributeName:o,target:s,type:a,oldValue:u,addedNodes:h,removedNodes:x}=n,b="attributes"===a,g="childList"===a,m=t===s,$=b&&o,S=$&&ft(s,o||""),O=w(S)?S:null,M=$&&u!==O,C=A(f,o)&&M;if(e&&(g||!m)){const e=b&&M,a=e&&c&&Tt(s,c),p=(a?!i(s,o,u,O):!b||e)&&!l(n,!!a,t,r);H(h,(t=>E(d,t))),H(x,(t=>E(d,t))),v=v||p}!e&&m&&M&&!i(s,o,u,O)&&(E(p,o),y=y||C)})),h((t=>I(d).reduce(((e,n)=>(E(e,Dt(t,n)),Tt(n,t)?E(e,n):e)),[]))),e)return!o&&v&&n(!1),[!1];if(!z(p)||y){const t=[I(p),y];return!o&&n.apply(0,t),t}}},b=new y(st(x,!1));return[()=>(b.observe(t,{attributes:!0,attributeOldValue:!0,attributeFilter:v,subtree:e,childList:e,characterData:e}),o=!0,()=>{o&&(p(),b.disconnect(),o=!1)}),()=>{if(o)return d.m(),x(!0,b.takeRecords())}]},Tn=(t,n,r)=>{const{dt:o}=r||{},s=we(un),[i]=e({o:!1,u:!0});return()=>{const e=[],r=It(`<div class="${je}"><div class="${Ve}"></div></div>`)[0],l=r.firstChild,c=t=>{let e=!1,r=!1;if(t instanceof ResizeObserverEntry){const[n,,o]=i(t.contentRect),s=ne(n);r=re(n,o),e=!r&&!s}else r=!0===t;e||n({ft:!0,dt:r})};if(f){const t=new f((t=>c(t.pop())));t.observe(l),E(e,(()=>{t.disconnect()}))}else{if(!s)return ot;{const[t,n]=s(l,c,o);E(e,L([Ct(r,Ne),se(r,"animationstart",t)],n))}}return st(_,E(e,Rt(t,r)))}},kn=(t,n)=>{let r;const o=zt(We),[s]=e({o:!1}),i=(t,e)=>{if(t){const r=s((t=>0===t.h||t.isIntersecting||t.intersectionRatio>0)(t)),[,o]=r;return o&&!e&&n(r)&&[r]}},l=(t,e)=>i(e.pop(),t);return[()=>{const e=[];if(h)r=new h(st(l,!1),{root:t}),r.observe(o),E(e,(()=>{r.disconnect()}));else{const t=()=>{const t=Jt(o);i(t)};E(e,Tn(o,t)()),t()}return st(_,E(e,Rt(t,o)))},()=>r&&l(!0,r.takeRecords())]},Pn=(t,n,r,o)=>{let s,i,l,c,a,u;const d=`[${Te}]`,p=`[${Pe}]`,y=["id","class","style","open","wrap","cols","rows"],{vt:h,ht:v,U:x,gt:b,bt:w,L:m,yt:O,wt:M,St:C,Ot:D}=t,k=t=>"rtl"===Zt(t,"direction"),P={$t:!1,B:k(h)},H=Cn(),A=we(fn),[E]=e({i:et,o:{w:0,h:0}},(()=>{const e=A&&A.V(t,n,P,H,r).W,o=!(O&&m)&&St(v,Te,Ce),s=!m&&M(Le),i=s&&ue(b),l=i&&D(),c=C(Ee,o),a=s&&e&&e()[0],u=Qt(x),d=te(x);return a&&a(),ae(b,i),l&&l(),o&&c(),{w:u.w+d.w,h:u.h+d.h}})),R=lt(o,{_:()=>s,v:()=>i,S(t,e){const[n]=t,[r]=e;return[L(at(n),at(r)).reduce(((t,e)=>(t[e]=n[e]||r[e],t)),{})]}}),U=t=>{const e=k(h);ut(t,{Ct:u!==e}),ut(P,{B:e}),u=e},z=(t,e)=>{const[n,r]=t,s={xt:r};return ut(P,{$t:n}),!e&&o(s),s},I=({ft:t,dt:e})=>{const n=t&&!e||!H.P?o:R,r={ft:t||e,dt:e};U(r),n(r)},_=(t,e)=>{const[,n]=E(),r={Ht:n};return U(r),n&&!e&&(t?o:R)(r),r},j=(t,e,n)=>{const r={Et:e};return U(r),e&&!n&&R(r),r},[N,V]=w?kn(v,z):[],B=!m&&Tn(v,I,{dt:!0}),[F,Z]=Dn(v,!1,j,{rt:y,ct:y}),W=m&&f&&new f((t=>{const e=t[t.length-1].contentRect;I({ft:!0,dt:re(e,a)}),a=e})),q=lt((()=>{const[,t]=E();o({Ht:t})}),{_:222,p:!0});return[()=>{W&&W.observe(v);const t=B&&B(),e=N&&N(),n=F(),r=H.K((t=>{t?R({zt:t}):q()}));return()=>{W&&W.disconnect(),t&&t(),e&&e(),c&&c(),n(),r()}},({It:t,At:e,Dt:n})=>{const r={},[o]=t("update.ignoreMutation"),[a,u]=t("update.attributes"),[h,f]=t("update.elementEvents"),[v,b]=t("update.debounce"),O=e||n;if(f||u){l&&l(),c&&c();const[t,e]=Dn(w||x,!0,_,{ct:L(y,a||[]),lt:h,it:d,_t:(t,e)=>{const{target:n,attributeName:r}=t;return!(e||!r||m)&&((t,e,n)=>{const r=At(t,e),o=t&&((t,e)=>{const n=e?T(e)&&e:document;return n&&n.querySelector(t)})(n,r),s=At(o,e)===r;return!(!r||!o)&&(r===t||o===t||s&&At(At(t,n),e)!==r)})(n,d,p)||!!At(n,`.${Ye}`)||!!(t=>$(o)&&o(t))(t)}});c=t(),l=e}if(b)if(R.m(),S(v)){const t=v[0],e=v[1];s=g(t)&&t,i=g(e)&&e}else g(v)?(s=v,i=!1):(s=!1,i=!1);if(O){const t=Z(),e=V&&V(),n=l&&l();t&&ut(r,j(t[0],t[1],O)),e&&ut(r,z(e[0],O)),n&&ut(r,_(n[0],O))}return U(r),r},P]},Hn=(t,e)=>$(e)?e.apply(0,t):e,An=(t,e,n,r)=>{const o=x(r)?n:r;return Hn(t,o)||e.apply(0,t)},Ln=(t,e,n,r)=>{const o=x(r)?n:r,s=Hn(t,o);return!!s&&(D(s)?s:e.apply(0,t))},En=(t,e,n,r)=>{const o="--os-viewport-percent",s="--os-scroll-percent",i="--os-scroll-direction",{Z:l}=Cn(),{scrollbars:c}=l(),{slot:a}=c,{vt:u,ht:d,U:p,Mt:y,gt:h,yt:f,L:x}=e,{scrollbars:b}=y?{}:t,{slot:g}=b||{},w=[],$=[],S=[],O=Ln([u,d,p],(()=>x&&f?u:d),a,g),M=t=>{if(v){let e=null,r=[];const o=new v({source:h,axis:t}),s=()=>{e&&e.cancel(),e=null},i=i=>{const{Tt:l}=n,c=pe(l)[t],a="x"===t,u=[qt(0,a),qt(`calc(100cq${a?"w":"h"} + -100%)`,a)],d=c?u:u.reverse();return r[0]===d[0]&&r[1]===d[1]||(s(),r=d,e=i.kt.animate({clear:["left"],transform:d},{timeline:o})),s};return{Rt:i}}},C={x:M("x"),y:M("y")},D=(t,e,n)=>{const r=n?Ct:Mt;H(t,(t=>{r(t.Ut,e)}))},T=(t,e)=>{H(t,(t=>{const[n,r]=e(t);Ft(n,r)}))},k=(t,e,n)=>{const r=m(n),o=!r||!n;(!r||n)&&D($,t,e),o&&D(S,t,e)},P=t=>{const e=t?"x":"y",n=zt(`${Ye} ${t?Ge:Je}`),o=zt(Ke),s=zt(Qe),i={Ut:n,Pt:o,kt:s},l=C[e];return E(t?$:S,i),E(w,[Rt(n,o),Rt(o,s),st(Et,n),l&&l.Rt(i),r(i,k,t)]),i},A=st(P,!0),L=st(P,!1);return A(),L(),[{Nt:()=>{const t=(()=>{const{Vt:t,Lt:e}=n,r=(t,e)=>yt(0,1,t/(t+e)||0);return{x:r(e.x,t.x),y:r(e.y,t.y)}})(),e=t=>e=>[e.Ut,{[o]:Vt(t)+""}];T($,e(t.x)),T(S,e(t.y))},qt:()=>{if(!v){const{Tt:t}=n,e=ye(t,ue(h)),r=t=>e=>[e.Ut,{[s]:Vt(t)+""}];T($,r(e.x)),T(S,r(e.y))}},jt:()=>{const{Tt:t}=n,e=pe(t),r=t=>e=>[e.Ut,{[i]:t?"0":"1"}];T($,r(e.x)),T(S,r(e.y)),v&&($.forEach(C.x.Rt),S.forEach(C.y.Rt))},Bt:()=>{if(x&&!f){const{Vt:t,Tt:e}=n,r=pe(e),o=ye(e,ue(h)),s=e=>{const{Ut:n}=e,s=Ht(n)===p&&n,i=(t,e,n)=>{const r=e*t;return Bt(n?r:-r)};return[s,s&&{transform:qt({x:i(o.x,t.x,r.x),y:i(o.y,t.y,r.y)})}]};T($,s),T(S,s)}},Ft:k,Xt:{Yt:$,Wt:A,Gt:st(T,$)},Jt:{Yt:S,Wt:L,Gt:st(T,S)}},()=>(Rt(O,$[0].Ut),Rt(O,S[0].Ut),st(_,w))]},Rn=(t,e,n,r)=>(o,l,c)=>{const{ht:a,U:d,L:p,gt:y,Kt:h,Ot:f}=e,{Ut:v,Pt:x,kt:b}=o,[g,w]=it(333),[m,S]=it(444),O=t=>{$(y.scrollBy)&&y.scrollBy({behavior:"smooth",left:t.x,top:t.y})};let M=!0;return st(_,[se(b,"pointermove pointerleave",r),se(v,"pointerenter",(()=>{l(nn,!0)})),se(v,"pointerleave pointercancel",(()=>{l(nn,!1)})),!p&&se(v,"mousedown",(()=>{const t=Lt();(vt(t,Pe)||vt(t,Te)||t===document.body)&&u(st(he,d),25)})),se(v,"wheel",(t=>{const{deltaX:e,deltaY:n,deltaMode:r}=t;M&&0===r&&Ht(v)===a&&O({x:e,y:n}),M=!1,l(ln,!0),g((()=>{M=!0,l(ln)})),le(t)}),{H:!1,I:!0}),se(v,"pointerdown",st(se,h,"click",ce,{A:!0,I:!0,H:!1}),{I:!0}),(()=>{const e="pointerup pointercancel lostpointercapture",r=`client${c?"X":"Y"}`,o=c?X:G,l=c?"left":"top",a=c?"w":"h",u=c?"x":"y",d=(t,e)=>r=>{const{Vt:o}=n,s=Jt(x)[a]-Jt(b)[a],i=e*r/s*o[u];ae(y,{[u]:t+i})},p=[];return se(x,"pointerdown",(n=>{const c=At(n.target,`.${Qe}`)===b,v=c?b:x,g=t.scrollbars,w=g[c?"dragScroll":"clickScroll"],{button:$,isPrimary:M,pointerType:C}=n,{pointers:D}=g;if(0===$&&M&&w&&(D||[]).includes(C)){_(p),S();const t=!c&&(n.shiftKey||"instant"===w),g=st(ee,b),$=st(ee,x),M=(t,e)=>(t||g())[l]-(e||$())[l],C=s(ee(y)[o])/Jt(y)[a]||1,D=d(ue(y)[u],1/C),T=n[r],k=g(),P=$(),H=k[o],A=M(k,P)+H/2,L=T-P[l],R=c?0:L-A,U=t=>{_(j),v.releasePointerCapture(t.pointerId)},z=c||t,I=f(),j=[se(h,e,U),se(h,"selectstart",(t=>le(t)),{H:!1}),se(x,e,U),z&&se(x,"pointermove",(t=>D(R+(t[r]-T)))),z&&(()=>{const t=ue(y);I();const e=ue(y),n={x:e.x-t.x,y:e.y-t.y};(i(n.x)>3||i(n.y)>3)&&(f(),ae(y,t),O(n),m(I))})];if(v.setPointerCapture(n.pointerId),t)D(R);else if(!c){const t=we(xn);if(t){const e=t(D,R,H,(t=>{t?I():E(j,I)}));E(j,e),E(p,st(e,!0))}}}}))})(),w,S])},Un=t=>{const e=Cn(),{Z:r,P:o}=e,{elements:s}=r(),{padding:i,viewport:l,content:c}=s,a=D(t),u=a?{}:t,{elements:d}=u,{padding:p,viewport:y,content:h}=d||{},f=a?t:u.target,v=kt(f),x=f.ownerDocument,b=x.documentElement,g=()=>x.defaultView||n,w=st(An,[f]),m=st(Ln,[f]),$=st(zt,""),S=st(w,$,l),O=st(m,$,c),M=S(y),C=M===f,T=C&&v,k=!C&&O(h),P=!C&&M===k,H=T?b:M,L=T?H:f,R=!C&&m($,i,p),U=!P&&k,z=[U,H,R,L].map((t=>D(t)&&!Ht(t)&&t)),I=t=>t&&A(z,t),j=!I(H)&&(t=>{const e=Jt(t),n=Qt(t),r=Zt(t,q),o=Zt(t,Y);return n.w-e.w>0&&!yn(r)||n.h-e.h>0&&!yn(o)})(H)?H:f,N=T?b:H,V={vt:f,ht:L,U:H,ln:R,bt:U,gt:N,Qt:T?x:H,an:v?b:j,Kt:x,yt:v,Mt:a,L:C,un:g,wt:t=>St(H,Pe,t),St:(t,e)=>$t(H,Pe,t,e),Ot:()=>$t(N,Pe,Re,!0)},{vt:B,ht:F,ln:Z,U:W,bt:X}=V,G=[()=>{bt(F,[Te,Me]),bt(B,Me),v&&bt(b,[Me,Te])}];let J=Pt([X,W,Z,F,B].find((t=>t&&!I(t))));const K=T?B:X||W,Q=st(_,G);return[V,()=>{const t=g(),e=Lt(),n=t=>{Rt(Ht(t),Pt(t)),Et(t)},r=t=>se(t,"focusin focusout focus blur",ce,{I:!0,H:!1}),s="tabindex",i=ft(W,s),l=r(e);return xt(F,Te,C?"":ke),xt(Z,Ie,""),xt(W,Pe,""),xt(X,_e,""),C||(xt(W,s,i||"-1"),v&&xt(b,De,"")),Rt(K,J),Rt(F,Z),Rt(Z||F,!C&&W),Rt(W,X),E(G,[l,()=>{const t=Lt(),e=I(W),o=e&&t===W?B:t,l=r(o);bt(Z,Ie),bt(X,_e),bt(W,Pe),v&&bt(b,De),i?xt(W,s,i):bt(W,s),I(X)&&n(X),e&&n(W),I(Z)&&n(Z),he(o),l()}]),o&&!C&&(mt(W,Pe,Ue),E(G,st(bt,W,Pe))),he(!C&&v&&e===B&&t.top===t?W:e),l(),J=0,Q},Q]},zn=({bt:t})=>({Zt:e,_n:n,Dt:r})=>{const{xt:o}=e||{},{$t:s}=n;t&&(o||r)&&Ft(t,{[G]:s&&"100%"})},In=({ht:t,ln:n,U:r,L:o},s)=>{const[i,l]=e({i:rt,o:Wt()},st(Wt,t,"padding",""));return({It:t,Zt:e,_n:c,Dt:a})=>{let[u,d]=l(a);const{P:p}=Cn(),{ft:y,Ht:h,Ct:f}=e||{},{B:v}=c,[x,b]=t("paddingAbsolute");(y||d||a||h)&&([u,d]=i(a));const g=!o&&(b||f||d);if(g){const t=!x||!n&&!p,e=u.r+u.l,o=u.t+u.b,i={[Z]:t&&!v?-e:0,[W]:t?-o:0,[F]:t&&v?-e:0,top:t?-u.t:0,right:t?v?-u.r:"auto":0,left:t?v?"auto":-u.l:0,[X]:t&&`calc(100% + ${e}px)`},l={[j]:t?u.t:0,[N]:t?u.r:0,[B]:t?u.b:0,[V]:t?u.l:0};Ft(n||r,i),Ft(r,l),ut(s,{ln:u,dn:!t,F:n?l:ut({},i,l)})}return{fn:g}}},_n=(t,o)=>{const s=Cn(),{ht:i,ln:l,U:c,L:u,Qt:d,gt:p,yt:y,St:h,un:f}=t,{P:v}=s,x=y&&u,b=st(r,0),g={display:()=>!1,direction:t=>"ltr"!==t,flexDirection:t=>t.endsWith("-reverse"),writingMode:t=>"horizontal-tb"!==t},w=at(g),m={i:et,o:{w:0,h:0}},$={i:nt,o:{}},S=t=>{h(Ee,!x&&t)},O=t=>{const e=w.some((e=>{const n=t[e];return n&&g[e](n)}));if(!e)return{D:{x:0,y:0},M:{x:1,y:1}};S(!0);const n=ue(p),r=h(ze,!0),o=se(d,Q,(t=>{const e=ue(p);t.isTrusted&&e.x===n.x&&e.y===n.y&&ie(t)}),{I:!0,A:!0});ae(p,{x:0,y:0}),r();const s=ue(p),i=Qt(p);ae(p,{x:i.w,y:i.h});const l=ue(p);ae(p,{x:l.x-s.x<1&&-i.w,y:l.y-s.y<1&&-i.h});const c=ue(p);return ae(p,n),a((()=>o())),{D:s,M:c}},M=(t,e)=>{const r=n.devicePixelRatio%1!=0?1:0,o={w:b(t.w-e.w),h:b(t.h-e.h)};return{w:o.w>r?o.w:0,h:o.h>r?o.h:0}},[C,D]=e(m,st(te,c)),[T,k]=e(m,st(Qt,c)),[P,H]=e(m),[A]=e($),[L,E]=e(m),[R]=e($),[U]=e({i:(t,e)=>tt(t,e,w),o:{}},(()=>(t=>!!t&&(t=>!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length))(t))(c)?Zt(c,w):{})),[z,I]=e({i:(t,e)=>nt(t.D,e.D)&&nt(t.M,e.M),o:{D:{x:0,y:0},M:{x:0,y:0}}}),_=we(fn),j=(t,e)=>`${e?He:Ae}${(t=>{const e=String(t||"");return e?e[0].toUpperCase()+e.slice(1):""})(t)}`,N=t=>{const e=t=>[J,K,Q].map((e=>j(e,t))),n=e(!0).concat(e()).join(" ");h(n),h(at(t).map((e=>j(t[e],"x"===e))).join(" "),!0)};return({It:e,Zt:n,_n:r,Dt:a},{fn:u})=>{const{ft:d,Ht:p,Ct:y,dt:g,zt:w}=n||{},m=_&&_.V(t,o,r,s,e),{Y:$,W:j,G:V}=m||{},[B,F]=pn(e,s),[Z,W]=e("overflow"),q=yn(Z.x),Y=yn(Z.y);let X=D(a),G=k(a),J=H(a),K=E(a);F&&v&&h(Ue,!B);{St(i,Te,Ce)&&S(!0);const[t]=j?j():[],[e]=X=C(a),[n]=G=T(a),r=Kt(c),o=x&&Gt(f()),s={w:b(n.w+e.w),h:b(n.h+e.h)},l={w:b((o?o.w:r.w+b(r.w-n.w))+e.w),h:b((o?o.h:r.h+b(r.h-n.h))+e.h)};t&&t(),K=L(l),J=P(M(s,l),a)}const[Q,tt]=K,[et,nt]=J,[rt,ot]=G,[st,it]=X,[lt,ct]=A({x:et.w>0,y:et.h>0}),at=q&&Y&&(lt.x||lt.y)||q&&lt.x&&!lt.y||Y&&lt.y&&!lt.x,dt=u||y||w||it||ot||tt||nt||W||F||!0,pt=hn(lt,Z),[yt,ht]=R(pt.k),[ft,vt]=U(a),xt=y||g||vt||ct||a,[bt,gt]=xt?z(O(ft),a):I();return dt&&(ht&&N(pt.k),V&&$&&Ft(c,V(pt,r,$(pt,rt,st)))),S(!1),$t(i,Te,Ce,at),$t(l,Ie,Ce,at),ut(o,{k:yt,Lt:{x:Q.w,y:Q.h},Vt:{x:et.w,y:et.h},rn:lt,Tt:de(bt,et)}),{en:ht,nn:tt,sn:nt,cn:gt||nt,vn:xt}}},jn=t=>{const[e,n,r]=Un(t),o={ln:{t:0,r:0,b:0,l:0},dn:!1,F:{[Z]:0,[W]:0,[F]:0,[j]:0,[N]:0,[B]:0,[V]:0},Lt:{x:0,y:0},Vt:{x:0,y:0},k:{x:K,y:K},rn:{x:!1,y:!1},Tt:{D:{x:0,y:0},M:{x:0,y:0}}},{vt:s,gt:i,L:l,Ot:c}=e,{P:a,T:u}=Cn(),d=!a&&(u.x||u.y),p=[zn(e),In(e,o),_n(e,o)];return[n,t=>{const e={},n=d&&ue(i),r=n&&c();return H(p,(n=>{ut(e,n(t,e)||{})})),ae(i,n),r&&r(),!l&&ae(s,0),e},o,e,r]},Nn=new WeakMap,Vn=t=>Nn.get(t),Bn=(t,e,n)=>{const{nt:r}=Cn(),o=D(t),s=o?t:t.target,i=Vn(s);if(e&&!i){let i=!1;const l=[],c={},a=t=>{const e=dt(t),n=we(me);return n?n(e,!0):e},u=ut({},r(),a(e)),[d,p,y]=ve(),[h,f,v]=ve(n),g=(t,e)=>{v(t,e),y(t,e)},[w,m,$,S,O]=((t,e,n,r)=>{let o=!1;const s=Sn(e,{}),[i,l,c,a,u]=jn(t),[d,p,y]=Pn(a,c,s,(t=>{b({},t)})),[h,f,,v]=((t,e,n,r,o,s)=>{let i,l,c,a,u,d=ot,p=0;const y=["mouse","pen"],h=t=>y.includes(t.pointerType),[f,v]=it(),[x,b]=it(100),[g,w]=it(100),[m,$]=it((()=>p)),[S,O]=En(t,o,r,Rn(e,o,r,(t=>h(t)&&R()))),{ht:M,Qt:C,yt:D}=o,{Ft:T,Nt:k,qt:P,jt:H,Bt:A}=S,L=(t,e)=>{if($(),t)T(sn);else{const t=st(T,sn,!0);p>0&&!e?m(t):t()}},R=()=>{(c?i:a)||(L(!0),x((()=>{L(!1)})))},U=t=>{T(on,t,!0),T(on,t,!1)},z=t=>{h(t)&&(i=c,c&&L(!0))},I=[$,b,w,v,()=>d(),se(M,"pointerover",z,{A:!0}),se(M,"pointerenter",z),se(M,"pointerleave",(t=>{h(t)&&(i=!1,c&&L(!1))})),se(M,"pointermove",(t=>{h(t)&&l&&R()})),se(C,"scroll",(t=>{f((()=>{P(),R()})),s(t),A()}))];return[()=>st(_,E(I,O())),({It:t,Dt:e,Zt:o,tn:s})=>{const{nn:i,sn:y,en:h,cn:f}=s||{},{Ct:v,dt:x}=o||{},{B:b}=n,{T:w}=Cn(),{k:m,rn:$}=r,[S,O]=t("showNativeOverlaidScrollbars"),[M,E]=t("scrollbars.theme"),[R,z]=t("scrollbars.visibility"),[I,_]=t("scrollbars.autoHide"),[j,N]=t("scrollbars.autoHideSuspend"),[V]=t("scrollbars.autoHideDelay"),[B,F]=t("scrollbars.dragScroll"),[Z,W]=t("scrollbars.clickScroll"),[q,Y]=t("overflow"),X=x&&!e,G=$.x||$.y,K=i||y||f||v||e,tt=h||z||Y,et=S&&w.x&&w.y,nt=(t,e,n)=>{const r=t.includes(Q)&&(R===J||"auto"===R&&e===Q);return T(tn,r,n),r};if(p=V,X&&(j&&G?(U(!1),d(),g((()=>{d=se(C,"scroll",st(U,!0),{A:!0})}))):U(!0)),O&&T(qe,et),E&&(T(u),T(M,!0),u=M),N&&!j&&U(!0),_&&(l="move"===I,c="leave"===I,a="never"===I,L(a,!0)),F&&T(an,B),W&&T(cn,!!Z),tt){const t=nt(q.x,m.x,!0),e=nt(q.y,m.y,!1);T(en,!(t&&e))}K&&(P(),k(),A(),f&&H(),T(rn,!$.x,!0),T(rn,!$.y,!1),T(Xe,b&&!D))},{},S]})(t,e,y,c,a,(t=>g("scroll",[C,t]))),x=t=>at(t).some((e=>!!t[e])),b=(t,s)=>{if(n())return!1;const{pn:i,Dt:c,At:a,hn:u}=t,d=i||{},h=!!c||!o,v={It:Sn(e,d,h),pn:d,Dt:h};if(u)return f(v),!1;const b=s||p(ut({},v,{At:a})),g=l(ut({},v,{_n:y,Zt:b}));f(ut({},v,{Zt:b,tn:g}));const w=x(b),m=x(g),$=w||m||!pt(d)||h;return o=!0,$&&r(t,{Zt:b,tn:g}),$};return[()=>{const{an:t,gt:e,Ot:n}=a,r=ue(t),o=[d(),i(),h()],s=n();return ae(e,r),s(),st(_,o)},b,()=>({gn:y,bn:c}),{yn:a,wn:v},u]})(t,u,(()=>i),(({pn:t,Dt:e},{Zt:n,tn:r})=>{const{ft:o,Ct:s,xt:i,Ht:l,Et:c,dt:a}=n,{nn:u,sn:d,en:p,cn:y}=r;g("updated",[C,{updateHints:{sizeChanged:!!o,directionChanged:!!s,heightIntrinsicChanged:!!i,overflowEdgeChanged:!!u,overflowAmountChanged:!!d,overflowStyleChanged:!!p,scrollCoordinatesChanged:!!y,contentMutation:!!l,hostMutation:!!c,appear:!!a},changedOptions:t||{},force:!!e}])})),M=t=>{(t=>{Nn.delete(t)})(s),_(l),i=!0,g("destroyed",[C,t]),p(),f()},C={options(t,e){if(t){const n=e?r():{},o=$n(u,ut(n,a(t)));pt(o)||(ut(u,o),m({pn:o}))}return ut({},u)},on:h,off:(t,e)=>{t&&e&&f(t,e)},state(){const{gn:t,bn:e}=$(),{B:n}=t,{Lt:r,Vt:o,k:s,rn:l,ln:c,dn:a,Tt:u}=e;return ut({},{overflowEdge:r,overflowAmount:o,overflowStyle:s,hasOverflow:l,scrollCoordinates:{start:u.D,end:u.M},padding:c,paddingAbsolute:a,directionRTL:n,destroyed:i})},elements(){const{vt:t,ht:e,ln:n,U:r,bt:o,gt:s,Qt:i}=S.yn,{Xt:l,Jt:c}=S.wn,a=t=>{const{kt:e,Pt:n,Ut:r}=t;return{scrollbar:r,track:n,handle:e}},u=t=>{const{Yt:e,Wt:n}=t,r=a(e[0]);return ut({},r,{clone:()=>{const t=a(n());return m({hn:!0}),t}})};return ut({},{target:t,host:e,padding:n||r,viewport:r,content:o||r,scrollOffsetElement:s,scrollEventElement:i,scrollbarHorizontal:u(l),scrollbarVertical:u(c)})},update:t=>m({Dt:t,At:!0}),destroy:st(M,!1),plugin:t=>c[at(t)[0]]};return E(l,[O]),((t,e)=>{Nn.set(t,e)})(s,C),ge(xe,Bn,[C,d,c]),((t,e)=>{const{nativeScrollbarsOverlaid:n,body:r}=e||{},{T:o,P:s,Z:i}=Cn(),{nativeScrollbarsOverlaid:l,body:c}=i().cancel,a=null!=n?n:l,u=x(r)?c:r,d=(o.x||o.y)&&a,p=t&&(b(u)?!s:u);return!!d||!!p})(S.yn.yt,!o&&t.cancel)?(M(!0),C):(E(l,w()),g("initialized",[C]),C.update(),C)}return i};return Bn.plugin=t=>{const e=S(t),n=e?t:[t],r=n.map((t=>ge(t,Bn)[0]));return(t=>{H(t,(t=>H(t,((e,n)=>{xe[n]=t[n]}))))})(n),e?r:r[0]},Bn.valid=t=>{const e=t&&t.elements,n=$(e)&&e();return C(n)&&!!Vn(n.target)},Bn.env=()=>{const{N:t,T:e,P:n,J:r,st:o,et:s,Z:i,tt:l,nt:c,ot:a}=Cn();return ut({},{scrollbarsSize:t,scrollbarsOverlaid:e,scrollbarsHiding:n,scrollTimeline:r,staticDefaultInitialization:o,staticDefaultOptions:s,getDefaultInitialization:i,setDefaultInitialization:l,getDefaultOptions:c,setDefaultOptions:a})},Bn.nonce=t=>{On=t},Bn.trustedTypePolicy=t=>{Ut=t},t.ClickScrollPlugin=bn,t.OverlayScrollbars=Bn,t.ScrollbarsHidingPlugin=vn,t.SizeObserverPlugin=dn,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),t}({}); \ No newline at end of file
diff --git a/assets/overlayscrollbars.min.css b/assets/overlayscrollbars.min.css
new file mode 100644
index 0000000..47a496c
--- /dev/null
+++ b/assets/overlayscrollbars.min.css
@@ -0,0 +1,9 @@
+/*!
+ * OverlayScrollbars
+ * Version: 2.11.0
+ *
+ * Copyright (c) Rene Haas | KingSora.
+ * https://github.com/KingSora
+ *
+ * Released under the MIT license.
+ */.os-size-observer,.os-size-observer-listener{box-sizing:border-box;direction:inherit;overflow:hidden;pointer-events:none;scroll-behavior:auto!important;visibility:hidden}.os-size-observer,.os-size-observer-listener,.os-size-observer-listener-item,.os-size-observer-listener-item-final{left:0;position:absolute;top:0;writing-mode:horizontal-tb}.os-size-observer{border:inherit;bottom:0;box-sizing:inherit;contain:strict;display:flex;flex-direction:row;flex-wrap:nowrap;left:0;margin:-133px;padding:inherit;right:0;top:0;transform:scale(.1);z-index:-1}.os-size-observer:before{box-sizing:inherit;content:"";flex:none;height:10px;padding:10px;width:10px}.os-size-observer-appear{animation:os-size-observer-appear-animation 1ms forwards}.os-size-observer-listener{border:inherit;box-sizing:border-box;flex:auto;margin:-133px;padding:inherit;position:relative;transform:scale(10)}.os-size-observer-listener.ltr{margin-left:0;margin-right:-266px}.os-size-observer-listener.rtl{margin-left:-266px;margin-right:0}.os-size-observer-listener:empty:before{content:"";height:100%;width:100%}.os-size-observer-listener:empty:before,.os-size-observer-listener>.os-size-observer-listener-item{border:inherit;box-sizing:content-box;display:block;flex:auto;padding:inherit;position:relative}.os-size-observer-listener-scroll{box-sizing:border-box;display:flex}.os-size-observer-listener-item{bottom:0;direction:ltr;flex:none;overflow:hidden;right:0}.os-size-observer-listener-item-final{transition:none}@keyframes os-size-observer-appear-animation{0%{cursor:auto}to{cursor:none}}.os-trinsic-observer{border:none;box-sizing:border-box;contain:strict;flex:none;height:0;margin:0;max-height:1px;max-width:0;overflow:hidden;padding:0;position:relative;top:calc(100% + 1px);z-index:-1}.os-trinsic-observer:not(:empty){height:calc(100% + 1px);top:-1px}.os-trinsic-observer:not(:empty)>.os-size-observer{height:1000%;min-height:1px;min-width:1px;width:1000%}[data-overlayscrollbars-initialize],[data-overlayscrollbars-viewport~=scrollbarHidden]{scrollbar-width:none!important}[data-overlayscrollbars-initialize]::-webkit-scrollbar,[data-overlayscrollbars-initialize]::-webkit-scrollbar-corner,[data-overlayscrollbars-viewport~=scrollbarHidden]::-webkit-scrollbar,[data-overlayscrollbars-viewport~=scrollbarHidden]::-webkit-scrollbar-corner{-webkit-appearance:none!important;appearance:none!important;display:none!important;height:0!important;width:0!important}[data-overlayscrollbars-initialize]:not([data-overlayscrollbars]):not(html):not(body){overflow:auto}html[data-overlayscrollbars-body]{overflow:hidden}html[data-overlayscrollbars-body],html[data-overlayscrollbars-body]>body{height:100%;margin:0;width:100%}html[data-overlayscrollbars-body]>body{margin:0;overflow:visible}[data-overlayscrollbars]{position:relative}[data-overlayscrollbars-padding],[data-overlayscrollbars~=host]{align-items:stretch!important;display:flex;flex-direction:row!important;flex-wrap:nowrap!important;scroll-behavior:auto!important}[data-overlayscrollbars-padding],[data-overlayscrollbars-viewport]:not([data-overlayscrollbars]){border:none;box-sizing:inherit;flex:auto!important;height:auto;margin:0;min-width:0;padding:0;position:relative;width:100%;z-index:0}[data-overlayscrollbars-viewport]:not([data-overlayscrollbars]){--os-vaw:0;--os-vah:0;outline:none}[data-overlayscrollbars-viewport]:not([data-overlayscrollbars]):focus{outline:none}[data-overlayscrollbars-viewport][data-overlayscrollbars-viewport~=arrange]:before{content:"";height:var(--os-vah);min-height:1px;min-width:1px;pointer-events:none;position:absolute;width:var(--os-vaw);z-index:-1}[data-overlayscrollbars-padding],[data-overlayscrollbars-viewport],[data-overlayscrollbars]{overflow:hidden!important}[data-overlayscrollbars-padding~=noClipping],[data-overlayscrollbars~=noClipping]{overflow:visible!important}[data-overlayscrollbars-viewport~=measuring]{overflow:hidden!important;scroll-behavior:auto!important;scroll-snap-type:none!important}[data-overlayscrollbars-viewport~=overflowXVisible]:not([data-overlayscrollbars-viewport~=measuring]){overflow-x:visible!important}[data-overlayscrollbars-viewport~=overflowXHidden]{overflow-x:hidden!important}[data-overlayscrollbars-viewport~=overflowXScroll]{overflow-x:scroll!important}[data-overlayscrollbars-viewport~=overflowYVisible]:not([data-overlayscrollbars-viewport~=measuring]){overflow-y:visible!important}[data-overlayscrollbars-viewport~=overflowYHidden]{overflow-y:hidden!important}[data-overlayscrollbars-viewport~=overflowYScroll]{overflow-y:scroll!important}[data-overlayscrollbars-viewport~=noContent]:not(#osFakeId){font-size:0!important;line-height:0!important}[data-overlayscrollbars-viewport~=noContent]:not(#osFakeId):after,[data-overlayscrollbars-viewport~=noContent]:not(#osFakeId):before,[data-overlayscrollbars-viewport~=noContent]:not(#osFakeId)>*{clip:rect(0,0,0,0)!important;border-width:0!important;display:none!important;height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;white-space:nowrap!important;width:1px!important}[data-overlayscrollbars-viewport~=scrolling]{scroll-behavior:auto!important;scroll-snap-type:none!important}[data-overlayscrollbars-content]{box-sizing:inherit}[data-overlayscrollbars-contents]:not(#osFakeId):not([data-overlayscrollbars-padding]):not([data-overlayscrollbars-viewport]):not([data-overlayscrollbars-content]){display:contents}[data-overlayscrollbars-grid],[data-overlayscrollbars-grid] [data-overlayscrollbars-padding]{display:grid;grid-template:1fr/1fr}[data-overlayscrollbars-grid]>[data-overlayscrollbars-padding],[data-overlayscrollbars-grid]>[data-overlayscrollbars-padding]>[data-overlayscrollbars-viewport],[data-overlayscrollbars-grid]>[data-overlayscrollbars-viewport]{height:auto!important;width:auto!important}@property --os-scroll-percent{syntax:"<number>";inherits:true;initial-value:0}@property --os-viewport-percent{syntax:"<number>";inherits:true;initial-value:0}.os-scrollbar{--os-viewport-percent:0;--os-scroll-percent:0;--os-scroll-direction:0;--os-scroll-percent-directional:calc(var(--os-scroll-percent) - (var(--os-scroll-percent) + (1 - var(--os-scroll-percent))*-1)*var(--os-scroll-direction));contain:size layout;contain:size layout style;opacity:0;pointer-events:none;position:absolute;transition:opacity .15s,visibility .15s,top .15s,right .15s,bottom .15s,left .15s;visibility:hidden}body>.os-scrollbar{position:fixed;z-index:99999}.os-scrollbar-transitionless{transition:none!important}.os-scrollbar-track{border:none!important;padding:0!important;position:relative}.os-scrollbar-handle{position:absolute}.os-scrollbar-handle,.os-scrollbar-track{height:100%;pointer-events:none;width:100%}.os-scrollbar.os-scrollbar-handle-interactive .os-scrollbar-handle,.os-scrollbar.os-scrollbar-track-interactive .os-scrollbar-track{pointer-events:auto;touch-action:none}.os-scrollbar-horizontal{bottom:0;left:0}.os-scrollbar-vertical{right:0;top:0}.os-scrollbar-rtl.os-scrollbar-horizontal{right:0}.os-scrollbar-rtl.os-scrollbar-vertical{left:0;right:auto}.os-scrollbar-visible{opacity:1;visibility:visible}.os-scrollbar-auto-hide.os-scrollbar-auto-hide-hidden{opacity:0;visibility:hidden}.os-scrollbar-interaction.os-scrollbar-visible{opacity:1;visibility:visible}.os-scrollbar-unusable,.os-scrollbar-unusable *,.os-scrollbar-wheel,.os-scrollbar-wheel *{pointer-events:none!important}.os-scrollbar-unusable .os-scrollbar-handle{opacity:0!important;transition:none!important}.os-scrollbar-horizontal .os-scrollbar-handle{bottom:0;left:calc(var(--os-scroll-percent-directional)*100%);transform:translateX(calc(var(--os-scroll-percent-directional)*-100%));width:calc(var(--os-viewport-percent)*100%)}.os-scrollbar-vertical .os-scrollbar-handle{height:calc(var(--os-viewport-percent)*100%);right:0;top:calc(var(--os-scroll-percent-directional)*100%);transform:translateY(calc(var(--os-scroll-percent-directional)*-100%))}@supports (container-type:size){.os-scrollbar-track{container-type:size}.os-scrollbar-horizontal .os-scrollbar-handle{left:auto;transform:translateX(calc(var(--os-scroll-percent-directional)*100cqw + var(--os-scroll-percent-directional)*-100%))}.os-scrollbar-vertical .os-scrollbar-handle{top:auto;transform:translateY(calc(var(--os-scroll-percent-directional)*100cqh + var(--os-scroll-percent-directional)*-100%))}.os-scrollbar-rtl.os-scrollbar-horizontal .os-scrollbar-handle{left:0;right:auto}}.os-scrollbar-rtl.os-scrollbar-vertical .os-scrollbar-handle{left:0;right:auto}.os-scrollbar.os-scrollbar-horizontal.os-scrollbar-cornerless,.os-scrollbar.os-scrollbar-horizontal.os-scrollbar-cornerless.os-scrollbar-rtl{left:0;right:0}.os-scrollbar.os-scrollbar-vertical.os-scrollbar-cornerless,.os-scrollbar.os-scrollbar-vertical.os-scrollbar-cornerless.os-scrollbar-rtl{bottom:0;top:0}@media print{.os-scrollbar{display:none}}.os-scrollbar{--os-size:0;--os-padding-perpendicular:0;--os-padding-axis:0;--os-track-border-radius:0;--os-track-bg:none;--os-track-bg-hover:none;--os-track-bg-active:none;--os-track-border:none;--os-track-border-hover:none;--os-track-border-active:none;--os-handle-border-radius:0;--os-handle-bg:none;--os-handle-bg-hover:none;--os-handle-bg-active:none;--os-handle-border:none;--os-handle-border-hover:none;--os-handle-border-active:none;--os-handle-min-size:33px;--os-handle-max-size:none;--os-handle-perpendicular-size:100%;--os-handle-perpendicular-size-hover:100%;--os-handle-perpendicular-size-active:100%;--os-handle-interactive-area-offset:0}.os-scrollbar-track{background:var(--os-track-bg);border:var(--os-track-border);border-radius:var(--os-track-border-radius);transition:opacity .15s,background-color .15s,border-color .15s}.os-scrollbar-track:hover{background:var(--os-track-bg-hover);border:var(--os-track-border-hover)}.os-scrollbar-track:active{background:var(--os-track-bg-active);border:var(--os-track-border-active)}.os-scrollbar-handle{background:var(--os-handle-bg);border:var(--os-handle-border);border-radius:var(--os-handle-border-radius)}.os-scrollbar-handle:hover{background:var(--os-handle-bg-hover);border:var(--os-handle-border-hover)}.os-scrollbar-handle:active{background:var(--os-handle-bg-active);border:var(--os-handle-border-active)}.os-scrollbar-handle:before,.os-scrollbar-track:before{bottom:0;content:"";display:block;left:0;position:absolute;right:0;top:0}.os-scrollbar-horizontal{height:var(--os-size);padding:var(--os-padding-perpendicular) var(--os-padding-axis);right:var(--os-size)}.os-scrollbar-horizontal.os-scrollbar-rtl{left:var(--os-size);right:0}.os-scrollbar-horizontal .os-scrollbar-track:before{bottom:calc(var(--os-padding-perpendicular)*-1);top:calc(var(--os-padding-perpendicular)*-1)}.os-scrollbar-horizontal .os-scrollbar-handle{height:var(--os-handle-perpendicular-size);max-width:var(--os-handle-max-size);min-width:var(--os-handle-min-size);transition:opacity .15s,background-color .15s,border-color .15s,height .15s}.os-scrollbar-horizontal .os-scrollbar-handle:before{bottom:calc(var(--os-padding-perpendicular)*-1);top:calc((var(--os-padding-perpendicular) + var(--os-handle-interactive-area-offset))*-1)}.os-scrollbar-horizontal:hover .os-scrollbar-handle{height:var(--os-handle-perpendicular-size-hover)}.os-scrollbar-horizontal:active .os-scrollbar-handle{height:var(--os-handle-perpendicular-size-active)}.os-scrollbar-vertical{bottom:var(--os-size);padding:var(--os-padding-axis) var(--os-padding-perpendicular);width:var(--os-size)}.os-scrollbar-vertical .os-scrollbar-track:before{left:calc(var(--os-padding-perpendicular)*-1);right:calc(var(--os-padding-perpendicular)*-1)}.os-scrollbar-vertical .os-scrollbar-handle{max-height:var(--os-handle-max-size);min-height:var(--os-handle-min-size);transition:opacity .15s,background-color .15s,border-color .15s,width .15s;width:var(--os-handle-perpendicular-size)}.os-scrollbar-vertical .os-scrollbar-handle:before{left:calc((var(--os-padding-perpendicular) + var(--os-handle-interactive-area-offset))*-1);right:calc(var(--os-padding-perpendicular)*-1)}.os-scrollbar-vertical.os-scrollbar-rtl .os-scrollbar-handle:before{left:calc(var(--os-padding-perpendicular)*-1);right:calc((var(--os-padding-perpendicular) + var(--os-handle-interactive-area-offset))*-1)}.os-scrollbar-vertical:hover .os-scrollbar-handle{width:var(--os-handle-perpendicular-size-hover)}.os-scrollbar-vertical:active .os-scrollbar-handle{width:var(--os-handle-perpendicular-size-active)}.os-theme-none.os-scrollbar,[data-overlayscrollbars-viewport~=measuring]>.os-scrollbar{display:none!important}.os-theme-dark,.os-theme-light{--os-size:8px;--os-padding-perpendicular:0px;--os-padding-axis:0px;--os-track-border-radius:0px;--os-handle-interactive-area-offset:0px;--os-handle-border-radius:0px;box-sizing:border-box}.os-theme-dark{--os-handle-bg:rgba(0,0,0);--os-handle-bg-hover:rgba(0,0,0);--os-handle-bg-active:rgba(0,0,0)}.os-theme-light{--os-handle-bg:hsla(0,0%,100%,.44);--os-handle-bg-hover:hsla(0,0%,100%,.55);--os-handle-bg-active:hsla(0,0%,100%,.66)}
diff --git a/assets/style.scss b/assets/style.scss
new file mode 100644
index 0000000..10042c2
--- /dev/null
+++ b/assets/style.scss
@@ -0,0 +1,1605 @@
+/*
+ * SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/* App-wide styling */
+img {
+ display: block;
+}
+
+::-webkit-scrollbar {
+ width: 8px;
+}
+.messages-buffer::-webkit-scrollbar {
+ width: 10px !important;
+}
+
+::-webkit-scrollbar-thumb {
+ background: black;
+}
+
+body {
+ max-width: 100vw;
+ max-height: 100vh;
+ min-width: 100vw;
+ min-height: 100vh;
+ background: linear-gradient(180deg, #392C25 40%, #71574A 100%);
+ color: #dcdcdc;
+ font-family: 'K2D', Tahoma, Geneva, Verdana, sans-serif;
+ padding: 0;
+ margin: 0;
+ border: 0;
+ word-break: break-word;
+ word-wrap: break-word;
+ text-wrap: wrap;
+ overflow-wrap: break-word;
+ display: flex;
+ justify-content: stretch;
+ align-items: stretch;
+ flex: 1 1 auto;
+}
+
+html {
+ padding: 0;
+ margin: 0;
+ border: 0;
+}
+
+.fill {
+ width: 100vw;
+ height: 100vh;
+}
+
+.center {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+#hero {
+ display: flex;
+ flex-wrap: wrap;
+ flex: 1 1 auto;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+}
+
+#hero img {
+ height: 128px;
+ width: 128px;
+ max-width: 100%;
+}
+
+#hero h1 {
+ font-size: 64px;
+ display: inline-block;
+ width: max(auto, min-content);
+}
+
+.panel {
+ border: 2px solid black;
+ border-color: black;
+ border-style: solid;
+ background: linear-gradient(180deg, #364B3B 0%, #19311F 100%);
+}
+
+#login-form {
+ padding: 32px;
+ margin: 4px;
+ max-width: 850px;
+}
+
+#login-form form {
+ margin: auto;
+ max-width: 350px;
+ display: flex;
+ flex-direction: column;
+ /* align-items: center; */
+ justify-content: center;
+ gap: 4px;
+}
+
+#login-form .button {
+ margin-top: 16px;
+}
+
+#login-form form>input {
+ min-width: 0;
+ width: 100%;
+ max-width: 100%;
+ font-size: 1.5rem;
+}
+
+label {
+ font-weight: bold;
+}
+
+input[type="text"],
+input[type="password"] {
+ font-size: 1rem;
+ border: 2px solid black;
+ font-family: "k2d";
+}
+
+aside {
+ width: 500px;
+ max-height: 100%;
+}
+
+main {
+ width: 100%;
+ height: 100%;
+}
+
+button,
+.button {
+ text-align: center;
+ font-size: 1rem;
+ background: #FACC34;
+ color: #000000;
+ text-decoration: none !important;
+ border: 2px solid black;
+ border-radius: 12px;
+ padding: 0.5em 1em;
+ font-family: K2D;
+ font-weight: 600;
+ box-shadow: inset 0px -0.75em 0.5em #ebb62e, 0 0.25em 0.25em #00000048;
+ cursor: pointer;
+}
+
+.overlay .error {
+ color: black;
+}
+
+button:hover, .button:hover {
+ background: #f9c61b;
+}
+
+button:clicked, .button:clicked {
+ background: #ffc300;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: Diolce;
+ margin: 0;
+}
+
+p {
+ margin: 0;
+}
+
+.chats-list,
+.roster-list,
+.settings-sidebar {
+ border-width: 0 2px 0 0;
+ border-color: black;
+ border-style: solid;
+ height: 100%;
+ width: 400px;
+ flex: 0 0 auto;
+ align-self: stretch;
+ display: flex;
+ flex-direction: column;
+}
+
+.settings-sidebar {
+ width: 200px;
+ display: flex;
+ flex-direction: column;
+}
+
+.settings-sidebar>* {
+ padding: 8px 16px;
+ cursor: pointer;
+}
+
+.settings-sidebar>.open,
+.settings-sidebar>*:hover {
+ background: #00000060;
+}
+
+.chats-list>*,
+.roster-list>*,
+.settings .header {
+ padding: 8px 16px;
+}
+
+.chats-list .header,
+.roster-list .header,
+.settings .header {
+ margin-top: 0.4rem;
+ border-bottom: 2px solid black;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.chats-list h2,
+.roster-list h2 {
+ flex: 0 0 auto;
+}
+
+.chats-list-item,
+.roster-list-item,
+{
+ cursor: pointer;
+ display: flex;
+ align-items: start;
+ gap: 8px;
+ border-radius: 1em;
+ padding: 0.5em;
+ margin: 0.5em 0;
+ box-sizing: border-box;
+ align-items: stretch;
+}
+
+.chats-list-item:hover,
+.chats-list-item.open,
+.roster-list-item:hover,
+.roster-list-item.open,
+{
+background: #00000060;
+/* color: black; */
+/* border: 2px solid black; */
+/* box-shadow: inset 0px -0.75em 0.5em #19311F, 0 0.25em 0.25em #00000048; */
+/* background: linear-gradient(0deg, #364B3B 0%, #19311F 100%); */
+}
+
+.chats-list-item .item-info,
+.roster-list-item .item-info {
+ width: 0;
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+ justify-content: space-between;
+}
+
+.chats-list-item .main-info,
+.roster-list-item .main-info {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+}
+
+.chats-list-item .name,
+.roster-list-item .name {
+ font-weight: bold;
+ font-size: 1.1em;
+}
+
+.chats-list-item .name,
+.roster-list-item .name {
+ font-weight: light;
+}
+
+.chats-list-item .sub-info,
+.roster-list-item .sub-info {
+ display: flex;
+}
+
+.chats-list-item .message-preview {
+ font-weight: light;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ max-width: auto;
+}
+
+.roster-list-item .jid {
+ opacity: 0;
+ font-family: Diolce;
+ font-weight: normal;
+}
+
+.roster-list-item:hover .jid {
+ opacity: 100%;
+}
+
+.avatar {
+ object-fit: contain;
+ border-radius: 50%;
+ width: 48px;
+ height: 48px;
+}
+
+.chats-list-item .avatar,
+.roster-list-item .avatar {
+ width: 48px;
+ height: 48px;
+}
+
+.chats-list-chats,
+.roster-list-roster {
+ flex-grow: 1;
+ /* overflow-y: scroll; */
+}
+
+.open-chat-views {
+ flex: 1 1 100%;
+ display: flex;
+ flex-direction: column;
+ max-height: auto;
+}
+
+.open-chat-view {
+ height: auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: stretch;
+ max-height: auto;
+}
+
+.chat-view-header .avatar {
+ width: 64px;
+ height: 64px;
+}
+
+.chat-view-header {
+ border-width: 0 0 2px 0;
+ display: flex;
+ padding: 16px;
+ gap: 16px;
+}
+
+.chat-view-header h2 {
+ font-family: 'K2D';
+ font-weight: bold;
+}
+
+.new-message-composer {
+ border-width: 2px 0 0 0;
+ width: auto;
+ padding: 16px;
+ max-height: auto;
+}
+
+.new-message-composer .overlay-scroll {
+ max-height: min(32em, 50vh);
+ background-color: #dcdcdc;
+ border: 2px solid black;
+}
+
+[contenteditable]:focus {
+ outline: 0px solid transparent;
+}
+
+.new-message-composer .text-box {
+ padding: 8px;
+ white-space: pre-wrap;
+ color: black;
+ /* overflow-y: auto; */
+ font-size: 16px;
+ margin: 0;
+ height: fit-content;
+}
+
+.grow-wrap {
+ /* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */
+ display: grid;
+}
+
+.grow-wrap::after {
+ /* Note the weird space! Needed to preventy jumpy behavior */
+ content: attr(data-replicated-value) " ";
+ /* This is how textarea text behaves */
+ white-space: pre-wrap;
+ /* Hidden from view, clicks, and screen readers */
+ visibility: hidden;
+ max-height: 5em;
+}
+
+.grow-wrap>textarea {
+ /* You could leave this, but after a user resizes, then it ruins the auto sizing */
+ resize: none;
+ /* Firefox shows scrollbar on growth, you can hide like this. */
+ overflow-y: scroll;
+}
+
+.grow-wrap>textarea,
+.grow-wrap::after {
+ /* Identical styling required!! */
+ border: 2px solid black;
+ padding: 8px;
+ font: inherit;
+
+ /* Place on top of each other */
+ grid-area: 1 / 1 / 2 / 2;
+}
+
+.open-chat-view {
+ flex-grow: 1;
+ max-height: 100%;
+}
+
+.messages-buffer {
+ display: flex;
+ flex-direction: column-reverse;
+ flex-grow: 1;
+ overflow-y: scroll;
+}
+
+
+.messages-buffer .new-day {
+ display: flex;
+ align-items: center;
+ margin: 0.5em 0 0 0;
+ /* margin: 1em -1em; */
+
+
+ &:before,
+ &:after {
+ content: "";
+ flex: 1;
+ height: 2px;
+ margin: 0 1em;
+ background: black;
+ }
+}
+
+.chat-message {
+ display: flex;
+ padding: 4px 0;
+}
+
+.chat-message:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+.chat-message .left {
+ width: 48px;
+ flex: none;
+ padding: 0 8px;
+}
+
+.chat-message .middle {
+ width: auto;
+ flex-grow: 1;
+}
+
+.chat-message .right {
+ width: 16px;
+ padding: 0 8px;
+ flex: none;
+ align-self: end;
+}
+
+.message-info {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+}
+
+.message-text {
+ white-space: pre-wrap;
+}
+
+.chat-message.major {
+ margin-top: 8px;
+}
+
+.chat-message.major .left {
+ height: 48px;
+}
+
+.chat-message.major .middle {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+}
+
+.message-info .message-user-name {
+ font-weight: bold;
+ font-size: 1.1em;
+ margin-bottom: 4px;
+}
+
+.chat-message.major .message-timestamp {
+ font-weight: light;
+ font-size: 0.8em;
+}
+
+.chat-message.minor .message-timestamp {
+ font-weight: light;
+ font-size: 0.7em;
+ /* TODO: better alignment */
+ margin-top: 0.3em;
+}
+
+.chat-message.minor:not(:hover) .message-timestamp {
+ opacity: 0;
+}
+
+.light {
+ filter: invert(99%) sepia(1%) saturate(2689%) hue-rotate(208deg) brightness(106%) contrast(73%);
+}
+
+.chat-message.final .message-delivery,
+.visible {
+ opacity: 100% !important;
+}
+
+.chat-message.final {
+ margin-bottom: 8px;
+}
+
+.chat-message:not(:hover) .message-delivery {
+ opacity: 0;
+}
+
+.sidebar {
+ display: flex;
+ z-index: 1;
+}
+
+.dock {
+ z-index: 30;
+ display: flex;
+ flex-direction: column;
+ border-width: 0 2px 0 0;
+}
+
+
+.dock-item {
+ display: flex;
+ justify-content: center;
+ position: relative;
+ cursor: pointer;
+}
+
+.dock-pill {
+ position: absolute;
+ border-radius: 50%;
+ left: -4px;
+ top: calc(50% - 4px);
+ width: 8px;
+ height: 8px;
+ opacity: 0;
+ background-color: #dcdcdc;
+ transition-duration: 250ms;
+}
+
+.dock-item.open .dock-pill {
+ opacity: 100%;
+}
+
+.dock-item:hover .dock-pill,
+.dock-item.hovering .dock-pill {
+ top: calc(50% - 14px);
+ height: 28px;
+ opacity: 100%;
+}
+
+.dock-item.focused .dock-pill {
+ top: calc(50% - 24px);
+ height: 48px;
+ opacity: 100%;
+}
+
+.dock .shortcuts {
+ padding: 8px 0;
+ border-bottom: 2px solid black;
+}
+
+.dock .shortcuts .dock-item img {
+ width: 64px;
+ height: 64px;
+}
+
+.dock-icon {
+ width: 64px;
+ height: 64px;
+ padding: 4px 8px;
+}
+
+.avatar-with-presence {
+ position: relative;
+ height: fit-content;
+}
+
+.avatar-with-presence>.presence-show-icon {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+}
+
+.dock .pins {
+ flex: 1 1 auto;
+}
+
+.dock .personal {
+ padding: 8px 0;
+ flex: 0 0 auto;
+ border-top: 2px solid black;
+}
+
+.sidebar {
+ position: relative;
+}
+
+.dock .personal .dock-item {
+ padding: 8px 16px
+}
+
+.dock .personal .avatar-with-presence .avatar {
+ width: 48px;
+ height: 48px;
+}
+
+.sidebar-drawer {
+ height: auto;
+ z-index: 5;
+}
+
+.sidebar-hovering-drawer {
+ z-index: 10;
+}
+
+.sidebar .sidebar-hovering-drawer {
+ position: absolute;
+ height: 100%;
+ /* must be width of dock... */
+ left: 82px;
+ transition: left 0.2s ease-out allow-discrete;
+}
+
+@starting-style {
+ .sidebar .sidebar-hovering-drawer {
+ /* TODO: allow to edit this width */
+ left: -318px;
+ }
+}
+
+.sidebar .behind-hovering {
+ width: 402px;
+ transition: width 0.2s ease-out allow-discrete;
+}
+
+@starting-style {
+ .sidebar .behind-hovering {
+ width: 0;
+ }
+}
+
+.overlay {
+ position: relative;
+ z-index: 100;
+}
+
+.overlay-background,
+.modal-background {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+}
+
+.overlay-content {
+ z-index: 101;
+ position: absolute;
+}
+
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ z-index: 150;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: rgba(0, 0, 0, 0.4);
+}
+
+.settings {
+ width: 75vw;
+ height: 75vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.settings-main {
+ height: calc(100% - 56px);
+ display: flex;
+ flex: 1 1 auto;
+}
+
+.settings-page {
+ flex: 1 1 0;
+ overflow-y: auto;
+}
+
+.profile-settings {
+ display: flex;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ flex-direction: row-reverse;
+}
+
+.profile-form {
+ flex: 1 1 auto;
+ margin: 16px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 16px;
+}
+
+.profile-form label {
+ font-size: 1rem;
+}
+
+.profile-form .change-avatar {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 16px;
+}
+
+#client-user-avatar {
+ opacity: 0;
+ position: absolute;
+ z-index: -1;
+}
+
+.profile-preview {
+ margin: 24px;
+ border: 2px solid black;
+ min-width: 240px;
+}
+
+.profile-preview .preview {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.profile-preview h2 {
+ padding: 8px 16px;
+ border-bottom: 2px solid black;
+}
+
+.profile-preview .avatar {
+ width: 64px;
+ height: 64px;
+}
+
+.profile-preview .nick {
+ font-size: 20px;
+ font-weight: bold;
+}
+
+hr {
+ width: 100%;
+ border: 1px solid black;
+}
+
+.profile-preview .preview {
+ padding: 16px;
+}
+
+.modal .overlay {
+ x-index: 200;
+}
+
+.modal .overlay-content {
+ z-index: 201;
+}
+
+.personal .overlay-content {
+ bottom: 0;
+ left: 100%;
+}
+
+.menu {
+ border: 2px solid black;
+ background-color: #dcdcdc;
+ color: #000000;
+ width: 300px;
+}
+
+.menu-item {
+ cursor: pointer;
+ padding: 4px 8px;
+}
+
+.menu-item:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+.personal-status-menu .user {
+ display: flex;
+ gap: 8px;
+ padding: 8px;
+}
+
+.personal-status-menu .user .user-info {
+ display: flex;
+ flex-direction: column;
+}
+
+.personal-status-menu .user .nick {
+ font-size: 1.1em;
+ font-weight: bold;
+}
+
+.personal-status-menu .user .jid {
+ font-family: Diolce;
+}
+
+hr {
+ margin: 0;
+}
+
+.status-edit {
+ padding: 0 8px 8px 8px;
+}
+
+.status-edit select {
+ width: 100%;
+}
+
+.header .header-icon {
+ position: relative;
+ top: -0.2rem;
+ border-radius: 1em;
+ padding: 4px;
+}
+
+.new-chat, .add-contact, .close {
+ cursor: pointer;
+}
+
+.new-chat:hover,
+.new-chat.open,
+.add-contact:hover,
+.add-contact.open,
+.close:hover {
+ background: #00000060;
+}
+
+.add-contact-panel {
+ flex-shrink: 0;
+ max-height: calc(100% - 150px);
+ border-bottom: 2px solid black;
+}
+
+.new-chat-widget form {
+ border: 2px solid black;
+ padding: 4px;
+ gap: 4px;
+ background-color: #dcdcdc;
+ display: flex;
+ flex-direction: column;
+ align-items: end;
+}
+
+.add-contact-menu h3 {
+ margin: 1em 0 0.5em;
+}
+
+.add-contact-menu form {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.add-contact-menu form input {
+ flex: 1 1 auto;
+}
+
+.jid-with-button {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.jid-with-buttons {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 4px;
+ margin-bottom: 8px;
+}
+
+.jid-with-buttons .buttons {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.jid-with-buttons .buttons>* {
+ flex: 1 1 auto;
+}
+
+.jid-with-button .jid {
+ flex: 1 1 auto;
+}
+
+.jid-with-button .button {
+ flex: 0 0 auto;
+}
+
+.icon-with-badge {
+ position: relative;
+}
+
+.badge {
+ position: absolute;
+ top: 0;
+ right: 0;
+ border-radius: 50%;
+ background-color: #C1173C;
+ color: #dcdcdc;
+ height: 1em;
+ min-width: 1em;
+ padding: 2px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.header-icon .badge {
+ height: 8px;
+ width: 8px;
+ min-width: revert;
+}
+
+/* font-families */
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 100;
+ font-display: block;
+ src: url('/assets/fonts/k2d-1.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 100;
+ font-display: block;
+ src: url('/assets/fonts/k2d-2.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 100;
+ font-display: block;
+ src: url('/assets/fonts/k2d-3.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 100;
+ font-display: block;
+ src: url('/assets/fonts/k2d-4.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 200;
+ font-display: block;
+ src: url('/assets/fonts/k2d-5.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 200;
+ font-display: block;
+ src: url('/assets/fonts/k2d-6.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 200;
+ font-display: block;
+ src: url('/assets/fonts/k2d-7.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 200;
+ font-display: block;
+ src: url('/assets/fonts/k2d-8.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 300;
+ font-display: block;
+ src: url('/assets/fonts/k2d-9.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 300;
+ font-display: block;
+ src: url('/assets/fonts/k2d-10.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 300;
+ font-display: block;
+ src: url('/assets/fonts/k2d-11.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 300;
+ font-display: block;
+ src: url('/assets/fonts/k2d-12.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 400;
+ font-display: block;
+ src: url('/assets/fonts/k2d-13.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 400;
+ font-display: block;
+ src: url('/assets/fonts/k2d-14.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 400;
+ font-display: block;
+ src: url('/assets/fonts/k2d-15.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 400;
+ font-display: block;
+ src: url('/assets/fonts/k2d-16.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 500;
+ font-display: block;
+ src: url('/assets/fonts/k2d-17.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 500;
+ font-display: block;
+ src: url('/assets/fonts/k2d-18.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 500;
+ font-display: block;
+ src: url('/assets/fonts/k2d-19.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 500;
+ font-display: block;
+ src: url('/assets/fonts/k2d-20.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 600;
+ font-display: block;
+ src: url('/assets/fonts/k2d-21.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 600;
+ font-display: block;
+ src: url('/assets/fonts/k2d-22.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 600;
+ font-display: block;
+ src: url('/assets/fonts/k2d-23.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 600;
+ font-display: block;
+ src: url('/assets/fonts/k2d-24.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 700;
+ font-display: block;
+ src: url('/assets/fonts/k2d-25.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 700;
+ font-display: block;
+ src: url('/assets/fonts/k2d-26.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 700;
+ font-display: block;
+ src: url('/assets/fonts/k2d-27.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 700;
+ font-display: block;
+ src: url('/assets/fonts/k2d-28.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 800;
+ font-display: block;
+ src: url('/assets/fonts/k2d-29.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 800;
+ font-display: block;
+ src: url('/assets/fonts/k2d-30.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 800;
+ font-display: block;
+ src: url('/assets/fonts/k2d-31.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: italic;
+ font-weight: 800;
+ font-display: block;
+ src: url('/assets/fonts/k2d-32.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 100;
+ font-display: block;
+ src: url('/assets/fonts/k2d-33.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 100;
+ font-display: block;
+ src: url('/assets/fonts/k2d-34.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 100;
+ font-display: block;
+ src: url('/assets/fonts/k2d-35.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 100;
+ font-display: block;
+ src: url('/assets/fonts/k2d-36.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 200;
+ font-display: block;
+ src: url('/assets/fonts/k2d-37.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 200;
+ font-display: block;
+ src: url('/assets/fonts/k2d-38.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 200;
+ font-display: block;
+ src: url('/assets/fonts/k2d-39.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 200;
+ font-display: block;
+ src: url('/assets/fonts/k2d-40.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 300;
+ font-display: block;
+ src: url('/assets/fonts/k2d-41.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 300;
+ font-display: block;
+ src: url('/assets/fonts/k2d-42.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 300;
+ font-display: block;
+ src: url('/assets/fonts/k2d-43.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 300;
+ font-display: block;
+ src: url('/assets/fonts/k2d-44.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 400;
+ font-display: block;
+ src: url('/assets/fonts/k2d-45.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 400;
+ font-display: block;
+ src: url('/assets/fonts/k2d-46.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 400;
+ font-display: block;
+ src: url('/assets/fonts/k2d-47.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 400;
+ font-display: block;
+ src: url('/assets/fonts/k2d-48.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 500;
+ font-display: block;
+ src: url('/assets/fonts/k2d-49.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 500;
+ font-display: block;
+ src: url('/assets/fonts/k2d-50.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 500;
+ font-display: block;
+ src: url('/assets/fonts/k2d-51.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 500;
+ font-display: block;
+ src: url('/assets/fonts/k2d-52.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 600;
+ font-display: block;
+ src: url('/assets/fonts/k2d-53.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 600;
+ font-display: block;
+ src: url('/assets/fonts/k2d-54.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 600;
+ font-display: block;
+ src: url('/assets/fonts/k2d-55.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 600;
+ font-display: block;
+ src: url('/assets/fonts/k2d-56.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 700;
+ font-display: block;
+ src: url('/assets/fonts/k2d-57.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 700;
+ font-display: block;
+ src: url('/assets/fonts/k2d-58.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 700;
+ font-display: block;
+ src: url('/assets/fonts/k2d-59.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 700;
+ font-display: block;
+ src: url('/assets/fonts/k2d-60.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* thai */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 800;
+ font-display: block;
+ src: url('/assets/fonts/k2d-61.woff2') format('woff2');
+ unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
+}
+
+/* vietnamese */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 800;
+ font-display: block;
+ src: url('/assets/fonts/k2d-62.woff2') format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 800;
+ font-display: block;
+ src: url('/assets/fonts/k2d-63.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* latin */
+@font-face {
+ font-family: 'K2D';
+ font-style: normal;
+ font-weight: 800;
+ font-display: block;
+ src: url('/assets/fonts/k2d-64.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Diolce';
+ src: url('/assets/fonts/Diolce-Regular.woff2') format('woff2');
+ font-weight: normal;
+ font-style: italic;
+ font-display: block;
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..25f84d1
--- /dev/null
+++ b/index.html
@@ -0,0 +1,33 @@
+<!--
+SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+
+SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<!DOCTYPE html>
+<html>
+
+<head>
+
+ <!-- Add a plain CSS file: see https://trunkrs.dev/assets/#css -->
+ <!-- If using Tailwind with Leptos CSR, see https://trunkrs.dev/assets/#tailwind instead-->
+ <link data-trunk rel="scss" href="assets/style.scss" />
+
+ <!-- overlay scrollbars -->
+ <link type="text/css" href="/assets/overlayscrollbars.min.css" rel="stylesheet" />
+
+ <!-- Include favicon in dist output: see https://trunkrs.dev/assets/#icon -->
+ <link data-trunk rel="icon" href="assets/bubble.png" />
+
+ <!-- include support for `wasm-bindgen --weak-refs` - see: https://rustwasm.github.io/docs/wasm-bindgen/reference/weak-references.html -->
+ <link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs />
+
+ <link data-trunk rel="copy-dir" href="assets" />
+
+ <title>Macaw IM</title>
+
+</head>
+
+<body></body>
+
+</html>
diff --git a/README.md b/notes/README.md
index 76a4853..76a4853 100644
--- a/README.md
+++ b/notes/README.md
diff --git a/components.md b/notes/components.md
index f03c5a8..0c3c2f3 100644
--- a/components.md
+++ b/notes/components.md
@@ -1,6 +1,6 @@
# components
-## basic building blocks: can use iced widgets for now
+## basic building blocks?
- button
- search box (1 line)
diff --git a/icons.md b/notes/icons.md
index 9c3dd89..d882da2 100644
--- a/icons.md
+++ b/notes/icons.md
@@ -20,5 +20,4 @@
- [ ] mobile phone
- [ ] default avatar
- [ ] call
-
- [ ] add
diff --git a/ideas.md b/notes/ideas.md
index 23d2a89..de60fbe 100644
--- a/ideas.md
+++ b/notes/ideas.md
@@ -1,3 +1,7 @@
+# ideas
+
+this document is full of ideas in no order but all eventually to be implemented
+
- e2ee
- default omemo and no mam for 1to1 and private group chats
- default archiving and no e2ee for guilds and public group chats
@@ -7,12 +11,23 @@
- moving of xmpp accounts, download of account data from server
- threads in different windows, threads menu, select thread separately under person in chat list
- configure if threaded messages come under the main chat or not
+ - threads visible under right-click menu on chat
+ - can pin threads to sidebar too
- proper oauth
- guilds
- voice channels
- video streaming
- broadcast rooms
-- request message sync between devices (chat and global level) and with particular contacts (on a chat level)
+- request encrypted message sync between devices (chat and global level) and with particular contacts (on a chat level) e.g. to recover chat history
+- xep for deciding upon mutual chat settings (what version of omemo is being used, chat background, deleting messages, etc)
+- message deletion explanation and warnings
+ - notify if user has logged in with a client that does not support message deletion
+- social features
+ - can check in (let circles of your contacts know where you are or are planning to be irl)
+ - bubble map/venn diagram grouping people by activity and location both irl and online
+ - open to meet/chat broadcast
+ - detailed mood/status
+ - some kind of away message system
- pass the aux
- when on call, can start an aux session to share application audio in high quality, and pass around with audio status shared (track etc.) in call.
- (when mobile is good): proper location sharing map
@@ -21,16 +36,16 @@
- may include assets for borders, backgrounds, etc.
- icon themes
- app icon
- - emojis
+ - emoji
- status icons
- general icons (e.g. call button, send message, etc)
- menu bar icons
- sound theme
- - message style (through custom blitz render), html and css templates
+ - message style, html and css templates
- much later: main application layout, done through editing an xml file with a custom schema
- adium+-level themeability
- - ichat theme
- - window layout customisation as part of theme
+ - ichat theme as an example...
+ - window layout customisation as part of theme?
- media chat features:
- investigate whether it is worth encrypting all media chats using dtls-srtp
- ichat photo booth video chat filters
@@ -55,20 +70,20 @@
- greylisting
- vouches as an anti-spam measure
- fallback/backup servers through friends
+ - xmpp accounts more like relays?
- composable moderation
- subscriptions to labelers (like bsky)
- advanced privacy settings
- allow certain profile information to only be seen by certain people, in certain contexts
- how open your inbox is
- profile styling/customization
- - profile colors, fonts, background, profile border, effects
+ - profile colors, fonts, background, profile border, effects - just custom css basically
- message styling?
- per channel/guild styling and privacy settings
-- guild/channel level styling
+- guild/channel-level styling
- pinned messages
- mam extension for non-e2e channels
- non-e2e channels rely on mam first and foremost to save client storage
-- encrypted message history sync across devices
- polls
- server admin menu
- (this one is so stupid) caw button, akin to a poke feature, as a way to nudge if people are available to hang out, or to announce you are available to hang out (can configure who, in this case, would be notified)
diff --git a/old_code.md b/old_code.md
deleted file mode 100644
index acf7bfa..0000000
--- a/old_code.md
+++ /dev/null
@@ -1,460 +0,0 @@
-// pub struct State<'a> {
-// // open_chat: Option<OpenChat<'a>>,
-// chats: Vec<BorrowedChat<'a>>,
-// roster: Vec<BorrowedContact<'a>>,
-// }
-
-// impl Macaw {
-// pub fn state(&self) -> State {
-// let chats = self
-// .chats
-// .iter()
-// .map(|(_, chat)| {
-// let user = chat.user.as_ref().borrow();
-// let contact = self.roster.get(&user.inner.jid);
-// let latest_message = match &chat.message {
-// Some(the_message) => {
-// let message: Ref<'_, OwnedMessage> = the_message.as_ref().borrow();
-// let user = {
-// let user = message.user.as_ref().borrow();
-// let contact = self.roster.get(&user.inner.jid);
-
-// BorrowedUser {
-// inner: user,
-// contact,
-// }
-// };
-// Some(BorrowedMessage {
-// inner: message,
-// user,
-// })
-// }
-// None => None,
-// };
-// BorrowedChat {
-// inner: &chat,
-// user: BorrowedUser {
-// inner: user,
-// contact,
-// },
-// latest_message,
-// }
-// })
-// .collect();
-// State {
-// // open_chat: todo!(),
-// chats,
-// roster: todo!(),
-// }
-// }
-// }
-
-// pub enum Node<'a> {
-// User(BorrowedUser<'a>),
-// Message(BorrowedMessage<'a>),
-// Contact(BorrowedContact<'a>),
-// Chat(BorrowedChat<'a>),
-// OpenChat(OpenChat<'a>),
-// }
-
-// pub enum Orphan {
-// User(JID),
-// Message(Uuid),
-// }
-
-// pub struct BorrowedUser<'a> {
-// inner: Ref<'a, OwnedUser>,
-// // inner: &'a OwnedUser,
-// contact: Option<&'a OwnedContact>,
-// }
-
-// pub struct BorrowedMessage<'a> {
-// inner: Ref<'a, OwnedMessage>,
-// // user: Rc<RefCell<User>>,
-// user: BorrowedUser<'a>,
-// }
-
-// pub struct BorrowedContact<'a> {
-// inner: &'a OwnedContact,
-// user: BorrowedUser<'a>,
-// }
-
-// pub struct BorrowedChat<'a> {
-// inner: &'a OwnedChat,
-// // user: Rc<RefCell<OwnedUser>>,
-// user: BorrowedUser<'a>,
-// // latest_message: Option<Rc<RefCell<OwnedMessage>>>,
-// latest_message: Option<BorrowedMessage<'a>>,
-// }
-
-// pub struct OpenChat<'a> {
-// chat: BorrowedChat<'a>,
-// messages: Vec<BorrowedMessage<'a>>,
-// }
-
-// there are two states: an editing state and a reading state.
-// because the state is self-referential, each time there is a change, the state needs to be recalculated.
-// pub struct State<'a> {
-// // open_chat: Option<OpenChat<'a>>,
-// chats: Vec<BorrowedChat<'a>>,
-// roster: Vec<BorrowedContact<'a>>,
-// }
-
-// pub struct AState {
-// open_chat: Option<OwnedOpenChat>,
-// chats: HashMap<JID, (Chat, (User, Option<Contact>), Option<ChatMessage>)>,
-// roster: HashMap<JID, (Contact, User)>,
-// }
-
-// pub struct OwnedOpenChat {
-// chat: (Chat, (User, Option<Contact>)),
-// messages: IndexMap<Uuid, (ChatMessage, (User, Option<Contact>))>,
-// }
-
-// pub struct MacawUser {
-// inner: User,
-// contact: Option<Contact>,
-// }
-
-// impl Deref for MacawUser {
-
-// }
-
-// other option is forget about all this bullshit and just build the relations graph again after each update?
-// impl Macaw {
-// pub async fn insert_contact(
-// &mut self,
-// contact: Contact,
-// ) -> Result<(), CommandError<DatabaseError>> {
-// let user_jid = contact.user_jid.clone();
-// if !self.users.contains_key(&user_jid) {
-// // TODO: all methods only on logged-in client
-// match &self.client {
-// Account::LoggedIn(client) => {
-// let user = client.get_user(user_jid.clone()).await?;
-// self.users.insert(user_jid, user);
-// }
-// Account::LoggedOut(login_modal) => todo!(),
-// }
-// } else {
-// // up the dependency count
-// }
-// self.roster.insert(contact.user_jid.clone(), contact);
-// Ok(())
-// }
-
-// pub fn get_contact(&self, jid: &JID) -> Option<(&Contact, &User)> {}
-
-// pub fn get_contact_mut(&mut self, jid: &JID) -> Option<&mut Contact> {}
-
-// pub fn remove_contact(&mut self, jid: JID) -> Option<Contact> {
-// self.roster.
-// }
-
-// // updating is fine to do directly on any part of the store.
-// // if a contact is retrieved, it is guaranteed that there is a user in the store, because when the contact was inserted, a user was added to the user store, and the userstore is a droppable store that does not allow you to remove items if there are other things depending on it.
-// }
-
-// pub struct MacawContact<'a> {
-// inner: Contact,
-// user: &'a User,
-// }
-
-// messages must be tombstoned because of this
-// items are never directly added to or removed from a droppablestore
-// remove can only be called from a store that called insert
-// if a store calls insert it must call remove if something is removed from that store
-pub struct DroppableStore<K: Clone, V: Clone>(HashMap<K, (usize, V)>);
-
-impl<K: Clone, V: Clone> DroppableStore<K, V> {
- pub fn capacity(&self) -> usize {
- self.0.capacity()
- }
-
- pub fn contains_key<Q: ?Sized>(&self, k: &Q) -> bool
- where
- Q: std::hash::Hash + Eq,
- K: Borrow<Q> + std::hash::Hash + Eq,
- {
- self.0.contains_key(k)
- }
-
- pub fn get<Q>(&self, k: &Q) -> Option<&V>
- where
- Q: std::hash::Hash + Eq,
- K: Borrow<Q> + std::hash::Hash + Eq,
- {
- self.0.get(k).map(|(_, v)| v)
- }
-
- pub fn get_key_value<Q>(&self, k: &Q) -> Option<(&K, &V)>
- where
- Q: std::hash::Hash + Eq + ?Sized,
- K: Borrow<Q> + std::hash::Hash + Eq,
- {
- self.0.get_key_value(k).map(|(k, (_, v))| (k, v))
- }
-
- pub fn get_mut<Q>(&mut self, k: &Q) -> Option<&mut V>
- where
- Q: std::hash::Hash + Eq + ?Sized,
- K: Borrow<Q> + std::hash::Hash + Eq,
- {
- self.0.get_mut(k).map(|(_, v)| v)
- }
-
- pub fn insert(&mut self, k: K, v: V) -> Option<V>
- where
- K: std::hash::Hash + Eq,
- {
- if let Some((count, old_v)) = self.0.get_mut(&k) {
- *count += 1;
- let output = std::mem::replace(old_v, v);
- Some(output)
- } else {
- self.0.insert(k, (1, v));
- None
- }
- }
-
- pub fn is_empty(&self) -> bool {
- self.0.is_empty()
- }
-
- pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
- self.0.iter().map(|(k, (_, v))| (k, v))
- }
-
- pub fn iter_mut(&mut self) -> impl Iterator<Item = (&K, &mut V)> {
- self.0.iter_mut().map(|(k, (_, v))| (k, v))
- }
-
- pub fn keys(&self) -> impl Iterator<Item = &K> {
- self.0.keys()
- }
-
- pub fn len(&self) -> usize {
- self.0.len()
- }
-
- pub fn new() -> Self {
- Self(HashMap::new())
- }
-
- pub fn remove<Q>(&mut self, k: &Q) -> Option<V>
- where
- K: Borrow<Q> + std::hash::Hash + Eq,
- Q: std::hash::Hash + Eq + ?Sized,
- {
- if let Some((count, old_v)) = self.0.get_mut(&k) {
- if *count == 1 {
- self.0.remove(&k).map(|(_, v)| v)
- } else {
- *count = *count - 1;
- Some(old_v.clone())
- }
- } else {
- None
- }
- }
-
- pub fn remove_entry<Q>(&mut self, k: &K) -> Option<(K, V)>
- where
- K: std::hash::Hash + Eq,
- {
- if let Some((count, old_v)) = self.0.get_mut(&k) {
- if *count == 1 {
- self.0.remove_entry(&k).map(|(k, (_, v))| (k, v))
- } else {
- *count = *count - 1;
- Some((k.clone(), old_v.clone()))
- }
- } else {
- None
- }
- }
-
- pub fn reserve(&mut self, additional: usize)
- where
- K: std::hash::Hash + Eq,
- {
- self.0.reserve(additional)
- }
-
- pub fn shrink_to(&mut self, min_capacity: usize)
- where
- K: std::hash::Hash + Eq,
- {
- self.0.shrink_to(min_capacity)
- }
-
- pub fn shrink_to_fit(&mut self)
- where
- K: std::hash::Hash + Eq,
- {
- self.0.shrink_to_fit()
- }
-
- pub fn try_reserve(
- &mut self,
- additional: usize,
- ) -> Result<(), std::collections::TryReserveError>
- where
- K: std::hash::Hash + Eq,
- {
- self.0.try_reserve(additional)
- }
-
- pub fn values(&self) -> impl Iterator<Item = &V> {
- self.0.values().map(|(_, v)| v)
- }
-
- pub fn values_mut(&mut self) -> impl Iterator<Item = &mut V> {
- self.0.values_mut().map(|(_, v)| v)
- }
-
- pub fn with_capacity(capacity: usize) -> Self {
- Self(HashMap::with_capacity(capacity))
- }
-}
-
-// #[derive(Debug, Clone)]
-// pub struct MacawChat {
-// inner: Arc<Mutex<Chat>>,
-// jid: JID,
-// latest_message: Option<MacawChatMessage>,
-// store: MacawChatStore,
-// }
-
-// impl Drop for MacawChat {
-// fn drop(&mut self) {
-// let mut store = match self.store.0.lock() {
-// Ok(s) => s,
-// Err(s) => s.into_inner(),
-// };
-// if Arc::<Mutex<Chat>>::strong_count(&self.inner) == 2 {
-// store.shift_remove(&self.jid);
-// }
-// }
-// }
-
-// #[derive(Debug, Clone)]
-// pub struct MacawChatStore(Arc<Mutex<IndexMap<JID, MacawChat>>>);
-
-// #[derive(Debug, Clone)]
-// pub struct MacawChatMessage {
-// inner: Arc<Mutex<ChatMessage>>,
-// id: Uuid,
-// user: BorrowedUser,
-// store: MacawChatMessageStore,
-// }
-
-// impl Drop for MacawChatMessage {
-// fn drop(&mut self) {
-// let mut store = match self.store.0.lock() {
-// Ok(s) => s,
-// Err(s) => s.into_inner(),
-// };
-// if Rc::<ChatMessage>::strong_count(&self.inner) == 1 {
-// store.shift_remove(&self.id)
-// }
-// }
-// }
-
-// #[derive(Debug, Clone)]
-// pub struct MacawChatMessageStore(HashMap<Uuid, MacawChatMessage>);
-
-// impl Deref for MacawChatMessageStore {
-// type Target = HashMap<Uuid, (ChatMessage, BorrowedUser)>;
-
-// fn deref(&self) -> &Self::Target {
-// &self.0
-// }
-// }
-
-// impl DerefMut for MacawChatMessageStore {
-// fn deref_mut(&mut self) -> &mut Self::Target {
-// &mut self.0
-// }
-// }
-
-// #[derive(Debug, Clone)]
-// pub struct BorrowedUser {
-// inner: Rc<User>,
-// contact: Option<Box<BorrowedContact>>,
-// store: Rc<RefCell<MacawUserStore>>,
-// }
-
-// impl Drop for BorrowedUser {
-// fn drop(&mut self) {
-// if Rc::<User>::strong_count(&self.inner) == 1 {
-// self.store.borrow_mut().remove(&self.inner.jid);
-// }
-// }
-// }
-
-// impl Deref for BorrowedUser {
-// type Target = User;
-
-// fn deref(&self) -> &Self::Target {
-// self.inner.as_ref()
-// }
-// }
-
-// #[derive(Debug)]
-// pub struct MacawUserStore(HashMap<JID, (User, Option<BorrowedContact>)>);
-
-// impl Deref for MacawUserStore {
-// type Target = HashMap<JID, (User, Option<BorrowedContact>)>;
-
-// fn deref(&self) -> &Self::Target {
-// &self.0
-// }
-// }
-
-// impl DerefMut for MacawUserStore {
-// fn deref_mut(&mut self) -> &mut Self::Target {
-// &mut self.0
-// }
-// }
-
-// #[derive(Debug, Clone)]
-// pub struct BorrowedContact {
-// inner: Rc<Contact>,
-// user: Box<BorrowedUser>,
-// store: Rc<RefCell<MacawRosterStore>>,
-// }
-
-// impl Drop for BorrowedContact {
-// fn drop(&mut self) {
-// if Rc::<Contact>::strong_count(&self.inner) == 1 {
-// self.store.borrow_mut().remove(&self.inner.user_jid);
-// }
-// }
-// }
-
-// impl Deref for BorrowedContact {
-// type Target = Contact;
-
-// fn deref(&self) -> &Self::Target {
-// self.inner.as_ref()
-// }
-// }
-
-// #[derive(Debug)]
-// pub struct MacawRosterStore(HashMap<JID, (Contact, BorrowedUser)>);
-
-// impl Deref for MacawRosterStore {
-// type Target = HashMap<JID, (Contact, BorrowedUser)>;
-
-// fn deref(&self) -> &Self::Target {
-// &self.0
-// }
-// }
-
-// impl DerefMut for MacawRosterStore {
-// fn deref_mut(&mut self) -> &mut Self::Target {
-// &mut self.0
-// }
-// }
-
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644
index 0000000..76a88f6
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,6 @@
+# SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+[toolchain]
+channel = "nightly"
diff --git a/src/chat.rs b/src/chat.rs
new file mode 100644
index 0000000..e40119f
--- /dev/null
+++ b/src/chat.rs
@@ -0,0 +1,90 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::ops::{Deref, DerefMut};
+
+use filamento::{chat::Chat, user::User};
+use jid::BareJID;
+use leptos::prelude::*;
+use reactive_stores::ArcStore;
+
+use crate::{
+ state_store::{StateListener, StateStore},
+ user::{ArcMacawUser, MacawUser},
+};
+
+#[derive(Clone, Copy)]
+pub struct MacawChat {
+ pub chat: ArenaItem<StateListener<BareJID, ArcStore<Chat>>>,
+ pub user: MacawUser,
+ // user: StateListener<BareJID, ArcStore<User>>,
+}
+
+impl MacawChat {
+ pub fn get(&self) -> ArcStore<Chat> {
+ self.try_get_value().unwrap().get()
+ }
+}
+
+impl Deref for MacawChat {
+ type Target = ArenaItem<StateListener<BareJID, ArcStore<Chat>>>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.chat
+ }
+}
+
+impl DerefMut for MacawChat {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.chat
+ }
+}
+
+impl From<ArcMacawChat> for MacawChat {
+ fn from(value: ArcMacawChat) -> Self {
+ Self {
+ chat: ArenaItem::new_with_storage(value.chat),
+ user: value.user.into(),
+ }
+ }
+}
+
+impl From<MacawChat> for ArcMacawChat {
+ fn from(value: MacawChat) -> Self {
+ Self {
+ chat: value.chat.try_get_value().unwrap(),
+ user: value.user.into(),
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct ArcMacawChat {
+ pub chat: StateListener<BareJID, ArcStore<Chat>>,
+ pub user: ArcMacawUser,
+}
+
+impl ArcMacawChat {
+ pub async fn got_chat_and_user(chat: Chat, user: User) -> Self {
+ let chat_state_store: StateStore<BareJID, ArcStore<Chat>> =
+ use_context().expect("no chat state store");
+ let chat = chat_state_store.store(chat.correspondent.clone(), ArcStore::new(chat));
+ let user = ArcMacawUser::got_user(user).await;
+ Self { chat, user }
+ }
+}
+
+impl Deref for ArcMacawChat {
+ type Target = StateListener<BareJID, ArcStore<Chat>>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.chat
+ }
+}
+
+impl DerefMut for ArcMacawChat {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.chat
+ }
+}
diff --git a/src/client.rs b/src/client.rs
new file mode 100644
index 0000000..02ee537
--- /dev/null
+++ b/src/client.rs
@@ -0,0 +1,36 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::{
+ ops::{Deref, DerefMut},
+ sync::Arc,
+};
+
+use jid::BareJID;
+use leptos::prelude::*;
+
+use crate::files::Files;
+
+#[derive(Clone)]
+pub struct Client {
+ // TODO: not pub
+ pub client: filamento::Client<Files>,
+ pub resource: ArcRwSignal<Option<String>>,
+ pub jid: Arc<BareJID>,
+ pub file_store: Files,
+}
+
+impl Deref for Client {
+ type Target = filamento::Client<Files>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.client
+ }
+}
+
+impl DerefMut for Client {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.client
+ }
+}
diff --git a/src/components/avatar.rs b/src/components/avatar.rs
new file mode 100644
index 0000000..11d2097
--- /dev/null
+++ b/src/components/avatar.rs
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use filamento::{presence::PresenceType, user::User};
+use leptos::prelude::*;
+use reactive_stores::Store;
+
+use crate::{
+ components::icon::{IconComponent, show_to_icon},
+ icon::Icon,
+ user::{MacawUser, get_avatar},
+ user_presences::UserPresences,
+};
+
+#[component]
+pub fn AvatarWithPresence(user: MacawUser) -> impl IntoView {
+ let user_presences: Store<UserPresences> = use_context().expect("no user presences in context");
+ let presence = move || {
+ user_presences
+ .write()
+ .get_user_presences(&user.get().read().jid)
+ .read()
+ .presence()
+ };
+ let show_icon = move || {
+ presence()
+ .map(|(_, presence)| match presence.presence {
+ PresenceType::Online(online) => {
+ if let Some(show) = online.show {
+ Some(show_to_icon(show))
+ } else {
+ Some(Icon::Available16Color)
+ }
+ }
+ PresenceType::Offline(offline) => None,
+ })
+ .unwrap_or_default()
+ };
+
+ view! {
+ <div class="avatar-with-presence">
+ <img class="avatar" src=move || user.avatar().get() />
+ {move || {
+ if let Some(icon) = show_icon() {
+ view! { <IconComponent icon=icon class:presence-show-icon=true /> }.into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ </div>
+ }
+}
diff --git a/src/components/chat_header.rs b/src/components/chat_header.rs
new file mode 100644
index 0000000..3fb5df8
--- /dev/null
+++ b/src/components/chat_header.rs
@@ -0,0 +1,26 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use filamento::user::UserStoreFields;
+use leptos::prelude::*;
+use reactive_stores::ArcStore;
+
+use crate::{chat::MacawChat, components::avatar::AvatarWithPresence, user::get_name};
+
+#[component]
+pub fn ChatViewHeader(chat: MacawChat) -> impl IntoView {
+ let name = move || get_name(chat.user.get().into(), true);
+ let jid = move || chat.user.get().jid().read().to_string();
+
+ view! {
+ <div class="chat-view-header panel">
+ {move || {
+ view! { <AvatarWithPresence user=chat.user /> }
+ }} <div class="user-info">
+ <h2 class="name">{name}</h2>
+ <h3>{jid}</h3>
+ </div>
+ </div>
+ }
+}
diff --git a/src/components/chats_list.rs b/src/components/chats_list.rs
new file mode 100644
index 0000000..73ffdff
--- /dev/null
+++ b/src/components/chats_list.rs
@@ -0,0 +1,156 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use chats_list_item::ChatsListItem;
+use indexmap::IndexMap;
+use jid::BareJID;
+use js_sys::{wasm_bindgen::UnwrapThrowExt, Object, Reflect, JSON};
+use leptos::{html::Div, prelude::*};
+use overlay_scrollbars::OverlayScrollbars;
+use tracing::debug;
+
+use crate::{
+ chat::{ArcMacawChat, MacawChat},
+ client::Client,
+ components::{icon::IconComponent, new_chat::NewChatWidget, overlay::Overlay},
+ icon::Icon,
+ message::{ArcMacawMessage, MacawMessage},
+ message_subscriptions::MessageSubscriptions,
+};
+
+mod chats_list_item;
+
+#[component]
+pub fn ChatsList() -> impl IntoView {
+ let (chats, set_chats) = signal(IndexMap::new());
+
+ let load_chats = LocalResource::new(move || async move {
+ let client = use_context::<Client>().expect("client not in context");
+ let chats = client
+ .get_chats_ordered_with_latest_messages_and_users()
+ .await
+ .map_err(|e| e.to_string());
+ match chats {
+ Ok(c) => {
+ let mut chats = IndexMap::new();
+ for ((chat, chat_user), (message, message_user)) in c {
+ chats.insert(
+ chat.correspondent.clone(),
+ (
+ ArcMacawChat::got_chat_and_user(chat, chat_user).await,
+ ArcMacawMessage::got_message_and_user(message, message_user).await,
+ ),
+ );
+ }
+ set_chats.set(chats);
+ }
+ Err(_) => {
+ // TODO: show error message at top of chats list
+ }
+ }
+ });
+
+ let (open_new_chat, set_open_new_chat) = signal(false);
+
+ // TODO: filter new messages signal
+ let new_messages_signal: RwSignal<MessageSubscriptions> = use_context().unwrap();
+ let (sub_id, set_sub_id) = signal(None);
+ let _load_new_messages = LocalResource::new(move || async move {
+ load_chats.await;
+ let (sub_id, mut new_messages) = new_messages_signal.write().subscribe_all();
+ set_sub_id.set(Some(sub_id));
+ while let Some((to, new_message)) = new_messages.recv().await {
+ debug!("got new message in let");
+ let mut chats = set_chats.write();
+ if let Some((chat, _latest_message)) = chats.shift_remove(&to) {
+ // TODO: check if new message is actually latest message
+ debug!("chat existed");
+ debug!(
+ "new message: {}",
+ new_message.message.get().read().body.body
+ );
+ chats.insert_before(0, to, (chat.clone(), new_message));
+ debug!("done setting");
+ } else {
+ debug!("the chat didn't exist");
+ let client = use_context::<Client>().expect("client not in context");
+ let chat = client.get_chat(to.clone()).await.unwrap();
+ let user = client.get_user(to.clone()).await.unwrap();
+ debug!("before got chat");
+ let chat = ArcMacawChat::got_chat_and_user(chat, user).await;
+ debug!("after got chat");
+ chats.insert_before(0, to, (chat, new_message));
+ debug!("done setting");
+ }
+ }
+ debug!("set the new message");
+ });
+ on_cleanup(move || {
+ if let Some(sub_id) = sub_id.get_untracked() {
+ new_messages_signal.write().unsubscribe_all(sub_id);
+ }
+ });
+
+ let chats_list: NodeRef<Div> = NodeRef::new();
+ let chats_list_viewport: NodeRef<Div> = NodeRef::new();
+
+ let _scrollbars = Effect::new(move |_| {
+ if let Some(buffer) = chats_list.get() {
+ if let Some(viewport) = chats_list_viewport.get() {
+ let scrollbars_obj = Object::new();
+ // Reflect::set(&scrollbars_obj, &"theme".into(), &"os-macaw".into()).unwrap_throw();
+ // Reflect::set(&scrollbars_obj, &"autoHide".into(), &"leave".into()).unwrap_throw();
+ let options_obj = Object::new();
+ // Reflect::set(&options_obj, &"scrollbars".into(), &scrollbars_obj).unwrap_throw();
+
+ let elements_obj = Object::new();
+ Reflect::set(&elements_obj, &"viewport".into(), &viewport.into()).unwrap_throw();
+ let element_obj = Object::new();
+ Reflect::set(&elements_obj, &"elements".into(), &elements_obj).unwrap_throw();
+ Reflect::set(&element_obj, &"target".into(), &buffer.into()).unwrap_throw();
+ // let element = Object::define_property(&Object::define_property(&Object::new(), &"target".into(), &buffer.into()), &"elements".into(), &Object::define_property(&Object::new(), &"viewport".into(), &viewport.into()));
+ debug!("scrollable element: {}", JSON::stringify(&element_obj.clone().into()).unwrap_throw());
+ OverlayScrollbars(element_obj, options_obj);
+ }
+ }
+ });
+
+ view! {
+ <div class="chats-list panel">
+ // TODO: update icon, tooltip on hover.
+ <div class="header">
+ <h2>Chats</h2>
+ <div class="new-chat header-icon" class:open=open_new_chat>
+ <IconComponent
+ icon=Icon::NewBubble24
+ on:click=move |_| set_open_new_chat.update(|state| *state = !*state)
+ />
+ {move || {
+ if *open_new_chat.read() {
+ view! {
+ <Overlay set_open=set_open_new_chat>
+ <NewChatWidget set_open_new_chat />
+ </Overlay>
+ }
+ .into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ </div>
+ </div>
+ <div class="overlay-scroll" node_ref=chats_list>
+ <div class="chats-list-chats" node_ref=chats_list_viewport>
+ <For
+ each=move || chats.get()
+ key=|chat| chat.1.1.message.get().read().id
+ let(chat)
+ >
+ <ChatsListItem chat=chat.1.0.into() message=chat.1.1.into() />
+ </For>
+ </div>
+ </div>
+ </div>
+ }
+}
diff --git a/src/components/chats_list/chats_list_item.rs b/src/components/chats_list/chats_list_item.rs
new file mode 100644
index 0000000..3e18dbe
--- /dev/null
+++ b/src/components/chats_list/chats_list_item.rs
@@ -0,0 +1,93 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::ops::Deref;
+
+use chrono::Local;
+use filamento::{
+ chat::{Chat, ChatStoreFields, Message, MessageStoreFields},
+ user::User,
+};
+use leptos::prelude::*;
+use reactive_stores::{ArcStore, Store};
+use tracing::debug;
+
+use crate::{
+ chat::MacawChat,
+ components::{avatar::AvatarWithPresence, sidebar::Open},
+ message::MacawMessage,
+ open_chats::{OpenChatsPanel, OpenChatsPanelStoreFields},
+ user::get_name,
+};
+
+#[component]
+pub fn ChatsListItem(chat: MacawChat, message: MacawMessage) -> impl IntoView {
+ let name = move || get_name(chat.user.get().into(), true);
+
+ // TODO: store fine-grained reactivity
+ let latest_message_body = move || message.get().body().get().body;
+ let open_chats: Store<OpenChatsPanel> =
+ use_context().expect("no open chats panel store in context");
+
+ let open_chat = move |_| {
+ debug!("opening chat");
+ open_chats.update(|open_chats| open_chats.open(chat.into()));
+ // open_chats.update(|open_chats| open_chats.open(chat.chat.try_get_value().unwrap().get().clone()));
+ };
+
+ let open = move || {
+ if let Some(open_chat) = &*open_chats.chat_view().read() {
+ debug!("got open chat: {:?}", open_chat);
+ if *open_chat == *chat.get().correspondent().read() {
+ return Open::Focused;
+ }
+ }
+ if let Some(_backgrounded_chat) = open_chats
+ .chats()
+ .read()
+ .get(chat.get().correspondent().read().deref())
+ {
+ return Open::Open;
+ }
+ Open::Closed
+ };
+ let focused = move || open().is_focused();
+ let open = move || open().is_open();
+
+ let date = move || message.get().timestamp().read().naive_local();
+ let now = move || Local::now().naive_local();
+ let timeinfo = move || {
+ if date().date() == now().date() {
+ // TODO: localisation/config
+ date().time().format("%H:%M").to_string()
+ } else {
+ date().date().format("%d/%m").to_string()
+ }
+ };
+
+ view! {
+ <div
+ class="chats-list-item"
+ class:open=move || open()
+ class:focused=move || focused()
+ on:click=open_chat
+ >
+ {move || {
+ view! { <AvatarWithPresence user=chat.user /> }
+ }}
+ <div class="item-info">
+ <div class="main-info">
+ <p class="name">{name}</p>
+ <p class="timestamp">{timeinfo}</p>
+ </div>
+ <div class="sub-info">
+ <p class="message-preview">{latest_message_body}</p>
+ <p>
+ <!-- "TODO: delivery or unread state" -->
+ </p>
+ </div>
+ </div>
+ </div>
+ }
+}
diff --git a/src/components/icon.rs b/src/components/icon.rs
new file mode 100644
index 0000000..73b0f5d
--- /dev/null
+++ b/src/components/icon.rs
@@ -0,0 +1,64 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use filamento::{chat::Delivery, presence::Show};
+use leptos::prelude::*;
+
+use crate::icon::Icon;
+
+// TODO: rename
+#[component]
+pub fn IconComponent(icon: Icon) -> impl IntoView {
+ view! {
+ <img
+ class:light=icon.light()
+ class:icon=true
+ style=move || format!("height: {}px; width: {}px", icon.size(), icon.size())
+ src=move || icon.src()
+ />
+ }
+}
+
+pub fn show_to_icon(show: Show) -> Icon {
+ match show {
+ Show::Away => Icon::Away16Color,
+ Show::Chat => Icon::Chat16Color,
+ Show::DoNotDisturb => Icon::Dnd16Color,
+ Show::ExtendedAway => Icon::Xa16Color,
+ }
+}
+
+#[component]
+pub fn Delivery(delivery: Delivery) -> impl IntoView {
+ match delivery {
+ // TODO: proper icon coloring/theming
+ Delivery::Sending => {
+ view! { <IconComponent class:visible=true class:light=true icon=Icon::Sending16 /> }
+ .into_any()
+ }
+ Delivery::Written => {
+ view! { <IconComponent class:light=true icon=Icon::Sent16 /> }.into_any()
+ }
+ // TODO: message receipts
+ // Delivery::Written => view! {}.into_any(),
+ Delivery::Sent => view! { <IconComponent class:light=true icon=Icon::Sent16 /> }.into_any(),
+ Delivery::Delivered => {
+ view! { <IconComponent class:light=true icon=Icon::Delivered16 /> }.into_any()
+ }
+ // TODO: check if there is also the icon class
+ Delivery::Read => {
+ view! { <IconComponent class:light=true class:read=true icon=Icon::Delivered16 /> }
+ .into_any()
+ }
+ Delivery::Failed => {
+ view! { <IconComponent class:visible=true class:light=true icon=Icon::Error16Color /> }
+ .into_any()
+ }
+ // TODO: queued icon
+ Delivery::Queued => {
+ view! { <IconComponent class:visible=true class:light=true icon=Icon::Sending16 /> }
+ .into_any()
+ }
+ }
+}
diff --git a/src/components/message.rs b/src/components/message.rs
new file mode 100644
index 0000000..83a4bad
--- /dev/null
+++ b/src/components/message.rs
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use filamento::chat::MessageStoreFields;
+use leptos::prelude::*;
+use reactive_stores::{ArcStore, Store};
+
+use crate::{
+ message::MacawMessage,
+ user::{get_avatar, get_name, NO_AVATAR},
+};
+
+use super::icon::Delivery;
+
+#[component]
+pub fn Message(message: MacawMessage, major: bool, r#final: bool, new_day: bool) -> impl IntoView {
+ let name = move || get_name(message.user.get().into(), false);
+
+ // TODO: chrono-humanize?
+ // TODO: if final, show delivery not only on hover.
+ // {move || message_message.delivery().read().map(|delivery| delivery.to_string()).unwrap_or_default()}
+ view! {
+ {move || {
+ if major {
+ view! {
+ <div class:final=r#final class="chat-message major">
+ <div class="left">
+ <Transition fallback=|| view! { <img class="avatar" src=NO_AVATAR /> }>
+ <img class="avatar" src=move || message.user.avatar().get() />
+ </Transition>
+ </div>
+ <div class="middle">
+ <div class="message-info">
+ <div class="message-user-name">{name}</div>
+ <div class="message-timestamp">
+ {move || {
+ message.get().timestamp().read().format("%H:%M").to_string()
+ }}
+ </div>
+ </div>
+ <div class="message-text">
+ {move || message.get().body().read().body.clone()}
+ </div>
+ </div>
+ <div class="right message-delivery">
+ {move || {
+ message
+ .get()
+ .delivery()
+ .get()
+ .map(|delivery| {
+ view! { <Delivery class:light=true delivery /> }
+ })
+ }}
+ </div>
+ </div>
+ }
+ .into_any()
+ } else {
+ view! {
+ <div class:final=r#final class="chat-message minor">
+ <div class="left message-timestamp">
+ {move || message.get().timestamp().read().format("%H:%M").to_string()}
+ </div>
+ <div class="middle message-text">
+ {move || message.get().body().read().body.clone()}
+ </div>
+ <div class="right message-delivery">
+ {move || {
+ message
+ .get()
+ .delivery()
+ .get()
+ .map(|delivery| view! { <Delivery delivery /> })
+ }}
+ </div>
+ </div>
+ }
+ .into_any()
+ }
+ }}
+ {move || {
+ if new_day {
+ view! {
+ <div class="new-day">
+ {move || {
+ message.get().timestamp().read().format("%Y-%m-%d").to_string()
+ }}
+ </div>
+ }
+ .into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ }
+}
diff --git a/src/components/message_composer.rs b/src/components/message_composer.rs
new file mode 100644
index 0000000..fd4e59b
--- /dev/null
+++ b/src/components/message_composer.rs
@@ -0,0 +1,134 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use filamento::chat::Body;
+use jid::BareJID;
+use js_sys::{wasm_bindgen::UnwrapThrowExt, Object, Reflect, JSON};
+use leptos::{html::Div, prelude::*, task::spawn_local};
+use overlay_scrollbars::OverlayScrollbars;
+use tracing::debug;
+
+use crate::client::Client;
+
+#[component]
+pub fn ChatViewMessageComposer(chat: BareJID) -> impl IntoView {
+ let message_input: NodeRef<Div> = NodeRef::new();
+
+ // TODO: load last message draft
+ let new_message = RwSignal::new("".to_string());
+ let client: Client = use_context().expect("no client in context");
+ let client = RwSignal::new(client);
+ let (shift_pressed, set_shift_pressed) = signal(false);
+
+ let send_message = move || {
+ let value = chat.clone();
+ spawn_local(async move {
+ match client
+ .read()
+ .send_message(
+ value,
+ Body {
+ body: new_message.get(),
+ },
+ )
+ .await
+ {
+ Ok(_) => {
+ new_message.set("".to_string());
+ message_input
+ .write()
+ .as_ref()
+ .expect("message input div not mounted")
+ .set_text_content(Some(""));
+ }
+ Err(e) => tracing::error!("message send error: {}", e),
+ }
+ })
+ };
+
+ let _focus = Effect::new(move |_| {
+ if let Some(input) = message_input.get() {
+ let _ = input.focus();
+ // TODO: set the last draft
+ input.set_text_content(Some(""));
+ // input.style("height: 0");
+ // let height = input.scroll_height();
+ // input.style(format!("height: {}px", height));
+ }
+ });
+
+ // let on_input = move |ev: Event| {
+ // // let keyboard_event: KeyboardEvent = ev.try_into().unwrap();
+ // debug!("got input event");
+ // let key= event_target_value(&ev);
+ // new_message.set(key);
+ // debug!("set new message");
+ // };
+ //
+
+ let composer: NodeRef<Div> = NodeRef::new();
+
+ let _scrollbars = Effect::new(move |_| {
+ if let Some(buffer) = composer.get() {
+ if let Some(viewport) = message_input.get() {
+ let scrollbars_obj = Object::new();
+ Reflect::set(&scrollbars_obj, &"theme".into(), &"os-macaw".into()).unwrap_throw();
+ Reflect::set(&scrollbars_obj, &"autoHide".into(), &"leave".into()).unwrap_throw();
+ let options_obj = Object::new();
+ Reflect::set(&options_obj, &"scrollbars".into(), &scrollbars_obj).unwrap_throw();
+
+ let elements_obj = Object::new();
+ Reflect::set(&elements_obj, &"viewport".into(), &viewport.into()).unwrap_throw();
+ let element_obj = Object::new();
+ Reflect::set(&elements_obj, &"elements".into(), &elements_obj).unwrap_throw();
+ Reflect::set(&element_obj, &"target".into(), &buffer.into()).unwrap_throw();
+ // let element = Object::define_property(&Object::define_property(&Object::new(), &"target".into(), &buffer.into()), &"elements".into(), &Object::define_property(&Object::new(), &"viewport".into(), &viewport.into()));
+ debug!(
+ "scrollable element: {}",
+ JSON::stringify(&element_obj.clone().into()).unwrap_throw()
+ );
+ debug!(
+ "scrollable options: {}",
+ JSON::stringify(&options_obj.clone().into()).unwrap_throw()
+ );
+ OverlayScrollbars(element_obj, options_obj);
+ }
+ }
+ });
+
+ // TODO: placeholder
+ view! {
+ <form class="new-message-composer panel">
+ <div class="overlay-scroll" node_ref=composer>
+ <div
+ class="text-box"
+ on:input:target=move |ev| {
+ new_message.set(ev.target().text_content().unwrap_or_default())
+ }
+ node_ref=message_input
+ contenteditable
+ on:keydown=move |ev| {
+ match ev.key_code() {
+ 16 => set_shift_pressed.set(true),
+ 13 => {
+ if !shift_pressed.get() {
+ ev.prevent_default();
+ send_message();
+ }
+ }
+ _ => {}
+ }
+ }
+ on:keyup=move |ev| {
+ match ev.key_code() {
+ 16 => set_shift_pressed.set(false),
+ _ => {}
+ }
+ }
+ ></div>
+ </div>
+ // <input hidden type="submit" />
+ </form>
+ }
+}
diff --git a/src/components/message_history_buffer.rs b/src/components/message_history_buffer.rs
new file mode 100644
index 0000000..c733700
--- /dev/null
+++ b/src/components/message_history_buffer.rs
@@ -0,0 +1,158 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use chrono::{NaiveDateTime, TimeDelta};
+use filamento::{
+ chat::{Chat, ChatStoreFields, MessageStoreFields},
+ user::User,
+};
+use indexmap::IndexMap;
+use jid::BareJID;
+use leptos::prelude::*;
+use reactive_stores::{ArcStore, Store};
+use tracing::{debug, error};
+use uuid::Uuid;
+
+use crate::{
+ chat::MacawChat,
+ client::Client,
+ components::message::Message,
+ message::{ArcMacawMessage, MacawMessage},
+ message_subscriptions::MessageSubscriptions,
+};
+
+#[component]
+pub fn MessageHistoryBuffer(chat: MacawChat) -> impl IntoView {
+ let (messages, set_messages) = arc_signal(IndexMap::new());
+
+ let load_set_messages = set_messages.clone();
+ let load_messages = LocalResource::new(move || {
+ let load_set_messages = load_set_messages.clone();
+ async move {
+ let client = use_context::<Client>().expect("client not in context");
+ let messages = client
+ .get_messages_with_users(chat.get().correspondent().get())
+ .await
+ .map_err(|e| e.to_string());
+ match messages {
+ Ok(m) => {
+ let mut messages = IndexMap::new();
+ for (message, message_user) in m {
+ messages.insert(
+ message.id,
+ ArcMacawMessage::got_message_and_user(message, message_user).await,
+ );
+ }
+ load_set_messages.set(messages);
+ }
+ Err(err) => {
+ error!("{err}")
+ // TODO: show error message at top of chats list
+ }
+ }
+ }
+ });
+
+ // TODO: filter new messages signal
+ let new_messages_signal: RwSignal<MessageSubscriptions> = use_context().unwrap();
+ let (sub_id, set_sub_id) = signal(None);
+ let load_new_messages_set = set_messages.clone();
+ let _load_new_messages = LocalResource::new(move || {
+ let load_new_messages_set = load_new_messages_set.clone();
+ async move {
+ load_messages.await;
+ let (sub_id, mut new_messages) = new_messages_signal
+ .write()
+ .subscribe_chat(chat.get().correspondent().get());
+ set_sub_id.set(Some(sub_id));
+ while let Some(new_message) = new_messages.recv().await {
+ debug!("got new message in let message buffer");
+ let mut messages = load_new_messages_set.write();
+ if let Some((_, last)) = messages.last() {
+ if *last.get().timestamp().read() < *new_message.get().timestamp().read() {
+ messages.insert(new_message.get().id().get(), new_message);
+ debug!("set the new message in message buffer");
+ } else {
+ let index = match messages.binary_search_by(|_, value| {
+ value
+ .get()
+ .timestamp()
+ .read()
+ .cmp(&new_message.get().timestamp().read())
+ }) {
+ Ok(i) => i,
+ Err(i) => i,
+ };
+ messages.insert_before(
+ // TODO: check if this logic is correct
+ index,
+ new_message.get().id().get(),
+ new_message,
+ );
+ debug!("set the new message in message buffer");
+ }
+ } else {
+ messages.insert(new_message.get().id().get(), new_message);
+ debug!("set the new message in message buffer");
+ }
+ }
+ }
+ });
+ on_cleanup(move || {
+ if let Some(sub_id) = sub_id.get_untracked() {
+ new_messages_signal
+ .write()
+ .unsubscribe_chat(sub_id, chat.get().correspondent().get_untracked());
+ }
+ });
+
+ let each = move || {
+ let mut last_timestamp = NaiveDateTime::MIN;
+ let mut last_user: Option<BareJID> = None;
+ let mut messages = messages
+ .get()
+ .into_iter()
+ .map(|(id, message)| {
+ let message_timestamp = message.message.get().timestamp().read().naive_local();
+ let mut major = if last_user.as_ref() != Some(&message.message.get().read().from)
+ || message_timestamp - last_timestamp > TimeDelta::minutes(3)
+ {
+ true
+ } else {
+ false
+ };
+ let new_day = if message_timestamp.date() > last_timestamp.date() {
+ major = true;
+ true
+ } else {
+ false
+ };
+ last_user = Some(message.get().from().get());
+ last_timestamp = message_timestamp;
+ (id, (message, major, false, new_day))
+ })
+ .collect::<Vec<_>>();
+ if let Some((_id, (_, _, last, _))) = messages.last_mut() {
+ *last = true
+ }
+ messages.into_iter().rev()
+ };
+
+ view! {
+ <div class="messages-buffer">
+ <For
+ each=each
+ key=|message| (message.0, message.1.1, message.1.2, message.1.3)
+ let(message)
+ >
+ <Message
+ message=message.1.0.into()
+ major=message.1.1
+ r#final=message.1.2
+ new_day=message.1.3
+ />
+ </For>
+ </div>
+ }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
new file mode 100644
index 0000000..d2fb6b5
--- /dev/null
+++ b/src/components/mod.rs
@@ -0,0 +1,17 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+mod avatar;
+pub mod chat_header;
+mod chats_list;
+pub mod icon;
+mod message;
+pub mod message_composer;
+pub mod message_history_buffer;
+pub mod modal;
+mod new_chat;
+mod overlay;
+mod personal_status;
+mod roster_list;
+pub mod sidebar;
diff --git a/src/components/modal.rs b/src/components/modal.rs
new file mode 100644
index 0000000..e23fa5d
--- /dev/null
+++ b/src/components/modal.rs
@@ -0,0 +1,25 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use leptos::ev::MouseEvent;
+use leptos::prelude::*;
+
+#[component]
+pub fn Modal(
+ on_background_click: impl Fn(MouseEvent) + 'static,
+ children: Children,
+) -> impl IntoView {
+ view! {
+ <div
+ class="modal"
+ on:click=move |e| {
+ if e.current_target() == e.target() {
+ on_background_click(e)
+ }
+ }
+ >
+ {children()}
+ </div>
+ }
+}
diff --git a/src/components/new_chat.rs b/src/components/new_chat.rs
new file mode 100644
index 0000000..925ec57
--- /dev/null
+++ b/src/components/new_chat.rs
@@ -0,0 +1,147 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::str::FromStr;
+
+use filamento::{
+ chat::Chat,
+ error::{CommandError, DatabaseError},
+ user::User,
+};
+use jid::{BareJID, JID};
+use leptos::{html::Input, prelude::*};
+use reactive_stores::{ArcStore, Store};
+use thiserror::Error;
+
+use crate::{
+ chat::{ArcMacawChat, MacawChat},
+ client::Client,
+ open_chats::OpenChatsPanel,
+ state_store::StateStore,
+ user::{ArcMacawUser, MacawUser, fetch_avatar},
+};
+
+#[derive(Clone, Debug, Error)]
+pub enum NewChatError {
+ #[error("Missing JID")]
+ MissingJID,
+ #[error("Invalid JID: {0}")]
+ InvalidJID(#[from] jid::ParseError),
+ #[error("Database: {0}")]
+ Db(#[from] CommandError<DatabaseError>),
+}
+
+#[component]
+pub fn NewChatWidget(set_open_new_chat: WriteSignal<bool>) -> impl IntoView {
+ let jid = RwSignal::new("".to_string());
+
+ // TODO: compartmentalise into error component, form component...
+ let (error, set_error) = signal(None::<NewChatError>);
+ let error_message = move || {
+ error.with(|error| {
+ if let Some(error) = error {
+ view! { <div class="error">{error.to_string()}</div> }.into_any()
+ } else {
+ view! {}.into_any()
+ }
+ })
+ };
+ let (new_chat_pending, set_new_chat_pending) = signal(false);
+
+ let open_chats: Store<OpenChatsPanel> =
+ use_context().expect("no open chats panel store in context");
+ let client = use_context::<Client>().expect("client not in context");
+
+ let chat_state_store: StateStore<BareJID, ArcStore<Chat>> =
+ use_context().expect("no chat state store");
+ let user_state_store: StateStore<BareJID, (ArcStore<User>, ArcRwSignal<String>)> =
+ use_context().expect("no user state store");
+
+ let open_chat = Action::new_local(move |_| {
+ let client = client.clone();
+ async move {
+ set_new_chat_pending.set(true);
+
+ if jid.read_untracked().is_empty() {
+ set_error.set(Some(NewChatError::MissingJID));
+ set_new_chat_pending.set(false);
+ return;
+ }
+
+ let jid = match JID::from_str(&jid.read_untracked()) {
+ // TODO: ability to direct address a resource?
+ Ok(j) => j.to_bare(),
+ Err(e) => {
+ set_error.set(Some(e.into()));
+ set_new_chat_pending.set(false);
+ return;
+ }
+ };
+
+ let chat_jid = jid;
+ let (chat, user) = match client.get_chat_and_user(chat_jid).await {
+ Ok(c) => c,
+ Err(e) => {
+ set_error.set(Some(e.into()));
+ set_new_chat_pending.set(false);
+ return;
+ }
+ };
+
+ let chat = {
+ // let user = MacawUser::got_user(user);
+ // let user = user_state_store.store(user.jid.clone(), ArcStore::new(user));
+ let old_user = user_state_store.get_listener(user.jid.clone());
+ let user = if let Some(old_user) = old_user {
+ old_user.update(|(old_user, _avatar)| {
+ old_user.set(user);
+ });
+ old_user
+ } else {
+ let avatar = fetch_avatar(user.avatar.as_deref()).await;
+ let avatar = ArcRwSignal::new(avatar);
+ user_state_store.store(user.jid.clone(), (ArcStore::new(user), avatar))
+ };
+ let user = ArcMacawUser { user };
+ let chat = chat_state_store.store(chat.correspondent.clone(), ArcStore::new(chat));
+ ArcMacawChat { chat, user }
+ };
+ open_chats.update(|open_chats| open_chats.open(chat.clone()));
+ set_open_new_chat.set(false);
+ }
+ });
+
+ let jid_input = NodeRef::<Input>::new();
+ let _focus = Effect::new(move |_| {
+ if let Some(input) = jid_input.get() {
+ let _ = input.focus();
+ input.set_text_content(Some(""));
+ // input.style("height: 0");
+ // let height = input.scroll_height();
+ // input.style(format!("height: {}px", height));
+ }
+ });
+
+ view! {
+ <div class="new-chat-widget">
+ <form on:submit=move |ev| {
+ ev.prevent_default();
+ open_chat.dispatch(());
+ }>
+ {error_message}
+ <input
+ disabled=new_chat_pending
+ placeholder="JID"
+ type="text"
+ node_ref=jid_input
+ bind:value=jid
+ name="jid"
+ id="jid"
+ autofocus="true"
+ />
+ <input disabled=new_chat_pending class="button" type="submit" value="Start Chat" />
+ </form>
+ </div>
+ }
+}
diff --git a/src/components/overlay.rs b/src/components/overlay.rs
new file mode 100644
index 0000000..d10f33a
--- /dev/null
+++ b/src/components/overlay.rs
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use leptos::prelude::*;
+use tracing::debug;
+
+#[component]
+pub fn Overlay(set_open: WriteSignal<bool>, children: Children) -> impl IntoView {
+ view! {
+ <div class="overlay">
+ <div
+ class="overlay-background"
+ on:click=move |_| {
+ debug!("set open to false");
+ set_open.update(|state| *state = false)
+ }
+ ></div>
+ <div class="overlay-content">{children()}</div>
+ </div>
+ }
+}
diff --git a/src/components/personal_status.rs b/src/components/personal_status.rs
new file mode 100644
index 0000000..b74b366
--- /dev/null
+++ b/src/components/personal_status.rs
@@ -0,0 +1,249 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use filamento::{
+ presence::{Offline, Online, PresenceType, Show},
+ user::{User, UserStoreFields},
+};
+use leptos::{html, prelude::*};
+use reactive_stores::{ArcStore, Store};
+use tracing::{debug, error};
+
+use crate::{
+ client::Client,
+ components::{avatar::AvatarWithPresence, overlay::Overlay},
+ user::{MacawUser, get_name},
+ user_presences::UserPresences,
+ views::{AppState, macaw::settings::SettingsPage},
+};
+
+#[component]
+pub fn PersonalStatus() -> impl IntoView {
+ let user: LocalResource<MacawUser> = use_context().expect("no local user in context");
+
+ let (open, set_open) = signal(false);
+ move || {
+ if let Some(user) = user.get() {
+ view! {
+ <div
+ class="dock-item"
+ class:focused=move || *open.read()
+ on:click=move |_| {
+ debug!("set open to true");
+ set_open.update(|state| *state = !*state)
+ }
+ >
+ <AvatarWithPresence user />
+ <div class="dock-pill"></div>
+ </div>
+ {move || {
+ let open = open.get();
+ debug!("open = {:?}", open);
+ if open {
+ view! {
+ <Overlay set_open>
+ <PersonalStatusMenu user set_open />
+ </Overlay>
+ }
+ .into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ }
+ .into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }
+}
+
+#[component]
+pub fn PersonalStatusMenu(user: MacawUser, set_open: WriteSignal<bool>) -> impl IntoView {
+ let set_app: WriteSignal<AppState> = use_context().unwrap();
+ let show_settings: RwSignal<Option<SettingsPage>> = use_context().unwrap();
+ let user_presences: Store<UserPresences> = use_context().expect("no user presence store");
+
+ let client = use_context::<Client>().expect("client not in context");
+ let client1 = client.clone();
+ let (show_value, set_show_value) = signal({
+ let show = match user_presences
+ .write()
+ .get_user_presences(&user.get().jid().read())
+ .write()
+ .resource_presence(client.resource.read().clone().unwrap_or_default())
+ .presence
+ {
+ PresenceType::Online(online) => match online.show {
+ Some(s) => match s {
+ Show::Away => 3,
+ Show::Chat => 0,
+ Show::DoNotDisturb => 2,
+ Show::ExtendedAway => 4,
+ },
+ None => 1,
+ },
+ PresenceType::Offline(_offline) => 5,
+ };
+ debug!("initial show = {show}");
+ show
+ });
+
+ let show_select: NodeRef<html::Select> = NodeRef::new();
+
+ let disconnect = Action::new_local(move |()| {
+ let client = client.clone();
+ async move {
+ client.disconnect(Offline::default()).await;
+ }
+ });
+ let set_status = Action::new_local(move |show_value: &i32| {
+ let show_value = show_value.to_owned();
+ let client = client1.clone();
+ async move {
+ if let Err(e) = match show_value {
+ 0 => {
+ if let Ok(r) = client.connect().await {
+ client.resource.set(Some(r))
+ };
+ client
+ .set_status(Online {
+ show: Some(Show::Chat),
+ ..Default::default()
+ })
+ .await
+ }
+ 1 => {
+ if let Ok(r) = client.connect().await {
+ client.resource.set(Some(r))
+ };
+ client
+ .set_status(Online {
+ show: None,
+ ..Default::default()
+ })
+ .await
+ }
+ 2 => {
+ if let Ok(r) = client.connect().await {
+ client.resource.set(Some(r))
+ };
+ client
+ .set_status(Online {
+ show: Some(Show::DoNotDisturb),
+ ..Default::default()
+ })
+ .await
+ }
+ 3 => {
+ if let Ok(r) = client.connect().await {
+ client.resource.set(Some(r))
+ };
+ client
+ .set_status(Online {
+ show: Some(Show::Away),
+ ..Default::default()
+ })
+ .await
+ }
+ 4 => {
+ if let Ok(r) = client.connect().await {
+ client.resource.set(Some(r))
+ };
+ client
+ .set_status(Online {
+ show: Some(Show::ExtendedAway),
+ ..Default::default()
+ })
+ .await
+ }
+ 5 => {
+ if let Ok(_) = client.disconnect(Offline::default()).await {
+ client.resource.set(None)
+ }
+ set_show_value.set(5);
+ return;
+ }
+ _ => {
+ error!("invalid availability select");
+ return;
+ }
+ } {
+ error!("show set error: {e}");
+ return;
+ }
+ set_show_value.set(show_value);
+ }
+ });
+
+ view! {
+ <div class="personal-status-menu menu">
+ <div class="user">
+ <AvatarWithPresence user=user />
+ <div class="user-info">
+ <div class="nick">{move || get_name(user.get().into(), false)}</div>
+ <div class="jid">{move || user.get().jid().with(|jid| jid.to_string())}</div>
+ </div>
+ </div>
+ <div class="status-edit">
+ <select
+ node_ref=show_select
+ on:change:target=move |ev| {
+ let show_value = ev.target().value().parse().unwrap();
+ set_status.dispatch(show_value);
+ }
+ prop:show_value=move || show_value.get().to_string()
+ >
+ <option value="0" selected=move || show_value.get_untracked() == 0>
+ Available to Chat
+ </option>
+ <option value="1" selected=move || show_value.get_untracked() == 1>
+ Online
+ </option>
+ <option value="2" selected=move || show_value.get_untracked() == 2>
+ Do not disturb
+ </option>
+ <option value="3" selected=move || show_value.get_untracked() == 3>
+ Away
+ </option>
+ <option value="4" selected=move || show_value.get_untracked() == 4>
+ Extended Away
+ </option>
+ <option value="5" selected=move || show_value.get_untracked() == 5>
+ Offline
+ </option>
+ </select>
+ </div>
+ <hr />
+ <div
+ class="menu-item"
+ on:click=move |_| {
+ show_settings.set(Some(SettingsPage::Profile));
+ set_open.set(false);
+ }
+ >
+ Profile
+ </div>
+ <div
+ class="menu-item"
+ on:click=move |_| {
+ show_settings.set(Some(SettingsPage::Account));
+ set_open.set(false);
+ }
+ >
+ Settings
+ </div>
+ <hr />
+ <div
+ class="menu-item"
+ on:click=move |_| {
+ disconnect.dispatch(());
+ set_app.set(AppState::LoggedOut)
+ }
+ >
+ Log out
+ </div>
+ </div>
+ }
+}
diff --git a/src/components/roster_list.rs b/src/components/roster_list.rs
new file mode 100644
index 0000000..b018d45
--- /dev/null
+++ b/src/components/roster_list.rs
@@ -0,0 +1,133 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::collections::HashSet;
+
+use contact_request_manager::AddContact;
+use jid::BareJID;
+use js_sys::{wasm_bindgen::UnwrapThrowExt, Object, Reflect, JSON};
+use leptos::{html::Div, prelude::*};
+use overlay_scrollbars::OverlayScrollbars;
+use reactive_stores::Store;
+use roster_list_item::RosterListItem;
+use tracing::debug;
+
+use crate::{
+ components::icon::IconComponent,
+ icon::Icon,
+ open_chats::{OpenChatsPanel, OpenChatsPanelStoreFields},
+ roster::{Roster, RosterStoreFields},
+};
+
+mod contact_request_manager;
+mod roster_list_item;
+
+#[component]
+pub fn RosterList() -> impl IntoView {
+ let requests: ReadSignal<HashSet<BareJID>> =
+ use_context().expect("no pending subscriptions in context");
+
+ let open_chats: Store<OpenChatsPanel> =
+ use_context().expect("no open chats panel store in context");
+
+ let roster: Store<Roster> = use_context().expect("no roster in context");
+ let (open_add_contact, set_open_add_contact) = signal(false);
+ let open_chat = Memo::new(move |_| open_chats.chat_view().get());
+ provide_context(open_chat);
+
+ let roster_list: NodeRef<Div> = NodeRef::new();
+ let roster_list_viewport: NodeRef<Div> = NodeRef::new();
+
+ let _scrollbars = Effect::new(move |_| {
+ if let Some(buffer) = roster_list.get() {
+ if let Some(viewport) = roster_list_viewport.get() {
+ let elements_obj = Object::new();
+ Reflect::set(&elements_obj, &"viewport".into(), &viewport.into()).unwrap_throw();
+ let element_obj = Object::new();
+ Reflect::set(&elements_obj, &"elements".into(), &elements_obj).unwrap_throw();
+ Reflect::set(&element_obj, &"target".into(), &buffer.into()).unwrap_throw();
+ // let element = Object::define_property(&Object::define_property(&Object::new(), &"target".into(), &buffer.into()), &"elements".into(), &Object::define_property(&Object::new(), &"viewport".into(), &viewport.into()));
+ debug!(
+ "scrollable element: {}",
+ JSON::stringify(&element_obj.clone().into()).unwrap_throw()
+ );
+ OverlayScrollbars(element_obj, Object::new());
+ }
+ }
+ });
+
+ let add_contact: NodeRef<Div> = NodeRef::new();
+ let add_contact_viewport: NodeRef<Div> = NodeRef::new();
+
+ let _scrollbars = Effect::new(move |_| {
+ if let Some(buffer) = add_contact.get() {
+ if let Some(viewport) = add_contact_viewport.get() {
+ let scrollbars_obj = Object::new();
+ Reflect::set(&scrollbars_obj, &"theme".into(), &"os-macaw".into()).unwrap_throw();
+ Reflect::set(&scrollbars_obj, &"move".into(), &"leave".into()).unwrap_throw();
+ let options_obj = Object::new();
+ Reflect::set(&options_obj, &"scrollbars".into(), &scrollbars_obj).unwrap_throw();
+
+ let elements_obj = Object::new();
+ Reflect::set(&elements_obj, &"viewport".into(), &viewport.into()).unwrap_throw();
+ let element_obj = Object::new();
+ Reflect::set(&elements_obj, &"elements".into(), &elements_obj).unwrap_throw();
+ Reflect::set(&element_obj, &"target".into(), &buffer.into()).unwrap_throw();
+ // let element = Object::define_property(&Object::define_property(&Object::new(), &"target".into(), &buffer.into()), &"elements".into(), &Object::define_property(&Object::new(), &"viewport".into(), &viewport.into()));
+ debug!(
+ "scrollable element: {}",
+ JSON::stringify(&element_obj.clone().into()).unwrap_throw()
+ );
+ OverlayScrollbars(element_obj, options_obj);
+ }
+ }
+ });
+
+ // TODO: filter new messages signal
+ view! {
+ <div class="roster-list panel">
+ <div class="header">
+ <h2>Roster</h2>
+ <div class="add-contact header-icon" class:open=open_add_contact>
+ <IconComponent
+ icon=Icon::AddContact24
+ on:click=move |_| set_open_add_contact.update(|state| *state = !*state)
+ />
+ {move || {
+ if !requests.read().is_empty() {
+ view! { <div class="badge"></div> }.into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ </div>
+ </div>
+ {move || {
+ if *open_add_contact.read() {
+ view! {
+ <div class="overlay-scroll add-contact-panel" node_ref=add_contact>
+ <div class="roster-add-contact" node_ref=add_contact_viewport>
+ <AddContact />
+ </div>
+ </div>
+ }
+ .into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ <div class="overlay-scroll" node_ref=roster_list>
+ <div class="roster-list-roster" node_ref=roster_list_viewport>
+ <For
+ each=move || roster.contacts().get()
+ key=|contact| contact.0.clone()
+ let(contact)
+ >
+ <RosterListItem contact=contact.1 />
+ </For>
+ </div>
+ </div>
+ </div>
+ }
+}
diff --git a/src/components/roster_list/contact_request_manager.rs b/src/components/roster_list/contact_request_manager.rs
new file mode 100644
index 0000000..4c28142
--- /dev/null
+++ b/src/components/roster_list/contact_request_manager.rs
@@ -0,0 +1,284 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::{collections::HashSet, str::FromStr};
+
+use filamento::{
+ error::{CommandError, SubscribeError},
+ roster::ContactStoreFields,
+};
+use jid::{BareJID, JID};
+use leptos::{html::Input, prelude::*};
+use reactive_stores::Store;
+use thiserror::Error;
+
+use crate::{
+ client::Client,
+ roster::{Roster, RosterStoreFields},
+};
+
+#[derive(Clone, Debug, Error)]
+pub enum AddContactError {
+ #[error("Missing JID")]
+ MissingJID,
+ #[error("Invalid JID: {0}")]
+ InvalidJID(#[from] jid::ParseError),
+ #[error("Subscription: {0}")]
+ Db(#[from] CommandError<SubscribeError>),
+}
+
+#[component]
+// TODO: rename
+pub fn AddContact() -> impl IntoView {
+ let requests: ReadSignal<HashSet<BareJID>> =
+ use_context().expect("no pending subscriptions in context");
+ let set_requests: WriteSignal<HashSet<BareJID>> =
+ use_context().expect("no pending subscriptions write signal in context");
+ let roster: Store<Roster> = use_context().expect("no roster in context");
+
+ let jid = RwSignal::new("".to_string());
+ // TODO: compartmentalise into error component, form component...
+ let (error, set_error) = signal(None::<AddContactError>);
+ let error_message = move || {
+ error.with(|error| {
+ if let Some(error) = error {
+ view! { <div class="error">{error.to_string()}</div> }.into_any()
+ } else {
+ view! {}.into_any()
+ }
+ })
+ };
+ let (add_contact_pending, set_add_contact_pending) = signal(false);
+
+ let client = use_context::<Client>().expect("client not in context");
+ let client2 = client.clone();
+ let client3 = client.clone();
+ let client4 = client.clone();
+ let client5 = client.clone();
+
+ let add_contact = Action::new_local(move |_| {
+ let client = client.clone();
+ async move {
+ set_add_contact_pending.set(true);
+
+ if jid.read_untracked().is_empty() {
+ set_error.set(Some(AddContactError::MissingJID));
+ set_add_contact_pending.set(false);
+ return;
+ }
+
+ let jid = match JID::from_str(&jid.read_untracked()) {
+ Ok(j) => j.to_bare(),
+ Err(e) => {
+ set_error.set(Some(e.into()));
+ set_add_contact_pending.set(false);
+ return;
+ }
+ };
+
+ let chat_jid = jid;
+ // TODO: more options?
+ match client.buddy_request(chat_jid).await {
+ Ok(c) => c,
+ Err(e) => {
+ set_error.set(Some(e.into()));
+ set_add_contact_pending.set(false);
+ return;
+ }
+ };
+
+ set_add_contact_pending.set(false);
+ }
+ });
+
+ let jid_input = NodeRef::<Input>::new();
+ let _focus = Effect::new(move |_| {
+ if let Some(input) = jid_input.get() {
+ let _ = input.focus();
+ input.set_text_content(Some(""));
+ // input.style("height: 0");
+ // let height = input.scroll_height();
+ // input.style(format!("height: {}px", height));
+ }
+ });
+
+ let outgoing = move || {
+ roster
+ .contacts()
+ .get()
+ .into_iter()
+ .filter(
+ |(jid, contact)| match *contact.contact.subscription().read() {
+ filamento::roster::Subscription::None => false,
+ filamento::roster::Subscription::PendingOut => true,
+ filamento::roster::Subscription::PendingIn => false,
+ filamento::roster::Subscription::PendingInPendingOut => true,
+ filamento::roster::Subscription::OnlyOut => false,
+ filamento::roster::Subscription::OnlyIn => false,
+ filamento::roster::Subscription::OutPendingIn => false,
+ filamento::roster::Subscription::InPendingOut => true,
+ filamento::roster::Subscription::Buddy => false,
+ },
+ )
+ .collect::<Vec<_>>()
+ };
+
+ let accept_friend_request = Action::new_local(move |jid: &BareJID| {
+ let client = client2.clone();
+ let jid = jid.clone();
+ async move {
+ // TODO: error
+ client.accept_buddy_request(jid.clone()).await;
+ set_requests.write().remove(&jid);
+ }
+ });
+
+ let accept_subscription_request = Action::new_local(move |jid: &BareJID| {
+ let client = client5.clone();
+ let jid = jid.clone();
+ async move {
+ // TODO: error
+ client.accept_subscription_request(jid.clone()).await;
+ set_requests.write().remove(&jid);
+ }
+ });
+
+ let reject_friend_request = Action::new_local(move |jid: &BareJID| {
+ let client = client3.clone();
+ let jid = jid.clone();
+ async move {
+ // TODO: error
+ client.unsubscribe_contact(jid.clone()).await;
+ set_requests.write().remove(&jid);
+ }
+ });
+
+ let cancel_subscription_request = Action::new_local(move |jid: &BareJID| {
+ let client = client4.clone();
+ let jid = jid.clone();
+ async move {
+ // TODO: error
+ client.unsubscribe_from_contact(jid).await;
+ }
+ });
+
+ view! {
+ <div class="add-contact-menu">
+ <div>
+ {error_message}
+ <form on:submit=move |ev| {
+ ev.prevent_default();
+ add_contact.dispatch(());
+ }>
+ <input
+ disabled=add_contact_pending
+ placeholder="JID"
+ type="text"
+ node_ref=jid_input
+ bind:value=jid
+ name="jid"
+ id="jid"
+ autofocus="true"
+ />
+ <input
+ disabled=add_contact_pending
+ class="button"
+ type="submit"
+ value="Send Friend Request"
+ />
+ </form>
+ </div>
+ {move || {
+ if !requests.read().is_empty() {
+ view! {
+ <div>
+ <h3>Incoming Subscription Requests</h3>
+ <For
+ each=move || requests.get()
+ key=|request| request.clone()
+ let(request)
+ >
+ {
+ let request2 = request.clone();
+ let request3 = request.clone();
+ let request4 = request.clone();
+ let jid_string = move || request.to_string();
+ view! {
+ <div class="jid-with-buttons">
+ <div class="jid">{jid_string}</div>
+ <div class="buttons">
+ <div
+ class="button"
+ on:click=move |_| {
+ reject_friend_request.dispatch(request3.clone());
+ }
+ >
+ Reject
+ </div>
+ <div
+ class="button"
+ on:click=move |_| {
+ accept_subscription_request.dispatch(request4.clone());
+ }
+ >
+ Sub-only
+ </div>
+ <div
+ class="button"
+ on:click=move |_| {
+ accept_friend_request.dispatch(request2.clone());
+ }
+ >
+ Accept Buddy
+ </div>
+ </div>
+ </div>
+ }
+ }
+ </For>
+ </div>
+ }
+ .into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ {move || {
+ if !outgoing().is_empty() {
+ view! {
+ <div>
+ <h3>Pending Outgoing Subscription Requests</h3>
+ <For
+ each=move || outgoing()
+ key=|(jid, _contact)| jid.clone()
+ let((jid, contact))
+ >
+ {
+ let jid2 = jid.clone();
+ let jid_string = move || jid.to_string();
+ view! {
+ <div class="jid-with-button">
+ <div class="jid">{jid_string}</div>
+ <div
+ class="button"
+ on:click=move |_| {
+ cancel_subscription_request.dispatch(jid2.clone());
+ }
+ >
+ Cancel
+ </div>
+ </div>
+ }
+ }
+ </For>
+ </div>
+ }
+ .into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ </div>
+ }
+}
diff --git a/src/components/roster_list/roster_list_item.rs b/src/components/roster_list/roster_list_item.rs
new file mode 100644
index 0000000..a6fd714
--- /dev/null
+++ b/src/components/roster_list/roster_list_item.rs
@@ -0,0 +1,122 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::ops::Deref;
+
+use filamento::{
+ chat::Chat,
+ roster::{Contact, ContactStoreFields},
+ user::{User, UserStoreFields},
+};
+use jid::BareJID;
+use leptos::prelude::*;
+use reactive_stores::{ArcStore, Store};
+use tracing::debug;
+
+use crate::{
+ chat::{ArcMacawChat, MacawChat},
+ client::Client,
+ components::{avatar::AvatarWithPresence, sidebar::Open},
+ contact::MacawContact,
+ open_chats::{OpenChatsPanel, OpenChatsPanelStoreFields},
+ state_store::StateStore,
+ user::{ArcMacawUser, fetch_avatar, get_name},
+};
+
+#[component]
+pub fn RosterListItem(contact: MacawContact) -> impl IntoView {
+ let name = move || get_name(contact.user.get().into(), false);
+
+ let open_chats: Store<OpenChatsPanel> =
+ use_context().expect("no open chats panel store in context");
+
+ let client = use_context::<Client>().expect("client not in context");
+
+ let chat_state_store: StateStore<BareJID, ArcStore<Chat>> =
+ use_context().expect("no chat state store");
+ let user_state_store: StateStore<BareJID, (ArcStore<User>, ArcRwSignal<String>)> =
+ use_context().expect("no user state store");
+
+ let open_chat = Action::new_local(move |_| {
+ let client = client.clone();
+ async move {
+ let to = contact.user.get().jid().get();
+ let (chat, user) = match client.get_chat_and_user(to).await {
+ Ok(c) => c,
+ Err(e) => {
+ // TODO: error
+ // set_error.set(Some(e.into()));
+ // set_new_chat_pending.set(false);
+ return;
+ }
+ };
+
+ let chat = {
+ // let user = MacawUser::got_user(user);
+ // let user = user_state_store.store(user.jid.clone(), ArcStore::new(user));
+ let old_user = user_state_store.get_listener(user.jid.clone());
+ let user = if let Some(old_user) = old_user {
+ old_user.update(|(old_user, _avatar)| {
+ old_user.set(user);
+ });
+ old_user
+ } else {
+ let avatar = fetch_avatar(user.avatar.as_deref()).await;
+ let avatar = ArcRwSignal::new(avatar);
+ user_state_store.store(user.jid.clone(), (ArcStore::new(user), avatar))
+ };
+ let user = ArcMacawUser { user };
+ let chat = chat_state_store.store(chat.correspondent.clone(), ArcStore::new(chat));
+ ArcMacawChat { chat, user }
+ };
+ open_chats.update(|open_chats| open_chats.open(chat.clone()));
+ }
+ });
+
+ let current_open_chat: Memo<Option<BareJID>> =
+ use_context().expect("no open chat memo in context");
+
+ let open = move || {
+ if let Some(open_chat) = &*current_open_chat.read() {
+ debug!("got open chat: {:?}", open_chat);
+ if *open_chat == *contact.user.get().jid().read() {
+ return Open::Focused;
+ }
+ }
+ if let Some(_backgrounded_chat) = open_chats
+ .chats()
+ .read()
+ .get(contact.user.get().jid().read().deref())
+ {
+ return Open::Open;
+ }
+ Open::Closed
+ };
+ let focused = move || open().is_focused();
+ let open = move || open().is_open();
+
+ view! {
+ <div
+ class="roster-list-item"
+ class:open=move || open()
+ class:focused=move || focused()
+ on:click=move |_| {
+ open_chat.dispatch(());
+ }
+ >
+ {move || {
+ view! { <AvatarWithPresence user=contact.user /> }
+ }}
+ <div class="item-info">
+ <div class="main-info">
+ <p class="name">
+ {name}
+ <span class="jid">- {move || contact.user_jid().read().to_string()}</span>
+ </p>
+ </div>
+ <div class="sub-info">{move || contact.subscription().read().to_string()}</div>
+ </div>
+ </div>
+ }
+}
diff --git a/src/components/sidebar.rs b/src/components/sidebar.rs
new file mode 100644
index 0000000..9f555b5
--- /dev/null
+++ b/src/components/sidebar.rs
@@ -0,0 +1,233 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::collections::{HashMap, HashSet};
+
+use jid::BareJID;
+use leptos::prelude::*;
+use reactive_stores::Store;
+
+use crate::components::{
+ chats_list::ChatsList, personal_status::PersonalStatus, roster_list::RosterList,
+};
+
+#[derive(PartialEq, Eq, Clone, Copy, Hash)]
+pub enum SidebarOpen {
+ Roster,
+ Chats,
+}
+
+#[derive(Store)]
+pub struct Drawer {
+ open: SidebarOpen,
+ hovering: bool,
+}
+
+pub enum Open {
+ /// Currently on screen
+ Focused,
+ /// Open in background somewhere (e.g. in another chat tab)
+ Open,
+ /// Closed
+ Closed,
+}
+
+impl Open {
+ pub fn is_focused(&self) -> bool {
+ match self {
+ Open::Focused => true,
+ Open::Open => false,
+ Open::Closed => false,
+ }
+ }
+
+ pub fn is_open(&self) -> bool {
+ match self {
+ Open::Focused => true,
+ Open::Open => true,
+ Open::Closed => false,
+ }
+ }
+}
+
+/// returns whether the state was changed to open (true) or closed (false)
+pub fn toggle_open(state: &mut Option<SidebarOpen>, open: SidebarOpen) -> bool {
+ match state {
+ Some(opened) => {
+ if *opened == open {
+ *state = None;
+ false
+ } else {
+ *state = Some(open);
+ true
+ }
+ }
+ None => {
+ *state = Some(open);
+ true
+ }
+ }
+}
+
+#[component]
+pub fn Sidebar() -> impl IntoView {
+ let requests: ReadSignal<HashSet<BareJID>> =
+ use_context().expect("no pending subscriptions in context");
+
+ // for what has been clicked open (in the background)
+ let (open, set_open) = signal(None::<SidebarOpen>);
+ // for what is just in the hovered state (not clicked to be pinned open yet necessarily)
+ let open = Memo::new(move |_| open.get());
+ let (hovered, set_hovered) = signal(None::<SidebarOpen>);
+ let hovered = Memo::new(move |_| hovered.get());
+ let (just_closed, set_just_closed) = signal(false);
+ let just_closed = Memo::new(move |_| just_closed.get());
+
+ let pages = Memo::new(move |_| {
+ let mut pages = HashSet::new();
+ if let Some(hovered) = *hovered.read() {
+ pages.insert(hovered);
+ }
+ if let Some(opened) = *open.read() {
+ pages.insert(opened);
+ }
+ pages
+ });
+
+ view! {
+ <div
+ class="sidebar"
+ on:mouseleave=move |_| {
+ set_hovered.set(None);
+ set_just_closed.set(false);
+ }
+ >
+ <div class="dock panel">
+ <div class="shortcuts">
+ <div
+ class="roster-tab dock-item"
+ class:focused=move || *open.read() == Some(SidebarOpen::Roster)
+ class:hovering=move || *hovered.read() == Some(SidebarOpen::Roster)
+ on:mouseenter=move |_| {
+ set_just_closed.set(false);
+ set_hovered.set(Some(SidebarOpen::Roster))
+ }
+ on:click=move |_| {
+ set_open
+ .update(|state| {
+ if !toggle_open(state, SidebarOpen::Roster) {
+ set_just_closed.set(true);
+ } else {
+ set_just_closed.set(false);
+ }
+ })
+ }
+ >
+ <div class="dock-pill"></div>
+ <div class="dock-icon">
+ <div class="icon-with-badge">
+ <img src="/assets/caw.png" />
+ {move || {
+ let len = requests.read().len();
+ if len > 0 {
+ view! { <div class="badge">{len}</div> }.into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ </div>
+ </div>
+ </div>
+ <div
+ class="chats-tab dock-item"
+ class:focused=move || *open.read() == Some(SidebarOpen::Chats)
+ class:hovering=move || *hovered.read() == Some(SidebarOpen::Chats)
+ on:mouseenter=move |_| {
+ set_just_closed.set(false);
+ set_hovered.set(Some(SidebarOpen::Chats))
+ }
+ on:click=move |_| {
+ set_open
+ .update(|state| {
+ if !toggle_open(state, SidebarOpen::Chats) {
+ set_just_closed.set(true);
+ } else {
+ set_just_closed.set(false);
+ }
+ })
+ }
+ >
+ <div class="dock-pill"></div>
+ <img src="/assets/bubble.png" />
+ </div>
+ </div>
+ <div class="pins"></div>
+ <div class="personal">
+ <PersonalStatus />
+ </div>
+ </div>
+ {move || {
+ if !just_closed.get() {
+ view! {
+ <For each=move || pages.get() key=|page| *page let(page)>
+ {move || match page {
+ SidebarOpen::Roster => {
+ view! {
+ {move || {
+ if *open.read() == None
+ && *hovered.read() == Some(SidebarOpen::Roster)
+ {
+ view! { <div class="sidebar-drawer behind-hovering"></div> }
+ .into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ <div
+ class:sidebar-drawer=true
+ class:sidebar-hovering-drawer=move || {
+ !(*open.read() == Some(SidebarOpen::Roster))
+ && (*hovered.read() == Some(SidebarOpen::Roster))
+ }
+ >
+ <RosterList />
+ </div>
+ }
+ .into_any()
+ }
+ SidebarOpen::Chats => {
+ view! {
+ {move || {
+ if *open.read() == None
+ && *hovered.read() == Some(SidebarOpen::Chats)
+ {
+ view! { <div class="sidebar-drawer behind-hovering"></div> }
+ .into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ <div
+ class:sidebar-drawer=true
+ class:sidebar-hovering-drawer=move || {
+ !(*open.read() == Some(SidebarOpen::Chats))
+ && (*hovered.read() == Some(SidebarOpen::Chats))
+ }
+ >
+ <ChatsList />
+ </div>
+ }
+ .into_any()
+ }
+ }}
+ </For>
+ }
+ .into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ </div>
+ }
+}
diff --git a/src/contact.rs b/src/contact.rs
new file mode 100644
index 0000000..b7f57fa
--- /dev/null
+++ b/src/contact.rs
@@ -0,0 +1,67 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::ops::{Deref, DerefMut};
+
+use filamento::{roster::Contact, user::User};
+use reactive_stores::Store;
+
+use crate::user::{ArcMacawUser, MacawUser};
+
+#[derive(Clone, Copy)]
+pub struct MacawContact {
+ pub contact: Store<Contact>,
+ pub user: MacawUser,
+}
+
+impl Deref for MacawContact {
+ type Target = Store<Contact>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.contact
+ }
+}
+
+impl DerefMut for MacawContact {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.contact
+ }
+}
+
+impl From<ArcMacawContact> for MacawContact {
+ fn from(value: ArcMacawContact) -> Self {
+ Self {
+ contact: value.contact,
+ user: value.user.into(),
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct ArcMacawContact {
+ pub contact: Store<Contact>,
+ pub user: ArcMacawUser,
+}
+
+impl ArcMacawContact {
+ pub async fn got_contact_and_user(contact: Contact, user: User) -> Self {
+ let contact = Store::new(contact);
+ let user = ArcMacawUser::got_user(user).await;
+ Self { contact, user }
+ }
+}
+
+impl Deref for ArcMacawContact {
+ type Target = Store<Contact>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.contact
+ }
+}
+
+impl DerefMut for ArcMacawContact {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.contact
+ }
+}
diff --git a/src/context.rs b/src/context.rs
new file mode 100644
index 0000000..da660dc
--- /dev/null
+++ b/src/context.rs
@@ -0,0 +1,3 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/src/files.rs b/src/files.rs
new file mode 100644
index 0000000..760549f
--- /dev/null
+++ b/src/files.rs
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use base64::{Engine, prelude::BASE64_STANDARD};
+use filamento::files::{FileStore, FilesMem, FilesOPFS, opfs::OPFSError};
+
+#[derive(Clone, Debug)]
+pub enum Files {
+ Mem(FilesMem),
+ Opfs(FilesOPFS),
+}
+
+impl FileStore for Files {
+ type Err = OPFSError;
+
+ async fn is_stored(&self, name: &str) -> Result<bool, Self::Err> {
+ match self {
+ Files::Mem(files_mem) => Ok(files_mem.is_stored(name).await.unwrap()),
+ Files::Opfs(files_opfs) => Ok(files_opfs.is_stored(name).await?),
+ }
+ }
+
+ async fn store(&self, name: &str, data: &[u8]) -> Result<(), Self::Err> {
+ match self {
+ Files::Mem(files_mem) => Ok(files_mem.store(name, data).await.unwrap()),
+ Files::Opfs(files_opfs) => Ok(files_opfs.store(name, data).await?),
+ }
+ }
+
+ async fn delete(&self, name: &str) -> Result<(), Self::Err> {
+ match self {
+ Files::Mem(files_mem) => Ok(files_mem.delete(name).await.unwrap()),
+ Files::Opfs(files_opfs) => Ok(files_opfs.delete(name).await?),
+ }
+ }
+}
+
+impl Files {
+ pub async fn get_src(&self, file_name: &str) -> Option<String> {
+ match self {
+ Files::Mem(files_mem) => {
+ if let Some(data) = files_mem.get_file(file_name).await {
+ let data = BASE64_STANDARD.encode(data);
+ Some(format!("data:image/jpg;base64, {}", data))
+ } else {
+ None
+ }
+ }
+ Files::Opfs(files_opfs) => files_opfs.get_src(file_name).await.ok(),
+ }
+ }
+}
diff --git a/src/icon.rs b/src/icon.rs
new file mode 100644
index 0000000..231ad2e
--- /dev/null
+++ b/src/icon.rs
@@ -0,0 +1,114 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+#[derive(Copy, Clone)]
+pub enum Icon {
+ AddContact24,
+ Attachment24,
+ Away16,
+ Away16Color,
+ Bubble16,
+ Bubble16Color,
+ Bubble24,
+ Close24,
+ Contact24,
+ Delivered16,
+ Dnd16,
+ Dnd16Color,
+ Error16Color,
+ Forward24,
+ Heart24,
+ NewBubble24,
+ Reply24,
+ Sending16,
+ Sent16,
+ Chat16Color,
+ Xa16Color,
+ Available16Color,
+}
+
+pub const ICONS_SRC: &str = "/assets/icons/";
+
+impl Icon {
+ pub fn src(&self) -> String {
+ match self {
+ Icon::AddContact24 => format!("{}addcontact24.svg", ICONS_SRC),
+ Icon::Attachment24 => format!("{}attachment24.svg", ICONS_SRC),
+ Icon::Away16 => format!("{}away16.svg", ICONS_SRC),
+ Icon::Away16Color => format!("{}away16color.svg", ICONS_SRC),
+ Icon::Bubble16 => format!("{}bubble16.svg", ICONS_SRC),
+ Icon::Bubble16Color => format!("{}bubble16color.svg", ICONS_SRC),
+ Icon::Bubble24 => format!("{}bubble24.svg", ICONS_SRC),
+ Icon::Close24 => format!("{}close24.svg", ICONS_SRC),
+ Icon::Contact24 => format!("{}contact24.svg", ICONS_SRC),
+ Icon::Delivered16 => format!("{}delivered16.svg", ICONS_SRC),
+ Icon::Dnd16 => format!("{}dnd16.svg", ICONS_SRC),
+ Icon::Dnd16Color => format!("{}dnd16color.svg", ICONS_SRC),
+ Icon::Error16Color => format!("{}error16color.svg", ICONS_SRC),
+ Icon::Forward24 => format!("{}forward24.svg", ICONS_SRC),
+ Icon::Heart24 => format!("{}heart24.svg", ICONS_SRC),
+ Icon::NewBubble24 => format!("{}newbubble24.svg", ICONS_SRC),
+ Icon::Reply24 => format!("{}reply24.svg", ICONS_SRC),
+ Icon::Sending16 => format!("{}sending16.svg", ICONS_SRC),
+ Icon::Sent16 => format!("{}sent16.svg", ICONS_SRC),
+ Icon::Chat16Color => format!("{}chat16color.svg", ICONS_SRC),
+ Icon::Xa16Color => format!("{}xa16color.svg", ICONS_SRC),
+ Icon::Available16Color => format!("{}available16color.svg", ICONS_SRC),
+ }
+ }
+
+ pub fn size(&self) -> isize {
+ match self {
+ Icon::AddContact24 => 24,
+ Icon::Attachment24 => 24,
+ Icon::Away16 => 16,
+ Icon::Away16Color => 16,
+ Icon::Bubble16 => 16,
+ Icon::Bubble16Color => 16,
+ Icon::Bubble24 => 24,
+ Icon::Close24 => 24,
+ Icon::Contact24 => 24,
+ Icon::Delivered16 => 16,
+ Icon::Dnd16 => 16,
+ Icon::Dnd16Color => 16,
+ Icon::Error16Color => 16,
+ Icon::Forward24 => 24,
+ Icon::Heart24 => 24,
+ Icon::NewBubble24 => 24,
+ Icon::Reply24 => 24,
+ Icon::Sending16 => 16,
+ Icon::Sent16 => 16,
+ Icon::Chat16Color => 16,
+ Icon::Xa16Color => 16,
+ Icon::Available16Color => 16,
+ }
+ }
+
+ pub fn light(&self) -> bool {
+ match self {
+ Icon::AddContact24 => true,
+ Icon::Attachment24 => true,
+ Icon::Away16 => true,
+ Icon::Away16Color => false,
+ Icon::Bubble16 => true,
+ Icon::Bubble16Color => false,
+ Icon::Bubble24 => true,
+ Icon::Close24 => true,
+ Icon::Contact24 => true,
+ Icon::Delivered16 => true,
+ Icon::Dnd16 => true,
+ Icon::Dnd16Color => false,
+ Icon::Error16Color => false,
+ Icon::Forward24 => true,
+ Icon::Heart24 => true,
+ Icon::NewBubble24 => true,
+ Icon::Reply24 => true,
+ Icon::Sending16 => true,
+ Icon::Sent16 => true,
+ Icon::Chat16Color => false,
+ Icon::Xa16Color => false,
+ Icon::Available16Color => false,
+ }
+ }
+}
diff --git a/src/icons.rs b/src/icons.rs
deleted file mode 100644
index 934a0c8..0000000
--- a/src/icons.rs
+++ /dev/null
@@ -1,127 +0,0 @@
-use iced::widget::svg;
-use iced::widget::{svg::Handle, Svg};
-use iced::Element;
-
-pub enum Icon {
- AddContact24,
- Attachment24,
- Away16,
- Away16Color,
- Bubble16,
- Bubble16Color,
- Bubble24,
- Contact24,
- Delivered16,
- Dnd16,
- Dnd16Color,
- Error16Color,
- Forward24,
- Heart24,
- NewBubble24,
- Reply24,
- Sending16,
- Sent16,
-}
-
-impl From<Icon> for Svg<'_> {
- fn from(value: Icon) -> Self {
- match value {
- Icon::AddContact24 => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/addcontact24.svg"
- )))
- .width(24)
- .height(24),
- Icon::Attachment24 => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/attachment24.svg"
- )))
- .width(24)
- .height(24),
- Icon::Away16 => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/away16.svg"
- )))
- .width(16)
- .height(16),
- Icon::Away16Color => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/away16color.svg"
- )))
- .width(16)
- .height(16),
- Icon::Bubble16 => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/bubble16.svg"
- )))
- .width(16)
- .height(16),
- Icon::Bubble16Color => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/bubble16color.svg"
- )))
- .width(16)
- .height(16),
- Icon::Bubble24 => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/bubble24.svg"
- )))
- .width(24)
- .height(24),
- Icon::Contact24 => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/contact24.svg"
- )))
- .width(24)
- .height(24),
- Icon::Delivered16 => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/delivered16.svg"
- )))
- .width(16)
- .height(16),
- Icon::Dnd16 => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/dnd16.svg"
- )))
- .width(16)
- .height(16),
- Icon::Dnd16Color => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/dnd16color.svg"
- )))
- .width(16)
- .height(16),
- Icon::Error16Color => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/error16color.svg"
- )))
- .width(16)
- .height(16),
- Icon::Forward24 => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/forward24.svg"
- )))
- .width(24)
- .height(24),
- Icon::Heart24 => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/heart24.svg"
- )))
- .width(24)
- .height(24),
- Icon::NewBubble24 => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/newbubble24.svg"
- )))
- .width(24)
- .height(24),
- Icon::Reply24 => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/reply24.svg"
- )))
- .width(24)
- .height(24),
- Icon::Sending16 => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/sending16.svg"
- )))
- .width(16)
- .height(16),
- Icon::Sent16 => svg(Handle::from_memory(include_bytes!(
- "../assets/icons/sent16.svg"
- )))
- .width(16)
- .height(16),
- }
- }
-}
-
-impl<Message> From<Icon> for Element<'_, Message> {
- fn from(value: Icon) -> Self {
- Into::<Svg>::into(value).into()
- }
-}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..fcd632e
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,20 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pub use views::App;
+
+mod chat;
+mod client;
+mod components;
+mod contact;
+mod files;
+mod icon;
+mod message;
+mod message_subscriptions;
+mod open_chats;
+mod roster;
+mod state_store;
+mod user;
+mod user_presences;
+mod views;
diff --git a/src/login_modal.rs b/src/login_modal.rs
deleted file mode 100644
index f61c204..0000000
--- a/src/login_modal.rs
+++ /dev/null
@@ -1,112 +0,0 @@
-use filamento::presence::{Offline, Presence};
-use iced::{
- futures::StreamExt,
- widget::{button, checkbox, column, container, text, text_input},
- Element, Task,
-};
-use jid::JID;
-use keyring::Entry;
-use serde::{Deserialize, Serialize};
-use tokio_stream::wrappers::ReceiverStream;
-use tracing::info;
-
-use crate::Client;
-
-#[derive(Default)]
-pub struct LoginModal {
- jid: String,
- password: String,
- remember_me: bool,
- error: Option<Error>,
-}
-
-#[derive(Debug, Clone)]
-pub enum Message {
- JID(String),
- Password(String),
- RememberMe,
- Submit,
- Error(Error),
-}
-
-#[derive(Debug, Clone)]
-pub enum Error {
- InvalidJID,
-}
-
-pub enum Action {
- None,
- ClientCreated(Task<crate::Message>),
- CreateClient(String, String, bool),
-}
-
-#[derive(Serialize, Deserialize, Clone)]
-pub struct Creds {
- pub jid: String,
- pub password: String,
-}
-
-impl LoginModal {
- pub fn update(&mut self, message: Message) -> Action {
- match message {
- Message::JID(j) => {
- self.jid = j;
- Action::None
- }
- Message::Password(p) => {
- self.password = p;
- Action::None
- }
- Message::RememberMe => {
- self.remember_me = !self.remember_me;
- Action::None
- }
- Message::Submit => {
- info!("submitting login");
- let jid_str = self.jid.clone();
- let password = self.password.clone();
- let remember_me = self.remember_me.clone();
- Action::CreateClient(jid_str, password, remember_me)
- }
- Message::Error(error) => {
- self.error = Some(error);
- Action::None
- }
- }
- }
-
- pub fn view(&self) -> Element<Message> {
- container(
- column![
- text("Log In").size(24),
- column![
- column![
- text("JID").size(12),
- text_input("berry@macaw.chat", &self.jid)
- .on_input(|j| Message::JID(j))
- .on_submit(Message::Submit)
- .padding(5),
- ]
- .spacing(5),
- column![
- text("Password").size(12),
- text_input("", &self.password)
- .on_input(|p| Message::Password(p))
- .on_submit(Message::Submit)
- .secure(true)
- .padding(5),
- ]
- .spacing(5),
- checkbox("remember me", self.remember_me).on_toggle(|_| Message::RememberMe),
- button(text("Submit")).on_press(Message::Submit),
- ]
- .spacing(10)
- ]
- .spacing(20),
- )
- .width(300)
- .padding(10)
- .style(container::rounded_box)
- .into()
- }
-}
diff --git a/src/main.rs b/src/main.rs
index b785562..3db4c25 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,1477 +1,13 @@
-use std::borrow::{Borrow, Cow};
-use std::cell::{Ref, RefCell};
-use std::collections::{HashMap, HashSet};
-use std::fmt::Debug;
-use std::ops::{Deref, DerefMut};
-use std::path::{Path, PathBuf};
-use std::rc::{Rc, Weak};
-use std::str::FromStr;
-use std::sync::{Arc, Mutex};
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
-use chrono::{Local, Utc};
-use filamento::chat::{Chat, Message as ChatMessage};
-use filamento::error::{CommandError, DatabaseError};
-use filamento::files::Files;
-use filamento::presence::{Offline, Presence, PresenceType};
-use filamento::{roster::Contact, user::User, UpdateMessage};
-use iced::alignment::Horizontal::Right;
-use iced::font::{Stretch, Weight};
-use iced::futures::{SinkExt, Stream, StreamExt};
-use iced::keyboard::{on_key_press, on_key_release, Key, Modifiers};
-use iced::theme::palette::{Background, Danger, Extended, Pair, Primary, Secondary, Success};
-use iced::theme::{Custom, Palette};
-use iced::widget::button::Status;
-use iced::widget::text::{Fragment, IntoFragment, Wrapping};
-use iced::widget::{
- button, center, checkbox, column, container, horizontal_space, image, mouse_area, opaque, row,
- scrollable, stack, text, text_input, toggler, Column, Svg, Text, Toggler,
-};
-use iced::Length::{self, Fill, Shrink};
-use iced::{color, stream, Color, Element, Font, Subscription, Task, Theme};
-use icons::Icon;
-use indexmap::{indexmap, IndexMap};
-use jid::JID;
-use keyring::Entry;
-use login_modal::{Creds, LoginModal};
-use message_view::MessageView;
-use serde::{Deserialize, Serialize};
-use thiserror::Error;
-use tokio::sync::mpsc::Sender;
-use tokio::sync::{mpsc, oneshot};
-use tokio_stream::wrappers::ReceiverStream;
-use tracing::{debug, error, info};
-use uuid::Uuid;
+use leptos::prelude::*;
+use macaw_web::App;
-mod icons;
-mod login_modal;
-mod message_view;
+fn main() {
+ tracing_wasm::set_as_global_default();
+ console_error_panic_hook::set_once();
-#[derive(Serialize, Deserialize, Clone)]
-pub struct Config {
- auto_connect: bool,
- storage_dir: Option<String>,
- dburl: Option<String>,
- message_view_config: message_view::Config,
-}
-
-impl Default for Config {
- fn default() -> Self {
- Self {
- auto_connect: true,
- storage_dir: None,
- dburl: None,
- message_view_config: message_view::Config::default(),
- }
- }
-}
-
-// any object that references another contains an arc to that object, so that items can be garbage-collected by checking reference count
-// maybe have a cache which is a set of an enum of reference counted objects, so that when an object is needed it's first cloned from the set, otherwise it is added then cloned. then once an object is no longer needed, it is automatically garbage collected.
-// or maybe have the cache items automatically drop themselves at 1 reference? some kind of custom pointer. items in the cache must be easily addressable and updateable.
-pub struct Macaw {
- client: Account,
- config: Config,
- presences: HashMap<JID, Presence>,
- subscription_requests: HashSet<MacawUser>,
- new_chat: Option<NewChat>,
- // references chats, users, messages
- open_chat: Option<MessageView>,
- // references users, contacts
- roster: HashMap<JID, MacawContact>,
- // references chats, users, messages
- chats_list: IndexMap<JID, ChatListItem>,
-}
-
-#[derive(Debug, Clone)]
-pub struct MacawMessage {
- inner: ChatMessage,
- from: MacawUser,
-}
-
-impl Deref for MacawMessage {
- type Target = ChatMessage;
-
- fn deref(&self) -> &Self::Target {
- &self.inner
- }
-}
-
-impl DerefMut for MacawMessage {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.inner
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct MacawUser {
- inner: User,
- // contact not needed, as can always query the roster store to get this option.
- // contact: Option<Contact>,
-}
-
-impl Deref for MacawUser {
- type Target = User;
-
- fn deref(&self) -> &Self::Target {
- &self.inner
- }
-}
-
-impl DerefMut for MacawUser {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.inner
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct MacawContact {
- inner: Contact,
- user: User,
-}
-
-impl Deref for MacawContact {
- type Target = Contact;
-
- fn deref(&self) -> &Self::Target {
- &self.inner
- }
-}
-
-impl DerefMut for MacawContact {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.inner
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct MacawChat {
- inner: Chat,
- user: MacawUser,
-}
-
-pub struct ChatListItem {
- // references chats
- inner: MacawChat,
- // references users, messages
- latest_message: Option<MacawMessage>,
-}
-
-impl Deref for ChatListItem {
- type Target = MacawChat;
-
- fn deref(&self) -> &Self::Target {
- &self.inner
- }
-}
-
-impl DerefMut for ChatListItem {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.inner
- }
-}
-
-impl Deref for MacawChat {
- type Target = Chat;
-
- fn deref(&self) -> &Self::Target {
- &self.inner
- }
-}
-
-impl DerefMut for MacawChat {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.inner
- }
-}
-
-pub struct NewChat;
-
-impl Macaw {
- pub fn new(client: Option<Client>, config: Config) -> Self {
- let account;
- if let Some(client) = client {
- account = Account::LoggedIn(client);
- } else {
- account = Account::LoggedOut(LoginModal::default());
- }
-
- Self {
- client: account,
- config,
- presences: HashMap::new(),
- subscription_requests: HashSet::new(),
- new_chat: None,
- open_chat: None,
- roster: HashMap::new(),
- chats_list: IndexMap::new(),
- }
- }
-}
-
-pub enum Account {
- LoggedIn(Client),
- LoggedOut(LoginModal),
-}
-
-impl Account {
- pub fn is_connected(&self) -> bool {
- match self {
- Account::LoggedIn(client) => client.connection_state.is_connected(),
- Account::LoggedOut(login_modal) => false,
- }
- }
-
- pub fn connection_status(&self) -> String {
- match self {
- Account::LoggedIn(client) => match client.connection_state {
- ConnectionState::Online => "online".to_string(),
- ConnectionState::Connecting => "connecting".to_string(),
- ConnectionState::Offline => "offline".to_string(),
- },
- Account::LoggedOut(login_modal) => "no account".to_string(),
- }
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct Client {
- client: filamento::Client<Files>,
- files_root: PathBuf,
- jid: JID,
- status: Presence,
- connection_state: ConnectionState,
-}
-
-impl Client {
- pub fn is_connected(&self) -> bool {
- self.connection_state.is_connected()
- }
-
- pub fn files_root(&self) -> &Path {
- &self.files_root
- }
-}
-
-#[derive(Clone, Debug)]
-pub enum ConnectionState {
- Online,
- Connecting,
- Offline,
-}
-
-impl ConnectionState {
- pub fn is_connected(&self) -> bool {
- match self {
- ConnectionState::Online => true,
- ConnectionState::Connecting => false,
- ConnectionState::Offline => false,
- }
- }
-}
-
-impl DerefMut for Client {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.client
- }
-}
-
-impl Deref for Client {
- type Target = filamento::Client<Files>;
-
- fn deref(&self) -> &Self::Target {
- &self.client
- }
-}
-
-async fn filamento(
- jid: &JID,
- creds: &Creds,
- cfg: &Config,
-) -> (
- (filamento::Client<Files>, mpsc::Receiver<UpdateMessage>),
- PathBuf,
-) {
- let filamento;
- if let Some(ref dburl) = cfg.dburl {
- // TODO: have some sort of crash popup for this stuff
- let db_path = dburl.strip_prefix("sqlite://").unwrap_or(&dburl);
- let db_path = PathBuf::from_str(db_path).expect("invalid database path");
- let db = filamento::db::Db::create_connect_and_migrate(db_path)
- .await
- .unwrap();
- let files;
- if let Some(ref dir) = cfg.storage_dir {
- let mut data_dir = PathBuf::from_str(&dir).expect("invalid storage directory path");
- data_dir.push(creds.jid.clone());
- let files_dir = data_dir.join("files");
- files = Files::new(&files_dir);
- if !tokio::fs::try_exists(&files_dir)
- .await
- .expect("could not read storage directory")
- {
- tokio::fs::create_dir_all(&files_dir)
- .await
- .expect("could not create file storage directory")
- }
- filamento = (
- filamento::Client::new(jid.clone(), creds.password.to_string(), db, files),
- files_dir,
- );
- } else {
- let mut data_dir = dirs::data_dir().expect(
- "operating system does not support retreiving determining default data dir",
- );
- data_dir.push("macaw");
- data_dir.push(creds.jid.clone());
- data_dir.push("files");
- let files_dir = data_dir;
- files = Files::new(&files_dir);
- if !tokio::fs::try_exists(&files_dir)
- .await
- .expect("could not read storage directory")
- {
- tokio::fs::create_dir_all(&files_dir)
- .await
- .expect("could not create file storage directory")
- }
- filamento = (
- filamento::Client::new(jid.clone(), creds.password.to_string(), db, files),
- files_dir,
- );
- }
- } else {
- if let Some(ref dir) = cfg.storage_dir {
- let mut data_dir = PathBuf::from_str(&dir).expect("invalid storage directory path");
- data_dir.push(creds.jid.clone());
- let files_dir = data_dir.join("files");
- let files = Files::new(&files_dir);
- data_dir.push(format!("{}.db", creds.jid.clone()));
- let db = filamento::db::Db::create_connect_and_migrate(data_dir)
- .await
- .unwrap();
- if !tokio::fs::try_exists(&files_dir)
- .await
- .expect("could not read storage directory")
- {
- tokio::fs::create_dir_all(&files_dir)
- .await
- .expect("could not create file storage directory")
- }
- filamento = (
- filamento::Client::new(jid.clone(), creds.password.to_string(), db, files),
- files_dir,
- );
- } else {
- let mut data_dir = dirs::data_dir().expect(
- "operating system does not support retreiving determining default data dir",
- );
- data_dir.push("macaw");
- data_dir.push(creds.jid.clone());
- let files_dir = data_dir.join("files");
- let files = Files::new(&files_dir);
- data_dir.push(format!("{}.db", creds.jid.clone()));
- info!("db_path: {:?}", data_dir);
- let db = filamento::db::Db::create_connect_and_migrate(data_dir)
- .await
- .unwrap();
- if !tokio::fs::try_exists(&files_dir)
- .await
- .expect("could not read storage directory")
- {
- tokio::fs::create_dir_all(&files_dir)
- .await
- .expect("could not create file storage directory")
- }
- filamento = (
- filamento::Client::new(jid.clone(), creds.password.to_string(), db, files),
- files_dir,
- );
- }
- }
- filamento
-}
-
-#[tokio::main]
-async fn main() -> iced::Result {
- tracing_subscriber::fmt::init();
-
- let cfg: Config = confy::load("macaw", None).unwrap_or_default();
- let entry = Entry::new("macaw", "macaw");
- let mut client_creation_error: Option<Error> = None;
- let mut creds: Option<Creds> = None;
-
- match entry {
- Ok(e) => {
- let result = e.get_password();
- match result {
- Ok(c) => {
- let result = toml::from_str(&c);
- match result {
- Ok(c) => creds = Some(c),
- Err(e) => {
- client_creation_error =
- Some(Error::CredentialsLoad(CredentialsLoadError::Toml(e.into())))
- }
- }
- }
- Err(e) => match e {
- keyring::Error::NoEntry => {}
- _ => {
- client_creation_error = Some(Error::CredentialsLoad(
- CredentialsLoadError::Keyring(e.into()),
- ))
- }
- },
- }
- }
- Err(e) => {
- client_creation_error = Some(Error::CredentialsLoad(CredentialsLoadError::Keyring(
- e.into(),
- )))
- }
- }
-
- let mut client: Option<(
- JID,
- filamento::Client<Files>,
- mpsc::Receiver<UpdateMessage>,
- PathBuf,
- )> = None;
- if let Some(creds) = creds {
- let jid = creds.jid.parse::<JID>();
- match jid {
- Ok(jid) => {
- let ((handle, updates), files_dir) = filamento(&jid, &creds, &cfg).await;
- client = Some((jid, handle, updates, files_dir));
- }
- Err(e) => client_creation_error = Some(Error::CredentialsLoad(e.into())),
- }
- }
-
- if let Some((jid, luz_handle, update_recv, files_root)) = client {
- let stream = ReceiverStream::new(update_recv);
- let stream = stream.map(|message| Message::Luz(message));
- let task = {
- let luz_handle1 = luz_handle.clone();
- let luz_handle2 = luz_handle.clone();
- if cfg.auto_connect {
- Task::batch([
- Task::batch([
- Task::perform(
- async move { luz_handle1.get_roster_with_users().await },
- |result| {
- let roster = result.unwrap();
- let mut macaw_roster = HashMap::new();
- for (contact, user) in roster {
- macaw_roster.insert(
- contact.user_jid.clone(),
- MacawContact {
- inner: contact,
- user,
- },
- );
- }
- Message::Roster(macaw_roster)
- },
- ),
- Task::perform(
- async move {
- luz_handle2
- .get_chats_ordered_with_latest_messages_and_users()
- .await
- },
- |chats| {
- let chats = chats.unwrap();
- info!("got chats: {:?}", chats);
- Message::GotChats(chats)
- },
- ),
- ])
- .chain(Task::done(Message::Connect)),
- Task::stream(stream),
- ])
- } else {
- debug!("no auto connect");
- Task::batch([
- Task::perform(
- async move { luz_handle1.get_roster_with_users().await },
- |result| {
- let roster = result.unwrap();
- let mut macaw_roster = HashMap::new();
- for (contact, user) in roster {
- macaw_roster.insert(
- contact.user_jid.clone(),
- MacawContact {
- inner: contact,
- user,
- },
- );
- }
- Message::Roster(macaw_roster)
- },
- ),
- Task::perform(
- async move {
- luz_handle2
- .get_chats_ordered_with_latest_messages_and_users()
- .await
- },
- |chats| {
- let chats = chats.unwrap();
- info!("got chats: {:?}", chats);
- Message::GotChats(chats)
- },
- ),
- Task::stream(stream),
- ])
- }
- };
- let mut font = Font::with_name("K2D");
- font.weight = Weight::Medium;
- // font.stretch = Stretch::Condensed;
- iced::application("Macaw", Macaw::update, Macaw::view)
- .font(include_bytes!("../assets/fonts/Diolce-Regular.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Italic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Thin.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ExtraBold.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ExtraLightItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ExtraLight.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-BoldItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-MediumItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ThinItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Medium.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Bold.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Regular.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ExtraBoldItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-LightItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-SemiBoldItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-SemiBold.ttf"))
- .default_font(font)
- .subscription(subscription)
- .theme(Macaw::theme)
- .run_with(|| {
- (
- Macaw::new(
- Some(Client {
- client: luz_handle,
- jid,
- status: Presence {
- timestamp: Utc::now(),
- presence: PresenceType::Offline(Offline::default()),
- },
- connection_state: ConnectionState::Offline,
- files_root,
- }),
- cfg,
- ),
- task,
- )
- })
- } else {
- if let Some(e) = client_creation_error {
- iced::application("Macaw", Macaw::update, Macaw::view)
- .font(include_bytes!("../assets/fonts/Diolce-Regular.otf"))
- .font(include_bytes!("../assets/fonts/K2D-Italic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Thin.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ExtraBold.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ExtraLightItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ExtraLight.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-BoldItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-MediumItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ThinItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Medium.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Bold.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Regular.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ExtraBoldItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-LightItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-SemiBoldItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-SemiBold.ttf"))
- .default_font(Font::with_name("K2D"))
- .theme(Macaw::theme)
- .run_with(|| (Macaw::new(None, cfg), Task::done(Message::Error(e))))
- } else {
- iced::application("Macaw", Macaw::update, Macaw::view)
- .font(include_bytes!("../assets/fonts/Diolce-Regular.otf"))
- .font(include_bytes!("../assets/fonts/K2D-Italic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Thin.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ExtraBold.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ExtraLightItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ExtraLight.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-BoldItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-MediumItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ThinItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Medium.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Bold.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-Regular.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-ExtraBoldItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-LightItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-SemiBoldItalic.ttf"))
- .font(include_bytes!("../assets/fonts/K2D-SemiBold.ttf"))
- .default_font(Font::with_name("K2D"))
- .theme(Macaw::theme)
- .run_with(|| (Macaw::new(None, cfg), Task::none()))
- }
- }
-}
-
-fn subscription(state: &Macaw) -> Subscription<Message> {
- Subscription::batch([press_subscription(state), release_subscription(state)])
-}
-
-fn press_subscription(_state: &Macaw) -> Subscription<Message> {
- on_key_press(handle_key_press)
-}
-
-fn handle_key_press(key: Key, r#mod: Modifiers) -> Option<Message> {
- match key {
- Key::Named(iced::keyboard::key::Named::Shift) => Some(Message::ShiftPressed),
- _ => None,
- }
-}
-
-fn release_subscription(_state: &Macaw) -> Subscription<Message> {
- on_key_release(handle_key_release)
-}
-
-fn handle_key_release(key: Key, r#mod: Modifiers) -> Option<Message> {
- match key {
- Key::Named(iced::keyboard::key::Named::Shift) => Some(Message::ShiftReleased),
- _ => None,
- }
-}
-
-#[derive(Debug, Clone)]
-pub enum Message {
- ShiftPressed,
- ShiftReleased,
- LoginModal(login_modal::Message),
- ClientCreated(Client),
- Luz(UpdateMessage),
- Roster(HashMap<JID, MacawContact>),
- Connect,
- Disconnect,
- GotChats(Vec<((Chat, User), (ChatMessage, User))>),
- ToggleChat(JID),
- SendMessage(JID, String),
- Error(Error),
- MessageView(message_view::Message),
-}
-
-#[derive(Debug, Error, Clone)]
-pub enum Error {
- #[error("failed to create Luz client: {0}")]
- ClientCreation(#[from] filamento::error::DatabaseError),
- #[error("failed to save credentials: {0}")]
- CredentialsSave(CredentialsSaveError),
- #[error("failed to load credentials: {0}")]
- CredentialsLoad(CredentialsLoadError),
- #[error("failed to retreive messages for chat {0}")]
- MessageHistory(JID, CommandError<filamento::error::DatabaseError>),
-}
-
-#[derive(Debug, Error, Clone)]
-pub enum CredentialsSaveError {
- #[error("keyring: {0}")]
- Keyring(Arc<keyring::Error>),
- #[error("toml serialisation: {0}")]
- Toml(#[from] toml::ser::Error),
-}
-
-impl From<keyring::Error> for CredentialsSaveError {
- fn from(e: keyring::Error) -> Self {
- Self::Keyring(Arc::new(e))
- }
-}
-
-#[derive(Debug, Error, Clone)]
-pub enum CredentialsLoadError {
- #[error("keyring: {0}")]
- Keyring(Arc<keyring::Error>),
- #[error("toml serialisation: {0}")]
- Toml(#[from] toml::de::Error),
- #[error("invalid jid: {0}")]
- JID(#[from] jid::ParseError),
-}
-
-impl From<keyring::Error> for CredentialsLoadError {
- fn from(e: keyring::Error) -> Self {
- Self::Keyring(Arc::new(e))
- }
-}
-
-impl Macaw {
- fn update(&mut self, message: Message) -> Task<Message> {
- match message {
- Message::Luz(update_message) => match update_message {
- UpdateMessage::Online(online, vec) => match &mut self.client {
- Account::LoggedIn(client) => {
- client.status = Presence {
- timestamp: Utc::now(),
- presence: PresenceType::Online(online),
- };
- client.connection_state = ConnectionState::Online;
- let roster = vec
- .into_iter()
- .map(|(contact, user)| {
- (
- contact.user_jid.clone(),
- MacawContact {
- inner: contact,
- user,
- },
- )
- })
- .collect();
- // no need to also update users as any user updates will come in separately
- self.roster = roster;
- Task::none()
- }
- Account::LoggedOut(login_modal) => Task::none(),
- },
- UpdateMessage::Offline(offline) => {
- // TODO: update all contacts' presences to unknown (offline)
- match &mut self.client {
- Account::LoggedIn(client) => {
- client.status = Presence {
- timestamp: Utc::now(),
- presence: PresenceType::Offline(offline),
- };
- client.connection_state = ConnectionState::Offline;
- Task::none()
- }
- Account::LoggedOut(login_modal) => Task::none(),
- }
- }
- UpdateMessage::RosterUpdate(contact, user) => {
- self.roster.insert(
- contact.user_jid.clone(),
- MacawContact {
- inner: contact,
- user,
- },
- );
- Task::none()
- }
- UpdateMessage::RosterDelete(jid) => {
- self.roster.remove(&jid);
- Task::none()
- }
- UpdateMessage::Presence { from, presence } => {
- // TODO: presence handling
- info!("got presence from {:?} {:?}", from, presence);
- self.presences.insert(from.as_bare(), presence);
- Task::none()
- }
- UpdateMessage::Message { to, message, from } => {
- let message = MacawMessage {
- inner: message,
- from: MacawUser { inner: from },
- };
- if let Some((chat_jid, mut chat_list_item)) =
- self.chats_list.shift_remove_entry(&to)
- {
- chat_list_item.latest_message = Some(message.clone());
- self.chats_list.insert_before(0, chat_jid, chat_list_item);
- if let Some(open_chat) = &mut self.open_chat {
- if open_chat.chat.user.jid == to {
- open_chat.update(message_view::Message::Message(message));
- }
- }
- } else {
- // TODO: get the actual chat from the thing, or send the chat first, from the client side.
- let chat = Chat {
- correspondent: to.clone(),
- have_chatted: false,
- };
- let chat_list_item = ChatListItem {
- inner: MacawChat {
- inner: chat,
- user: message.from.clone(),
- },
- latest_message: Some(message),
- };
- self.chats_list.insert_before(0, to, chat_list_item);
- }
- Task::none()
- }
- UpdateMessage::SubscriptionRequest(jid) => {
- // TODO: subscription requests
- Task::none()
- }
- UpdateMessage::MessageDelivery { chat, id, delivery } => {
- if let Some(chat_list_item) = self.chats_list.get_mut(&chat) {
- if let Some(latest_message) = &mut chat_list_item.latest_message {
- if latest_message.id == id {
- latest_message.delivery = Some(delivery)
- }
- }
- }
- if let Some(open_chat) = &mut self.open_chat {
- if let Some(message) = open_chat.messages.get_mut(&id) {
- message.delivery = Some(delivery)
- }
- }
- Task::none()
- }
- UpdateMessage::NickChanged { jid, nick } => {
- // roster, chats_list, open chat
- if let Some(contact) = self.roster.get_mut(&jid) {
- contact.user.nick = nick.clone();
- }
- if let Some(chats_list_item) = self.chats_list.get_mut(&jid) {
- chats_list_item.user.nick = nick.clone()
- }
- if let Some(open_chat) = &mut self.open_chat {
- for (_, message) in &mut open_chat.messages {
- if message.from.jid == jid {
- message.from.nick = nick.clone()
- }
- }
- if open_chat.chat.user.jid == jid {
- open_chat.chat.user.nick = nick
- }
- }
- Task::none()
- }
- UpdateMessage::AvatarChanged { jid, id } => {
- // roster, chats_list, open chat
- if let Some(contact) = self.roster.get_mut(&jid) {
- contact.user.avatar = id.clone();
- }
- if let Some(chats_list_item) = self.chats_list.get_mut(&jid) {
- chats_list_item.user.avatar = id.clone()
- }
- if let Some(open_chat) = &mut self.open_chat {
- // TODO: consider using an indexmap with two keys for speeding this up?
- for (_, message) in &mut open_chat.messages {
- if message.from.jid == jid {
- message.from.avatar = id.clone()
- }
- }
- if open_chat.chat.user.jid == jid {
- open_chat.chat.user.avatar = id
- }
- }
- Task::none()
- }
- },
- // TODO: NEXT
- Message::ClientCreated(client) => {
- self.client = Account::LoggedIn(client.clone());
- let client1 = client.clone();
- let client2 = client.clone();
- if self.config.auto_connect {
- Task::batch([
- Task::perform(
- async move { client1.client.get_roster_with_users().await },
- |result| {
- let roster = result.unwrap();
- let mut macaw_roster = HashMap::new();
- for (contact, user) in roster {
- macaw_roster.insert(
- contact.user_jid.clone(),
- MacawContact {
- inner: contact,
- user,
- },
- );
- }
- // TODO: clean this up
- Message::Roster(macaw_roster)
- },
- ),
- Task::perform(
- async move {
- client2
- .client
- .get_chats_ordered_with_latest_messages_and_users()
- .await
- },
- |chats| {
- let chats = chats.unwrap();
- // let chats: HashMap<JID, (Chat, IndexMap<Uuid, ChatMessage>)> = chats
- // .into_iter()
- // .map(|chat| (chat.correspondent.clone(), (chat, IndexMap::new())))
- // .collect();
- info!("got chats: {:?}", chats);
- Message::GotChats(chats)
- },
- ),
- ])
- .chain(Task::done(Message::Connect))
- } else {
- Task::batch([
- Task::perform(
- async move { client1.client.get_roster_with_users().await },
- |result| {
- let roster = result.unwrap();
- let mut macaw_roster = HashMap::new();
- for (contact, user) in roster {
- macaw_roster.insert(
- contact.user_jid.clone(),
- MacawContact {
- inner: contact,
- user,
- },
- );
- }
- Message::Roster(macaw_roster)
- },
- ),
- Task::perform(
- async move {
- client2
- .client
- .get_chats_ordered_with_latest_messages_and_users()
- .await
- },
- |chats| {
- let chats = chats.unwrap();
- // let chats: HashMap<JID, (Chat, IndexMap<Uuid, ChatMessage>)> = chats
- // .into_iter()
- // .map(|chat| (chat.correspondent.clone(), (chat, IndexMap::new())))
- // .collect();
- info!("got chats: {:?}", chats);
- Message::GotChats(chats)
- },
- ),
- ])
- }
- }
- Message::Roster(hash_map) => {
- self.roster = hash_map;
- Task::none()
- }
- Message::Connect => match &mut self.client {
- Account::LoggedIn(client) => {
- client.connection_state = ConnectionState::Connecting;
- let client = client.client.clone();
- Task::future(async move {
- client.connect().await.unwrap();
- })
- .discard()
- }
- Account::LoggedOut(login_modal) => Task::none(),
- },
- Message::Disconnect => match &self.client {
- Account::LoggedIn(client) => {
- let client = client.client.clone();
- Task::future(async move {
- client.disconnect(Offline::default()).await.unwrap();
- })
- .discard()
- }
- Account::LoggedOut(login_modal) => Task::none(),
- },
- Message::ToggleChat(jid) => {
- match &self.open_chat {
- Some(message_view) => {
- if message_view.chat.user.jid == jid {
- self.open_chat = None;
- return Task::none();
- }
- }
- None => {}
- }
- if let Some(chat) = self.chats_list.get(&jid) {
- match &self.client {
- Account::LoggedIn(client) => {
- let client = client.clone();
- self.open_chat = Some(MessageView::new(
- (*chat).clone(),
- &self.config,
- client.files_root.clone(),
- ));
- Task::perform(
- async move { client.get_messages_with_users(jid).await },
- move |result| {
- let message_history = result.unwrap();
- let messages = message_history
- .into_iter()
- .map(|(message, user)| MacawMessage {
- inner: message,
- from: MacawUser { inner: user },
- })
- .collect();
- Message::MessageView(message_view::Message::MessageHistory(
- messages,
- ))
- },
- )
- }
- Account::LoggedOut(login_modal) => Task::none(),
- }
- } else {
- Task::none()
- }
- }
- Message::LoginModal(login_modal_message) => match &mut self.client {
- Account::LoggedIn(_client) => Task::none(),
- Account::LoggedOut(login_modal) => {
- let action = login_modal.update(login_modal_message);
- match action {
- login_modal::Action::None => Task::none(),
- login_modal::Action::CreateClient(jid, password, remember_me) => {
- let creds = Creds { jid, password };
- let jid = creds.jid.parse::<JID>();
- let config = self.config.clone();
- match jid {
- Ok(jid) => {
- Task::perform(async move {
- let (jid, creds, config) = (jid, creds, config);
- let ((handle, recv), files_root) = filamento(&jid, &creds, &config).await;
- (handle, recv, jid, creds, config, files_root)
- }, move |(handle, recv, jid, creds, config, files_root)| {
- let creds = creds;
- let mut tasks = Vec::new();
- tasks.push(Task::done(crate::Message::ClientCreated(
- Client {
- client: handle,
- jid,
- status: Presence { timestamp: Utc::now(), presence: PresenceType::Offline(Offline::default()) },
- connection_state: ConnectionState::Offline,
- files_root,
- },
- )));
- let stream = ReceiverStream::new(recv);
- let stream =
- stream.map(|message| crate::Message::Luz(message));
- tasks.push(Task::stream(stream));
-
- if remember_me {
- let entry = Entry::new("macaw", "macaw");
- match entry {
- Ok(e) => {
- let creds = toml::to_string(&creds);
- match creds {
- Ok(c) => {
- let result = e.set_password(&c);
- if let Err(e) = result {
- tasks.push(Task::done(crate::Message::Error(
- crate::Error::CredentialsSave(e.into()),
- )));
- }
- }
- Err(e) => tasks.push(Task::done(
- crate::Message::Error(
- crate::Error::CredentialsSave(
- e.into(),
- ),
- ),
- )),
- }
- }
- Err(e) => {
- tasks.push(Task::done(crate::Message::Error(
- crate::Error::CredentialsSave(e.into()),
- )))
- }
- }
- }
- tasks
- }).then(|tasks| Task::batch(tasks))
- }
- Err(e) => Task::done(Message::LoginModal(
- login_modal::Message::Error(login_modal::Error::InvalidJID),
- )),
- }
- }
- login_modal::Action::ClientCreated(task) => task,
- }
- }
- },
- Message::GotChats(chats) => {
- let mut tasks = Vec::new();
- let client = match &self.client {
- Account::LoggedIn(client) => client,
- Account::LoggedOut(_) => {
- // TODO: error into event tracing subscriber
- error!("no client, cannot retreive chat history for chats");
- return Task::none();
- }
- };
- for ((chat, chat_user), (message, message_user)) in chats {
- let chat = MacawChat {
- inner: chat,
- user: MacawUser { inner: chat_user },
- };
- let latest_message = MacawMessage {
- inner: message,
- from: MacawUser {
- inner: message_user,
- },
- };
- let chat_list_item = ChatListItem {
- inner: chat.clone(),
- latest_message: Some(latest_message),
- };
- self.chats_list
- .insert(chat.correspondent.clone(), chat_list_item);
- }
- Task::batch(tasks)
- }
- Message::SendMessage(jid, body) => {
- let client = match &self.client {
- Account::LoggedIn(client) => client.clone(),
- Account::LoggedOut(_) => {
- error!("cannot send message when no client set up");
- return Task::none();
- }
- };
- Task::future(async move {
- client
- .send_message(jid, filamento::chat::Body { body })
- .await
- })
- .discard()
- }
- Message::Error(error) => {
- error!("{}", error);
- Task::none()
- }
- Message::MessageView(message) => {
- if let Some(message_view) = &mut self.open_chat {
- let action = message_view.update(message);
- match action {
- message_view::Action::None => Task::none(),
- message_view::Action::SendMessage(m) => {
- Task::done(Message::SendMessage(message_view.chat.user.jid.clone(), m))
- }
- }
- } else {
- Task::none()
- }
- }
- Message::ShiftPressed => {
- info!("shift pressed");
- if let Some(open_chat) = &mut self.open_chat {
- open_chat.shift_pressed = true;
- }
- Task::none()
- }
- Message::ShiftReleased => {
- info!("shift released");
- if let Some(open_chat) = &mut self.open_chat {
- open_chat.shift_pressed = false;
- }
- Task::none()
- }
- }
- }
-
- fn view(&self) -> Element<Message> {
- let mut ui: Element<Message> = {
- let mut chats_list: Column<Message> = column![];
- if let Account::LoggedIn(client) = &self.client {
- for (jid, chat) in &self.chats_list {
- let mut open = false;
- if let Some(open_chat) = &self.open_chat {
- if open_chat.chat.user.jid == *jid {
- open = true;
- }
- }
- let chat_list_item = chat_list_item(
- &self.presences,
- &self.roster,
- client.files_root(),
- chat,
- open,
- );
- chats_list = chats_list.push(chat_list_item);
- }
- }
- let chats_list = scrollable(chats_list.spacing(8).padding(8))
- .spacing(1)
- .height(Fill);
-
- let connection_status = self.client.connection_status();
- let client_jid: Cow<'_, str> = match &self.client {
- Account::LoggedIn(client) => (&client.jid).into(),
- Account::LoggedOut(_) => Cow::from("no account"),
- // map(|client| (&client.jid).into());
- };
- let connected = self.client.is_connected();
-
- let account_view = container(row![
- text(client_jid),
- horizontal_space(),
- text(connection_status),
- horizontal_space().width(8),
- toggler(connected).on_toggle(|connect| {
- if connect {
- Message::Connect
- } else {
- Message::Disconnect
- }
- })
- ])
- .padding(8);
-
- // TODO: config width/resizing
- let sidebar = column![chats_list, account_view].height(Fill).width(300);
-
- let message_view;
- if let Some(open_chat) = &self.open_chat {
- message_view = open_chat.view().map(Message::MessageView)
- } else {
- message_view = column![].into();
- }
-
- row![sidebar, container(message_view).width(Fill)]
- }
- .into();
-
- if let Some(new_chat) = &self.new_chat {
- // TODO: close new chat window
- ui = modal(ui, text("new chat"), None);
- }
- // temporarily center to fill space
- // let ui = center(ui).into();
- let ui = container(ui).center_x(Fill).center_y(Fill);
-
- match &self.client {
- Account::LoggedIn(_client) => ui.into(),
- Account::LoggedOut(login_modal) => {
- let signup = login_modal.view().map(Message::LoginModal);
- modal(ui, signup, None)
- }
- }
- }
-
- fn theme(&self) -> Theme {
- let extended = Extended {
- background: Background {
- base: Pair {
- color: color!(0x503e34),
- text: color!(0xdcdcdc),
- },
- weak: Pair {
- color: color!(0x392c25),
- text: color!(0xdcdcdc),
- },
- strong: Pair {
- color: color!(0x293f2e),
- text: color!(0xdcdcdc),
- },
- },
- primary: Primary {
- base: Pair {
- color: color!(0x2b33b4),
- text: color!(0xdcdcdc),
- },
- weak: Pair {
- color: color!(0x4D4A5E),
- text: color!(0xdcdcdc),
- },
- strong: Pair {
- color: color!(0x2b33b4),
- text: color!(0xdcdcdc),
- },
- },
- secondary: Secondary {
- base: Pair {
- color: color!(0xffce07),
- text: color!(0xdcdcdc),
- },
- weak: Pair {
- color: color!(0xffce07),
- text: color!(0xdcdcdc),
- },
- strong: Pair {
- color: color!(0xffce07),
- text: color!(0xdcdcdc),
- },
- },
- success: Success {
- base: Pair {
- color: color!(0x14802E),
- text: color!(0xdcdcdc),
- },
- weak: Pair {
- color: color!(0x14802E),
- text: color!(0xdcdcdc),
- },
- strong: Pair {
- color: color!(0x14802E),
- text: color!(0xdcdcdc),
- },
- },
- danger: Danger {
- base: Pair {
- color: color!(0xC1173C),
- text: color!(0xdcdcdc),
- },
- weak: Pair {
- color: color!(0xC1173C),
- text: color!(0xdcdcdc),
- },
- strong: Pair {
- color: color!(0xC1173C),
- text: color!(0xdcdcdc),
- },
- },
- is_dark: true,
- };
- Theme::Custom(Arc::new(Custom::with_fn(
- "macaw".to_string(),
- Palette::DARK,
- |_| extended,
- )))
- // Theme::Custom(Arc::new(Custom::new(
- // "macaw".to_string(),
- // Palette {
- // background: color!(0x392c25),
- // text: color!(0xdcdcdc),
- // primary: color!(0x2b33b4),
- // success: color!(0x14802e),
- // warning: color!(0xffce07),
- // danger: color!(0xc1173c),
- // },
- // )))
- }
-}
-
-fn modal<'a, Message>(
- base: impl Into<Element<'a, Message>>,
- content: impl Into<Element<'a, Message>>,
- on_blur: Option<Message>,
-) -> Element<'a, Message>
-where
- Message: Clone + 'a,
-{
- let mut mouse_area = mouse_area(center(opaque(content)).style(|_theme| {
- container::Style {
- background: Some(
- Color {
- a: 0.8,
- ..Color::BLACK
- }
- .into(),
- ),
- ..container::Style::default()
- }
- })); // .on_press(on_blur)
- if let Some(on_blur) = on_blur {
- mouse_area = mouse_area.on_press(on_blur)
- }
- stack![base.into(), opaque(mouse_area)].into()
-}
-
-fn chat_list_item<'a>(
- presences: &HashMap<JID, Presence>,
- roster: &HashMap<JID, MacawContact>,
- file_root: &'a Path,
- chat_list_item: &'a ChatListItem,
- open: bool,
-) -> Element<'a, Message> {
- let name: String;
- if let Some(Some(contact_name)) = roster
- .get(chat_list_item.correspondent())
- .map(|contact| &contact.name)
- {
- name = contact_name.clone()
- } else if let Some(nick) = &chat_list_item.user.nick {
- name = nick.clone()
- } else {
- name = chat_list_item.correspondent().to_string();
- }
-
- let avatar: Option<String>;
- if let Some(user_avatar) = &chat_list_item.user.avatar {
- avatar = Some(user_avatar.clone())
- } else {
- avatar = None
- }
-
- let latest_message_text: Option<(String, String)>;
- if let Some(latest_message) = &chat_list_item.latest_message {
- let message = latest_message.body.body.replace("\n", " ");
- let date = latest_message.timestamp.naive_local();
- let now = Local::now().naive_local();
- let timeinfo;
- if date.date() == now.date() {
- // TODO: localisation/config
- timeinfo = date.time().format("%H:%M").to_string()
- } else {
- timeinfo = date.date().format("%d/%m").to_string()
- }
- latest_message_text = Some((message, timeinfo));
- // content = content.push(
- // row![
- // container(text(message).wrapping(Wrapping::None))
- // .clip(true)
- // .width(Fill),
- // timeinfo
- // ]
- // .spacing(8)
- // .width(Fill),
- // );
- } else {
- latest_message_text = None;
- }
-
- let mut avatar_stack = stack([]);
- if let Some(avatar) = avatar {
- let mut path = file_root.join(avatar);
- path.set_extension("jpg");
- info!("got avatar: {:?}", path);
- avatar_stack = avatar_stack.push(image(path).width(48).height(48));
- }
- let mut status_icon: Option<Icon> = None;
- if let Some(presence) = presences.get(&chat_list_item.user.jid) {
- debug!("found a presence");
- match &presence.presence {
- PresenceType::Online(online) => match online.show {
- Some(s) => match s {
- filamento::presence::Show::Away => status_icon = Some(Icon::Away16Color),
- filamento::presence::Show::Chat => status_icon = Some(Icon::Bubble16Color),
- filamento::presence::Show::DoNotDisturb => status_icon = Some(Icon::Dnd16Color),
- filamento::presence::Show::ExtendedAway => {
- status_icon = Some(Icon::Away16Color)
- }
- },
- None => status_icon = Some(Icon::Bubble16Color),
- },
- PresenceType::Offline(offline) => {}
- }
- }
- if let Some(status_icon) = status_icon {
- avatar_stack = avatar_stack.push(Into::<Svg>::into(status_icon));
- }
- let content: Element<Message> = if let Some((message, time)) = latest_message_text {
- row![
- avatar_stack,
- column![
- text(name),
- row![
- container(text(message).wrapping(Wrapping::None))
- .clip(true)
- .width(Fill),
- text(time)
- ]
- .spacing(8)
- .width(Fill)
- ]
- .spacing(8)
- ]
- .spacing(8)
- .into()
- } else {
- row![avatar_stack, text(name)].spacing(8).into()
- };
- let mut button =
- button(content).on_press(Message::ToggleChat(chat_list_item.correspondent().clone()));
- if open {
- button = button.style(|theme: &Theme, status| {
- let palette = theme.extended_palette();
- button::Style::default().with_background(palette.primary.weak.color)
- });
- }
- button.width(Fill).into()
+ leptos::mount::mount_to_body(App)
}
diff --git a/src/message.rs b/src/message.rs
new file mode 100644
index 0000000..20e37b9
--- /dev/null
+++ b/src/message.rs
@@ -0,0 +1,80 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::ops::{Deref, DerefMut};
+
+use filamento::{chat::Message, user::User};
+use leptos::prelude::*;
+use reactive_stores::ArcStore;
+use uuid::Uuid;
+
+use crate::{
+ state_store::{StateListener, StateStore},
+ user::{ArcMacawUser, MacawUser},
+};
+
+#[derive(Clone, Copy)]
+pub struct MacawMessage {
+ pub message: ArenaItem<StateListener<Uuid, ArcStore<Message>>>,
+ pub user: MacawUser,
+}
+
+impl MacawMessage {
+ pub fn get(&self) -> ArcStore<Message> {
+ self.try_get_value().unwrap().get()
+ }
+}
+
+impl Deref for MacawMessage {
+ type Target = ArenaItem<StateListener<Uuid, ArcStore<Message>>>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.message
+ }
+}
+
+impl DerefMut for MacawMessage {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.message
+ }
+}
+
+impl From<ArcMacawMessage> for MacawMessage {
+ fn from(value: ArcMacawMessage) -> Self {
+ Self {
+ message: ArenaItem::new_with_storage(value.message),
+ user: value.user.into(),
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct ArcMacawMessage {
+ pub message: StateListener<Uuid, ArcStore<Message>>,
+ pub user: ArcMacawUser,
+}
+
+impl ArcMacawMessage {
+ pub async fn got_message_and_user(message: Message, user: User) -> Self {
+ let message_state_store: StateStore<Uuid, ArcStore<Message>> =
+ use_context().expect("no message state store");
+ let message = message_state_store.store(message.id, ArcStore::new(message));
+ let user = ArcMacawUser::got_user(user).await;
+ Self { message, user }
+ }
+}
+
+impl Deref for ArcMacawMessage {
+ type Target = StateListener<Uuid, ArcStore<Message>>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.message
+ }
+}
+
+impl DerefMut for ArcMacawMessage {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.message
+ }
+}
diff --git a/src/message_subscriptions.rs b/src/message_subscriptions.rs
new file mode 100644
index 0000000..eebbef3
--- /dev/null
+++ b/src/message_subscriptions.rs
@@ -0,0 +1,89 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::collections::HashMap;
+
+use jid::BareJID;
+use tokio::sync::mpsc::{self, Receiver};
+use uuid::Uuid;
+
+use crate::message::{ArcMacawMessage, MacawMessage};
+
+pub struct MessageSubscriptions {
+ all: HashMap<Uuid, mpsc::Sender<(BareJID, ArcMacawMessage)>>,
+ subset: HashMap<BareJID, HashMap<Uuid, mpsc::Sender<ArcMacawMessage>>>,
+}
+
+impl MessageSubscriptions {
+ pub fn new() -> Self {
+ Self {
+ all: HashMap::new(),
+ subset: HashMap::new(),
+ }
+ }
+
+ pub async fn broadcast(&mut self, to: BareJID, message: ArcMacawMessage) {
+ // subscriptions to all
+ let mut removals = Vec::new();
+ for (id, sender) in &self.all {
+ match sender.send((to.clone(), message.clone())).await {
+ Ok(_) => {}
+ Err(_) => {
+ removals.push(*id);
+ }
+ }
+ }
+ for removal in removals {
+ self.all.remove(&removal);
+ }
+
+ // subscriptions to specific chat
+ if let Some(subscribers) = self.subset.get_mut(&to) {
+ let mut removals = Vec::new();
+ for (id, sender) in &*subscribers {
+ match sender.send(message.clone()).await {
+ Ok(_) => {}
+ Err(_) => {
+ removals.push(*id);
+ }
+ }
+ }
+ for removal in removals {
+ subscribers.remove(&removal);
+ }
+ if subscribers.is_empty() {
+ self.subset.remove(&to);
+ }
+ }
+ }
+
+ pub fn subscribe_all(&mut self) -> (Uuid, Receiver<(BareJID, ArcMacawMessage)>) {
+ let (send, recv) = mpsc::channel(10);
+ let id = Uuid::new_v4();
+ self.all.insert(id, send);
+ (id, recv)
+ }
+
+ pub fn subscribe_chat(&mut self, chat: BareJID) -> (Uuid, Receiver<ArcMacawMessage>) {
+ let (send, recv) = mpsc::channel(10);
+ let id = Uuid::new_v4();
+ if let Some(chat_subscribers) = self.subset.get_mut(&chat) {
+ chat_subscribers.insert(id, send);
+ } else {
+ let hash_map = HashMap::from([(id, send)]);
+ self.subset.insert(chat, hash_map);
+ }
+ (id, recv)
+ }
+
+ pub fn unsubscribe_all(&mut self, sub_id: Uuid) {
+ self.all.remove(&sub_id);
+ }
+
+ pub fn unsubscribe_chat(&mut self, sub_id: Uuid, chat: BareJID) {
+ if let Some(chat_subs) = self.subset.get_mut(&chat) {
+ chat_subs.remove(&sub_id);
+ }
+ }
+}
diff --git a/src/message_view.rs b/src/message_view.rs
deleted file mode 100644
index f5319bd..0000000
--- a/src/message_view.rs
+++ /dev/null
@@ -1,280 +0,0 @@
-use std::{path::PathBuf, time::Duration};
-
-use chrono::{NaiveDate, NaiveDateTime, TimeDelta};
-use iced::color;
-use iced::widget::text_editor;
-use iced::{
- border::Radius,
- font::{Style, Weight},
- widget::{button, column, container, image, row, scrollable, text, text_editor::Content},
- Border, Color, Element, Font,
- Length::{Fill, Shrink},
- Theme,
-};
-use indexmap::IndexMap;
-use jid::JID;
-use serde::{Deserialize, Serialize};
-use uuid::Uuid;
-
-use crate::{icons::Icon, MacawChat, MacawMessage};
-
-pub struct MessageView {
- pub file_root: PathBuf,
- // references chats, users
- pub chat: MacawChat,
- // references users, messages
- pub messages: IndexMap<Uuid, MacawMessage>,
- pub config: Config,
- pub new_message: Content,
- pub shift_pressed: bool,
-}
-
-#[derive(Serialize, Deserialize, Clone)]
-pub struct Config {
- pub send_on_enter: bool,
-}
-
-impl Default for Config {
- fn default() -> Self {
- Self {
- send_on_enter: true,
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub enum Message {
- MessageHistory(Vec<MacawMessage>),
- Message(MacawMessage),
- MessageCompose(text_editor::Action),
- SendMessage(String),
-}
-
-pub enum Action {
- None,
- SendMessage(String),
-}
-
-impl MessageView {
- pub fn new(chat: MacawChat, config: &super::Config, file_root: PathBuf) -> Self {
- Self {
- chat,
- // TODO: save position in message history
- messages: IndexMap::new(),
- // TODO: save draft (as part of chat struct?)
- new_message: Content::new(),
- config: config.message_view_config.clone(),
- // TODO: have centralised modifier state location?
- shift_pressed: false,
- file_root,
- }
- }
-
- pub fn update(&mut self, message: Message) -> Action {
- match message {
- Message::MessageCompose(a) => {
- match &a {
- text_editor::Action::Edit(edit) => match edit {
- text_editor::Edit::Enter => {
- if self.config.send_on_enter {
- if !self.shift_pressed {
- let message = self.new_message.text();
- self.new_message = Content::new();
- return Action::SendMessage(message);
- }
- } else {
- if self.shift_pressed {
- let message = self.new_message.text();
- self.new_message = Content::new();
- return Action::SendMessage(message);
- }
- }
- }
- _ => {}
- },
- _ => {}
- }
- self.new_message.perform(a);
- Action::None
- }
- Message::SendMessage(m) => {
- self.new_message = Content::new();
- Action::SendMessage(m)
- }
- Message::MessageHistory(macaw_messages) => {
- if self.messages.is_empty() {
- self.messages = macaw_messages
- .into_iter()
- .map(|message| (message.id, message))
- .collect()
- } else {
- for message in macaw_messages {
- let index = match self
- .messages
- .binary_search_by(|_, value| value.timestamp.cmp(&message.timestamp))
- {
- Ok(i) => i,
- Err(i) => i,
- };
- self.messages.insert_before(index, message.id, message);
- }
- }
- Action::None
- }
- Message::Message(macaw_message) => {
- if let Some((_, last)) = self.messages.last() {
- if last.timestamp < macaw_message.timestamp {
- self.messages.insert(macaw_message.id, macaw_message);
- } else {
- let index = match self.messages.binary_search_by(|_, value| {
- value.timestamp.cmp(&macaw_message.timestamp)
- }) {
- Ok(i) => i,
- Err(i) => i,
- };
- self.messages
- .insert_before(index, macaw_message.id, macaw_message);
- }
- } else {
- self.messages.insert(macaw_message.id, macaw_message);
- }
- Action::None
- }
- }
- }
-
- pub fn view(&self) -> Element<Message> {
- let mut messages_view = column![].spacing(8).padding(8);
- let mut last_timestamp = NaiveDateTime::MIN;
- let mut last_user: Option<JID> = None;
- for (_id, message) in &self.messages {
- let message_timestamp = message.timestamp.naive_local();
- if message_timestamp.date() > last_timestamp.date() {
- messages_view = messages_view.push(date(message_timestamp.date()));
- }
- if last_user.as_ref() != Some(&message.from.jid)
- || message_timestamp - last_timestamp > TimeDelta::minutes(3)
- {
- messages_view = messages_view.push(self.message(message, true));
- } else {
- messages_view = messages_view.push(self.message(message, false));
- }
- last_user = Some(message.from.jid.clone());
- last_timestamp = message_timestamp;
- }
- let text_editor = text_editor(&self.new_message)
- .placeholder("new message")
- .on_action(Message::MessageCompose)
- .wrapping(text::Wrapping::WordOrGlyph)
- .style(|theme, status| text_editor::Style {
- background: color!(0xdcdcdc).into(),
- border: Border {
- color: Color::BLACK,
- width: 0.0,
- radius: 0.into(),
- },
- icon: color!(0x00000000),
- placeholder: color!(0xacacac),
- value: color!(0x000000),
- selection: color!(0xffce07),
- });
- let message_send_input = row![
- text_editor,
- // button(Icon::NewBubble24).on_press(Message::SendMessage(self.new_message.text()))
- ]
- .padding(8);
- column![
- self.header(),
- scrollable(messages_view)
- .height(Fill)
- .width(Fill)
- .spacing(1)
- .anchor_bottom(),
- message_send_input
- ]
- .into()
- }
-
- pub fn header(&self) -> Element<'_, Message> {
- // TODO: contact stored here for name
- let mut bold = Font::with_name("K2D");
- bold.weight = Weight::Bold;
- let mut sweet = Font::with_name("Diolce");
- sweet.style = Style::Italic;
- let mut name_and_jid = column![];
- if let Some(nick) = &self.chat.user.nick {
- name_and_jid = name_and_jid.push(text(nick).font(bold).size(20));
- }
- let jid = self.chat.user.jid.as_bare().to_string();
- name_and_jid = name_and_jid.push(text(jid).font(sweet));
- let mut header = row![];
- if let Some(avatar) = &self.chat.user.avatar {
- let mut path = self.file_root.join(avatar);
- path.set_extension("jpg");
- header = header.push(container(image(path).width(48).height(48)));
- }
- header = header.push(name_and_jid);
- container(
- container(header.spacing(8).padding(8))
- .style(|theme: &Theme| {
- container::Style::default()
- .background(theme.extended_palette().background.strong.color)
- })
- .width(Fill),
- )
- .padding(8)
- .width(Fill)
- .into()
- }
-
- pub fn message<'a>(&'a self, message: &'a MacawMessage, major: bool) -> Element<'a, Message> {
- let timestamp = message.timestamp.naive_local();
- let timestamp = timestamp.time().format("%H:%M").to_string();
-
- if major {
- let nick: String = if let Some(nick) = &message.from.nick {
- nick.to_string()
- } else {
- message.from.jid.as_bare().to_string()
- };
- let mut bold = Font::with_name("K2D");
- bold.weight = Weight::Bold;
- let mut header = row![text(nick).font(bold), text(timestamp)].spacing(8);
- if let Some(delivery) = message.delivery {
- let icon = match delivery {
- filamento::chat::Delivery::Sending => Some(Icon::Sending16),
- filamento::chat::Delivery::Written => None,
- filamento::chat::Delivery::Sent => Some(Icon::Sent16),
- filamento::chat::Delivery::Delivered => Some(Icon::Delivered16),
- filamento::chat::Delivery::Read => Some(Icon::Delivered16),
- filamento::chat::Delivery::Failed => Some(Icon::Error16Color),
- filamento::chat::Delivery::Queued => Some(Icon::Sending16),
- };
- if let Some(icon) = icon {
- header = header.push(icon);
- }
- }
- let message_right = column![header, text(&message.body.body)].spacing(8);
- let mut major_message = row([]);
- if let Some(avatar) = &message.from.avatar {
- let mut path = self.file_root.join(avatar);
- path.set_extension("jpg");
- // info!("got avatar: {:?}", path);
- major_message = major_message.push(container(image(path).width(48).height(48)));
- }
- major_message = major_message.push(message_right);
- major_message.spacing(8).into()
- } else {
- row![
- container(text(timestamp)).width(48),
- text(&message.body.body)
- ]
- .spacing(8)
- .into()
- }
- }
-}
-
-pub fn date(date: NaiveDate) -> Element<'static, Message> {
- container(text(date.to_string())).center_x(Fill).into()
-}
diff --git a/src/open_chats.rs b/src/open_chats.rs
new file mode 100644
index 0000000..bf2eb73
--- /dev/null
+++ b/src/open_chats.rs
@@ -0,0 +1,72 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use filamento::chat::ChatStoreFields;
+use indexmap::IndexMap;
+use jid::BareJID;
+use leptos::prelude::*;
+use reactive_stores::{ArcStore, Store};
+use tracing::debug;
+
+use crate::chat::{ArcMacawChat, MacawChat};
+
+#[derive(Store, Default)]
+pub struct OpenChatsPanel {
+ // jid must be a chat in the chats map
+ chat_view: Option<BareJID>,
+ #[store(key: BareJID = |(jid, _)| jid.clone())]
+ chats: IndexMap<BareJID, ArcMacawChat>,
+}
+
+pub fn open_chat(open_chats: Store<OpenChatsPanel>, chat: ArcMacawChat) {
+ if let Some(jid) = &*open_chats.chat_view().read() {
+ if let Some((index, _jid, entry)) = open_chats.chats().write().shift_remove_full(jid) {
+ let new_jid = chat.get().correspondent().read().clone();
+ open_chats
+ .chats()
+ .write()
+ .insert_before(index, new_jid.clone(), chat);
+ *open_chats.chat_view().write() = Some(new_jid);
+ } else {
+ let new_jid = chat.get().correspondent().read().clone();
+ open_chats.chats().write().insert(new_jid.clone(), chat);
+ *open_chats.chat_view().write() = Some(new_jid);
+ }
+ } else {
+ let new_jid = chat.get().correspondent().read().clone();
+ open_chats.chats().write().insert(new_jid.clone(), chat);
+ *open_chats.chat_view().write() = Some(new_jid);
+ }
+}
+
+impl OpenChatsPanel {
+ pub fn open(&mut self, chat: ArcMacawChat) {
+ if let Some(jid) = &mut self.chat_view {
+ debug!("a chat was already open");
+ if let Some((index, _jid, entry)) = self.chats.shift_remove_full(jid) {
+ let new_jid = chat.get().correspondent().read().clone();
+ self.chats.insert_before(index, new_jid.clone(), chat);
+ *&mut self.chat_view = Some(new_jid);
+ } else {
+ let new_jid = chat.get().correspondent().read().clone();
+ self.chats.insert(new_jid.clone(), chat);
+ *&mut self.chat_view = Some(new_jid);
+ }
+ } else {
+ let new_jid = chat.get().correspondent().read().clone();
+ self.chats.insert(new_jid.clone(), chat);
+ *&mut self.chat_view = Some(new_jid);
+ }
+ debug!("opened chat");
+ }
+
+ // TODO:
+ // pub fn open_in_new_tab_unfocused(&mut self) {
+
+ // }
+
+ // pub fn open_in_new_tab_focus(&mut self) {
+
+ // }
+}
diff --git a/src/roster.rs b/src/roster.rs
new file mode 100644
index 0000000..13aed19
--- /dev/null
+++ b/src/roster.rs
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::collections::HashMap;
+
+use jid::BareJID;
+use reactive_stores::Store;
+
+use crate::contact::MacawContact;
+
+#[derive(Store, Clone)]
+pub struct Roster {
+ #[store(key: BareJID = |(jid, _)| jid.clone())]
+ contacts: HashMap<BareJID, MacawContact>,
+}
+
+impl Roster {
+ pub fn new() -> Self {
+ Self {
+ contacts: HashMap::new(),
+ }
+ }
+}
diff --git a/src/state_store.rs b/src/state_store.rs
new file mode 100644
index 0000000..1e67f34
--- /dev/null
+++ b/src/state_store.rs
@@ -0,0 +1,270 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::{
+ collections::HashMap,
+ ops::{Deref, DerefMut},
+ sync::{Arc, RwLock},
+};
+
+use leptos::prelude::*;
+use tracing::debug;
+
+// TODO: get rid of this
+// V has to be an arc signal
+#[derive(Debug)]
+pub struct ArcStateStore<K, V> {
+ store: Arc<RwLock<HashMap<K, (ArcRwSignal<V>, usize)>>>,
+}
+
+impl<K, V> PartialEq for ArcStateStore<K, V> {
+ fn eq(&self, other: &Self) -> bool {
+ Arc::ptr_eq(&self.store, &other.store)
+ }
+}
+
+impl<K, V> Clone for ArcStateStore<K, V> {
+ fn clone(&self) -> Self {
+ Self {
+ store: Arc::clone(&self.store),
+ }
+ }
+}
+
+impl<K, V> Eq for ArcStateStore<K, V> {}
+
+impl<K, V> ArcStateStore<K, V> {
+ pub fn new() -> Self {
+ Self {
+ store: Arc::new(RwLock::new(HashMap::new())),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct StateStore<K, V, S = SyncStorage> {
+ inner: ArenaItem<ArcStateStore<K, V>, S>,
+}
+
+impl<K, V, S> Dispose for StateStore<K, V, S> {
+ fn dispose(self) {
+ self.inner.dispose()
+ }
+}
+
+impl<K, V> StateStore<K, V>
+where
+ K: Send + Sync + 'static,
+ V: Send + Sync + 'static,
+{
+ pub fn new() -> Self {
+ Self::new_with_storage()
+ }
+}
+
+impl<K, V, S> StateStore<K, V, S>
+where
+ K: 'static,
+ V: 'static,
+ S: Storage<ArcStateStore<K, V>>,
+{
+ pub fn new_with_storage() -> Self {
+ Self {
+ inner: ArenaItem::new_with_storage(ArcStateStore::new()),
+ }
+ }
+}
+
+impl<K, V> StateStore<K, V, LocalStorage>
+where
+ K: 'static,
+ V: 'static,
+{
+ pub fn new_local() -> Self {
+ Self::new_with_storage()
+ }
+}
+
+impl<
+ K: std::marker::Send + std::marker::Sync + 'static,
+ V: std::marker::Send + std::marker::Sync + 'static,
+> From<ArcStateStore<K, V>> for StateStore<K, V>
+{
+ fn from(value: ArcStateStore<K, V>) -> Self {
+ Self {
+ inner: ArenaItem::new_with_storage(value),
+ }
+ }
+}
+
+impl<K: 'static, V: 'static> FromLocal<ArcStateStore<K, V>> for StateStore<K, V, LocalStorage> {
+ fn from_local(value: ArcStateStore<K, V>) -> Self {
+ Self {
+ inner: ArenaItem::new_with_storage(value),
+ }
+ }
+}
+
+impl<K, V, S> Copy for StateStore<K, V, S> {}
+
+impl<K, V, S> Clone for StateStore<K, V, S> {
+ fn clone(&self) -> Self {
+ *self
+ }
+}
+
+impl<K: Eq + std::hash::Hash + Clone + std::fmt::Debug, V: Clone + std::fmt::Debug> StateStore<K, V>
+where
+ K: Send + Sync + 'static,
+ V: Send + Sync + 'static,
+{
+ pub fn store(&self, key: K, value: V) -> StateListener<K, V> {
+ let arc_store = self.inner.try_get_value().unwrap();
+ let store = arc_store.store.clone();
+ let mut store = store.write().unwrap();
+ debug!("store state: {:?}", store);
+ if let Some((v, count)) = store.get_mut(&key) {
+ debug!("updating old value already in store");
+ v.set(value);
+ *count += 1;
+ StateListener {
+ value: v.clone(),
+ cleaner: StateCleaner {
+ key,
+ state_store: arc_store,
+ },
+ }
+ } else {
+ let v = ArcRwSignal::new(value);
+ store.insert(key.clone(), (v.clone(), 1));
+ debug!("inserting new value: {:?}", store);
+ StateListener {
+ value: v.into(),
+ cleaner: StateCleaner {
+ key,
+ state_store: arc_store,
+ },
+ }
+ }
+ }
+
+ pub fn get_listener(&self, key: K) -> Option<StateListener<K, V>> {
+ let arc_store = self.inner.try_get_value().unwrap();
+ let store = arc_store.store.clone();
+ let mut store = store.write().unwrap();
+ debug!("store state: {:?}", store);
+ if let Some((v, count)) = store.get_mut(&key) {
+ *count += 1;
+ Some(StateListener {
+ value: v.clone(),
+ cleaner: StateCleaner {
+ key,
+ state_store: arc_store,
+ },
+ })
+ } else {
+ None
+ }
+ }
+}
+
+impl<K, V> StateStore<K, V>
+where
+ K: Eq + std::hash::Hash + Send + Sync + 'static,
+ V: Send + Sync + 'static,
+{
+ pub fn update(&self, key: &K, value: V) {
+ let store = self.inner.try_get_value().unwrap();
+ let mut store = store.store.write().unwrap();
+ if let Some((v, _)) = store.get_mut(key) {
+ v.set(value)
+ }
+ }
+
+ pub fn modify(&self, key: &K, modify: impl Fn(&mut V)) {
+ let store = self.inner.try_get_value().unwrap();
+ let mut store = store.store.write().unwrap();
+ if let Some((v, _)) = store.get_mut(key) {
+ v.update(|v| modify(v));
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct StateListener<K, V>
+where
+ K: Eq + std::hash::Hash + 'static + std::marker::Send + std::marker::Sync,
+ V: 'static + std::marker::Send + std::marker::Sync,
+{
+ value: ArcRwSignal<V>,
+ cleaner: StateCleaner<K, V>,
+}
+
+impl<
+ K: std::cmp::Eq + std::hash::Hash + std::marker::Send + std::marker::Sync,
+ V: std::marker::Send + std::marker::Sync,
+> Deref for StateListener<K, V>
+{
+ type Target = ArcRwSignal<V>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.value
+ }
+}
+
+impl<K: std::cmp::Eq + std::hash::Hash + Send + Sync, V: Send + Sync> DerefMut
+ for StateListener<K, V>
+{
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.value
+ }
+}
+
+struct ArcStateCleaner<K, V> {
+ key: K,
+ state_store: ArcStateStore<K, V>,
+}
+
+struct StateCleaner<K, V>
+where
+ K: Eq + std::hash::Hash + Send + Sync + 'static,
+ V: Send + Sync + 'static,
+{
+ key: K,
+ state_store: ArcStateStore<K, V>,
+}
+
+impl<K, V> Clone for StateCleaner<K, V>
+where
+ K: Eq + std::hash::Hash + Clone + Send + Sync,
+ V: Send + Sync,
+{
+ fn clone(&self) -> Self {
+ {
+ let mut store = self.state_store.store.write().unwrap();
+ if let Some((_v, count)) = store.get_mut(&self.key) {
+ *count += 1;
+ }
+ }
+ Self {
+ key: self.key.clone(),
+ state_store: self.state_store.clone(),
+ }
+ }
+}
+
+impl<K: Eq + std::hash::Hash + Send + Sync + 'static, V: Send + Sync + 'static> Drop
+ for StateCleaner<K, V>
+{
+ fn drop(&mut self) {
+ let mut store = self.state_store.store.write().unwrap();
+ if let Some((_v, count)) = store.get_mut(&self.key) {
+ *count -= 1;
+ if *count == 0 {
+ store.remove(&self.key);
+ debug!("dropped item from store");
+ }
+ }
+ }
+}
diff --git a/src/user.rs b/src/user.rs
new file mode 100644
index 0000000..e277efd
--- /dev/null
+++ b/src/user.rs
@@ -0,0 +1,153 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::ops::{Deref, DerefMut};
+
+use filamento::user::{User, UserStoreFields};
+use jid::BareJID;
+use leptos::prelude::*;
+use reactive_stores::{ArcStore, Store};
+
+use crate::{
+ client::Client,
+ roster::{Roster, RosterStoreFields},
+ state_store::{StateListener, StateStore},
+};
+
+#[derive(Clone, Copy)]
+pub struct MacawUser {
+ pub user: ArenaItem<ArcMacawUser>,
+ // TODO: just store avatar src in user
+ // pub avatar: String,
+ // pub avatar: RwSignal<String>,
+}
+
+impl MacawUser {
+ pub fn get(&self) -> ArcStore<User> {
+ self.try_get_value().unwrap().get().0
+ }
+
+ pub fn avatar(&self) -> ArcRwSignal<String> {
+ self.try_get_value().unwrap().get().1
+ }
+}
+
+impl Deref for MacawUser {
+ type Target = ArenaItem<ArcMacawUser>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.user
+ }
+}
+
+impl DerefMut for MacawUser {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.user
+ }
+}
+
+impl From<ArcMacawUser> for MacawUser {
+ fn from(value: ArcMacawUser) -> Self {
+ Self {
+ user: ArenaItem::new_with_storage(value),
+ // avatar: value.avatar.into(),
+ }
+ }
+}
+
+impl From<MacawUser> for ArcMacawUser {
+ fn from(value: MacawUser) -> Self {
+ value.user.try_get_value().unwrap()
+ }
+}
+
+#[derive(Clone)]
+pub struct ArcMacawUser {
+ pub user: StateListener<BareJID, (ArcStore<User>, ArcRwSignal<String>)>,
+}
+
+impl ArcMacawUser {
+ pub async fn got_user(user: User) -> Self {
+ let user_state_store: StateStore<BareJID, (ArcStore<User>, ArcRwSignal<String>)> =
+ use_context().expect("no user state store");
+ let old_user = user_state_store.get_listener(user.jid.clone());
+ let user = if let Some(old_user) = old_user {
+ old_user.update(|(old_user, _avatar)| {
+ old_user.set(user);
+ });
+ old_user
+ } else {
+ let avatar = fetch_avatar(user.avatar.as_deref()).await;
+ let avatar = ArcRwSignal::new(avatar);
+ user_state_store.store(user.jid.clone(), (ArcStore::new(user), avatar))
+ };
+ let user = ArcMacawUser { user };
+ user
+ }
+}
+
+impl Deref for ArcMacawUser {
+ type Target = StateListener<BareJID, (ArcStore<User>, ArcRwSignal<String>)>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.user
+ }
+}
+
+impl DerefMut for ArcMacawUser {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.user
+ }
+}
+
+pub const NO_AVATAR: &str = "/assets/no-avatar.png";
+
+pub async fn fetch_avatar(id: Option<&str>) -> String {
+ if let Some(avatar) = id {
+ let client = use_context::<Client>().expect("client not in context");
+ if let Some(data) = client.file_store.get_src(avatar).await {
+ data
+ } else {
+ NO_AVATAR.to_string()
+ }
+ } else {
+ NO_AVATAR.to_string()
+ }
+}
+
+pub async fn get_avatar(user: Store<User>) -> String {
+ if let Some(avatar) = &user.read().avatar {
+ let client = use_context::<Client>().expect("client not in context");
+ if let Some(data) = client.file_store.get_src(avatar).await {
+ data
+ } else {
+ NO_AVATAR.to_string()
+ }
+ } else {
+ NO_AVATAR.to_string()
+ }
+}
+
+pub fn get_name(user: Store<User>, note_to_self: bool) -> String {
+ let roster: Store<Roster> = use_context().expect("no roster in context");
+ if note_to_self {
+ let client: Client = use_context().expect("no client in context");
+ if *client.jid == *user.jid().read() {
+ return "Note to self".to_string();
+ }
+ }
+ if let Some(name) = roster
+ .contacts()
+ .read()
+ .get(&user.read().jid)
+ .map(|contact| contact.read().name.clone())
+ .unwrap_or_default()
+ {
+ name.to_string()
+ } else if let Some(nick) = &user.read().nick {
+ nick.to_string()
+ } else {
+ user.read().jid.to_string()
+ }
+}
diff --git a/src/user_presences.rs b/src/user_presences.rs
new file mode 100644
index 0000000..87f9bdc
--- /dev/null
+++ b/src/user_presences.rs
@@ -0,0 +1,173 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::collections::HashMap;
+
+use chrono::Utc;
+use filamento::presence::{Offline, Presence, PresenceType, Show};
+use indexmap::IndexMap;
+use jid::BareJID;
+use leptos::prelude::*;
+use reactive_stores::Store;
+
+#[derive(Store)]
+pub struct UserPresences {
+ #[store(key: BareJID = |(jid, _)| jid.clone())]
+ pub user_presences: HashMap<BareJID, ArcRwSignal<Presences>>,
+}
+
+impl UserPresences {
+ pub fn clear(&mut self) {
+ for (_user, presences) in &mut self.user_presences {
+ presences.set(Presences::new())
+ }
+ }
+
+ // TODO: should be a bare jid
+ pub fn get_user_presences(&mut self, user: &BareJID) -> ArcRwSignal<Presences> {
+ if let Some(presences) = self.user_presences.get(user) {
+ presences.clone()
+ } else {
+ let presences = Presences::new();
+ let signal = ArcRwSignal::new(presences);
+ self.user_presences.insert(user.clone(), signal.clone());
+ signal
+ }
+ }
+}
+
+impl UserPresences {
+ pub fn new() -> Self {
+ Self {
+ user_presences: HashMap::new(),
+ }
+ }
+}
+
+pub struct Presences {
+ /// presences are sorted by time, first by type, then by last activity.
+ presences: IndexMap<String, Presence>,
+}
+
+impl Presences {
+ pub fn new() -> Self {
+ Self {
+ presences: IndexMap::new(),
+ }
+ }
+
+ /// gets the highest priority presence
+ pub fn presence(&self) -> Option<(String, Presence)> {
+ if let Some((resource, presence)) = self
+ .presences
+ .iter()
+ .filter(|(_resource, presence)| {
+ if let PresenceType::Online(online) = &presence.presence {
+ online.show == Some(Show::DoNotDisturb)
+ } else {
+ false
+ }
+ })
+ .next()
+ {
+ return Some((resource.clone(), presence.clone()));
+ }
+ if let Some((resource, presence)) = self
+ .presences
+ .iter()
+ .filter(|(_resource, presence)| {
+ if let PresenceType::Online(online) = &presence.presence {
+ online.show == Some(Show::Chat)
+ } else {
+ false
+ }
+ })
+ .next()
+ {
+ return Some((resource.clone(), presence.clone()));
+ }
+ if let Some((resource, presence)) = self
+ .presences
+ .iter()
+ .filter(|(_resource, presence)| {
+ if let PresenceType::Online(online) = &presence.presence {
+ online.show == None
+ } else {
+ false
+ }
+ })
+ .next()
+ {
+ return Some((resource.clone(), presence.clone()));
+ }
+ if let Some((resource, presence)) = self
+ .presences
+ .iter()
+ .filter(|(_resource, presence)| {
+ if let PresenceType::Online(online) = &presence.presence {
+ online.show == Some(Show::Away)
+ } else {
+ false
+ }
+ })
+ .next()
+ {
+ return Some((resource.clone(), presence.clone()));
+ }
+ if let Some((resource, presence)) = self
+ .presences
+ .iter()
+ .filter(|(_resource, presence)| {
+ if let PresenceType::Online(online) = &presence.presence {
+ online.show == Some(Show::ExtendedAway)
+ } else {
+ false
+ }
+ })
+ .next()
+ {
+ return Some((resource.clone(), presence.clone()));
+ }
+ if let Some((resource, presence)) = self
+ .presences
+ .iter()
+ .filter(|(_resource, presence)| {
+ if let PresenceType::Offline(_offline) = &presence.presence {
+ true
+ } else {
+ false
+ }
+ })
+ .next()
+ {
+ return Some((resource.clone(), presence.clone()));
+ } else {
+ None
+ }
+ }
+
+ pub fn update_presence(&mut self, resource: String, presence: Presence) {
+ let index = match self.presences.binary_search_by(|_, existing_presence| {
+ presence.timestamp.cmp(&existing_presence.timestamp)
+ }) {
+ Ok(i) => i,
+ Err(i) => i,
+ };
+ self.presences.insert_before(
+ // TODO: check if this logic is correct
+ index, resource, presence,
+ );
+ }
+
+ pub fn resource_presence(&mut self, resource: String) -> Presence {
+ if let Some(presence) = self.presences.get(&resource) {
+ presence.clone()
+ } else {
+ Presence {
+ timestamp: Utc::now(),
+ presence: PresenceType::Offline(Offline::default()),
+ }
+ }
+ }
+}
diff --git a/src/views/login_page.rs b/src/views/login_page.rs
new file mode 100644
index 0000000..d1bb29a
--- /dev/null
+++ b/src/views/login_page.rs
@@ -0,0 +1,202 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::{str::FromStr, sync::Arc};
+
+use filamento::{
+ db::Db, error::{CommandError, ConnectionError, DatabaseOpenError}, files::{opfs::OPFSError, FilesMem, FilesOPFS}, UpdateMessage
+};
+use jid::JID;
+use leptos::prelude::*;
+use thiserror::Error;
+use tokio::sync::mpsc::Receiver;
+use tracing::debug;
+
+use crate::{client::Client, files::Files};
+
+use super::AppState;
+
+#[derive(Clone, Debug, Error)]
+pub enum LoginError {
+ #[error("Missing Password")]
+ MissingPassword,
+ #[error("Missing JID")]
+ MissingJID,
+ #[error("Invalid JID: {0}")]
+ InvalidJID(#[from] jid::ParseError),
+ #[error("Connection Error: {0}")]
+ ConnectionError(#[from] CommandError<ConnectionError>),
+ #[error("Failed to open database: {0}")]
+ DatabaseOpen(#[from] DatabaseOpenError),
+ #[error("OPFS: {0}")]
+ OPFS(#[from] OPFSError),
+}
+
+#[component]
+pub fn LoginPage(
+ set_app: WriteSignal<AppState>,
+ set_client: WriteSignal<Option<(Client, Receiver<UpdateMessage>)>>,
+) -> impl IntoView {
+ let jid = RwSignal::new("".to_string());
+ let password = RwSignal::new("".to_string());
+ let remember_me = RwSignal::new(false);
+ let connect_on_login = RwSignal::new(true);
+
+ let (error, set_error) = signal(None::<LoginError>);
+ let error_message = move || {
+ error.with(|error| {
+ if let Some(error) = error {
+ view! { <div class="error">{error.to_string()}</div> }.into_any()
+ } else {
+ view! {}.into_any()
+ }
+ })
+ };
+
+ let (login_pending, set_login_pending) = signal(false);
+
+ let login = Action::new_local(move |_| {
+ async move {
+ set_login_pending.set(true);
+
+ if jid.read_untracked().is_empty() {
+ set_error.set(Some(LoginError::MissingJID));
+ set_login_pending.set(false);
+ return;
+ }
+
+ if password.read_untracked().is_empty() {
+ set_error.set(Some(LoginError::MissingPassword));
+ set_login_pending.set(false);
+ return;
+ }
+
+ let jid = match JID::from_str(&jid.read_untracked()) {
+ Ok(j) => j,
+ Err(e) => {
+ set_error.set(Some(e.into()));
+ set_login_pending.set(false);
+ return;
+ }
+ };
+
+ let remember_me = remember_me.get_untracked();
+ // initialise the client
+ let db = if remember_me {
+ debug!("creating db in opfs");
+ Db::create_connect_and_migrate(jid.as_bare().to_string())
+ .await
+ } else {
+ debug!("creating db in memory");
+ Db::create_connect_and_migrate_memory().await
+ };
+ let db = match db {
+ Ok(db) => db,
+ Err(e) => {
+ set_error.set(Some(e.into()));
+ set_login_pending.set(false);
+ return;
+ }
+ };
+ let files = if remember_me {
+ let opfs = FilesOPFS::new(jid.as_bare().to_string()).await;
+ match opfs {
+ Ok(f) => Files::Opfs(f),
+ Err(e) => {
+ set_error.set(Some(e.into()));
+ set_login_pending.set(false);
+ return;
+ }
+ }
+ } else {
+ Files::Mem(FilesMem::new())
+ };
+ let (client, updates) = filamento::Client::new(
+ jid.clone(),
+ password.read_untracked().clone(),
+ db,
+ files.clone(),
+ );
+ let resource = ArcRwSignal::new(None::<String>);
+ let client = Client {
+ client,
+ resource: resource.clone(),
+ jid: Arc::new(jid.to_bare()),
+ file_store: files,
+ };
+
+ if *connect_on_login.read_untracked() {
+ match client.connect().await {
+ Ok(r) => resource.set(Some(r)),
+ Err(e) => {
+ set_error.set(Some(e.into()));
+ set_login_pending.set(false);
+ return;
+ }
+ }
+ }
+
+ // debug!("before setting app state");
+ set_client.set(Some((client, updates)));
+ set_app.set(AppState::LoggedIn);
+ }
+ });
+
+ view! {
+ <div class="center fill">
+ <div id="login-form" class="panel">
+ <div id="hero">
+ <img src="/assets/macaw-icon.png" />
+ <h1>Macaw Instant Messenger</h1>
+ </div>
+ {error_message}
+ <form on:submit=move |ev| {
+ ev.prevent_default();
+ login.dispatch(());
+ }>
+ <label for="jid">JID</label>
+ <input
+ disabled=login_pending
+ placeholder="caw@macaw.chat"
+ type="text"
+ bind:value=jid
+ name="jid"
+ id="jid"
+ autofocus="true"
+ />
+ <label for="password">Password</label>
+ <input
+ disabled=login_pending
+ placeholder="••••••••"
+ type="password"
+ bind:value=password
+ name="password"
+ id="password"
+ />
+ <div>
+ <label for="remember_me">Remember me</label>
+ <input
+ disabled=login_pending
+ type="checkbox"
+ bind:checked=remember_me
+ name="remember_me"
+ id="remember_me"
+ />
+ </div>
+ <div>
+ <label for="connect_on_login">Connect on login</label>
+ <input
+ disabled=login_pending
+ type="checkbox"
+ bind:checked=connect_on_login
+ name="connect_on_login"
+ id="connect_on_login"
+ />
+ </div>
+ <input disabled=login_pending class="button" type="submit" value="Log In" />
+ </form>
+ </div>
+ </div>
+ }
+}
diff --git a/src/views/macaw.rs b/src/views/macaw.rs
new file mode 100644
index 0000000..e91e08a
--- /dev/null
+++ b/src/views/macaw.rs
@@ -0,0 +1,199 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::collections::{HashMap, HashSet};
+
+use filamento::{
+ UpdateMessage,
+ chat::{Chat, Message, MessageStoreFields},
+ user::User,
+};
+use jid::BareJID;
+use leptos::{prelude::*, task::spawn_local};
+use open_chats_panel::OpenChatsPanelView;
+use reactive_stores::{ArcStore, Store};
+use settings::{Settings, SettingsPage};
+use tokio::sync::mpsc::Receiver;
+use tracing::debug;
+use uuid::Uuid;
+
+use crate::{
+ client::Client,
+ components::sidebar::Sidebar,
+ contact::{ArcMacawContact, MacawContact},
+ message::{ArcMacawMessage, MacawMessage},
+ message_subscriptions::MessageSubscriptions,
+ open_chats::OpenChatsPanel,
+ roster::{Roster, RosterStoreFields},
+ state_store::StateStore,
+ user::{ArcMacawUser, MacawUser, fetch_avatar},
+ user_presences::{Presences, UserPresences},
+};
+
+use super::AppState;
+
+mod open_chats_panel;
+pub mod settings;
+
+#[component]
+pub fn Macaw(
+ // TODO: logout
+ // app_state: WriteSignal<Option<essage>)>, LocalStorage>,
+ client: Client,
+ mut updates: Receiver<UpdateMessage>,
+ set_app: WriteSignal<AppState>,
+) -> impl IntoView {
+ let (updates, set_updates) = signal(Some(updates));
+ provide_context(set_app);
+ provide_context(client);
+
+ let roster = Store::new(Roster::new());
+ provide_context(roster);
+
+ let message_subscriptions = RwSignal::new(MessageSubscriptions::new());
+ provide_context(message_subscriptions);
+
+ let messages_store: StateStore<Uuid, ArcStore<Message>> = StateStore::new();
+ provide_context(messages_store);
+ let chats_store: StateStore<BareJID, ArcStore<Chat>> = StateStore::new();
+ provide_context(chats_store);
+ let users_store: StateStore<BareJID, (ArcStore<User>, ArcRwSignal<String>)> = StateStore::new();
+ provide_context(users_store);
+
+ let open_chats = Store::new(OpenChatsPanel::default());
+ provide_context(open_chats);
+ let show_settings = RwSignal::new(None::<SettingsPage>);
+ provide_context(show_settings);
+
+ let user_presences = Store::new(UserPresences::new());
+ provide_context(user_presences);
+
+ let client_user: LocalResource<MacawUser> = LocalResource::new(move || async move {
+ let client = use_context::<Client>().expect("client not in context");
+ let user = client.get_user((*client.jid).clone()).await.unwrap();
+ ArcMacawUser::got_user(user).await.into()
+ });
+ provide_context(client_user);
+
+ // TODO: timestamp incoming/outgoing subscription requests
+ let (subscription_requests, set_subscription_requests) = signal(HashSet::<BareJID>::new());
+ provide_context(subscription_requests);
+ provide_context(set_subscription_requests);
+
+ // TODO: get cached contacts on login before getting the updated contacts
+
+ LocalResource::new(move || async move {
+ let mut updates = set_updates.write().take().expect("main loop ran twice");
+ while let Some(update) = updates.recv().await {
+ match update {
+ UpdateMessage::Online(online, items) => {
+ let mut contacts = HashMap::new();
+ for (contact, user) in items {
+ contacts.insert(
+ contact.user_jid.clone(),
+ ArcMacawContact::got_contact_and_user(contact, user)
+ .await
+ .into(),
+ );
+ }
+ roster.contacts().set(contacts);
+ }
+ UpdateMessage::Offline(offline) => {
+ // when offline, will no longer receive updated user presences, consider everybody offline.
+ user_presences.write().clear();
+ }
+ UpdateMessage::RosterUpdate(contact, user) => {
+ let new_contact = ArcMacawContact::got_contact_and_user(contact.clone(), user)
+ .await
+ .into();
+ roster.contacts().update(|roster| {
+ if let Some(macaw_contact) = roster.get_mut(&contact.user_jid) {
+ macaw_contact.set(contact);
+ } else {
+ let jid = contact.user_jid.clone();
+ roster.insert(jid, new_contact);
+ }
+ });
+ }
+ UpdateMessage::RosterDelete(jid) => {
+ roster.contacts().update(|roster| {
+ roster.remove(&jid);
+ });
+ }
+ UpdateMessage::Presence { from, presence } => {
+ let bare_jid = from.to_bare();
+ if let Some(presences) = user_presences
+ .read_untracked()
+ .user_presences
+ .get(&bare_jid)
+ {
+ if let Some(resource) = from.resourcepart() {
+ presences
+ .write()
+ .update_presence(resource.clone(), presence);
+ }
+ } else {
+ if let Some(resource) = from.resourcepart() {
+ let mut presences = Presences::new();
+ presences.update_presence(resource.clone(), presence);
+ user_presences
+ .write()
+ .user_presences
+ .insert(bare_jid, ArcRwSignal::new(presences));
+ }
+ }
+ }
+ UpdateMessage::Message { to, from, message } => {
+ debug!("before got message");
+ let new_message = ArcMacawMessage::got_message_and_user(message, from).await;
+ debug!("after got message");
+ spawn_local(async move {
+ message_subscriptions
+ .write()
+ .broadcast(to, new_message)
+ .await
+ });
+ debug!("after set message");
+ }
+ UpdateMessage::MessageDelivery { id, chat, delivery } => {
+ messages_store.modify(&id, |message| {
+ <ArcStore<filamento::chat::Message> as Clone>::clone(&message)
+ .delivery()
+ .set(Some(delivery))
+ });
+ }
+ UpdateMessage::SubscriptionRequest(jid) => {
+ set_subscription_requests.update(|req| {
+ req.insert(jid);
+ });
+ }
+ UpdateMessage::NickChanged { jid, nick } => {
+ users_store.modify(&jid, |(user, _avatar)| {
+ user.update(|user| *&mut user.nick = nick.clone())
+ });
+ }
+ UpdateMessage::AvatarChanged { jid, id } => {
+ let new_avatar = fetch_avatar(id.as_deref()).await;
+ users_store.modify(&jid, |(user, avatar)| {
+ *&mut user.write().avatar = id.clone();
+ *&mut avatar.set(new_avatar.clone())
+ });
+ }
+ }
+ }
+ });
+
+ view! {
+ <Sidebar />
+ // <ChatsList />
+ <OpenChatsPanelView />
+ {move || {
+ if let Some(_) = *show_settings.read() {
+ view! { <Settings /> }.into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ }
+}
diff --git a/src/views/macaw/open_chats_panel.rs b/src/views/macaw/open_chats_panel.rs
new file mode 100644
index 0000000..375e8f3
--- /dev/null
+++ b/src/views/macaw/open_chats_panel.rs
@@ -0,0 +1,81 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use leptos::prelude::*;
+use open_chat::OpenChatView;
+use reactive_stores::{ArcStore, Store};
+
+use crate::open_chats::{OpenChatsPanel, OpenChatsPanelStoreFields};
+
+// TODO: multiple panels
+// pub struct OpenChats {
+// panels:
+// }
+
+#[component]
+pub fn OpenChatsPanelView() -> impl IntoView {
+ let open_chats: Store<OpenChatsPanel> = use_context().expect("no open chats panel in context");
+
+ // TODO: tabs
+ // view! {
+ // {move || {
+ // if open_chats.chats().read().len() > 1 {
+ // Some(
+ // view! {
+ // <For
+ // each=move || open_chats.chats().get()
+ // key=|(jid, _)| jid.clone()
+ // let(chat)
+ // ></For>
+ // },
+ // )
+ // } else {
+ // None
+ // }
+ // }}
+ // }
+ view! {
+ <div class="open-chat-views">
+ {move || {
+ if let Some(open_chat) = open_chats.chat_view().get() {
+ if let Some(open_chat) = open_chats.chats().read().get(&open_chat) {
+ view! { <OpenChatView chat=open_chat.clone().into() /> }.into_any()
+ } else {
+ view! {}.into_any()
+ }
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ </div>
+ }
+}
+
+mod open_chat {
+ use filamento::chat::{Chat, ChatStoreFields};
+ use leptos::prelude::*;
+ use reactive_stores::{ArcStore, Store};
+
+ use crate::{
+ chat::MacawChat,
+ components::{
+ chat_header::ChatViewHeader, message_composer::ChatViewMessageComposer,
+ message_history_buffer::MessageHistoryBuffer,
+ },
+ };
+
+ #[component]
+ pub fn OpenChatView(chat: MacawChat) -> impl IntoView {
+ view! {
+ <div class="open-chat-view">
+ <ChatViewHeader chat=chat.clone() />
+ <MessageHistoryBuffer chat=chat.clone() />
+ {move || {
+ let chat_jid = chat.get().correspondent().get();
+ view! { <ChatViewMessageComposer chat=chat_jid /> }
+ }}
+ </div>
+ }
+ }
+}
diff --git a/src/views/macaw/settings.rs b/src/views/macaw/settings.rs
new file mode 100644
index 0000000..7bdc2b9
--- /dev/null
+++ b/src/views/macaw/settings.rs
@@ -0,0 +1,391 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use leptos::prelude::*;
+use profile_settings::ProfileSettings;
+
+use crate::{
+ components::{icon::IconComponent, modal::Modal},
+ icon::Icon,
+};
+
+mod profile_settings {
+ use filamento::{
+ error::{AvatarPublishError, CommandError, NickError},
+ user::User,
+ };
+ use leptos::prelude::*;
+ use thiserror::Error;
+ use web_sys::{
+ Event, FileReader, HtmlInputElement, ProgressEvent, Url,
+ js_sys::Uint8Array,
+ wasm_bindgen::{JsCast, UnwrapThrowExt, prelude::Closure},
+ };
+
+ use crate::{
+ client::Client,
+ files::Files,
+ user::{NO_AVATAR, fetch_avatar},
+ };
+
+ #[derive(Debug, Clone, Error)]
+ pub enum ProfileSaveError {
+ #[error("avatar publish: {0}")]
+ Avatar(#[from] CommandError<AvatarPublishError<Files>>),
+ #[error("nick publish: {0}")]
+ Nick(#[from] CommandError<NickError>),
+ }
+
+ #[component]
+ pub fn ProfileSettings() -> impl IntoView {
+ let client: Client = use_context().expect("no client in context");
+
+ let old_profile = LocalResource::new(move || {
+ let value = client.clone();
+ async move {
+ // TODO: error
+ let jid = &*value.jid;
+ let old_profile = value.get_user(jid.clone()).await.unwrap();
+ old_profile
+ }
+ });
+
+ view! {
+ {move || {
+ if let Some(old_profile) = old_profile.get() {
+ view! { <ProfileForm old_profile /> }.into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ }
+ }
+
+ #[component]
+ pub fn ProfileForm(old_profile: User) -> impl IntoView {
+ let client: Client = use_context().expect("no client in context");
+
+ // TODO: compartmentalise into error component, form component...
+ let (error, set_error) = signal(None::<ProfileSaveError>);
+ let error_message = move || {
+ error.with(|error| {
+ if let Some(error) = error {
+ view! { <div class="error">{error.to_string()}</div> }.into_any()
+ } else {
+ view! {}.into_any()
+ }
+ })
+ };
+
+ let (success_message, set_success_message) = signal(None::<String>);
+ let success_message = move || {
+ if let Some(message) = success_message.get() {
+ view! { <div class="success">{message}</div> }.into_any()
+ } else {
+ view! {}.into_any()
+ }
+ };
+
+ let (profile_save_pending, set_profile_save_pending) = signal(false);
+
+ let profile_upload_data = RwSignal::new(None::<Vec<u8>>);
+ let new_nick = RwSignal::new(old_profile.nick.clone().unwrap_or_default().to_string());
+ let has_avatar = RwSignal::new(old_profile.avatar.is_some());
+ let new_avatar_preview_url = RwSignal::new(None::<String>);
+ let remove_avatar = RwSignal::new(false);
+
+ let from_input = move |ev: Event| {
+ let elem = ev.target().unwrap().unchecked_into::<HtmlInputElement>();
+
+ // let UploadSignal(file_signal) = expect_context();
+ // file_signal.update(Vec::clear); // Clear list from previous change
+ let files = elem.files().unwrap_throw();
+
+ if let Some(file) = files.get(0) {
+ let url = Url::create_object_url_with_blob(&file).unwrap_throw();
+
+ new_avatar_preview_url.set(Some(url));
+ let reader = FileReader::new().unwrap_throw();
+ // let name = file.name();
+
+ // This closure only needs to be called a single time as we will just
+ // remake it on each loop
+ // * web_sys drops it for us when using this specific constructor
+ let read_file = {
+ // FileReader is cloned prior to moving into the closure
+ let reader = reader.to_owned();
+ Closure::once_into_js(move |_: ProgressEvent| {
+ // `.result` valid after the `read_*` completes on FileReader
+ // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/result
+ let result = reader.result().unwrap_throw();
+ let data = Uint8Array::new(&result).to_vec();
+ // Do whatever you want with the Vec<u8>
+ profile_upload_data.set(Some(data));
+ })
+ };
+ reader.set_onloadend(Some(read_file.as_ref().unchecked_ref()));
+
+ // read_as_array_buffer takes a &Blob
+ //
+ // Per https://w3c.github.io/FileAPI/#file-section
+ // > A File object is a Blob object with a name attribute..
+ //
+ // File is a subclass (inherits) from the Blob interface, so a File
+ // can be used anywhere a Blob is required.
+ reader.read_as_array_buffer(&file).unwrap_throw();
+
+ // You can also use `.read_as_text(&file)` instead if you just want a string.
+ // This example shows how to extract an array buffer as it is more flexible
+ //
+ // If you use `.read_as_text` change the closure from ...
+ //
+ // let result = reader.result().unwrap_throw();
+ // let vec_of_u8_bytes = Uint8Array::new(&result).to_vec();
+ // let content = String::from_utf8(vec_of_u8_bytes).unwrap_throw();
+ //
+ // to ...
+ //
+ // let result = reader.result().unwrap_throw();
+ // let content = result.as_string().unwrap_throw();
+ } else {
+ profile_upload_data.set(None);
+ }
+ };
+
+ let save_profile = Action::new_local(move |_| {
+ let client = client.clone();
+ let old_nick = old_profile.nick.clone();
+ async move {
+ set_profile_save_pending.set(true);
+
+ let new_nick = new_nick.get();
+ let new_nick = if new_nick.is_empty() {
+ None
+ } else {
+ Some(new_nick)
+ };
+ if new_nick != old_nick {
+ match client.change_nick(new_nick).await {
+ Ok(_) => {}
+ Err(e) => {
+ set_error.set(Some(ProfileSaveError::Nick(e)));
+ set_profile_save_pending.set(false);
+ return;
+ }
+ }
+ }
+
+ if let Some(profile_data) = profile_upload_data.get() {
+ match client.change_avatar(Some(profile_data)).await {
+ Ok(_) => {}
+ Err(e) => {
+ set_error.set(Some(ProfileSaveError::Avatar(e)));
+ set_profile_save_pending.set(false);
+ return;
+ }
+ }
+ } else if remove_avatar.get() {
+ match client.change_avatar(None).await {
+ Ok(_) => {}
+ Err(e) => {
+ set_error.set(Some(ProfileSaveError::Avatar(e)));
+ set_profile_save_pending.set(false);
+ return;
+ }
+ }
+ }
+
+ set_profile_save_pending.set(false);
+ set_error.set(None);
+ set_success_message.set(Some("Profile Updated!".to_string()));
+ }
+ });
+
+ let _old_account_avatar = LocalResource::new(move || {
+ let avatar = old_profile.avatar.clone();
+ async move {
+ let url = fetch_avatar(avatar.as_deref()).await;
+ new_avatar_preview_url.set(Some(url));
+ }
+ });
+
+ view! {
+ <div class="profile-settings">
+ <div class="profile-preview">
+ <h2>Profile Preview</h2>
+ <div class="preview">
+ <img class="avatar" src=new_avatar_preview_url />
+ <div class="nick">
+ {move || {
+ let nick = new_nick.get();
+ if nick.is_empty() { old_profile.jid.to_string() } else { nick }
+ }}
+ </div>
+ </div>
+ </div>
+ <form
+ class="profile-form"
+ on:submit=move |ev| {
+ ev.prevent_default();
+ save_profile.dispatch(());
+ }
+ >
+ {success_message}
+ {error_message}
+ <div>
+ <h3>Nick</h3>
+ <input
+ disabled=profile_save_pending
+ placeholder="Nick"
+ type="text"
+ id="client-user-nick"
+ bind:value=new_nick
+ name="client-user-nick"
+ />
+ </div>
+ <div>
+ <h3>Avatar</h3>
+ <div class="change-avatar">
+ <label for="client-user-avatar">
+ <div class="button">Change Avatar</div>
+ </label>
+ <input
+ type="file"
+ id="client-user-avatar"
+ on:change=move |e| {
+ has_avatar.set(true);
+ remove_avatar.set(false);
+ from_input(e);
+ }
+ />
+ {move || {
+ if has_avatar.get() {
+ view! {
+ <a
+ on:click=move |_| {
+ profile_upload_data.set(None);
+ remove_avatar.set(true);
+ has_avatar.set(false);
+ new_avatar_preview_url.set(Some(NO_AVATAR.to_string()));
+ }
+ style="cursor: pointer"
+ >
+ Remove Avatar
+ </a>
+ }
+ .into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ </div>
+ </div>
+ <hr />
+ <input
+ disabled=profile_save_pending
+ class="button"
+ type="submit"
+ value="Save Changes"
+ />
+ </form>
+ </div>
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum SettingsPage {
+ Account,
+ Chat,
+ Profile,
+ Privacy,
+}
+
+#[component]
+pub fn Settings() -> impl IntoView {
+ let show_settings: RwSignal<Option<SettingsPage>> = use_context().unwrap();
+
+ view! {
+ <Modal on_background_click=move |_| {
+ show_settings.set(None);
+ }>
+ <div class="settings panel">
+ <div class="header">
+ <h2>Settings</h2>
+ <div class="header-icon close">
+ <IconComponent
+ icon=Icon::Close24
+ on:click=move |_| show_settings.set(None)
+ />
+ </div>
+ </div>
+ <div class="settings-main">
+ <div class="settings-sidebar">
+ <div
+ class:open=move || *show_settings.read() == Some(SettingsPage::Account)
+ on:click=move |_| show_settings.set(Some(SettingsPage::Account))
+ >
+ Account
+ </div>
+ <div
+ class:open=move || *show_settings.read() == Some(SettingsPage::Chat)
+ on:click=move |_| show_settings.set(Some(SettingsPage::Chat))
+ >
+ Chat
+ </div>
+ <div
+ class:open=move || *show_settings.read() == Some(SettingsPage::Privacy)
+ on:click=move |_| show_settings.set(Some(SettingsPage::Privacy))
+ >
+ Privacy
+ </div>
+ <div
+ class:open=move || *show_settings.read() == Some(SettingsPage::Profile)
+ on:click=move |_| show_settings.set(Some(SettingsPage::Profile))
+ >
+ Profile
+ </div>
+ </div>
+ <div class="settings-page">
+ {move || {
+ if let Some(page) = show_settings.get() {
+ match page {
+ SettingsPage::Account => {
+ view! {
+ <div style="padding: 16px">
+ "Account settings coming soon!"
+ </div>
+ }
+ .into_any()
+ }
+ SettingsPage::Chat => {
+ view! {
+ <div style="padding: 16px">
+ "Chat settings coming soon!"
+ </div>
+ }
+ .into_any()
+ }
+ SettingsPage::Profile => {
+ view! { <ProfileSettings /> }.into_any()
+ }
+ SettingsPage::Privacy => {
+ view! {
+ <div style="padding: 16px">
+ "Privacy settings coming soon!"
+ </div>
+ }
+ .into_any()
+ }
+ }
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ </div>
+ </div>
+ </div>
+ </Modal>
+ }
+}
diff --git a/src/views/mod.rs b/src/views/mod.rs
new file mode 100644
index 0000000..69ba606
--- /dev/null
+++ b/src/views/mod.rs
@@ -0,0 +1,39 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use filamento::UpdateMessage;
+use leptos::prelude::*;
+use login_page::LoginPage;
+use macaw::Macaw;
+use tokio::sync::mpsc::Receiver;
+
+use crate::client::Client;
+
+pub mod login_page;
+pub mod macaw;
+
+pub enum AppState {
+ LoggedOut,
+ LoggedIn,
+}
+
+#[component]
+pub fn App() -> impl IntoView {
+ let (app, set_app) = signal(AppState::LoggedOut);
+ let (client, set_client) = signal(None::<(Client, Receiver<UpdateMessage>)>);
+
+ view! {
+ {move || match &*app.read() {
+ AppState::LoggedOut => view! { <LoginPage set_app set_client /> }.into_any(),
+ AppState::LoggedIn => {
+ if let Some((client, updates)) = set_client.write_untracked().take() {
+ view! { <Macaw client updates set_app /> }.into_any()
+ } else {
+ set_app.set(AppState::LoggedOut);
+ view! { <LoginPage set_app set_client /> }.into_any()
+ }
+ }
+ }}
+ }
+}