Compare commits

..
Author SHA1 Message Date
b57bc0683c Merge pull request '(fix): typescript error' (#19) from dev into main
All checks were successful
/ build-web (push) Successful in 1m49s
/ build-desktop (linux) (push) Successful in 9m0s
/ build-mobile (push) Successful in 20m25s
/ release (push) Successful in 32s
Reviewed-on: #19
2026-06-09 16:18:04 +02:00
73ff854f0f Merge pull request '(feat): call improvements' (#18) from dev into main
Some checks failed
/ build-desktop (linux) (push) Failing after 1m29s
/ build-web (push) Failing after 1m42s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
Reviewed-on: #18
2026-06-09 16:15:01 +02:00
forgejo-actions
abc0cceb56 (qol): update release flake hash 2026-06-03 07:26:40 +00:00
67756255db Merge pull request '(fix): a few mobile related bugs' (#17) from dev into main
All checks were successful
/ build-web (push) Successful in 1m22s
/ build-desktop (linux) (push) Successful in 5m12s
/ build-mobile (push) Successful in 12m0s
/ release (push) Successful in 29s
Reviewed-on: #17
2026-06-03 09:14:14 +02:00
257ef320d8 Merge pull request '(feat): remove cleanup workflow' (#16) from dev into main
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
Reviewed-on: #16
2026-06-02 22:47:28 +02:00
4a53d5c6dc Merge pull request '(fix): cleanup workflow' (#15) from dev into main
Some checks failed
/ release (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
Reviewed-on: #15
2026-06-02 22:33:05 +02:00
forgejo-actions
089e6c5b28 (qol): update release flake hash 2026-05-29 13:21:12 +00:00
00598348ab Merge pull request '(feat): some workflow updates' (#14) from dev into main
All checks were successful
/ build-web (push) Successful in 2m53s
/ build-desktop (linux) (push) Successful in 7m34s
/ build-mobile (push) Successful in 21m41s
/ release (push) Successful in 54s
Reviewed-on: #14
2026-05-29 14:58:44 +02:00
forgejo-actions
0c06516984 (qol): update release flake hash 2026-05-28 14:09:23 +00:00
8f4f356746 Merge pull request '(feat): now using the .deb instead of the .AppImage for nixos' (#13) from dev into main
All checks were successful
/ build-web (push) Successful in 3m38s
/ build-desktop (linux) (push) Successful in 9m5s
/ build-mobile (push) Successful in 26m16s
/ release (push) Successful in 55s
Reviewed-on: #13
2026-05-28 15:42:18 +02:00
21 changed files with 433 additions and 894 deletions

View file

@ -30,7 +30,6 @@
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-barcode-scanner": "~2", "@tauri-apps/plugin-barcode-scanner": "~2",
"@tauri-apps/plugin-deep-link": "~2", "@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-log": "~2",
"@tauri-apps/plugin-notification": "~2", "@tauri-apps/plugin-notification": "~2",
"@tensamin/shared": "workspace:*", "@tensamin/shared": "workspace:*",
"@tensamin/ui": "*", "@tensamin/ui": "*",

View file

@ -8,17 +8,6 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 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]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.4" version = "1.1.4"
@ -43,23 +32,6 @@ dependencies = [
"alloc-no-stdlib", "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]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@ -75,12 +47,6 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]] [[package]]
name = "asn1-rs" name = "asn1-rs"
version = "0.7.2" version = "0.7.2"
@ -360,18 +326,6 @@ dependencies = [
"serde_core", "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]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -412,30 +366,6 @@ dependencies = [
"piper", "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]] [[package]]
name = "brotli" name = "brotli"
version = "8.0.3" version = "8.0.3"
@ -472,40 +402,6 @@ version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" 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]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.25.0" version = "1.25.0"
@ -1173,16 +1069,6 @@ dependencies = [
"syn 2.0.117", "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]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@ -1246,15 +1132,6 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "fern"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
dependencies = [
"log",
]
[[package]] [[package]]
name = "field-offset" name = "field-offset"
version = "0.3.6" version = "0.3.6"
@ -1341,12 +1218,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.32" version = "0.3.32"
@ -1733,9 +1604,6 @@ name = "hashbrown"
version = "0.12.3" version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
@ -2318,9 +2186,6 @@ name = "log"
version = "0.4.32" version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
dependencies = [
"value-bag",
]
[[package]] [[package]]
name = "lru-slab" name = "lru-slab"
@ -2530,15 +2395,6 @@ dependencies = [
"syn 2.0.117", "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]] [[package]]
name = "objc2" name = "objc2"
version = "0.6.4" version = "0.6.4"
@ -3130,26 +2986,6 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.37.5" version = "0.37.5"
@ -3245,12 +3081,6 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.6" version = "0.8.6"
@ -3371,9 +3201,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.12.4" version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -3394,18 +3224,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.11" version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rend"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
dependencies = [
"bytecheck",
]
[[package]] [[package]]
name = "reqwest" name = "reqwest"
@ -3455,35 +3276,6 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "rust-ini" name = "rust-ini"
version = "0.21.3" version = "0.21.3"
@ -3494,23 +3286,6 @@ dependencies = [
"ordered-multimap", "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]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.2" version = "2.1.2"
@ -3694,12 +3469,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "seahash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "3.7.0" version = "3.7.0"
@ -3955,12 +3724,6 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.3" version = "1.0.3"
@ -4115,7 +3878,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote",
"unicode-ident", "unicode-ident",
] ]
@ -4214,12 +3976,6 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.12.16" version = "0.12.16"
@ -4419,28 +4175,6 @@ dependencies = [
"windows-result 0.3.4", "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]] [[package]]
name = "tauri-plugin-notification" name = "tauri-plugin-notification"
version = "2.3.3" version = "2.3.3"
@ -4660,11 +4394,10 @@ dependencies = [
"tauri-plugin-app-events", "tauri-plugin-app-events",
"tauri-plugin-barcode-scanner", "tauri-plugin-barcode-scanner",
"tauri-plugin-deep-link", "tauri-plugin-deep-link",
"tauri-plugin-log",
"tauri-plugin-notification", "tauri-plugin-notification",
"tauri-plugin-opener", "tauri-plugin-opener",
"ttp-core", "ttp-core",
"ttp-tauri", "ttp-native",
] ]
[[package]] [[package]]
@ -4715,9 +4448,7 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa", "itoa",
"libc",
"num-conv", "num-conv",
"num_threads",
"powerfmt", "powerfmt",
"serde_core", "serde_core",
"time-core", "time-core",
@ -5042,7 +4773,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "ttp-core" name = "ttp-core"
version = "0.1.0" 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 = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"byteorder", "byteorder",
@ -5055,7 +4786,7 @@ dependencies = [
[[package]] [[package]]
name = "ttp-native" name = "ttp-native"
version = "0.1.0" 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 = [ dependencies = [
"quinn", "quinn",
"rustls", "rustls",
@ -5063,25 +4794,9 @@ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"ttp-core", "ttp-core",
"webpki-roots",
"wtransport", "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]] [[package]]
name = "typeid" name = "typeid"
version = "1.0.3" version = "1.0.3"
@ -5207,12 +4922,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
[[package]] [[package]]
name = "utf8_iter" name = "utf8_iter"
version = "1.0.4" version = "1.0.4"
@ -5231,12 +4940,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "value-bag"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
[[package]] [[package]]
name = "version-compare" name = "version-compare"
version = "0.2.1" version = "0.2.1"
@ -5490,15 +5193,6 @@ dependencies = [
"system-deps", "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]] [[package]]
name = "webview2-com" name = "webview2-com"
version = "0.38.2" version = "0.38.2"
@ -6194,15 +5888,6 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]] [[package]]
name = "x11" name = "x11"
version = "2.21.0" version = "2.21.0"

View file

@ -24,8 +24,7 @@ serde_json = "1"
tauri-plugin-deep-link = "2" tauri-plugin-deep-link = "2"
tauri-plugin-notification = "2" tauri-plugin-notification = "2"
ttp-core = { git = "https://git.methanium.net/tensamin/ttp.git", package = "ttp-core" } 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" } ttp-native = { git = "https://git.methanium.net/tensamin/ttp.git", package = "ttp-native" }
tauri-plugin-log = "2"
[target.'cfg(target_os = "android")'.dependencies.tauri] [target.'cfg(target_os = "android")'.dependencies.tauri]
version = "2" version = "2"

View file

@ -15,11 +15,6 @@
"core:window:allow-minimize", "core:window:allow-minimize",
"core:event:default", "core:event:default",
"deep-link: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"
] ]
} }

View file

@ -13,11 +13,6 @@
"barcode-scanner:default", "barcode-scanner:default",
"barcode-scanner:allow-scan", "barcode-scanner:allow-scan",
"barcode-scanner:allow-cancel", "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"
] ]
} }

View file

@ -1,9 +1,6 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let builder = tauri::Builder::default() let builder = tauri::Builder::default().plugin(tauri_plugin_notification::init());
.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 let builder = builder
.plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_deep_link::init())

View file

@ -1,9 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!! // Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use lib::log;
fn main() { fn main() {
mobile_lib::run(); mobile_lib::run()
log("Test test 123")
} }

View file

@ -12,7 +12,7 @@
"preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .." "preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .."
}, },
"dependencies": { "dependencies": {
"@fontsource-variable/public-sans": "^5.2.7", "@fontsource-variable/inter": "^5.2.8",
"@tailwindcss/vite": "^4.2.4", "@tailwindcss/vite": "^4.2.4",
"@tanstack/react-router": "^1.169.1", "@tanstack/react-router": "^1.169.1",
"@tanstack/react-virtual": "^3.13.24", "@tanstack/react-virtual": "^3.13.24",

View file

@ -1,17 +1,8 @@
import { import { Label } from "@tensamin/ui";
Button, import { Switch as UISwitch } from "@tensamin/ui";
Checkbox,
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
Input,
Label,
Switch as UISwitch,
} from "@tensamin/ui";
import { useEffect, useState } from "react"; 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 { settingsStorageDefaults } from "@tensamin/shared/settings";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
@ -19,13 +10,6 @@ type BooleanStorageKey = {
[K in keyof Storage]: Storage[K] extends boolean ? K : never; [K in keyof Storage]: Storage[K] extends boolean ? K : never;
}[keyof Storage]; }[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({ export function Switch({
label, label,
id, id,
@ -54,125 +38,3 @@ export function Switch({
</div> </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>
);
}

View file

@ -1,13 +1,13 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tailwind-scrollbar-hide/v4"; @import "tailwind-scrollbar-hide/v4";
@import "tw-animate-css"; @import "tw-animate-css";
@import "@fontsource-variable/public-sans"; @import "@fontsource-variable/inter";
@source "./**/*.{ts,tsx}"; @source "./**/*.{ts,tsx}";
@source "../../../packages/**/src/**/*.{ts,tsx}"; @source "../../../packages/**/src/**/*.{ts,tsx}";
@theme { @theme {
--font-sans: "Public Sans Variable", sans-serif; --font-sans: "Inter Variable", sans-serif;
--animate-wiggle: wiggle 0.5s ease-in-out infinite; --animate-wiggle: wiggle 0.5s ease-in-out infinite;
@ -31,7 +31,7 @@ body,
} }
body { body {
font-family: "Public Sans Variable", sans-serif; font-family: "Inter Variable", sans-serif;
} }
#root { #root {

View file

@ -1,4 +1,4 @@
import { List, Switch } from "@/features/settings/components"; import { Switch } from "@/features/settings/components";
import { Kbd } from "@tensamin/ui"; import { Kbd } from "@tensamin/ui";
export default function Page() { export default function Page() {
@ -25,7 +25,6 @@ export default function Page() {
label="Sidebar message preview" label="Sidebar message preview"
id="settings.show_start_of_last_message_in_sidebar" id="settings.show_start_of_last_message_in_sidebar"
/> />
<List label="Trusted embed domains" id="chat_trusted_domains" />
</div> </div>
); );
} }

View file

@ -96,9 +96,6 @@ export default defineConfig({
}, },
}, },
envPrefix: ["VITE_", "TAURI_ENV_*"], envPrefix: ["VITE_", "TAURI_ENV_*"],
optimizeDeps: {
exclude: ["@tensamin/ttp-core"],
},
build: { build: {
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false, minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
sourcemap: !!process.env.TAURI_ENV_DEBUG, sourcemap: !!process.env.TAURI_ENV_DEBUG,

View file

@ -43,7 +43,6 @@
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-barcode-scanner": "~2", "@tauri-apps/plugin-barcode-scanner": "~2",
"@tauri-apps/plugin-deep-link": "~2", "@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-log": "~2",
"@tauri-apps/plugin-notification": "~2", "@tauri-apps/plugin-notification": "~2",
"@tensamin/shared": "workspace:*", "@tensamin/shared": "workspace:*",
"@tensamin/ui": "*", "@tensamin/ui": "*",
@ -59,7 +58,7 @@
"name": "@tensamin/web", "name": "@tensamin/web",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@fontsource-variable/public-sans": "^5.2.7", "@fontsource-variable/inter": "^5.2.8",
"@tailwindcss/vite": "^4.2.4", "@tailwindcss/vite": "^4.2.4",
"@tanstack/react-router": "^1.169.1", "@tanstack/react-router": "^1.169.1",
"@tanstack/react-virtual": "^3.13.24", "@tanstack/react-virtual": "^3.13.24",
@ -256,7 +255,7 @@
}, },
}, },
"overrides": { "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", "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.36.tar.gz",
}, },
"packages": { "packages": {
@ -472,8 +471,6 @@
"@fontsource-variable/inter": ["@fontsource-variable/inter@5.2.8", "", {}, "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ=="], "@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=="], "@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=="], "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
@ -722,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-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=="], "@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"], "@tensamin/call": ["@tensamin/call@workspace:packages/call"],
@ -748,7 +743,7 @@
"@tensamin/ttp": ["@tensamin/ttp@workspace:packages/ttp"], "@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=="], "@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=="],

View file

@ -6,8 +6,8 @@
outputs = {nixpkgs, ...}: let outputs = {nixpkgs, ...}: let
systems = ["x86_64-linux"]; systems = ["x86_64-linux"];
forAllSystems = nixpkgs.lib.genAttrs systems; forAllSystems = nixpkgs.lib.genAttrs systems;
version = "0.0.9"; version = "0.0.8";
x86_64DebHash = "sha256-8rVb12SGGSac6l1NVPGyRLsAKAGDAOJVUwG8l+hoRCU="; x86_64DebHash = "sha256-xC8YHVk+JpeMvXHB1dZihYLFyzFDXne4bkV6TZiOFrE=";
forgejoBaseUrl = "https://git.methanium.net/tensamin/client/releases/download/${version}"; forgejoBaseUrl = "https://git.methanium.net/tensamin/client/releases/download/${version}";
in { in {
packages = forAllSystems (system: let packages = forAllSystems (system: let

View file

@ -44,7 +44,7 @@
}, },
"overrides": { "overrides": {
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.36.tar.gz", "@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": { "dependencies": {
"@tensamin/ttp-core": "*", "@tensamin/ttp-core": "*",

View file

@ -1,42 +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";
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>
);
}

View file

@ -2,8 +2,6 @@ import * as React from "react";
import type { RawMessage } from "../values"; import type { RawMessage } from "../values";
import Text from "@tensamin/markdown/text"; import Text from "@tensamin/markdown/text";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
import { useEffect, useState } from "react";
import type { User } from "@tensamin/user/context";
import { import {
Avatar, Avatar,
@ -14,34 +12,22 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@tensamin/ui"; } from "@tensamin/ui";
import Wrapper from "@tensamin/user/wrapper";
import { useChat } from "../context";
import MessageContextMenu from "./messageContextMenu"; import MessageContextMenu from "./messageContextMenu";
import Media from "./media";
function MessageComponent({ function MessageComponent({
grouped, grouped,
message, message,
user,
}: { }: {
grouped: boolean; grouped: boolean;
message: RawMessage & { message: RawMessage & {
failed?: boolean; failed?: boolean;
}; };
user: User | null;
}) { }) {
const { userId } = useChat();
const actuallyFailed = message.failed && message.message_state === "awaiting"; const actuallyFailed = message.failed && message.message_state === "awaiting";
const [isValidURL, setIsValidURL] = useState(false);
useEffect(() => {
try {
if (message.content.split(" ").length > 1) throw new Error();
new URL(message.content);
setIsValidURL(true);
} catch {
setIsValidURL(false);
}
}, [message.content]);
return ( return (
<div <div
// pt-3 is to get a gap between messages // pt-3 is to get a gap between messages
@ -60,53 +46,51 @@ function MessageComponent({
}, },
)} )}
> >
{user ? ( <Wrapper
<> loading={"State not achievable"}
{grouped ? ( userId={message.sent_by_self ? "own" : userId}
<p className="w-9 text-xs group-hover:visible invisible text-muted-foreground"> component={(user) => (
{new Date(message.send_time).toLocaleString([], { <>
hour: "2-digit", {grouped ? (
minute: "2-digit", <p className="w-9 text-xs group-hover:visible invisible text-muted-foreground">
})} {new Date(message.send_time).toLocaleString([], {
</p> hour: "2-digit",
) : ( minute: "2-digit",
<Avatar className="mr-1 mb-auto mt-1 w-10"> })}
<AvatarImage src={user.avatar} /> </p>
<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} />
) : ( ) : (
<Text value={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>
)} )}
</div> {message.failed && message.message_state === "awaiting" && (
</> <Tooltip>
) : ( <TooltipContent>
"Loading" <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> </div>
</MessageContextMenu> </MessageContextMenu>
</div> </div>
@ -120,8 +104,6 @@ export default React.memo(MessageComponent, (prev, next) => {
prev.message.height === next.message.height && prev.message.height === next.message.height &&
prev.message.sent_by_self === next.message.sent_by_self && prev.message.sent_by_self === next.message.sent_by_self &&
prev.message.message_state === next.message.message_state && 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
); );
}); });

View file

@ -1,68 +1,56 @@
import * as React from "react"; import * as React from "react";
import { useInfiniteQuery } from "@tanstack/react-query"; import { useInfiniteQuery } from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from "@tanstack/react-virtual";
import { failedUser } from "@tensamin/shared/data"; import { useChat } from "./context";
import { useStorage } from "@tensamin/storage/context";
import { Loader2 } from "lucide-react";
import { useUser, type User } from "@tensamin/user/context";
import InputComponent from "./components/input"; import InputComponent from "./components/input";
import Message from "./components/message"; import Message from "./components/message";
import { useChat } from "./context";
import {
PAGE_SIZE,
FALLBACK_MESSAGE_HEIGHT,
MESSAGES_PER_VIRTUAL_ROW,
} from "./values";
function getEstimatedMessageHeight(message: { height?: number }) { import { PAGE_SIZE } from "./values";
return typeof message.height === "number" && Number.isFinite(message.height) import { useIsMobile } from "@tensamin/ui";
? Math.max(1, Math.ceil(message.height)) import { Loader2 } from "lucide-react";
: FALLBACK_MESSAGE_HEIGHT;
function getDistanceFromBottom(element: HTMLDivElement) {
return element.scrollHeight - (element.scrollTop + element.clientHeight);
} }
function shouldFetchPreviousPage({ const FALLBACK_MESSAGE_HEIGHT = 56;
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. * Renders the chat screen with virtualized history and live message updates.
* @returns Chat screen JSX. * @returns Chat screen JSX.
*/ */
export default function Screen() { export default function Screen() {
const { getMessages, liveMessages, clearLiveMessages, userId, sharedSecret } = const {
useChat(); getMessages,
const { get: getUser } = useUser(); liveMessages,
const { load } = useStorage(); clearLiveMessages,
userId,
sharedSecret,
inputBoxRef,
} = useChat();
const scrollRef = React.useRef<HTMLDivElement | null>(null); const scrollRef = React.useRef<HTMLDivElement | null>(null);
const topSentinelRef = React.useRef<HTMLDivElement | null>(null); const isMobile = useIsMobile();
const didInitialScrollRef = React.useRef(false); const stickyBottomThreshold = isMobile ? 220 : 140;
const userScrolledUpRef = React.useRef(false);
const isAtBottomRef = React.useRef(true);
const [messageUsers, setMessageUsers] = React.useState< const [hasScrolledToBottomInitially, setHasScrolledToBottomInitially] =
Record<string, User | undefined> React.useState(false);
>({});
const [lastLiveMessageCount, setLastLiveMessageCount] = React.useState(0); const [lastLiveMessageCount, setLastLiveMessageCount] = React.useState(0);
const [didInitialScroll, setDidInitialScroll] = React.useState(false); const [prependAnchor, setPrependAnchor] = React.useState<{
const [viewportHeight, setViewportHeight] = React.useState(0); totalSize: number;
const [value, setValue] = React.useState(""); 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 isAtBottomRef = React.useRef(true);
const shouldRestoreBottomOnFocusResizeRef = React.useRef(false);
const hasValidChatUser = Number.isSafeInteger(userId) && userId > 0; const hasValidChatUser = Number.isSafeInteger(userId) && userId > 0;
const hasSharedSecret = sharedSecret.length > 0; const hasSharedSecret = sharedSecret.length > 0;
@ -81,55 +69,18 @@ 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 historicalMessages = React.useMemo(() => {
const pages = messagesQuery.data?.pages ?? []; const pages = messagesQuery.data?.pages ?? [];
return [...pages].reverse().flat(); return [...pages].reverse().flat();
}, [messagesQuery.data]); }, [messagesQuery.data]);
React.useEffect(() => {
clearLiveMessages();
setHasScrolledToBottomInitially(false);
setLastLiveMessageCount(0);
setPrependAnchor(null);
}, [userId, clearLiveMessages]);
const liveMessagesSnapshot = liveMessages(); const liveMessagesSnapshot = liveMessages();
const messages = React.useMemo(() => { const messages = React.useMemo(() => {
@ -146,67 +97,51 @@ export default function Screen() {
return [...historicalMessages, ...liveWithoutDuplicates]; return [...historicalMessages, ...liveWithoutDuplicates];
}, [historicalMessages, liveMessagesSnapshot]); }, [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 = const shouldShowConversationStart =
!!messagesQuery.data && !messagesQuery.hasNextPage; !!messagesQuery.data && !messagesQuery.hasNextPage;
const virtualRowCount = const virtualRowCount =
messageChunks.length + (shouldShowConversationStart ? 1 : 0); messages.length + (shouldShowConversationStart ? 1 : 0);
const getItemKey = React.useCallback( const getItemKey = React.useCallback(
(index: number) => { (index: number) => {
if (shouldShowConversationStart && index === messageChunks.length) { if (shouldShowConversationStart && index === 0) {
return "conversation-start"; 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( const setMeasurementRef = React.useCallback(
(index: number) => { (sendTime: number, element: HTMLDivElement | null) => {
const isConversationStart = if (!element) {
shouldShowConversationStart && index === messageChunks.length; measurementRefs.current.delete(sendTime);
if (isConversationStart) { return;
return FALLBACK_MESSAGE_HEIGHT;
} }
const chunk = messageChunks[index]; measurementRefs.current.set(sendTime, element);
if (!chunk) {
return FALLBACK_MESSAGE_HEIGHT;
}
return chunk.messages.reduce(
(total, message) => total + getEstimatedMessageHeight(message),
0,
);
}, },
[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 // eslint-disable-next-line react-hooks/incompatible-library
@ -214,12 +149,9 @@ export default function Screen() {
count: virtualRowCount, count: virtualRowCount,
getScrollElement: () => scrollRef.current, getScrollElement: () => scrollRef.current,
getItemKey, getItemKey,
estimateSize, estimateSize: estimateMessageSize,
overscan: 2, overscan: isMobile ? 10 : 6,
}); });
const totalSize = virtualizer.getTotalSize();
const contentHeight = Math.max(totalSize, viewportHeight);
const verticalOffset = Math.max(0, viewportHeight - totalSize);
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
const element = scrollRef.current; const element = scrollRef.current;
@ -227,13 +159,14 @@ export default function Screen() {
return; return;
} }
const updateViewportHeight = () => { const updateDimensions = () => {
setViewportHeight(element.clientHeight); setScrollWidth(element.clientWidth);
setScrollHeight(element.clientHeight);
}; };
updateViewportHeight(); updateDimensions();
const observer = new ResizeObserver(updateViewportHeight); const observer = new ResizeObserver(updateDimensions);
observer.observe(element); observer.observe(element);
return () => { return () => {
@ -242,134 +175,277 @@ export default function Screen() {
}, []); }, []);
React.useLayoutEffect(() => { 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; return;
} }
requestAnimationFrame(() => { const measureHeights = () => {
if (scrollRef.current) { setMeasuredHeights((prev) => {
scrollRef.current.scrollTop = 0; 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);
} }
}
return () => {
observer.disconnect();
};
}, [messages, scrollWidth]);
React.useEffect(() => {
if (typeof window === "undefined") {
return;
}
const restoreBottomAfterResize = () => {
shouldRestoreBottomOnResizeRef.current =
isAtBottomRef.current || shouldRestoreBottomOnFocusResizeRef.current;
requestAnimationFrame(() => { requestAnimationFrame(() => {
didInitialScrollRef.current = true; virtualizer.measure();
setDidInitialScroll(true);
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(() => { React.useLayoutEffect(() => {
const count = liveMessagesSnapshot.length; const count = liveMessagesSnapshot.length;
if (count > lastLiveMessageCount && isAtBottomRef.current) { if (!scrollRef.current) {
requestAnimationFrame(() => { return;
if (scrollRef.current) { }
scrollRef.current.scrollTop = 0;
} if (count > lastLiveMessageCount) {
}); const shouldStickToBottom =
getDistanceFromBottom(scrollRef.current) <= stickyBottomThreshold;
if (shouldStickToBottom) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
isAtBottomRef.current = true;
}
} }
setLastLiveMessageCount(count); setLastLiveMessageCount(count);
}, [ }, [
lastLiveMessageCount, lastLiveMessageCount,
liveMessagesSnapshot.length, liveMessagesSnapshot.length,
virtualRowCount, stickyBottomThreshold,
virtualizer,
]); ]);
React.useEffect(() => {
if (
viewportHeight === 0 ||
totalSize > viewportHeight ||
messagesQuery.isFetchingNextPage ||
!messagesQuery.hasNextPage
) {
return;
}
void messagesQuery.fetchNextPage();
}, [didInitialScroll, messagesQuery, totalSize, viewportHeight]);
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
if ( if (
!didInitialScroll || !scrollRef.current ||
userScrolledUpRef.current || !prependAnchor ||
virtualRowCount === 0 messagesQuery.isFetchingNextPage
) { ) {
return; return;
} }
requestAnimationFrame(() => { const delta = virtualizer.getTotalSize() - prependAnchor.totalSize;
if (scrollRef.current) { scrollRef.current.scrollTop = prependAnchor.scrollTop + delta;
scrollRef.current.scrollTop = 0; setPrependAnchor(null);
} }, [messagesQuery.isFetchingNextPage, prependAnchor, virtualizer]);
});
}, [didInitialScroll, virtualRowCount]);
React.useEffect(() => { /**
const root = scrollRef.current; * Triggers asynchronous scroll pagination without returning a promise to JSX.
const sentinel = topSentinelRef.current; * @returns Void.
*/
if (!root || !sentinel || !didInitialScroll) { const handleContainerScroll = React.useCallback((): void => {
return; if (scrollRef.current) {
isAtBottomRef.current =
getDistanceFromBottom(scrollRef.current) <= stickyBottomThreshold;
} }
const observer = new IntersectionObserver( void onScroll();
(entries) => { }, [onScroll, stickyBottomThreshold]);
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); const [value, setValue] = React.useState("");
const totalSize = virtualizer.getTotalSize();
return () => { const verticalOffset = Math.max(0, scrollHeight - totalSize);
observer.disconnect(); const contentHeight = Math.max(totalSize, scrollHeight);
};
}, [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;
}
const handleWheel = (event: WheelEvent) => {
event.preventDefault();
event.stopPropagation();
element.scrollTop -= event.deltaY;
handleContainerScroll();
};
element.addEventListener("wheel", handleWheel, { passive: false });
return () => {
element.removeEventListener("wheel", handleWheel);
};
}, [handleContainerScroll]);
// Render
if (!hasValidChatUser) { if (!hasValidChatUser) {
return ( return (
<div className="w-full h-full flex items-center justify-center text-xl text-foreground/80"> <div className="w-full h-full flex items-center justify-center text-xl text-foreground/80">
@ -386,13 +462,12 @@ export default function Screen() {
className="min-h-0 flex-1 overflow-y-auto" className="min-h-0 flex-1 overflow-y-auto"
style={{ style={{
overflowAnchor: "none", overflowAnchor: "none",
paddingTop: "22px", paddingBottom: "22px",
transform: "scaleY(-1)",
}} }}
onScroll={handleContainerScroll} onScroll={handleContainerScroll}
> >
{messagesQuery.isPending && ( {messagesQuery.isPending && (
<div className="flex items-center justify-center pt-10 scale-y-[-1]"> <div className="flex items-center justify-center pt-10">
<p className="text-foreground/45 flex gap-1 items-center justify-center"> <p className="text-foreground/45 flex gap-1 items-center justify-center">
<Loader2 size={18} className="animate-spin" /> <Loader2 size={18} className="animate-spin" />
Loading... Loading...
@ -400,23 +475,18 @@ export default function Screen() {
</div> </div>
)} )}
{messagesQuery.isFetchingNextPage && ( {messagesQuery.isFetchingNextPage && (
<div className="flex items-center justify-center pt-3 pb-1 scale-y-[-1]"> <div className="flex items-center justify-center pt-3 pb-1">
<Loader2 size={16} className="animate-spin text-foreground/45" /> <Loader2 size={16} className="animate-spin text-foreground/45" />
</div> </div>
)} )}
<div <div
className="relative w-full" 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) => { {virtualizer.getVirtualItems().map((virtualRow) => {
if ( if (shouldShowConversationStart && virtualRow.index === 0) {
shouldShowConversationStart &&
virtualRow.index === messageChunks.length
) {
return ( return (
<div <div
key="conversation-start" key="conversation-start"
@ -427,10 +497,10 @@ export default function Screen() {
top: 0, top: 0,
left: 0, left: 0,
width: "100%", width: "100%",
transform: `translateY(${verticalOffset + virtualRow.start}px)`, transform: `translateY(${virtualRow.start + verticalOffset}px)`,
}} }}
> >
<div className="w-full flex justify-start scale-y-[-1]"> <div className="w-full flex justify-start">
<div className="text-sm text-foreground/55 px-2.5"> <div className="text-sm text-foreground/55 px-2.5">
Conversation start Conversation start
</div> </div>
@ -439,50 +509,35 @@ export default function Screen() {
); );
} }
const chunkIndex = virtualRow.index; const messageIndex = shouldShowConversationStart
const chunk = messageChunks[chunkIndex]; ? virtualRow.index - 1
if (!chunk) { : virtualRow.index;
const message = messages[messageIndex];
if (!message) {
return null; 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 ( return (
<div <div
key={chunk.key} key={message.send_time}
data-index={virtualRow.index} data-index={virtualRow.index}
className="flex flex-col"
ref={virtualizer.measureElement} ref={virtualizer.measureElement}
style={{ style={{
position: "absolute", position: "absolute",
top: 0, top: 0,
left: 0, left: 0,
width: "100%", width: "100%",
transform: `translateY(${verticalOffset + virtualRow.start}px)`, transform: `translateY(${virtualRow.start + verticalOffset}px)`,
}} }}
> >
<div className="flex flex-col scale-y-[-1]"> <Message grouped={isGrouped} message={message} />
{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> </div>
); );
})} })}
@ -491,6 +546,31 @@ export default function Screen() {
<div className="z-10 shrink-0"> <div className="z-10 shrink-0">
<InputComponent setValue={setValue} value={value} /> <InputComponent setValue={setValue} value={value} />
</div> </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> </div>
); );
} }

View file

@ -10,6 +10,4 @@ export type LiveMessage = RawMessage & {
localId: string; localId: string;
}; };
export const PAGE_SIZE = 30; export const PAGE_SIZE = 50;
export const FALLBACK_MESSAGE_HEIGHT = 28;
export const MESSAGES_PER_VIRTUAL_ROW = 30;

View file

@ -257,10 +257,8 @@ export interface Storage extends SettingsStorageDefaults {
cached_contacts: Contacts; cached_contacts: Contacts;
cached_communities: Communities; cached_communities: Communities;
ttp_url: string; ttp_url: string;
ttp_server_cert: string;
call_mute_range_start: number; call_mute_range_start: number;
call_mute_range_end: number; call_mute_range_end: number;
chat_trusted_domains: string[];
} }
export const storageDefaults: Storage = { export const storageDefaults: Storage = {
@ -294,10 +292,8 @@ export const storageDefaults: Storage = {
cached_contacts: [], cached_contacts: [],
cached_communities: [], cached_communities: [],
ttp_url: "https://tensamin.net:959", ttp_url: "https://tensamin.net:959",
ttp_server_cert: "",
call_mute_range_start: -55, call_mute_range_start: -55,
call_mute_range_end: -45, call_mute_range_end: -45,
chat_trusted_domains: ["tenor.com", "c.tenor.com"],
}; };
// User Status // User Status

View file

@ -195,14 +195,10 @@ export function Provider(props: {
// Load ttp url // Load ttp url
const [ttpUrl, setTtpUrl] = useState<string | null>(null); const [ttpUrl, setTtpUrl] = useState<string | null>(null);
const [ttpServerCert, setTtpServerCert] = useState<string>("");
useEffect(() => { useEffect(() => {
load("ttp_url").then((url) => { load("ttp_url").then((url) => {
setTtpUrl(url); setTtpUrl(url);
}); });
load("ttp_server_cert").then((cert) => {
setTtpServerCert(cert.trim());
});
}, [load]); }, [load]);
/** /**
@ -305,6 +301,7 @@ export function Provider(props: {
let reconnectScheduled = false; let reconnectScheduled = false;
let disposed = false; let disposed = false;
let resumeListenerRegistered = false; let resumeListenerRegistered = false;
let currentReadyState: number = READY_STATE.CLOSED;
/** /**
* Clears any scheduled reconnect timeout and resets scheduling flags. * Clears any scheduled reconnect timeout and resets scheduling flags.
@ -376,8 +373,8 @@ export function Provider(props: {
const transportClient = !props.blockConnection const transportClient = !props.blockConnection
? createTransportClient(schemas, { ? createTransportClient(schemas, {
url: ttpUrl, url: ttpUrl,
serverCert: ttpServerCert || undefined,
onReadyStateChange: (state) => { onReadyStateChange: (state) => {
currentReadyState = state;
setReadyState(state); setReadyState(state);
if (state === READY_STATE.OPEN) { if (state === READY_STATE.OPEN) {
@ -450,6 +447,13 @@ export function Provider(props: {
return; return;
} }
if (
currentReadyState === READY_STATE.OPEN ||
currentReadyState === READY_STATE.CONNECTING
) {
return;
}
clearReconnectTimer(); clearReconnectTimer();
clearReconnectResetTimer(); clearReconnectResetTimer();
attempts = 0; attempts = 0;
@ -483,13 +487,14 @@ export function Provider(props: {
} }
void transportClient?.close("context-dispose"); void transportClient?.close("context-dispose");
currentReadyState = READY_STATE.CLOSED;
setReadyState(READY_STATE.CLOSED); setReadyState(READY_STATE.CLOSED);
setConnected(false); setConnected(false);
setIdentified(false); setIdentified(false);
setIdentifying(false); setIdentifying(false);
identificationStartedRef.current = false; identificationStartedRef.current = false;
}; };
}, [props.blockConnection, ttpServerCert, ttpUrl]); }, [props.blockConnection, ttpUrl]);
useEffect(() => { useEffect(() => {
if (!connected) { if (!connected) {