From ac1bdf5ac2f6ef3f591ad3194a8cb2643f58abd5 Mon Sep 17 00:00:00 2001 From: forgejo-actions Date: Thu, 6 Aug 2026 14:47:23 +0000 Subject: [PATCH 01/19] (qol): update release flake hash --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 1ed0189..2b8781f 100644 --- a/flake.nix +++ b/flake.nix @@ -17,7 +17,7 @@ systems = [ "x86_64-linux" ]; forAllSystems = nixpkgs.lib.genAttrs systems; version = "0.0.11"; - x86_64DebHash = "sha256-GtPVJ+WK7np0iGA3OXTWtc7or5biFt2AkchqPRIR9Zg="; + x86_64DebHash = "sha256-E4FGMpCt2ByaN6d+YzglqeA6LbhMOQmPf3UzAj7uZXE="; forgejoBaseUrl = "https://git.methanium.net/tensamin/client/releases/download/${version}"; in { From 73db7e6b2b7541563a81f25a44fa1b2489c01860 Mon Sep 17 00:00:00 2001 From: alois Date: Wed, 12 Aug 2026 15:12:05 +0200 Subject: [PATCH 02/19] Update flake.nix --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 2b8781f..1ed0189 100644 --- a/flake.nix +++ b/flake.nix @@ -17,7 +17,7 @@ systems = [ "x86_64-linux" ]; forAllSystems = nixpkgs.lib.genAttrs systems; version = "0.0.11"; - x86_64DebHash = "sha256-E4FGMpCt2ByaN6d+YzglqeA6LbhMOQmPf3UzAj7uZXE="; + x86_64DebHash = "sha256-GtPVJ+WK7np0iGA3OXTWtc7or5biFt2AkchqPRIR9Zg="; forgejoBaseUrl = "https://git.methanium.net/tensamin/client/releases/download/${version}"; in { From d0918ff384a356cc50fa5a4bf6db24ae6d7f41c2 Mon Sep 17 00:00:00 2001 From: forgejo-actions Date: Tue, 18 Aug 2026 20:46:44 +0000 Subject: [PATCH 03/19] (qol): update release flake hash --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 2b8781f..d91a817 100644 --- a/flake.nix +++ b/flake.nix @@ -17,7 +17,7 @@ systems = [ "x86_64-linux" ]; forAllSystems = nixpkgs.lib.genAttrs systems; version = "0.0.11"; - x86_64DebHash = "sha256-E4FGMpCt2ByaN6d+YzglqeA6LbhMOQmPf3UzAj7uZXE="; + x86_64DebHash = "sha256-p/6boNv9uWTHsPTVa3EhGzr41EwRzCY/fJWlj854DVA="; forgejoBaseUrl = "https://git.methanium.net/tensamin/client/releases/download/${version}"; in { From 57c7ceb27a0513bd887f492960ac22305462c1f1 Mon Sep 17 00:00:00 2001 From: forgejo-actions Date: Tue, 18 Aug 2026 20:46:45 +0000 Subject: [PATCH 04/19] (qol): update release flake hash --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 2b8781f..d91a817 100644 --- a/flake.nix +++ b/flake.nix @@ -17,7 +17,7 @@ systems = [ "x86_64-linux" ]; forAllSystems = nixpkgs.lib.genAttrs systems; version = "0.0.11"; - x86_64DebHash = "sha256-E4FGMpCt2ByaN6d+YzglqeA6LbhMOQmPf3UzAj7uZXE="; + x86_64DebHash = "sha256-p/6boNv9uWTHsPTVa3EhGzr41EwRzCY/fJWlj854DVA="; forgejoBaseUrl = "https://git.methanium.net/tensamin/client/releases/download/${version}"; in { From 2a55c87df10b6e5101118f7b5a9c90f416db0fd4 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 20 Aug 2026 17:05:53 +0200 Subject: [PATCH 05/19] [Updt] Mtp 0.3.0 --- .forgejo/workflows/dependency-builds.yml | 12 + apps/tauri/src-tauri/Cargo.lock | 296 +++------- apps/tauri/src-tauri/Cargo.toml | 8 +- apps/tauri/src-tauri/src/main.rs | 3 - apps/tauri/src-tauri/src/mtp_backend.rs | 613 ++++++++++++++++---- apps/web/package.json | 1 + flake.nix | 2 + package.json | 20 +- packages/call/package.json | 1 + packages/call/src/mediaShare/index.ts | 1 - packages/crypto/package.json | 1 + packages/crypto/src/callSecret.ts | 11 +- packages/crypto/src/chatSecret.ts | 11 +- packages/mtp/package.json | 3 +- packages/mtp/src/context.test.tsx | 203 ++++++- packages/mtp/src/context.tsx | 365 ++++++++---- packages/mtp/src/index.ts | 1 + packages/mtp/src/protocolFields.test.ts | 44 ++ packages/mtp/src/protocolFields.ts | 58 ++ packages/mtp/src/requestIds.test.ts | 25 + packages/mtp/src/requestIds.ts | 24 + packages/mtp/src/values.ts | 5 + packages/notifications/src/context.tsx | 12 +- packages/settings/src/layout.tsx | 50 +- packages/settings/src/manifest.ts | 56 +- packages/settings/src/navigation.ts | 31 + packages/settings/src/pages/index.tsx | 2 +- packages/settings/src/sidebar.tsx | 48 ++ pnpm-lock.yaml | 702 +++++++++++++++-------- pnpm-workspace.yaml | 2 +- utils/scripts/build-packages.ts | 9 +- utils/scripts/lint-packages.ts | 9 +- utils/scripts/update-packages.ts | 9 +- 33 files changed, 1839 insertions(+), 799 deletions(-) create mode 100644 packages/mtp/src/protocolFields.test.ts create mode 100644 packages/mtp/src/protocolFields.ts create mode 100644 packages/mtp/src/requestIds.test.ts create mode 100644 packages/mtp/src/requestIds.ts create mode 100644 packages/settings/src/navigation.ts create mode 100644 packages/settings/src/sidebar.tsx diff --git a/.forgejo/workflows/dependency-builds.yml b/.forgejo/workflows/dependency-builds.yml index 709c75b..fdc778f 100644 --- a/.forgejo/workflows/dependency-builds.yml +++ b/.forgejo/workflows/dependency-builds.yml @@ -37,6 +37,18 @@ jobs: - run: nix develop .#electron --command pnpm run build:packages - run: nix develop .#electron --command pnpm run build:desktop + native-mtp: + if: ${{ github.actor == 'rasensprenger' }} + name: Test native MTP + runs-on: nixos + steps: + - run: nix profile add nixpkgs#nodejs_24 + - uses: https://data.forgejo.org/actions/checkout@v4 + with: + persist-credentials: false + - run: git submodule update --init --recursive + - run: nix develop .#electron --command bash -lc 'cd apps/tauri/src-tauri && cargo test' + mobile: if: ${{ github.actor == 'rasensprenger' }} name: Build mobile diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index b1b1c0b..d404d61 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/Cargo.lock @@ -674,12 +674,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "const-oid" version = "0.10.2" @@ -889,7 +883,6 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest 0.10.7", "fiat-crypto 0.2.9", "rustc_version", "subtle", @@ -974,25 +967,14 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid 0.9.6", - "pem-rfc7468 0.7.0", - "zeroize", -] - [[package]] name = "der" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ - "const-oid 0.10.2", - "pem-rfc7468 1.0.0", + "const-oid", + "pem-rfc7468", "zeroize", ] @@ -1057,7 +1039,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.1", - "const-oid 0.10.2", + "const-oid", "crypto-common 0.2.2", "ctutils", ] @@ -1204,38 +1186,14 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8 0.10.2", - "signature 2.2.0", -] - [[package]] name = "ed25519" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" dependencies = [ - "pkcs8 0.11.0", - "signature 3.0.0", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek 4.1.3", - "ed25519 2.2.3", - "serde", - "sha2 0.10.9", - "subtle", - "zeroize", + "pkcs8", + "signature", ] [[package]] @@ -1245,10 +1203,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ "curve25519-dalek 5.0.0", - "ed25519 3.0.0", + "ed25519", "serde", "sha2 0.11.0", - "signature 3.0.0", + "signature", "subtle", "zeroize", ] @@ -2737,14 +2695,14 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" dependencies = [ - "const-oid 0.10.2", + "const-oid", "crypto-common 0.2.2", "ctutils", "hybrid-array", "module-lattice", - "pkcs8 0.11.0", + "pkcs8", "shake", - "signature 3.0.0", + "signature", ] [[package]] @@ -2786,72 +2744,50 @@ dependencies = [ [[package]] name = "mtp" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "mtp-client", - "mtp-codec 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-crypto 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", + "mtp-codec", + "mtp-common", + "mtp-crypto", "mtp-host", - "mtp-transport 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-type-map 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", + "mtp-transport", + "mtp-type-map", "mtp-webserver", ] [[package]] name = "mtp-client" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ - "mtp-codec 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-crypto 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-transport 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", + "mtp-codec", + "mtp-common", + "mtp-crypto", + "mtp-transport", "rand 0.10.2", "tokio", ] [[package]] name = "mtp-codec" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "base64 0.23.1", "byteorder", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-crypto 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-type-map 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", + "mtp-common", + "mtp-crypto", + "mtp-type-map", "rand 0.10.2", -] - -[[package]] -name = "mtp-codec" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b#b067614a684eb1856bc5db7b3fd82148c036ce6b" -dependencies = [ - "base64 0.23.1", - "byteorder", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b)", - "mtp-type-map 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b)", - "rand 0.10.2", -] - -[[package]] -name = "mtp-common" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" -dependencies = [ - "quinn", - "rustls", "thiserror 2.0.19", - "wtransport", ] [[package]] name = "mtp-common" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b#b067614a684eb1856bc5db7b3fd82148c036ce6b" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "quinn", "rustls", @@ -2861,18 +2797,18 @@ dependencies = [ [[package]] name = "mtp-crypto" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ - "base64 0.23.1", + "base64 0.22.1", "chacha20poly1305", - "ed25519-dalek 2.2.0", + "ed25519-dalek", "getrandom 0.4.3", "hkdf", "ml-dsa", "mlkem-tls", "rand 0.10.2", - "rand_core 0.10.1", + "rand_core 0.6.4", "rustls", "serde", "sha2 0.11.0", @@ -2882,89 +2818,45 @@ dependencies = [ ] [[package]] -name = "mtp-crypto" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b#b067614a684eb1856bc5db7b3fd82148c036ce6b" +name = "mtp-host" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ - "base64 0.23.1", - "chacha20poly1305", - "ed25519-dalek 3.0.0", - "getrandom 0.4.3", - "hkdf", - "ml-dsa", + "mtp-codec", + "mtp-common", + "mtp-crypto", + "mtp-transport", "rand 0.10.2", - "rand_core 0.10.1", - "rustls", - "sha2 0.11.0", "thiserror 2.0.19", "tokio", + "tracing", + "wtransport", +] + +[[package]] +name = "mtp-transport" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +dependencies = [ + "async-trait", + "mtp-codec", + "mtp-common", + "mtp-crypto", + "rand 0.10.2", + "rcgen", + "rustls", + "rustls-native-certs", + "sha2 0.11.0", + "tokio", + "tracing", + "wtransport", "zeroize", ] -[[package]] -name = "mtp-host" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" -dependencies = [ - "mtp-codec 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-crypto 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-transport 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "rand 0.10.2", - "tokio", - "tracing", - "wtransport", -] - -[[package]] -name = "mtp-transport" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" -dependencies = [ - "async-trait", - "mtp-codec 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-crypto 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "rcgen", - "rustls", - "rustls-native-certs", - "sha2 0.11.0", - "tokio", - "tracing", - "wtransport", -] - -[[package]] -name = "mtp-transport" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b#b067614a684eb1856bc5db7b3fd82148c036ce6b" -dependencies = [ - "async-trait", - "mtp-codec 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b)", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b)", - "mtp-crypto 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b)", - "rcgen", - "rustls", - "rustls-native-certs", - "sha2 0.11.0", - "tokio", - "tracing", - "wtransport", -] - [[package]] name = "mtp-type-map" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" -dependencies = [ - "serde", - "serde_yaml", -] - -[[package]] -name = "mtp-type-map" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b#b067614a684eb1856bc5db7b3fd82148c036ce6b" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "serde", "serde_yaml", @@ -2972,8 +2864,8 @@ dependencies = [ [[package]] name = "mtp-webserver" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "async-trait", "bytes", @@ -2984,13 +2876,12 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "mtp-codec 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-crypto 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", + "mtp-codec", + "mtp-common", + "mtp-crypto", "mtp-host", - "mtp-transport 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", + "mtp-transport", "quinn", - "rand 0.10.2", "rustls", "thiserror 2.0.19", "tokio", @@ -3492,15 +3383,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "pem-rfc7468" version = "1.0.0" @@ -3586,24 +3468,14 @@ dependencies = [ "futures-io", ] -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der 0.7.10", - "spki 0.7.3", -] - [[package]] name = "pkcs8" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "der 0.8.1", - "spki 0.8.0", + "der", + "spki", ] [[package]] @@ -4592,15 +4464,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "signature" version = "3.0.0" @@ -4709,16 +4572,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der 0.7.10", -] - [[package]] name = "spki" version = "0.8.0" @@ -4726,7 +4579,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der 0.8.1", + "der", ] [[package]] @@ -5289,7 +5142,6 @@ dependencies = [ "base64 0.22.1", "jni 0.22.4", "mtp", - "mtp-transport 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b)", "reqwest", "serde", "serde_json", diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index 08a66d5..2a68e95 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -24,8 +24,7 @@ serde_json = "1" base64 = "0.22" reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] } tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] } -mtp = { git = "https://git.methanium.net/methanium/mtp.git", rev = "7182272e3edb0079d7b74b45b2f62a298d2fee22", features = ["client", "crypto"] } -mtp-transport = { git = "https://git.methanium.net/methanium/mtp.git", rev = "b067614a684eb1856bc5db7b3fd82148c036ce6b" } +mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "a5c8d4f0c898c78351e9d54124886c86e789a22a", features = ["client", "crypto"] } webpki-root-certs = "1" tauri-plugin-deep-link = "2" tauri-plugin-notification = "2" @@ -36,6 +35,11 @@ version = "2" features = [] default-features = true +[target.'cfg(not(target_os = "android"))'.dependencies.tauri] +version = "2" +features = [] +default-features = true + [target.'cfg(target_os = "android")'.dependencies] jni = "0.22" diff --git a/apps/tauri/src-tauri/src/main.rs b/apps/tauri/src-tauri/src/main.rs index e78a26a..272583a 100644 --- a/apps/tauri/src-tauri/src/main.rs +++ b/apps/tauri/src-tauri/src/main.rs @@ -1,9 +1,6 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use lib::log; - fn main() { mobile_lib::run(); - log("Test test 123") } diff --git a/apps/tauri/src-tauri/src/mtp_backend.rs b/apps/tauri/src-tauri/src/mtp_backend.rs index b343337..14365b8 100644 --- a/apps/tauri/src-tauri/src/mtp_backend.rs +++ b/apps/tauri/src-tauri/src/mtp_backend.rs @@ -2,7 +2,7 @@ use std::sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, Arc, Mutex, OnceLock, RwLock, }; -use std::time::Duration; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use base64::{ engine::general_purpose::{STANDARD, STANDARD_NO_PAD}, @@ -16,6 +16,7 @@ use mtp::crypto::{ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use tauri::{AppHandle, Emitter}; +use tokio::sync::mpsc; const EVENT_NAME: &str = "mtp://event"; const DISCONNECTED: u8 = 0; @@ -24,6 +25,9 @@ const CONNECTED: u8 = 2; const CHAT_SECRET_SALT: &[u8] = b"tensamin-chat-secret-v1"; const CHAT_MESSAGE_SALT: &[u8] = b"tensamin-chat-message-v1"; const CHAT_SECRET_SCHEME: &str = "mtp-chat-secret-kem-chacha20poly1305-hkdf-sha256-v1"; +const INITIAL_SYNC_TIMEOUT: Duration = Duration::from_secs(30); +const MAX_BUFFERED_INITIAL_FRAMES: usize = 1_000; +const NOTIFICATION_QUEUE_CAPACITY: usize = 32; #[cfg(target_os = "android")] const ROOT_YE_PEM: &[u8] = b"-----BEGIN CERTIFICATE-----\n\ MIIB2TCCAWCgAwIBAgIRAKQCa6LvbHwg1AR+XmWmk4AwCgYIKoZIzj0EAwMwLjEL\n\ @@ -95,7 +99,7 @@ enum MtpEvent { pub struct MtpManager { runtime: tokio::runtime::Runtime, config: RwLock>, - connection: RwLock>>, + connection: RwLock>>, snapshot: RwLock, generation: AtomicU64, enabled: AtomicBool, @@ -104,6 +108,46 @@ pub struct MtpManager { start_lock: Mutex<()>, } +struct RequestIdAllocator { + next: AtomicU64, +} + +impl RequestIdAllocator { + fn new() -> Self { + Self { + next: AtomicU64::new(1), + } + } + + fn next(&self) -> Result { + let value = self.next.fetch_add(1, Ordering::Relaxed); + u32::try_from(value) + .map_err(|_| "MTP request ID space exhausted for this connection".to_string()) + } +} + +struct ManagedConnection { + mtp: Arc, + request_ids: RequestIdAllocator, +} + +impl ManagedConnection { + async fn next_request_id(&self) -> Result { + let id = self.request_ids.next(); + if id.is_err() { + self.mtp.sender.close().await; + } + id + } +} + +struct PreparedConnection { + connection: MTPConnection, + request_ids: RequestIdAllocator, + initial_state: Value, + buffered_frames: Vec, +} + static MANAGER: OnceLock = OnceLock::new(); pub fn manager() -> &'static MtpManager { @@ -149,7 +193,7 @@ impl MtpManager { .take() { self.runtime - .spawn(async move { connection.sender.close().await }); + .spawn(async move { connection.mtp.sender.close().await }); } self.set_snapshot(MtpSnapshot { generation, @@ -171,7 +215,7 @@ impl MtpManager { .take() { self.runtime - .spawn(async move { connection.sender.close().await }); + .spawn(async move { connection.mtp.sender.close().await }); } self.set_snapshot(MtpSnapshot { generation: self.generation.load(Ordering::SeqCst), @@ -231,20 +275,16 @@ impl MtpManager { self.enabled.load(Ordering::SeqCst) && self.generation.load(Ordering::SeqCst) == generation } - async fn request( - &self, - type_name: &str, - data: Value, - id: Option, - ) -> Result { + async fn request(&self, type_name: &str, data: Value) -> Result { let connection = self .connection .read() .map_err(|_| "MTP connection lock is unavailable")? .clone() .ok_or_else(|| "MTP is not connected".to_string())?; - let request = json_to_frame(type_name, data, id)?; + let request = json_to_frame(type_name, data, connection.next_request_id().await?)?; let response = connection + .mtp .request(&request, None) .await .map_err(|error| error.to_string())?; @@ -261,9 +301,12 @@ async fn supervise(generation: u64) { manager.log(2, "Starting native MTP connection", None); android_status("Connecting"); match connect(&config).await { - Ok((connection, state)) => { + Ok(prepared) => { delay = Duration::from_secs(1); - let connection = Arc::new(connection); + let connection = Arc::new(ManagedConnection { + mtp: Arc::new(prepared.connection), + request_ids: prepared.request_ids, + }); let stale = { let _guard = manager.start_lock.lock().expect("start lock poisoned"); let mut current = manager @@ -279,22 +322,47 @@ async fn supervise(generation: u64) { generation, ready_state: CONNECTED, identified: true, - state: Some(state), + state: Some(prepared.initial_state), error: None, }); false } }; if stale { - connection.sender.close().await; + connection.mtp.sender.close().await; break; } android_status("Connected"); manager.log(2, "Native MTP connection established", None); + let (notification_tx, mut notification_rx) = + mpsc::channel(NOTIFICATION_QUEUE_CAPACITY); + let notification_connection = connection.clone(); + let notification_config = config.clone(); + let notification_worker = tokio::spawn(async move { + while let Some(frame) = notification_rx.recv().await { + if !manager().is_current(generation) { + break; + } + if let Err(error) = notify_message( + ¬ification_config, + notification_connection.clone(), + &frame, + ) + .await + { + eprintln!("failed to create background message notification: {error}"); + } + } + }); + + for frame in prepared.buffered_frames { + handle_push(generation, ¬ification_tx, frame).await; + } + while manager.is_current(generation) { - match connection.receive().await { - Ok(frame) => handle_push(generation, connection.clone(), frame).await, + match connection.mtp.receive().await { + Ok(frame) => handle_push(generation, ¬ification_tx, frame).await, Err(error) => { let _guard = manager.start_lock.lock().expect("start lock poisoned"); if manager.is_current(generation) { @@ -310,6 +378,13 @@ async fn supervise(generation: u64) { } } } + drop(notification_tx); + notification_worker.abort(); + if let Err(error) = notification_worker.await { + if !error.is_cancelled() { + eprintln!("background notification worker failed: {error}"); + } + } let mut current = manager .connection .write() @@ -339,12 +414,21 @@ async fn supervise(generation: u64) { break; } android_status("Reconnecting"); - tokio::time::sleep(delay).await; + tokio::time::sleep(jittered_retry_delay(delay)).await; delay = (delay * 2).min(Duration::from_secs(60)); } } -async fn connect(config: &MtpConfig) -> Result<(MTPConnection, Value), String> { +fn jittered_retry_delay(delay: Duration) -> Duration { + let entropy = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.subsec_nanos()) + .unwrap_or_default(); + let percent = 80 + entropy % 41; + delay.mul_f64(percent as f64 / 100.0) +} + +async fn connect(config: &MtpConfig) -> Result { let (url, public_key) = resolve_endpoint(config) .await .map_err(|error| format!("endpoint discovery failed: {error}"))?; @@ -381,39 +465,10 @@ async fn connect(config: &MtpConfig) -> Result<(MTPConnection, Value), String> { .map_err(|error| format!("transport authentication failed: {error}"))?; manager().log(2, "Native MTP authentication completed", None); - let connected = CommunicationValue::new(CommunicationType::ClientConnected) - .add_typed_default( - DataType::SessionId, - DataValue::UnsignedNumber(current_millis() as u128), - ) - .add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0)) - .add_typed_default(DataType::CacheValid, DataValue::BoolFalse) - .add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0)); - let state = tokio::time::timeout( - Duration::from_secs(30), - connection.request(&connected, None), - ) - .await - .map_err(|_| "initial state synchronization timed out".to_string())? - .map_err(|error| format!("initial state synchronization failed: {error}"))?; - if !state.is_type(CommunicationType::ClientStateSync) { - return Err(format!( - "expected ClientStateSync, received {}", - state.get_type_name().unwrap_or("unknown") - )); - } - let session_id = state - .get_data(DataType::SessionId) - .as_number() - .ok_or("ClientStateSync omitted SessionId")?; - let version = state - .get_data(DataType::VersionNumber) - .as_number() - .ok_or("ClientStateSync omitted VersionNumber")?; - let ack = CommunicationValue::new(CommunicationType::ClientStateAck) - .add_typed_default(DataType::SessionId, number_to_data(session_id)) - .add_typed_default(DataType::VersionNumber, number_to_data(version)); - let response = tokio::time::timeout(Duration::from_secs(30), connection.request(&ack, None)) + let (state, buffered_frames) = await_initial_state(&connection).await?; + let request_ids = RequestIdAllocator::new(); + let (initial_state, ack) = prepare_initial_state_ack(&state, &request_ids)?; + let response = tokio::time::timeout(INITIAL_SYNC_TIMEOUT, connection.request(&ack, None)) .await .map_err(|_| "state acknowledgement timed out".to_string())? .map_err(|error| format!("state acknowledgement failed: {error}"))?; @@ -423,7 +478,182 @@ async fn connect(config: &MtpConfig) -> Result<(MTPConnection, Value), String> { { return Err(format!("ClientStateAck failed: {response}")); } - Ok((connection, frame_data_to_json(&state)?)) + Ok(PreparedConnection { + connection, + request_ids, + initial_state, + buffered_frames, + }) +} + +fn prepare_initial_state_ack( + state: &CommunicationValue, + request_ids: &RequestIdAllocator, +) -> Result<(Value, CommunicationValue), String> { + let initial_state = frame_data_to_json(state)?; + validate_client_state_sync(&initial_state)?; + let data = initial_state + .as_object() + .ok_or("ClientStateSync payload is not an object")?; + let session_id = required_integer(data, "SessionId")?; + let version = required_integer(data, "VersionNumber")?; + let ack = CommunicationValue::new(CommunicationType::ClientStateAck) + .with_id(request_ids.next()?) + .add_typed_default(DataType::SessionId, number_to_data(session_id)) + .add_typed_default(DataType::VersionNumber, number_to_data(version)); + Ok((initial_state, ack)) +} + +fn validate_client_state_sync(state: &Value) -> Result<(), String> { + let data = state + .as_object() + .ok_or("ClientStateSync payload is not an object")?; + if required_integer(data, "SessionId")? <= 0 { + return Err("ClientStateSync SessionId must be a positive integer".into()); + } + for field in ["VersionNumber", "CacheSchemaVersion"] { + if required_integer(data, field)? < 0 { + return Err(format!("ClientStateSync {field} must be nonnegative")); + } + } + match data.get("SyncMode").and_then(Value::as_str) { + Some("full" | "delta") => {} + _ => return Err("ClientStateSync SyncMode must be 'full' or 'delta'".into()), + } + for field in ["Contacts", "Communities", "Calls", "Messages"] { + if !data.get(field).is_some_and(Value::is_array) { + return Err(format!("ClientStateSync {field} must be an array")); + } + } + for field in ["DeletedMessageIds", "DeletedContactIds"] { + if let Some(value) = data.get(field) { + let values = value + .as_array() + .ok_or_else(|| format!("ClientStateSync {field} must be an array"))?; + if values.iter().any(|value| !value.is_number()) { + return Err(format!("ClientStateSync {field} must contain numbers")); + } + } + } + validate_object_array(data, "Communities", |_| Ok(()))?; + validate_object_array(data, "Contacts", validate_contact)?; + validate_object_array(data, "Calls", validate_call)?; + validate_object_array(data, "Messages", validate_message)?; + Ok(()) +} + +fn validate_object_array( + data: &Map, + field: &str, + validate: impl Fn(&Map) -> Result<(), String>, +) -> Result<(), String> { + let values = data + .get(field) + .and_then(Value::as_array) + .ok_or_else(|| format!("ClientStateSync {field} must be an array"))?; + for value in values { + let object = value + .as_object() + .ok_or_else(|| format!("ClientStateSync {field} entries must be objects"))?; + validate(object)?; + } + Ok(()) +} + +fn validate_contact(contact: &Map) -> Result<(), String> { + if !contact.get("UserId").is_some_and(Value::is_number) { + return Err("ClientStateSync contact omitted numeric UserId".into()); + } + if let Some(messages) = contact.get("Messages") { + let messages = messages + .as_array() + .ok_or("ClientStateSync contact Messages must be an array")?; + for message in messages { + validate_message( + message + .as_object() + .ok_or("ClientStateSync contact message must be an object")?, + )?; + } + } + Ok(()) +} + +fn validate_call(call: &Map) -> Result<(), String> { + if !call.get("CallId").is_some_and(Value::is_string) { + return Err("ClientStateSync call omitted string CallId".into()); + } + let members = call + .get("CallMembers") + .and_then(Value::as_array) + .ok_or("ClientStateSync call omitted CallMembers array")?; + if members.iter().any(|member| !member.is_number()) { + return Err("ClientStateSync CallMembers must contain numbers".into()); + } + Ok(()) +} + +fn validate_message(message: &Map) -> Result<(), String> { + for field in ["SenderId", "SendTime"] { + if !message.get(field).is_some_and(Value::is_number) { + return Err(format!("ClientStateSync message omitted numeric {field}")); + } + } + let content = message + .get("Content") + .and_then(Value::as_str) + .ok_or("ClientStateSync message omitted string Content")?; + STANDARD + .decode(content) + .or_else(|_| STANDARD_NO_PAD.decode(content)) + .map_err(|_| "ClientStateSync message Content must be base64".to_string())?; + if let Some(state) = message.get("MessageState") { + match state.as_str() { + Some("read" | "received" | "sent" | "sending" | "awaiting") => {} + _ => return Err("ClientStateSync message has invalid MessageState".into()), + } + } + Ok(()) +} + +fn required_integer(data: &Map, field: &str) -> Result { + let value = data + .get(field) + .ok_or_else(|| format!("ClientStateSync omitted {field}"))?; + if let Some(value) = value.as_i64() { + return Ok(value as i128); + } + value + .as_u64() + .map(|value| value as i128) + .ok_or_else(|| format!("ClientStateSync {field} must be an integer")) +} + +async fn await_initial_state( + connection: &MTPConnection, +) -> Result<(CommunicationValue, Vec), String> { + let mut buffered = Vec::new(); + let deadline = tokio::time::Instant::now() + INITIAL_SYNC_TIMEOUT; + + loop { + let frame = tokio::time::timeout_at(deadline, connection.receive()) + .await + .map_err(|_| "initial state synchronization timed out".to_string())? + .map_err(|error| format!("initial state synchronization failed: {error}"))?; + + if frame.is_type(CommunicationType::ErrorNoIota) { + return Err("No Iota is currently connected".into()); + } + + if frame.is_type(CommunicationType::ClientStateSync) { + return Ok((frame, buffered)); + } + + if buffered.len() == MAX_BUFFERED_INITIAL_FRAMES { + return Err("initial state synchronization buffered too many frames".into()); + } + buffered.push(frame); + } } async fn resolve_endpoint(config: &MtpConfig) -> Result<(String, String), String> { @@ -465,7 +695,11 @@ async fn resolve_endpoint(config: &MtpConfig) -> Result<(String, String), String )) } -async fn handle_push(generation: u64, connection: Arc, frame: CommunicationValue) { +async fn handle_push( + generation: u64, + notification_tx: &mpsc::Sender, + frame: CommunicationValue, +) { let manager = manager(); if let Ok(message) = frame_to_json(&frame) { manager.emit(MtpEvent::Message { @@ -478,7 +712,7 @@ async fn handle_push(generation: u64, connection: Arc, frame: Com { if let Some(partner_id) = frame .get_data(DataType::ChatPartnerId) - .as_number() + .and_then(DataValue::as_number) .and_then(|value| u64::try_from(value).ok()) { if let Err(error) = android_cancel_notification(partner_id) { @@ -487,40 +721,39 @@ async fn handle_push(generation: u64, connection: Arc, frame: Com } } if frame.is_type(CommunicationType::MessageLive) && !manager.ui_visible.load(Ordering::SeqCst) { - if let Err(error) = notify_message(connection, &frame).await { - eprintln!("failed to create background message notification: {error}"); + if notification_tx.try_send(frame).is_err() { + eprintln!("background message notification queue is full"); } } } async fn notify_message( - connection: Arc, + config: &MtpConfig, + connection: Arc, frame: &CommunicationValue, ) -> Result<(), String> { let sender_id = frame .get_data(DataType::SenderId) - .as_number() + .and_then(DataValue::as_number) .and_then(|value| u64::try_from(value).ok()) .ok_or("MessageLive omitted SenderId")?; - let message = frame.get_data(DataType::Message); - let content = container_value(message, DataType::Content) + let message = frame + .get_data(DataType::Message) + .ok_or("MessageLive omitted Message")?; + let content = container_value(message, DataType::AppContent) .and_then(DataValue::as_str) .ok_or("MessageLive omitted Content")?; - let config = manager() - .config - .read() - .expect("config lock poisoned") - .clone() - .ok_or("missing config")?; let keyring_bytes = decode_browser_base64(&config.keyring)?; let keyring = Keyring::from_bytes(&keyring_bytes).map_err(|error| error.to_string())?; let chat_id = derive_chat_id(config.user_id, sender_id); let secret_id = format!("chat:{chat_id}:main"); let secret_request = CommunicationValue::new(CommunicationType::GetChatSecret) + .with_id(connection.next_request_id().await?) .add_typed_default(DataType::UserId, DataValue::Str(config.user_id.to_string())) .add_typed_default(DataType::ChatId, DataValue::Str(chat_id.clone())) .add_typed_default(DataType::SecretId, DataValue::Str(secret_id.clone())); let secret = connection + .mtp .request(&secret_request, None) .await .map_err(|error| error.to_string())?; @@ -532,7 +765,7 @@ async fn notify_message( } let version = secret .get_data(DataType::VersionNumber) - .as_number() + .and_then(DataValue::as_number) .ok_or("missing secret version")?; let encrypted_secret = secret .get_bytes(DataType::EncryptedSecret) @@ -560,11 +793,14 @@ async fn notify_message( .decrypt(&ciphertext, b"") .map_err(|error| error.to_string())?; - let user_request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default( - DataType::UserId, - DataValue::UnsignedNumber(sender_id as u128), - ); + let user_request = CommunicationValue::new(CommunicationType::GetUserData) + .with_id(connection.next_request_id().await?) + .add_typed_default( + DataType::UserId, + DataValue::UnsignedNumber(sender_id as u128), + ); let user = connection + .mtp .request(&user_request, None) .await .map_err(|error| error.to_string())?; @@ -652,24 +888,42 @@ fn container_value(value: &DataValue, field: DataType) -> Option<&DataValue> { value.get_field(id) } -fn json_to_frame( - type_name: &str, - data: Value, - id: Option, -) -> Result { +fn wire_field_name(name: &str) -> &str { + match name { + "Content" => "AppContent", + "CreatedAt" => "AppCreatedAt", + "MessageId" => "AppMessageId", + _ => name, + } +} + +fn application_field_name(name: &str) -> &str { + match name { + "AppContent" => "Content", + "AppCreatedAt" => "CreatedAt", + "AppMessageId" => "MessageId", + _ => name, + } +} + +fn json_to_frame(type_name: &str, data: Value, id: u32) -> Result { let comm_type = CommunicationType::from_name(type_name) .ok_or_else(|| format!("unknown communication type: {type_name}"))?; - let mut frame = CommunicationValue::new(comm_type); - if let Some(id) = id { - frame = frame.with_id(id); - } + let mut frame = CommunicationValue::new(comm_type).with_id(id); let Value::Object(fields) = data else { return Err("MTP request data must be an object".into()); }; + let mut translated_fields = std::collections::HashSet::::with_capacity(fields.len()); for (name, value) in fields { + let wire_name = wire_field_name(&name).to_owned(); + if !translated_fields.insert(wire_name.clone()) { + return Err(format!( + "duplicate MTP field after translation: {wire_name}" + )); + } let data_type = - DataType::from_name(&name).ok_or_else(|| format!("unknown data type: {name}"))?; - frame = frame.add_typed_default(data_type, json_to_data(&name, value)?); + DataType::from_name(&wire_name).ok_or_else(|| format!("unknown data type: {name}"))?; + frame = frame.add_typed_default(data_type, json_to_data(&wire_name, value)?); } Ok(frame) } @@ -707,13 +961,21 @@ fn json_to_data(field: &str, value: Value) -> Result { ), Value::Object(fields) => { let mut entries = Vec::with_capacity(fields.len()); + let mut translated_fields = + std::collections::HashSet::::with_capacity(fields.len()); for (name, value) in fields { - let data_type = DataType::from_name(&name) + let wire_name = wire_field_name(&name).to_owned(); + if !translated_fields.insert(wire_name.clone()) { + return Err(format!( + "duplicate MTP field after translation: {wire_name}" + )); + } + let data_type = DataType::from_name(&wire_name) .ok_or_else(|| format!("unknown nested data type: {name}"))?; let id = data_type .try_to_id(&TypeMap::latest()) .ok_or_else(|| format!("unmapped data type: {name}"))?; - entries.push((id, json_to_data(&name, value)?)); + entries.push((id, json_to_data(&wire_name, value)?)); } DataValue::Container(entries) } @@ -737,8 +999,8 @@ fn is_bytes_field(field: &str) -> bool { fn frame_to_json(frame: &CommunicationValue) -> Result { let mut result = Map::new(); - if frame.get_id() != 0 { - result.insert("id".into(), Value::from(frame.get_id())); + if let Some(id) = frame.id() { + result.insert("id".into(), Value::from(id)); } result.insert( "type".into(), @@ -751,11 +1013,20 @@ fn frame_to_json(frame: &CommunicationValue) -> Result { fn frame_data_to_json(frame: &CommunicationValue) -> Result { let map = frame.type_map().cloned().unwrap_or_else(TypeMap::latest); let mut result = Map::new(); - for (id, value) in frame.data() { + let entries = frame + .data() + .ok_or("MTP frame payload is not a data container")?; + for (id, value) in entries { let name = map .data_type_name(id.0) .ok_or_else(|| format!("unknown data type id: {}", id.0))?; - result.insert(name.to_owned(), data_to_json(value, &map)?); + let application_name = application_field_name(name); + if result.contains_key(application_name) { + return Err(format!( + "duplicate MTP field after translation: {application_name}" + )); + } + result.insert(application_name.to_owned(), data_to_json(value, &map)?); } Ok(Value::Object(result)) } @@ -786,7 +1057,13 @@ fn data_to_json(value: &DataValue, map: &TypeMap) -> Result { let name = map .data_type_name(id.0) .ok_or_else(|| format!("unknown nested data type id: {}", id.0))?; - object.insert(name.to_owned(), data_to_json(value, map)?); + let application_name = application_field_name(name); + if object.contains_key(application_name) { + return Err(format!( + "duplicate MTP field after translation: {application_name}" + )); + } + object.insert(application_name.to_owned(), data_to_json(value, map)?); } Value::Object(object) } @@ -801,16 +1078,15 @@ fn number_to_json(value: i128) -> Result { .map_err(|_| "number exceeds JSON range".into()) } -fn current_millis() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as u64 -} - #[cfg(test)] mod tests { - use super::{decode_browser_base64, decode_sdk_bytes}; + use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; + use serde_json::json; + + use super::{ + container_value, decode_browser_base64, decode_sdk_bytes, frame_to_json, + jittered_retry_delay, json_to_frame, prepare_initial_state_ack, RequestIdAllocator, + }; #[test] fn browser_base64_accepts_file_whitespace_and_missing_padding() { @@ -823,11 +1099,148 @@ mod tests { assert_eq!(decode_sdk_bytes("0x01:02-ff").unwrap(), [1, 2, 255]); assert_eq!(decode_sdk_bytes("AQI=").unwrap(), [1, 2]); } + + #[test] + fn request_ids_are_nonzero_and_monotonic() { + let ids = RequestIdAllocator::new(); + + assert_eq!(ids.next().unwrap(), 1); + assert_eq!(ids.next().unwrap(), 2); + } + + #[test] + fn retry_jitter_stays_within_policy_bounds() { + let delay = jittered_retry_delay(std::time::Duration::from_secs(10)); + + assert!(delay >= std::time::Duration::from_secs(8)); + assert!(delay <= std::time::Duration::from_secs(12)); + } + + #[test] + fn json_content_uses_app_content_wire_type() { + let frame = json_to_frame( + "MessageEdit", + json!({ + "Content": "ciphertext", + "ChatPartnerId": 42, + "SendTime": 10, + }), + 1, + ) + .unwrap(); + + assert_eq!( + frame + .get_data(DataType::AppContent) + .and_then(DataValue::as_str), + Some("ciphertext") + ); + } + + #[test] + fn nested_json_content_uses_app_content_wire_type() { + let frame = json_to_frame( + "MessageEdit", + json!({ + "Message": { "Content": "ciphertext" }, + }), + 1, + ) + .unwrap(); + let message = frame.get_data(DataType::Message).unwrap(); + + assert_eq!( + container_value(message, DataType::AppContent).and_then(DataValue::as_str), + Some("ciphertext") + ); + } + + #[test] + fn app_content_is_exposed_as_content_to_frontend() { + let frame = CommunicationValue::new(CommunicationType::MessageEditLive) + .with_id(1) + .add_typed_default(DataType::AppContent, DataValue::Str("ciphertext".into())); + + let json = frame_to_json(&frame).unwrap(); + + assert_eq!(json["data"]["Content"], "ciphertext"); + assert!(json["data"].get("AppContent").is_none()); + } + + #[test] + fn translated_field_collisions_are_rejected() { + assert!(json_to_frame( + "MessageEdit", + json!({ "Content": "a", "AppContent": "b" }), + 1, + ) + .is_err()); + } + + fn valid_initial_state() -> CommunicationValue { + CommunicationValue::new(CommunicationType::ClientStateSync) + .add_typed_default(DataType::SessionId, DataValue::UnsignedNumber(1)) + .add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0)) + .add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0)) + .add_typed_default(DataType::SyncMode, DataValue::Str("full".into())) + .add_typed_default(DataType::Contacts, DataValue::Array(vec![])) + .add_typed_default(DataType::Communities, DataValue::Array(vec![])) + .add_typed_default(DataType::Calls, DataValue::Array(vec![])) + .add_typed_default(DataType::Messages, DataValue::Array(vec![])) + } + + #[test] + fn valid_initial_state_is_prepared_before_ack() { + let ids = RequestIdAllocator::new(); + let (state, ack) = prepare_initial_state_ack(&valid_initial_state(), &ids).unwrap(); + + assert_eq!(state["SyncMode"], "full"); + assert!(ack.is_type(CommunicationType::ClientStateAck)); + assert_eq!(ack.id(), Some(1)); + } + + #[test] + fn malformed_initial_state_does_not_prepare_ack() { + let ids = RequestIdAllocator::new(); + let malformed = valid_initial_state() + .add_typed_default(DataType::SyncMode, DataValue::Str("invalid".into())); + + assert!(prepare_initial_state_ack(&malformed, &ids).is_err()); + assert_eq!( + ids.next().unwrap(), + 1, + "no acknowledgement ID was allocated" + ); + } + + #[test] + fn malformed_nested_initial_state_does_not_prepare_ack() { + let ids = RequestIdAllocator::new(); + let malformed = CommunicationValue::new(CommunicationType::ClientStateSync) + .add_typed_default(DataType::SessionId, DataValue::UnsignedNumber(1)) + .add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0)) + .add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0)) + .add_typed_default(DataType::SyncMode, DataValue::Str("full".into())) + .add_typed_default( + DataType::Contacts, + DataValue::Array(vec![DataValue::Container(vec![])]), + ) + .add_typed_default(DataType::Communities, DataValue::Array(vec![])) + .add_typed_default(DataType::Calls, DataValue::Array(vec![])) + .add_typed_default(DataType::Messages, DataValue::Array(vec![])); + + assert!(prepare_initial_state_ack(&malformed, &ids).is_err()); + assert_eq!( + ids.next().unwrap(), + 1, + "no acknowledgement ID was allocated" + ); + } } #[tauri::command] -pub async fn mtp_request(type_name: String, data: Value, id: Option) -> Result { - manager().request(&type_name, data, id).await +pub async fn mtp_request(type_name: String, data: Value) -> Result { + manager().request(&type_name, data).await } #[tauri::command] diff --git a/apps/web/package.json b/apps/web/package.json index f608fcb..a6b3951 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -55,6 +55,7 @@ "esbuild": "^0.28.1", "eslint": "^10.8.0", "globals": "^17.9.0", + "mtp": "*", "typescript": "~6.0.3", "typescript-eslint": "^8.66.0", "vite": "^8.2.1" diff --git a/flake.nix b/flake.nix index d91a817..6a29551 100644 --- a/flake.nix +++ b/flake.nix @@ -63,6 +63,7 @@ pipewire systemd wayland + webkitgtk_4_1 libX11 libXScrnSaver libXcomposite @@ -184,6 +185,7 @@ pipewire systemd wayland + webkitgtk_4_1 libX11 libXScrnSaver libXcomposite diff --git a/package.json b/package.json index 05856f5..051f699 100644 --- a/package.json +++ b/package.json @@ -29,22 +29,22 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@types/node": "^26.1.2", + "@types/node": "^26.2.0", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", - "@typescript-eslint/parser": "^8.66.0", - "eslint": "^10.8.0", + "@typescript-eslint/parser": "^8.67.0", + "eslint": "^10.8.1", "eslint-plugin-react-hooks": "^7.1.1", - "fallow": "^3.14.0", - "globals": "^17.9.0", + "fallow": "^3.17.0", + "globals": "^17.11.0", "prettier": "^3.9.6", "typescript": "^6.0.3", - "typescript-eslint": "^8.66.0", - "vitest": "^4.1.10" + "typescript-eslint": "^8.67.0", + "vitest": "^4.1.11" }, "dependencies": { - "@methanium/ui": "*", - "mtp": "*", - "sonner": "^2.0.7" + "@methanium/ui": "https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz", + "mtp": "https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz", + "sonner": "^2.0.8" } } diff --git a/packages/call/package.json b/packages/call/package.json index d4f7c10..7171f50 100644 --- a/packages/call/package.json +++ b/packages/call/package.json @@ -28,6 +28,7 @@ "deepfilternet3-noise-filter": "1.3.0", "livekit-client": "^2.21.0", "lucide-react": "^1.29.0", + "mtp": "*", "react": "^19.2.8", "react-dom": "^19.2.8", "recharts": "^3.10.1", diff --git a/packages/call/src/mediaShare/index.ts b/packages/call/src/mediaShare/index.ts index 6d2c47f..9706d4d 100644 --- a/packages/call/src/mediaShare/index.ts +++ b/packages/call/src/mediaShare/index.ts @@ -19,7 +19,6 @@ export function getMediaShareAdapter(): MediaShareAdapter { export type { MediaShareAdapter, - MediaShareCapabilities, MediaShareKind, MediaShareRequest, MediaShareSession, diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 62580df..2b40fdf 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -15,6 +15,7 @@ "build": "tsc -p tsconfig.json --noEmit" }, "dependencies": { + "mtp": "*", "react": "^19.2.8", "react-dom": "^19.2.8" } diff --git a/packages/crypto/src/callSecret.ts b/packages/crypto/src/callSecret.ts index 97ef318..556d131 100644 --- a/packages/crypto/src/callSecret.ts +++ b/packages/crypto/src/callSecret.ts @@ -13,11 +13,13 @@ export function deriveCallSecretId(callId: string): string { } export function ownKemPublicKeyFromKeyring(keyring: string): Uint8Array { - return crypto.keyringToKeys(keyring).kemPublicKey; + return crypto.keyringToKeys({ value: keyring, encoding: "base64" }) + .kemPublicKey; } export function kemPublicKeyFromPublicKeyBundle(publicKey: string): Uint8Array { - return crypto.publicKeyBundleToKeys(publicKey).kemPublicKey; + return crypto.publicKeyBundleToKeys({ value: publicKey, encoding: "base64" }) + .kemPublicKey; } export async function wrapCallSecret(args: { @@ -72,7 +74,10 @@ export async function unwrapCallSecret(args: { ); } - const ownKeys = crypto.keyringToKeys(args.keyring); + const ownKeys = crypto.keyringToKeys({ + value: args.keyring, + encoding: "base64", + }); const sharedSecret = crypto.decapsulate( ownKeys.kemSecretKey, args.kemCiphertext, diff --git a/packages/crypto/src/chatSecret.ts b/packages/crypto/src/chatSecret.ts index e6e836c..cbac59a 100644 --- a/packages/crypto/src/chatSecret.ts +++ b/packages/crypto/src/chatSecret.ts @@ -23,11 +23,13 @@ export function randomChatSecret(): Uint8Array { } export function ownKemPublicKeyFromKeyring(keyring: string): Uint8Array { - return crypto.keyringToKeys(keyring).kemPublicKey; + return crypto.keyringToKeys({ value: keyring, encoding: "base64" }) + .kemPublicKey; } export function kemPublicKeyFromPublicKeyBundle(publicKey: string): Uint8Array { - return crypto.publicKeyBundleToKeys(publicKey).kemPublicKey; + return crypto.publicKeyBundleToKeys({ value: publicKey, encoding: "base64" }) + .kemPublicKey; } export async function wrapChatSecret(args: { @@ -79,7 +81,10 @@ export async function unwrapChatSecret(args: { ); } - const ownKeys = crypto.keyringToKeys(args.keyring); + const ownKeys = crypto.keyringToKeys({ + value: args.keyring, + encoding: "base64", + }); const sharedSecret = crypto.decapsulate( ownKeys.kemSecretKey, args.kemCiphertext, diff --git a/packages/mtp/package.json b/packages/mtp/package.json index 982593a..e036029 100644 --- a/packages/mtp/package.json +++ b/packages/mtp/package.json @@ -9,7 +9,8 @@ "scripts": { "format": "pnpm exec prettier --write .", "lint": "eslint src --ext .ts,.tsx", - "build": "tsc -p tsconfig.json --noEmit" + "test": "vitest run", + "build": "pnpm run test && tsc -p tsconfig.json --noEmit" }, "dependencies": { "@methanium/ui": "*", diff --git a/packages/mtp/src/context.test.tsx b/packages/mtp/src/context.test.tsx index 7f50aba..b15043a 100644 --- a/packages/mtp/src/context.test.tsx +++ b/packages/mtp/src/context.test.tsx @@ -1,6 +1,62 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; -import { isPushType, validateResponse } from "./context"; +import { RequestIdAllocator } from "./requestIds"; + +vi.mock("@methanium/ui", () => ({ + toast: { + dismiss: vi.fn(), + error: vi.fn(), + loading: vi.fn(), + }, +})); + +vi.mock("@tensamin/shared/log", () => ({ log: vi.fn() })); +vi.mock("@tensamin/storage/context", () => ({ + useStorage: () => ({ load: vi.fn() }), +})); + +const { completeInitialSynchronization, isPushType, validateResponse } = + await import("./context"); + +const validState = { + SessionId: 7, + VersionNumber: 2, + CacheSchemaVersion: 0, + SyncMode: "full", + Contacts: [], + Communities: [], + Calls: [], + Messages: [], + DeletedMessageIds: [], + DeletedContactIds: [], +}; + +function mockInitialSyncClient( + state: { type: string; data: unknown } = { + type: "ClientStateSync", + data: validState, + }, + acknowledgement: unknown = { type: "ClientStateAck", data: {} }, +) { + const handlers = new Map void>(); + const request = vi.fn().mockResolvedValue(acknowledgement); + const client = { + auth: vi.fn(async () => { + handlers.get(state.type)?.(state as never); + }), + subscribe: vi.fn((type: string, handler: (message: never) => void) => { + handlers.set(type, handler); + return () => handlers.delete(type); + }), + request, + disconnect: vi.fn(), + } as unknown as Parameters[0]; + return { client, handlers, request }; +} + +afterEach(() => { + vi.useRealTimers(); +}); describe("MTP protocol dispatch", () => { it("preserves protocol errors for the request layer", () => { @@ -19,3 +75,146 @@ describe("MTP protocol dispatch", () => { expect(isPushType("UnknownMessage")).toBe(false); }); }); + +describe("browser initial synchronization", () => { + it("validates state, sends a nonzero acknowledgement, then resolves", async () => { + const { client, request } = mockInitialSyncClient(); + + const state = await completeInitialSynchronization( + client, + new RequestIdAllocator(), + new AbortController().signal, + ); + + expect(state.data).toEqual(validState); + expect(request).toHaveBeenCalledWith( + "ClientStateAck", + { SessionId: 7, VersionNumber: 2 }, + { id: 1 }, + ); + }); + + it("does not acknowledge malformed state", async () => { + const { client, request } = mockInitialSyncClient({ + type: "ClientStateSync", + data: { ...validState, SyncMode: "invalid" }, + }); + + await expect( + completeInitialSynchronization( + client, + new RequestIdAllocator(), + new AbortController().signal, + ), + ).rejects.toThrow("Response validation failed"); + expect(request).not.toHaveBeenCalled(); + }); + + it("rejects ErrorNoIota during initial synchronization", async () => { + const { client, request } = mockInitialSyncClient({ + type: "ErrorNoIota", + data: {}, + }); + + await expect( + completeInitialSynchronization( + client, + new RequestIdAllocator(), + new AbortController().signal, + ), + ).rejects.toThrow("No Iota is currently connected"); + expect(request).not.toHaveBeenCalled(); + }); + + it("rejects an acknowledgement protocol error", async () => { + const { client } = mockInitialSyncClient(undefined, { + type: "ErrorInvalidData", + data: {}, + }); + + await expect( + completeInitialSynchronization( + client, + new RequestIdAllocator(), + new AbortController().signal, + ), + ).rejects.toThrow("State acknowledgement failed: ErrorInvalidData"); + }); + + it("times out a missing acknowledgement", async () => { + vi.useFakeTimers(); + const { client } = mockInitialSyncClient(); + vi.mocked(client.request).mockReturnValue(new Promise(() => {})); + const result = completeInitialSynchronization( + client, + new RequestIdAllocator(), + new AbortController().signal, + 100, + 10, + ); + const assertion = expect(result).rejects.toThrow( + "State acknowledgement timed out", + ); + + await vi.advanceTimersByTimeAsync(10); + await assertion; + }); + + it("stops immediately when the connection attempt is cancelled", async () => { + const { client } = mockInitialSyncClient(); + vi.mocked(client.auth).mockImplementation(() => new Promise(() => {})); + const controller = new AbortController(); + const result = completeInitialSynchronization( + client, + new RequestIdAllocator(), + controller.signal, + ); + + controller.abort(new Error("MTP connection lost")); + await expect(result).rejects.toThrow("MTP connection lost"); + }); + + it("times out authentication while waiting for initial state", async () => { + vi.useFakeTimers(); + const { client, handlers } = mockInitialSyncClient(); + vi.mocked(client.auth).mockImplementation(() => { + handlers.get("ClientStateSync")?.({ + type: "ClientStateSync", + data: validState, + } as never); + return new Promise(() => {}); + }); + const result = completeInitialSynchronization( + client, + new RequestIdAllocator(), + new AbortController().signal, + 100, + 10, + ); + const assertion = expect(result).rejects.toThrow( + "MTP authentication timed out", + ); + + await vi.advanceTimersByTimeAsync(100); + await assertion; + }); + + it("uses a fresh request ID namespace for each connection", async () => { + const first = mockInitialSyncClient(); + const second = mockInitialSyncClient(); + + await completeInitialSynchronization( + first.client, + new RequestIdAllocator(), + new AbortController().signal, + ); + await completeInitialSynchronization( + second.client, + new RequestIdAllocator(), + new AbortController().signal, + ); + + expect(first.request.mock.calls[0]?.[2]).toEqual({ id: 1 }); + expect(second.request.mock.calls[0]?.[2]).toEqual({ id: 1 }); + }); +}); diff --git a/packages/mtp/src/context.tsx b/packages/mtp/src/context.tsx index af06ee0..53c1862 100644 --- a/packages/mtp/src/context.tsx +++ b/packages/mtp/src/context.tsx @@ -27,7 +27,20 @@ import { log } from "@tensamin/shared/log"; import { ProtocolError } from "@tensamin/shared/errors"; import { useStorage } from "@tensamin/storage/context"; -import { RECONNECT_RESET, RECONNECT_TRIES, RETRY_INTERVAL } from "./values"; +import { fromWireMessage, toWireData } from "./protocolFields"; +import { RequestIdAllocator } from "./requestIds"; +import { + DISCOVERY_TIMEOUT, + INITIAL_SYNC_TIMEOUT, + RECONNECT_JITTER, + RECONNECT_LONG_INTERVAL, + RECONNECT_RESET, + RECONNECT_TRIES, + RETRY_INTERVAL, + STATE_ACK_TIMEOUT, +} from "./values"; + +type BrowserMtpClient = Awaited>; function base64ToUint8Array(b64: string) { const bin = atob(b64); @@ -51,7 +64,6 @@ export type ProtocolMessage< export type BoundSendFn = ( type: T, data?: z.infer, - options?: { id?: number }, ) => Promise>; export type PushHandler = (message: ProtocolMessage) => void | Promise; @@ -72,6 +84,29 @@ export function isPushType(type: string): boolean { return (PUSH_TYPES as readonly string[]).includes(type); } +function normalizeMtpMessage(message: T): T { + return fromWireMessage(message); +} + +async function requestWithId( + client: BrowserMtpClient, + ids: RequestIdAllocator, + type: string, + data: Record, +) { + let id: number; + try { + id = ids.allocate(); + } catch (error) { + client.disconnect(); + throw error; + } + + return client.request(type, toWireData(data) as Record, { + id, + }); +} + function removeMissingContacts( contacts: Contacts, message: ProtocolMessage, @@ -161,6 +196,120 @@ export function validateResponse( } as ProtocolMessage; } +function abortError(signal: AbortSignal): Error { + return signal.reason instanceof Error + ? signal.reason + : new Error("Initial state synchronization was cancelled"); +} + +function withDeadline( + promise: Promise, + timeoutMs: number, + timeoutMessage: string, + signal: AbortSignal, +): Promise { + return new Promise((resolve, reject) => { + if (signal.aborted) { + reject(abortError(signal)); + return; + } + let settled = false; + const finish = (complete: () => void) => { + if (settled) return; + settled = true; + clearTimeout(timeout); + signal.removeEventListener("abort", onAbort); + complete(); + }; + const timeout = setTimeout( + () => finish(() => reject(new Error(timeoutMessage))), + timeoutMs, + ); + const onAbort = () => finish(() => reject(abortError(signal))); + signal.addEventListener("abort", onAbort, { once: true }); + promise.then( + (value) => finish(() => resolve(value)), + (error: unknown) => finish(() => reject(error)), + ); + }); +} + +export async function completeInitialSynchronization( + client: BrowserMtpClient, + ids: RequestIdAllocator, + signal: AbortSignal, + syncTimeoutMs = INITIAL_SYNC_TIMEOUT, + ackTimeoutMs = STATE_ACK_TIMEOUT, +): Promise> { + const stateSync = new Promise>( + (resolve, reject) => { + let unsubscribeStateSync = () => {}; + let unsubscribeNoIota = () => {}; + const cleanup = () => { + clearTimeout(timeout); + unsubscribeStateSync(); + unsubscribeNoIota(); + signal.removeEventListener("abort", onAbort); + }; + const onAbort = () => { + cleanup(); + reject(abortError(signal)); + }; + const timeout = setTimeout(() => { + cleanup(); + reject(new Error("Initial state synchronization timed out")); + }, syncTimeoutMs); + signal.addEventListener("abort", onAbort, { once: true }); + if (signal.aborted) { + onAbort(); + return; + } + unsubscribeStateSync = client.subscribe("ClientStateSync", (message) => { + cleanup(); + try { + resolve( + validateResponse("ClientStateSync", normalizeMtpMessage(message)), + ); + } catch (error) { + reject(error); + } + }); + unsubscribeNoIota = client.subscribe("ErrorNoIota", () => { + cleanup(); + reject(new Error("No Iota is currently connected")); + }); + }, + ); + const [, state] = await Promise.all([ + withDeadline( + client.auth(), + syncTimeoutMs, + "MTP authentication timed out", + signal, + ), + stateSync, + ]); + if (state.type.startsWith("Error")) { + throw new Error(`State synchronization failed: ${state.type}`); + } + const acknowledgement = normalizeMtpMessage( + await withDeadline( + requestWithId(client, ids, "ClientStateAck", { + SessionId: state.data.SessionId, + VersionNumber: state.data.VersionNumber, + }), + ackTimeoutMs, + "State acknowledgement timed out", + signal, + ), + ); + if (acknowledgement.type.startsWith("Error")) { + throw new Error(`State acknowledgement failed: ${acknowledgement.type}`); + } + if (signal.aborted) throw abortError(signal); + return state; +} + function useMessageHandlers() { const interceptorsRef = useRef(new Set()); const pushHandlersRef = useRef(new Set()); @@ -205,6 +354,7 @@ function BrowserProvider(props: { const clientRef = useRef> | null>( null, ); + const requestIdsRef = useRef(null); const { addInterceptor, interceptorsRef, @@ -223,19 +373,24 @@ function BrowserProvider(props: { // Validation override functions const send: BoundSendFn = useMemo( - () => async (type, data, options) => { + () => async (type, data) => { const client = clientRef.current; + const ids = requestIdsRef.current; if (!client) { throw new Error("mtp is not connected"); } + if (!ids) { + throw new Error("MTP request allocator is unavailable"); + } - const message = await client.request( + const rawMessage = await requestWithId( + client, + ids, type, (data ?? {}) as Record, - options, ); - const response = validateResponse(type, message); + const response = validateResponse(type, normalizeMtpMessage(rawMessage)); setFreshContacts((contacts) => removeMissingContacts(contacts, response)); if (response.type.startsWith("Error")) { const errorData = response.data as Record; @@ -260,7 +415,7 @@ function BrowserProvider(props: { } return client.subscribe(type, (message) => { - handler(validateResponse(type, message)); + handler(validateResponse(type, normalizeMtpMessage(message))); }); }, []); @@ -291,33 +446,39 @@ function BrowserProvider(props: { const scheduleReconnect = (error: unknown) => { if (disposed || reconnectScheduled) return; - if (attempts >= RECONNECT_TRIES) { + attempts += 1; + const shortRetry = attempts <= RECONNECT_TRIES; + if (!shortRetry) { log(0, "mtp", "red", "Reconnection attempts exhausted", error); sonnerToast.error("Connection failed", { id: "mtp-connection-toast", description: error instanceof Error - ? error.message.split(":")[0] - : "Connection lost", + ? `${error.message.split(":")[0]}. Retrying in the background.` + : "Connection lost. Retrying in the background.", icon: null, duration: Infinity, closeButton: true, promise: null, } as unknown as Parameters[1]); - return; + } else { + sonnerToast.loading( + `Reconnecting to server... (attempt ${attempts} of ${RECONNECT_TRIES})`, + { id: "mtp-connection-toast" }, + ); } - attempts += 1; - sonnerToast.loading( - `Reconnecting to server... (attempt ${attempts} of ${RECONNECT_TRIES})`, - { id: "mtp-connection-toast" }, - ); + const baseDelay = shortRetry ? RETRY_INTERVAL : RECONNECT_LONG_INTERVAL; + const jitter = 1 + (Math.random() * 2 - 1) * RECONNECT_JITTER; reconnectScheduled = true; - reconnectTimer = setTimeout(() => { - reconnectScheduled = false; - reconnectTimer = null; - void connect(); - }, RETRY_INTERVAL); + reconnectTimer = setTimeout( + () => { + reconnectScheduled = false; + reconnectTimer = null; + void connect(); + }, + Math.round(baseDelay * jitter), + ); }; async function connect() { @@ -326,10 +487,16 @@ function BrowserProvider(props: { const generation = ++connectionGeneration; let client: Awaited> | null = null; let failed = false; + let connectionReady = false; + const attemptAbort = new AbortController(); const cleanup = () => { + attemptAbort.abort( + new Error("Initial state synchronization was cancelled"), + ); client?.disconnect(); if (clientRef.current === client) { clientRef.current = null; + requestIdsRef.current = null; } clearReconnectResetTimer(); if (generation === connectionGeneration) { @@ -359,19 +526,18 @@ function BrowserProvider(props: { omikronPublicKey = forcedOmikronPublicKey; } else { log(2, "mtp", "purple", "Fetching Omikron data."); - const data = await fetch(`${mtpUrl}api/get/omikron/${userId}`); + const data = await fetch(`${mtpUrl}api/get/omikron/${userId}`, { + signal: AbortSignal.any([ + attemptAbort.signal, + AbortSignal.timeout(DISCOVERY_TIMEOUT), + ]), + }); if (data.status === 404) { - sonnerToast.error("We couldn't reach your Iota", { - description: - "Check your network connection and try restarting your Iota", - icon: null, - duration: Infinity, - closeButton: true, - }); - resolveConnectionRef.current?.(); - cleanup(); - return; + throw new Error("No Omikron assignment is currently available"); + } + if (!data.ok) { + throw new Error(`Omikron discovery failed: HTTP ${data.status}`); } const omikronData = (await data.json()) as { id: number; @@ -404,7 +570,10 @@ function BrowserProvider(props: { clientId: userId, keyring: base64ToUint8Array(keyring), }, - hostPublicKey: omikronPublicKey, + hostPublicKey: { + value: omikronPublicKey, + encoding: "base64", + }, descriptor: "client", pings: true, logger: (event) => { @@ -418,10 +587,12 @@ function BrowserProvider(props: { !failed ) { failed = true; - clientRef.current = null; - setIdentified(false); - setIdentifying(false); - scheduleReconnect(new Error("MTP connection lost")); + const error = new Error("MTP connection lost"); + attemptAbort.abort(error); + if (connectionReady) { + cleanup(); + scheduleReconnect(error); + } } } @@ -456,11 +627,12 @@ function BrowserProvider(props: { const activeClient = client; clientRef.current = activeClient; + requestIdsRef.current = new RequestIdAllocator(); for (const type of PUSH_TYPES) { activeClient.subscribe(type, (message) => { let validated: ProtocolMessage; try { - validated = validateResponse(type, message); + validated = validateResponse(type, normalizeMtpMessage(message)); } catch (error) { log(1, "mtp", "red", "Failed to validate push message", error, { type, @@ -482,80 +654,45 @@ function BrowserProvider(props: { if (validated.type === "GetStates") { lastInitialStateRef.current = validated; } + if ( + validated.type === "ErrorNoIota" && + clientRef.current === activeClient && + !failed + ) { + failed = true; + const error = new Error("No Iota is currently connected"); + attemptAbort.abort(error); + cleanup(); + scheduleReconnect(error); + } }); } - setReadyState(activeClient.state); - - clearReconnectTimer(); - - // Schedule reconnect reset - clearReconnectResetTimer(); - reconnectResetTimer = setTimeout(() => { - attempts = 0; - reconnectResetTimer = null; - }, RECONNECT_RESET * 1_000); - setReadyState(activeClient.state); setIdentifying(true); - const stateSync = new Promise>( - (resolve, reject) => { - let unsubscribeStateSync = () => {}; - let unsubscribeNoIota = () => {}; - const cleanupStateSync = () => { - clearTimeout(timeout); - unsubscribeStateSync(); - unsubscribeNoIota(); - }; - const timeout = setTimeout(() => { - cleanupStateSync(); - reject(new Error("Initial state synchronization timed out")); - }, 120_000); - unsubscribeStateSync = activeClient.subscribe( - "ClientStateSync", - (message) => { - cleanupStateSync(); - try { - resolve(validateResponse("ClientStateSync", message)); - } catch (error) { - reject(error); - } - }, - ); - unsubscribeNoIota = activeClient.subscribe("ErrorNoIota", () => { - cleanupStateSync(); - reject(new Error("No Iota is currently connected")); - }); - }, + const ids = requestIdsRef.current; + if (!ids) throw new Error("MTP request allocator is unavailable"); + const finalResponse = await completeInitialSynchronization( + activeClient, + ids, + attemptAbort.signal, ); - const [, finalResponse] = await Promise.all([ - activeClient.auth(), - stateSync, - ]); - - if (finalResponse.type.startsWith("Error")) { - throw new Error( - `State synchronization failed: ${finalResponse.type}`, - ); - } - - const acknowledgement = await activeClient.request("ClientStateAck", { - SessionId: finalResponse.data.SessionId, - VersionNumber: finalResponse.data.VersionNumber, - }); - if (acknowledgement.type.startsWith("Error")) { - throw new Error( - `State acknowledgement failed: ${acknowledgement.type}`, - ); - } if (disposed || clientRef.current !== activeClient) return; setFreshContacts(finalResponse.data.Contacts); setFreshCommunities(finalResponse.data.Communities); setFreshCalls(finalResponse.data.Calls); + connectionReady = true; setIdentifying(false); setIdentified(true); + + clearReconnectTimer(); + clearReconnectResetTimer(); + reconnectResetTimer = setTimeout(() => { + attempts = 0; + reconnectResetTimer = null; + }, RECONNECT_RESET * 1_000); resolveConnectionRef.current?.(); } catch (connectError) { if (disposed || generation !== connectionGeneration) { @@ -589,6 +726,7 @@ function BrowserProvider(props: { clientRef.current?.disconnect(); clientRef.current = null; + requestIdsRef.current = null; setReadyState(ConnectionState.Disconnected); setIdentified(false); setIdentifying(false); @@ -650,9 +788,9 @@ function BrowserProvider(props: { }, [connected, identified, mtpUrl, send, subscribe, subscribePush, mtpRef]); const sendQueued: BoundSendFn = useMemo( - () => async (type, data, options) => { + () => async (type, data) => { const mtp = await mtpRef.get(); - const response = await mtp.send(type, data, options); + const response = await mtp.send(type, data); for (const interceptor of interceptorsRef.current) { void Promise.resolve( interceptor({ type, data, response: response as ProtocolMessage }), @@ -724,16 +862,32 @@ function TauriProvider(props: { if (next.error) { log(0, "android", "orange", "MTP connection failed", next.error); } - setSnapshot(next); - if (!next.identified || next.state === undefined) return; + if (!next.identified) { + setSnapshot(next); + return; + } + if (next.state === undefined) { + setSnapshot({ + ...next, + identified: false, + error: "Native MTP connection omitted initial state", + }); + return; + } const parsed = schemas.ClientStateSync.response.safeParse(next.state); if (!parsed.success) { log(0, "mtp", "red", "Invalid native MTP state", parsed.error); + setSnapshot({ + ...next, + identified: false, + error: "Invalid ClientStateSync payload", + }); return; } setFreshContacts(parsed.data.Contacts); setFreshCommunities(parsed.data.Communities); setFreshCalls(parsed.data.Calls); + setSnapshot(next); }, []); const dispatchMessage = useCallback( @@ -744,7 +898,7 @@ function TauriProvider(props: { try { validated = validateResponse( message.type as keyof Schemas & string, - message, + normalizeMtpMessage(message), ); } catch (error) { log(1, "mtp", "red", "Failed to validate native MTP message", error); @@ -846,13 +1000,12 @@ function TauriProvider(props: { }, [props.blockConnection]); const send = useCallback( - async (type, data, options) => { + async (type, data) => { const response = await invoke("mtp_request", { typeName: type, data: data ?? {}, - id: options?.id, }); - const validated = validateResponse(type, response); + const validated = validateResponse(type, normalizeMtpMessage(response)); setFreshContacts((contacts) => removeMissingContacts(contacts, validated), ); diff --git a/packages/mtp/src/index.ts b/packages/mtp/src/index.ts index 39bce46..19f5acd 100644 --- a/packages/mtp/src/index.ts +++ b/packages/mtp/src/index.ts @@ -1,4 +1,5 @@ export { Provider, useMTP } from "./context"; +export { RequestIdAllocator } from "./requestIds"; export type { BoundSendFn, MTPExchange, diff --git a/packages/mtp/src/protocolFields.test.ts b/packages/mtp/src/protocolFields.test.ts new file mode 100644 index 0000000..ef0c18f --- /dev/null +++ b/packages/mtp/src/protocolFields.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; + +import { fromWireData, toWireData } from "./protocolFields"; + +describe("MTP protocol field translation", () => { + it("maps application message fields to MTP wire fields", () => { + expect( + toWireData({ + Messages: [{ Content: "abc", MessageId: 4 }], + }), + ).toEqual({ + Messages: [{ AppContent: "abc", AppMessageId: 4 }], + }); + }); + + it("maps MTP wire fields back to application fields", () => { + expect( + fromWireData({ + AppCreatedAt: 123, + Message: { AppContent: "abc", AppMessageId: 4 }, + }), + ).toEqual({ + CreatedAt: 123, + Message: { Content: "abc", MessageId: 4 }, + }); + }); + + it("preserves byte arrays and unrelated fields", () => { + const bytes = new Uint8Array([1, 2, 3]); + const translated = toWireData({ Payload: bytes, Other: "value" }) as { + Payload: Uint8Array; + Other: string; + }; + + expect(translated.Payload).toBe(bytes); + expect(translated.Other).toBe("value"); + }); + + it("rejects field mapping collisions", () => { + expect(() => toWireData({ Content: "a", AppContent: "b" })).toThrow( + "MTP field translation collision", + ); + }); +}); diff --git a/packages/mtp/src/protocolFields.ts b/packages/mtp/src/protocolFields.ts new file mode 100644 index 0000000..3ec3d5d --- /dev/null +++ b/packages/mtp/src/protocolFields.ts @@ -0,0 +1,58 @@ +const APPLICATION_TO_WIRE_FIELDS = { + Content: "AppContent", + CreatedAt: "AppCreatedAt", + MessageId: "AppMessageId", +} as const; + +const WIRE_TO_APPLICATION_FIELDS = { + AppContent: "Content", + AppCreatedAt: "CreatedAt", + AppMessageId: "MessageId", +} as const; + +function mapProtocolFields( + value: unknown, + fieldMap: Readonly>, +): unknown { + if ( + value === null || + typeof value !== "object" || + value instanceof Uint8Array + ) { + return value; + } + + if (Array.isArray(value)) { + return value.map((item) => mapProtocolFields(item, fieldMap)); + } + + const source = value as Record; + const target: Record = {}; + + for (const [key, child] of Object.entries(source)) { + const mappedKey = fieldMap[key] ?? key; + + if (mappedKey in target) { + throw new Error(`MTP field translation collision for ${mappedKey}`); + } + + target[mappedKey] = mapProtocolFields(child, fieldMap); + } + + return target; +} + +export function toWireData(value: unknown): unknown { + return mapProtocolFields(value, APPLICATION_TO_WIRE_FIELDS); +} + +export function fromWireData(value: unknown): unknown { + return mapProtocolFields(value, WIRE_TO_APPLICATION_FIELDS); +} + +export function fromWireMessage(message: T): T { + return { + ...message, + data: fromWireData(message.data), + }; +} diff --git a/packages/mtp/src/requestIds.test.ts b/packages/mtp/src/requestIds.test.ts new file mode 100644 index 0000000..82fd508 --- /dev/null +++ b/packages/mtp/src/requestIds.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; + +import { RequestIdAllocator } from "./requestIds"; + +describe("MTP request ID allocation", () => { + it("allocates nonzero request IDs monotonically", () => { + const ids = new RequestIdAllocator(); + + expect(ids.allocate()).toBe(1); + expect(ids.allocate()).toBe(2); + expect(new RequestIdAllocator().allocate()).toBe(1); + }); + + it("does not wrap exhausted request IDs", () => { + const ids = new RequestIdAllocator(0x1_0000_0000); + + expect(() => ids.allocate()).toThrow("MTP request ID space exhausted"); + }); + + it("rejects invalid allocator states", () => { + expect(() => new RequestIdAllocator(0)).toThrow( + "invalid MTP request ID allocator state", + ); + }); +}); diff --git a/packages/mtp/src/requestIds.ts b/packages/mtp/src/requestIds.ts new file mode 100644 index 0000000..5581e8a --- /dev/null +++ b/packages/mtp/src/requestIds.ts @@ -0,0 +1,24 @@ +const MAX_MTP_REQUEST_ID = 0xffff_ffff; + +export class RequestIdAllocator { + #next: number; + + constructor(next = 1) { + if ( + !Number.isSafeInteger(next) || + next <= 0 || + next > MAX_MTP_REQUEST_ID + 1 + ) { + throw new RangeError("invalid MTP request ID allocator state"); + } + this.#next = next; + } + + allocate(): number { + if (this.#next > MAX_MTP_REQUEST_ID) { + throw new Error("MTP request ID space exhausted for this connection"); + } + + return this.#next++; + } +} diff --git a/packages/mtp/src/values.ts b/packages/mtp/src/values.ts index d9f6cd4..c5ba321 100644 --- a/packages/mtp/src/values.ts +++ b/packages/mtp/src/values.ts @@ -1,3 +1,8 @@ export const RETRY_INTERVAL = 3_000; export const RECONNECT_TRIES = 3; export const RECONNECT_RESET = 6; +export const RECONNECT_LONG_INTERVAL = 60_000; +export const RECONNECT_JITTER = 0.2; +export const DISCOVERY_TIMEOUT = 20_000; +export const INITIAL_SYNC_TIMEOUT = 120_000; +export const STATE_ACK_TIMEOUT = 30_000; diff --git a/packages/notifications/src/context.tsx b/packages/notifications/src/context.tsx index 1835dc1..7a8b32a 100644 --- a/packages/notifications/src/context.tsx +++ b/packages/notifications/src/context.tsx @@ -87,15 +87,9 @@ export default function Provider(props: { children: React.ReactNode }) { moveUserIdToTop(data.SenderId); if (await load("settings.receive_confirmations")) { - void send( - "MessageState", - { - MessageState: "received", - }, - { - id: data.Message.SendTime, - }, - ); + void send("MessageState", { + MessageState: "received", + }); } } diff --git a/packages/settings/src/layout.tsx b/packages/settings/src/layout.tsx index 53759cd..5d49f0b 100644 --- a/packages/settings/src/layout.tsx +++ b/packages/settings/src/layout.tsx @@ -1,8 +1,8 @@ -import { Outlet, useLocation, useNavigate } from "@tanstack/react-router"; -import { Button, ClearStorageButton, cn, useIsMobile } from "@methanium/ui"; +import { Outlet, useLocation } from "@tanstack/react-router"; +import { Button, useIsMobile } from "@methanium/ui"; import { ArrowLeft } from "lucide-react"; -import { settingsNavigation } from "./manifest"; +import { SettingsSidebar } from "./sidebar"; export default function SettingsLayout() { const isMobile = useIsMobile(); @@ -36,47 +36,3 @@ export default function SettingsLayout() { ); } - -export function SettingsSidebar({ - mobile = false, - className, -}: { - mobile?: boolean; - className?: string; -}) { - const navigate = useNavigate(); - const categories = [ - ...new Set(settingsNavigation.map((page) => page.category)), - ]; - - return ( -
- {categories.map((category) => ( -
-

{category}

- {settingsNavigation - .filter((page) => page.category === category) - .map((page) => ( - - ))} -
- ))} -
- -
-
- ); -} diff --git a/packages/settings/src/manifest.ts b/packages/settings/src/manifest.ts index 41fe14a..1afdd5a 100644 --- a/packages/settings/src/manifest.ts +++ b/packages/settings/src/manifest.ts @@ -8,46 +8,24 @@ import Profile from "./pages/profile"; import Security from "./pages/security"; import Theme from "./pages/theme"; import Hotkeys from "./pages/hotkeys"; +import { settingsNavigation } from "./navigation"; + +const pageComponents = { + profile: Profile, + security: Security, + chat: Chat, + call: Call, + cache: Cache, + theme: Theme, + accessibility: Accessibility, + hotkeys: Hotkeys, + licenses: Licenses, +} as const; export const settingsPages = [ { path: "/", component: Index }, - { - category: "account", - path: "profile", - label: "Profile", - component: Profile, - }, - { - category: "account", - path: "security", - label: "Security", - component: Security, - }, - { category: "general", path: "chat", label: "Chat", component: Chat }, - { category: "general", path: "call", label: "Call", component: Call }, - { category: "application", path: "cache", label: "Cache", component: Cache }, - { category: "application", path: "theme", label: "Theme", component: Theme }, - { - category: "application", - path: "accessibility", - label: "Accessibility", - component: Accessibility, - }, - { - category: "application", - path: "hotkeys", - label: "Hotkeys", - component: Hotkeys, - }, - { - category: "application", - path: "licenses", - label: "Licenses", - component: Licenses, - }, + ...settingsNavigation.map((page) => ({ + ...page, + component: pageComponents[page.path], + })), ] as const; - -export const settingsNavigation = settingsPages.filter( - (page): page is Exclude<(typeof settingsPages)[number], { path: "/" }> => - page.path !== "/", -); diff --git a/packages/settings/src/navigation.ts b/packages/settings/src/navigation.ts new file mode 100644 index 0000000..b47fae9 --- /dev/null +++ b/packages/settings/src/navigation.ts @@ -0,0 +1,31 @@ +export const settingsNavigation = [ + { + category: "account", + path: "profile", + label: "Profile", + }, + { + category: "account", + path: "security", + label: "Security", + }, + { category: "general", path: "chat", label: "Chat" }, + { category: "general", path: "call", label: "Call" }, + { category: "application", path: "cache", label: "Cache" }, + { category: "application", path: "theme", label: "Theme" }, + { + category: "application", + path: "accessibility", + label: "Accessibility", + }, + { + category: "application", + path: "hotkeys", + label: "Hotkeys", + }, + { + category: "application", + path: "licenses", + label: "Licenses", + }, +] as const; diff --git a/packages/settings/src/pages/index.tsx b/packages/settings/src/pages/index.tsx index 803a0c1..b5d04e3 100644 --- a/packages/settings/src/pages/index.tsx +++ b/packages/settings/src/pages/index.tsx @@ -1,4 +1,4 @@ -import { SettingsSidebar } from "../layout"; +import { SettingsSidebar } from "../sidebar"; export default function Page() { return ( diff --git a/packages/settings/src/sidebar.tsx b/packages/settings/src/sidebar.tsx new file mode 100644 index 0000000..3a2155e --- /dev/null +++ b/packages/settings/src/sidebar.tsx @@ -0,0 +1,48 @@ +import { Button, ClearStorageButton, cn } from "@methanium/ui"; +import { useNavigate } from "@tanstack/react-router"; + +import { settingsNavigation } from "./navigation"; + +export function SettingsSidebar({ + mobile = false, + className, +}: { + mobile?: boolean; + className?: string; +}) { + const navigate = useNavigate(); + const categories = [ + ...new Set(settingsNavigation.map((page) => page.category)), + ]; + + return ( +
+ {categories.map((category) => ( +
+

{category}

+ {settingsNavigation + .filter((page) => page.category === category) + .map((page) => ( + + ))} +
+ ))} +
+ +
+
+ ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index faff301..0cbdd6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: overrides: '@methanium/ui': https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz - mtp: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz + mtp: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz importers: @@ -16,17 +16,17 @@ importers: specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) mtp: - specifier: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz - version: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz sonner: - specifier: ^2.0.7 - version: 2.0.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^2.0.8 + version: 2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) devDependencies: '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) '@types/node': - specifier: ^26.1.2 + specifier: ^26.2.0 version: 26.2.0 '@types/react': specifier: ^19.2.18 @@ -35,20 +35,20 @@ importers: specifier: ^19.2.4 version: 19.2.4(@types/react@19.2.18) '@typescript-eslint/parser': - specifier: ^8.66.0 - version: 8.66.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + specifier: ^8.67.0 + version: 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) eslint: - specifier: ^10.8.0 + specifier: ^10.8.1 version: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) eslint-plugin-react-hooks: specifier: ^7.1.1 version: 7.1.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) fallow: - specifier: ^3.14.0 - version: 3.14.0 + specifier: ^3.17.0 + version: 3.17.0 globals: - specifier: ^17.9.0 - version: 17.9.0 + specifier: ^17.11.0 + version: 17.11.0 prettier: specifier: ^3.9.6 version: 3.9.6 @@ -56,11 +56,11 @@ importers: specifier: ^6.0.3 version: 6.0.3 typescript-eslint: - specifier: ^8.66.0 - version: 8.66.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + specifier: ^8.67.0 + version: 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) vitest: - specifier: ^4.1.10 - version: 4.1.10(@types/node@26.2.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) + specifier: ^4.1.11 + version: 4.1.11(@types/node@26.2.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) apps/electron: devDependencies: @@ -98,8 +98,8 @@ importers: specifier: workspace:* version: link:../../packages/storage mtp: - specifier: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz - version: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz react: specifier: ^19.2.8 version: 19.2.8 @@ -289,6 +289,9 @@ importers: globals: specifier: ^17.9.0 version: 17.9.0 + mtp: + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz typescript: specifier: ~6.0.3 version: 6.0.3 @@ -352,6 +355,9 @@ importers: lucide-react: specifier: ^1.29.0 version: 1.30.0(react@19.2.8) + mtp: + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz react: specifier: ^19.2.8 version: 19.2.8 @@ -424,6 +430,9 @@ importers: packages/crypto: dependencies: + mtp: + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz react: specifier: ^19.2.8 version: 19.2.8 @@ -468,8 +477,8 @@ importers: specifier: workspace:* version: link:../storage mtp: - specifier: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz - version: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz react: specifier: ^19.2.8 version: 19.2.8 @@ -1264,8 +1273,8 @@ packages: '@codemirror/autocomplete@6.20.3': resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} - '@codemirror/commands@6.10.4': - resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} + '@codemirror/commands@6.11.0': + resolution: {integrity: sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==} '@codemirror/lang-css@6.3.1': resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} @@ -1288,8 +1297,8 @@ packages: '@codemirror/state@6.7.1': resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} - '@codemirror/view@6.43.8': - resolution: {integrity: sha512-qtItTDssZ/5GFfi94hrILu9j/VUeFPDPkhovEfmWFj2ipTxnzPB8DdHgfbb8HYTzLTYhrndKmyQxXUz/PDLenw==} + '@codemirror/view@6.43.9': + resolution: {integrity: sha512-sTuUzTpPMFebRhg6dawChoKKgndIwfjmJgKVxBefPElcU2NwQ6AFroupk0SFqEerQyZOGRfDNnSN8Dw/lMAsXw==} '@dotenvx/dotenvx@1.75.1': resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==} @@ -1537,43 +1546,43 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@fallow-cli/darwin-arm64@3.14.0': - resolution: {integrity: sha512-seaix3OqADcq90PkPOOPrN8Jl7QZDGjFziHA85Ikp+lGSHVjJ2IMyGh/QKLhvi9Q6Ha3Y//F0+PFFGNd0Gi4mw==} + '@fallow-cli/darwin-arm64@3.17.0': + resolution: {integrity: sha512-u9O4aQaTSdaZaa4MAVEZEiwOx6QQzChymyQPrxV6KvDcVbcl0oYSekJKdvN4gEqUY8ClMze9gWS1tbn/W3N4dQ==} cpu: [arm64] os: [darwin] - '@fallow-cli/darwin-x64@3.14.0': - resolution: {integrity: sha512-qgYZHHTFobuJCtON7aVe/tXCUjiF/9nKT4tNLVfUPH4Qe9KwieO4rAuknDOnxkjzsWPO4MfHyCbcVdslf4JMnw==} + '@fallow-cli/darwin-x64@3.17.0': + resolution: {integrity: sha512-l3tZJovnCa8j+im4qTl4GVSfrx4gisJYeHDwb5TbcdTGaWYz42gHriAaz5MhOR8S24q3xRegX8QrI8PA4XXU4g==} cpu: [x64] os: [darwin] - '@fallow-cli/linux-arm64-gnu@3.14.0': - resolution: {integrity: sha512-FwmIG391lMViBXh1cAvOcwQxbI2sLjEYkugFauVbmntjuJt6xVxQXAxjybdFXSL/jjfFnbAzPKy9oHlhq3sA6A==} + '@fallow-cli/linux-arm64-gnu@3.17.0': + resolution: {integrity: sha512-IAjS6qR4tHcRo1y5lzYT6mktN/ATPyPNrJ6yjwTORwwQV1YfMO58A5zGBfJlPBY+lUmaMZ9w37ftvH9a2AS4dQ==} cpu: [arm64] os: [linux] - '@fallow-cli/linux-arm64-musl@3.14.0': - resolution: {integrity: sha512-x4R9aNT2kIoSHRIZ5s2MZ+swyb3uutV6SSYCwWhPSE5UI/oCEYWNWiYS7uKMLM0cTuMPVcsm2qMTd4iYA3ACLw==} + '@fallow-cli/linux-arm64-musl@3.17.0': + resolution: {integrity: sha512-Oe1dvk5Wt5Bmnxfc6HufJTaFY9SEwDVrZpnSdVQlv+nvz7DzrTK1r91cR3Ip477ANSLa9XD/MHPMpfqqo0dlZg==} cpu: [arm64] os: [linux] - '@fallow-cli/linux-x64-gnu@3.14.0': - resolution: {integrity: sha512-8kBxy9FkwKjiH1C19T0nSkmbtldwVyWLQWajIqvyHE0x4rurHhve7RQrwEDPzNMFu7OgYCMU1nXr/AMIJ6Kg9w==} + '@fallow-cli/linux-x64-gnu@3.17.0': + resolution: {integrity: sha512-XgLU2a51zuky7gifjVDHuiJGToAReDsVCDcyDF84TQz/h3bqO0iRkpDL8dh7+HODku84+EffpK5ZHY19n6Ij0w==} cpu: [x64] os: [linux] - '@fallow-cli/linux-x64-musl@3.14.0': - resolution: {integrity: sha512-9TAdXSpYvk9WkKy2GR8T/PHbq7QCTO/SYUmiZ+EAJUMzXBvSfnpj8cCGQpeAD000Nb+o7rL94lIAPJMAqMKO3Q==} + '@fallow-cli/linux-x64-musl@3.17.0': + resolution: {integrity: sha512-GnDZx2kbLKKdYAp05NSKv6CZHgzEKvhRd0btCKFMMpUEhdzmr4uh2Qb4UzKDBg/q2r1EhkzN4b/qC+tbFCmtwA==} cpu: [x64] os: [linux] - '@fallow-cli/win32-arm64-msvc@3.14.0': - resolution: {integrity: sha512-xc495cracyJL+0UKwcqNhofIz2K5XiJbb+YFhxrbSrrbm+EVDnQKpCOzzOJYbm7RPxF6XOxJpATn7ciBy7Rqrw==} + '@fallow-cli/win32-arm64-msvc@3.17.0': + resolution: {integrity: sha512-u9n5WzPuKuC1PKegKQknWtngAUJ2szQaRXsbY54s7UMqeFNyC+W04LV5D+sEZywU7L0kzL+Kck9LxFLXBQcgvA==} cpu: [arm64] os: [win32] - '@fallow-cli/win32-x64-msvc@3.14.0': - resolution: {integrity: sha512-ihyCZ5GMoYGnzlPP6b5pZBBcGZhDHDW2u1YEGkZEuf3pbW6E45U7Hhf213cE2CCheScXOirK6AKWnLM/Qj4c2A==} + '@fallow-cli/win32-x64-msvc@3.17.0': + resolution: {integrity: sha512-/OcyxHuCKabzQ9hKIjM/CFJ9fr30n0Mw5MK6nzi5rn26mbYUgh073UZq4FONOpPJ/FqHtC726rxpgBtR4qS+gg==} cpu: [x64] os: [win32] @@ -1598,8 +1607,8 @@ packages: '@fontsource-variable/public-sans@5.3.0': resolution: {integrity: sha512-AVfkmAt50BMXWpOO21FAntiJFKGX6xTc2dSL8dxtDteONe9IuRXJWGbs0EbG955vAMCq23ENeuopuW87cGWDSQ==} - '@hono/node-server@2.1.0': - resolution: {integrity: sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg==} + '@hono/node-server@2.1.1': + resolution: {integrity: sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==} engines: {node: '>=20'} peerDependencies: hono: ^4 @@ -2226,32 +2235,32 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@shikijs/core@4.4.2': - resolution: {integrity: sha512-StyzbAyxg2/tBGf78gwbBkGyeQ73lf8UiJArFaQhTQIDqQOCKPCQFanvrs4/Yv3Yfyc+ONInJM6K+FMIf+P+kA==} + '@shikijs/core@4.4.3': + resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==} engines: {node: '>=20'} - '@shikijs/engine-javascript@4.4.2': - resolution: {integrity: sha512-MnIkeqWdVPUWsxlx8gKLVCJFTsqrQJgpTPBPpQwaFeJ56lOnJxj5aN2LUFnfxUEcvOQuNocmbaMnVrCEln6rkw==} + '@shikijs/engine-javascript@4.4.3': + resolution: {integrity: sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==} engines: {node: '>=20'} - '@shikijs/engine-oniguruma@4.4.2': - resolution: {integrity: sha512-GLhowz1+jixjz+wiZ3wMnOn1jTxiFCGl2PkXufivbnwPHKuyw1AYqu5/hbWhZZ2oAb0NP05WUJhYeigY14drnw==} + '@shikijs/engine-oniguruma@4.4.3': + resolution: {integrity: sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==} engines: {node: '>=20'} - '@shikijs/langs@4.4.2': - resolution: {integrity: sha512-8DfeusD+Zdv/eYIDdXyJTUnSMHt+aAWjAOCXV20HNGAHRlInXpG8wh421v6B91WOm9TFwRLN+b/LG5F2NAIojg==} + '@shikijs/langs@4.4.3': + resolution: {integrity: sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==} engines: {node: '>=20'} - '@shikijs/primitive@4.4.2': - resolution: {integrity: sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA==} + '@shikijs/primitive@4.4.3': + resolution: {integrity: sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==} engines: {node: '>=20'} - '@shikijs/themes@4.4.2': - resolution: {integrity: sha512-H0CFoL07ddDC2Dd6EdrPYNkRhUR6YCkJlnuYFceYYUJJA5TIm2b5B33qqiDYryBExgbKMndFJPb2u1gTuqO37g==} + '@shikijs/themes@4.4.3': + resolution: {integrity: sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==} engines: {node: '>=20'} - '@shikijs/types@4.4.2': - resolution: {integrity: sha512-PFYitV4vpDr/iPCIhnHp+Q4ftic5N5VeNJ3KQ1O8gn3h2ar8qgwMAXF7tq4m1CWaMS60fV4VqF6vfnWH4F7vqQ==} + '@shikijs/types@4.4.3': + resolution: {integrity: sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==} engines: {node: '>=20'} '@shikijs/vscode-textmate@10.0.2': @@ -2652,6 +2661,14 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/eslint-plugin@8.67.0': + resolution: {integrity: sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.67.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/parser@8.66.0': resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2659,22 +2676,45 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/parser@8.67.0': + resolution: {integrity: sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.66.0': resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.67.0': + resolution: {integrity: sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/scope-manager@8.66.0': resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.67.0': + resolution: {integrity: sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.66.0': resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/tsconfig-utils@8.67.0': + resolution: {integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.66.0': resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2682,16 +2722,33 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.67.0': + resolution: {integrity: sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/types@8.66.0': resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.67.0': + resolution: {integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.66.0': resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/typescript-estree@8.67.0': + resolution: {integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.66.0': resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2699,10 +2756,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.67.0': + resolution: {integrity: sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@8.66.0': resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.67.0': + resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/typescript-aix-ppc64@7.0.2': resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} engines: {node: '>=16.20.0'} @@ -2839,11 +2907,11 @@ packages: babel-plugin-react-compiler: optional: true - '@vitest/expect@4.1.10': - resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} - '@vitest/mocker@4.1.10': - resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2853,20 +2921,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.10': - resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} - '@vitest/runner@4.1.10': - resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} - '@vitest/snapshot@4.1.10': - resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} - '@vitest/spy@4.1.10': - resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} - '@vitest/utils@4.1.10': - resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} '@xmldom/xmldom@0.8.13': resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} @@ -2925,8 +2993,8 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} engines: {node: '>=12'} ansi-styles@4.3.0: @@ -3024,8 +3092,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.11.12: - resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==} + baseline-browser-mapping@2.11.15: + resolution: {integrity: sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==} engines: {node: '>=6.0.0'} hasBin: true @@ -3054,8 +3122,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.7: - resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -3242,8 +3310,8 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} engines: {node: '>=18'} convert-source-map@2.0.0: @@ -3406,8 +3474,8 @@ packages: resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} engines: {node: '>=18'} - default-browser@5.5.0: - resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + default-browser@5.5.1: + resolution: {integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==} engines: {node: '>=18'} defer-to-connect@2.0.1: @@ -3507,8 +3575,8 @@ packages: electron-publish@26.15.3: resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==} - electron-to-chromium@1.5.402: - resolution: {integrity: sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==} + electron-to-chromium@1.5.411: + resolution: {integrity: sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg==} electron-winstaller@5.4.0: resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} @@ -3596,8 +3664,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} @@ -3714,8 +3782,8 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - eventsource-parser@3.1.0: - resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + eventsource-parser@3.1.1: + resolution: {integrity: sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==} engines: {node: '>=18.0.0'} eventsource@3.0.7: @@ -3750,13 +3818,13 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - fallow-type-aware@3.14.0: - resolution: {integrity: sha512-D8+bNELjmoVV2dKpmHplQ1YzuheHltmZKaOVJ//hx7h8vFNyp5H3hcJV8rR26ofBU51z9K5eyA7FpgP3hebjwg==} + fallow-type-aware@3.17.0: + resolution: {integrity: sha512-DFtZUK5oqP56tSsrBWPzjrTIwPPxwlBV8QoSaGghMru347Nqj8ZBY6u9HFa/TIPcxvFU/zIZMRF8xS/XAWQTzg==} engines: {node: '>=20'} hasBin: true - fallow@3.14.0: - resolution: {integrity: sha512-hse+sChmWkrffLiZDPGdNSyiLybVEhgoiYCIYgqLwdYhfTCO+f2PS1g+gMhGAJdLlYpZ75tFwRoMBFmZjN3Q6g==} + fallow@3.17.0: + resolution: {integrity: sha512-tbLuvsPq3I2CzpoF/9MgSQ7hWzOqlj3avFUAmCr1J9OERE6z714SlDmA38NVvNy/zuK/70utXB3/b33xC9z29g==} engines: {node: '>=22'} hasBin: true @@ -3974,6 +4042,10 @@ packages: resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} engines: {node: '>=10.0'} + globals@17.11.0: + resolution: {integrity: sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==} + engines: {node: '>=18'} + globals@17.9.0: resolution: {integrity: sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==} engines: {node: '>=18'} @@ -4065,8 +4137,8 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - hono@4.13.1: - resolution: {integrity: sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==} + hono@4.13.3: + resolution: {integrity: sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==} engines: {node: '>=16.9.0'} hosted-git-info@4.1.0: @@ -4142,8 +4214,8 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} - input-otp@1.4.2: - resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} + input-otp@1.5.0: + resolution: {integrity: sha512-3AcfdW1sNG0FmSA5hHMBXG7jNW5CdcdKs8ln8JOmn6S2SRkoXoJx+2UwC+ZvjflfnOdHJx0SfQ9/YjlxVYLtGQ==} peerDependencies: react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc @@ -4156,8 +4228,8 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - ip-address@10.4.0: - resolution: {integrity: sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==} + ip-address@10.5.0: + resolution: {integrity: sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -4409,6 +4481,9 @@ packages: jose@6.2.8: resolution: {integrity: sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==} + jose@6.2.9: + resolution: {integrity: sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4695,8 +4770,8 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - lucide-react@1.31.0: - resolution: {integrity: sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg==} + lucide-react@1.32.0: + resolution: {integrity: sha512-txX56hMFnRxPi1f9/nH69YN8uvAO6a7Y1KSWKjCDAtdD9+soEgmWuCt6iRm1pkxUZo2+YntSdsE1L6bIuKoY8Q==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4962,9 +5037,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mtp@https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz: - resolution: {integrity: sha512-WcXHd53aDM0zQfO8eaUnK5tpnRP73GlWjwAMMcstcVAIWrvyFGuINkCRNEkb2RsNk9h57UMhxElCZeBkFrVmCg==, tarball: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz} - version: 0.2.0 + mtp@https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz: + resolution: {integrity: sha512-rI+xskgAp93o9EdBO/b7HnA0PZTQQPTYuF4Snon/hTvayN+x9O40oad4n/ZUF0ahS6grw00MPKPe89Mfe3BHnw==, tarball: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz} + version: 0.3.0 nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} @@ -5065,8 +5140,8 @@ packages: oniguruma-to-es@4.3.6: resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} - open@11.0.0: - resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + open@11.0.1: + resolution: {integrity: sha512-NzwMUB6C1D0+Kd+9iMS/H4k+Ck3cTX6Ckyfr/gAGlmvSE1LUQZnEZvWBi4PYmMwH/S5SMeTXnE+9uAz8uF+pWw==} engines: {node: '>=20'} open@8.4.2: @@ -5222,6 +5297,10 @@ packages: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} + powershell-utils@0.2.0: + resolution: {integrity: sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw==} + engines: {node: '>=20'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -5351,8 +5430,8 @@ packages: '@types/react': optional: true - react-resizable-panels@4.12.2: - resolution: {integrity: sha512-NwY5LCo4WrxVvDh0xoMML6EMLPONP/8ckKcIdpnojxexoatZdjLiRqLJQjQK5CPkd4SYiB/2M5BVrjZBQtOO7Q==} + react-resizable-panels@4.12.3: + resolution: {integrity: sha512-GHMJWnDXui/3RX4bT+cgBP+N3N2nkwcoZATr/2xLFpqQQe7TlBrE0cGM0dUa9ceT7A90tUrSRsiqgRG+g0/2AA==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 @@ -5378,8 +5457,8 @@ packages: readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - recast@0.23.19: - resolution: {integrity: sha512-T98lym7kH+pnZmRaD8yDRdaNqyUbwnbEBx0MuchrzMFOEMray4AO3ZJoTUZ5r78Ao78X/OhzW0DL8GB85w/I2w==} + recast@0.23.21: + resolution: {integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==} engines: {node: '>= 4'} recharts@3.10.1: @@ -5632,8 +5711,8 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shadcn@4.16.2: - resolution: {integrity: sha512-M1AvZKFWcCzWRDoyApIqJMSLIpY8Ev4uBGuiPLSFmiTbixXhPmzotSTvLzFmBrfoIxG9aIg2dZOETblEaXGUnQ==} + shadcn@4.18.0: + resolution: {integrity: sha512-tUFZgkYmfVNQVm3xX7lhSzOvDsp+O14ac5dwgXIr5mIsr79ISueb/Mu+ZtWMz0DH6v77u4eYyvbQ9TTMpSn3aw==} engines: {node: '>=20.18.1'} hasBin: true @@ -5645,8 +5724,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki@4.4.2: - resolution: {integrity: sha512-P8F/dFhRevaw2uSdeIYlq/5SXZNY85DPtmXQ947gD1Zj2JqO5AkNvVVBar0Me9JkFx3uzVud/qOtP5ek9NEQGA==} + shiki@4.4.3: + resolution: {integrity: sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==} engines: {node: '>=20'} side-channel-list@1.0.1: @@ -5682,10 +5761,18 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + smob@1.6.2: resolution: {integrity: sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==} engines: {node: '>=20.0.0'} + socks@2.8.9: + resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: @@ -5979,6 +6066,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + typescript-eslint@8.67.0: + resolution: {integrity: sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -6074,8 +6168,8 @@ packages: resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} engines: {node: '>=4'} - update-browserslist-db@1.3.0: - resolution: {integrity: sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA==} + update-browserslist-db@1.3.1: + resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -6201,20 +6295,20 @@ packages: yaml: optional: true - vitest@4.1.10: - resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.10 - '@vitest/browser-preview': 4.1.10 - '@vitest/browser-webdriverio': 4.1.10 - '@vitest/coverage-istanbul': 4.1.10 - '@vitest/coverage-v8': 4.1.10 - '@vitest/ui': 4.1.10 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -6356,8 +6450,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - wsl-utils@0.3.1: - resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + wsl-utils@1.0.0: + resolution: {integrity: sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==} engines: {node: '>=20'} xmlbuilder@15.1.1: @@ -6493,7 +6587,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.7 + browserslist: 4.28.8 lru-cache: 5.1.1 semver: 6.3.1 @@ -7173,14 +7267,14 @@ snapshots: dependencies: '@codemirror/language': 6.12.4 '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 + '@codemirror/view': 6.43.9 '@lezer/common': 1.5.2 - '@codemirror/commands@6.10.4': + '@codemirror/commands@6.11.0': dependencies: '@codemirror/language': 6.12.4 '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 + '@codemirror/view': 6.43.9 '@lezer/common': 1.5.2 '@codemirror/lang-css@6.3.1': @@ -7198,7 +7292,7 @@ snapshots: '@codemirror/lang-javascript': 6.2.5 '@codemirror/language': 6.12.4 '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 + '@codemirror/view': 6.43.9 '@lezer/common': 1.5.2 '@lezer/css': 1.3.6 '@lezer/html': 1.3.13 @@ -7209,7 +7303,7 @@ snapshots: '@codemirror/language': 6.12.4 '@codemirror/lint': 6.9.7 '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 + '@codemirror/view': 6.43.9 '@lezer/common': 1.5.2 '@lezer/javascript': 1.5.4 @@ -7219,14 +7313,14 @@ snapshots: '@codemirror/lang-html': 6.4.12 '@codemirror/language': 6.12.4 '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 + '@codemirror/view': 6.43.9 '@lezer/common': 1.5.2 '@lezer/markdown': 1.7.2 '@codemirror/language@6.12.4': dependencies: '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 + '@codemirror/view': 6.43.9 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 @@ -7235,14 +7329,14 @@ snapshots: '@codemirror/lint@6.9.7': dependencies: '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 + '@codemirror/view': 6.43.9 crelt: 1.0.7 '@codemirror/state@6.7.1': dependencies: '@marijn/find-cluster-break': 1.0.3 - '@codemirror/view@6.43.8': + '@codemirror/view@6.43.9': dependencies: '@codemirror/state': 6.7.1 crelt: 1.0.7 @@ -7476,28 +7570,28 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@fallow-cli/darwin-arm64@3.14.0': + '@fallow-cli/darwin-arm64@3.17.0': optional: true - '@fallow-cli/darwin-x64@3.14.0': + '@fallow-cli/darwin-x64@3.17.0': optional: true - '@fallow-cli/linux-arm64-gnu@3.14.0': + '@fallow-cli/linux-arm64-gnu@3.17.0': optional: true - '@fallow-cli/linux-arm64-musl@3.14.0': + '@fallow-cli/linux-arm64-musl@3.17.0': optional: true - '@fallow-cli/linux-x64-gnu@3.14.0': + '@fallow-cli/linux-x64-gnu@3.17.0': optional: true - '@fallow-cli/linux-x64-musl@3.14.0': + '@fallow-cli/linux-x64-musl@3.17.0': optional: true - '@fallow-cli/win32-arm64-msvc@3.14.0': + '@fallow-cli/win32-arm64-msvc@3.17.0': optional: true - '@fallow-cli/win32-x64-msvc@3.14.0': + '@fallow-cli/win32-x64-msvc@3.17.0': optional: true '@floating-ui/core@1.8.0': @@ -7524,9 +7618,9 @@ snapshots: '@fontsource-variable/public-sans@5.3.0': {} - '@hono/node-server@2.1.0(hono@4.13.1)': + '@hono/node-server@2.1.1(hono@4.13.3)': dependencies: - hono: 4.13.1 + hono: 4.13.3 '@humanfs/core@0.19.2': dependencies: @@ -7652,12 +7746,12 @@ snapshots: dependencies: '@base-ui/react': 1.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@codemirror/autocomplete': 6.20.3 - '@codemirror/commands': 6.10.4 + '@codemirror/commands': 6.11.0 '@codemirror/lang-markdown': 6.5.2 '@codemirror/language': 6.12.4 '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 - '@shikijs/langs': 4.4.2 + '@codemirror/view': 6.43.9 + '@shikijs/langs': 4.4.3 '@tauri-apps/api': 2.11.1 class-variance-authority: 0.7.1 clsx: 2.1.1 @@ -7665,14 +7759,14 @@ snapshots: embla-carousel-react: 8.6.0(react@19.2.8) emojibase-data: 17.0.0(emojibase@17.0.0) hast-util-to-jsx-runtime: 2.3.6(supports-color@7.2.0) - input-otp: 1.4.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + input-otp: 1.5.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) katex: 0.18.4 - lucide-react: 1.31.0(react@19.2.8) + lucide-react: 1.32.0(react@19.2.8) next-themes: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) react-markdown: 10.1.0(@types/react@19.2.18)(react@19.2.8)(supports-color@7.2.0) - react-resizable-panels: 4.12.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-resizable-panels: 4.12.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) recharts: 3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1) rehype-autolink-headings: 7.1.0 rehype-katex: 7.0.1 @@ -7680,8 +7774,8 @@ snapshots: remark-breaks: 4.0.0 remark-gfm: 4.0.1(supports-color@7.2.0) remark-math: 6.0.0(supports-color@7.2.0) - shadcn: 4.16.2(supports-color@7.2.0)(typescript@6.0.3) - shiki: 4.4.2 + shadcn: 4.18.0(supports-color@7.2.0)(typescript@6.0.3) + shiki: 4.4.3 sonner: 2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) tailwind-merge: 3.6.0 tw-animate-css: 1.4.0 @@ -7702,18 +7796,18 @@ snapshots: '@modelcontextprotocol/sdk@1.30.0(supports-color@7.2.0)(zod@3.25.76)': dependencies: - '@hono/node-server': 2.1.0(hono@4.13.1) + '@hono/node-server': 2.1.1(hono@4.13.3) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 cors: 2.8.6 cross-spawn: 7.0.6 eventsource: 3.0.7 - eventsource-parser: 3.1.0 + eventsource-parser: 3.1.1 express: 5.2.1(supports-color@7.2.0) express-rate-limit: 8.6.2(express@5.2.1(supports-color@7.2.0))(supports-color@7.2.0) - hono: 4.13.1 - jose: 6.2.8 + hono: 4.13.3 + jose: 6.2.9 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 @@ -8078,40 +8172,40 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@shikijs/core@4.4.2': + '@shikijs/core@4.4.3': dependencies: - '@shikijs/primitive': 4.4.2 - '@shikijs/types': 4.4.2 + '@shikijs/primitive': 4.4.3 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@4.4.2': + '@shikijs/engine-javascript@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.6 - '@shikijs/engine-oniguruma@4.4.2': + '@shikijs/engine-oniguruma@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@4.4.2': + '@shikijs/langs@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 - '@shikijs/primitive@4.4.2': + '@shikijs/primitive@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 - '@shikijs/themes@4.4.2': + '@shikijs/themes@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 - '@shikijs/types@4.4.2': + '@shikijs/types@4.4.3': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 @@ -8465,6 +8559,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.67.0 + '@typescript-eslint/type-utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.67.0 + eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + ignore: 7.0.6 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.66.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.66.0 @@ -8477,6 +8587,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.67.0 + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.67.0 + debug: 4.4.3(supports-color@7.2.0) + eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/project-service@8.66.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3) @@ -8486,15 +8608,33 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.67.0(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@6.0.3) + '@typescript-eslint/types': 8.67.0 + debug: 4.4.3(supports-color@7.2.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.66.0': dependencies: '@typescript-eslint/types': 8.66.0 '@typescript-eslint/visitor-keys': 8.66.0 + '@typescript-eslint/scope-manager@8.67.0': + dependencies: + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/tsconfig-utils@8.66.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 + '@typescript-eslint/tsconfig-utils@8.67.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + '@typescript-eslint/type-utils@8.66.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.66.0 @@ -8507,8 +8647,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + debug: 4.4.3(supports-color@7.2.0) + eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@8.66.0': {} + '@typescript-eslint/types@8.67.0': {} + '@typescript-eslint/typescript-estree@8.66.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/project-service': 8.66.0(supports-color@7.2.0)(typescript@6.0.3) @@ -8524,6 +8678,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.67.0(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@6.0.3) + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/visitor-keys': 8.67.0 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.66.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) @@ -8535,11 +8704,27 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) + '@typescript-eslint/scope-manager': 8.67.0 + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) + eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.66.0': dependencies: '@typescript-eslint/types': 8.66.0 eslint-visitor-keys: 5.0.1 + '@typescript-eslint/visitor-keys@8.67.0': + dependencies: + '@typescript-eslint/types': 8.67.0 + eslint-visitor-keys: 5.0.1 + '@typescript/typescript-aix-ppc64@7.0.2': optional: true @@ -8607,44 +8792,44 @@ snapshots: '@rolldown/pluginutils': 1.0.1 vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) - '@vitest/expect@4.1.10': + '@vitest/expect@4.1.11': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.11(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.10 + '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) - '@vitest/pretty-format@4.1.10': + '@vitest/pretty-format@4.1.11': dependencies: tinyrainbow: 3.1.1 - '@vitest/runner@4.1.10': + '@vitest/runner@4.1.11': dependencies: - '@vitest/utils': 4.1.10 + '@vitest/utils': 4.1.11 pathe: 2.0.3 - '@vitest/snapshot@4.1.10': + '@vitest/snapshot@4.1.11': dependencies: - '@vitest/pretty-format': 4.1.10 - '@vitest/utils': 4.1.10 + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.10': {} + '@vitest/spy@4.1.11': {} - '@vitest/utils@4.1.10': + '@vitest/utils@4.1.11': dependencies: - '@vitest/pretty-format': 4.1.10 + '@vitest/pretty-format': 4.1.11 convert-source-map: 2.0.0 tinyrainbow: 3.1.1 @@ -8691,7 +8876,7 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.2.2: {} + ansi-regex@6.3.0: {} ansi-styles@4.3.0: dependencies: @@ -8828,14 +9013,14 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.11.12: {} + baseline-browser-mapping@2.11.15: {} bluebird@3.7.2: {} body-parser@2.3.0(supports-color@7.2.0): dependencies: bytes: 3.1.2 - content-type: 2.0.0 + content-type: 2.1.0 debug: 4.4.3(supports-color@7.2.0) http-errors: 2.0.1 iconv-lite: 0.7.3 @@ -8866,13 +9051,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.7: + browserslist@4.28.8: dependencies: - baseline-browser-mapping: 2.11.12 + baseline-browser-mapping: 2.11.15 caniuse-lite: 1.0.30001809 - electron-to-chromium: 1.5.402 + electron-to-chromium: 1.5.411 node-releases: 2.0.53 - update-browserslist-db: 1.3.0(browserslist@4.28.7) + update-browserslist-db: 1.3.1(browserslist@4.28.8) buffer-from@1.1.2: {} @@ -9054,7 +9239,7 @@ snapshots: content-type@1.0.5: {} - content-type@2.0.0: {} + content-type@2.1.0: {} convert-source-map@2.0.0: {} @@ -9066,7 +9251,7 @@ snapshots: core-js-compat@3.50.0: dependencies: - browserslist: 4.28.7 + browserslist: 4.28.8 core-util-is@1.0.3: {} @@ -9189,7 +9374,7 @@ snapshots: default-browser-id@5.0.1: {} - default-browser@5.5.0: + default-browser@5.5.1: dependencies: bundle-name: 4.1.0 default-browser-id: 5.0.1 @@ -9313,7 +9498,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-to-chromium@1.5.402: {} + electron-to-chromium@1.5.411: {} electron-winstaller@5.4.0(supports-color@7.2.0): dependencies: @@ -9453,7 +9638,7 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@2.3.1: {} + es-module-lexer@2.3.2: {} es-object-atoms@1.1.2: dependencies: @@ -9612,11 +9797,11 @@ snapshots: events@3.3.0: {} - eventsource-parser@3.1.0: {} + eventsource-parser@3.1.1: {} eventsource@3.0.7: dependencies: - eventsource-parser: 3.1.0 + eventsource-parser: 3.1.1 execa@5.1.1: dependencies: @@ -9653,7 +9838,7 @@ snapshots: dependencies: debug: 4.4.3(supports-color@7.2.0) express: 5.2.1(supports-color@7.2.0) - ip-address: 10.4.0 + ip-address: 10.5.0 transitivePeerDependencies: - supports-color @@ -9692,24 +9877,24 @@ snapshots: extend@3.0.2: {} - fallow-type-aware@3.14.0: + fallow-type-aware@3.17.0: dependencies: typescript: 7.0.2 optional: true - fallow@3.14.0: + fallow@3.17.0: dependencies: detect-libc: 2.1.2 optionalDependencies: - '@fallow-cli/darwin-arm64': 3.14.0 - '@fallow-cli/darwin-x64': 3.14.0 - '@fallow-cli/linux-arm64-gnu': 3.14.0 - '@fallow-cli/linux-arm64-musl': 3.14.0 - '@fallow-cli/linux-x64-gnu': 3.14.0 - '@fallow-cli/linux-x64-musl': 3.14.0 - '@fallow-cli/win32-arm64-msvc': 3.14.0 - '@fallow-cli/win32-x64-msvc': 3.14.0 - fallow-type-aware: 3.14.0 + '@fallow-cli/darwin-arm64': 3.17.0 + '@fallow-cli/darwin-x64': 3.17.0 + '@fallow-cli/linux-arm64-gnu': 3.17.0 + '@fallow-cli/linux-arm64-musl': 3.17.0 + '@fallow-cli/linux-x64-gnu': 3.17.0 + '@fallow-cli/linux-x64-musl': 3.17.0 + '@fallow-cli/win32-arm64-msvc': 3.17.0 + '@fallow-cli/win32-x64-msvc': 3.17.0 + fallow-type-aware: 3.17.0 fast-deep-equal@3.1.3: {} @@ -9955,6 +10140,8 @@ snapshots: serialize-error: 7.0.1 optional: true + globals@17.11.0: {} + globals@17.9.0: {} globalthis@1.0.4: @@ -10110,7 +10297,7 @@ snapshots: dependencies: hermes-estree: 0.25.1 - hono@4.13.1: {} + hono@4.13.3: {} hosted-git-info@4.1.0: dependencies: @@ -10181,7 +10368,7 @@ snapshots: inline-style-parser@0.2.7: {} - input-otp@1.4.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + input-otp@1.5.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -10194,7 +10381,7 @@ snapshots: internmap@2.0.3: {} - ip-address@10.4.0: {} + ip-address@10.5.0: {} ipaddr.js@1.9.1: {} @@ -10399,6 +10586,8 @@ snapshots: jose@6.2.8: {} + jose@6.2.9: {} + js-tokens@4.0.0: {} js-yaml@4.3.1: @@ -10616,7 +10805,7 @@ snapshots: dependencies: react: 19.2.8 - lucide-react@1.31.0(react@19.2.8): + lucide-react@1.32.0(react@19.2.8): dependencies: react: 19.2.8 @@ -11085,7 +11274,7 @@ snapshots: ms@2.1.3: {} - mtp@https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz: + mtp@https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz: dependencies: yaml: 2.9.0 @@ -11183,14 +11372,14 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 - open@11.0.0: + open@11.0.1: dependencies: - default-browser: 5.5.0 + default-browser: 5.5.1 define-lazy-prop: 3.0.0 is-in-ssh: 1.0.0 is-inside-container: 1.0.0 - powershell-utils: 0.1.0 - wsl-utils: 0.3.1 + powershell-utils: 0.2.0 + wsl-utils: 1.0.0 open@8.4.2: dependencies: @@ -11349,6 +11538,8 @@ snapshots: powershell-utils@0.1.0: {} + powershell-utils@0.2.0: {} + prelude-ls@1.2.1: {} prettier@3.9.6: {} @@ -11474,7 +11665,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - react-resizable-panels@4.12.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + react-resizable-panels@4.12.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -11505,7 +11696,7 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 - recast@0.23.19: + recast@0.23.21: dependencies: ast-types: 0.16.1 esprima: 4.0.1 @@ -11894,7 +12085,7 @@ snapshots: setprototypeof@1.2.0: {} - shadcn@4.16.2(supports-color@7.2.0)(typescript@6.0.3): + shadcn@4.18.0(supports-color@7.2.0)(typescript@6.0.3): dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/parser': 7.29.8 @@ -11903,7 +12094,7 @@ snapshots: '@dotenvx/dotenvx': 1.75.1 '@modelcontextprotocol/sdk': 1.30.0(supports-color@7.2.0)(zod@3.25.76) '@types/validate-npm-package-name': 4.0.2 - browserslist: 4.28.7 + browserslist: 4.28.8 commander: 14.0.3 cosmiconfig: 9.0.2(typescript@6.0.3) dedent: 1.7.2 @@ -11914,12 +12105,13 @@ snapshots: fs-extra: 11.4.0 fuzzysort: 3.1.0 kleur: 4.1.5 - open: 11.0.0 + open: 11.0.1 ora: 8.2.0 postcss: 8.5.26 postcss-selector-parser: 7.1.5 prompts: 2.4.2 - recast: 0.23.19 + recast: 0.23.21 + socks: 2.8.9 stringify-object: 5.0.0 tailwind-merge: 3.6.0 ts-morph: 26.0.0 @@ -11940,14 +12132,14 @@ snapshots: shebang-regex@3.0.0: {} - shiki@4.4.2: + shiki@4.4.3: dependencies: - '@shikijs/core': 4.4.2 - '@shikijs/engine-javascript': 4.4.2 - '@shikijs/engine-oniguruma': 4.4.2 - '@shikijs/langs': 4.4.2 - '@shikijs/themes': 4.4.2 - '@shikijs/types': 4.4.2 + '@shikijs/core': 4.4.3 + '@shikijs/engine-javascript': 4.4.3 + '@shikijs/engine-oniguruma': 4.4.3 + '@shikijs/langs': 4.4.3 + '@shikijs/themes': 4.4.3 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 @@ -11991,8 +12183,15 @@ snapshots: sisteransi@1.0.5: {} + smart-buffer@4.2.0: {} + smob@1.6.2: {} + socks@2.8.9: + dependencies: + ip-address: 10.5.0 + smart-buffer: 4.2.0 + sonner@2.0.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: react: 19.2.8 @@ -12115,7 +12314,7 @@ snapshots: strip-ansi@7.2.0: dependencies: - ansi-regex: 6.2.2 + ansi-regex: 6.3.0 strip-bom@3.0.0: {} @@ -12260,7 +12459,7 @@ snapshots: type-is@2.1.0: dependencies: - content-type: 2.0.0 + content-type: 2.1.0 media-typer: 1.1.1 mime-types: 3.0.2 @@ -12312,6 +12511,17 @@ snapshots: transitivePeerDependencies: - supports-color + typescript-eslint@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + typescript@6.0.3: {} typescript@7.0.2: @@ -12429,9 +12639,9 @@ snapshots: upath@1.2.0: {} - update-browserslist-db@1.3.0(browserslist@4.28.7): + update-browserslist-db@1.3.1(browserslist@4.28.8): dependencies: - browserslist: 4.28.7 + browserslist: 4.28.8 escalade: 3.2.0 picocolors: 1.1.1 @@ -12538,16 +12748,16 @@ snapshots: terser: 5.50.0 yaml: 2.9.0 - vitest@4.1.10(@types/node@26.2.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)): + vitest@4.1.11(@types/node@26.2.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)): dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.3.1 + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 expect-type: 1.4.0 magic-string: 0.30.21 obug: 2.1.4 @@ -12766,7 +12976,7 @@ snapshots: wrappy@1.0.2: {} - wsl-utils@0.3.1: + wsl-utils@1.0.0: dependencies: is-wsl: 3.1.1 powershell-utils: 0.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ee3c57a..f713a2d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,4 +7,4 @@ allowBuilds: esbuild: true overrides: "@methanium/ui": "https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz" - mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz" + mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz" diff --git a/utils/scripts/build-packages.ts b/utils/scripts/build-packages.ts index 9518c49..1f1f4fd 100644 --- a/utils/scripts/build-packages.ts +++ b/utils/scripts/build-packages.ts @@ -15,7 +15,14 @@ function getPackageDirs(dir: string): string[] { for (const entry of entries) { const fullPath = join(dir, entry); - if (!statSync(fullPath).isDirectory()) continue; + if (entry === "node_modules" || entry.startsWith(".")) continue; + let stats; + try { + stats = statSync(fullPath); + } catch { + continue; + } + if (!stats.isDirectory()) continue; if (existsSync(join(fullPath, "package.json"))) { dirs.push(fullPath); diff --git a/utils/scripts/lint-packages.ts b/utils/scripts/lint-packages.ts index 0aea8c3..c84e0e0 100644 --- a/utils/scripts/lint-packages.ts +++ b/utils/scripts/lint-packages.ts @@ -16,7 +16,14 @@ function getPackageDirs(dir: string): string[] { for (const entry of entries) { const fullPath = join(dir, entry); - if (!statSync(fullPath).isDirectory()) continue; + if (entry === "node_modules" || entry.startsWith(".")) continue; + let stats; + try { + stats = statSync(fullPath); + } catch { + continue; + } + if (!stats.isDirectory()) continue; if (existsSync(join(fullPath, "package.json"))) { dirs.push(fullPath); diff --git a/utils/scripts/update-packages.ts b/utils/scripts/update-packages.ts index 22b258f..90b3b0c 100644 --- a/utils/scripts/update-packages.ts +++ b/utils/scripts/update-packages.ts @@ -15,7 +15,14 @@ function getPackageDirs(dir: string): string[] { for (const entry of entries) { const fullPath = join(dir, entry); - if (!statSync(fullPath).isDirectory()) continue; + if (entry === "node_modules" || entry.startsWith(".")) continue; + let stats; + try { + stats = statSync(fullPath); + } catch { + continue; + } + if (!stats.isDirectory()) continue; if (existsSync(join(fullPath, "package.json"))) { dirs.push(fullPath); From 97270abed80469c2d26419996076c937a2744f85 Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 20 Aug 2026 20:56:30 +0200 Subject: [PATCH 06/19] Update workflows --- .forgejo/workflows/dependency-builds.yml | 4 ---- .forgejo/workflows/deploy-dev.yml | 14 -------------- .forgejo/workflows/deploy-prod.yml | 14 -------------- 3 files changed, 32 deletions(-) diff --git a/.forgejo/workflows/dependency-builds.yml b/.forgejo/workflows/dependency-builds.yml index 709c75b..153ea73 100644 --- a/.forgejo/workflows/dependency-builds.yml +++ b/.forgejo/workflows/dependency-builds.yml @@ -4,7 +4,6 @@ on: pull_request: env: - NIX_CONFIG: experimental-features = nix-command flakes FORGEJO_TOKEN: "" GITHUB_TOKEN: "" @@ -14,7 +13,6 @@ jobs: name: Build web runs-on: nixos steps: - - run: nix profile add nixpkgs#nodejs_24 - uses: https://data.forgejo.org/actions/checkout@v4 with: persist-credentials: false @@ -28,7 +26,6 @@ jobs: name: Build desktop runs-on: nixos steps: - - run: nix profile add nixpkgs#nodejs_24 - uses: https://data.forgejo.org/actions/checkout@v4 with: persist-credentials: false @@ -42,7 +39,6 @@ jobs: name: Build mobile runs-on: nixos steps: - - run: nix profile add nixpkgs#nodejs_24 - uses: https://data.forgejo.org/actions/checkout@v4 with: persist-credentials: false diff --git a/.forgejo/workflows/deploy-dev.yml b/.forgejo/workflows/deploy-dev.yml index 0d5be50..27252b2 100644 --- a/.forgejo/workflows/deploy-dev.yml +++ b/.forgejo/workflows/deploy-dev.yml @@ -12,9 +12,6 @@ jobs: build-web: runs-on: nixos steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -39,9 +36,6 @@ jobs: build-mobile: runs-on: nixos steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -62,8 +56,6 @@ jobs: KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }} run: | - nix profile add nixpkgs#gnused - set -euo pipefail if [ -z "$KEYSTORE_BASE64" ]; then @@ -128,9 +120,6 @@ jobs: matrix: target: [linux] steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -181,9 +170,6 @@ jobs: runs-on: nixos needs: [build-web, build-mobile, build-desktop] steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 with: diff --git a/.forgejo/workflows/deploy-prod.yml b/.forgejo/workflows/deploy-prod.yml index d85ce27..0bb5887 100644 --- a/.forgejo/workflows/deploy-prod.yml +++ b/.forgejo/workflows/deploy-prod.yml @@ -13,9 +13,6 @@ jobs: build-web: runs-on: nixos steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -40,9 +37,6 @@ jobs: build-mobile: runs-on: nixos steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -63,8 +57,6 @@ jobs: KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }} run: | - nix profile add nixpkgs#gnused - set -euo pipefail if [ -z "$KEYSTORE_BASE64" ]; then @@ -129,9 +121,6 @@ jobs: matrix: target: [linux] steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -180,9 +169,6 @@ jobs: runs-on: nixos needs: [build-web, build-mobile, build-desktop] steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 From 88a094c8bfdc9a99381f64bff83bb90825f7623b Mon Sep 17 00:00:00 2001 From: Alois Date: Tue, 25 Aug 2026 20:46:14 +0200 Subject: [PATCH 07/19] feat(pwa): remove update toast --- apps/electron/build/icons/128x128.png | Bin 14041 -> 0 bytes apps/electron/build/icons/128x128@2x.png | Bin 22479 -> 0 bytes apps/electron/build/icons/32x32.png | Bin 2320 -> 0 bytes apps/electron/build/icons/64x64.png | Bin 5757 -> 0 bytes apps/electron/build/icons/icon.icns | Bin 257363 -> 0 bytes apps/electron/build/icons/icon.ico | Bin 37998 -> 0 bytes apps/electron/build/icons/icon.png | Bin 41711 -> 0 bytes apps/pwa/src/runtime.tsx | 8 - packages/markdown/package.json | 30 + packages/markdown/src/emoji.tsx | 50 ++ packages/markdown/src/emojiData.ts | 94 +++ packages/markdown/src/input.tsx | 850 +++++++++++++++++++++++ packages/markdown/src/markdown.tsx | 815 ++++++++++++++++++++++ packages/markdown/src/text.tsx | 30 + packages/markdown/todo.md | 1 + packages/markdown/tsconfig.json | 12 + pnpm-lock.yaml | 63 ++ 17 files changed, 1945 insertions(+), 8 deletions(-) delete mode 100644 apps/electron/build/icons/128x128.png delete mode 100644 apps/electron/build/icons/128x128@2x.png delete mode 100644 apps/electron/build/icons/32x32.png delete mode 100644 apps/electron/build/icons/64x64.png delete mode 100644 apps/electron/build/icons/icon.icns delete mode 100644 apps/electron/build/icons/icon.ico delete mode 100644 apps/electron/build/icons/icon.png create mode 100644 packages/markdown/package.json create mode 100644 packages/markdown/src/emoji.tsx create mode 100644 packages/markdown/src/emojiData.ts create mode 100644 packages/markdown/src/input.tsx create mode 100644 packages/markdown/src/markdown.tsx create mode 100644 packages/markdown/src/text.tsx create mode 100644 packages/markdown/todo.md create mode 100644 packages/markdown/tsconfig.json diff --git a/apps/electron/build/icons/128x128.png b/apps/electron/build/icons/128x128.png deleted file mode 100644 index 357e1c2a4faa748fde46c79bbddab4bae04d05d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14041 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ⅆ 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 diff --git a/apps/electron/build/icons/icon.icns b/apps/electron/build/icons/icon.icns deleted file mode 100644 index a7f0a1e940bbdd0f4c8b453dc3ff837603c4dc08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 257363 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

l6`H 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ⅆ&9z8LqNY;8W0LUaDGtXnOMj)@3Og%$ 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%0AwIQn~ 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 diff --git a/apps/electron/build/icons/icon.ico b/apps/electron/build/icons/icon.ico deleted file mode 100644 index c5c68f820df524e5a23ecdd2a569d8874bbedf65..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37998 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 CvgwuP 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 P0|SGntDnm{r-UW|U4bl% diff --git a/apps/pwa/src/runtime.tsx b/apps/pwa/src/runtime.tsx index 21c2556..5d78973 100644 --- a/apps/pwa/src/runtime.tsx +++ b/apps/pwa/src/runtime.tsx @@ -94,15 +94,7 @@ export default function PwaRuntime() { if (worker.state !== "installed") return; if (!navigator.serviceWorker.controller) { toast.success("Tensamin is ready for offline startup"); - return; } - toast("A Tensamin update is ready", { - duration: Infinity, - action: { - label: "Update", - onClick: () => worker.postMessage({ type: "SKIP_WAITING" }), - }, - }); }); }; if (registration.installing) watchWorker(registration.installing); diff --git a/packages/markdown/package.json b/packages/markdown/package.json new file mode 100644 index 0000000..247d715 --- /dev/null +++ b/packages/markdown/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tensamin/markdown", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + "./text": "./src/text.tsx", + "./input": "./src/input.tsx", + "./emoji": "./src/emoji.tsx" + }, + "scripts": { + "format": "pnpm exec prettier --write .", + "lint": "eslint src", + "build": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@codemirror/autocomplete": "^6.20.3", + "@codemirror/commands": "^6.10.4", + "@codemirror/lang-markdown": "^6.5.2", + "@codemirror/language": "^6.12.4", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.8", + "@methanium/ui": "*", + "@twemoji/api": "^17.0.3", + "emojibase-data": "^17.0.0", + "lucide-react": "^1.30.0", + "react": "^19.2.8", + "react-dom": "^19.2.8" + } +} diff --git a/packages/markdown/src/emoji.tsx b/packages/markdown/src/emoji.tsx new file mode 100644 index 0000000..c0eb397 --- /dev/null +++ b/packages/markdown/src/emoji.tsx @@ -0,0 +1,50 @@ +import twemoji from "@twemoji/api"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@methanium/ui"; +import { resolveEmoji } from "./emojiData"; + +export { + emojis, + findEmojiShortcodes, + normalizeShortcode, + resolveEmoji, + searchEmojis, +} from "./emojiData"; +export type { EmojiDefinition } from "./emojiData"; + +export function getEmojiUrl(shortcode: string): string | undefined { + const emoji = resolveEmoji(shortcode); + return emoji ? `${twemoji.base}svg/${emoji.hexcode}.svg` : undefined; +} + +export default function Emoji({ + className = "h-6 w-6", + shortcode, + tooltip = true, +}: { + className?: string; + shortcode: string; + tooltip?: boolean; +}) { + const emoji = resolveEmoji(shortcode); + if (!emoji) return {shortcode}; + + const image = ( + {emoji.shortcode} + ); + + if (!tooltip) return image; + + return ( + + + {emoji.shortcode} + + ); +} diff --git a/packages/markdown/src/emojiData.ts b/packages/markdown/src/emojiData.ts new file mode 100644 index 0000000..158b5b0 --- /dev/null +++ b/packages/markdown/src/emojiData.ts @@ -0,0 +1,94 @@ +import shortcodeData from "emojibase-data/en/shortcodes/joypixels.json"; + +export type EmojiDefinition = { + aliases: readonly string[]; + hexcode: string; + name: string; + shortcode: string; +}; + +function normalizeName(value: string) { + return value + .trim() + .replace(/^:+|:+$/g, "") + .toLowerCase(); +} + +export const emojis: readonly EmojiDefinition[] = Object.entries( + shortcodeData as Record, +).map(([hexcode, value]) => { + const aliases = Array.isArray(value) ? value : [value]; + const name = aliases[0]; + + return { + aliases, + hexcode: hexcode.toLowerCase().replaceAll("_", "-"), + name, + shortcode: `:${name}:`, + }; +}); + +const emojiByName = new Map(); +for (const emoji of emojis) { + for (const alias of emoji.aliases) { + emojiByName.set(normalizeName(alias), emoji); + } +} + +export function resolveEmoji(value: string): EmojiDefinition | undefined { + return emojiByName.get(normalizeName(value)); +} + +export function normalizeShortcode(value: string): string | undefined { + return resolveEmoji(value)?.shortcode; +} + +export function findEmojiShortcodes(value: string) { + const matches: Array<{ + emoji: EmojiDefinition; + from: number; + to: number; + }> = []; + let searchFrom = 0; + + while (searchFrom < value.length) { + const from = value.indexOf(":", searchFrom); + if (from === -1) break; + + const candidate = value.slice(from).match(/^:([a-z0-9_+-]+):/i); + if (!candidate) { + searchFrom = from + 1; + continue; + } + + const emoji = resolveEmoji(candidate[1]); + if (!emoji) { + // The closing colon may also open the next valid shortcode. + searchFrom = from + candidate[0].length - 1; + continue; + } + + const to = from + candidate[0].length; + matches.push({ emoji, from, to }); + searchFrom = to; + } + + return matches; +} + +export function searchEmojis(query: string): EmojiDefinition[] { + const normalizedQuery = normalizeName(query); + if (!normalizedQuery) return [...emojis]; + + return emojis + .map((emoji) => { + const names = emoji.aliases.map(normalizeName); + const exact = names.includes(normalizedQuery); + const prefix = names.some((name) => name.startsWith(normalizedQuery)); + const contains = names.some((name) => name.includes(normalizedQuery)); + return { emoji, rank: exact ? 0 : prefix ? 1 : contains ? 2 : 3 }; + }) + .filter(({ rank }) => rank < 3) + .sort((a, b) => a.rank - b.rank || a.emoji.name.localeCompare(b.emoji.name)) + .map(({ emoji }) => emoji); +} diff --git a/packages/markdown/src/input.tsx b/packages/markdown/src/input.tsx new file mode 100644 index 0000000..e16c84d --- /dev/null +++ b/packages/markdown/src/input.tsx @@ -0,0 +1,850 @@ +import { markdown } from "@codemirror/lang-markdown"; +import { syntaxTree } from "@codemirror/language"; +import { + acceptCompletion, + autocompletion, + completionStatus, + pickedCompletion, + startCompletion, + type Completion, + type CompletionContext, + type CompletionResult, +} from "@codemirror/autocomplete"; +import { + EditorState, + EditorSelection, + Annotation, + Compartment, + Prec, + Transaction, + type Extension, + type Range, + type SelectionRange, +} from "@codemirror/state"; +import { + Decoration, + EditorView, + keymap, + placeholder, + ViewPlugin, + WidgetType, + type DecorationSet, + type KeyBinding, + type ViewUpdate, +} from "@codemirror/view"; +import { + defaultKeymap, + history, + historyKeymap, + indentWithTab, +} from "@codemirror/commands"; +import { useEffect, useRef } from "react"; +import type { CSSProperties } from "react"; +import { createRoot, type Root } from "react-dom/client"; + +import { collectInlineRanges, ensureMarkdownStyles } from "./markdown"; +import Emoji, { + findEmojiShortcodes, + getEmojiUrl, + resolveEmoji, + searchEmojis, +} from "./emoji"; + +export const MAX_RENDERED_EMOJI_OPTIONS = 100; + +export type InputController = { + focus: () => void; + hasFocus: () => boolean; + insertText: (text: string) => void; +}; + +export type InputProps = { + ref?: HTMLDivElement; + placeholder?: string; + value: string; + setValue: (value: string) => void; + onSubmit?: () => void; + invertEnterBehavior?: boolean; + styled?: boolean; + fontSize?: CSSProperties["fontSize"]; + paddingX?: CSSProperties["padding"]; + paddingY?: CSSProperties["padding"]; + className?: string; + emojiFrequencies?: Readonly>; + onEmojiSelect?: (shortcode: string) => void; + autoFocus?: boolean; + onControllerChange?: (controller: InputController | null) => void; +}; + +function toCssLength(value: CSSProperties["padding"]): string | undefined { + if (value === undefined) { + return undefined; + } + + return typeof value === "number" ? `${value}px` : value; +} + +function toCssPadding( + vertical: CSSProperties["padding"], + horizontal: CSSProperties["padding"], + styled: boolean, +): string { + const defaultVertical = styled ? "0.25rem" : "0"; + const defaultHorizontal = styled ? "0.625rem" : "0"; + + return `${toCssLength(vertical) ?? defaultVertical} ${toCssLength(horizontal) ?? defaultHorizontal}`; +} + +const hiddenTokenDecoration = Decoration.mark({ class: "tm-md-hidden-token" }); +const strongDecoration = Decoration.mark({ class: "tm-md-strong" }); +const emDecoration = Decoration.mark({ class: "tm-md-em" }); +const delDecoration = Decoration.mark({ class: "tm-md-del" }); +const codeDecoration = Decoration.mark({ class: "tm-md-code" }); +const linkDecoration = Decoration.mark({ class: "tm-md-link" }); +const codeLineDecoration = Decoration.line({ class: "tm-md-code-line" }); +const externalValueSync = Annotation.define(); +const widgetRoots = new WeakMap(); + +type EmojiRange = { + from: number; + shortcode: string; + to: number; + url: string; +}; + +class EmojiWidget extends WidgetType { + readonly shortcode: string; + readonly url: string; + + constructor(shortcode: string, url: string) { + super(); + this.shortcode = shortcode; + this.url = url; + } + + eq(other: EmojiWidget) { + return other.shortcode === this.shortcode && other.url === this.url; + } + + toDOM() { + const container = document.createElement("span"); + const root = createRoot(container); + root.render( + , + ); + widgetRoots.set(container, root); + return container; + } + + destroy(dom: HTMLElement) { + widgetRoots.get(dom)?.unmount(); + widgetRoots.delete(dom); + } + + ignoreEvent() { + return true; + } +} + +function codeRanges(state: EditorState) { + const ranges: Array<{ from: number; to: number }> = []; + + syntaxTree(state).iterate({ + enter(node) { + if ( + node.name === "InlineCode" || + node.name === "FencedCode" || + node.name === "CodeBlock" + ) { + ranges.push({ from: node.from, to: node.to }); + return false; + } + }, + }); + + return ranges; +} + +export function findEmojiRanges(state: EditorState): EmojiRange[] { + const document = state.doc.toString(); + const excluded = codeRanges(state); + const ranges: EmojiRange[] = []; + + for (const match of findEmojiShortcodes(document)) { + const { from, to } = match; + const inCode = excluded.some((range) => from < range.to && to > range.from); + const emoji = inCode ? undefined : match.emoji; + const url = emoji ? getEmojiUrl(emoji.shortcode) : undefined; + + if (emoji && url) { + ranges.push({ from, shortcode: emoji.shortcode, to, url }); + } + } + + return ranges; +} + +class EmojiPluginValue { + decorations: DecorationSet; + ranges: EmojiRange[]; + + constructor(view: EditorView) { + this.ranges = findEmojiRanges(view.state); + this.decorations = this.buildDecorations(); + } + + update(update: ViewUpdate) { + if ( + update.docChanged || + syntaxTree(update.startState) !== syntaxTree(update.state) + ) { + this.ranges = findEmojiRanges(update.state); + this.decorations = this.buildDecorations(); + } + } + + private buildDecorations() { + return Decoration.set( + this.ranges.map((range) => + Decoration.replace({ + inclusive: false, + widget: new EmojiWidget(range.shortcode, range.url), + }).range(range.from, range.to), + ), + true, + ); + } +} + +const emojiDecorations = ViewPlugin.fromClass(EmojiPluginValue, { + decorations: (instance) => instance.decorations, + provide: (plugin) => + EditorView.atomicRanges.of( + (view) => view.plugin(plugin)?.decorations ?? Decoration.none, + ), +}); + +function deleteEmoji(view: EditorView, direction: "backward" | "forward") { + const ranges = view.plugin(emojiDecorations)?.ranges ?? []; + const deletions: Array<{ from: number; to: number }> = []; + + for (const selection of view.state.selection.ranges) { + if (selection.empty) { + const emoji = ranges.find((range) => + direction === "backward" + ? selection.from > range.from && selection.from <= range.to + : selection.from >= range.from && selection.from < range.to, + ); + if (emoji) deletions.push({ from: emoji.from, to: emoji.to }); + continue; + } + + let from = selection.from; + let to = selection.to; + let changed = false; + + for (const emoji of ranges) { + if (from < emoji.to && to > emoji.from) { + from = Math.min(from, emoji.from); + to = Math.max(to, emoji.to); + changed = true; + } + } + + if (changed) deletions.push({ from, to }); + } + + if (deletions.length === 0) return false; + + const merged = deletions + .sort((a, b) => a.from - b.from) + .reduce>((result, deletion) => { + const previous = result.at(-1); + if (previous && deletion.from <= previous.to) { + previous.to = Math.max(previous.to, deletion.to); + } else { + result.push({ ...deletion }); + } + return result; + }, []); + + view.dispatch({ + changes: merged.map((range) => ({ from: range.from, to: range.to })), + selection: EditorSelection.cursor(merged[0].from), + scrollIntoView: true, + userEvent: direction === "backward" ? "delete.backward" : "delete.forward", + }); + return true; +} + +/** + * Builds markdown styling decorations every time the document or cursor selection changes. + * Token delimiters are hidden unless the cursor is currently intersecting that token range. + */ +const markdownDecorations = ViewPlugin.fromClass( + class { + decorations: DecorationSet; + + constructor(view: EditorView) { + this.decorations = buildDecorations(view); + } + + update(update: ViewUpdate) { + if (update.docChanged || update.selectionSet || update.viewportChanged) { + this.decorations = buildDecorations(update.view); + } + } + }, + { + decorations: (instance: { decorations: DecorationSet }) => + instance.decorations, + }, +); + +/** + * Executes Input. + * @param props Parameter props. + * @returns unknown. + */ +export default function Input(props: InputProps) { + ensureMarkdownStyles(); + + const shellClassName = props.styled + ? "min-h-8 w-full min-w-0 rounded-lg border border-input bg-transparent text-base transition-colors outline-none placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40" + : ""; + + const elementRef = useRef(null); + const viewRef = useRef(undefined); + const setValueRef = useRef(props.setValue); + const onSubmitRef = useRef(props.onSubmit); + const onEmojiSelectRef = useRef( + props.onEmojiSelect, + ); + const invertEnterBehaviorRef = useRef(Boolean(props.invertEnterBehavior)); + const completionCompartmentRef = useRef(null); + completionCompartmentRef.current ??= new Compartment(); + const completionCompartment = completionCompartmentRef.current; + + useEffect(() => { + setValueRef.current = props.setValue; + onSubmitRef.current = props.onSubmit; + onEmojiSelectRef.current = props.onEmojiSelect; + invertEnterBehaviorRef.current = Boolean(props.invertEnterBehavior); + }, [ + props.onEmojiSelect, + props.onSubmit, + props.invertEnterBehavior, + props.setValue, + ]); + + useEffect(() => { + if (!elementRef.current) return; + + const state = EditorState.create({ + doc: props.value, + extensions: createEditorExtensions( + (value) => { + setValueRef.current(value); + }, + () => props.placeholder, + () => invertEnterBehaviorRef.current, + () => onSubmitRef.current?.(), + completionCompartment, + props.emojiFrequencies, + (shortcode) => onEmojiSelectRef.current?.(shortcode), + ), + }); + + viewRef.current = new EditorView({ + state, + parent: elementRef.current, + }); + props.onControllerChange?.({ + focus: () => viewRef.current?.contentDOM.focus({ preventScroll: true }), + hasFocus: () => viewRef.current?.hasFocus ?? false, + insertText: (text) => { + const editor = viewRef.current; + if (!editor) return; + editor.dispatch({ + ...editor.state.replaceSelection(text), + annotations: Transaction.userEvent.of("input.type"), + scrollIntoView: true, + }); + }, + }); + if (props.autoFocus) viewRef.current.focus(); + + return () => { + props.onControllerChange?.(null); + viewRef.current?.destroy(); + viewRef.current = undefined; + }; + // Run once to initialize/destroy the editor instance. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const editor = viewRef.current; + if (!editor) return; + + const next = props.value; + const current = editor.state.doc.toString(); + + if (next === current) return; + + editor.dispatch({ + changes: { + from: 0, + to: current.length, + insert: next, + }, + annotations: [ + externalValueSync.of(true), + Transaction.addToHistory.of(false), + ], + filter: false, + }); + }, [props.value]); + + useEffect(() => { + const editor = viewRef.current; + const compartment = completionCompartmentRef.current; + if (!editor || !compartment) return; + + const wasActive = completionStatus(editor.state) === "active"; + editor.dispatch({ + effects: compartment.reconfigure( + createEmojiAutocomplete(props.emojiFrequencies, (shortcode) => + onEmojiSelectRef.current?.(shortcode), + ), + ), + }); + if (wasActive) startCompletion(editor); + }, [props.emojiFrequencies]); + + return ( +

+ ); +} + +/** + * Executes createEditorExtensions. + * @param onChange Parameter onChange. + * @param getPlaceholder Parameter getPlaceholder. + * @param getInvertEnterBehavior Parameter getInvertEnterBehavior. + * @param onSubmit Parameter onSubmit. + * @returns Extension[]. + */ +function createEditorExtensions( + onChange: (value: string) => void, + getPlaceholder: () => string | undefined, + getInvertEnterBehavior: () => boolean, + onSubmit: () => void, + completionCompartment: Compartment, + emojiFrequencies: Readonly> | undefined, + onEmojiSelect: (shortcode: string) => void, +): Extension[] { + const editorKeymap = [ + ...defaultKeymap, + ...historyKeymap, + indentWithTab, + ] as unknown as readonly KeyBinding[]; + + const customEnterKeymap = keymap.of([ + { + key: "Shift-Enter", + run: (view) => { + if (completionStatus(view.state) === "active") { + return acceptCompletion(view); + } + if (!getInvertEnterBehavior()) { + return false; + } + + onSubmit(); + return true; + }, + }, + { + key: "Enter", + run: (view) => { + if (completionStatus(view.state) === "active") { + return acceptCompletion(view); + } + if (getInvertEnterBehavior()) { + return false; + } + + onSubmit(); + return true; + }, + }, + ]); + const completionTabKeymap = keymap.of([ + { + key: "Tab", + run: (view) => + completionStatus(view.state) === "active" + ? acceptCompletion(view) + : false, + }, + ]); + const emojiDeletionKeymap = keymap.of([ + { + key: "Backspace", + run: (view) => deleteEmoji(view, "backward"), + }, + { + key: "Delete", + run: (view) => deleteEmoji(view, "forward"), + }, + ]); + + return [ + history(), + markdown(), + completionCompartment.of( + createEmojiAutocomplete(emojiFrequencies, onEmojiSelect), + ), + emojiDecorations, + keymap.of(editorKeymap), + Prec.highest(completionTabKeymap), + Prec.highest(emojiDeletionKeymap), + Prec.highest(customEnterKeymap), + EditorView.lineWrapping, + placeholder(getPlaceholder() ?? ""), + EditorView.updateListener.of((update: ViewUpdate) => { + if (!update.docChanged) return; + if ( + update.transactions.some( + (transaction) => transaction.annotation(externalValueSync) === true, + ) + ) { + return; + } + onChange(update.state.doc.toString()); + }), + EditorView.theme({ + "&": { + fontSize: "inherit", + }, + "&.cm-editor": { + width: "100%", + }, + }), + EditorView.editorAttributes.of({ + class: "tm-md-editor", + spellcheck: "true", + "aria-label": "Markdown input", + }), + markdownDecorations, + ]; +} + +function createEmojiAutocomplete( + frequencies: Readonly> | undefined, + onEmojiSelect: (shortcode: string) => void, +) { + return autocompletion({ + activateOnTyping: true, + addToOptions: [ + { + position: 20, + render(completion) { + const container = document.createElement("span"); + createRoot(container).render( + , + ); + return container; + }, + }, + ], + maxRenderedOptions: MAX_RENDERED_EMOJI_OPTIONS, + override: [createEmojiCompletionSource(frequencies, onEmojiSelect)], + }); +} + +function normalizedFrequencies( + frequencies: Readonly> | undefined, +) { + const normalized = new Map(); + for (const [value, frequency] of Object.entries(frequencies ?? {})) { + const shortcode = resolveEmoji(value)?.shortcode; + if (!shortcode || !Number.isFinite(frequency) || frequency <= 0) continue; + normalized.set(shortcode, (normalized.get(shortcode) ?? 0) + frequency); + } + return normalized; +} + +export function createEmojiCompletionSource( + frequencies?: Readonly>, + onEmojiSelect: (shortcode: string) => void = () => undefined, +) { + const normalized = normalizedFrequencies(frequencies); + const maxFrequency = Math.max(0, ...normalized.values()); + + return (context: CompletionContext): CompletionResult | null => { + const token = context.matchBefore(/:[a-z0-9_+-]*$/i); + if (!token) return null; + if ( + codeRanges(context.state).some( + (range) => token.from < range.to && token.to > range.from, + ) + ) { + return null; + } + + const characterBefore = context.state.sliceDoc( + Math.max(0, token.from - 1), + token.from, + ); + if (characterBefore && /[a-z0-9_]/i.test(characterBefore)) return null; + + const query = token.text.slice(1).toLowerCase(); + const options: Completion[] = searchEmojis(query) + .map((emoji) => { + const aliases = emoji.aliases.map((alias) => alias.toLowerCase()); + const matchedAlias = + aliases.find((alias) => alias === query) ?? + aliases.find((alias) => alias.startsWith(query)) ?? + aliases.find((alias) => alias.includes(query)) ?? + emoji.name; + const relevance = !query + ? 0 + : matchedAlias === query + ? 80 + : matchedAlias.startsWith(query) + ? 40 + : 0; + const frequency = normalized.get(emoji.shortcode) ?? 0; + const usage = + maxFrequency > 0 + ? (15 * Math.log1p(frequency)) / Math.log1p(maxFrequency) + : 0; + + return { + apply(view, completion, from, to) { + view.dispatch({ + annotations: pickedCompletion.of(completion), + changes: { from, insert: `${emoji.shortcode} `, to }, + selection: EditorSelection.cursor( + from + emoji.shortcode.length + 1, + ), + }); + onEmojiSelect(emoji.shortcode); + }, + boost: relevance + usage, + displayLabel: emoji.shortcode, + label: `:${matchedAlias}:`, + type: "text", + frequency, + relevance, + } satisfies Completion & { frequency: number; relevance: number }; + }) + .sort( + (a, b) => + b.relevance - a.relevance || + b.frequency - a.frequency || + (a.displayLabel ?? a.label).localeCompare(b.displayLabel ?? b.label), + ); + + return { + from: token.from, + options, + validFor: /^:[a-z0-9_+-]*$/i, + }; + }; +} + +export const emojiCompletionSource = createEmojiCompletionSource(); + +/** + * Executes buildDecorations. + * @param view Parameter view. + * @returns DecorationSet. + */ +function buildDecorations(view: EditorView): DecorationSet { + const builder: Range[] = []; + const selections = view.state.selection.ranges.map( + (range: SelectionRange) => ({ + from: range.from, + to: range.to, + }), + ); + + let codeFenceOpen = false; + + for ( + let lineNumber = 1; + lineNumber <= view.state.doc.lines; + lineNumber += 1 + ) { + const line = view.state.doc.line(lineNumber); + const text = line.text; + const lineFrom = line.from; + const trimmed = text.trim(); + + const fence = text.match(/^```\s*([^`]*)$/); + if (fence) { + const ticksStart = lineFrom + text.indexOf("```"); + const ticksEnd = ticksStart + 3; + addHiddenToken(builder, selections, { from: ticksStart, to: ticksEnd }); + if (trimmed.length > 3) { + addHiddenToken(builder, selections, { + from: ticksEnd, + to: line.to, + }); + } + codeFenceOpen = !codeFenceOpen; + continue; + } + + if (codeFenceOpen) { + builder.push(codeLineDecoration.range(lineFrom)); + continue; + } + + const heading = text.match(/^(#{1,6})\s+/); + if (heading) { + const markerLength = heading[0].length; + addHiddenToken(builder, selections, { + from: lineFrom, + to: lineFrom + markerLength, + }); + + const level = heading[1].length; + const headingClass = Decoration.mark({ + class: `tm-md-heading tm-md-h${String(level)}`, + }); + const contentFrom = lineFrom + markerLength; + if (contentFrom < line.to) { + builder.push(headingClass.range(contentFrom, line.to)); + } + } + + const quote = text.match(/^>\s?/); + if (quote) { + addHiddenToken(builder, selections, { + from: lineFrom, + to: lineFrom + quote[0].length, + }); + } + + const unordered = text.match(/^(\s*)([-+*])\s+(?:\[( |x|X)\]\s+)?/); + if (unordered) { + const markerStart = lineFrom + unordered[1].length; + const markerEnd = markerStart + unordered[2].length + 1; + addHiddenToken(builder, selections, { from: markerStart, to: markerEnd }); + + const checkbox = unordered[0].match(/\[( |x|X)\]\s+$/); + if (checkbox) { + const checkboxStart = lineFrom + unordered[0].lastIndexOf("["); + addHiddenToken(builder, selections, { + from: checkboxStart, + to: checkboxStart + checkbox[0].length, + }); + } + } + + const ordered = text.match(/^(\s*)(\d+\.)\s+/); + if (ordered) { + const markerStart = lineFrom + ordered[1].length; + addHiddenToken(builder, selections, { + from: markerStart, + to: markerStart + ordered[2].length + 1, + }); + } + + if (/^(?:\*\s*){3,}$|^(?:-\s*){3,}$|^(?:_\s*){3,}$/.test(trimmed)) { + if (lineFrom < line.to) { + builder.push( + Decoration.mark({ class: "tm-md-hr" }).range(lineFrom, line.to), + ); + } + continue; + } + + const tableSeparator = /^\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?$/.test( + text, + ); + if (tableSeparator) { + if (lineFrom < line.to) { + builder.push( + Decoration.mark({ class: "tm-md-del" }).range(lineFrom, line.to), + ); + } + continue; + } + + const { styleRanges, tokenRanges } = collectInlineRanges(text, lineFrom); + + for (const range of styleRanges) { + if (range.from >= range.to) continue; + + if (range.className === "tm-md-strong") { + builder.push(strongDecoration.range(range.from, range.to)); + } else if (range.className === "tm-md-em") { + builder.push(emDecoration.range(range.from, range.to)); + } else if (range.className === "tm-md-del") { + builder.push(delDecoration.range(range.from, range.to)); + } else if (range.className === "tm-md-code") { + builder.push(codeDecoration.range(range.from, range.to)); + } else if (range.className === "tm-md-link") { + builder.push(linkDecoration.range(range.from, range.to)); + } + } + + for (const token of tokenRanges) { + addHiddenToken(builder, selections, token); + } + } + + return Decoration.set(builder, true); +} + +/** + * Keeps markdown syntax visible only when user selection intersects the token. + * This preserves cursor predictability and cross-token selection while still hiding syntax during reading. + */ +function addHiddenToken( + builder: Range[], + selections: ReadonlyArray<{ from: number; to: number }>, + token: { + from: number; + to: number; + }, +): void { + if (token.from >= token.to) return; + + const overlapsSelection = selections.some((selection) => { + const selectionFrom = Math.min(selection.from, selection.to); + const selectionTo = Math.max(selection.from, selection.to); + + if (selectionFrom === selectionTo) { + return selectionFrom >= token.from && selectionFrom <= token.to; + } + + return selectionFrom < token.to && selectionTo > token.from; + }); + + if (overlapsSelection) return; + builder.push(hiddenTokenDecoration.range(token.from, token.to)); +} diff --git a/packages/markdown/src/markdown.tsx b/packages/markdown/src/markdown.tsx new file mode 100644 index 0000000..cd2a210 --- /dev/null +++ b/packages/markdown/src/markdown.tsx @@ -0,0 +1,815 @@ +import { + Fragment, + useEffect, + useRef, + useState, + type ReactElement, + type ReactNode, +} from "react"; +import Emoji from "./emoji"; +import { findEmojiShortcodes } from "./emojiData"; +import { Check } from "lucide-react"; + +type InlineNode = + | { type: "text"; value: string } + | { type: "emoji"; shortcode: string } + | { type: "strong"; value: string } + | { type: "em"; value: string } + | { type: "del"; value: string } + | { type: "code"; value: string } + | { type: "link"; label: string; href: string } + | { type: "image"; alt: string; src: string }; + +type InlineDecorationRange = { + from: number; + to: number; + className: string; +}; + +type InlineTokenRange = { + from: number; + to: number; +}; + +type ListItem = { + text: string; + checked: boolean | null; +}; + +type TableBlock = { + type: "table"; + headers: string[]; + rows: string[][]; +}; + +type MarkdownBlock = + | { + type: "paragraph"; + text: string; + } + | { + type: "heading"; + level: number; + text: string; + } + | { + type: "hr"; + } + | { + type: "blockquote"; + text: string; + } + | { + type: "code"; + language: string; + code: string; + } + | { + type: "list"; + ordered: boolean; + items: ListItem[]; + } + | TableBlock; + +const INLINE_TOKEN_REGEX = + /!\[([^\]]*)\]\(([^)\s]+(?:\s+"[^"]*")?)\)|\[([^\]]+)\]\(([^)\s]+(?:\s+"[^"]*")?)\)|`([^`\n]+)`|~~([^~\n]+)~~|\*\*([^*\n]+)\*\*|__([^_\n]+)__|\*([^*\n]+)\*|(? + + + ); +} + +function CopyableCode({ + block = false, + language, + value, +}: { + block?: boolean; + language?: string; + value: string; +}) { + const [copied, setCopied] = useState(false); + const copiedTimer = useRef | undefined>( + undefined, + ); + + useEffect( + () => () => { + clearTimeout(copiedTimer.current); + }, + [], + ); + + async function copy() { + await navigator.clipboard.writeText(value); + setCopied(true); + clearTimeout(copiedTimer.current); + copiedTimer.current = setTimeout(() => setCopied(false), 1200); + } + + const code = ( + void copy()} + onKeyDown={(event) => { + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + void copy(); + }} + > + {value} + + ); + + if (block) { + return ( +
+
{code}
+ +
+ ); + } + + return ( + <> + {code} + + + ); +} + +/** + * Executes parseInlineNodes. + * @param input Parameter input. + * @returns InlineNode[]. + */ +export function parseInlineNodes(input: string): InlineNode[] { + const nodes: InlineNode[] = []; + + let cursor = 0; + let match = INLINE_TOKEN_REGEX.exec(input); + + while (match) { + const index = match.index; + const raw = match[0]; + + if (index > cursor) { + nodes.push(...parseEmojiText(input.slice(cursor, index))); + } + + if (match[1] !== undefined && match[2] !== undefined) { + nodes.push({ type: "image", alt: match[1], src: normalizeUrl(match[2]) }); + } else if (match[3] !== undefined && match[4] !== undefined) { + nodes.push({ + type: "link", + label: match[3], + href: normalizeUrl(match[4]), + }); + } else if (match[5] !== undefined) { + nodes.push({ type: "code", value: match[5] }); + } else if (match[6] !== undefined) { + nodes.push({ type: "del", value: match[6] }); + } else if (match[7] !== undefined || match[8] !== undefined) { + nodes.push({ type: "strong", value: match[7] ?? match[8] ?? "" }); + } else if (match[9] !== undefined || match[10] !== undefined) { + nodes.push({ type: "em", value: match[9] ?? match[10] ?? "" }); + } else { + nodes.push(...parseEmojiText(raw)); + } + + cursor = index + raw.length; + match = INLINE_TOKEN_REGEX.exec(input); + } + + if (cursor < input.length) { + nodes.push(...parseEmojiText(input.slice(cursor))); + } + + INLINE_TOKEN_REGEX.lastIndex = 0; + return nodes; +} + +export function parseEmojiText(input: string): InlineNode[] { + const nodes: InlineNode[] = []; + let cursor = 0; + + for (const match of findEmojiShortcodes(input)) { + if (match.from > cursor) { + nodes.push({ type: "text", value: input.slice(cursor, match.from) }); + } + + nodes.push({ type: "emoji", shortcode: match.emoji.shortcode }); + cursor = match.to; + } + + if (cursor < input.length) { + nodes.push({ type: "text", value: input.slice(cursor) }); + } + + return nodes; +} + +/** + * Executes collectInlineRanges. + * @param input Parameter input. + * @param offset Parameter offset. + * @returns { + styleRanges: InlineDecorationRange[]; + tokenRanges: InlineTokenRange[]; +}. + */ +export function collectInlineRanges( + input: string, + offset = 0, +): { + styleRanges: InlineDecorationRange[]; + tokenRanges: InlineTokenRange[]; +} { + const styleRanges: InlineDecorationRange[] = []; + const tokenRanges: InlineTokenRange[] = []; + + let match = INLINE_TOKEN_REGEX.exec(input); + + while (match) { + const raw = match[0]; + const start = offset + match.index; + const end = start + raw.length; + + if (match[1] !== undefined && match[2] !== undefined) { + const openLength = 2; + const closeLength = raw.endsWith(")") ? 1 : 0; + + const imageEnd = start + openLength + match[1].length; + tokenRanges.push({ from: start, to: start + openLength }); + tokenRanges.push({ from: imageEnd, to: imageEnd + 1 }); + + const srcStart = imageEnd + 1; + const srcEnd = end - closeLength; + tokenRanges.push({ from: srcStart, to: srcStart + 1 }); + tokenRanges.push({ from: srcEnd, to: srcEnd + closeLength }); + } else if (match[3] !== undefined && match[4] !== undefined) { + const label = match[3]; + const labelStart = start + 1; + const labelEnd = labelStart + label.length; + + tokenRanges.push({ from: start, to: start + 1 }); + tokenRanges.push({ from: labelEnd, to: labelEnd + 1 }); + tokenRanges.push({ from: labelEnd + 1, to: labelEnd + 2 }); + tokenRanges.push({ from: end - 1, to: end }); + + styleRanges.push({ + from: labelStart, + to: labelEnd, + className: "tm-md-link", + }); + } else if (match[5] !== undefined) { + const codeStart = start + 1; + const codeEnd = end - 1; + + tokenRanges.push({ from: start, to: start + 1 }); + tokenRanges.push({ from: end - 1, to: end }); + styleRanges.push({ + from: codeStart, + to: codeEnd, + className: "tm-md-code", + }); + } else if (match[6] !== undefined) { + const contentStart = start + 2; + const contentEnd = end - 2; + + tokenRanges.push({ from: start, to: start + 2 }); + tokenRanges.push({ from: end - 2, to: end }); + styleRanges.push({ + from: contentStart, + to: contentEnd, + className: "tm-md-del", + }); + } else if (match[7] !== undefined || match[8] !== undefined) { + const contentStart = start + 2; + const contentEnd = end - 2; + + tokenRanges.push({ from: start, to: start + 2 }); + tokenRanges.push({ from: end - 2, to: end }); + styleRanges.push({ + from: contentStart, + to: contentEnd, + className: "tm-md-strong", + }); + } else if (match[9] !== undefined || match[10] !== undefined) { + const contentStart = start + 1; + const contentEnd = end - 1; + + tokenRanges.push({ from: start, to: start + 1 }); + tokenRanges.push({ from: end - 1, to: end }); + styleRanges.push({ + from: contentStart, + to: contentEnd, + className: "tm-md-em", + }); + } + + match = INLINE_TOKEN_REGEX.exec(input); + } + + INLINE_TOKEN_REGEX.lastIndex = 0; + return { styleRanges, tokenRanges }; +} + +/** + * Executes parseMarkdownBlocks. + * @param markdown Parameter markdown. + * @returns MarkdownBlock[]. + */ +export function parseMarkdownBlocks(markdown: string): MarkdownBlock[] { + const lines = markdown.replace(/\r\n/g, "\n").split("\n"); + const blocks: MarkdownBlock[] = []; + + let index = 0; + + while (index < lines.length) { + const line = lines[index]; + + if (!line.trim()) { + index += 1; + continue; + } + + const codeFence = line.match(/^```\s*([^`]*)$/); + if (codeFence) { + const language = (codeFence[1] ?? "").trim(); + const codeLines: string[] = []; + index += 1; + + while (index < lines.length && !/^```\s*$/.test(lines[index])) { + codeLines.push(lines[index]); + index += 1; + } + + if (index < lines.length) { + index += 1; + } + + blocks.push({ type: "code", language, code: codeLines.join("\n") }); + continue; + } + + if (/^(?:\*\s*){3,}$|^(?:-\s*){3,}$|^(?:_\s*){3,}$/.test(line.trim())) { + blocks.push({ type: "hr" }); + index += 1; + continue; + } + + const heading = line.match(/^(#{1,6})\s+(.+)$/); + if (heading) { + blocks.push({ + type: "heading", + level: heading[1].length, + text: heading[2], + }); + index += 1; + continue; + } + + const quote = line.match(/^>\s?(.*)$/); + if (quote) { + const quoteLines: string[] = [quote[1]]; + index += 1; + + while (index < lines.length) { + const next = lines[index].match(/^>\s?(.*)$/); + if (!next) break; + quoteLines.push(next[1]); + index += 1; + } + + blocks.push({ type: "blockquote", text: quoteLines.join("\n") }); + continue; + } + + const tableCandidate = readTable(lines, index); + if (tableCandidate) { + blocks.push(tableCandidate.block); + index = tableCandidate.nextIndex; + continue; + } + + const unordered = line.match(/^\s*[-*+]\s+(.*)$/); + const ordered = line.match(/^\s*\d+\.\s+(.*)$/); + if (unordered || ordered) { + const orderedList = Boolean(ordered); + const items: ListItem[] = []; + + while (index < lines.length) { + const current = lines[index]; + const match = orderedList + ? current.match(/^\s*\d+\.\s+(.*)$/) + : current.match(/^\s*[-*+]\s+(.*)$/); + + if (!match) break; + + const task = match[1].match(/^\[( |x|X)\]\s+(.*)$/); + if (task) { + items.push({ + text: task[2], + checked: task[1].toLowerCase() === "x", + }); + } else { + items.push({ text: match[1], checked: null }); + } + + index += 1; + } + + blocks.push({ type: "list", ordered: orderedList, items }); + continue; + } + + const paragraphLines = [line]; + index += 1; + + while ( + index < lines.length && + lines[index].trim() && + !/^(#{1,6})\s+/.test(lines[index]) && + !/^```\s*/.test(lines[index]) && + !/^>\s?/.test(lines[index]) && + !/^\s*[-*+]\s+/.test(lines[index]) && + !/^\s*\d+\.\s+/.test(lines[index]) && + !/^(?:\*\s*){3,}$|^(?:-\s*){3,}$|^(?:_\s*){3,}$/.test(lines[index].trim()) + ) { + paragraphLines.push(lines[index]); + index += 1; + } + + blocks.push({ type: "paragraph", text: paragraphLines.join("\n") }); + } + + return blocks; +} + +/** + * Executes renderInline. + * @param nodes Parameter nodes. + * @returns React.ReactNode[]. + */ +function renderInline(nodes: InlineNode[]): ReactNode[] { + return nodes.map((node, index) => { + if (node.type === "text") { + return node.value; + } + + if (node.type === "emoji") { + return ( + + ); + } + + if (node.type === "strong") { + return ( + + {renderInline(parseEmojiText(node.value))} + + ); + } + + if (node.type === "em") { + return ( + + {renderInline(parseEmojiText(node.value))} + + ); + } + + if (node.type === "del") { + return ( + + {renderInline(parseEmojiText(node.value))} + + ); + } + + if (node.type === "code") { + return ; + } + + if (node.type === "link") { + return ( +
+ {renderInline(parseEmojiText(node.label))} + + ); + } + + return ( + {node.alt} + ); + }); +} + +/** + * Executes renderBlocks. + * @param blocks Parameter blocks. + * @returns React.ReactElement. + */ +export function renderBlocks(blocks: MarkdownBlock[]): ReactElement { + return ( + <> + {blocks.map((block, blockIndex) => { + if (block.type === "heading") { + const className = `tm-md-heading tm-md-h${String(block.level)}`; + if (block.level === 1) + return ( +

+ {renderInline(parseInlineNodes(block.text))} +

+ ); + if (block.level === 2) + return ( +

+ {renderInline(parseInlineNodes(block.text))} +

+ ); + if (block.level === 3) + return ( +

+ {renderInline(parseInlineNodes(block.text))} +

+ ); + if (block.level === 4) + return ( +

+ {renderInline(parseInlineNodes(block.text))} +

+ ); + if (block.level === 5) + return ( +
+ {renderInline(parseInlineNodes(block.text))} +
+ ); + return ( +
+ {renderInline(parseInlineNodes(block.text))} +
+ ); + } + + if (block.type === "blockquote") { + return ( +
+ {block.text.split("\n").map((line, lineIndex) => ( +

{renderInline(parseInlineNodes(line))}

+ ))} +
+ ); + } + + if (block.type === "code") { + return ( + + ); + } + + if (block.type === "list") { + const Tag = block.ordered ? "ol" : "ul"; + return ( + + {block.items.map((item, itemIndex) => ( +
  • + {item.checked !== null ? ( + + ) : null} + {renderInline(parseInlineNodes(item.text))} +
  • + ))} +
    + ); + } + + if (block.type === "table") { + return ( +
    + + + + {block.headers.map((header, headerIndex) => ( + + ))} + + + + {block.rows.map((row, rowIndex) => ( + + {row.map((cell, cellIndex) => ( + + ))} + + ))} + +
    + {renderInline(parseInlineNodes(header))} +
    + {renderInline(parseInlineNodes(cell))} +
    +
    + ); + } + + if (block.type === "hr") { + return
    ; + } + + return ( +

    + {block.text.split("\n").map((line, lineIndex) => ( + + {lineIndex > 0 ?
    : null} + {renderInline(parseInlineNodes(line))} +
    + ))} +

    + ); + })} + + ); +} + +/** + * Executes normalizeUrl. + * @param input Parameter input. + * @returns string. + */ +function normalizeUrl(input: string): string { + const value = input.trim(); + if (/^(https?:|mailto:|tel:|\/)/i.test(value)) { + return value; + } + + return "#"; +} + +/** + * Executes splitTableRow. + * @param row Parameter row. + * @returns string[]. + */ +function splitTableRow(row: string): string[] { + const cleaned = row.trim().replace(/^\|/, "").replace(/\|$/, ""); + return cleaned.split("|").map((cell) => cell.trim()); +} + +/** + * Executes readTable. + * @param lines Parameter lines. + * @param index Parameter index. + * @returns { block: TableBlock; nextIndex: number } | null. + */ +function readTable( + lines: string[], + index: number, +): { block: TableBlock; nextIndex: number } | null { + const header = lines[index] ?? ""; + const separator = lines[index + 1] ?? ""; + + if (!header.includes("|") || !separator.includes("|")) { + return null; + } + + const separatorCells = splitTableRow(separator); + const isSeparator = separatorCells.every((cell) => /^:?-{3,}:?$/.test(cell)); + if (!isSeparator) { + return null; + } + + const headers = splitTableRow(header); + const rows: string[][] = []; + let cursor = index + 2; + + while (cursor < lines.length && lines[cursor].includes("|")) { + rows.push(splitTableRow(lines[cursor])); + cursor += 1; + } + + return { + block: { type: "table", headers, rows }, + nextIndex: cursor, + }; +} + +const markdownStyles = ` +.tm-md-root { color: var(--foreground); line-height: 1.65; font-size: 1rem; } +.tm-md-heading { margin: 0.2rem 0 0.35rem; font-weight: 700; line-height: 1.25; } +.tm-md-h1 { font-size: 1.65rem; } +.tm-md-h2 { font-size: 1.45rem; } +.tm-md-h3 { font-size: 1.25rem; } +.tm-md-h4 { font-size: 1.1rem; } +.tm-md-h5 { font-size: 1rem; } +.tm-md-h6 { font-size: 0.95rem; opacity: 0.9; } +.tm-md-blockquote { margin: 0.45rem 0; padding-left: 0.75rem; opacity: 0.95; } +.tm-md-blockquote p { margin: 0.2rem 0; } +.tm-md-pre { margin: 0.45rem 0; padding: 0.65rem 0.75rem; border-radius: 0.5rem; background: var(--muted); overflow-x: auto; } +.tm-md-code, .tm-md-codeblock { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.87em; cursor: pointer; } +.tm-md-code { padding: 0.08rem 0.32rem; border: 1px solid var(--border); border-radius: 0.28rem; background: var(--muted); } +.tm-md-codeblock { display: block; } +.tm-md-code:focus-visible, .tm-md-codeblock:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; } +.tm-md-strong { font-weight: 700; } +.tm-md-em { font-style: italic; } +.tm-md-del { text-decoration: line-through; } +.tm-md-link { color: var(--primary); text-decoration: underline; text-underline-offset: 0.14rem; } +.tm-md-image { display: block; max-width: 100%; border-radius: 0.4rem; margin: 0.5rem 0; } +.tm-md-emoji { display: inline-block; width: 1.15em; height: 1.15em; vertical-align: -0.18em; } +.tm-md-ul, .tm-md-ol { margin: 0.3rem 0 0.35rem 1.2rem; padding: 0; } +.tm-md-li { margin: 0.2rem 0; } +.tm-md-checkbox { margin-right: 0.5rem; vertical-align: middle; } +.tm-md-table-wrap { overflow-x: auto; margin: 0.45rem 0; } +.tm-md-table { border-collapse: collapse; width: 100%; min-width: 16rem; } +.tm-md-table th, .tm-md-table td { padding: 0.4rem 0.5rem; text-align: left; } +.tm-md-table th { background: var(--muted); font-weight: 600; } +.tm-md-hr { margin: 0.55rem 0; } + +.cm-editor.tm-md-editor { border-radius: inherit; background: transparent; caret-color: var(--foreground); } +.cm-editor.tm-md-editor.cm-focused { outline: none; box-shadow: none; } +.cm-editor.tm-md-editor .cm-scroller { font-family: inherit; line-height: 1.55; max-height: 30vh; overflow-y: auto; overflow-x: hidden; } +.cm-editor.tm-md-editor .cm-content { caret-color: var(--foreground); } +.cm-editor.tm-md-editor .cm-content { padding: var(--tm-md-content-padding, 0.25rem 0.625rem); min-height: 2rem; } +.cm-editor.tm-md-editor .cm-line { padding: 0; color: var(--foreground); } +.cm-editor.tm-md-editor .tm-md-editor-emoji { display: inline-block; width: 1.15em; height: 1.15em; vertical-align: -0.18em; object-fit: contain; pointer-events: none; } +.cm-editor.tm-md-editor .tm-md-hidden-token { color: transparent; opacity: 0; font-size: inherit; } +.cm-editor.tm-md-editor .tm-md-code-line { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; background: var(--muted); border-radius: 0.3rem; } +.cm-tooltip.cm-tooltip-autocomplete { min-width: 18rem; max-width: min(26rem, calc(100vw - 1rem)); overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius); background: var(--popover); color: var(--popover-foreground); box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); font-family: "Public Sans Variable", sans-serif; font-size: 0.875rem; } +.cm-editor.tm-md-editor .cm-tooltip.cm-tooltip-autocomplete > ul { max-height: min(20rem, 45vh); padding: 0.25rem; font-family: "Public Sans Variable", sans-serif; scrollbar-width: thin; scrollbar-color: var(--border) transparent; } +.cm-tooltip.cm-tooltip-autocomplete > ul::-webkit-scrollbar { width: 6px; height: 6px; } +.cm-tooltip.cm-tooltip-autocomplete > ul::-webkit-scrollbar-track { background: transparent; } +.cm-tooltip.cm-tooltip-autocomplete > ul::-webkit-scrollbar-thumb { border-radius: 9999px; background: var(--border); } +.cm-tooltip.cm-tooltip-autocomplete > ul > li { display: flex; min-height: 2.25rem; align-items: center; border-radius: calc(var(--radius) * 0.8); padding: 0.3rem 0.5rem; color: var(--popover-foreground); } +.cm-tooltip.cm-tooltip-autocomplete > ul > li:hover, +.cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected] { background: var(--accent); color: var(--accent-foreground); } +.cm-tooltip.cm-tooltip-autocomplete .cm-completionIcon { display: none; } +.cm-tooltip.cm-tooltip-autocomplete .cm-completionLabel { overflow: hidden; text-overflow: ellipsis; } +.cm-tooltip.cm-tooltip-autocomplete .cm-completionMatchedText { color: inherit; text-decoration: none; font-weight: 600; } +.cm-tooltip-autocomplete .tm-md-completion-emoji { display: inline-block; width: 1.35rem; height: 1.35rem; flex: 0 0 auto; margin-right: 0.5rem; vertical-align: middle; } +`; + +/** + * Executes ensureMarkdownStyles. + * @param none This function has no parameters. + * @returns void. + */ +export function ensureMarkdownStyles(): void { + if (typeof document === "undefined") return; + + const styleId = "tensamin-markdown-styles"; + let style = document.getElementById(styleId) as HTMLStyleElement | null; + + if (!style) { + style = document.createElement("style"); + style.id = styleId; + document.head.appendChild(style); + } + + if (style.textContent !== markdownStyles) { + style.textContent = markdownStyles; + } +} diff --git a/packages/markdown/src/text.tsx b/packages/markdown/src/text.tsx new file mode 100644 index 0000000..ca65fe0 --- /dev/null +++ b/packages/markdown/src/text.tsx @@ -0,0 +1,30 @@ +import { useMemo, type CSSProperties } from "react"; + +import { + ensureMarkdownStyles, + parseMarkdownBlocks, + renderBlocks, +} from "./markdown"; + +export type TextProps = { + value: string; + fontSize?: CSSProperties["fontSize"]; +}; + +/** + * Executes Text. + * @param props Parameter props. + * @returns unknown. + */ +export default function Text(props: TextProps) { + ensureMarkdownStyles(); + + const blocks = useMemo(() => parseMarkdownBlocks(props.value), [props.value]); + const renderedBlocks = useMemo(() => renderBlocks(blocks), [blocks]); + + return ( +
    + {renderedBlocks} +
    + ); +} diff --git a/packages/markdown/todo.md b/packages/markdown/todo.md new file mode 100644 index 0000000..b59da98 --- /dev/null +++ b/packages/markdown/todo.md @@ -0,0 +1 @@ +- Improve the Input box diff --git a/packages/markdown/tsconfig.json b/packages/markdown/tsconfig.json new file mode 100644 index 0000000..9714625 --- /dev/null +++ b/packages/markdown/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cbdd6e..107149b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -459,6 +459,45 @@ importers: specifier: ^8.2.1 version: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) + packages/markdown: + dependencies: + '@codemirror/autocomplete': + specifier: ^6.20.3 + version: 6.20.3 + '@codemirror/commands': + specifier: ^6.10.4 + version: 6.11.0 + '@codemirror/lang-markdown': + specifier: ^6.5.2 + version: 6.5.2 + '@codemirror/language': + specifier: ^6.12.4 + version: 6.12.4 + '@codemirror/state': + specifier: ^6.7.1 + version: 6.7.1 + '@codemirror/view': + specifier: ^6.43.8 + version: 6.43.9 + '@methanium/ui': + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + '@twemoji/api': + specifier: ^17.0.3 + version: 17.0.3 + emojibase-data: + specifier: ^17.0.0 + version: 17.0.0(emojibase@17.0.0) + lucide-react: + specifier: ^1.30.0 + version: 1.32.0(react@19.2.8) + react: + specifier: ^19.2.8 + version: 19.2.8 + react-dom: + specifier: ^19.2.8 + version: 19.2.8(react@19.2.8) + packages/mtp: dependencies: '@methanium/ui': @@ -2540,6 +2579,12 @@ packages: '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} + '@twemoji/api@17.0.3': + resolution: {integrity: sha512-iwERjxY0QgPGVwT6b1OKG0Oa9nIfHhJw+Ij1TapTBMKTvVCU6qdXPXX/XKwxKx5QZIJW5GwELUCtw8wlaIQ2ug==} + + '@twemoji/parser@17.0.2': + resolution: {integrity: sha512-X/P7pHsGOxnrupQYUVetIeuxBGgffFu8CLwoPMMjH9CWmQvlXiCpbTW/BXxMOCWXQojgHdmgdvm6IsCqAQ5nxA==} + '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} @@ -4528,6 +4573,9 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@5.0.0: + resolution: {integrity: sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==} + jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} @@ -8431,6 +8479,15 @@ snapshots: minimatch: 10.2.6 path-browserify: 1.0.1 + '@twemoji/api@17.0.3': + dependencies: + '@twemoji/parser': 17.0.2 + fs-extra: 8.1.0 + jsonfile: 5.0.0 + universalify: 0.1.2 + + '@twemoji/parser@17.0.2': {} + '@types/cacheable-request@6.0.3': dependencies: '@types/http-cache-semantics': 4.2.0 @@ -10619,6 +10676,12 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonfile@5.0.0: + dependencies: + universalify: 0.1.2 + optionalDependencies: + graceful-fs: 4.2.11 + jsonfile@6.2.1: dependencies: universalify: 2.0.1 From ede0199858d505be64126c6f03c681c0f71127f0 Mon Sep 17 00:00:00 2001 From: Alois Date: Tue, 25 Aug 2026 20:48:56 +0200 Subject: [PATCH 08/19] chore(mtp): udpate type maps --- mtp-type-maps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mtp-type-maps b/mtp-type-maps index a297dcc..6d97b5b 160000 --- a/mtp-type-maps +++ b/mtp-type-maps @@ -1 +1 @@ -Subproject commit a297dcce60bc6e84696c6a16f5fd510beb2ca643 +Subproject commit 6d97b5b9935b15d957444d38609378c66a24262a From 5056f084b41b74ffec84303d548a725a677edb67 Mon Sep 17 00:00:00 2001 From: Alois Date: Tue, 25 Aug 2026 20:58:32 +0200 Subject: [PATCH 09/19] chore(mtp): update type-maps --- mtp-type-maps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mtp-type-maps b/mtp-type-maps index 6d97b5b..e044182 160000 --- a/mtp-type-maps +++ b/mtp-type-maps @@ -1 +1 @@ -Subproject commit 6d97b5b9935b15d957444d38609378c66a24262a +Subproject commit e044182b895da4f220d266275eebeb0f264b1dbe From edcf0e15dd07a26d9e9635c5db77227baf4eb005 Mon Sep 17 00:00:00 2001 From: Alois Date: Tue, 25 Aug 2026 21:00:32 +0200 Subject: [PATCH 10/19] chore(mtp): update type-maps --- mtp-type-maps | 2 +- package.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mtp-type-maps b/mtp-type-maps index e044182..a70085b 160000 --- a/mtp-type-maps +++ b/mtp-type-maps @@ -1 +1 @@ -Subproject commit e044182b895da4f220d266275eebeb0f264b1dbe +Subproject commit a70085b0c41844745449c9913a361f5bb9bee529 diff --git a/package.json b/package.json index 051f699..1975028 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "start-adb:mobile": "cd apps/tauri && pnpm run start-adb:mobile", "dev:desktop": "cd apps/electron && pnpm run dev", "build:desktop": "cd apps/electron && pnpm run package", - "delete:mobile": "nix develop .#tauri --command node apps/tauri/scripts/delete-mobile.ts" + "delete:mobile": "nix develop .#tauri --command node apps/tauri/scripts/delete-mobile.ts", + "update-submodules": "git submodule update --remote --force --recursive" }, "devDependencies": { "@eslint/js": "^10.0.1", From 96b81d30b1408d3a94f1cf191eedbdc705f192b4 Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 27 Aug 2026 14:38:53 +0200 Subject: [PATCH 11/19] feat(pwa): keep service workers from getting registerd outside the pwa --- apps/pwa/src/runtime.tsx | 66 ++++++---------------------------------- apps/pwa/src/vite.ts | 1 + 2 files changed, 10 insertions(+), 57 deletions(-) diff --git a/apps/pwa/src/runtime.tsx b/apps/pwa/src/runtime.tsx index d7917cc..a1947eb 100644 --- a/apps/pwa/src/runtime.tsx +++ b/apps/pwa/src/runtime.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { toast } from "sonner"; import { setDatabaseEntry } from "@tensamin/shared/indexedDb"; @@ -6,11 +6,6 @@ import { isTauri } from "@tauri-apps/api/core"; import "./style.css"; -type BeforeInstallPromptEvent = Event & { - prompt: () => Promise; - userChoice: Promise<{ outcome: "accepted" | "dismissed" }>; -}; - const launchedFiles: File[] = []; const fileListeners = new Set<(file: File) => void>(); @@ -27,9 +22,10 @@ export function subscribeTuFileLaunch(listener: (file: File) => void) { }; } -function isStandalone() { +function isInstalledPwa() { return ( window.matchMedia("(display-mode: standalone)").matches || + window.matchMedia("(display-mode: window-controls-overlay)").matches || (navigator as Navigator & { standalone?: boolean }).standalone === true ); } @@ -65,13 +61,9 @@ async function enablePush() { await setDatabaseEntry("keys", "push-subscription", subscription.toJSON()); } -export default function PwaRuntime() { - const [installPrompt, setInstallPrompt] = - useState(null); - +function InstalledPwaRuntime() { useEffect(() => { if ( - isTauri() || !("serviceWorker" in navigator) || !["http:", "https:"].includes(window.location.protocol) ) { @@ -129,16 +121,6 @@ export default function PwaRuntime() { }; }, []); - useEffect(() => { - const handleInstallPrompt = (event: Event) => { - event.preventDefault(); - setInstallPrompt(event as BeforeInstallPromptEvent); - }; - window.addEventListener("beforeinstallprompt", handleInstallPrompt); - return () => - window.removeEventListener("beforeinstallprompt", handleInstallPrompt); - }, []); - useEffect(() => { const launchQueue = ( window as Window & { @@ -159,41 +141,7 @@ export default function PwaRuntime() { }, []); useEffect(() => { - if (!installPrompt) return; - toast("Install Tensamin for a native app experience", { - duration: Infinity, - action: { - label: "Install", - onClick: () => { - void installPrompt.prompt().then(() => installPrompt.userChoice); - setInstallPrompt(null); - }, - }, - }); - }, [installPrompt]); - - useEffect(() => { - const isAppleMobile = /iPad|iPhone|iPod/.test(navigator.userAgent); if ( - isTauri() || - !isAppleMobile || - isStandalone() || - localStorage.getItem("pwa-ios-install-hint") - ) { - return; - } - localStorage.setItem("pwa-ios-install-hint", "shown"); - toast( - "Install Tensamin from Safari's Share menu to enable background notifications.", - { - duration: 12_000, - }, - ); - }, []); - - useEffect(() => { - if ( - !isStandalone() || !("Notification" in window) || Notification.permission !== "default" || localStorage.getItem("pwa-push-hint") @@ -222,7 +170,6 @@ export default function PwaRuntime() { useEffect(() => { if ( - isStandalone() && "Notification" in window && Notification.permission === "granted" && import.meta.env.VITE_WEB_PUSH_PUBLIC_KEY @@ -235,3 +182,8 @@ export default function PwaRuntime() { return null; } + +export default function PwaRuntime() { + if (isTauri() || !isInstalledPwa()) return null; + return ; +} diff --git a/apps/pwa/src/vite.ts b/apps/pwa/src/vite.ts index 908a3d2..962dce1 100644 --- a/apps/pwa/src/vite.ts +++ b/apps/pwa/src/vite.ts @@ -128,6 +128,7 @@ export function tensaminPwa(): Plugin[] { filename: "serviceWorker.ts", injectRegister: null, registerType: "prompt", + buildBase: "/", manifestFilename: "manifest.json", includeAssets: ["favicon.ico", "icons/*.png"], manifest: { From 0fbedf6733710d08b5cedc28bd3e4043012198b0 Mon Sep 17 00:00:00 2001 From: forgejo-actions Date: Thu, 27 Aug 2026 12:59:08 +0000 Subject: [PATCH 12/19] (qol): update release flake hash --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index d91a817..e02a001 100644 --- a/flake.nix +++ b/flake.nix @@ -17,7 +17,7 @@ systems = [ "x86_64-linux" ]; forAllSystems = nixpkgs.lib.genAttrs systems; version = "0.0.11"; - x86_64DebHash = "sha256-p/6boNv9uWTHsPTVa3EhGzr41EwRzCY/fJWlj854DVA="; + x86_64DebHash = "sha256-iMEgdjY8Ghlx6K/YzKC5JAZbeHNJDIFHm8MKmM4efaI="; forgejoBaseUrl = "https://git.methanium.net/tensamin/client/releases/download/${version}"; in { From 1d1b304bcf8e056de20360d08ff5be028e947778 Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 27 Aug 2026 19:32:48 +0200 Subject: [PATCH 13/19] feat(workflows): remove env thing --- .forgejo/workflows/dependency-builds.yml | 1 - .forgejo/workflows/deploy-dev.yml | 3 --- .forgejo/workflows/deploy-prod.yml | 3 --- 3 files changed, 7 deletions(-) diff --git a/.forgejo/workflows/dependency-builds.yml b/.forgejo/workflows/dependency-builds.yml index 709c75b..f8ec1cd 100644 --- a/.forgejo/workflows/dependency-builds.yml +++ b/.forgejo/workflows/dependency-builds.yml @@ -4,7 +4,6 @@ on: pull_request: env: - NIX_CONFIG: experimental-features = nix-command flakes FORGEJO_TOKEN: "" GITHUB_TOKEN: "" diff --git a/.forgejo/workflows/deploy-dev.yml b/.forgejo/workflows/deploy-dev.yml index 0d5be50..a09f69b 100644 --- a/.forgejo/workflows/deploy-dev.yml +++ b/.forgejo/workflows/deploy-dev.yml @@ -5,9 +5,6 @@ on: paths-ignore: - flake.nix -env: - NIX_CONFIG: experimental-features = nix-command flakes - jobs: build-web: runs-on: nixos diff --git a/.forgejo/workflows/deploy-prod.yml b/.forgejo/workflows/deploy-prod.yml index d85ce27..5fe2ba9 100644 --- a/.forgejo/workflows/deploy-prod.yml +++ b/.forgejo/workflows/deploy-prod.yml @@ -6,9 +6,6 @@ on: paths-ignore: - flake.nix -env: - NIX_CONFIG: experimental-features = nix-command flakes - jobs: build-web: runs-on: nixos From aa34bd962b6539289755f28e8cd7b938bd19f7e9 Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 27 Aug 2026 19:35:17 +0200 Subject: [PATCH 14/19] temp(mtp): temp commit stuff that needs to get a rework --- packages/mtp/package.json | 5 +- packages/mtp/src/context.test.tsx | 21 -- packages/mtp/src/context.tsx | 308 ++++++++++++------------------ pnpm-lock.yaml | 9 - 4 files changed, 124 insertions(+), 219 deletions(-) delete mode 100644 packages/mtp/src/context.test.tsx diff --git a/packages/mtp/package.json b/packages/mtp/package.json index 982593a..dfd973c 100644 --- a/packages/mtp/package.json +++ b/packages/mtp/package.json @@ -14,13 +14,10 @@ "dependencies": { "@methanium/ui": "*", "@tauri-apps/api": "^2.11.1", - "@tensamin/crypto": "workspace:*", "@tensamin/shared": "workspace:*", "@tensamin/storage": "workspace:*", "mtp": "*", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "zod": "^4.4.3" + "react": "^19.2.8" }, "devDependencies": { "eslint": "^10.8.0" diff --git a/packages/mtp/src/context.test.tsx b/packages/mtp/src/context.test.tsx deleted file mode 100644 index 7f50aba..0000000 --- a/packages/mtp/src/context.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { isPushType, validateResponse } from "./context"; - -describe("MTP protocol dispatch", () => { - it("preserves protocol errors for the request layer", () => { - const error = validateResponse("GetStates", { - id: 12, - type: "ErrorInternal", - data: { ErrorType: "temporary" }, - }); - expect(error.type).toBe("ErrorInternal"); - expect(error.id).toBe(12); - }); - - it("recognizes initial and live presence pushes", () => { - expect(isPushType("GetStates")).toBe(true); - expect(isPushType("ClientChanged")).toBe(true); - expect(isPushType("UnknownMessage")).toBe(false); - }); -}); diff --git a/packages/mtp/src/context.tsx b/packages/mtp/src/context.tsx index af06ee0..288acbf 100644 --- a/packages/mtp/src/context.tsx +++ b/packages/mtp/src/context.tsx @@ -10,9 +10,18 @@ import { } from "react"; import { invoke, isTauri } from "@tauri-apps/api/core"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import { MTPClient } from "mtp"; -import { type z } from "zod"; -import { ConnectionState } from "mtp"; +import { + base64ToBytes, + ConnectionState, + MTPClient, + MTPProxyConnection, + type MTPFrame, + type MTPKeyMaterialInput, + type MTPProxyAdapter, + type MTPRequestOptions, + type MTPRequestFunction, + type MTPResponseFrame, +} from "mtp"; import createAsyncQueue from "@tensamin/shared/asyncQueue"; import { toast as sonnerToast } from "@methanium/ui"; @@ -24,35 +33,15 @@ import { type MTP as Schemas, } from "@tensamin/shared/data"; import { log } from "@tensamin/shared/log"; -import { ProtocolError } from "@tensamin/shared/errors"; import { useStorage } from "@tensamin/storage/context"; import { RECONNECT_RESET, RECONNECT_TRIES, RETRY_INTERVAL } from "./values"; -function base64ToUint8Array(b64: string) { - const bin = atob(b64); - const out = new Uint8Array(bin.length); - - for (let i = 0; i < bin.length; i++) { - out[i] = bin.charCodeAt(i); - } - - return out; -} - export type ProtocolMessage< T extends keyof Schemas & string = keyof Schemas & string, -> = { - id?: number; - type: T | string; - data: z.infer; -}; +> = MTPResponseFrame; -export type BoundSendFn = ( - type: T, - data?: z.infer, - options?: { id?: number }, -) => Promise>; +export type BoundSendFn = MTPRequestFunction; export type PushHandler = (message: ProtocolMessage) => void | Promise; @@ -68,6 +57,15 @@ const PUSH_TYPES = [ "ErrorNoIota", ] as const; +function encodedPublicKey(value: string): MTPKeyMaterialInput { + const trimmed = value.trim(); + const hex = trimmed.replace(/^0x/i, ""); + return { + value: /^[0-9a-f]+$/i.test(hex) ? hex : trimmed, + encoding: /^[0-9a-f]+$/i.test(hex) ? "hex" : "base64", + }; +} + export function isPushType(type: string): boolean { return (PUSH_TYPES as readonly string[]).includes(type); } @@ -114,53 +112,6 @@ type ContextType = { const MTPContext = createContext(undefined); -function getProtocolErrorDetails(error: unknown) { - if (typeof error !== "object" || error === null || !("type" in error)) { - return null; - } - - const protocolError = error as { - id?: unknown; - type?: unknown; - data?: unknown; - }; - return { - id: protocolError.id, - type: protocolError.type, - data: protocolError.data, - }; -} - -// Zod schema validation -export function validateResponse( - type: T, - message: { id?: number; type: string; data: unknown }, -): ProtocolMessage { - if (message.type.startsWith("Error")) { - return message as ProtocolMessage; - } - - const schema = - schemas[message.type as keyof Schemas & string]?.response ?? - schemas[type]?.response; - if (!schema) { - return message as ProtocolMessage; - } - - const parsed = schema.safeParse(message.data); - if (!parsed.success) { - throw new Error( - `Response validation failed for ${type}: ${parsed.error.message}`, - ); - } - - return { - id: message.id, - type: message.type, - data: parsed.data, - } as ProtocolMessage; -} - function useMessageHandlers() { const interceptorsRef = useRef(new Set()); const pushHandlersRef = useRef(new Set()); @@ -202,9 +153,7 @@ function BrowserProvider(props: { const [freshContacts, setFreshContacts] = useState([]); const [freshCalls, setFreshCalls] = useState([]); - const clientRef = useRef> | null>( - null, - ); + const clientRef = useRef | null>(null); const { addInterceptor, interceptorsRef, @@ -221,7 +170,6 @@ function BrowserProvider(props: { load("omega_url").then(setMtpUrl); }, [load]); - // Validation override functions const send: BoundSendFn = useMemo( () => async (type, data, options) => { const client = clientRef.current; @@ -230,24 +178,8 @@ function BrowserProvider(props: { throw new Error("mtp is not connected"); } - const message = await client.request( - type, - (data ?? {}) as Record, - options, - ); - const response = validateResponse(type, message); + const response = await client.request(type, data, options); setFreshContacts((contacts) => removeMissingContacts(contacts, response)); - if (response.type.startsWith("Error")) { - const errorData = response.data as Record; - throw new ProtocolError({ - type: response.type, - requestId: response.id, - errorType: - typeof errorData.ErrorType === "string" - ? errorData.ErrorType - : undefined, - }); - } return response; }, [], @@ -259,9 +191,7 @@ function BrowserProvider(props: { return () => {}; } - return client.subscribe(type, (message) => { - handler(validateResponse(type, message)); - }); + return client.subscribe(type, handler); }, []); // Reconnect stuff @@ -324,7 +254,7 @@ function BrowserProvider(props: { if (disposed || props.blockConnection) return; const generation = ++connectionGeneration; - let client: Awaited> | null = null; + let client: MTPClient | null = null; let failed = false; const cleanup = () => { client?.disconnect(); @@ -398,15 +328,20 @@ function BrowserProvider(props: { log(2, "mtp", "green", "Connecting to: " + url); - client = await MTPClient.create({ + client = await MTPClient.create({ url, credentials: { clientId: userId, - keyring: base64ToUint8Array(keyring), + keyring: base64ToBytes(keyring), }, - hostPublicKey: omikronPublicKey, + hostPublicKey: encodedPublicKey(omikronPublicKey), descriptor: "client", pings: true, + schemas, + throwProtocolErrors: true, + onValidationError: (error) => { + log(1, "mtp", "red", "Failed to validate MTP message", error); + }, logger: (event) => { if (event.type === "state") { if (generation !== connectionGeneration) return; @@ -458,16 +393,7 @@ function BrowserProvider(props: { clientRef.current = activeClient; for (const type of PUSH_TYPES) { activeClient.subscribe(type, (message) => { - let validated: ProtocolMessage; - try { - validated = validateResponse(type, message); - } catch (error) { - log(1, "mtp", "red", "Failed to validate push message", error, { - type, - data: message.data, - }); - return; - } + const validated = message as ProtocolMessage; setFreshContacts((contacts) => removeMissingContacts(contacts, validated), @@ -515,11 +441,7 @@ function BrowserProvider(props: { "ClientStateSync", (message) => { cleanupStateSync(); - try { - resolve(validateResponse("ClientStateSync", message)); - } catch (error) { - reject(error); - } + resolve(message); }, ); unsubscribeNoIota = activeClient.subscribe("ErrorNoIota", () => { @@ -539,15 +461,10 @@ function BrowserProvider(props: { ); } - const acknowledgement = await activeClient.request("ClientStateAck", { + await activeClient.request("ClientStateAck", { SessionId: finalResponse.data.SessionId, VersionNumber: finalResponse.data.VersionNumber, }); - if (acknowledgement.type.startsWith("Error")) { - throw new Error( - `State acknowledgement failed: ${acknowledgement.type}`, - ); - } if (disposed || clientRef.current !== activeClient) return; @@ -573,7 +490,7 @@ function BrowserProvider(props: { "mtp", "red", `Connection/authentication attempt failed: ${connectErrorMessage}`, - getProtocolErrorDetails(connectError) ?? connectError, + connectError, ); scheduleReconnect(connectError); @@ -694,6 +611,39 @@ type NativeSnapshot = { error?: string; }; +class TauriMTPAdapter implements MTPProxyAdapter { + readonly #subscriptions = new Map void>>(); + + async request( + type: string, + data: Record, + options?: MTPRequestOptions, + ) { + return await invoke("mtp_request", { + typeName: type, + data, + id: options?.id, + }); + } + + subscribe(type: string, handler: (message: MTPFrame) => void) { + const handlers = + this.#subscriptions.get(type) ?? new Set<(message: MTPFrame) => void>(); + handlers.add(handler); + this.#subscriptions.set(type, handlers); + return () => { + handlers.delete(handler); + if (handlers.size === 0) this.#subscriptions.delete(type); + }; + } + + dispatch(message: MTPFrame) { + for (const handler of this.#subscriptions.get(message.type) ?? []) { + handler(message); + } + } +} + function TauriProvider(props: { children: ReactNode; blockConnection?: boolean; @@ -714,8 +664,16 @@ function TauriProvider(props: { pushHandlersRef, subscribePush, } = useMessageHandlers(); - const subscriptionsRef = useRef( - new Map void>>(), + const [adapter] = useState(() => new TauriMTPAdapter()); + const [connection] = useState( + () => + new MTPProxyConnection(adapter, { + schemas, + throwProtocolErrors: true, + onValidationError: (error) => { + log(1, "mtp", "red", "Failed to validate native MTP message", error); + }, + }), ); const applySnapshot = useCallback((next: NativeSnapshot) => { @@ -739,39 +697,35 @@ function TauriProvider(props: { const dispatchMessage = useCallback( (raw: unknown) => { if (!raw || typeof raw !== "object" || !("type" in raw)) return; - const message = raw as { id?: number; type: string; data: unknown }; - let validated: ProtocolMessage; - try { - validated = validateResponse( - message.type as keyof Schemas & string, - message, - ); - } catch (error) { - log(1, "mtp", "red", "Failed to validate native MTP message", error); - return; - } - for (const handler of subscriptionsRef.current.get(validated.type) ?? - []) { - handler(validated); - } - if (!isPushType(validated.type)) return; - setFreshContacts((contacts) => - removeMissingContacts(contacts, validated), - ); - for (const handler of [...pushHandlersRef.current]) { - void Promise.resolve(handler(validated)).catch((error) => { - log(1, "mtp", "red", "Native MTP push handler failed", error, { - type: validated.type, - }); - }); - } - if (validated.type === "GetStates") { - lastInitialStateRef.current = validated; - } + adapter.dispatch(raw as MTPFrame); }, - [lastInitialStateRef, pushHandlersRef], + [adapter], ); + useEffect(() => { + const unsubscribers = PUSH_TYPES.map((type) => + connection.subscribe(type, (message) => { + const validated = message as ProtocolMessage; + setFreshContacts((contacts) => + removeMissingContacts(contacts, validated), + ); + for (const handler of [...pushHandlersRef.current]) { + void Promise.resolve(handler(validated)).catch((error) => { + log(1, "mtp", "red", "Native MTP push handler failed", error, { + type, + }); + }); + } + if (validated.type === "GetStates") { + lastInitialStateRef.current = validated; + } + }), + ); + return () => { + for (const unsubscribe of unsubscribers) unsubscribe(); + }; + }, [connection, lastInitialStateRef, pushHandlersRef]); + useEffect(() => { if (props.blockConnection) return; let disposed = false; @@ -847,26 +801,10 @@ function TauriProvider(props: { const send = useCallback( async (type, data, options) => { - const response = await invoke("mtp_request", { - typeName: type, - data: data ?? {}, - id: options?.id, - }); - const validated = validateResponse(type, response); + const validated = await connection.request(type, data, options); setFreshContacts((contacts) => removeMissingContacts(contacts, validated), ); - if (validated.type.startsWith("Error")) { - const errorData = validated.data as Record; - throw new ProtocolError({ - type: validated.type, - requestId: validated.id, - errorType: - typeof errorData.ErrorType === "string" - ? errorData.ErrorType - : undefined, - }); - } for (const interceptor of interceptorsRef.current) { void Promise.resolve( interceptor({ type, data, response: validated as ProtocolMessage }), @@ -876,20 +814,13 @@ function TauriProvider(props: { } return validated; }, - [interceptorsRef], + [connection, interceptorsRef], ); - const subscribe = useCallback((type, handler) => { - const handlers = - subscriptionsRef.current.get(type) ?? - new Set<(message: ProtocolMessage) => void>(); - handlers.add(handler as (message: ProtocolMessage) => void); - subscriptionsRef.current.set(type, handlers); - return () => { - handlers.delete(handler as (message: ProtocolMessage) => void); - if (handlers.size === 0) subscriptionsRef.current.delete(type); - }; - }, []); + const subscribe = useCallback( + (type, handler) => connection.subscribe(type, handler), + [connection], + ); const connected = snapshot.readyState === ConnectionState.Connected; const contextReady = connected && snapshot.identified; @@ -916,7 +847,7 @@ function TauriProvider(props: { ); } -export function Provider(props: { +function BrowserProviderLoader(props: { children: ReactNode; blockConnection?: boolean; }) { @@ -941,10 +872,17 @@ export function Provider(props: { if (wasmError) throw wasmError; if (!wasmReady) return null; + return ; +} + +export function Provider(props: { + children: ReactNode; + blockConnection?: boolean; +}) { return isTauri() ? ( ) : ( - + ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index faff301..721102f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -458,9 +458,6 @@ importers: '@tauri-apps/api': specifier: ^2.11.1 version: 2.11.1 - '@tensamin/crypto': - specifier: workspace:* - version: link:../crypto '@tensamin/shared': specifier: workspace:* version: link:../shared @@ -473,12 +470,6 @@ importers: react: specifier: ^19.2.8 version: 19.2.8 - react-dom: - specifier: ^19.2.8 - version: 19.2.8(react@19.2.8) - zod: - specifier: ^4.4.3 - version: 4.4.3 devDependencies: eslint: specifier: ^10.8.0 From 0a304e44f2023807703e44fd4995b8840a3b9aaf Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 27 Aug 2026 23:30:33 +0200 Subject: [PATCH 15/19] feat(mtp): move useful stuff over to mtp directly --- .cargo/config.toml | 2 + apps/tauri/.cargo/config.toml | 2 - apps/tauri/src-tauri/src/mtp_backend.rs | 99 +- packages/cache/src/sync.tsx | 18 +- packages/call/src/store.tsx | 15 +- packages/chat/src/context.tsx | 160 +--- packages/markdown/src/markdown.tsx | 4 +- packages/mtp/package.json | 7 +- packages/mtp/src/browser.tsx | 529 +++++++++++ packages/mtp/src/context.test.tsx | 220 ----- packages/mtp/src/context.tsx | 1105 +---------------------- packages/mtp/src/index.ts | 4 +- packages/mtp/src/mtpContext.tsx | 159 ++++ packages/mtp/src/protocolFields.test.ts | 44 - packages/mtp/src/protocolFields.ts | 58 -- packages/mtp/src/requestIds.test.ts | 25 - packages/mtp/src/requestIds.ts | 24 - packages/mtp/src/tauri.tsx | 258 ++++++ packages/notifications/src/context.tsx | 251 +++-- packages/user/src/context.tsx | 58 +- pnpm-lock.yaml | 41 +- pnpm-workspace.yaml | 2 +- 22 files changed, 1216 insertions(+), 1869 deletions(-) create mode 100644 .cargo/config.toml delete mode 100644 apps/tauri/.cargo/config.toml create mode 100644 packages/mtp/src/browser.tsx delete mode 100644 packages/mtp/src/context.test.tsx create mode 100644 packages/mtp/src/mtpContext.tsx delete mode 100644 packages/mtp/src/protocolFields.test.ts delete mode 100644 packages/mtp/src/protocolFields.ts delete mode 100644 packages/mtp/src/requestIds.test.ts delete mode 100644 packages/mtp/src/requestIds.ts create mode 100644 packages/mtp/src/tauri.tsx diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..46adaad --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +MTP_TYPE_MAPS = { value = "mtp-type-maps/type-maps.yaml", relative = true } diff --git a/apps/tauri/.cargo/config.toml b/apps/tauri/.cargo/config.toml deleted file mode 100644 index 161678f..0000000 --- a/apps/tauri/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[env] -MTP_TYPE_MAPS = { value = "../../mtp-type-maps/type-maps.yaml", relative = true } diff --git a/apps/tauri/src-tauri/src/mtp_backend.rs b/apps/tauri/src-tauri/src/mtp_backend.rs index 14365b8..fa28597 100644 --- a/apps/tauri/src-tauri/src/mtp_backend.rs +++ b/apps/tauri/src-tauri/src/mtp_backend.rs @@ -341,7 +341,7 @@ async fn supervise(generation: u64) { let notification_config = config.clone(); let notification_worker = tokio::spawn(async move { while let Some(frame) = notification_rx.recv().await { - if !manager().is_current(generation) { + if !manager.is_current(generation) { break; } if let Err(error) = notify_message( @@ -497,7 +497,7 @@ fn prepare_initial_state_ack( .ok_or("ClientStateSync payload is not an object")?; let session_id = required_integer(data, "SessionId")?; let version = required_integer(data, "VersionNumber")?; - let ack = CommunicationValue::new(CommunicationType::ClientStateAck) + let ack = CommunicationValue::new(communication_type("ClientStateAck")?) .with_id(request_ids.next()?) .add_typed_default(DataType::SessionId, number_to_data(session_id)) .add_typed_default(DataType::VersionNumber, number_to_data(version)); @@ -645,7 +645,7 @@ async fn await_initial_state( return Err("No Iota is currently connected".into()); } - if frame.is_type(CommunicationType::ClientStateSync) { + if frame.get_type_name() == Some("ClientStateSync") { return Ok((frame, buffered)); } @@ -740,7 +740,7 @@ async fn notify_message( let message = frame .get_data(DataType::Message) .ok_or("MessageLive omitted Message")?; - let content = container_value(message, DataType::AppContent) + let content = container_value_by_name(message, "Content") .and_then(DataValue::as_str) .ok_or("MessageLive omitted Content")?; let keyring_bytes = decode_browser_base64(&config.keyring)?; @@ -888,42 +888,24 @@ fn container_value(value: &DataValue, field: DataType) -> Option<&DataValue> { value.get_field(id) } -fn wire_field_name(name: &str) -> &str { - match name { - "Content" => "AppContent", - "CreatedAt" => "AppCreatedAt", - "MessageId" => "AppMessageId", - _ => name, - } +fn container_value_by_name<'a>(value: &'a DataValue, field: &str) -> Option<&'a DataValue> { + container_value(value, DataType::from_name(field)?) } -fn application_field_name(name: &str) -> &str { - match name { - "AppContent" => "Content", - "AppCreatedAt" => "CreatedAt", - "AppMessageId" => "MessageId", - _ => name, - } +fn communication_type(name: &str) -> Result { + CommunicationType::from_name(name).ok_or_else(|| format!("unknown communication type: {name}")) } fn json_to_frame(type_name: &str, data: Value, id: u32) -> Result { - let comm_type = CommunicationType::from_name(type_name) - .ok_or_else(|| format!("unknown communication type: {type_name}"))?; + let comm_type = communication_type(type_name)?; let mut frame = CommunicationValue::new(comm_type).with_id(id); let Value::Object(fields) = data else { return Err("MTP request data must be an object".into()); }; - let mut translated_fields = std::collections::HashSet::::with_capacity(fields.len()); for (name, value) in fields { - let wire_name = wire_field_name(&name).to_owned(); - if !translated_fields.insert(wire_name.clone()) { - return Err(format!( - "duplicate MTP field after translation: {wire_name}" - )); - } let data_type = - DataType::from_name(&wire_name).ok_or_else(|| format!("unknown data type: {name}"))?; - frame = frame.add_typed_default(data_type, json_to_data(&wire_name, value)?); + DataType::from_name(&name).ok_or_else(|| format!("unknown data type: {name}"))?; + frame = frame.add_typed_default(data_type, json_to_data(&name, value)?); } Ok(frame) } @@ -961,21 +943,13 @@ fn json_to_data(field: &str, value: Value) -> Result { ), Value::Object(fields) => { let mut entries = Vec::with_capacity(fields.len()); - let mut translated_fields = - std::collections::HashSet::::with_capacity(fields.len()); for (name, value) in fields { - let wire_name = wire_field_name(&name).to_owned(); - if !translated_fields.insert(wire_name.clone()) { - return Err(format!( - "duplicate MTP field after translation: {wire_name}" - )); - } - let data_type = DataType::from_name(&wire_name) + let data_type = DataType::from_name(&name) .ok_or_else(|| format!("unknown nested data type: {name}"))?; let id = data_type .try_to_id(&TypeMap::latest()) .ok_or_else(|| format!("unmapped data type: {name}"))?; - entries.push((id, json_to_data(&wire_name, value)?)); + entries.push((id, json_to_data(&name, value)?)); } DataValue::Container(entries) } @@ -1020,13 +994,7 @@ fn frame_data_to_json(frame: &CommunicationValue) -> Result { let name = map .data_type_name(id.0) .ok_or_else(|| format!("unknown data type id: {}", id.0))?; - let application_name = application_field_name(name); - if result.contains_key(application_name) { - return Err(format!( - "duplicate MTP field after translation: {application_name}" - )); - } - result.insert(application_name.to_owned(), data_to_json(value, &map)?); + result.insert(name.to_owned(), data_to_json(value, &map)?); } Ok(Value::Object(result)) } @@ -1057,13 +1025,7 @@ fn data_to_json(value: &DataValue, map: &TypeMap) -> Result { let name = map .data_type_name(id.0) .ok_or_else(|| format!("unknown nested data type id: {}", id.0))?; - let application_name = application_field_name(name); - if object.contains_key(application_name) { - return Err(format!( - "duplicate MTP field after translation: {application_name}" - )); - } - object.insert(application_name.to_owned(), data_to_json(value, map)?); + object.insert(name.to_owned(), data_to_json(value, map)?); } Value::Object(object) } @@ -1084,7 +1046,7 @@ mod tests { use serde_json::json; use super::{ - container_value, decode_browser_base64, decode_sdk_bytes, frame_to_json, + container_value_by_name, decode_browser_base64, decode_sdk_bytes, frame_to_json, jittered_retry_delay, json_to_frame, prepare_initial_state_ack, RequestIdAllocator, }; @@ -1117,7 +1079,7 @@ mod tests { } #[test] - fn json_content_uses_app_content_wire_type() { + fn json_content_uses_content_wire_type() { let frame = json_to_frame( "MessageEdit", json!({ @@ -1131,14 +1093,14 @@ mod tests { assert_eq!( frame - .get_data(DataType::AppContent) + .get_data(DataType::Content) .and_then(DataValue::as_str), Some("ciphertext") ); } #[test] - fn nested_json_content_uses_app_content_wire_type() { + fn nested_json_content_uses_content_wire_type() { let frame = json_to_frame( "MessageEdit", json!({ @@ -1150,35 +1112,24 @@ mod tests { let message = frame.get_data(DataType::Message).unwrap(); assert_eq!( - container_value(message, DataType::AppContent).and_then(DataValue::as_str), + container_value_by_name(message, "Content").and_then(DataValue::as_str), Some("ciphertext") ); } #[test] - fn app_content_is_exposed_as_content_to_frontend() { + fn content_is_exposed_to_frontend() { let frame = CommunicationValue::new(CommunicationType::MessageEditLive) .with_id(1) - .add_typed_default(DataType::AppContent, DataValue::Str("ciphertext".into())); + .add_typed_default(DataType::Content, DataValue::Str("ciphertext".into())); let json = frame_to_json(&frame).unwrap(); assert_eq!(json["data"]["Content"], "ciphertext"); - assert!(json["data"].get("AppContent").is_none()); - } - - #[test] - fn translated_field_collisions_are_rejected() { - assert!(json_to_frame( - "MessageEdit", - json!({ "Content": "a", "AppContent": "b" }), - 1, - ) - .is_err()); } fn valid_initial_state() -> CommunicationValue { - CommunicationValue::new(CommunicationType::ClientStateSync) + CommunicationValue::new(communication_type("ClientStateSync").unwrap()) .add_typed_default(DataType::SessionId, DataValue::UnsignedNumber(1)) .add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0)) .add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0)) @@ -1195,7 +1146,7 @@ mod tests { let (state, ack) = prepare_initial_state_ack(&valid_initial_state(), &ids).unwrap(); assert_eq!(state["SyncMode"], "full"); - assert!(ack.is_type(CommunicationType::ClientStateAck)); + assert_eq!(ack.get_type_name(), Some("ClientStateAck")); assert_eq!(ack.id(), Some(1)); } @@ -1216,7 +1167,7 @@ mod tests { #[test] fn malformed_nested_initial_state_does_not_prepare_ack() { let ids = RequestIdAllocator::new(); - let malformed = CommunicationValue::new(CommunicationType::ClientStateSync) + let malformed = CommunicationValue::new(communication_type("ClientStateSync").unwrap()) .add_typed_default(DataType::SessionId, DataValue::UnsignedNumber(1)) .add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0)) .add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0)) diff --git a/packages/cache/src/sync.tsx b/packages/cache/src/sync.tsx index 37076c1..8928e23 100644 --- a/packages/cache/src/sync.tsx +++ b/packages/cache/src/sync.tsx @@ -21,8 +21,7 @@ export function removeMissingContactSnapshots( } export default function CacheSync() { - const { addInterceptor, contextReady, freshContacts, subscribePush } = - useMTP(); + const { addInterceptor, contextReady, freshContacts, subscribe } = useMTP(); const { load } = useStorage(); const [accountId, setAccountId] = useState(0); const queueRef = useRef(Promise.resolve()); @@ -321,10 +320,19 @@ export default function CacheSync() { useEffect(() => { if (!accountId || !contextReady) return; - return subscribePush((message) => { + const handleMessage = (message: ProtocolMessage) => { void enqueue(() => synchronizePush(message)); - }); - }, [accountId, contextReady, enqueue, subscribePush, synchronizePush]); + }; + const unsubscribers = [ + subscribe("GetStates", handleMessage), + subscribe("MessageLive", handleMessage), + subscribe("MessageEditLive", handleMessage), + subscribe("MessageDeleteLive", handleMessage), + subscribe("MessageState", handleMessage), + subscribe("MessageReactionLive", handleMessage), + ]; + return () => unsubscribers.forEach((unsubscribe) => unsubscribe()); + }, [accountId, contextReady, enqueue, subscribe, synchronizePush]); return null; } diff --git a/packages/call/src/store.tsx b/packages/call/src/store.tsx index 6f7f524..bbf3f4e 100644 --- a/packages/call/src/store.tsx +++ b/packages/call/src/store.tsx @@ -1223,7 +1223,7 @@ export const useCall = create<{ export function useInitializeCall() { const navigate = useNavigate(); const location = useLocation(); - const { send, subscribePush } = useMTP(); + const { send, subscribe } = useMTP(); const { load } = useStorage(); const { insertCall } = useSession(); const { get } = useUser(); @@ -1313,14 +1313,9 @@ export function useInitializeCall() { // listen to call invites useEffect(() => { - return subscribePush(async (message) => { - if (message.type !== "CallInvite") return; - - const { CallId, CallSecret, SenderId } = message.data as { - CallId: string; - CallSecret: ProtocolCallSecret; - SenderId: number; - }; + return subscribe("CallInvite", async ({ data }) => { + const { CallId, CallSecret, SenderId } = data; + if (!CallId || !CallSecret || !SenderId) return; if (SenderId === Number(await load("user_id"))) { return; @@ -1337,7 +1332,7 @@ export function useInitializeCall() { SenderId, ); }); - }, [load, subscribePush, showCallingScreen]); + }, [load, subscribe, showCallingScreen]); // get callId from url useEffect(() => { diff --git a/packages/chat/src/context.tsx b/packages/chat/src/context.tsx index b4f1f5b..7c88df1 100644 --- a/packages/chat/src/context.tsx +++ b/packages/chat/src/context.tsx @@ -226,7 +226,7 @@ export async function fetchReplyMessage({ export default function Provider({ children }: { children: ReactNode }) { const { load } = useStorage(); - const { send, subscribePush } = useMTP(); + const { send, subscribe } = useMTP(); const { get: getUser } = useUser(); const { moveUserIdToTop } = useSession(); @@ -912,124 +912,47 @@ export default function Provider({ children }: { children: ReactNode }) { // Get live updates for message states useEffect(() => { - return subscribePush((message) => { - if (message.type === "MessageEditLive") { - if (!currentChatSecret) return; - - const rawData = message.data as { - ChatPartnerId: unknown; - SendTime: unknown; - Content: string; - }; - - const chatPartnerId = Number(rawData.ChatPartnerId); - const sendTime = Number(rawData.SendTime); - - if (!Number.isFinite(chatPartnerId) || !Number.isFinite(sendTime)) { - log( - 3, - "chat", - "yellow", - "Cancel message edit update due to invalid data", - ); - return; - } - - if (chatPartnerId !== userIdValue) { - log( - 3, - "chat", - "yellow", - "Cancel message edit update due to user ID mismatch", - { - expected: userIdValue, - received: chatPartnerId, - }, - ); - return; - } - - void decryptChatText(currentChatSecret, rawData.Content) - .then((content) => { - editMessage(sendTime, { Content: content, Edited: true }); - }) - .catch((err) => { - log(1, "chat", "red", "Failed to decrypt message edit", err, { - SendTime: sendTime, - }); - }); - return; - } - - if (message.type === "MessageReactionLive") { - const rawData = message.data as { - ChatPartnerId: unknown; - SendTime: unknown; - Reaction: string; - SenderId: unknown; - Accepted: boolean; - }; - const chatPartnerId = Number(rawData.ChatPartnerId); - const sendTime = Number(rawData.SendTime); - const senderId = Number(rawData.SenderId); - - if ( - chatPartnerId !== userIdValue || - !Number.isFinite(sendTime) || - !Number.isFinite(senderId) - ) { - return; - } - - applyLiveReaction( - sendTime, - rawData.Reaction, - senderId, - rawData.Accepted, - ); - return; - } - - if (message.type === "MessageDeleteLive") { - const data = message.data as { - ChatPartnerId: number; - SendTime: number; - }; - - if (data.ChatPartnerId !== userIdValue) return; - - removeMessage(data.SendTime); - return; - } - - if (message.type !== "MessageState") return; - - const rawData = message.data as { - ChatPartnerId: unknown; - SendTime: unknown; - MessageState: RawMessage["MessageState"]; - }; - - const nextState = { - ChatPartnerId: Number(rawData.ChatPartnerId), - SendTime: Number(rawData.SendTime), - MessageState: rawData.MessageState, - }; - - if ( - !Number.isFinite(nextState.ChatPartnerId) || - !Number.isFinite(nextState.SendTime) - ) { + const unsubscribeEdit = subscribe("MessageEditLive", ({ data }) => { + if (!currentChatSecret) return; + if (data.ChatPartnerId !== userIdValue) { log( 3, "chat", "yellow", - "Cancel message state update due to invalid data", + "Cancel message edit update due to user ID mismatch", + { + expected: userIdValue, + received: data.ChatPartnerId, + }, ); return; } - if (nextState.ChatPartnerId !== userIdValue) { + void decryptChatText(currentChatSecret, data.Content) + .then((content) => { + editMessage(data.SendTime, { Content: content, Edited: true }); + }) + .catch((err) => { + log(1, "chat", "red", "Failed to decrypt message edit", err, { + SendTime: data.SendTime, + }); + }); + }); + const unsubscribeReaction = subscribe("MessageReactionLive", ({ data }) => { + if (data.ChatPartnerId !== userIdValue) return; + applyLiveReaction( + data.SendTime, + data.Reaction, + data.SenderId, + data.Accepted, + ); + }); + const unsubscribeDelete = subscribe("MessageDeleteLive", ({ data }) => { + if (data.ChatPartnerId !== userIdValue) return; + removeMessage(data.SendTime); + }); + const unsubscribeState = subscribe("MessageState", ({ data }) => { + if (data.ChatPartnerId !== userIdValue) { log( 3, "chat", @@ -1037,22 +960,27 @@ export default function Provider({ children }: { children: ReactNode }) { "Cancel message state update due to user ID mismatch", { expected: userIdValue, - received: nextState.ChatPartnerId, + received: data.ChatPartnerId, }, ); return; } - - editMessage(nextState.SendTime, { - MessageState: nextState.MessageState, + editMessage(data.SendTime, { + MessageState: data.MessageState, }); }); + return () => { + unsubscribeEdit(); + unsubscribeReaction(); + unsubscribeDelete(); + unsubscribeState(); + }; }, [ currentChatSecret, applyLiveReaction, editMessage, removeMessage, - subscribePush, + subscribe, userIdValue, ]); diff --git a/packages/markdown/src/markdown.tsx b/packages/markdown/src/markdown.tsx index cd2a210..f8684c4 100644 --- a/packages/markdown/src/markdown.tsx +++ b/packages/markdown/src/markdown.tsx @@ -159,7 +159,7 @@ function CopyableCode({ * @param input Parameter input. * @returns InlineNode[]. */ -export function parseInlineNodes(input: string): InlineNode[] { +function parseInlineNodes(input: string): InlineNode[] { const nodes: InlineNode[] = []; let cursor = 0; @@ -205,7 +205,7 @@ export function parseInlineNodes(input: string): InlineNode[] { return nodes; } -export function parseEmojiText(input: string): InlineNode[] { +function parseEmojiText(input: string): InlineNode[] { const nodes: InlineNode[] = []; let cursor = 0; diff --git a/packages/mtp/package.json b/packages/mtp/package.json index e036029..44f5750 100644 --- a/packages/mtp/package.json +++ b/packages/mtp/package.json @@ -9,19 +9,16 @@ "scripts": { "format": "pnpm exec prettier --write .", "lint": "eslint src --ext .ts,.tsx", - "test": "vitest run", + "test": "vitest run --passWithNoTests", "build": "pnpm run test && tsc -p tsconfig.json --noEmit" }, "dependencies": { "@methanium/ui": "*", "@tauri-apps/api": "^2.11.1", - "@tensamin/crypto": "workspace:*", "@tensamin/shared": "workspace:*", "@tensamin/storage": "workspace:*", "mtp": "*", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "zod": "^4.4.3" + "react": "^19.2.8" }, "devDependencies": { "eslint": "^10.8.0" diff --git a/packages/mtp/src/browser.tsx b/packages/mtp/src/browser.tsx new file mode 100644 index 0000000..755a021 --- /dev/null +++ b/packages/mtp/src/browser.tsx @@ -0,0 +1,529 @@ +import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; +import { toast as sonnerToast } from "@methanium/ui"; +import { base64ToBytes, ConnectionState, MTPClient } from "mtp"; +import createAsyncQueue from "@tensamin/shared/asyncQueue"; +import { + mtp as mtpSchemas, + type Calls, + type Communities, + type Contacts, +} from "@tensamin/shared/data"; +import { log } from "@tensamin/shared/log"; +import { useStorage } from "@tensamin/storage/context"; + +import { + type BoundSendFn, + MTPContext, + type MTPContextType, + type ProtocolMessage, + removeMissingContacts, + useMessageHandlers, +} from "./mtpContext"; +import { + DISCOVERY_TIMEOUT, + INITIAL_SYNC_TIMEOUT, + RECONNECT_JITTER, + RECONNECT_LONG_INTERVAL, + RECONNECT_RESET, + RECONNECT_TRIES, + RETRY_INTERVAL, + STATE_ACK_TIMEOUT, +} from "./values"; + +type BrowserMtpClient = Awaited>; + +function createBrowserClient( + options: Omit[0], "schemas">, +) { + return MTPClient.create({ + ...options, + schemas: mtpSchemas, + throwProtocolErrors: true, + onValidationError: (error) => { + log(1, "mtp", "red", "Failed to validate push message", error); + }, + }); +} + +function abortError(signal: AbortSignal): Error { + return signal.reason instanceof Error + ? signal.reason + : new Error("Initial state synchronization was cancelled"); +} + +function withDeadline( + promise: Promise, + timeoutMs: number, + timeoutMessage: string, + signal: AbortSignal, +): Promise { + return new Promise((resolve, reject) => { + if (signal.aborted) { + reject(abortError(signal)); + return; + } + let settled = false; + const finish = (complete: () => void) => { + if (settled) return; + settled = true; + clearTimeout(timeout); + signal.removeEventListener("abort", onAbort); + complete(); + }; + const timeout = setTimeout( + () => finish(() => reject(new Error(timeoutMessage))), + timeoutMs, + ); + const onAbort = () => finish(() => reject(abortError(signal))); + signal.addEventListener("abort", onAbort, { once: true }); + promise.then( + (value) => finish(() => resolve(value)), + (error: unknown) => finish(() => reject(error)), + ); + }); +} + +async function completeInitialSynchronization( + client: BrowserMtpClient, + subscribe: MTPContextType["subscribe"], + signal: AbortSignal, + syncTimeoutMs = INITIAL_SYNC_TIMEOUT, + ackTimeoutMs = STATE_ACK_TIMEOUT, +): Promise> { + const stateSync = new Promise>( + (resolve, reject) => { + let unsubscribeStateSync = () => {}; + let unsubscribeNoIota = () => {}; + const cleanup = () => { + clearTimeout(timeout); + unsubscribeStateSync(); + unsubscribeNoIota(); + signal.removeEventListener("abort", onAbort); + }; + const onAbort = () => { + cleanup(); + reject(abortError(signal)); + }; + const timeout = setTimeout(() => { + cleanup(); + reject(new Error("Initial state synchronization timed out")); + }, syncTimeoutMs); + signal.addEventListener("abort", onAbort, { once: true }); + if (signal.aborted) { + onAbort(); + return; + } + unsubscribeStateSync = subscribe("ClientStateSync", (message) => { + cleanup(); + resolve(message); + }); + unsubscribeNoIota = subscribe("ErrorNoIota", () => { + cleanup(); + reject(new Error("No Iota is currently connected")); + }); + }, + ); + const [, state] = await Promise.all([ + withDeadline( + client.auth(), + syncTimeoutMs, + "MTP authentication timed out", + signal, + ), + stateSync, + ]); + await withDeadline( + client.request("ClientStateAck", { + SessionId: state.data.SessionId, + VersionNumber: state.data.VersionNumber, + }), + ackTimeoutMs, + "State acknowledgement timed out", + signal, + ); + if (signal.aborted) throw abortError(signal); + return state; +} + +function protocolErrorDetails(error: unknown) { + if (typeof error !== "object" || error === null || !("type" in error)) { + return null; + } + const protocolError = error as { + id?: unknown; + type?: unknown; + frame?: unknown; + }; + return { + id: protocolError.id, + type: protocolError.type, + frame: protocolError.frame, + }; +} + +export function BrowserProvider(props: { + children: ReactNode; + blockConnection?: boolean; +}) { + const { load } = useStorage(); + const [readyState, setReadyState] = useState( + ConnectionState.Disconnected, + ); + const [identified, setIdentified] = useState(false); + const [identifying, setIdentifying] = useState(false); + const [freshCommunities, setFreshCommunities] = useState([]); + const [freshContacts, setFreshContacts] = useState([]); + const [freshCalls, setFreshCalls] = useState([]); + const clientRef = useRef(null); + const { addInterceptor, attachSubscriptions, interceptorsRef, subscribe } = + useMessageHandlers(); + const connected = readyState === ConnectionState.Connected; + + const [mtpUrl, setMtpUrl] = useState(null); + useEffect(() => { + load("omega_url").then(setMtpUrl); + }, [load]); + + const send: BoundSendFn = useMemo( + () => async (type, data, options) => { + const client = clientRef.current; + if (!client) throw new Error("mtp is not connected"); + const response = await client.request(type, data, options); + if (response.type === "GetStates") { + setFreshContacts((contacts) => + removeMissingContacts( + contacts, + response as ProtocolMessage<"GetStates">, + ), + ); + } + return response; + }, + [], + ); + + const resolveConnectionRef = useRef(() => {}); + useEffect(() => { + if (!mtpUrl) return; + let attempts = 0; + let reconnectTimer: ReturnType | null = null; + let reconnectResetTimer: ReturnType | null = null; + let reconnectScheduled = false; + let disposed = false; + let connectionGeneration = 0; + let cleanupConnection = () => {}; + + const clearReconnectTimer = () => { + if (!reconnectTimer) return; + clearTimeout(reconnectTimer); + reconnectTimer = null; + reconnectScheduled = false; + }; + const clearReconnectResetTimer = () => { + if (!reconnectResetTimer) return; + clearTimeout(reconnectResetTimer); + reconnectResetTimer = null; + }; + const scheduleReconnect = (error: unknown) => { + if (disposed || reconnectScheduled) return; + attempts += 1; + const shortRetry = attempts <= RECONNECT_TRIES; + if (!shortRetry) { + log(0, "mtp", "red", "Reconnection attempts exhausted", error); + sonnerToast.error("Connection failed", { + id: "mtp-connection-toast", + description: + error instanceof Error + ? `${error.message.split(":")[0]}. Retrying in the background.` + : "Connection lost. Retrying in the background.", + icon: null, + duration: Infinity, + closeButton: true, + promise: null, + } as unknown as Parameters[1]); + } else { + sonnerToast.loading( + `Reconnecting to server... (attempt ${attempts} of ${RECONNECT_TRIES})`, + { id: "mtp-connection-toast" }, + ); + } + const baseDelay = shortRetry ? RETRY_INTERVAL : RECONNECT_LONG_INTERVAL; + const jitter = 1 + (Math.random() * 2 - 1) * RECONNECT_JITTER; + reconnectScheduled = true; + reconnectTimer = setTimeout( + () => { + reconnectScheduled = false; + reconnectTimer = null; + void connect(); + }, + Math.round(baseDelay * jitter), + ); + }; + + async function connect() { + if (disposed || props.blockConnection) return; + const generation = ++connectionGeneration; + let client: BrowserMtpClient | null = null; + let failed = false; + let connectionReady = false; + let detachSubscriptions = () => {}; + let unsubscribeNoIota = () => {}; + const attemptAbort = new AbortController(); + const cleanup = () => { + attemptAbort.abort( + new Error("Initial state synchronization was cancelled"), + ); + unsubscribeNoIota(); + detachSubscriptions(); + client?.disconnect(); + if (clientRef.current === client) clientRef.current = null; + clearReconnectResetTimer(); + if (generation === connectionGeneration) { + setReadyState(ConnectionState.Disconnected); + setIdentified(false); + setIdentifying(false); + } + }; + cleanupConnection = cleanup; + try { + setIdentified(false); + setIdentifying(false); + const [userId, keyring] = await Promise.all([ + load("user_id"), + load("mtp_keyring"), + ]); + if (!userId || !keyring) throw new Error("Missing login credentials"); + const forcedOmikronUrl = await load("forced_omikron_url"); + const forcedOmikronPublicKey = await load("forced_omikron_public_key"); + let url = null; + let omikronPublicKey = null; + if (forcedOmikronUrl && forcedOmikronPublicKey) { + url = forcedOmikronUrl; + omikronPublicKey = forcedOmikronPublicKey; + } else { + log(2, "mtp", "purple", "Fetching Omikron data."); + const data = await fetch(`${mtpUrl}api/get/omikron/${userId}`, { + signal: AbortSignal.any([ + attemptAbort.signal, + AbortSignal.timeout(DISCOVERY_TIMEOUT), + ]), + }); + if (data.status === 404) { + throw new Error("No Omikron assignment is currently available"); + } + if (!data.ok) + throw new Error(`Omikron discovery failed: HTTP ${data.status}`); + const omikronData = (await data.json()) as { + ip_address: string; + port: number; + public_key: string; + }; + if ( + !omikronData.ip_address || + !omikronData.port || + !omikronData.public_key + ) { + throw new Error("Invalid Omikron data"); + } + url = `https://${omikronData.ip_address}:${omikronData.port}`; + omikronPublicKey = omikronData.public_key; + } + if (!url || !omikronPublicKey) { + throw new Error("Missing Omikron URL or Public Key"); + } + log(2, "mtp", "green", "Connecting to: " + url); + client = await createBrowserClient({ + url, + credentials: { clientId: userId, keyring: base64ToBytes(keyring) }, + hostPublicKey: { value: omikronPublicKey, encoding: "base64" }, + descriptor: "client", + pings: true, + logger: (event) => { + if (event.type === "state") { + if (generation !== connectionGeneration) return; + const state = client?.state ?? ConnectionState.Disconnected; + setReadyState(state); + if ( + state === ConnectionState.Disconnected && + clientRef.current === client && + !failed + ) { + failed = true; + const error = new Error("MTP connection lost"); + attemptAbort.abort(error); + if (connectionReady) { + cleanup(); + scheduleReconnect(error); + } + } + } + if (event.type !== "Pong" && event.type !== "Ping") { + log( + 2, + "mtp", + event.type === "state" + ? "purple" + : event.direction === "recv" + ? "cyan" + : event.direction === "send" + ? "gray" + : "blue", + event.type === "state" + ? event.data + : event.direction === "recv" + ? "< " + event.type + : event.direction === "send" + ? "> " + event.type + : event.type, + event, + ); + } + }, + }); + if (disposed || generation !== connectionGeneration) { + client.disconnect(); + return; + } + const activeClient = client; + clientRef.current = activeClient; + detachSubscriptions = attachSubscriptions(activeClient); + unsubscribeNoIota = subscribe("ErrorNoIota", () => { + if (clientRef.current !== activeClient || failed) return; + failed = true; + const error = new Error("No Iota is currently connected"); + attemptAbort.abort(error); + cleanup(); + scheduleReconnect(error); + }); + setReadyState(activeClient.state); + setIdentifying(true); + const finalResponse = await completeInitialSynchronization( + activeClient, + subscribe, + attemptAbort.signal, + ); + if (disposed || clientRef.current !== activeClient) return; + setFreshContacts(finalResponse.data.Contacts); + setFreshCommunities(finalResponse.data.Communities); + setFreshCalls(finalResponse.data.Calls); + connectionReady = true; + setIdentifying(false); + setIdentified(true); + clearReconnectTimer(); + clearReconnectResetTimer(); + reconnectResetTimer = setTimeout(() => { + attempts = 0; + reconnectResetTimer = null; + }, RECONNECT_RESET * 1_000); + resolveConnectionRef.current?.(); + } catch (connectError) { + if (disposed || generation !== connectionGeneration) { + client?.disconnect(); + return; + } + failed = true; + cleanup(); + const message = + connectError instanceof Error + ? connectError.message + : String(connectError ?? "Unknown error"); + log( + 0, + "mtp", + "red", + `Connection/authentication attempt failed: ${message}`, + protocolErrorDetails(connectError) ?? connectError, + ); + scheduleReconnect(connectError); + } + } + + void connect(); + return () => { + disposed = true; + clearReconnectTimer(); + clearReconnectResetTimer(); + cleanupConnection(); + setReadyState(ConnectionState.Disconnected); + setIdentified(false); + setIdentifying(false); + sonnerToast.dismiss("mtp-connection-toast"); + }; + }, [attachSubscriptions, load, mtpUrl, props.blockConnection, subscribe]); + + useEffect(() => { + return subscribe("ErrorNoIota", () => { + setIdentified(false); + setIdentifying(false); + sonnerToast.error("We couldn't reach your Iota", { + description: + "Check your network connection and try restarting your Iota", + icon: null, + duration: Infinity, + closeButton: true, + }); + resolveConnectionRef.current?.(); + }); + }, [subscribe]); + + useEffect( + () => + subscribe("GetStates", (message) => { + setFreshContacts((contacts) => + removeMissingContacts(contacts, message), + ); + }), + [subscribe], + ); + + const loadingDescription = useMemo(() => { + if (!mtpUrl) return "Loading connection details"; + if (readyState === ConnectionState.Connecting || !connected) { + return "Establishing transport channel"; + } + if (identifying || !identified) return "Waiting for authenticated session"; + return "Loading..."; + }, [connected, identified, identifying, readyState, mtpUrl]); + const contextReady = connected && identified && mtpUrl !== null; + const mtpRef = useMemo(() => createAsyncQueue<{ send: typeof send }>(), []); + useEffect(() => { + if (connected && identified && mtpUrl) { + mtpRef.set({ send }); + } + }, [connected, identified, mtpUrl, send, mtpRef]); + + const sendQueued: BoundSendFn = useMemo( + () => async (type, data, options) => { + const mtp = await mtpRef.get(); + const response = await mtp.send(type, data, options); + for (const interceptor of interceptorsRef.current) { + void Promise.resolve(interceptor({ type, data, response })).catch( + (error) => { + log(1, "mtp", "yellow", "MTP interceptor failed", error, { type }); + }, + ); + } + return response; + }, + [interceptorsRef, mtpRef], + ); + + return ( + + {props.children} + + ); +} diff --git a/packages/mtp/src/context.test.tsx b/packages/mtp/src/context.test.tsx deleted file mode 100644 index b15043a..0000000 --- a/packages/mtp/src/context.test.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; - -import { RequestIdAllocator } from "./requestIds"; - -vi.mock("@methanium/ui", () => ({ - toast: { - dismiss: vi.fn(), - error: vi.fn(), - loading: vi.fn(), - }, -})); - -vi.mock("@tensamin/shared/log", () => ({ log: vi.fn() })); -vi.mock("@tensamin/storage/context", () => ({ - useStorage: () => ({ load: vi.fn() }), -})); - -const { completeInitialSynchronization, isPushType, validateResponse } = - await import("./context"); - -const validState = { - SessionId: 7, - VersionNumber: 2, - CacheSchemaVersion: 0, - SyncMode: "full", - Contacts: [], - Communities: [], - Calls: [], - Messages: [], - DeletedMessageIds: [], - DeletedContactIds: [], -}; - -function mockInitialSyncClient( - state: { type: string; data: unknown } = { - type: "ClientStateSync", - data: validState, - }, - acknowledgement: unknown = { type: "ClientStateAck", data: {} }, -) { - const handlers = new Map void>(); - const request = vi.fn().mockResolvedValue(acknowledgement); - const client = { - auth: vi.fn(async () => { - handlers.get(state.type)?.(state as never); - }), - subscribe: vi.fn((type: string, handler: (message: never) => void) => { - handlers.set(type, handler); - return () => handlers.delete(type); - }), - request, - disconnect: vi.fn(), - } as unknown as Parameters[0]; - return { client, handlers, request }; -} - -afterEach(() => { - vi.useRealTimers(); -}); - -describe("MTP protocol dispatch", () => { - it("preserves protocol errors for the request layer", () => { - const error = validateResponse("GetStates", { - id: 12, - type: "ErrorInternal", - data: { ErrorType: "temporary" }, - }); - expect(error.type).toBe("ErrorInternal"); - expect(error.id).toBe(12); - }); - - it("recognizes initial and live presence pushes", () => { - expect(isPushType("GetStates")).toBe(true); - expect(isPushType("ClientChanged")).toBe(true); - expect(isPushType("UnknownMessage")).toBe(false); - }); -}); - -describe("browser initial synchronization", () => { - it("validates state, sends a nonzero acknowledgement, then resolves", async () => { - const { client, request } = mockInitialSyncClient(); - - const state = await completeInitialSynchronization( - client, - new RequestIdAllocator(), - new AbortController().signal, - ); - - expect(state.data).toEqual(validState); - expect(request).toHaveBeenCalledWith( - "ClientStateAck", - { SessionId: 7, VersionNumber: 2 }, - { id: 1 }, - ); - }); - - it("does not acknowledge malformed state", async () => { - const { client, request } = mockInitialSyncClient({ - type: "ClientStateSync", - data: { ...validState, SyncMode: "invalid" }, - }); - - await expect( - completeInitialSynchronization( - client, - new RequestIdAllocator(), - new AbortController().signal, - ), - ).rejects.toThrow("Response validation failed"); - expect(request).not.toHaveBeenCalled(); - }); - - it("rejects ErrorNoIota during initial synchronization", async () => { - const { client, request } = mockInitialSyncClient({ - type: "ErrorNoIota", - data: {}, - }); - - await expect( - completeInitialSynchronization( - client, - new RequestIdAllocator(), - new AbortController().signal, - ), - ).rejects.toThrow("No Iota is currently connected"); - expect(request).not.toHaveBeenCalled(); - }); - - it("rejects an acknowledgement protocol error", async () => { - const { client } = mockInitialSyncClient(undefined, { - type: "ErrorInvalidData", - data: {}, - }); - - await expect( - completeInitialSynchronization( - client, - new RequestIdAllocator(), - new AbortController().signal, - ), - ).rejects.toThrow("State acknowledgement failed: ErrorInvalidData"); - }); - - it("times out a missing acknowledgement", async () => { - vi.useFakeTimers(); - const { client } = mockInitialSyncClient(); - vi.mocked(client.request).mockReturnValue(new Promise(() => {})); - const result = completeInitialSynchronization( - client, - new RequestIdAllocator(), - new AbortController().signal, - 100, - 10, - ); - const assertion = expect(result).rejects.toThrow( - "State acknowledgement timed out", - ); - - await vi.advanceTimersByTimeAsync(10); - await assertion; - }); - - it("stops immediately when the connection attempt is cancelled", async () => { - const { client } = mockInitialSyncClient(); - vi.mocked(client.auth).mockImplementation(() => new Promise(() => {})); - const controller = new AbortController(); - const result = completeInitialSynchronization( - client, - new RequestIdAllocator(), - controller.signal, - ); - - controller.abort(new Error("MTP connection lost")); - await expect(result).rejects.toThrow("MTP connection lost"); - }); - - it("times out authentication while waiting for initial state", async () => { - vi.useFakeTimers(); - const { client, handlers } = mockInitialSyncClient(); - vi.mocked(client.auth).mockImplementation(() => { - handlers.get("ClientStateSync")?.({ - type: "ClientStateSync", - data: validState, - } as never); - return new Promise(() => {}); - }); - const result = completeInitialSynchronization( - client, - new RequestIdAllocator(), - new AbortController().signal, - 100, - 10, - ); - const assertion = expect(result).rejects.toThrow( - "MTP authentication timed out", - ); - - await vi.advanceTimersByTimeAsync(100); - await assertion; - }); - - it("uses a fresh request ID namespace for each connection", async () => { - const first = mockInitialSyncClient(); - const second = mockInitialSyncClient(); - - await completeInitialSynchronization( - first.client, - new RequestIdAllocator(), - new AbortController().signal, - ); - await completeInitialSynchronization( - second.client, - new RequestIdAllocator(), - new AbortController().signal, - ); - - expect(first.request.mock.calls[0]?.[2]).toEqual({ id: 1 }); - expect(second.request.mock.calls[0]?.[2]).toEqual({ id: 1 }); - }); -}); diff --git a/packages/mtp/src/context.tsx b/packages/mtp/src/context.tsx index 53c1862..c07579f 100644 --- a/packages/mtp/src/context.tsx +++ b/packages/mtp/src/context.tsx @@ -1,1110 +1,41 @@ -import { - createContext, - type ReactNode, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { invoke, isTauri } from "@tauri-apps/api/core"; -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { type ReactNode, useContext, useEffect, useState } from "react"; +import { isTauri } from "@tauri-apps/api/core"; import { MTPClient } from "mtp"; -import { type z } from "zod"; -import { ConnectionState } from "mtp"; -import createAsyncQueue from "@tensamin/shared/asyncQueue"; -import { toast as sonnerToast } from "@methanium/ui"; -import { - type Calls, - type Communities, - type Contacts, - mtp as schemas, - type MTP as Schemas, -} from "@tensamin/shared/data"; -import { log } from "@tensamin/shared/log"; -import { ProtocolError } from "@tensamin/shared/errors"; -import { useStorage } from "@tensamin/storage/context"; - -import { fromWireMessage, toWireData } from "./protocolFields"; -import { RequestIdAllocator } from "./requestIds"; -import { - DISCOVERY_TIMEOUT, - INITIAL_SYNC_TIMEOUT, - RECONNECT_JITTER, - RECONNECT_LONG_INTERVAL, - RECONNECT_RESET, - RECONNECT_TRIES, - RETRY_INTERVAL, - STATE_ACK_TIMEOUT, -} from "./values"; - -type BrowserMtpClient = Awaited>; - -function base64ToUint8Array(b64: string) { - const bin = atob(b64); - const out = new Uint8Array(bin.length); - - for (let i = 0; i < bin.length; i++) { - out[i] = bin.charCodeAt(i); - } - - return out; -} - -export type ProtocolMessage< - T extends keyof Schemas & string = keyof Schemas & string, -> = { - id?: number; - type: T | string; - data: z.infer; -}; - -export type BoundSendFn = ( - type: T, - data?: z.infer, -) => Promise>; - -export type PushHandler = (message: ProtocolMessage) => void | Promise; - -const PUSH_TYPES = [ - "MessageLive", - "MessageEditLive", - "MessageReactionLive", - "MessageDeleteLive", - "MessageState", - "CallInvite", - "GetStates", - "ClientChanged", - "ErrorNoIota", -] as const; - -export function isPushType(type: string): boolean { - return (PUSH_TYPES as readonly string[]).includes(type); -} - -function normalizeMtpMessage(message: T): T { - return fromWireMessage(message); -} - -async function requestWithId( - client: BrowserMtpClient, - ids: RequestIdAllocator, - type: string, - data: Record, -) { - let id: number; - try { - id = ids.allocate(); - } catch (error) { - client.disconnect(); - throw error; - } - - return client.request(type, toWireData(data) as Record, { - id, - }); -} - -function removeMissingContacts( - contacts: Contacts, - message: ProtocolMessage, -): Contacts { - if (message.type !== "GetStates") return contacts; - const data = message.data as { MissingUserIds?: unknown }; - if (!Array.isArray(data.MissingUserIds)) return contacts; - const missing = new Set( - data.MissingUserIds.filter( - (userId): userId is number => typeof userId === "number", - ), - ); - return contacts.filter((contact) => !missing.has(contact.UserId)); -} - -export type MTPExchange = { - type: keyof Schemas & string; - data: unknown; - response: ProtocolMessage; -}; - -export type MTPInterceptor = (exchange: MTPExchange) => void | Promise; - -type ContextType = { - send: BoundSendFn; - subscribe: ( - type: T, - handler: (message: ProtocolMessage) => void, - ) => () => void; - subscribePush: (handler: PushHandler) => () => void; - addInterceptor: (interceptor: MTPInterceptor) => () => void; - readyState: number; - identified: boolean; - freshContacts: Contacts; - freshCommunities: Communities; - freshCalls: Calls; - contextReady: boolean; - loadingDescription: string; -}; - -const MTPContext = createContext(undefined); - -function getProtocolErrorDetails(error: unknown) { - if (typeof error !== "object" || error === null || !("type" in error)) { - return null; - } - - const protocolError = error as { - id?: unknown; - type?: unknown; - data?: unknown; - }; - return { - id: protocolError.id, - type: protocolError.type, - data: protocolError.data, - }; -} - -// Zod schema validation -export function validateResponse( - type: T, - message: { id?: number; type: string; data: unknown }, -): ProtocolMessage { - if (message.type.startsWith("Error")) { - return message as ProtocolMessage; - } - - const schema = - schemas[message.type as keyof Schemas & string]?.response ?? - schemas[type]?.response; - if (!schema) { - return message as ProtocolMessage; - } - - const parsed = schema.safeParse(message.data); - if (!parsed.success) { - throw new Error( - `Response validation failed for ${type}: ${parsed.error.message}`, - ); - } - - return { - id: message.id, - type: message.type, - data: parsed.data, - } as ProtocolMessage; -} - -function abortError(signal: AbortSignal): Error { - return signal.reason instanceof Error - ? signal.reason - : new Error("Initial state synchronization was cancelled"); -} - -function withDeadline( - promise: Promise, - timeoutMs: number, - timeoutMessage: string, - signal: AbortSignal, -): Promise { - return new Promise((resolve, reject) => { - if (signal.aborted) { - reject(abortError(signal)); - return; - } - let settled = false; - const finish = (complete: () => void) => { - if (settled) return; - settled = true; - clearTimeout(timeout); - signal.removeEventListener("abort", onAbort); - complete(); - }; - const timeout = setTimeout( - () => finish(() => reject(new Error(timeoutMessage))), - timeoutMs, - ); - const onAbort = () => finish(() => reject(abortError(signal))); - signal.addEventListener("abort", onAbort, { once: true }); - promise.then( - (value) => finish(() => resolve(value)), - (error: unknown) => finish(() => reject(error)), - ); - }); -} - -export async function completeInitialSynchronization( - client: BrowserMtpClient, - ids: RequestIdAllocator, - signal: AbortSignal, - syncTimeoutMs = INITIAL_SYNC_TIMEOUT, - ackTimeoutMs = STATE_ACK_TIMEOUT, -): Promise> { - const stateSync = new Promise>( - (resolve, reject) => { - let unsubscribeStateSync = () => {}; - let unsubscribeNoIota = () => {}; - const cleanup = () => { - clearTimeout(timeout); - unsubscribeStateSync(); - unsubscribeNoIota(); - signal.removeEventListener("abort", onAbort); - }; - const onAbort = () => { - cleanup(); - reject(abortError(signal)); - }; - const timeout = setTimeout(() => { - cleanup(); - reject(new Error("Initial state synchronization timed out")); - }, syncTimeoutMs); - signal.addEventListener("abort", onAbort, { once: true }); - if (signal.aborted) { - onAbort(); - return; - } - unsubscribeStateSync = client.subscribe("ClientStateSync", (message) => { - cleanup(); - try { - resolve( - validateResponse("ClientStateSync", normalizeMtpMessage(message)), - ); - } catch (error) { - reject(error); - } - }); - unsubscribeNoIota = client.subscribe("ErrorNoIota", () => { - cleanup(); - reject(new Error("No Iota is currently connected")); - }); - }, - ); - const [, state] = await Promise.all([ - withDeadline( - client.auth(), - syncTimeoutMs, - "MTP authentication timed out", - signal, - ), - stateSync, - ]); - if (state.type.startsWith("Error")) { - throw new Error(`State synchronization failed: ${state.type}`); - } - const acknowledgement = normalizeMtpMessage( - await withDeadline( - requestWithId(client, ids, "ClientStateAck", { - SessionId: state.data.SessionId, - VersionNumber: state.data.VersionNumber, - }), - ackTimeoutMs, - "State acknowledgement timed out", - signal, - ), - ); - if (acknowledgement.type.startsWith("Error")) { - throw new Error(`State acknowledgement failed: ${acknowledgement.type}`); - } - if (signal.aborted) throw abortError(signal); - return state; -} - -function useMessageHandlers() { - const interceptorsRef = useRef(new Set()); - const pushHandlersRef = useRef(new Set()); - const lastInitialStateRef = useRef(null); - const subscribePush = useCallback((handler: PushHandler) => { - pushHandlersRef.current.add(handler); - const initialState = lastInitialStateRef.current; - if (initialState?.type === "GetStates") { - void Promise.resolve(handler(initialState)).catch(() => undefined); - } - return () => pushHandlersRef.current.delete(handler); - }, []); - const addInterceptor = useCallback((interceptor: MTPInterceptor) => { - interceptorsRef.current.add(interceptor); - return () => interceptorsRef.current.delete(interceptor); - }, []); - return { - addInterceptor, - interceptorsRef, - lastInitialStateRef, - pushHandlersRef, - subscribePush, - }; -} - -function BrowserProvider(props: { - children: ReactNode; - blockConnection?: boolean; -}) { - const { load } = useStorage(); - - const [readyState, setReadyState] = useState( - ConnectionState.Disconnected, - ); - const [identified, setIdentified] = useState(false); - const [identifying, setIdentifying] = useState(false); - - const [freshCommunities, setFreshCommunities] = useState([]); - const [freshContacts, setFreshContacts] = useState([]); - const [freshCalls, setFreshCalls] = useState([]); - - const clientRef = useRef> | null>( - null, - ); - const requestIdsRef = useRef(null); - const { - addInterceptor, - interceptorsRef, - lastInitialStateRef, - pushHandlersRef, - subscribePush, - } = useMessageHandlers(); - - const connected = readyState === ConnectionState.Connected; - - // MTP url - const [mtpUrl, setMtpUrl] = useState(null); - useEffect(() => { - load("omega_url").then(setMtpUrl); - }, [load]); - - // Validation override functions - const send: BoundSendFn = useMemo( - () => async (type, data) => { - const client = clientRef.current; - const ids = requestIdsRef.current; - - if (!client) { - throw new Error("mtp is not connected"); - } - if (!ids) { - throw new Error("MTP request allocator is unavailable"); - } - - const rawMessage = await requestWithId( - client, - ids, - type, - (data ?? {}) as Record, - ); - const response = validateResponse(type, normalizeMtpMessage(rawMessage)); - setFreshContacts((contacts) => removeMissingContacts(contacts, response)); - if (response.type.startsWith("Error")) { - const errorData = response.data as Record; - throw new ProtocolError({ - type: response.type, - requestId: response.id, - errorType: - typeof errorData.ErrorType === "string" - ? errorData.ErrorType - : undefined, - }); - } - return response; - }, - [], - ); - - const subscribe = useCallback((type, handler) => { - const client = clientRef.current; - if (!client) { - return () => {}; - } - - return client.subscribe(type, (message) => { - handler(validateResponse(type, normalizeMtpMessage(message))); - }); - }, []); - - // Reconnect stuff - const resolveConnectionRef = useRef(() => {}); - useEffect(() => { - if (!mtpUrl) return; - - let attempts = 0; - let reconnectTimer: ReturnType | null = null; - let reconnectResetTimer: ReturnType | null = null; - let reconnectScheduled = false; - let disposed = false; - let connectionGeneration = 0; - - const clearReconnectTimer = () => { - if (!reconnectTimer) return; - clearTimeout(reconnectTimer); - reconnectTimer = null; - reconnectScheduled = false; - }; - - const clearReconnectResetTimer = () => { - if (!reconnectResetTimer) return; - clearTimeout(reconnectResetTimer); - reconnectResetTimer = null; - }; - - const scheduleReconnect = (error: unknown) => { - if (disposed || reconnectScheduled) return; - attempts += 1; - const shortRetry = attempts <= RECONNECT_TRIES; - if (!shortRetry) { - log(0, "mtp", "red", "Reconnection attempts exhausted", error); - sonnerToast.error("Connection failed", { - id: "mtp-connection-toast", - description: - error instanceof Error - ? `${error.message.split(":")[0]}. Retrying in the background.` - : "Connection lost. Retrying in the background.", - icon: null, - duration: Infinity, - closeButton: true, - promise: null, - } as unknown as Parameters[1]); - } else { - sonnerToast.loading( - `Reconnecting to server... (attempt ${attempts} of ${RECONNECT_TRIES})`, - { id: "mtp-connection-toast" }, - ); - } - - const baseDelay = shortRetry ? RETRY_INTERVAL : RECONNECT_LONG_INTERVAL; - const jitter = 1 + (Math.random() * 2 - 1) * RECONNECT_JITTER; - reconnectScheduled = true; - reconnectTimer = setTimeout( - () => { - reconnectScheduled = false; - reconnectTimer = null; - void connect(); - }, - Math.round(baseDelay * jitter), - ); - }; - - async function connect() { - if (disposed || props.blockConnection) return; - - const generation = ++connectionGeneration; - let client: Awaited> | null = null; - let failed = false; - let connectionReady = false; - const attemptAbort = new AbortController(); - const cleanup = () => { - attemptAbort.abort( - new Error("Initial state synchronization was cancelled"), - ); - client?.disconnect(); - if (clientRef.current === client) { - clientRef.current = null; - requestIdsRef.current = null; - } - clearReconnectResetTimer(); - if (generation === connectionGeneration) { - setReadyState(ConnectionState.Disconnected); - setIdentified(false); - setIdentifying(false); - } - }; - try { - setIdentified(false); - setIdentifying(false); - - const [userId, keyring] = await Promise.all([ - load("user_id"), - load("mtp_keyring"), - ]); - if (!userId || !keyring) { - throw new Error("Missing login credentials"); - } - const forcedOmikronUrl = await load("forced_omikron_url"); - const forcedOmikronPublicKey = await load("forced_omikron_public_key"); - - let url = null; - let omikronPublicKey = null; - if (forcedOmikronUrl && forcedOmikronPublicKey) { - url = forcedOmikronUrl; - omikronPublicKey = forcedOmikronPublicKey; - } else { - log(2, "mtp", "purple", "Fetching Omikron data."); - const data = await fetch(`${mtpUrl}api/get/omikron/${userId}`, { - signal: AbortSignal.any([ - attemptAbort.signal, - AbortSignal.timeout(DISCOVERY_TIMEOUT), - ]), - }); - - if (data.status === 404) { - throw new Error("No Omikron assignment is currently available"); - } - if (!data.ok) { - throw new Error(`Omikron discovery failed: HTTP ${data.status}`); - } - const omikronData = (await data.json()) as { - id: number; - ip_address: string; - port: number; - public_key: string; - status: string; - }; - - if ( - !omikronData.ip_address || - !omikronData.port || - !omikronData.public_key - ) - throw new Error("Invalid Omikron data"); - - url = `https://${omikronData.ip_address}:${omikronData.port}`; - omikronPublicKey = omikronData.public_key; - } - //codec.decode(new Uint8Array(await res.arrayBuffer())), - - if (!url || !omikronPublicKey) - throw new Error("Missing Omikron URL or Public Key"); - - log(2, "mtp", "green", "Connecting to: " + url); - - client = await MTPClient.create({ - url, - credentials: { - clientId: userId, - keyring: base64ToUint8Array(keyring), - }, - hostPublicKey: { - value: omikronPublicKey, - encoding: "base64", - }, - descriptor: "client", - pings: true, - logger: (event) => { - if (event.type === "state") { - if (generation !== connectionGeneration) return; - const state = client?.state ?? ConnectionState.Disconnected; - setReadyState(state); - if ( - state === ConnectionState.Disconnected && - clientRef.current === client && - !failed - ) { - failed = true; - const error = new Error("MTP connection lost"); - attemptAbort.abort(error); - if (connectionReady) { - cleanup(); - scheduleReconnect(error); - } - } - } - - if (event.type !== "Pong" && event.type !== "Ping") { - log( - 2, - "mtp", - event.type === "state" - ? "purple" - : event.direction === "recv" - ? "cyan" - : event.direction === "send" - ? "gray" - : "blue", - event.type === "state" - ? event.data - : event.direction === "recv" - ? "< " + event.type - : event.direction === "send" - ? "> " + event.type - : event.type, - event, - ); - } - }, - }); - - if (disposed || generation !== connectionGeneration) { - client.disconnect(); - return; - } - const activeClient = client; - - clientRef.current = activeClient; - requestIdsRef.current = new RequestIdAllocator(); - for (const type of PUSH_TYPES) { - activeClient.subscribe(type, (message) => { - let validated: ProtocolMessage; - try { - validated = validateResponse(type, normalizeMtpMessage(message)); - } catch (error) { - log(1, "mtp", "red", "Failed to validate push message", error, { - type, - data: message.data, - }); - return; - } - - setFreshContacts((contacts) => - removeMissingContacts(contacts, validated), - ); - for (const handler of [...pushHandlersRef.current]) { - void Promise.resolve() - .then(() => handler(validated)) - .catch((error) => { - log(1, "mtp", "red", "Push handler failed", error, { type }); - }); - } - if (validated.type === "GetStates") { - lastInitialStateRef.current = validated; - } - if ( - validated.type === "ErrorNoIota" && - clientRef.current === activeClient && - !failed - ) { - failed = true; - const error = new Error("No Iota is currently connected"); - attemptAbort.abort(error); - cleanup(); - scheduleReconnect(error); - } - }); - } - setReadyState(activeClient.state); - setIdentifying(true); - - const ids = requestIdsRef.current; - if (!ids) throw new Error("MTP request allocator is unavailable"); - const finalResponse = await completeInitialSynchronization( - activeClient, - ids, - attemptAbort.signal, - ); - - if (disposed || clientRef.current !== activeClient) return; - - setFreshContacts(finalResponse.data.Contacts); - setFreshCommunities(finalResponse.data.Communities); - setFreshCalls(finalResponse.data.Calls); - connectionReady = true; - setIdentifying(false); - setIdentified(true); - - clearReconnectTimer(); - clearReconnectResetTimer(); - reconnectResetTimer = setTimeout(() => { - attempts = 0; - reconnectResetTimer = null; - }, RECONNECT_RESET * 1_000); - resolveConnectionRef.current?.(); - } catch (connectError) { - if (disposed || generation !== connectionGeneration) { - client?.disconnect(); - return; - } - failed = true; - cleanup(); - const connectErrorMessage = - connectError instanceof Error - ? connectError.message - : String(connectError ?? "Unknown error"); - log( - 0, - "mtp", - "red", - `Connection/authentication attempt failed: ${connectErrorMessage}`, - getProtocolErrorDetails(connectError) ?? connectError, - ); - - scheduleReconnect(connectError); - } - } - - void connect(); - - return () => { - disposed = true; - clearReconnectTimer(); - clearReconnectResetTimer(); - - clientRef.current?.disconnect(); - clientRef.current = null; - requestIdsRef.current = null; - setReadyState(ConnectionState.Disconnected); - setIdentified(false); - setIdentifying(false); - sonnerToast.dismiss("mtp-connection-toast"); - }; - }, [ - lastInitialStateRef, - mtpUrl, - props.blockConnection, - load, - pushHandlersRef, - ]); - - // No Iota check - useEffect(() => { - if (!connected) return; - - return subscribe("ErrorNoIota", () => { - setIdentified(false); - setIdentifying(false); - sonnerToast.error("We couldn't reach your Iota", { - description: - "Check your network connection and try restarting your Iota", - icon: null, - duration: Infinity, - closeButton: true, - }); - resolveConnectionRef.current?.(); - }); - }, [connected, subscribe]); - - // Async queue - const loadingDescription = useMemo(() => { - if (!mtpUrl) return "Loading connection details"; - if (readyState === ConnectionState.Connecting || !connected) { - return "Establishing transport channel"; - } - if (identifying || !identified) return "Waiting for authenticated session"; - return "Loading..."; - }, [connected, identified, identifying, readyState, mtpUrl]); - const contextReady = connected && identified && mtpUrl !== null; - const mtpRef = useMemo( - () => - createAsyncQueue<{ - send: typeof send; - subscribe: typeof subscribe; - subscribePush: typeof subscribePush; - }>(), - [], - ); - useEffect(() => { - if (connected && identified && mtpUrl) { - mtpRef.set({ - send, - subscribe, - subscribePush, - }); - } - }, [connected, identified, mtpUrl, send, subscribe, subscribePush, mtpRef]); - - const sendQueued: BoundSendFn = useMemo( - () => async (type, data) => { - const mtp = await mtpRef.get(); - const response = await mtp.send(type, data); - for (const interceptor of interceptorsRef.current) { - void Promise.resolve( - interceptor({ type, data, response: response as ProtocolMessage }), - ).catch((error) => { - log(1, "mtp", "yellow", "MTP interceptor failed", error, { type }); - }); - } - return response; - }, - [interceptorsRef, mtpRef], - ); - - return ( - - {props.children} - - ); -} - -type NativeSnapshot = { - generation: number; - readyState: number; - identified: boolean; - state?: unknown; - error?: string; -}; - -function TauriProvider(props: { - children: ReactNode; - blockConnection?: boolean; -}) { - const [snapshot, setSnapshot] = useState({ - generation: 0, - readyState: ConnectionState.Disconnected, - identified: false, - }); - const [freshContacts, setFreshContacts] = useState([]); - const [freshCommunities, setFreshCommunities] = useState([]); - const [freshCalls, setFreshCalls] = useState([]); - const generationRef = useRef(0); - const { - addInterceptor, - interceptorsRef, - lastInitialStateRef, - pushHandlersRef, - subscribePush, - } = useMessageHandlers(); - const subscriptionsRef = useRef( - new Map void>>(), - ); - - const applySnapshot = useCallback((next: NativeSnapshot) => { - if (next.generation < generationRef.current) return; - generationRef.current = next.generation; - if (next.error) { - log(0, "android", "orange", "MTP connection failed", next.error); - } - if (!next.identified) { - setSnapshot(next); - return; - } - if (next.state === undefined) { - setSnapshot({ - ...next, - identified: false, - error: "Native MTP connection omitted initial state", - }); - return; - } - const parsed = schemas.ClientStateSync.response.safeParse(next.state); - if (!parsed.success) { - log(0, "mtp", "red", "Invalid native MTP state", parsed.error); - setSnapshot({ - ...next, - identified: false, - error: "Invalid ClientStateSync payload", - }); - return; - } - setFreshContacts(parsed.data.Contacts); - setFreshCommunities(parsed.data.Communities); - setFreshCalls(parsed.data.Calls); - setSnapshot(next); - }, []); - - const dispatchMessage = useCallback( - (raw: unknown) => { - if (!raw || typeof raw !== "object" || !("type" in raw)) return; - const message = raw as { id?: number; type: string; data: unknown }; - let validated: ProtocolMessage; - try { - validated = validateResponse( - message.type as keyof Schemas & string, - normalizeMtpMessage(message), - ); - } catch (error) { - log(1, "mtp", "red", "Failed to validate native MTP message", error); - return; - } - for (const handler of subscriptionsRef.current.get(validated.type) ?? - []) { - handler(validated); - } - if (!isPushType(validated.type)) return; - setFreshContacts((contacts) => - removeMissingContacts(contacts, validated), - ); - for (const handler of [...pushHandlersRef.current]) { - void Promise.resolve(handler(validated)).catch((error) => { - log(1, "mtp", "red", "Native MTP push handler failed", error, { - type: validated.type, - }); - }); - } - if (validated.type === "GetStates") { - lastInitialStateRef.current = validated; - } - }, - [lastInitialStateRef, pushHandlersRef], - ); - - useEffect(() => { - if (props.blockConnection) return; - let disposed = false; - let unlisten: UnlistenFn | undefined; - void (async () => { - try { - const nextUnlisten = await listen< - | { kind: "state"; snapshot: NativeSnapshot } - | { kind: "message"; generation: number; message: unknown } - | { - kind: "log"; - level: number; - message: string; - details?: unknown; - } - >("mtp://event", ({ payload }) => { - if (disposed) return; - if (payload.kind === "state") { - applySnapshot(payload.snapshot); - return; - } - if (payload.kind === "message") { - if (payload.generation === generationRef.current) { - dispatchMessage(payload.message); - } - return; - } - log( - payload.level, - "android", - "orange", - payload.message, - payload.details, - ); - }); - if (disposed) nextUnlisten(); - else unlisten = nextUnlisten; - } catch (error) { - log(0, "mtp", "red", "Failed to subscribe to native MTP events", error); - } - - try { - const current = await invoke("mtp_status"); - if (!disposed) applySnapshot(current); - } catch (error) { - log(0, "mtp", "red", "Failed to load native MTP status", error); - } - })(); - return () => { - disposed = true; - unlisten?.(); - }; - }, [applySnapshot, dispatchMessage, props.blockConnection]); - - useEffect(() => { - if (props.blockConnection) return; - const updateVisibility = () => { - void invoke("mtp_set_ui_visible", { - visible: document.visibilityState === "visible" && document.hasFocus(), - }); - }; - updateVisibility(); - document.addEventListener("visibilitychange", updateVisibility); - window.addEventListener("focus", updateVisibility); - window.addEventListener("blur", updateVisibility); - return () => { - document.removeEventListener("visibilitychange", updateVisibility); - window.removeEventListener("focus", updateVisibility); - window.removeEventListener("blur", updateVisibility); - void invoke("mtp_set_ui_visible", { visible: false }); - }; - }, [props.blockConnection]); - - const send = useCallback( - async (type, data) => { - const response = await invoke("mtp_request", { - typeName: type, - data: data ?? {}, - }); - const validated = validateResponse(type, normalizeMtpMessage(response)); - setFreshContacts((contacts) => - removeMissingContacts(contacts, validated), - ); - if (validated.type.startsWith("Error")) { - const errorData = validated.data as Record; - throw new ProtocolError({ - type: validated.type, - requestId: validated.id, - errorType: - typeof errorData.ErrorType === "string" - ? errorData.ErrorType - : undefined, - }); - } - for (const interceptor of interceptorsRef.current) { - void Promise.resolve( - interceptor({ type, data, response: validated as ProtocolMessage }), - ).catch((error) => { - log(1, "mtp", "yellow", "MTP interceptor failed", error, { type }); - }); - } - return validated; - }, - [interceptorsRef], - ); - - const subscribe = useCallback((type, handler) => { - const handlers = - subscriptionsRef.current.get(type) ?? - new Set<(message: ProtocolMessage) => void>(); - handlers.add(handler as (message: ProtocolMessage) => void); - subscriptionsRef.current.set(type, handlers); - return () => { - handlers.delete(handler as (message: ProtocolMessage) => void); - if (handlers.size === 0) subscriptionsRef.current.delete(type); - }; - }, []); - const connected = snapshot.readyState === ConnectionState.Connected; - const contextReady = connected && snapshot.identified; - - return ( - - {props.children} - - ); -} +import { BrowserProvider } from "./browser"; +import { MTPContext, type MTPContextType } from "./mtpContext"; +import { TauriProvider } from "./tauri"; export function Provider(props: { children: ReactNode; blockConnection?: boolean; +}) { + if (isTauri()) return ; + return ; +} + +function BrowserWasmProvider(props: { + children: ReactNode; + blockConnection?: boolean; }) { const [wasmReady, setWasmReady] = useState(false); const [wasmError, setWasmError] = useState(); - useEffect(() => { let active = true; void MTPClient.init().then( - () => { - if (active) setWasmReady(true); - }, - (error: unknown) => { - if (active) setWasmError(() => error); - }, + () => active && setWasmReady(true), + (error: unknown) => active && setWasmError(() => error), ); return () => { active = false; }; }, []); - if (wasmError) throw wasmError; - if (!wasmReady) return null; - - return isTauri() ? ( - - ) : ( - - ); + return wasmReady ? : null; } -export function useMTP(): ContextType { +export function useMTP(): MTPContextType { const context = useContext(MTPContext); - if (!context) { - throw new Error("useMTP must be used within an MTPProvider"); - } + if (!context) throw new Error("useMTP must be used within an MTPProvider"); return context; } diff --git a/packages/mtp/src/index.ts b/packages/mtp/src/index.ts index 19f5acd..a4f05c8 100644 --- a/packages/mtp/src/index.ts +++ b/packages/mtp/src/index.ts @@ -1,9 +1,7 @@ export { Provider, useMTP } from "./context"; -export { RequestIdAllocator } from "./requestIds"; export type { BoundSendFn, MTPExchange, MTPInterceptor, - PushHandler, ProtocolMessage, -} from "./context"; +} from "./mtpContext"; diff --git a/packages/mtp/src/mtpContext.tsx b/packages/mtp/src/mtpContext.tsx new file mode 100644 index 0000000..1f1654d --- /dev/null +++ b/packages/mtp/src/mtpContext.tsx @@ -0,0 +1,159 @@ +import { createContext, useCallback, useRef } from "react"; +import type { + MTPRequestFunction, + MTPResponseFrame, + MTPSubscriptionFunction, +} from "mtp"; +import { + mtp as mtpSchemas, + type Calls, + type Communities, + type Contacts, +} from "@tensamin/shared/data"; +import { log } from "@tensamin/shared/log"; + +export type ProtocolMessage< + Type extends keyof typeof mtpSchemas & string = keyof typeof mtpSchemas & + string, +> = MTPResponseFrame; + +export type BoundSendFn = MTPRequestFunction; + +export type MTPExchange = { + type: keyof typeof mtpSchemas & string; + data: unknown; + response: ProtocolMessage; +}; + +export type MTPInterceptor = (exchange: MTPExchange) => void | Promise; + +export type MTPContextType = { + send: BoundSendFn; + subscribe: MTPSubscriptionFunction; + addInterceptor: (interceptor: MTPInterceptor) => () => void; + readyState: number; + identified: boolean; + freshContacts: Contacts; + freshCommunities: Communities; + freshCalls: Calls; + contextReady: boolean; + loadingDescription: string; +}; + +export const MTPContext = createContext(undefined); + +export function removeMissingContacts( + contacts: Contacts, + message: ProtocolMessage<"GetStates">, +): Contacts { + const missing = new Set(message.data.MissingUserIds ?? []); + return contacts.filter((contact) => !missing.has(contact.UserId)); +} + +export function useMessageHandlers() { + const interceptorsRef = useRef(new Set()); + const subscriptionHandlersRef = useRef( + new Map void | Promise>>(), + ); + const transportRef = useRef<{ + subscribe: MTPSubscriptionFunction; + } | null>(null); + const transportGenerationRef = useRef(0); + const transportUnsubscribersRef = useRef(new Map void>()); + const lastInitialStateRef = useRef | null>(null); + + const attachType = useCallback( + (type: Type) => { + const transport = transportRef.current; + if (!transport || transportUnsubscribersRef.current.has(type)) return; + const generation = transportGenerationRef.current; + const unsubscribe = transport.subscribe(type, (message) => { + if ( + transportRef.current !== transport || + transportGenerationRef.current !== generation + ) + return; + if (type === "GetStates") { + lastInitialStateRef.current = message as ProtocolMessage<"GetStates">; + } + for (const handler of [ + ...(subscriptionHandlersRef.current.get(type) ?? []), + ]) { + void Promise.resolve(handler(message as ProtocolMessage)).catch( + (error) => { + log(1, "mtp", "red", "Subscription handler failed", error, { + type, + }); + }, + ); + } + }); + transportUnsubscribersRef.current.set(type, unsubscribe); + }, + [], + ); + + const attachSubscriptions = useCallback( + (transport: { subscribe: MTPSubscriptionFunction }) => { + for (const unsubscribe of transportUnsubscribersRef.current.values()) { + unsubscribe(); + } + transportUnsubscribersRef.current.clear(); + transportRef.current = transport; + const generation = ++transportGenerationRef.current; + for (const type of subscriptionHandlersRef.current.keys()) { + attachType(type as keyof typeof mtpSchemas & string); + } + return () => { + if ( + transportRef.current !== transport || + transportGenerationRef.current !== generation + ) + return; + transportRef.current = null; + transportGenerationRef.current += 1; + for (const unsubscribe of transportUnsubscribersRef.current.values()) { + unsubscribe(); + } + transportUnsubscribersRef.current.clear(); + }; + }, + [attachType], + ); + + const subscribe = useCallback>( + (type, handler) => { + const handlers = subscriptionHandlersRef.current.get(type) ?? new Set(); + const untypedHandler = handler as ( + message: ProtocolMessage, + ) => void | Promise; + handlers.add(untypedHandler); + subscriptionHandlersRef.current.set(type, handlers); + attachType(type); + const initialState = lastInitialStateRef.current; + if (type === "GetStates" && initialState) { + void Promise.resolve(untypedHandler(initialState)).catch( + () => undefined, + ); + } + return () => { + handlers.delete(untypedHandler); + if (handlers.size !== 0) return; + subscriptionHandlersRef.current.delete(type); + transportUnsubscribersRef.current.get(type)?.(); + transportUnsubscribersRef.current.delete(type); + }; + }, + [attachType], + ); + const addInterceptor = useCallback((interceptor: MTPInterceptor) => { + interceptorsRef.current.add(interceptor); + return () => interceptorsRef.current.delete(interceptor); + }, []); + return { + addInterceptor, + attachSubscriptions, + interceptorsRef, + subscribe, + }; +} diff --git a/packages/mtp/src/protocolFields.test.ts b/packages/mtp/src/protocolFields.test.ts deleted file mode 100644 index ef0c18f..0000000 --- a/packages/mtp/src/protocolFields.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { fromWireData, toWireData } from "./protocolFields"; - -describe("MTP protocol field translation", () => { - it("maps application message fields to MTP wire fields", () => { - expect( - toWireData({ - Messages: [{ Content: "abc", MessageId: 4 }], - }), - ).toEqual({ - Messages: [{ AppContent: "abc", AppMessageId: 4 }], - }); - }); - - it("maps MTP wire fields back to application fields", () => { - expect( - fromWireData({ - AppCreatedAt: 123, - Message: { AppContent: "abc", AppMessageId: 4 }, - }), - ).toEqual({ - CreatedAt: 123, - Message: { Content: "abc", MessageId: 4 }, - }); - }); - - it("preserves byte arrays and unrelated fields", () => { - const bytes = new Uint8Array([1, 2, 3]); - const translated = toWireData({ Payload: bytes, Other: "value" }) as { - Payload: Uint8Array; - Other: string; - }; - - expect(translated.Payload).toBe(bytes); - expect(translated.Other).toBe("value"); - }); - - it("rejects field mapping collisions", () => { - expect(() => toWireData({ Content: "a", AppContent: "b" })).toThrow( - "MTP field translation collision", - ); - }); -}); diff --git a/packages/mtp/src/protocolFields.ts b/packages/mtp/src/protocolFields.ts deleted file mode 100644 index 3ec3d5d..0000000 --- a/packages/mtp/src/protocolFields.ts +++ /dev/null @@ -1,58 +0,0 @@ -const APPLICATION_TO_WIRE_FIELDS = { - Content: "AppContent", - CreatedAt: "AppCreatedAt", - MessageId: "AppMessageId", -} as const; - -const WIRE_TO_APPLICATION_FIELDS = { - AppContent: "Content", - AppCreatedAt: "CreatedAt", - AppMessageId: "MessageId", -} as const; - -function mapProtocolFields( - value: unknown, - fieldMap: Readonly>, -): unknown { - if ( - value === null || - typeof value !== "object" || - value instanceof Uint8Array - ) { - return value; - } - - if (Array.isArray(value)) { - return value.map((item) => mapProtocolFields(item, fieldMap)); - } - - const source = value as Record; - const target: Record = {}; - - for (const [key, child] of Object.entries(source)) { - const mappedKey = fieldMap[key] ?? key; - - if (mappedKey in target) { - throw new Error(`MTP field translation collision for ${mappedKey}`); - } - - target[mappedKey] = mapProtocolFields(child, fieldMap); - } - - return target; -} - -export function toWireData(value: unknown): unknown { - return mapProtocolFields(value, APPLICATION_TO_WIRE_FIELDS); -} - -export function fromWireData(value: unknown): unknown { - return mapProtocolFields(value, WIRE_TO_APPLICATION_FIELDS); -} - -export function fromWireMessage(message: T): T { - return { - ...message, - data: fromWireData(message.data), - }; -} diff --git a/packages/mtp/src/requestIds.test.ts b/packages/mtp/src/requestIds.test.ts deleted file mode 100644 index 82fd508..0000000 --- a/packages/mtp/src/requestIds.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { RequestIdAllocator } from "./requestIds"; - -describe("MTP request ID allocation", () => { - it("allocates nonzero request IDs monotonically", () => { - const ids = new RequestIdAllocator(); - - expect(ids.allocate()).toBe(1); - expect(ids.allocate()).toBe(2); - expect(new RequestIdAllocator().allocate()).toBe(1); - }); - - it("does not wrap exhausted request IDs", () => { - const ids = new RequestIdAllocator(0x1_0000_0000); - - expect(() => ids.allocate()).toThrow("MTP request ID space exhausted"); - }); - - it("rejects invalid allocator states", () => { - expect(() => new RequestIdAllocator(0)).toThrow( - "invalid MTP request ID allocator state", - ); - }); -}); diff --git a/packages/mtp/src/requestIds.ts b/packages/mtp/src/requestIds.ts deleted file mode 100644 index 5581e8a..0000000 --- a/packages/mtp/src/requestIds.ts +++ /dev/null @@ -1,24 +0,0 @@ -const MAX_MTP_REQUEST_ID = 0xffff_ffff; - -export class RequestIdAllocator { - #next: number; - - constructor(next = 1) { - if ( - !Number.isSafeInteger(next) || - next <= 0 || - next > MAX_MTP_REQUEST_ID + 1 - ) { - throw new RangeError("invalid MTP request ID allocator state"); - } - this.#next = next; - } - - allocate(): number { - if (this.#next > MAX_MTP_REQUEST_ID) { - throw new Error("MTP request ID space exhausted for this connection"); - } - - return this.#next++; - } -} diff --git a/packages/mtp/src/tauri.tsx b/packages/mtp/src/tauri.tsx new file mode 100644 index 0000000..92ac209 --- /dev/null +++ b/packages/mtp/src/tauri.tsx @@ -0,0 +1,258 @@ +import { + type ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { + ConnectionState, + MTPProxyConnection, + type MTPFrame, + type MTPProxyAdapter, +} from "mtp"; +import { + mtp as mtpSchemas, + type Calls, + type Communities, + type Contacts, +} from "@tensamin/shared/data"; +import { log } from "@tensamin/shared/log"; + +import { + type BoundSendFn, + MTPContext, + type ProtocolMessage, + removeMissingContacts, + useMessageHandlers, +} from "./mtpContext"; + +type NativeSnapshot = { + generation: number; + readyState: number; + identified: boolean; + state?: unknown; + error?: string; +}; + +function createTauriAdapter() { + const subscriptions = new Map void>>(); + const adapter: MTPProxyAdapter = { + request: (type, data) => + invoke("mtp_request", { typeName: type, data }), + subscribe(type, handler) { + const handlers = subscriptions.get(type) ?? new Set(); + handlers.add(handler); + subscriptions.set(type, handlers); + return () => { + handlers.delete(handler); + if (handlers.size === 0) subscriptions.delete(type); + }; + }, + }; + return { + adapter, + dispatch(message: MTPFrame) { + for (const handler of subscriptions.get(message.type) ?? []) + handler(message); + }, + }; +} + +export function TauriProvider(props: { + children: ReactNode; + blockConnection?: boolean; +}) { + const [snapshot, setSnapshot] = useState({ + generation: 0, + readyState: ConnectionState.Disconnected, + identified: false, + }); + const [freshContacts, setFreshContacts] = useState([]); + const [freshCommunities, setFreshCommunities] = useState([]); + const [freshCalls, setFreshCalls] = useState([]); + const generationRef = useRef(0); + const { addInterceptor, attachSubscriptions, interceptorsRef, subscribe } = + useMessageHandlers(); + const [{ bridge, connection }] = useState(() => { + const bridge = createTauriAdapter(); + return { + bridge, + connection: new MTPProxyConnection(bridge.adapter, { + schemas: mtpSchemas, + throwProtocolErrors: true, + onValidationError: (error) => { + log(1, "mtp", "red", "Failed to validate native MTP message", error); + }, + }), + }; + }); + + const applySnapshot = useCallback(async (next: NativeSnapshot) => { + if (next.generation < generationRef.current) return; + generationRef.current = next.generation; + if (next.error) + log(0, "android", "orange", "MTP connection failed", next.error); + if (!next.identified) { + setSnapshot(next); + return; + } + if (next.state === undefined) { + setSnapshot({ + ...next, + identified: false, + error: "Native MTP connection omitted initial state", + }); + return; + } + try { + const state = await mtpSchemas.ClientStateSync.response.parseAsync( + next.state, + ); + setFreshContacts(state.Contacts); + setFreshCommunities(state.Communities); + setFreshCalls(state.Calls); + setSnapshot(next); + } catch (error) { + log(0, "mtp", "red", "Invalid native MTP state", error); + setSnapshot({ + ...next, + identified: false, + error: "Invalid ClientStateSync payload", + }); + } + }, []); + + const dispatchMessage = useCallback( + (message: MTPFrame) => { + bridge.dispatch(message); + }, + [bridge], + ); + + useEffect(() => { + return attachSubscriptions(connection); + }, [attachSubscriptions, connection]); + + useEffect( + () => + subscribe("GetStates", (message) => { + setFreshContacts((contacts) => + removeMissingContacts(contacts, message), + ); + }), + [subscribe], + ); + + useEffect(() => { + if (props.blockConnection) return; + let disposed = false; + let unlisten: UnlistenFn | undefined; + void (async () => { + try { + const nextUnlisten = await listen< + | { kind: "state"; snapshot: NativeSnapshot } + | { kind: "message"; generation: number; message: MTPFrame } + | { kind: "log"; level: number; message: string; details?: unknown } + >("mtp://event", ({ payload }) => { + if (disposed) return; + if (payload.kind === "state") { + void applySnapshot(payload.snapshot); + } else if (payload.kind === "message") { + if (payload.generation === generationRef.current) { + dispatchMessage(payload.message); + } + } else { + log( + payload.level, + "android", + "orange", + payload.message, + payload.details, + ); + } + }); + if (disposed) nextUnlisten(); + else unlisten = nextUnlisten; + } catch (error) { + log(0, "mtp", "red", "Failed to subscribe to native MTP events", error); + } + try { + const current = await invoke("mtp_status"); + if (!disposed) await applySnapshot(current); + } catch (error) { + log(0, "mtp", "red", "Failed to load native MTP status", error); + } + })(); + return () => { + disposed = true; + unlisten?.(); + }; + }, [applySnapshot, dispatchMessage, props.blockConnection]); + + useEffect(() => { + if (props.blockConnection) return; + const updateVisibility = () => { + void invoke("mtp_set_ui_visible", { + visible: document.visibilityState === "visible" && document.hasFocus(), + }); + }; + updateVisibility(); + document.addEventListener("visibilitychange", updateVisibility); + window.addEventListener("focus", updateVisibility); + window.addEventListener("blur", updateVisibility); + return () => { + document.removeEventListener("visibilitychange", updateVisibility); + window.removeEventListener("focus", updateVisibility); + window.removeEventListener("blur", updateVisibility); + void invoke("mtp_set_ui_visible", { visible: false }); + }; + }, [props.blockConnection]); + + const send = useCallback( + async (type, data, options) => { + const response = await connection.request(type, data, options); + if (response.type === "GetStates") { + setFreshContacts((contacts) => + removeMissingContacts( + contacts, + response as ProtocolMessage<"GetStates">, + ), + ); + } + for (const interceptor of interceptorsRef.current) { + void Promise.resolve(interceptor({ type, data, response })).catch( + (error) => { + log(1, "mtp", "yellow", "MTP interceptor failed", error, { type }); + }, + ); + } + return response; + }, + [connection, interceptorsRef], + ); + const connected = snapshot.readyState === ConnectionState.Connected; + + return ( + + {props.children} + + ); +} diff --git a/packages/notifications/src/context.tsx b/packages/notifications/src/context.tsx index 7a8b32a..e0da277 100644 --- a/packages/notifications/src/context.tsx +++ b/packages/notifications/src/context.tsx @@ -16,7 +16,6 @@ import { useLocation, useNavigate } from "@tanstack/react-router"; import { decryptChatText } from "@tensamin/crypto/chatSecret"; import { log } from "@tensamin/shared/log"; import { playSound } from "@tensamin/shared/sounds"; -import { type RawMessage } from "@tensamin/chat/values"; export const context = createContext(undefined); @@ -30,7 +29,7 @@ async function requestNotificationPermission() { } export default function Provider(props: { children: React.ReactNode }) { - const { subscribePush, send } = useMTP(); + const { subscribe, send } = useMTP(); const { load } = useStorage(); const { get } = useUser(); const { addLiveMessage, chatSecret, getChatSecret, userId } = useChat(); @@ -39,148 +38,140 @@ export default function Provider(props: { children: React.ReactNode }) { const location = useLocation(); useEffect(() => { - return subscribePush(async (message) => { - if (message.type === "MessageLive") { - const data = message.data as { - Message?: RawMessage; - SenderId?: number; - }; + return subscribe("MessageLive", async ({ data }) => { + if (!data.SenderId) return; - if (!data.SenderId) return; + const isCurrentChat = + location.pathname === "/chat" && userId === data.SenderId; + const appFocused = + document.hasFocus() && document.visibilityState === "visible"; + const shouldAlert = !isCurrentChat || !appFocused; - const isCurrentChat = - location.pathname === "/chat" && userId === data.SenderId; - const appFocused = - document.hasFocus() && document.visibilityState === "visible"; - const shouldAlert = !isCurrentChat || !appFocused; + const messageSecret = + isCurrentChat && chatSecret + ? chatSecret + : await getChatSecret(data.SenderId); - const messageSecret = - isCurrentChat && chatSecret - ? chatSecret - : await getChatSecret(data.SenderId); + if (!data.Message || !messageSecret) return; + if (shouldAlert) playSound("message"); - if (!data.Message || !messageSecret) return; - if (shouldAlert) playSound("message"); + void decryptChatText(messageSecret, data.Message.Content) + .catch((err) => { + log(1, "chat", "red", "Failed to decrypt live message", err, { + SendTime: data.Message?.SendTime, + }); + return null; + }) + .then(async (content) => { + if (!data.Message || !content || !data.SenderId) return; - void decryptChatText(messageSecret, data.Message.Content) - .catch((err) => { - log(1, "chat", "red", "Failed to decrypt live message", err, { - SendTime: data.Message?.SendTime, + if (isCurrentChat) { + addLiveMessage({ + ...data.Message, + Content: content ?? "Failed to decrypt message", + decryptionFailed: content === null, }); - return null; - }) - .then(async (content) => { - if (!data.Message || !content || !data.SenderId) return; + } - if (isCurrentChat) { - addLiveMessage({ - ...data.Message, - Content: content ?? "Failed to decrypt message", - decryptionFailed: content === null, + if (!shouldAlert) return; + + if (!isCurrentChat) { + // todo: add notification symbol to conversation cards (incl. message start) + moveUserIdToTop(data.SenderId); + + if (await load("settings.receive_confirmations")) { + void send("MessageState", { + MessageState: "received", }); } + } - if (!shouldAlert) return; + const user = await get(data.SenderId, [ + "UserId", + "Display", + "Avatar", + ]); - if (!isCurrentChat) { - // todo: add notification symbol to conversation cards (incl. message start) - moveUserIdToTop(data.SenderId); + if (isTauri()) { + if (!appFocused) return; + const permissionGranted = + (await isTauriNotificationPermissionGranted()) || + (await requestTauriNotificationPermission()) === "granted"; - if (await load("settings.receive_confirmations")) { - void send("MessageState", { - MessageState: "received", - }); - } - } - - const user = await get(data.SenderId, [ - "UserId", - "Display", - "Avatar", - ]); - - if (isTauri()) { - if (!appFocused) return; - const permissionGranted = - (await isTauriNotificationPermissionGranted()) || - (await requestTauriNotificationPermission()) === "granted"; - - if (permissionGranted) { - let handledNatively = false; - try { - handledNatively = await invoke( - "mtp_post_message_notification", - { - senderId: user.UserId, - sender: user.Display, - body: content, - avatar: user.Avatar, - }, - ); - } catch (error) { - log( - 1, - "notifications", - "red", - "Failed to create native message notification", - error, - ); - } - - if (!handledNatively) { - sendTauriNotification({ title: user.Display, body: content }); - } - } - } else { - const hasPermissions = await requestNotificationPermission(); - - if (hasPermissions) { - const options: NotificationOptions = { - body: content, - icon: user.Avatar || "/icons/icon-192.png", - badge: "/icons/notification-badge.png", - tag: `message-${user.UserId}`, - silent: true, - }; - if ("serviceWorker" in navigator) { - const registration = - await navigator.serviceWorker.getRegistration(); - if (registration) { - await registration.showNotification(user.Display, { - ...options, - data: { url: `/chat?id=${user.UserId}` }, - }); - return; - } - } - const notification = new Notification(user.Display, options); - notification.onclick = () => { - window.focus(); - navigate({ - to: `/chat?id=${user.UserId}`, - }); - notification.close(); - }; - } else { - sonnerToast(user.Display, { - classNames: { - content: "pl-4", + if (permissionGranted) { + let handledNatively = false; + try { + handledNatively = await invoke( + "mtp_post_message_notification", + { + senderId: user.UserId, + sender: user.Display, + body: content, + avatar: user.Avatar, }, - description: content, - icon: ( - - - - {user.Display.slice(0, 2).toUpperCase()} - - - ), - }); + ); + } catch (error) { + log( + 1, + "notifications", + "red", + "Failed to create native message notification", + error, + ); + } + + if (!handledNatively) { + sendTauriNotification({ title: user.Display, body: content }); } } - }); - return; - } + } else { + const hasPermissions = await requestNotificationPermission(); + + if (hasPermissions) { + const options: NotificationOptions = { + body: content, + icon: user.Avatar || "/icons/icon-192.png", + badge: "/icons/notification-badge.png", + tag: `message-${user.UserId}`, + silent: true, + }; + if ("serviceWorker" in navigator) { + const registration = + await navigator.serviceWorker.getRegistration(); + if (registration) { + await registration.showNotification(user.Display, { + ...options, + data: { url: `/chat?id=${user.UserId}` }, + }); + return; + } + } + const notification = new Notification(user.Display, options); + notification.onclick = () => { + window.focus(); + navigate({ + to: `/chat?id=${user.UserId}`, + }); + notification.close(); + }; + } else { + sonnerToast(user.Display, { + classNames: { + content: "pl-4", + }, + description: content, + icon: ( + + + + {user.Display.slice(0, 2).toUpperCase()} + + + ), + }); + } + } + }); }); }, [ addLiveMessage, @@ -191,7 +182,7 @@ export default function Provider(props: { children: React.ReactNode }) { navigate, send, moveUserIdToTop, - subscribePush, + subscribe, getChatSecret, userId, ]); diff --git a/packages/user/src/context.tsx b/packages/user/src/context.tsx index 423661c..8c41aa4 100644 --- a/packages/user/src/context.tsx +++ b/packages/user/src/context.tsx @@ -9,13 +9,12 @@ import { useState, useSyncExternalStore, } from "react"; -import { useMTP, type ProtocolMessage } from "@tensamin/mtp"; +import { useMTP } from "@tensamin/mtp"; import { clientUserStateSchema, mtp as schemas, publicUserStateSchema, - userStateEntrySchema, } from "@tensamin/shared/data"; import type z from "zod"; import { createCache } from "@tensamin/cache"; @@ -89,7 +88,7 @@ export default function UserProvider(props: { children: ReactNode }) { ); const revisionsRef = useRef(new Map>()); - const { send, subscribePush } = useMTP(); + const { send, subscribe: subscribeMTP } = useMTP(); const { load } = useStorage(); const { contacts } = useSession(); const [accountId, setAccountId] = useState(null); @@ -174,39 +173,6 @@ export default function UserProvider(props: { children: ReactNode }) { [publishUser], ); - const handleStatePush = useCallback( - async (message: ProtocolMessage) => { - if (!accountId) return; - const data = message.data as Record; - if (message.type === "GetStates") { - if (Array.isArray(data.MissingUserIds)) { - for (const userId of data.MissingUserIds) { - if (typeof userId === "number") removePresence(userId); - } - } - if (!Array.isArray(data.UserStates)) return; - for (const entry of data.UserStates) { - const parsed = userStateEntrySchema.safeParse(entry); - if (!parsed.success) continue; - if (parsed.data.UserId === accountId) continue; - initialStatesRef.current.set( - parsed.data.UserId, - parsed.data.UserState, - ); - if (applyUserState(parsed.data.UserId, parsed.data.UserState)) { - initialStatesRef.current.delete(parsed.data.UserId); - } - } - return; - } - if (message.type !== "ClientChanged") return; - const parsed = schemas.ClientChanged.response.safeParse(data); - if (!parsed.success) return; - applyUserState(parsed.data.UserId, parsed.data.UserState, true); - }, - [accountId, applyUserState, removePresence], - ); - useEffect(() => { void load("user_id").then((accountId) => { accountIdRef.current = accountId; @@ -223,8 +189,24 @@ export default function UserProvider(props: { children: ReactNode }) { useEffect(() => { if (!accountId) return; - return subscribePush(handleStatePush); - }, [accountId, handleStatePush, subscribePush]); + const unsubscribeStates = subscribeMTP("GetStates", ({ data }) => { + for (const userId of data.MissingUserIds ?? []) removePresence(userId); + for (const entry of data.UserStates) { + if (!entry || entry.UserId === accountId) continue; + initialStatesRef.current.set(entry.UserId, entry.UserState); + if (applyUserState(entry.UserId, entry.UserState)) { + initialStatesRef.current.delete(entry.UserId); + } + } + }); + const unsubscribeChanged = subscribeMTP("ClientChanged", ({ data }) => { + applyUserState(data.UserId, data.UserState, true); + }); + return () => { + unsubscribeStates(); + unsubscribeChanged(); + }; + }, [accountId, applyUserState, removePresence, subscribeMTP]); const loadUser = useCallback( async (userId: number): Promise => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 107149b..83d88ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: overrides: '@methanium/ui': https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz - mtp: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz + mtp: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz importers: @@ -16,8 +16,8 @@ importers: specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) mtp: - specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz - version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz sonner: specifier: ^2.0.8 version: 2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -98,8 +98,8 @@ importers: specifier: workspace:* version: link:../../packages/storage mtp: - specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz - version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz react: specifier: ^19.2.8 version: 19.2.8 @@ -290,8 +290,8 @@ importers: specifier: ^17.9.0 version: 17.9.0 mtp: - specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz - version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz typescript: specifier: ~6.0.3 version: 6.0.3 @@ -356,8 +356,8 @@ importers: specifier: ^1.29.0 version: 1.30.0(react@19.2.8) mtp: - specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz - version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz react: specifier: ^19.2.8 version: 19.2.8 @@ -431,8 +431,8 @@ importers: packages/crypto: dependencies: mtp: - specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz - version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz react: specifier: ^19.2.8 version: 19.2.8 @@ -506,9 +506,6 @@ importers: '@tauri-apps/api': specifier: ^2.11.1 version: 2.11.1 - '@tensamin/crypto': - specifier: workspace:* - version: link:../crypto '@tensamin/shared': specifier: workspace:* version: link:../shared @@ -516,17 +513,11 @@ importers: specifier: workspace:* version: link:../storage mtp: - specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz - version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz react: specifier: ^19.2.8 version: 19.2.8 - react-dom: - specifier: ^19.2.8 - version: 19.2.8(react@19.2.8) - zod: - specifier: ^4.4.3 - version: 4.4.3 devDependencies: eslint: specifier: ^10.8.0 @@ -5085,8 +5076,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mtp@https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz: - resolution: {integrity: sha512-rI+xskgAp93o9EdBO/b7HnA0PZTQQPTYuF4Snon/hTvayN+x9O40oad4n/ZUF0ahS6grw00MPKPe89Mfe3BHnw==, tarball: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz} + mtp@https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz: + resolution: {integrity: sha512-MzqeWSaS2lVoiK0coNfY8EPgGNk7QYFS3eRqVYeYBCT0NNm2lQ7T8lLjBuIuMZ6Zh2KAVWhwI2xQIkMdZ7QThA==, tarball: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz} version: 0.3.0 nanoid@3.3.18: @@ -11337,7 +11328,7 @@ snapshots: ms@2.1.3: {} - mtp@https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz: + mtp@https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz: dependencies: yaml: 2.9.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f713a2d..6ec3531 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,4 +7,4 @@ allowBuilds: esbuild: true overrides: "@methanium/ui": "https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz" - mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz" + mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz" From 8a04b327fcb4f04dfff212af13a5b9e901622818 Mon Sep 17 00:00:00 2001 From: Alois Date: Fri, 28 Aug 2026 20:02:35 +0200 Subject: [PATCH 16/19] chore(mtp): update type-maps --- mtp-type-maps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mtp-type-maps b/mtp-type-maps index a70085b..6e5122f 160000 --- a/mtp-type-maps +++ b/mtp-type-maps @@ -1 +1 @@ -Subproject commit a70085b0c41844745449c9913a361f5bb9bee529 +Subproject commit 6e5122fe44f793c0e0d3229b3d34145ce17c2d31 From 7c9d9529b45ae3574abcfe5dfe33dc0f2e834b0f Mon Sep 17 00:00:00 2001 From: Alois Date: Sat, 29 Aug 2026 12:57:44 +0200 Subject: [PATCH 17/19] feat(workflows): allow manual runs for the dev deploy --- .forgejo/workflows/deploy-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.forgejo/workflows/deploy-dev.yml b/.forgejo/workflows/deploy-dev.yml index c340cbf..7f59ec1 100644 --- a/.forgejo/workflows/deploy-dev.yml +++ b/.forgejo/workflows/deploy-dev.yml @@ -1,4 +1,5 @@ on: + workflow_dispatch: push: branches: - dev From f5461bbd427b8e9be2440784991bc1f2e19b02d5 Mon Sep 17 00:00:00 2001 From: Alex Emmet <111742636+Alex-Emmet@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:11:24 +0200 Subject: [PATCH 18/19] chore(mtp): update type-maps --- mtp-type-maps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mtp-type-maps b/mtp-type-maps index 6e5122f..f4e45aa 160000 --- a/mtp-type-maps +++ b/mtp-type-maps @@ -1 +1 @@ -Subproject commit 6e5122fe44f793c0e0d3229b3d34145ce17c2d31 +Subproject commit f4e45aa3a3ad0e3c3a257f66857b904a1af7901c From f74ca52f4faf18161a69652c3b149cf5d7143727 Mon Sep 17 00:00:00 2001 From: Alois Date: Sun, 30 Aug 2026 15:58:34 +0200 Subject: [PATCH 19/19] feat(nix-flake): rework nix package --- .forgejo/workflows/deploy-prod.yml | 55 --------- flake.nix | 181 +++++++++++++++++++---------- 2 files changed, 118 insertions(+), 118 deletions(-) diff --git a/.forgejo/workflows/deploy-prod.yml b/.forgejo/workflows/deploy-prod.yml index dd7a5f9..621a0cd 100644 --- a/.forgejo/workflows/deploy-prod.yml +++ b/.forgejo/workflows/deploy-prod.yml @@ -311,58 +311,3 @@ jobs: "$API/repos/$REPO/releases/$release_id" done < "$DELETE_RELEASES" EOF - - - name: Update root flake release hash - env: - TAG: ${{ steps.version.outputs.tag }} - run: | - nix develop .#electron --command bash <<'EOF' - set -eu - - DEB="$(find releases -maxdepth 1 -type f -name 'Tensamin-*-linux-amd64.deb' -print -quit)" - test -n "$DEB" - - HASH="$(node -e 'const fs = require("fs"); const crypto = require("crypto"); const file = process.argv[1]; console.log("sha256-" + crypto.createHash("sha256").update(fs.readFileSync(file)).digest("base64"));' "$DEB")" - export HASH - - node -e ' - const fs = require("fs"); - const version = process.env.TAG; - const hash = process.env.HASH; - let content = fs.readFileSync("flake.nix", "utf8"); - content = content.replace(/version = "[^"]+";/, `version = "${version}";`); - content = content.replace(/x86_64DebHash = "sha256-[^"]+";/, `x86_64DebHash = "${hash}";`); - fs.writeFileSync("flake.nix", content); - ' - - if git diff --quiet -- flake.nix; then - echo "flake.nix already has the current release hash on main." - else - git add flake.nix - git -c user.name="forgejo-actions" -c user.email="forgejo-actions@localhost" commit -m "(qol): update release flake hash" - git push - fi - - git fetch origin dev - git worktree add ../dev-flake-update origin/dev - cd ../dev-flake-update - - node -e ' - const fs = require("fs"); - const version = process.env.TAG; - const hash = process.env.HASH; - let content = fs.readFileSync("flake.nix", "utf8"); - content = content.replace(/version = "[^"]+";/, `version = "${version}";`); - content = content.replace(/x86_64DebHash = "sha256-[^"]+";/, `x86_64DebHash = "${hash}";`); - fs.writeFileSync("flake.nix", content); - ' - - if git diff --quiet -- flake.nix; then - echo "flake.nix already has the current release hash on dev." - exit 0 - fi - - git add flake.nix - git -c user.name="forgejo-actions" -c user.email="forgejo-actions@localhost" commit -m "(qol): update release flake hash" - git push origin HEAD:dev - EOF diff --git a/flake.nix b/flake.nix index e02a001..0b0d63c 100644 --- a/flake.nix +++ b/flake.nix @@ -17,8 +17,6 @@ systems = [ "x86_64-linux" ]; forAllSystems = nixpkgs.lib.genAttrs systems; version = "0.0.11"; - x86_64DebHash = "sha256-iMEgdjY8Ghlx6K/YzKC5JAZbeHNJDIFHm8MKmM4efaI="; - forgejoBaseUrl = "https://git.methanium.net/tensamin/client/releases/download/${version}"; in { packages = forAllSystems ( @@ -32,8 +30,6 @@ android_sdk.accept_license = true; }; }; - debArtifact = "Tensamin-${version}-linux-amd64.deb"; - electronRuntimeLibs = with pkgs; [ alsa-lib at-spi2-atk @@ -75,71 +71,130 @@ libXtst libxcb ]; - packageDeb = - src: - pkgs.stdenv.mkDerivation { - pname = "tensamin"; - inherit version src; - - nativeBuildInputs = with pkgs; [ - autoPatchelfHook - dpkg - makeWrapper - ]; - buildInputs = electronRuntimeLibs; - - dontConfigure = true; - dontBuild = true; - - unpackPhase = '' - runHook preUnpack - dpkg-deb -x "$src" . - runHook postUnpack - ''; - - installPhase = '' - runHook preInstall - - mkdir -p "$out" - cp -r opt "$out/" - cp -r usr/* "$out/" - - mkdir -p "$out/bin" - makeWrapper "$out/opt/Tensamin/tensamin" "$out/bin/tensamin" \ - --prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath electronRuntimeLibs}" - - substituteInPlace "$out/share/applications/Tensamin.desktop" \ - --replace-fail "Exec=/opt/Tensamin/tensamin" "Exec=tensamin" - - runHook postInstall - ''; + electron = pkgs.electron; + pnpm = pkgs.pnpm; + mtpTypeMaps = pkgs.fetchgit { + url = "https://git.methanium.net/tensamin/mtp-type-maps"; + rev = "6e5122fe44f793c0e0d3229b3d34145ce17c2d31"; + hash = "sha256-/4n8F0YLJaLncefKL907P5l+en1CTjpgdFmLBF3gbiQ="; + }; + mtpSource = pkgs.fetchzip { + url = "https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz"; + hash = "sha256-XLTa8DxP93Q4hBHRCLUzCPOqkbdb4V3aZxE5Iuq+kW0="; + }; + mtpCargoDeps = pkgs.rustPlatform.fetchCargoVendor { + src = mtpSource; + hash = "sha256-8MZ65N/EtWPAggal0JkGDx3WSn+LxWQORinkqVbsrys="; + }; + wasmBindgenCliSource = pkgs.fetchCrate { + pname = "wasm-bindgen-cli"; + version = "0.2.127"; + hash = "sha256-di+qBAdd7pENLiIB9CoZoab+W5xeDoByMREcCGTSzWo="; + }; + wasmBindgenCli = pkgs.buildWasmBindgenCli { + src = wasmBindgenCliSource; + cargoDeps = pkgs.rustPlatform.fetchCargoVendor { + src = wasmBindgenCliSource; + hash = "sha256-FTv2GZIAQs0ePdIZXIXil7JbZ6kIT05VG6vqC1qNFxQ="; }; - defaultPackage = packageDeb ( - pkgs.fetchurl { - url = "${forgejoBaseUrl}/${debArtifact}"; - hash = x86_64DebHash; - } - ); - localDebPath = builtins.getEnv "TENSAMIN_DEB"; - localPathPackage = - if localDebPath == "" then - pkgs.writeShellScriptBin "tensamin" '' - echo "Set TENSAMIN_DEB to a local .deb path and run with --impure." >&2 - exit 1 - '' - else - packageDeb ( - builtins.path { - path = localDebPath; - name = debArtifact; - } - ); + }; + desktopItem = pkgs.makeDesktopItem { + name = "tensamin"; + desktopName = "Tensamin"; + exec = "tensamin"; + icon = "tensamin"; + startupWMClass = "Tensamin"; + categories = [ "Network" ]; + }; + defaultPackage = pkgs.stdenv.mkDerivation (finalAttrs: { + pname = "tensamin"; + inherit version; + src = self; + + pnpmDeps = pkgs.fetchPnpmDeps { + inherit (finalAttrs) pname version src; + inherit pnpm; + fetcherVersion = 4; + hash = "sha256-imP3MTr1YLc28Z9n617m0Wt/6vPirFznzoriKzojlEg="; + }; + + nativeBuildInputs = with pkgs; [ + copyDesktopItems + makeWrapper + nodejs_22 + pnpm + pnpmConfigHook + cargo + lld + rustc + wasm-pack + wasmBindgenCli + binaryen + ]; + + env.ELECTRON_SKIP_BINARY_DOWNLOAD = 1; + + postPatch = '' + rm -rf mtp-type-maps + ln -s ${mtpTypeMaps} mtp-type-maps + + node -e ' + const fs = require("fs"); + const path = "apps/electron/package.json"; + const pkg = JSON.parse(fs.readFileSync(path, "utf8")); + pkg.version = "${version}"; + fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n"); + ' + ''; + + buildPhase = '' + runHook preBuild + + mkdir -p "$HOME/.cargo" + substitute ${mtpCargoDeps}/.cargo/config.toml "$HOME/.cargo/config.toml" \ + --replace-fail @vendor@ ${mtpCargoDeps} + + pnpm run copy-licenses + pnpm run build:packages + pnpm run build:web + pnpm --dir apps/tauri run gen-icons + pnpm --dir apps/electron run build + pnpm --dir apps/electron exec electron-builder --dir --linux --publish never \ + --config.electronDist=${electron.dist} \ + --config.electronVersion=${electron.version} + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p "$out/lib/tensamin" "$out/bin" + cp -r apps/electron/release/linux-unpacked/. "$out/lib/tensamin/" + + makeWrapper "$out/lib/tensamin/tensamin" "$out/bin/tensamin" \ + --prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath electronRuntimeLibs}" + + install -Dm644 apps/electron/build/icons/icon.png \ + "$out/share/icons/hicolor/512x512/apps/tensamin.png" + + runHook postInstall + ''; + + desktopItems = [ desktopItem ]; + + meta = { + description = "Tensamin desktop client"; + homepage = "https://git.methanium.net/tensamin/client"; + mainProgram = "tensamin"; + platforms = pkgs.lib.platforms.linux; + }; + }); in { default = defaultPackage; tensamin = defaultPackage; electron = defaultPackage; - localPathForDev = localPathPackage; } );