diff --git a/apps/tauri/package.json b/apps/tauri/package.json index b5d89ec..80b8b3a 100644 --- a/apps/tauri/package.json +++ b/apps/tauri/package.json @@ -18,11 +18,9 @@ } }, "scripts": { - "dev:mobile:raw": "tauri android dev --host ${TAURI_DEV_HOST:-127.0.0.1}", - "start-adb:mobile:raw": "adb devices", + "dev:mobile:raw": "tauri android dev", "build:mobile:raw": "tauri android build", "dev:mobile": "if command -v nix >/dev/null 2>&1; then nix develop --command bun dev:mobile:raw; else bun dev:mobile:raw; fi", - "start-adb:mobile": "if command -v nix >/dev/null 2>&1; then nix develop --command bun start-adb:mobile:raw; else bun start-adb:mobile:raw; fi", "build:mobile": "bun run render-version.ts && if command -v nix >/dev/null 2>&1; then nix develop --command bun build:mobile:raw; else bun build:mobile:raw; fi && bun run render-version.ts --unrender", "gen-icons": "tauri icon ./logo.json && bun scripts/sync-electron-icons.ts", "format": "bunx prettier --write .", @@ -32,7 +30,6 @@ "@tauri-apps/api": "^2", "@tauri-apps/plugin-barcode-scanner": "~2", "@tauri-apps/plugin-deep-link": "~2", - "@tauri-apps/plugin-log": "~2", "@tauri-apps/plugin-notification": "~2", "@tensamin/shared": "workspace:*", "@tensamin/ui": "*", diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index 37a6827..386b3fc 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/Cargo.lock @@ -8,17 +8,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "version_check", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -43,23 +32,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "android_log-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" - -[[package]] -name = "android_logger" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" -dependencies = [ - "android_log-sys", - "env_filter", - "log", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -75,12 +47,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - [[package]] name = "asn1-rs" version = "0.7.2" @@ -360,18 +326,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -412,30 +366,6 @@ dependencies = [ "piper", ] -[[package]] -name = "borsh" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" -dependencies = [ - "borsh-derive", - "bytes", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" -dependencies = [ - "once_cell", - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "brotli" version = "8.0.3" @@ -472,40 +402,6 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" -[[package]] -name = "byte-unit" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" -dependencies = [ - "rust_decimal", - "schemars 1.2.1", - "serde", - "utf8-width", -] - -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "bytemuck" version = "1.25.0" @@ -1173,16 +1069,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "env_filter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" -dependencies = [ - "log", - "regex", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1246,15 +1132,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "fern" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" -dependencies = [ - "log", -] - [[package]] name = "field-offset" version = "0.3.6" @@ -1341,12 +1218,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures-channel" version = "0.3.32" @@ -1733,9 +1604,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] [[package]] name = "hashbrown" @@ -2318,9 +2186,6 @@ name = "log" version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" -dependencies = [ - "value-bag", -] [[package]] name = "lru-slab" @@ -2530,15 +2395,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - [[package]] name = "objc2" version = "0.6.4" @@ -3130,26 +2986,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "quick-xml" version = "0.37.5" @@ -3245,12 +3081,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.6" @@ -3371,9 +3201,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.4" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -3394,18 +3224,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.11" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -3455,35 +3276,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rkyv" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "rust-ini" version = "0.21.3" @@ -3494,23 +3286,6 @@ dependencies = [ "ordered-multimap", ] -[[package]] -name = "rust_decimal" -version = "1.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c5108e3d4d903e21aac27f12ba5377b6b34f9f44b325e4894c7924169d06995" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand 0.8.6", - "rkyv", - "serde", - "serde_json", - "wasm-bindgen", -] - [[package]] name = "rustc-hash" version = "2.1.2" @@ -3694,12 +3469,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - [[package]] name = "security-framework" version = "3.7.0" @@ -3955,12 +3724,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - [[package]] name = "siphasher" version = "1.0.3" @@ -4115,7 +3878,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", "unicode-ident", ] @@ -4214,12 +3976,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "target-lexicon" version = "0.12.16" @@ -4419,28 +4175,6 @@ dependencies = [ "windows-result 0.3.4", ] -[[package]] -name = "tauri-plugin-log" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" -dependencies = [ - "android_logger", - "byte-unit", - "fern", - "log", - "objc2", - "objc2-foundation", - "serde", - "serde_json", - "serde_repr", - "swift-rs", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", - "time", -] - [[package]] name = "tauri-plugin-notification" version = "2.3.3" @@ -4660,11 +4394,10 @@ dependencies = [ "tauri-plugin-app-events", "tauri-plugin-barcode-scanner", "tauri-plugin-deep-link", - "tauri-plugin-log", "tauri-plugin-notification", "tauri-plugin-opener", "ttp-core", - "ttp-tauri", + "ttp-native", ] [[package]] @@ -4715,9 +4448,7 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", - "libc", "num-conv", - "num_threads", "powerfmt", "serde_core", "time-core", @@ -5042,7 +4773,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ttp-core" version = "0.1.0" -source = "git+https://git.methanium.net/tensamin/ttp.git#0ccc2b808c1f8ff38ede906491ef703a51bd7786" +source = "git+https://git.methanium.net/tensamin/ttp.git#23438fa8f884e6ad0d32ca1004c0dedcce0cc8d2" dependencies = [ "base64 0.22.1", "byteorder", @@ -5055,7 +4786,7 @@ dependencies = [ [[package]] name = "ttp-native" version = "0.1.0" -source = "git+https://git.methanium.net/tensamin/ttp.git#0ccc2b808c1f8ff38ede906491ef703a51bd7786" +source = "git+https://git.methanium.net/tensamin/ttp.git#23438fa8f884e6ad0d32ca1004c0dedcce0cc8d2" dependencies = [ "quinn", "rustls", @@ -5063,25 +4794,9 @@ dependencies = [ "thiserror 2.0.18", "tokio", "ttp-core", - "webpki-roots", "wtransport", ] -[[package]] -name = "ttp-tauri" -version = "0.1.0" -source = "git+https://git.methanium.net/tensamin/ttp.git#0ccc2b808c1f8ff38ede906491ef703a51bd7786" -dependencies = [ - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", - "tokio", - "ttp-core", - "ttp-native", -] - [[package]] name = "typeid" version = "1.0.3" @@ -5207,12 +4922,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf8-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -5231,12 +4940,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" - [[package]] name = "version-compare" version = "0.2.1" @@ -5490,15 +5193,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "webpki-roots" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "webview2-com" version = "0.38.2" @@ -6194,15 +5888,6 @@ dependencies = [ "url", ] -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "x11" version = "2.21.0" diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index 556bb8b..ba011c1 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -24,8 +24,7 @@ serde_json = "1" tauri-plugin-deep-link = "2" tauri-plugin-notification = "2" ttp-core = { git = "https://git.methanium.net/tensamin/ttp.git", package = "ttp-core" } -ttp-tauri = { git = "https://git.methanium.net/tensamin/ttp.git", package = "ttp-tauri" } -tauri-plugin-log = "2" +ttp-native = { git = "https://git.methanium.net/tensamin/ttp.git", package = "ttp-native" } [target.'cfg(target_os = "android")'.dependencies.tauri] version = "2" diff --git a/apps/tauri/src-tauri/capabilities/default.json b/apps/tauri/src-tauri/capabilities/default.json index 352eda0..02d2627 100644 --- a/apps/tauri/src-tauri/capabilities/default.json +++ b/apps/tauri/src-tauri/capabilities/default.json @@ -15,11 +15,6 @@ "core:window:allow-minimize", "core:event:default", "deep-link:default", - "notification:default", - "log:default", - "ttp-tauri:allow-connect", - "ttp-tauri:allow-send", - "ttp-tauri:allow-close", - "ttp-tauri:allow-ready-state" + "notification:default" ] -} +} \ No newline at end of file diff --git a/apps/tauri/src-tauri/capabilities/mobile.json b/apps/tauri/src-tauri/capabilities/mobile.json index d9914a6..ceee8f7 100644 --- a/apps/tauri/src-tauri/capabilities/mobile.json +++ b/apps/tauri/src-tauri/capabilities/mobile.json @@ -13,11 +13,6 @@ "barcode-scanner:default", "barcode-scanner:allow-scan", "barcode-scanner:allow-cancel", - "notification:default", - "log:default", - "ttp-tauri:allow-connect", - "ttp-tauri:allow-send", - "ttp-tauri:allow-close", - "ttp-tauri:allow-ready-state" + "notification:default" ] -} +} \ No newline at end of file diff --git a/apps/tauri/src-tauri/gen/android/app/build.gradle.kts b/apps/tauri/src-tauri/gen/android/app/build.gradle.kts index 3775319..f13d1b3 100644 --- a/apps/tauri/src-tauri/gen/android/app/build.gradle.kts +++ b/apps/tauri/src-tauri/gen/android/app/build.gradle.kts @@ -40,7 +40,6 @@ android { } buildTypes { getByName("debug") { - applicationIdSuffix = ".dev" manifestPlaceholders["usesCleartextTraffic"] = "true" isDebuggable = true isJniDebuggable = true diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index cfa496a..f01b2ad 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -1,9 +1,6 @@ #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let builder = tauri::Builder::default() - .plugin(tauri_plugin_log::Builder::new().level(tauri_plugin_log::log::LevelFilter::Info).build()) - .plugin(tauri_plugin_notification::init()) - .plugin(ttp_tauri::init()); + let builder = tauri::Builder::default().plugin(tauri_plugin_notification::init()); let builder = builder .plugin(tauri_plugin_deep_link::init()) diff --git a/apps/tauri/src-tauri/src/main.rs b/apps/tauri/src-tauri/src/main.rs index e78a26a..a9346de 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") + mobile_lib::run() } diff --git a/apps/tauri/src-tauri/tauri.conf.json b/apps/tauri/src-tauri/tauri.conf.json index 17effab..15e46c0 100644 --- a/apps/tauri/src-tauri/tauri.conf.json +++ b/apps/tauri/src-tauri/tauri.conf.json @@ -25,9 +25,6 @@ } }, "bundle": { - "android": { - "debugApplicationIdSuffix": ".dev" - }, "active": true, "targets": [], "icon": [ diff --git a/apps/web/package.json b/apps/web/package.json index 5cbfb60..04d731f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,7 +12,7 @@ "preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .." }, "dependencies": { - "@fontsource-variable/public-sans": "^5.2.7", + "@fontsource-variable/inter": "^5.2.8", "@tailwindcss/vite": "^4.2.4", "@tanstack/react-router": "^1.169.1", "@tanstack/react-virtual": "^3.13.24", diff --git a/apps/web/src/features/settings/components.tsx b/apps/web/src/features/settings/components.tsx index 491b839..b9f2967 100644 --- a/apps/web/src/features/settings/components.tsx +++ b/apps/web/src/features/settings/components.tsx @@ -1,17 +1,8 @@ -import { - Button, - Checkbox, - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, - Input, - Label, - Switch as UISwitch, -} from "@tensamin/ui"; +import { Label } from "@tensamin/ui"; +import { Switch as UISwitch } from "@tensamin/ui"; import { useEffect, useState } from "react"; -import { storageDefaults, type Storage } from "@tensamin/shared/data"; +import type { Storage } from "@tensamin/shared/data"; import { settingsStorageDefaults } from "@tensamin/shared/settings"; import { useStorage } from "@tensamin/storage/context"; @@ -19,13 +10,6 @@ type BooleanStorageKey = { [K in keyof Storage]: Storage[K] extends boolean ? K : never; }[keyof Storage]; -type ListStorageKey = { - [K in keyof Storage]: Storage[K] extends (string | number)[] ? K : never; -}[keyof Storage]; - -type ListStorageItem = - Storage[K] extends Array ? Item : never; - export function Switch({ label, id, @@ -54,125 +38,3 @@ export function Switch({ ); } - -export function List({ - label, - id, -}: { - label: React.ReactNode; - id: K; -}) { - const { save, load } = useStorage(); - const [items, setItems] = useState(storageDefaults[id]); - const [inputValue, setInputValue] = useState(""); - const [selectedItems, setSelectedItems] = useState>(new Set()); - - useEffect(() => { - load(id).then((value) => setItems(value)); - }, [id, load]); - - const persistItems = (nextItems: Storage[K]) => { - setItems(nextItems); - save(id, nextItems); - }; - - const toStorageItem = (value: string): ListStorageItem => { - const referenceItem = items[0] ?? storageDefaults[id][0]; - - if (typeof referenceItem === "number") { - return Number(value) as ListStorageItem; - } - - return value as ListStorageItem; - }; - - const addItem = () => { - const trimmedValue = inputValue.trim(); - if (!trimmedValue) return; - - const nextItem = toStorageItem(trimmedValue); - if (typeof nextItem === "number" && Number.isNaN(nextItem)) return; - - persistItems([...items, nextItem] as Storage[K]); - setInputValue(""); - }; - - const deleteItems = (indexes: Set) => { - const nextItems = items.filter( - (_, index) => !indexes.has(index), - ) as Storage[K]; - - setItems(nextItems); - setSelectedItems(new Set()); - save(id, nextItems); - }; - - return ( -
- -
-
- setInputValue(event.target.value)} - onKeyDown={(event) => { - if (event.key === "Enter") addItem(); - }} - /> - -
-
- {items.map((item, index) => { - const labelId = `${String(id)}-${index}`; - const selected = selectedItems.has(index); - const deletingSelectedItems = selectedItems.size > 1; - - return ( - - - { - setSelectedItems((previous) => { - const nextSelected = new Set(previous); - - if (checked) { - nextSelected.add(index); - } else { - nextSelected.delete(index); - } - - return nextSelected; - }); - }} - /> - -
-
- } - /> - - - deleteItems( - deletingSelectedItems - ? selectedItems - : new Set([index]), - ) - } - > - {deletingSelectedItems ? "Delete Selected" : "Delete"} - - -
- ); - })} -
-
-
- ); -} diff --git a/apps/web/src/index.css b/apps/web/src/index.css index f7dcb91..925d401 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1,13 +1,13 @@ @import "tailwindcss"; @import "tailwind-scrollbar-hide/v4"; @import "tw-animate-css"; -@import "@fontsource-variable/public-sans"; +@import "@fontsource-variable/inter"; @source "./**/*.{ts,tsx}"; @source "../../../packages/**/src/**/*.{ts,tsx}"; @theme { - --font-sans: "Public Sans Variable", sans-serif; + --font-sans: "Inter Variable", sans-serif; --animate-wiggle: wiggle 0.5s ease-in-out infinite; @@ -31,7 +31,7 @@ body, } body { - font-family: "Public Sans Variable", sans-serif; + font-family: "Inter Variable", sans-serif; } #root { diff --git a/apps/web/src/routes/settings/chat.tsx b/apps/web/src/routes/settings/chat.tsx index aef03c6..f67a2c6 100644 --- a/apps/web/src/routes/settings/chat.tsx +++ b/apps/web/src/routes/settings/chat.tsx @@ -1,4 +1,4 @@ -import { List, Switch } from "@/features/settings/components"; +import { Switch } from "@/features/settings/components"; import { Kbd } from "@tensamin/ui"; export default function Page() { @@ -25,11 +25,6 @@ export default function Page() { label="Sidebar message preview" id="settings.show_start_of_last_message_in_sidebar" /> -

- Trusted embed domains can get your IP-Address! Only add domains if you - really trust them! -

- ); } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 71fb4ac..f9d51eb 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -96,9 +96,6 @@ export default defineConfig({ }, }, envPrefix: ["VITE_", "TAURI_ENV_*"], - optimizeDeps: { - exclude: ["@tensamin/ttp-core"], - }, build: { minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false, sourcemap: !!process.env.TAURI_ENV_DEBUG, diff --git a/bun.lock b/bun.lock index a7ffa1c..dc7cf4b 100644 --- a/bun.lock +++ b/bun.lock @@ -43,7 +43,6 @@ "@tauri-apps/api": "^2", "@tauri-apps/plugin-barcode-scanner": "~2", "@tauri-apps/plugin-deep-link": "~2", - "@tauri-apps/plugin-log": "~2", "@tauri-apps/plugin-notification": "~2", "@tensamin/shared": "workspace:*", "@tensamin/ui": "*", @@ -59,7 +58,7 @@ "name": "@tensamin/web", "version": "0.0.0", "dependencies": { - "@fontsource-variable/public-sans": "^5.2.7", + "@fontsource-variable/inter": "^5.2.8", "@tailwindcss/vite": "^4.2.4", "@tanstack/react-router": "^1.169.1", "@tanstack/react-virtual": "^3.13.24", @@ -126,7 +125,6 @@ "name": "@tensamin/chat", "version": "0.0.0", "dependencies": { - "@tanstack/pacer": "^0.21.1", "@tanstack/react-query": "^5.0.0", "@tanstack/react-router": "^1.0.0", "@tanstack/react-virtual": "^3.0.0", @@ -257,7 +255,7 @@ }, }, "overrides": { - "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.24.tar.gz", + "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.20.tar.gz", "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.36.tar.gz", }, "packages": { @@ -473,8 +471,6 @@ "@fontsource-variable/inter": ["@fontsource-variable/inter@5.2.8", "", {}, "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ=="], - "@fontsource-variable/public-sans": ["@fontsource-variable/public-sans@5.2.7", "", {}, "sha512-4mvade2J3slKkvwRkS+p8T3szet/0vhWoSnuUJTVU81Uo2pRpSZY/Y8bSLRqpSwzIPxjVmRJ53oq6JKP/l/PSg=="], - "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], @@ -675,12 +671,8 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.2.4", "", { "dependencies": { "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "tailwindcss": "4.2.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw=="], - "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.3", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw=="], - "@tanstack/history": ["@tanstack/history@1.161.6", "", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="], - "@tanstack/pacer": ["@tanstack/pacer@0.21.1", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.3", "@tanstack/store": "^0.11.0" } }, "sha512-hB01dd4rlsYcTCNP7wK186jgAe6K5qimgM1Y5Jtvz+9PUaILvpmeLLjmQNUNSO1l23lIt+CeQR6mO1mjlPvRtQ=="], - "@tanstack/query-core": ["@tanstack/query-core@5.100.8", "", {}, "sha512-ceYwSFOqjPwET5TA6IOYxzxlGc0ekyH/gfOtWkP0PX43rzX9bxW48Iuw8KAduKCToi4rJAQ6nRy2kAe8gszdmg=="], "@tanstack/react-query": ["@tanstack/react-query@5.100.8", "", { "dependencies": { "@tanstack/query-core": "5.100.8" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-iNNEekixXU5vtAGKKZX2lx3jTooG5yNY+kv0wSgEdEYG0Mj0JM5bcuQtC35ZAP3nDopT6jciUK3xeX65U7AnfA=="], @@ -693,7 +685,7 @@ "@tanstack/router-core": ["@tanstack/router-core@1.169.1", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-x+2gIGKTTE1qAn7tLieGfrB5ciOviDmmi2ox9fAWUubRV+yTU5ruGFXocoCIWF+lB+SOtnHjo2E9BLSWyYoEmA=="], - "@tanstack/store": ["@tanstack/store@0.11.0", "", {}, "sha512-WlzzCt3xi0G6pCAJu1U+2jiECwabETDpQDi3hfkFZvJii9AuZqEKbOiVarX1/bWhTNjU486yQtJCCasi/0q+Cw=="], + "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], "@tanstack/virtual-core": ["@tanstack/virtual-core@3.14.0", "", {}, "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q=="], @@ -727,8 +719,6 @@ "@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.9", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-u0SKOUHnJ1wqeqXsDFq2+kASCBj9xxbG0g9XZWPy9SOmU4wXtp6b/wiYpm6oH6/5fBTQsLqnLhIvqLBRpgHJlA=="], - "@tauri-apps/plugin-log": ["@tauri-apps/plugin-log@2.8.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-a+7rOq3MJwpTOLLKbL8d0qGZ85hgHw5pNOWusA9o3cf7cEgtYHiGY/+O8fj8MvywQIGqFv0da2bYQDlrqLE7rw=="], - "@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg=="], "@tensamin/call": ["@tensamin/call@workspace:packages/call"], @@ -753,7 +743,7 @@ "@tensamin/ttp": ["@tensamin/ttp@workspace:packages/ttp"], - "@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.24.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@tauri-apps/api": "^2.9.0", "@typescript-eslint/parser": "^8.59.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.3", "typescript-eslint": "^8.59.1", "zod": "^4.4.1" } }, "sha512-Db94CDOTYvzJSQDZGDgziUXRvXx9PiEfYDQt6sYe8Nyg+KD/aiKnyQx7NeEiG03TfYE3ogNxXOsrcsip4di8+g=="], + "@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.20.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.59.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.3", "typescript-eslint": "^8.59.1", "zod": "^4.4.1" } }, "sha512-08pYuWTivuDWNiBoSSY2P8rIZz4FvuvLQZMjaQnf7KE/Pz9bGO69XHFcCHQXIz1SWsT2TyjochT0sPEGAta15g=="], "@tensamin/ui": ["@tensamin/ui@https://git.methanium.net/tensamin/ui/archive/0.0.36.tar.gz", { "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/inter": "^5.2.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^1.8.0", "next-themes": "^0.4.6", "react-day-picker": "^9.14.0", "react-resizable-panels": "^4.10.0", "recharts": "3.8.0", "shadcn": "^3.5.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.3.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.2.0", "react-dom": "^19.2.0" } }, "sha512-5zql8LtBKVn7WuQDCIdLbthKvLEyxjWShAceXCYbk+kwO41VVILhS1EWuDvjgvv2BBdyuxoVWvdblPIW4BcdSQ=="], @@ -2023,8 +2013,6 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], - "@tensamin/electron/@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], "@tensamin/tauri/@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], diff --git a/flake.nix b/flake.nix index b70fd6b..c1ae625 100644 --- a/flake.nix +++ b/flake.nix @@ -6,8 +6,8 @@ outputs = {nixpkgs, ...}: let systems = ["x86_64-linux"]; forAllSystems = nixpkgs.lib.genAttrs systems; - version = "0.0.9"; - x86_64DebHash = "sha256-8rVb12SGGSac6l1NVPGyRLsAKAGDAOJVUwG8l+hoRCU="; + version = "0.0.8"; + x86_64DebHash = "sha256-xC8YHVk+JpeMvXHB1dZihYLFyzFDXne4bkV6TZiOFrE="; forgejoBaseUrl = "https://git.methanium.net/tensamin/client/releases/download/${version}"; in { packages = forAllSystems (system: let diff --git a/package.json b/package.json index 37fe3a9..86c8924 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "preview:web": "cd apps/web && bun run preview", "dev:mobile": "cd apps/tauri && bun dev:mobile", "build:mobile": "cd apps/tauri && bun run build:mobile", - "start-adb:mobile": "cd apps/tauri && bun run start-adb:mobile", "dev:desktop": "cd apps/electron && bun run dev", "build:desktop": "cd apps/electron && bun run package", "run:desktop": "TENSAMIN_DEB=\"$PWD/apps/electron/release/Tensamin-$(node -p \\\"require('./package.json').version\\\")-linux-amd64.deb\" nix run .#localPathForDev --impure", @@ -45,7 +44,7 @@ }, "overrides": { "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.36.tar.gz", - "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.24.tar.gz" + "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.20.tar.gz" }, "dependencies": { "@tensamin/ttp-core": "*", diff --git a/packages/chat/package.json b/packages/chat/package.json index a4b9b86..3846ddd 100644 --- a/packages/chat/package.json +++ b/packages/chat/package.json @@ -15,17 +15,16 @@ "build": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@tanstack/pacer": "^0.21.1", "@tanstack/react-query": "^5.0.0", "@tanstack/react-router": "^1.0.0", "@tanstack/react-virtual": "^3.0.0", "@tensamin/crypto": "workspace:*", + "@tensamin/ttp": "workspace:*", + "@tensamin/storage": "workspace:*", + "@tensamin/user": "workspace:*", "@tensamin/markdown": "workspace:*", "@tensamin/shared": "workspace:*", - "@tensamin/storage": "workspace:*", - "@tensamin/ttp": "workspace:*", "@tensamin/ui": "*", - "@tensamin/user": "workspace:*", "lucide-react": "^1.14.0", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/packages/chat/src/components/gifPicker.tsx b/packages/chat/src/components/gifPicker.tsx deleted file mode 100644 index 32229a5..0000000 --- a/packages/chat/src/components/gifPicker.tsx +++ /dev/null @@ -1,638 +0,0 @@ -import { Debouncer } from "@tanstack/pacer"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import { - Button, - Input, - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@tensamin/ui"; -import { useStorage } from "@tensamin/storage/context"; -import { Loader2, Search } from "lucide-react"; -import MediaSaveButton from "./mediaSaveButton"; -import React, { - useEffect, - useLayoutEffect, - useRef, - useState, - startTransition, -} from "react"; - -const klipyApiKey = - "KzyghnvdlaHjJuDycVx6rLIEu5eaacPyFERieoI7L8UWLt2rnJloL45UL5MVc9IG"; -const klipyBaseUrl = "https://api.klipy.com/api/v1"; -const pageSize = 24; -const minColumnWidth = 200; - -function getColumnCount(width: number, itemCount: number) { - return Math.max( - 1, - Math.min(itemCount || 1, Math.floor(width / minColumnWidth)), - ); -} - -type KlipyKind = "gif" | "meme"; - -type KlipyMediaFile = { - url?: string; - width?: number; - height?: number; -}; - -type KlipyMediaFormats = Record; - -type KlipyItem = { - id: number | string; - title?: string; - file?: Record; - blur_preview?: string; -}; - -type KlipyPage = { - items: KlipyItem[]; - currentPage: number; - hasNext: boolean; -}; - -type KlipyResponse = { - data?: { - data?: KlipyItem[]; - current_page?: number; - has_next?: boolean; - }; -}; - -type PickerMedia = { - key: React.Key; - url: string; - alt: string; - width?: number; - height?: number; -}; - -function distributeByHeight( - items: T[], - columnCount: number, - getAspectRatio: (item: T) => number, -) { - const columns = Array.from({ length: columnCount }, () => [] as T[]); - const columnHeights = Array.from({ length: columnCount }, () => 0); - - for (const item of items) { - const shortestColumnIndex = columnHeights.indexOf( - Math.min(...columnHeights), - ); - - columns[shortestColumnIndex].push(item); - columnHeights[shortestColumnIndex] += getAspectRatio(item); - } - - return columns; -} - -function getKlipyPath(kind: KlipyKind) { - const resource = kind === "gif" ? "gifs" : "static-memes"; - - return `${resource}/search`; -} - -async function fetchKlipyPage({ - kind, - page, - search, -}: { - kind: KlipyKind; - page: number; - search: string; -}): Promise { - const params = new URLSearchParams({ - page: String(page), - per_page: String(pageSize), - content_filter: "medium", - }); - - if (search !== "") { - params.set("q", search); - } - - if (kind === "gif") { - params.set("format_filter", "gif,webp"); - } - - const response = await fetch( - `${klipyBaseUrl}/${klipyApiKey}/${getKlipyPath(kind)}?${params}`, - ); - - if (!response.ok) { - throw new Error(`Klipy request failed with status ${response.status}`); - } - - const body = (await response.json()) as KlipyResponse; - const data = body.data; - - return { - items: data?.data ?? [], - currentPage: data?.current_page ?? page, - hasNext: data?.has_next ?? false, - }; -} - -function getFormat(item: KlipyItem, size: string, format: string) { - return item.file?.[size]?.[format]; -} - -function pickDisplayMedia(kind: KlipyKind, item: KlipyItem) { - if (kind === "gif") { - return ( - getFormat(item, "sm", "webp") ?? - getFormat(item, "md", "webp") ?? - getFormat(item, "sm", "gif") ?? - getFormat(item, "md", "gif") ?? - getFormat(item, "hd", "webp") ?? - getFormat(item, "hd", "gif") - ); - } - - return ( - getFormat(item, "sm", "webp") ?? - getFormat(item, "md", "webp") ?? - getFormat(item, "sm", "png") ?? - getFormat(item, "md", "png") ?? - getFormat(item, "hd", "webp") ?? - getFormat(item, "hd", "png") - ); -} - -function pickSelectionUrl(kind: KlipyKind, item: KlipyItem) { - if (kind === "gif") { - return ( - getFormat(item, "md", "gif")?.url ?? - getFormat(item, "sm", "gif")?.url ?? - getFormat(item, "hd", "gif")?.url - ); - } - - return ( - getFormat(item, "md", "png")?.url ?? - getFormat(item, "md", "webp")?.url ?? - getFormat(item, "sm", "png")?.url ?? - getFormat(item, "sm", "webp")?.url ?? - getFormat(item, "hd", "png")?.url ?? - getFormat(item, "hd", "webp")?.url - ); -} - -function useMeasuredWidth() { - const scrollRef = useRef(null); - const [scrollWidth, setScrollWidth] = useState(0); - - useLayoutEffect(() => { - const element = scrollRef.current; - - if (!element || typeof ResizeObserver === "undefined") return; - - const updateWidth = () => setScrollWidth(element.clientWidth); - const observer = new ResizeObserver(updateWidth); - - updateWidth(); - observer.observe(element); - - return () => observer.disconnect(); - }, []); - - return [scrollRef, scrollWidth] as const; -} - -function useImageSizes(urls: string[]) { - const [sizes, setSizes] = useState< - Record - >({}); - - useEffect(() => { - let active = true; - - for (const url of urls) { - if (sizes[url]) continue; - - const image = new Image(); - - image.onload = () => { - if (!active) return; - - setSizes((current) => ({ - ...current, - [url]: { - width: image.naturalWidth, - height: image.naturalHeight, - }, - })); - }; - image.src = url; - } - - return () => { - active = false; - }; - }, [sizes, urls]); - - return sizes; -} - -function MediaGrid({ - hasNextPage = false, - isFetchingNextPage = false, - items, - onLoadMore, - onSelect, - resizeWidth, -}: { - hasNextPage?: boolean; - isFetchingNextPage?: boolean; - items: PickerMedia[]; - onLoadMore?: () => void; - onSelect: (url: string) => void; - resizeWidth?: number; -}) { - const [scrollRef, measuredWidth] = useMeasuredWidth(); - const columnWidth = resizeWidth ?? measuredWidth; - const columnCount = getColumnCount(columnWidth, items.length); - const columns = distributeByHeight(items, columnCount, (item) => { - if (!item.width || !item.height) return 1; - - return item.height / item.width; - }); - - function handleScroll(event: React.UIEvent) { - const element = event.currentTarget; - const distanceFromBottom = - element.scrollHeight - element.scrollTop - element.clientHeight; - - if ( - distanceFromBottom > 240 || - !hasNextPage || - isFetchingNextPage || - !onLoadMore - ) { - return; - } - - onLoadMore(); - } - - useEffect(() => { - const element = scrollRef.current; - - if (!element || !hasNextPage || isFetchingNextPage || !onLoadMore) return; - - const distanceFromBottom = - element.scrollHeight - element.scrollTop - element.clientHeight; - - if (distanceFromBottom <= 240) { - onLoadMore(); - } - }, [ - columnCount, - hasNextPage, - isFetchingNextPage, - items.length, - onLoadMore, - scrollRef, - ]); - - return ( -
-
- {columns.map((column, columnIndex) => ( -
- {column.map((item) => ( - - ))} -
- ))} -
-
- {hasNextPage ? ( -
- - Loading more... -
- ) : null} -
-
- ); -} - -function KlipyPanel({ - kind, - onSelect, - resizeWidth, - searchString, -}: { - kind: KlipyKind; - onSelect: (url: string) => void; - resizeWidth?: number; - searchString: string; -}) { - const query = useInfiniteQuery({ - queryKey: ["klipy", kind, searchString], - initialPageParam: 1, - enabled: searchString !== "", - queryFn: ({ pageParam }) => - fetchKlipyPage({ - kind, - page: Number(pageParam), - search: searchString, - }), - getNextPageParam: (lastPage) => - lastPage.hasNext ? lastPage.currentPage + 1 : undefined, - }); - const items: PickerMedia[] = - query.data?.pages.flatMap((page) => - page.items.flatMap((item) => { - const displayMedia = pickDisplayMedia(kind, item); - const selectionUrl = pickSelectionUrl(kind, item); - - if (!displayMedia?.url || !selectionUrl) return []; - - return [ - { - key: item.id, - url: selectionUrl, - alt: item.title ?? "Klipy result", - width: displayMedia.width, - height: displayMedia.height, - }, - ]; - }), - ) ?? []; - - if (searchString === "") { - return ( -
- -

- {kind === "gif" && 'Try searching for "Funny cat"'} - {kind === "meme" && - 'Try searching for "Spiderman pointing at Spiderman"'} -

-
- ); - } - - if (query.isLoading) { - return ( -
- - Loading... -
- ); - } - - if (query.isError) { - return ( -
-

Failed to load {kind === "gif" ? "GIFs" : "memes"}.

- -
- ); - } - - if (items.length === 0) { - return ( -
- No {kind === "gif" ? "GIFs" : "memes"} found. -
- ); - } - - return ( - void query.fetchNextPage()} - onSelect={onSelect} - resizeWidth={resizeWidth} - /> - ); -} - -function SavedPanel({ - onSelect, - onSavedMediaChange, - resizeWidth, - urls, -}: { - onSelect: (url: string) => void; - onSavedMediaChange: (savedMedia: string[]) => void; - resizeWidth?: number; - urls: string[]; -}) { - const [scrollRef, measuredWidth] = useMeasuredWidth(); - const imageSizes = useImageSizes(urls); - const columnWidth = resizeWidth ?? measuredWidth; - const columnCount = getColumnCount(columnWidth, urls.length); - const columns = distributeByHeight(urls, columnCount, (url) => { - const size = imageSizes[url]; - - if (!size?.width || !size.height) return 1; - - return size.height / size.width; - }); - - if (urls.length === 0) { - return ( -
- Saved media will appear here. -
- ); - } - - return ( -
-
- {columns.map((column, columnIndex) => ( -
- {column.map((url) => { - const size = imageSizes[url]; - - return ( -
- - -
- ); - })} -
- ))} -
-
- ); -} - -export default function GifPicker({ - onSelect, - resizeHeight, - resizeWidth, -}: { - onSelect: (url: string) => void; - resizeHeight?: number; - resizeWidth?: number; -}) { - const { load, save } = useStorage(); - const [tab, setTab] = useState("gif"); - const [searchString, setSearchString] = useState(""); - const [debouncedSearchString, setDebouncedSearchString] = useState(""); - const [savedMedia, setSavedMedia] = useState([]); - const didLoadLastTabRef = useRef(false); - const searchDebouncerRef = useRef void> | null>( - null, - ); - - if (searchDebouncerRef.current === null) { - searchDebouncerRef.current = new Debouncer( - (value) => { - startTransition(() => { - setDebouncedSearchString(value.trim()); - }); - }, - { wait: 300 }, - ); - } - - useEffect(() => { - return () => { - searchDebouncerRef.current?.cancel(); - }; - }, []); - - useEffect(() => { - if (tab !== "saved") return; - - void load("chat_picker_saved_media").then(setSavedMedia); - }, [load, tab]); - - useEffect(() => { - void load("chat_picker_last_tab").then((savedTab) => { - setTab(savedTab); - didLoadLastTabRef.current = true; - }); - }, [load]); - - useEffect(() => { - if (!didLoadLastTabRef.current) return; - - void save("chat_picker_last_tab", tab); - }, [save, tab]); - - function handleTabChange(nextTab: string) { - if (nextTab !== "gif" && nextTab !== "meme" && nextTab !== "saved") return; - - setTab(nextTab); - } - - function handleSearchChange(event: React.ChangeEvent) { - const nextValue = event.target.value; - - setSearchString(nextValue); - searchDebouncerRef.current?.maybeExecute(nextValue); - } - - return ( - - - GIFs - Memes - Saved - - - - - - - - - - - - - - ); -} diff --git a/packages/chat/src/components/input.tsx b/packages/chat/src/components/input.tsx index ae27596..cf74848 100644 --- a/packages/chat/src/components/input.tsx +++ b/packages/chat/src/components/input.tsx @@ -1,23 +1,16 @@ import Input from "@tensamin/markdown/input"; -import { - Card, - CardHeader, - Popover, - PopoverContent, - PopoverTrigger, -} from "@tensamin/ui"; +import { Card, CardHeader } from "@tensamin/ui"; import { useStorage } from "@tensamin/storage/context"; import * as React from "react"; import { Button } from "@tensamin/ui"; -import { Plus, Laugh, FileVideo } from "lucide-react"; +import { Plus, Laugh, Clapperboard } from "lucide-react"; import { useChat } from "../context"; import { useTTP } from "@tensamin/ttp"; import { log, toast } from "@tensamin/shared/log"; import { cn, useIsMobile } from "@tensamin/ui"; import { encryptText } from "@tensamin/crypto/worker"; import { useSession } from "@tensamin/storage/session"; -import GifPicker from "./gifPicker"; export default function InputComponent({ value, @@ -30,14 +23,8 @@ export default function InputComponent({ const { send } = useTTP(); const { addLiveMessage, sharedSecret, userId, inputBoxRef } = useChat(); - const { load, save } = useStorage(); + const { load } = useStorage(); const { moveUserIdToTop } = useSession(); - const gifPopoverRef = React.useRef(null); - const [gifPopoverOpen, setGifPopoverOpen] = React.useState(false); - const [gifPopoverSize, setGifPopoverSize] = React.useState<{ - width: number; - height: number; - }>(); React.useEffect(() => { void load("settings.reverse_enter_behavior").then((shouldInvert) => { @@ -45,19 +32,16 @@ export default function InputComponent({ }); }, [load]); - React.useEffect(() => { - void load("chat_picker_size").then((size) => { - if (size) { - setGifPopoverSize(size); - } - }); - }, [load]); - - async function handleSubmit(content = value, preserveContent = false) { - if (content.trim() === "") return; + /** + * Executes handleSubmit. + * @param none This function has no parameters. + * @returns unknown. + */ + async function handleSubmit() { + if (value.trim() === "") return; const time = Date.now(); - const currentValue = content; + const currentValue = value; if (!Number.isSafeInteger(userId) || userId <= 0) { toast("error", "No conversation selected"); @@ -98,70 +82,17 @@ export default function InputComponent({ moveUserIdToTop(userId); - if (!preserveContent) { - setValue(""); - } + setValue(""); } const isMobile = useIsMobile(); - function handleGifPopoverResizeStart( - event: React.PointerEvent, - ) { - const popover = gifPopoverRef.current; - - if (!popover) return; - - event.preventDefault(); - event.stopPropagation(); - - const rect = popover.getBoundingClientRect(); - const startX = event.clientX; - const startY = event.clientY; - const startWidth = rect.width; - const startHeight = rect.height; - const minSize = 40; - const maxSize = 720; - - function clampSize(size: number) { - return Math.min(Math.max(size, minSize), maxSize); - } - - function handlePointerMove(moveEvent: PointerEvent) { - const nextSize = { - width: clampSize(startWidth + startX - moveEvent.clientX), - height: clampSize(startHeight + startY - moveEvent.clientY), - }; - - setGifPopoverSize(nextSize); - } - - function handlePointerUp() { - const popover = gifPopoverRef.current; - - if (popover) { - const rect = popover.getBoundingClientRect(); - - void save("chat_picker_size", { - width: clampSize(rect.width), - height: clampSize(rect.height), - }); - } - - window.removeEventListener("pointermove", handlePointerMove); - window.removeEventListener("pointerup", handlePointerUp); - } - - window.addEventListener("pointermove", handlePointerMove); - window.addEventListener("pointerup", handlePointerUp); - } - return ( @@ -188,38 +119,9 @@ export default function InputComponent({ - - - - - } - /> - event.stopPropagation()} - onWheelCapture={(event) => event.stopPropagation()} - style={{ - ...gifPopoverSize, - maxHeight: gifPopoverSize?.height, - }} - > -
- { - void handleSubmit(url, true); - setGifPopoverOpen(false); - }} - /> - - +
diff --git a/packages/chat/src/components/media.tsx b/packages/chat/src/components/media.tsx deleted file mode 100644 index b647c77..0000000 --- a/packages/chat/src/components/media.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Text from "@tensamin/markdown/text"; -import { useStorage } from "@tensamin/storage/context"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@tensamin/ui"; -import { TriangleAlert } from "lucide-react"; -import { useState, useMemo, useEffect } from "react"; -import MediaSaveButton from "./mediaSaveButton"; - -export default function Media({ link }: { link: string }) { - const { load } = useStorage(); - - const [trustedDomains, setTrustedDomains] = useState([]); - const [hidden, setHidden] = useState(true); - const hostname = useMemo(() => new URL(link).hostname, [link]); - - useEffect(() => { - load("chat_trusted_domains").then(setTrustedDomains); - }, [load]); - - return trustedDomains.includes(hostname) ? ( -
- {hidden ? ( - - ) : ( - - )} - setHidden(false)} - className="max-h-70 max-w-70 py-1 rounded-lg" - /> -
- ) : ( -
- - } - /> - - Link embeds can get your IP-Address! You can configure trusted domains - in the settings. - - - -
- ); -} diff --git a/packages/chat/src/components/mediaSaveButton.tsx b/packages/chat/src/components/mediaSaveButton.tsx deleted file mode 100644 index 5179aac..0000000 --- a/packages/chat/src/components/mediaSaveButton.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Button, cn } from "@tensamin/ui"; -import { useStorage } from "@tensamin/storage/context"; -import { Save } from "lucide-react"; -import { useEffect, useState } from "react"; - -export default function MediaSaveButton({ - ariaLabel, - className, - defaultSaved = false, - onSavedMediaChange, - url, -}: { - ariaLabel?: string; - className?: string; - defaultSaved?: boolean; - onSavedMediaChange?: (savedMedia: string[]) => void; - url: string; -}) { - const { load, save } = useStorage(); - const [savedMedia, setSavedMedia] = useState(null); - const isSaved = savedMedia ? savedMedia.includes(url) : defaultSaved; - - useEffect(() => { - void load("chat_picker_saved_media").then(setSavedMedia); - }, [load]); - - async function handleClick(event: React.MouseEvent) { - event.stopPropagation(); - - const currentSavedMedia = - savedMedia ?? (await load("chat_picker_saved_media")); - const nextSavedMedia = currentSavedMedia.includes(url) - ? currentSavedMedia.filter((item) => item !== url) - : [url, ...currentSavedMedia]; - - setSavedMedia(nextSavedMedia); - onSavedMediaChange?.(nextSavedMedia); - void save("chat_picker_saved_media", nextSavedMedia); - } - - return ( -
- -
- ); -} diff --git a/packages/chat/src/components/message.tsx b/packages/chat/src/components/message.tsx index 51705ff..60afa75 100644 --- a/packages/chat/src/components/message.tsx +++ b/packages/chat/src/components/message.tsx @@ -2,8 +2,6 @@ import * as React from "react"; import type { RawMessage } from "../values"; import Text from "@tensamin/markdown/text"; import { AlertTriangle } from "lucide-react"; -import { useEffect, useState } from "react"; -import type { User } from "@tensamin/user/context"; import { Avatar, @@ -14,54 +12,26 @@ import { TooltipContent, TooltipTrigger, } from "@tensamin/ui"; +import Wrapper from "@tensamin/user/wrapper"; +import { useChat } from "../context"; import MessageContextMenu from "./messageContextMenu"; -import Media from "./media"; function MessageComponent({ grouped, message, - user, }: { grouped: boolean; message: RawMessage & { failed?: boolean; }; - user: User | null; }) { + const { userId } = useChat(); const actuallyFailed = message.failed && message.message_state === "awaiting"; - const [hasFadedIn, setHasFadedIn] = useState(false); - const opacityClass = !hasFadedIn - ? "opacity-0" - : actuallyFailed || message.message_state === "awaiting" - ? "opacity-50" - : "opacity-100"; - - const [isValidURL, setIsValidURL] = useState(false); - useEffect(() => { - const timeout = window.setTimeout(() => { - setHasFadedIn(true); - }, 100); - - return () => { - window.clearTimeout(timeout); - }; - }, []); - - useEffect(() => { - try { - if (message.content.split(" ").length > 1) throw new Error(); - - new URL(message.content); - setIsValidURL(true); - } catch { - setIsValidURL(false); - } - }, [message.content]); return (
- {user ? ( - <> - {grouped ? ( -

- {new Date(message.send_time).toLocaleString([], { - hour: "2-digit", - minute: "2-digit", - })} -

- ) : ( - - - - {user.display.slice(0, 2).toUpperCase()} - - - )} - {message.failed && message.message_state === "awaiting" && ( - - -

Failed to send message

-
- } /> -
- )} -
- {!grouped && ( -
-

{user.display}

-

- {new Date(message.send_time).toLocaleString([], { - hour: "2-digit", - minute: "2-digit", - })} -

-
- )} - {isValidURL ? ( - + ( + <> + {grouped ? ( +

+ {new Date(message.send_time).toLocaleString([], { + hour: "2-digit", + minute: "2-digit", + })} +

) : ( - + + + + {user.display.slice(0, 2).toUpperCase()} + + )} -
- - ) : ( - "Loading" - )} + {message.failed && message.message_state === "awaiting" && ( + + +

Failed to send message

+
+ } /> +
+ )} +
+ {!grouped && ( +
+

{user.display}

+

+ {new Date(message.send_time).toLocaleString([], { + hour: "2-digit", + minute: "2-digit", + })} +

+
+ )} + +
+ + )} + />
@@ -136,8 +104,6 @@ export default React.memo(MessageComponent, (prev, next) => { prev.message.height === next.message.height && prev.message.sent_by_self === next.message.sent_by_self && prev.message.message_state === next.message.message_state && - prev.message.failed === next.message.failed && - prev.grouped === next.grouped && - prev.user === next.user + prev.message.failed === next.message.failed ); }); diff --git a/packages/chat/src/context.tsx b/packages/chat/src/context.tsx index 09d65c3..38ee454 100644 --- a/packages/chat/src/context.tsx +++ b/packages/chat/src/context.tsx @@ -266,11 +266,7 @@ export default function Provider(props: { children: ReactNode }) { return updated ? next : prev; }); - const queryKey = [ - "chat-messages", - String(userIdValue), - currentSharedSecret.length > 0, - ] as const; + const queryKey = ["chat-messages", String(userIdValue)] as const; queryClient.setQueryData>( queryKey, (current) => { @@ -305,7 +301,7 @@ export default function Provider(props: { children: ReactNode }) { }, ); }); - }, [currentSharedSecret, subscribePush, userIdValue]); + }, [subscribePush, userIdValue]); return ( diff --git a/packages/chat/src/screen.tsx b/packages/chat/src/screen.tsx index f6bcff5..44e581b 100644 --- a/packages/chat/src/screen.tsx +++ b/packages/chat/src/screen.tsx @@ -1,108 +1,56 @@ import * as React from "react"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useVirtualizer } from "@tanstack/react-virtual"; -import { failedUser } from "@tensamin/shared/data"; -import { useStorage } from "@tensamin/storage/context"; -import { useUser, type User } from "@tensamin/user/context"; +import { useChat } from "./context"; import InputComponent from "./components/input"; import Message from "./components/message"; -import { useChat } from "./context"; -import { - PAGE_SIZE, - FALLBACK_MESSAGE_HEIGHT, - MESSAGES_PER_VIRTUAL_ROW, - type LiveMessage, - type RawMessage, -} from "./values"; -type MessageChunk = { - key: string; - messages: Array; - startIndex: number; -}; +import { PAGE_SIZE } from "./values"; +import { useIsMobile } from "@tensamin/ui"; +import { Loader2 } from "lucide-react"; -function getEstimatedMessageHeight(message: { height?: number }) { - return typeof message.height === "number" && Number.isFinite(message.height) - ? Math.max(1, Math.ceil(message.height)) - : FALLBACK_MESSAGE_HEIGHT; +function getDistanceFromBottom(element: HTMLDivElement) { + return element.scrollHeight - (element.scrollTop + element.clientHeight); } -function shouldFetchPreviousPage({ - entry, - hasNextPage, - isFetchingNextPage, - userScrolledUp, -}: { - entry: IntersectionObserverEntry | undefined; - hasNextPage: boolean; - isFetchingNextPage: boolean; - userScrolledUp: boolean; -}) { - return [ - entry?.isIntersecting === true, - userScrolledUp, - hasNextPage, - !isFetchingNextPage, - ].every(Boolean); -} - -function getMessageRenderKey(message: RawMessage | LiveMessage) { - return "localId" in message ? message.localId : String(message.send_time); -} - -function buildMessageChunks( - messages: Array, - keyPrefix: string, - startOffset = 0, -) { - const chunks: MessageChunk[] = []; - - for (let end = messages.length; end > 0; end -= MESSAGES_PER_VIRTUAL_ROW) { - const start = Math.max(0, end - MESSAGES_PER_VIRTUAL_ROW); - const chunkMessages = messages.slice(start, end); - const firstMessage = chunkMessages[0]; - - if (firstMessage) { - chunks.push({ - key: `${keyPrefix}-${getMessageRenderKey(firstMessage)}`, - messages: chunkMessages, - startIndex: startOffset + start, - }); - } - } - - return chunks; -} +const FALLBACK_MESSAGE_HEIGHT = 56; /** * Renders the chat screen with virtualized history and live message updates. * @returns Chat screen JSX. */ export default function Screen() { - const { getMessages, liveMessages, clearLiveMessages, userId, sharedSecret } = - useChat(); - const { get: getUser } = useUser(); - const { load } = useStorage(); + const { + getMessages, + liveMessages, + clearLiveMessages, + userId, + sharedSecret, + inputBoxRef, + } = useChat(); const scrollRef = React.useRef(null); - const topSentinelRef = React.useRef(null); - const didInitialScrollRef = React.useRef(false); - const userScrolledUpRef = React.useRef(false); - const isAtBottomRef = React.useRef(true); - const smoothScrollFrameRef = React.useRef(null); - const smoothScrollTargetRef = React.useRef(0); - const hasNextPageRef = React.useRef(false); - const isFetchingNextPageRef = React.useRef(false); - const fetchNextPageRef = React.useRef<(() => void) | null>(null); + const isMobile = useIsMobile(); + const stickyBottomThreshold = isMobile ? 220 : 140; - const [messageUsers, setMessageUsers] = React.useState< - Record - >({}); + const [hasScrolledToBottomInitially, setHasScrolledToBottomInitially] = + React.useState(false); const [lastLiveMessageCount, setLastLiveMessageCount] = React.useState(0); - const [didInitialScroll, setDidInitialScroll] = React.useState(false); - const [viewportHeight, setViewportHeight] = React.useState(0); - const [value, setValue] = React.useState(""); + const [prependAnchor, setPrependAnchor] = React.useState<{ + totalSize: number; + scrollTop: number; + } | null>(null); + const [scrollWidth, setScrollWidth] = React.useState(0); + const [scrollHeight, setScrollHeight] = React.useState(0); + const [inputHeight, setInputHeight] = React.useState(0); + const [measuredHeights, setMeasuredHeights] = React.useState< + Record + >({}); + const measurementRefs = React.useRef(new Map()); + const shouldRestoreBottomOnResizeRef = React.useRef(false); + const isAtBottomRef = React.useRef(true); + const shouldRestoreBottomOnFocusResizeRef = React.useRef(false); const hasValidChatUser = Number.isSafeInteger(userId) && userId > 0; const hasSharedSecret = sharedSecret.length > 0; @@ -120,153 +68,80 @@ export default function Screen() { return allPages.length * PAGE_SIZE; }, }); - const { - fetchNextPage: fetchMessagesNextPage, - hasNextPage: hasMessagesNextPage, - isFetchingNextPage: isFetchingMessagesNextPage, - } = messagesQuery; - - React.useEffect(() => { - hasNextPageRef.current = hasMessagesNextPage; - isFetchingNextPageRef.current = isFetchingMessagesNextPage; - fetchNextPageRef.current = () => { - void fetchMessagesNextPage(); - }; - }, [ - fetchMessagesNextPage, - hasMessagesNextPage, - isFetchingMessagesNextPage, - ]); - - React.useEffect(() => { - clearLiveMessages(); - didInitialScrollRef.current = false; - userScrolledUpRef.current = false; - isAtBottomRef.current = true; - setDidInitialScroll(false); - setLastLiveMessageCount(0); - }, [clearLiveMessages, userId]); - - React.useEffect(() => { - if (!hasValidChatUser) { - setMessageUsers({}); - return; - } - - let active = true; - - const loadMessageUser = async ( - key: string, - resolveUserId: () => Promise, - ) => { - try { - const resolvedUserId = await resolveUserId(); - const value = await getUser(resolvedUserId); - - if (active) { - setMessageUsers((prev) => ({ ...prev, [key]: value })); - } - } catch { - if (active) { - setMessageUsers((prev) => ({ ...prev, [key]: failedUser })); - } - } - }; - - setMessageUsers({}); - void loadMessageUser("peer", async () => userId); - void loadMessageUser("own", async () => Number(await load("user_id"))); - - return () => { - active = false; - }; - }, [getUser, hasValidChatUser, load, userId]); const historicalMessages = React.useMemo(() => { const pages = messagesQuery.data?.pages ?? []; - const seenSendTimes = new Set(); - const dedupedMessages: RawMessage[] = []; - - for (const message of [...pages].reverse().flat()) { - if (seenSendTimes.has(message.send_time)) { - continue; - } - - seenSendTimes.add(message.send_time); - dedupedMessages.push(message); - } - - return dedupedMessages; + return [...pages].reverse().flat(); }, [messagesQuery.data]); + React.useEffect(() => { + clearLiveMessages(); + setHasScrolledToBottomInitially(false); + setLastLiveMessageCount(0); + setPrependAnchor(null); + }, [userId, clearLiveMessages]); + const liveMessagesSnapshot = liveMessages(); - const liveWithoutDuplicates = React.useMemo(() => { + const messages = React.useMemo(() => { + if (historicalMessages.length === 0) { + return liveMessagesSnapshot; + } + const historicalSendTimes = new Set( historicalMessages.map((message) => message.send_time), ); - - return liveMessagesSnapshot.filter( + const liveWithoutDuplicates = liveMessagesSnapshot.filter( (message) => !historicalSendTimes.has(message.send_time), ); - }, [historicalMessages, liveMessagesSnapshot]); - const messages = React.useMemo(() => { return [...historicalMessages, ...liveWithoutDuplicates]; - }, [historicalMessages, liveWithoutDuplicates]); - - const historicalMessageChunks = React.useMemo(() => { - return buildMessageChunks(historicalMessages, "history"); - }, [historicalMessages]); - - const liveMessageChunks = React.useMemo(() => { - return buildMessageChunks( - liveWithoutDuplicates, - "live", - historicalMessages.length, - ); - }, [historicalMessages.length, liveWithoutDuplicates]); - - const messageChunks = React.useMemo(() => { - return [...liveMessageChunks, ...historicalMessageChunks]; - }, [historicalMessageChunks, liveMessageChunks]); - + }, [historicalMessages, liveMessagesSnapshot]); const shouldShowConversationStart = !!messagesQuery.data && !messagesQuery.hasNextPage; const virtualRowCount = - messageChunks.length + (shouldShowConversationStart ? 1 : 0); + messages.length + (shouldShowConversationStart ? 1 : 0); const getItemKey = React.useCallback( (index: number) => { - if (shouldShowConversationStart && index === messageChunks.length) { + if (shouldShowConversationStart && index === 0) { return "conversation-start"; } - return messageChunks[index]?.key ?? index; + const messageIndex = shouldShowConversationStart ? index - 1 : index; + return messages[messageIndex]?.send_time ?? index; }, - [messageChunks, shouldShowConversationStart], + [shouldShowConversationStart, messages], ); - const estimateSize = React.useCallback( - (index: number) => { - const isConversationStart = - shouldShowConversationStart && index === messageChunks.length; - if (isConversationStart) { - return FALLBACK_MESSAGE_HEIGHT; + const setMeasurementRef = React.useCallback( + (sendTime: number, element: HTMLDivElement | null) => { + if (!element) { + measurementRefs.current.delete(sendTime); + return; } - const chunk = messageChunks[index]; - - if (!chunk) { - return FALLBACK_MESSAGE_HEIGHT; - } - - return chunk.messages.reduce( - (total, message) => total + getEstimatedMessageHeight(message), - 0, - ); + measurementRefs.current.set(sendTime, element); }, - [messageChunks, shouldShowConversationStart], + [], + ); + + const estimateMessageSize = React.useCallback( + (index: number) => { + if (shouldShowConversationStart && index === 0) { + return FALLBACK_MESSAGE_HEIGHT; + } + + const messageIndex = shouldShowConversationStart ? index - 1 : index; + const sendTime = messages[messageIndex]?.send_time; + + if (sendTime === undefined) { + return FALLBACK_MESSAGE_HEIGHT; + } + + return measuredHeights[sendTime] ?? FALLBACK_MESSAGE_HEIGHT; + }, + [measuredHeights, messages, shouldShowConversationStart], ); // eslint-disable-next-line react-hooks/incompatible-library @@ -274,12 +149,9 @@ export default function Screen() { count: virtualRowCount, getScrollElement: () => scrollRef.current, getItemKey, - estimateSize, - overscan: 2, + estimateSize: estimateMessageSize, + overscan: isMobile ? 10 : 6, }); - const totalSize = virtualizer.getTotalSize(); - const contentHeight = Math.max(totalSize, viewportHeight); - const verticalOffset = Math.max(0, viewportHeight - totalSize); React.useLayoutEffect(() => { const element = scrollRef.current; @@ -287,13 +159,14 @@ export default function Screen() { return; } - const updateViewportHeight = () => { - setViewportHeight(element.clientHeight); + const updateDimensions = () => { + setScrollWidth(element.clientWidth); + setScrollHeight(element.clientHeight); }; - updateViewportHeight(); + updateDimensions(); - const observer = new ResizeObserver(updateViewportHeight); + const observer = new ResizeObserver(updateDimensions); observer.observe(element); return () => { @@ -302,185 +175,277 @@ export default function Screen() { }, []); React.useLayoutEffect(() => { - if (didInitialScrollRef.current || virtualRowCount === 0) { + const frame = requestAnimationFrame(() => { + virtualizer.measure(); + }); + + return () => { + cancelAnimationFrame(frame); + }; + }, [inputHeight, measuredHeights, scrollWidth, virtualizer]); + + React.useLayoutEffect(() => { + if (typeof ResizeObserver === "undefined") { return; } - requestAnimationFrame(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = 0; + const measureHeights = () => { + setMeasuredHeights((prev) => { + let changed = false; + const next: Record = {}; + + for (const message of messages) { + const element = measurementRefs.current.get(message.send_time); + const measuredHeight = element?.getBoundingClientRect().height; + const nextHeight = + typeof measuredHeight === "number" && + Number.isFinite(measuredHeight) + ? Math.ceil(measuredHeight) + : (prev[message.send_time] ?? FALLBACK_MESSAGE_HEIGHT); + + next[message.send_time] = nextHeight; + + if (prev[message.send_time] !== nextHeight) { + changed = true; + } + } + + if (!changed && Object.keys(prev).length === messages.length) { + return prev; + } + + return next; + }); + }; + + measureHeights(); + + const observer = new ResizeObserver(() => { + measureHeights(); + }); + + for (const message of messages) { + const element = measurementRefs.current.get(message.send_time); + if (element) { + observer.observe(element); } + } + + return () => { + observer.disconnect(); + }; + }, [messages, scrollWidth]); + + React.useEffect(() => { + if (typeof window === "undefined") { + return; + } + + const restoreBottomAfterResize = () => { + shouldRestoreBottomOnResizeRef.current = + isAtBottomRef.current || shouldRestoreBottomOnFocusResizeRef.current; requestAnimationFrame(() => { - didInitialScrollRef.current = true; - setDidInitialScroll(true); + virtualizer.measure(); + + requestAnimationFrame(() => { + if (!scrollRef.current || !shouldRestoreBottomOnResizeRef.current) { + return; + } + + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + isAtBottomRef.current = true; + shouldRestoreBottomOnFocusResizeRef.current = false; + shouldRestoreBottomOnResizeRef.current = false; + }); }); + }; + + window.addEventListener("resize", restoreBottomAfterResize); + window.visualViewport?.addEventListener("resize", restoreBottomAfterResize); + + return () => { + window.removeEventListener("resize", restoreBottomAfterResize); + window.visualViewport?.removeEventListener( + "resize", + restoreBottomAfterResize, + ); + }; + }, [virtualizer]); + + React.useLayoutEffect(() => { + const element = inputBoxRef.current; + if (!element || typeof ResizeObserver === "undefined") { + return; + } + + const updateHeight = () => { + setInputHeight(element.getBoundingClientRect().height); + }; + + updateHeight(); + + const observer = new ResizeObserver(updateHeight); + observer.observe(element); + + return () => { + observer.disconnect(); + }; + }, [inputBoxRef]); + + React.useEffect(() => { + const element = inputBoxRef.current; + if (!element) { + return; + } + + const handleFocusIn = () => { + if (!scrollRef.current) { + return; + } + + shouldRestoreBottomOnFocusResizeRef.current = + getDistanceFromBottom(scrollRef.current) <= stickyBottomThreshold; + }; + + const handleFocusOut = () => { + shouldRestoreBottomOnFocusResizeRef.current = false; + }; + + element.addEventListener("focusin", handleFocusIn); + element.addEventListener("focusout", handleFocusOut); + + return () => { + element.removeEventListener("focusin", handleFocusIn); + element.removeEventListener("focusout", handleFocusOut); + }; + }, [inputBoxRef, stickyBottomThreshold]); + + /** + * Loads the next page when the scroll container reaches the top. + * @returns Promise that resolves once pagination handling completes. + */ + const onScroll = React.useCallback(async () => { + if (!scrollRef.current) { + return; + } + + if (scrollRef.current.scrollTop > 96) { + return; + } + + if (messagesQuery.isFetchingNextPage || !messagesQuery.hasNextPage) { + return; + } + + setPrependAnchor({ + totalSize: virtualizer.getTotalSize(), + scrollTop: scrollRef.current.scrollTop, }); - }, [virtualizer, virtualRowCount]); + + await messagesQuery.fetchNextPage(); + }, [messagesQuery, virtualizer]); + + React.useLayoutEffect(() => { + if ( + !scrollRef.current || + hasScrolledToBottomInitially || + virtualRowCount === 0 + ) { + return; + } + + let rafId: number; + let stableFrames = 0; + let lastScrollHeight = 0; + + const settleToBottom = () => { + if (!scrollRef.current || hasScrolledToBottomInitially) { + return; + } + + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + const currentScrollHeight = scrollRef.current.scrollHeight; + + if (currentScrollHeight === lastScrollHeight) { + stableFrames++; + } else { + stableFrames = 0; + lastScrollHeight = currentScrollHeight; + } + + if (stableFrames >= 2) { + isAtBottomRef.current = true; + setHasScrolledToBottomInitially(true); + } else { + rafId = requestAnimationFrame(settleToBottom); + } + }; + + settleToBottom(); + + return () => { + cancelAnimationFrame(rafId); + }; + }, [hasScrolledToBottomInitially, virtualRowCount]); React.useLayoutEffect(() => { const count = liveMessagesSnapshot.length; - if (count > lastLiveMessageCount && isAtBottomRef.current) { - requestAnimationFrame(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = 0; - } - }); + if (!scrollRef.current) { + return; + } + + if (count > lastLiveMessageCount) { + const shouldStickToBottom = + getDistanceFromBottom(scrollRef.current) <= stickyBottomThreshold; + + if (shouldStickToBottom) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + isAtBottomRef.current = true; + } } setLastLiveMessageCount(count); }, [ lastLiveMessageCount, liveMessagesSnapshot.length, - virtualRowCount, - virtualizer, + stickyBottomThreshold, ]); - React.useEffect(() => { - if ( - viewportHeight === 0 || - totalSize > viewportHeight || - messagesQuery.isFetchingNextPage || - !messagesQuery.hasNextPage - ) { - return; - } - - void messagesQuery.fetchNextPage(); - }, [didInitialScroll, messagesQuery, totalSize, viewportHeight]); - React.useLayoutEffect(() => { if ( - !didInitialScroll || - userScrolledUpRef.current || - virtualRowCount === 0 + !scrollRef.current || + !prependAnchor || + messagesQuery.isFetchingNextPage ) { return; } - requestAnimationFrame(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = 0; - } - }); - }, [didInitialScroll, virtualRowCount]); + const delta = virtualizer.getTotalSize() - prependAnchor.totalSize; + scrollRef.current.scrollTop = prependAnchor.scrollTop + delta; + setPrependAnchor(null); + }, [messagesQuery.isFetchingNextPage, prependAnchor, virtualizer]); - React.useEffect(() => { - const root = scrollRef.current; - const sentinel = topSentinelRef.current; - - if (!root || !sentinel || !didInitialScroll) { - return; + /** + * Triggers asynchronous scroll pagination without returning a promise to JSX. + * @returns Void. + */ + const handleContainerScroll = React.useCallback((): void => { + if (scrollRef.current) { + isAtBottomRef.current = + getDistanceFromBottom(scrollRef.current) <= stickyBottomThreshold; } - const observer = new IntersectionObserver( - (entries) => { - const entry = entries[0]; - if ( - shouldFetchPreviousPage({ - entry, - hasNextPage: messagesQuery.hasNextPage, - isFetchingNextPage: messagesQuery.isFetchingNextPage, - userScrolledUp: userScrolledUpRef.current, - }) - ) { - void messagesQuery.fetchNextPage(); - } - }, - { root, rootMargin: "240px 0px 0px 0px" }, - ); + void onScroll(); + }, [onScroll, stickyBottomThreshold]); - observer.observe(sentinel); - - return () => { - observer.disconnect(); - }; - }, [didInitialScroll, messagesQuery, virtualRowCount]); - - const handleContainerScroll = React.useCallback(() => { - if (!scrollRef.current) { - return; - } - - isAtBottomRef.current = scrollRef.current.scrollTop <= 140; - - if (didInitialScrollRef.current && scrollRef.current.scrollTop > 140) { - userScrolledUpRef.current = true; - } - }, []); - - React.useEffect(() => { - const element = scrollRef.current; - if (!element) { - return; - } - - smoothScrollTargetRef.current = element.scrollTop; - - const animateScroll = () => { - const distance = smoothScrollTargetRef.current - element.scrollTop; - if (Math.abs(distance) < 0.5) { - element.scrollTop = smoothScrollTargetRef.current; - smoothScrollFrameRef.current = null; - handleContainerScroll(); - return; - } - - element.scrollTop += distance * 0.35; - handleContainerScroll(); - smoothScrollFrameRef.current = requestAnimationFrame(animateScroll); - }; - - const fetchNextPageNearTop = (scrollTop: number, maxScrollTop: number) => { - if (didInitialScrollRef.current && scrollTop > 140) { - userScrolledUpRef.current = true; - } - - if ( - scrollTop >= maxScrollTop - 240 && - userScrolledUpRef.current && - hasNextPageRef.current && - !isFetchingNextPageRef.current - ) { - fetchNextPageRef.current?.(); - } - }; - - const handleWheel = (event: WheelEvent) => { - event.preventDefault(); - event.stopPropagation(); - - if (smoothScrollFrameRef.current === null) { - smoothScrollTargetRef.current = element.scrollTop; - } - - const maxScrollTop = Math.max( - 0, - element.scrollHeight - element.clientHeight, - ); - smoothScrollTargetRef.current = Math.min( - Math.max(0, smoothScrollTargetRef.current - event.deltaY), - maxScrollTop, - ); - fetchNextPageNearTop(smoothScrollTargetRef.current, maxScrollTop); - - if (smoothScrollFrameRef.current === null) { - smoothScrollFrameRef.current = requestAnimationFrame(animateScroll); - } - }; - - element.addEventListener("wheel", handleWheel, { passive: false }); - - return () => { - element.removeEventListener("wheel", handleWheel); - if (smoothScrollFrameRef.current !== null) { - cancelAnimationFrame(smoothScrollFrameRef.current); - smoothScrollFrameRef.current = null; - } - }; - }, [handleContainerScroll]); + const [value, setValue] = React.useState(""); + const totalSize = virtualizer.getTotalSize(); + const verticalOffset = Math.max(0, scrollHeight - totalSize); + const contentHeight = Math.max(totalSize, scrollHeight); + // Render if (!hasValidChatUser) { return (
@@ -497,24 +462,31 @@ export default function Screen() { className="min-h-0 flex-1 overflow-y-auto" style={{ overflowAnchor: "none", - paddingTop: "22px", - transform: "scaleY(-1)", + paddingBottom: "22px", }} onScroll={handleContainerScroll} > + {messagesQuery.isPending && ( +
+

+ + Loading... +

+
+ )} + {messagesQuery.isFetchingNextPage && ( +
+ +
+ )}
-
{virtualizer.getVirtualItems().map((virtualRow) => { - if ( - shouldShowConversationStart && - virtualRow.index === messageChunks.length - ) { + if (shouldShowConversationStart && virtualRow.index === 0) { return (
-
+
Conversation start
@@ -537,50 +509,35 @@ export default function Screen() { ); } - const chunkIndex = virtualRow.index; - const chunk = messageChunks[chunkIndex]; - if (!chunk) { + const messageIndex = shouldShowConversationStart + ? virtualRow.index - 1 + : virtualRow.index; + const message = messages[messageIndex]; + if (!message) { return null; } + const lastMessage = messages[messageIndex - 1]; + const isGrouped = + lastMessage && + lastMessage.sent_by_self === message.sent_by_self && + Math.round(lastMessage.send_time / 10000) === + Math.round(message.send_time / 10000); + return (
-
- {chunk.messages.map((message, chunkMessageIndex) => { - const messageIndex = chunk.startIndex + chunkMessageIndex; - const lastMessage = messages[messageIndex - 1]; - const isGrouped = - lastMessage && - lastMessage.sent_by_self === message.sent_by_self && - Math.round(lastMessage.send_time / 10000) === - Math.round(message.send_time / 10000); - - return ( - - ); - })} -
+
); })} @@ -589,6 +546,31 @@ export default function Screen() {
+
); } diff --git a/packages/chat/src/values.ts b/packages/chat/src/values.ts index 8a97ef0..880091c 100644 --- a/packages/chat/src/values.ts +++ b/packages/chat/src/values.ts @@ -10,6 +10,4 @@ export type LiveMessage = RawMessage & { localId: string; }; -export const PAGE_SIZE = 30; -export const FALLBACK_MESSAGE_HEIGHT = 28; -export const MESSAGES_PER_VIRTUAL_ROW = 30; +export const PAGE_SIZE = 50; diff --git a/packages/shared/src/data.ts b/packages/shared/src/data.ts index aa14339..2210192 100644 --- a/packages/shared/src/data.ts +++ b/packages/shared/src/data.ts @@ -257,16 +257,8 @@ export interface Storage extends SettingsStorageDefaults { cached_contacts: Contacts; cached_communities: Communities; ttp_url: string; - ttp_server_cert: string; call_mute_range_start: number; call_mute_range_end: number; - chat_trusted_domains: string[]; - chat_picker_saved_media: string[]; - chat_picker_last_tab: "gif" | "meme" | "saved"; - chat_picker_size: { - width: number; - height: number; - } | null; } export const storageDefaults: Storage = { @@ -300,36 +292,8 @@ export const storageDefaults: Storage = { cached_contacts: [], cached_communities: [], ttp_url: "https://tensamin.net:959", - ttp_server_cert: "", call_mute_range_start: -55, call_mute_range_end: -45, - chat_trusted_domains: [ - // Other platforms - "cdn.discordapp.com", - - // Tenor - "tenor.com", - "c.tenor.com", - - // Klipy - "static.klipy.com", - "static1.klipy.com", - "static2.klipy.com", - - // Giphy - "giphy.com", - "www.giphy.com", - "media.giphy.com", - "i.giphy.com", - "media0.giphy.com", - "media1.giphy.com", - "media2.giphy.com", - "media3.giphy.com", - "media4.giphy.com", - ], - chat_picker_saved_media: [], - chat_picker_last_tab: "gif", - chat_picker_size: null, }; // User Status diff --git a/packages/ttp/src/context.tsx b/packages/ttp/src/context.tsx index 50eec69..d33313d 100644 --- a/packages/ttp/src/context.tsx +++ b/packages/ttp/src/context.tsx @@ -195,14 +195,10 @@ export function Provider(props: { // Load ttp url const [ttpUrl, setTtpUrl] = useState(null); - const [ttpServerCert, setTtpServerCert] = useState(""); useEffect(() => { load("ttp_url").then((url) => { setTtpUrl(url); }); - load("ttp_server_cert").then((cert) => { - setTtpServerCert(cert.trim()); - }); }, [load]); /** @@ -305,6 +301,7 @@ export function Provider(props: { let reconnectScheduled = false; let disposed = false; let resumeListenerRegistered = false; + let currentReadyState: number = READY_STATE.CLOSED; /** * Clears any scheduled reconnect timeout and resets scheduling flags. @@ -376,8 +373,8 @@ export function Provider(props: { const transportClient = !props.blockConnection ? createTransportClient(schemas, { url: ttpUrl, - serverCert: ttpServerCert || undefined, onReadyStateChange: (state) => { + currentReadyState = state; setReadyState(state); if (state === READY_STATE.OPEN) { @@ -450,6 +447,13 @@ export function Provider(props: { return; } + if ( + currentReadyState === READY_STATE.OPEN || + currentReadyState === READY_STATE.CONNECTING + ) { + return; + } + clearReconnectTimer(); clearReconnectResetTimer(); attempts = 0; @@ -483,13 +487,14 @@ export function Provider(props: { } void transportClient?.close("context-dispose"); + currentReadyState = READY_STATE.CLOSED; setReadyState(READY_STATE.CLOSED); setConnected(false); setIdentified(false); setIdentifying(false); identificationStartedRef.current = false; }; - }, [props.blockConnection, ttpServerCert, ttpUrl]); + }, [props.blockConnection, ttpUrl]); useEffect(() => { if (!connected) {