Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7dc68d1867 | |||
| ff41763d76 | |||
| f3cf836fcd | |||
| 41517631ee | |||
| 4c0f2fde91 | |||
| 1e90171ee9 | |||
|
|
9f3d610bb4 |
23 changed files with 947 additions and 440 deletions
|
|
@ -18,9 +18,11 @@
|
|||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev:mobile:raw": "tauri android dev",
|
||||
"dev:mobile:raw": "tauri android dev --host ${TAURI_DEV_HOST:-127.0.0.1}",
|
||||
"start-adb:mobile:raw": "adb devices",
|
||||
"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 .",
|
||||
|
|
@ -30,6 +32,7 @@
|
|||
"@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": "*",
|
||||
|
|
|
|||
329
apps/tauri/src-tauri/Cargo.lock
generated
329
apps/tauri/src-tauri/Cargo.lock
generated
|
|
@ -8,6 +8,17 @@ 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"
|
||||
|
|
@ -32,6 +43,23 @@ 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"
|
||||
|
|
@ -47,6 +75,12 @@ 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"
|
||||
|
|
@ -326,6 +360,18 @@ 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"
|
||||
|
|
@ -366,6 +412,30 @@ 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"
|
||||
|
|
@ -402,6 +472,40 @@ 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"
|
||||
|
|
@ -1069,6 +1173,16 @@ 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"
|
||||
|
|
@ -1132,6 +1246,15 @@ 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"
|
||||
|
|
@ -1218,6 +1341,12 @@ 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"
|
||||
|
|
@ -1604,6 +1733,9 @@ name = "hashbrown"
|
|||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
|
|
@ -2186,6 +2318,9 @@ 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"
|
||||
|
|
@ -2395,6 +2530,15 @@ 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"
|
||||
|
|
@ -2986,6 +3130,26 @@ 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"
|
||||
|
|
@ -3081,6 +3245,12 @@ 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"
|
||||
|
|
@ -3201,9 +3371,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
version = "1.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
|
@ -3224,9 +3394,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rend"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
|
||||
dependencies = [
|
||||
"bytecheck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
|
|
@ -3276,6 +3455,35 @@ 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"
|
||||
|
|
@ -3286,6 +3494,23 @@ 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"
|
||||
|
|
@ -3469,6 +3694,12 @@ 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"
|
||||
|
|
@ -3724,6 +3955,12 @@ 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"
|
||||
|
|
@ -3878,6 +4115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
|
|
@ -3976,6 +4214,12 @@ 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"
|
||||
|
|
@ -4175,6 +4419,28 @@ 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"
|
||||
|
|
@ -4394,10 +4660,11 @@ 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-native",
|
||||
"ttp-tauri",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4448,7 +4715,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
|||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"libc",
|
||||
"num-conv",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
|
|
@ -4773,7 +5042,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
|||
[[package]]
|
||||
name = "ttp-core"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.methanium.net/tensamin/ttp.git#23438fa8f884e6ad0d32ca1004c0dedcce0cc8d2"
|
||||
source = "git+https://git.methanium.net/tensamin/ttp.git#0ccc2b808c1f8ff38ede906491ef703a51bd7786"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"byteorder",
|
||||
|
|
@ -4786,7 +5055,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "ttp-native"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.methanium.net/tensamin/ttp.git#23438fa8f884e6ad0d32ca1004c0dedcce0cc8d2"
|
||||
source = "git+https://git.methanium.net/tensamin/ttp.git#0ccc2b808c1f8ff38ede906491ef703a51bd7786"
|
||||
dependencies = [
|
||||
"quinn",
|
||||
"rustls",
|
||||
|
|
@ -4794,9 +5063,25 @@ 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"
|
||||
|
|
@ -4922,6 +5207,12 @@ 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"
|
||||
|
|
@ -4940,6 +5231,12 @@ 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"
|
||||
|
|
@ -5193,6 +5490,15 @@ 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"
|
||||
|
|
@ -5888,6 +6194,15 @@ 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"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ 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-native = { git = "https://git.methanium.net/tensamin/ttp.git", package = "ttp-native" }
|
||||
ttp-tauri = { git = "https://git.methanium.net/tensamin/ttp.git", package = "ttp-tauri" }
|
||||
tauri-plugin-log = "2"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.tauri]
|
||||
version = "2"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@
|
|||
"core:window:allow-minimize",
|
||||
"core:event:default",
|
||||
"deep-link:default",
|
||||
"notification:default"
|
||||
"notification:default",
|
||||
"log:default",
|
||||
"ttp-tauri:allow-connect",
|
||||
"ttp-tauri:allow-send",
|
||||
"ttp-tauri:allow-close",
|
||||
"ttp-tauri:allow-ready-state"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@
|
|||
"barcode-scanner:default",
|
||||
"barcode-scanner:allow-scan",
|
||||
"barcode-scanner:allow-cancel",
|
||||
"notification:default"
|
||||
"notification:default",
|
||||
"log:default",
|
||||
"ttp-tauri:allow-connect",
|
||||
"ttp-tauri:allow-send",
|
||||
"ttp-tauri:allow-close",
|
||||
"ttp-tauri:allow-ready-state"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ android {
|
|||
}
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
applicationIdSuffix = ".dev"
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let builder = tauri::Builder::default().plugin(tauri_plugin_notification::init());
|
||||
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 = builder
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
// 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()
|
||||
mobile_lib::run();
|
||||
log("Test test 123")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@
|
|||
}
|
||||
},
|
||||
"bundle": {
|
||||
"android": {
|
||||
"debugApplicationIdSuffix": ".dev"
|
||||
},
|
||||
"active": true,
|
||||
"targets": [],
|
||||
"icon": [
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .."
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@fontsource-variable/public-sans": "^5.2.7",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"@tanstack/react-router": "^1.169.1",
|
||||
"@tanstack/react-virtual": "^3.13.24",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
import { Label } from "@tensamin/ui";
|
||||
import { Switch as UISwitch } from "@tensamin/ui";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
Input,
|
||||
Label,
|
||||
Switch as UISwitch,
|
||||
} from "@tensamin/ui";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { Storage } from "@tensamin/shared/data";
|
||||
import { storageDefaults, type Storage } from "@tensamin/shared/data";
|
||||
import { settingsStorageDefaults } from "@tensamin/shared/settings";
|
||||
import { useStorage } from "@tensamin/storage/context";
|
||||
|
||||
|
|
@ -10,6 +19,13 @@ 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<K extends ListStorageKey> =
|
||||
Storage[K] extends Array<infer Item> ? Item : never;
|
||||
|
||||
export function Switch({
|
||||
label,
|
||||
id,
|
||||
|
|
@ -38,3 +54,125 @@ export function Switch({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function List<K extends ListStorageKey>({
|
||||
label,
|
||||
id,
|
||||
}: {
|
||||
label: React.ReactNode;
|
||||
id: K;
|
||||
}) {
|
||||
const { save, load } = useStorage();
|
||||
const [items, setItems] = useState<Storage[K]>(storageDefaults[id]);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [selectedItems, setSelectedItems] = useState<Set<number>>(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<K> => {
|
||||
const referenceItem = items[0] ?? storageDefaults[id][0];
|
||||
|
||||
if (typeof referenceItem === "number") {
|
||||
return Number(value) as ListStorageItem<K>;
|
||||
}
|
||||
|
||||
return value as ListStorageItem<K>;
|
||||
};
|
||||
|
||||
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<number>) => {
|
||||
const nextItems = items.filter(
|
||||
(_, index) => !indexes.has(index),
|
||||
) as Storage[K];
|
||||
|
||||
setItems(nextItems);
|
||||
setSelectedItems(new Set());
|
||||
save(id, nextItems);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 pt-4">
|
||||
<Label>{label}</Label>
|
||||
<div className="flex flex-col gap-0 overflow-hidden p-1 border-2 rounded-xl">
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={(event) => setInputValue(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") addItem();
|
||||
}}
|
||||
/>
|
||||
<Button onClick={addItem}>Add item</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0">
|
||||
{items.map((item, index) => {
|
||||
const labelId = `${String(id)}-${index}`;
|
||||
const selected = selectedItems.has(index);
|
||||
const deletingSelectedItems = selectedItems.size > 1;
|
||||
|
||||
return (
|
||||
<ContextMenu key={`${String(item)}-${index}`}>
|
||||
<ContextMenuTrigger
|
||||
render={
|
||||
<div className="grid grid-cols-[auto_auto_1fr] items-center gap-2 border-b px-1 py-2 last:border-b-0">
|
||||
<Checkbox
|
||||
id={labelId}
|
||||
checked={selected}
|
||||
onCheckedChange={(checked) => {
|
||||
setSelectedItems((previous) => {
|
||||
const nextSelected = new Set(previous);
|
||||
|
||||
if (checked) {
|
||||
nextSelected.add(index);
|
||||
} else {
|
||||
nextSelected.delete(index);
|
||||
}
|
||||
|
||||
return nextSelected;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor={labelId}>{String(item)}</Label>
|
||||
<div />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
variant="destructive"
|
||||
onClick={() =>
|
||||
deleteItems(
|
||||
deletingSelectedItems
|
||||
? selectedItems
|
||||
: new Set([index]),
|
||||
)
|
||||
}
|
||||
>
|
||||
{deletingSelectedItems ? "Delete Selected" : "Delete"}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
@import "tailwindcss";
|
||||
@import "tailwind-scrollbar-hide/v4";
|
||||
@import "tw-animate-css";
|
||||
@import "@fontsource-variable/inter";
|
||||
@import "@fontsource-variable/public-sans";
|
||||
|
||||
@source "./**/*.{ts,tsx}";
|
||||
@source "../../../packages/**/src/**/*.{ts,tsx}";
|
||||
|
||||
@theme {
|
||||
--font-sans: "Inter Variable", sans-serif;
|
||||
--font-sans: "Public Sans Variable", sans-serif;
|
||||
|
||||
--animate-wiggle: wiggle 0.5s ease-in-out infinite;
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ body,
|
|||
}
|
||||
|
||||
body {
|
||||
font-family: "Inter Variable", sans-serif;
|
||||
font-family: "Public Sans Variable", sans-serif;
|
||||
}
|
||||
|
||||
#root {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Switch } from "@/features/settings/components";
|
||||
import { List, Switch } from "@/features/settings/components";
|
||||
import { Kbd } from "@tensamin/ui";
|
||||
|
||||
export default function Page() {
|
||||
|
|
@ -25,6 +25,7 @@ export default function Page() {
|
|||
label="Sidebar message preview"
|
||||
id="settings.show_start_of_last_message_in_sidebar"
|
||||
/>
|
||||
<List label="Trusted embed domains" id="chat_trusted_domains" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,9 @@ 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,
|
||||
|
|
|
|||
11
bun.lock
11
bun.lock
|
|
@ -43,6 +43,7 @@
|
|||
"@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": "*",
|
||||
|
|
@ -58,7 +59,7 @@
|
|||
"name": "@tensamin/web",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@fontsource-variable/public-sans": "^5.2.7",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"@tanstack/react-router": "^1.169.1",
|
||||
"@tanstack/react-virtual": "^3.13.24",
|
||||
|
|
@ -255,7 +256,7 @@
|
|||
},
|
||||
},
|
||||
"overrides": {
|
||||
"@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.20.tar.gz",
|
||||
"@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.24.tar.gz",
|
||||
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.36.tar.gz",
|
||||
},
|
||||
"packages": {
|
||||
|
|
@ -471,6 +472,8 @@
|
|||
|
||||
"@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=="],
|
||||
|
|
@ -719,6 +722,8 @@
|
|||
|
||||
"@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"],
|
||||
|
|
@ -743,7 +748,7 @@
|
|||
|
||||
"@tensamin/ttp": ["@tensamin/ttp@workspace:packages/ttp"],
|
||||
|
||||
"@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/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/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=="],
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
outputs = {nixpkgs, ...}: let
|
||||
systems = ["x86_64-linux"];
|
||||
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||
version = "0.0.8";
|
||||
x86_64DebHash = "sha256-xC8YHVk+JpeMvXHB1dZihYLFyzFDXne4bkV6TZiOFrE=";
|
||||
version = "0.0.9";
|
||||
x86_64DebHash = "sha256-8rVb12SGGSac6l1NVPGyRLsAKAGDAOJVUwG8l+hoRCU=";
|
||||
forgejoBaseUrl = "https://git.methanium.net/tensamin/client/releases/download/${version}";
|
||||
in {
|
||||
packages = forAllSystems (system: let
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"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",
|
||||
|
|
@ -44,7 +45,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.20.tar.gz"
|
||||
"@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.24.tar.gz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tensamin/ttp-core": "*",
|
||||
|
|
|
|||
42
packages/chat/src/components/media.tsx
Normal file
42
packages/chat/src/components/media.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
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";
|
||||
|
||||
export default function Media({ link }: { link: string }) {
|
||||
const { load } = useStorage();
|
||||
|
||||
const [trustedDomains, setTrustedDomains] = useState<string[]>([]);
|
||||
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 && <Text value={link} />}
|
||||
<img
|
||||
src={link}
|
||||
hidden={hidden}
|
||||
onLoad={() => setHidden(false)}
|
||||
className="max-h-70 max-w-70"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex gap-1 items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<TriangleAlert className="size-4 stroke-yellow-400" />}
|
||||
/>
|
||||
<TooltipContent>
|
||||
Link embeds can expose your ip address. You can configure trusted
|
||||
domains in the settings.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Text value={link} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ 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,
|
||||
|
|
@ -12,26 +14,54 @@ 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 (
|
||||
<div
|
||||
// pt-3 is to get a gap between messages
|
||||
className={`${grouped ? "" : "pt-3"} w-full flex justify-start transition-opacity duration-150 ${actuallyFailed || message.message_state === "awaiting" ? "opacity-50" : ""}`}
|
||||
className={`${grouped ? "" : "pt-3"} w-full flex justify-start transition-opacity duration-150 ${opacityClass}`}
|
||||
>
|
||||
<MessageContextMenu
|
||||
content={message.content}
|
||||
|
|
@ -46,51 +76,53 @@ function MessageComponent({
|
|||
},
|
||||
)}
|
||||
>
|
||||
<Wrapper
|
||||
loading={"State not achievable"}
|
||||
userId={message.sent_by_self ? "own" : userId}
|
||||
component={(user) => (
|
||||
<>
|
||||
{grouped ? (
|
||||
<p className="w-9 text-xs group-hover:visible invisible text-muted-foreground">
|
||||
{new Date(message.send_time).toLocaleString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
{user ? (
|
||||
<>
|
||||
{grouped ? (
|
||||
<p className="w-9 text-xs group-hover:visible invisible text-muted-foreground">
|
||||
{new Date(message.send_time).toLocaleString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<Avatar className="mr-1 mb-auto mt-1 w-10">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback>
|
||||
{user.display.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
{message.failed && message.message_state === "awaiting" && (
|
||||
<Tooltip>
|
||||
<TooltipContent>
|
||||
<p>Failed to send message</p>
|
||||
</TooltipContent>
|
||||
<TooltipTrigger render={<AlertTriangle size={17} />} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
{!grouped && (
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="font-medium">{user.display}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{new Date(message.send_time).toLocaleString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{isValidURL ? (
|
||||
<Media link={message.content} />
|
||||
) : (
|
||||
<Avatar className="mr-1 mb-auto mt-1 w-10">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback>
|
||||
{user.display.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
{message.failed && message.message_state === "awaiting" && (
|
||||
<Tooltip>
|
||||
<TooltipContent>
|
||||
<p>Failed to send message</p>
|
||||
</TooltipContent>
|
||||
<TooltipTrigger render={<AlertTriangle size={17} />} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
{!grouped && (
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="font-medium">{user.display}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{new Date(message.send_time).toLocaleString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Text value={message.content} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
"Loading"
|
||||
)}
|
||||
</div>
|
||||
</MessageContextMenu>
|
||||
</div>
|
||||
|
|
@ -104,6 +136,8 @@ 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.message.failed === next.message.failed &&
|
||||
prev.grouped === next.grouped &&
|
||||
prev.user === next.user
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,56 +1,69 @@
|
|||
import * as React from "react";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useChat } from "./context";
|
||||
import { failedUser } from "@tensamin/shared/data";
|
||||
import { useStorage } from "@tensamin/storage/context";
|
||||
import { useUser, type User } from "@tensamin/user/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,
|
||||
} from "./values";
|
||||
|
||||
import { PAGE_SIZE } from "./values";
|
||||
import { useIsMobile } from "@tensamin/ui";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
function getDistanceFromBottom(element: HTMLDivElement) {
|
||||
return element.scrollHeight - (element.scrollTop + element.clientHeight);
|
||||
function getEstimatedMessageHeight(message: { height?: number }) {
|
||||
return typeof message.height === "number" && Number.isFinite(message.height)
|
||||
? Math.max(1, Math.ceil(message.height))
|
||||
: FALLBACK_MESSAGE_HEIGHT;
|
||||
}
|
||||
|
||||
const FALLBACK_MESSAGE_HEIGHT = 56;
|
||||
function shouldFetchPreviousPage({
|
||||
entry,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
userScrolledUp,
|
||||
}: {
|
||||
entry: IntersectionObserverEntry | undefined;
|
||||
hasNextPage: boolean;
|
||||
isFetchingNextPage: boolean;
|
||||
userScrolledUp: boolean;
|
||||
}) {
|
||||
return [
|
||||
entry?.isIntersecting === true,
|
||||
userScrolledUp,
|
||||
hasNextPage,
|
||||
!isFetchingNextPage,
|
||||
].every(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
inputBoxRef,
|
||||
} = useChat();
|
||||
const { getMessages, liveMessages, clearLiveMessages, userId, sharedSecret } =
|
||||
useChat();
|
||||
const { get: getUser } = useUser();
|
||||
const { load } = useStorage();
|
||||
|
||||
const scrollRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const isMobile = useIsMobile();
|
||||
const stickyBottomThreshold = isMobile ? 220 : 140;
|
||||
|
||||
const [hasScrolledToBottomInitially, setHasScrolledToBottomInitially] =
|
||||
React.useState(false);
|
||||
const [lastLiveMessageCount, setLastLiveMessageCount] = React.useState(0);
|
||||
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<number, number>
|
||||
>({});
|
||||
const measurementRefs = React.useRef(new Map<number, HTMLDivElement>());
|
||||
const shouldRestoreBottomOnResizeRef = React.useRef(false);
|
||||
const topSentinelRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const didInitialScrollRef = React.useRef(false);
|
||||
const userScrolledUpRef = React.useRef(false);
|
||||
const isAtBottomRef = React.useRef(true);
|
||||
const shouldRestoreBottomOnFocusResizeRef = React.useRef(false);
|
||||
const smoothScrollFrameRef = React.useRef<number | null>(null);
|
||||
const smoothScrollTargetRef = React.useRef(0);
|
||||
|
||||
const [messageUsers, setMessageUsers] = React.useState<
|
||||
Record<string, User | undefined>
|
||||
>({});
|
||||
const [lastLiveMessageCount, setLastLiveMessageCount] = React.useState(0);
|
||||
const [didInitialScroll, setDidInitialScroll] = React.useState(false);
|
||||
const [viewportHeight, setViewportHeight] = React.useState(0);
|
||||
const [value, setValue] = React.useState("");
|
||||
|
||||
const hasValidChatUser = Number.isSafeInteger(userId) && userId > 0;
|
||||
const hasSharedSecret = sharedSecret.length > 0;
|
||||
|
|
@ -69,18 +82,55 @@ export default function Screen() {
|
|||
},
|
||||
});
|
||||
|
||||
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<number>,
|
||||
) => {
|
||||
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 ?? [];
|
||||
return [...pages].reverse().flat();
|
||||
}, [messagesQuery.data]);
|
||||
|
||||
React.useEffect(() => {
|
||||
clearLiveMessages();
|
||||
setHasScrolledToBottomInitially(false);
|
||||
setLastLiveMessageCount(0);
|
||||
setPrependAnchor(null);
|
||||
}, [userId, clearLiveMessages]);
|
||||
|
||||
const liveMessagesSnapshot = liveMessages();
|
||||
|
||||
const messages = React.useMemo(() => {
|
||||
|
|
@ -97,51 +147,67 @@ export default function Screen() {
|
|||
|
||||
return [...historicalMessages, ...liveWithoutDuplicates];
|
||||
}, [historicalMessages, liveMessagesSnapshot]);
|
||||
|
||||
const messageChunks = React.useMemo(() => {
|
||||
const chunks: Array<{
|
||||
key: number;
|
||||
messages: typeof messages;
|
||||
startIndex: number;
|
||||
}> = [];
|
||||
|
||||
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: firstMessage.send_time,
|
||||
messages: chunkMessages,
|
||||
startIndex: start,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}, [messages]);
|
||||
|
||||
const shouldShowConversationStart =
|
||||
!!messagesQuery.data && !messagesQuery.hasNextPage;
|
||||
const virtualRowCount =
|
||||
messages.length + (shouldShowConversationStart ? 1 : 0);
|
||||
messageChunks.length + (shouldShowConversationStart ? 1 : 0);
|
||||
|
||||
const getItemKey = React.useCallback(
|
||||
(index: number) => {
|
||||
if (shouldShowConversationStart && index === 0) {
|
||||
if (shouldShowConversationStart && index === messageChunks.length) {
|
||||
return "conversation-start";
|
||||
}
|
||||
|
||||
const messageIndex = shouldShowConversationStart ? index - 1 : index;
|
||||
return messages[messageIndex]?.send_time ?? index;
|
||||
return messageChunks[index]?.key ?? index;
|
||||
},
|
||||
[shouldShowConversationStart, messages],
|
||||
[messageChunks, shouldShowConversationStart],
|
||||
);
|
||||
|
||||
const setMeasurementRef = React.useCallback(
|
||||
(sendTime: number, element: HTMLDivElement | null) => {
|
||||
if (!element) {
|
||||
measurementRefs.current.delete(sendTime);
|
||||
return;
|
||||
}
|
||||
|
||||
measurementRefs.current.set(sendTime, element);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const estimateMessageSize = React.useCallback(
|
||||
const estimateSize = React.useCallback(
|
||||
(index: number) => {
|
||||
if (shouldShowConversationStart && index === 0) {
|
||||
const isConversationStart =
|
||||
shouldShowConversationStart && index === messageChunks.length;
|
||||
if (isConversationStart) {
|
||||
return FALLBACK_MESSAGE_HEIGHT;
|
||||
}
|
||||
|
||||
const messageIndex = shouldShowConversationStart ? index - 1 : index;
|
||||
const sendTime = messages[messageIndex]?.send_time;
|
||||
const chunk = messageChunks[index];
|
||||
|
||||
if (sendTime === undefined) {
|
||||
if (!chunk) {
|
||||
return FALLBACK_MESSAGE_HEIGHT;
|
||||
}
|
||||
|
||||
return measuredHeights[sendTime] ?? FALLBACK_MESSAGE_HEIGHT;
|
||||
return chunk.messages.reduce(
|
||||
(total, message) => total + getEstimatedMessageHeight(message),
|
||||
0,
|
||||
);
|
||||
},
|
||||
[measuredHeights, messages, shouldShowConversationStart],
|
||||
[messageChunks, shouldShowConversationStart],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/incompatible-library
|
||||
|
|
@ -149,9 +215,12 @@ export default function Screen() {
|
|||
count: virtualRowCount,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
getItemKey,
|
||||
estimateSize: estimateMessageSize,
|
||||
overscan: isMobile ? 10 : 6,
|
||||
estimateSize,
|
||||
overscan: 2,
|
||||
});
|
||||
const totalSize = virtualizer.getTotalSize();
|
||||
const contentHeight = Math.max(totalSize, viewportHeight);
|
||||
const verticalOffset = Math.max(0, viewportHeight - totalSize);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const element = scrollRef.current;
|
||||
|
|
@ -159,14 +228,13 @@ export default function Screen() {
|
|||
return;
|
||||
}
|
||||
|
||||
const updateDimensions = () => {
|
||||
setScrollWidth(element.clientWidth);
|
||||
setScrollHeight(element.clientHeight);
|
||||
const updateViewportHeight = () => {
|
||||
setViewportHeight(element.clientHeight);
|
||||
};
|
||||
|
||||
updateDimensions();
|
||||
updateViewportHeight();
|
||||
|
||||
const observer = new ResizeObserver(updateDimensions);
|
||||
const observer = new ResizeObserver(updateViewportHeight);
|
||||
observer.observe(element);
|
||||
|
||||
return () => {
|
||||
|
|
@ -175,277 +243,169 @@ export default function Screen() {
|
|||
}, []);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const frame = requestAnimationFrame(() => {
|
||||
virtualizer.measure();
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
};
|
||||
}, [inputHeight, measuredHeights, scrollWidth, virtualizer]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
if (typeof ResizeObserver === "undefined") {
|
||||
if (didInitialScrollRef.current || virtualRowCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const measureHeights = () => {
|
||||
setMeasuredHeights((prev) => {
|
||||
let changed = false;
|
||||
const next: Record<number, number> = {};
|
||||
|
||||
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);
|
||||
requestAnimationFrame(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [messages, scrollWidth]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const restoreBottomAfterResize = () => {
|
||||
shouldRestoreBottomOnResizeRef.current =
|
||||
isAtBottomRef.current || shouldRestoreBottomOnFocusResizeRef.current;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
virtualizer.measure();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!scrollRef.current || !shouldRestoreBottomOnResizeRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
isAtBottomRef.current = true;
|
||||
shouldRestoreBottomOnFocusResizeRef.current = false;
|
||||
shouldRestoreBottomOnResizeRef.current = false;
|
||||
});
|
||||
didInitialScrollRef.current = true;
|
||||
setDidInitialScroll(true);
|
||||
});
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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]);
|
||||
}, [virtualizer, virtualRowCount]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const count = liveMessagesSnapshot.length;
|
||||
|
||||
if (!scrollRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count > lastLiveMessageCount) {
|
||||
const shouldStickToBottom =
|
||||
getDistanceFromBottom(scrollRef.current) <= stickyBottomThreshold;
|
||||
|
||||
if (shouldStickToBottom) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
isAtBottomRef.current = true;
|
||||
}
|
||||
if (count > lastLiveMessageCount && isAtBottomRef.current) {
|
||||
requestAnimationFrame(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setLastLiveMessageCount(count);
|
||||
}, [
|
||||
lastLiveMessageCount,
|
||||
liveMessagesSnapshot.length,
|
||||
stickyBottomThreshold,
|
||||
virtualRowCount,
|
||||
virtualizer,
|
||||
]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
!scrollRef.current ||
|
||||
!prependAnchor ||
|
||||
messagesQuery.isFetchingNextPage
|
||||
viewportHeight === 0 ||
|
||||
totalSize > viewportHeight ||
|
||||
messagesQuery.isFetchingNextPage ||
|
||||
!messagesQuery.hasNextPage
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = virtualizer.getTotalSize() - prependAnchor.totalSize;
|
||||
scrollRef.current.scrollTop = prependAnchor.scrollTop + delta;
|
||||
setPrependAnchor(null);
|
||||
}, [messagesQuery.isFetchingNextPage, prependAnchor, virtualizer]);
|
||||
void messagesQuery.fetchNextPage();
|
||||
}, [didInitialScroll, messagesQuery, totalSize, viewportHeight]);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
React.useLayoutEffect(() => {
|
||||
if (
|
||||
!didInitialScroll ||
|
||||
userScrolledUpRef.current ||
|
||||
virtualRowCount === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
void onScroll();
|
||||
}, [onScroll, stickyBottomThreshold]);
|
||||
requestAnimationFrame(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = 0;
|
||||
}
|
||||
});
|
||||
}, [didInitialScroll, virtualRowCount]);
|
||||
|
||||
const [value, setValue] = React.useState("");
|
||||
const totalSize = virtualizer.getTotalSize();
|
||||
const verticalOffset = Math.max(0, scrollHeight - totalSize);
|
||||
const contentHeight = Math.max(totalSize, scrollHeight);
|
||||
React.useEffect(() => {
|
||||
const root = scrollRef.current;
|
||||
const sentinel = topSentinelRef.current;
|
||||
|
||||
if (!root || !sentinel || !didInitialScroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
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" },
|
||||
);
|
||||
|
||||
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 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,
|
||||
);
|
||||
|
||||
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]);
|
||||
|
||||
// Render
|
||||
if (!hasValidChatUser) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center text-xl text-foreground/80">
|
||||
|
|
@ -462,31 +422,24 @@ export default function Screen() {
|
|||
className="min-h-0 flex-1 overflow-y-auto"
|
||||
style={{
|
||||
overflowAnchor: "none",
|
||||
paddingBottom: "22px",
|
||||
paddingTop: "22px",
|
||||
transform: "scaleY(-1)",
|
||||
}}
|
||||
onScroll={handleContainerScroll}
|
||||
>
|
||||
{messagesQuery.isPending && (
|
||||
<div className="flex items-center justify-center pt-10">
|
||||
<p className="text-foreground/45 flex gap-1 items-center justify-center">
|
||||
<Loader2 size={18} className="animate-spin" />
|
||||
Loading...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{messagesQuery.isFetchingNextPage && (
|
||||
<div className="flex items-center justify-center pt-3 pb-1">
|
||||
<Loader2 size={16} className="animate-spin text-foreground/45" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{
|
||||
height: `${contentHeight}px`,
|
||||
}}
|
||||
style={{ height: `${contentHeight}px` }}
|
||||
>
|
||||
<div
|
||||
ref={topSentinelRef}
|
||||
className="absolute bottom-0 left-0 h-px w-full"
|
||||
/>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
if (shouldShowConversationStart && virtualRow.index === 0) {
|
||||
if (
|
||||
shouldShowConversationStart &&
|
||||
virtualRow.index === messageChunks.length
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
key="conversation-start"
|
||||
|
|
@ -497,10 +450,10 @@ export default function Screen() {
|
|||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
transform: `translateY(${virtualRow.start + verticalOffset}px)`,
|
||||
transform: `translateY(${verticalOffset + virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex justify-start">
|
||||
<div className="w-full flex justify-start scale-y-[-1]">
|
||||
<div className="text-sm text-foreground/55 px-2.5">
|
||||
Conversation start
|
||||
</div>
|
||||
|
|
@ -509,35 +462,50 @@ export default function Screen() {
|
|||
);
|
||||
}
|
||||
|
||||
const messageIndex = shouldShowConversationStart
|
||||
? virtualRow.index - 1
|
||||
: virtualRow.index;
|
||||
const message = messages[messageIndex];
|
||||
if (!message) {
|
||||
const chunkIndex = virtualRow.index;
|
||||
const chunk = messageChunks[chunkIndex];
|
||||
if (!chunk) {
|
||||
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 (
|
||||
<div
|
||||
key={message.send_time}
|
||||
key={chunk.key}
|
||||
data-index={virtualRow.index}
|
||||
className="flex flex-col"
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
transform: `translateY(${virtualRow.start + verticalOffset}px)`,
|
||||
transform: `translateY(${verticalOffset + virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<Message grouped={isGrouped} message={message} />
|
||||
<div className="flex flex-col scale-y-[-1]">
|
||||
{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 (
|
||||
<Message
|
||||
key={message.send_time}
|
||||
grouped={isGrouped}
|
||||
message={message}
|
||||
user={
|
||||
message.sent_by_self
|
||||
? (messageUsers.own ?? null)
|
||||
: (messageUsers.peer ?? null)
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
@ -546,31 +514,6 @@ export default function Screen() {
|
|||
<div className="z-10 shrink-0">
|
||||
<InputComponent setValue={setValue} value={value} />
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute left-0 top-0 -z-10 overflow-hidden opacity-0"
|
||||
style={{ width: scrollWidth > 0 ? `${scrollWidth}px` : "100%" }}
|
||||
>
|
||||
{messages.map((message, messageIndex) => {
|
||||
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 (
|
||||
<div
|
||||
key={`measure-${message.send_time}`}
|
||||
ref={(element) => {
|
||||
setMeasurementRef(message.send_time, element);
|
||||
}}
|
||||
>
|
||||
<Message grouped={isGrouped} message={message} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,4 +10,6 @@ export type LiveMessage = RawMessage & {
|
|||
localId: string;
|
||||
};
|
||||
|
||||
export const PAGE_SIZE = 50;
|
||||
export const PAGE_SIZE = 30;
|
||||
export const FALLBACK_MESSAGE_HEIGHT = 28;
|
||||
export const MESSAGES_PER_VIRTUAL_ROW = 30;
|
||||
|
|
|
|||
|
|
@ -257,8 +257,10 @@ 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[];
|
||||
}
|
||||
|
||||
export const storageDefaults: Storage = {
|
||||
|
|
@ -292,8 +294,10 @@ 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: ["tenor.com", "c.tenor.com"],
|
||||
};
|
||||
|
||||
// User Status
|
||||
|
|
|
|||
|
|
@ -195,10 +195,14 @@ export function Provider(props: {
|
|||
|
||||
// Load ttp url
|
||||
const [ttpUrl, setTtpUrl] = useState<string | null>(null);
|
||||
const [ttpServerCert, setTtpServerCert] = useState<string>("");
|
||||
useEffect(() => {
|
||||
load("ttp_url").then((url) => {
|
||||
setTtpUrl(url);
|
||||
});
|
||||
load("ttp_server_cert").then((cert) => {
|
||||
setTtpServerCert(cert.trim());
|
||||
});
|
||||
}, [load]);
|
||||
|
||||
/**
|
||||
|
|
@ -301,7 +305,6 @@ 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.
|
||||
|
|
@ -373,8 +376,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) {
|
||||
|
|
@ -447,13 +450,6 @@ export function Provider(props: {
|
|||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
currentReadyState === READY_STATE.OPEN ||
|
||||
currentReadyState === READY_STATE.CONNECTING
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearReconnectTimer();
|
||||
clearReconnectResetTimer();
|
||||
attempts = 0;
|
||||
|
|
@ -487,14 +483,13 @@ 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, ttpUrl]);
|
||||
}, [props.blockConnection, ttpServerCert, ttpUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue