diff --git a/apps/tauri/package.json b/apps/tauri/package.json index 3835cd1..80b8b3a 100644 --- a/apps/tauri/package.json +++ b/apps/tauri/package.json @@ -30,7 +30,6 @@ "@tauri-apps/api": "^2", "@tauri-apps/plugin-barcode-scanner": "~2", "@tauri-apps/plugin-deep-link": "~2", - "@tauri-apps/plugin-log": "~2", "@tauri-apps/plugin-notification": "~2", "@tensamin/shared": "workspace:*", "@tensamin/ui": "*", diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index 37a6827..386b3fc 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/Cargo.lock @@ -8,17 +8,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "version_check", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -43,23 +32,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "android_log-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" - -[[package]] -name = "android_logger" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" -dependencies = [ - "android_log-sys", - "env_filter", - "log", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -75,12 +47,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - [[package]] name = "asn1-rs" version = "0.7.2" @@ -360,18 +326,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -412,30 +366,6 @@ dependencies = [ "piper", ] -[[package]] -name = "borsh" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" -dependencies = [ - "borsh-derive", - "bytes", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" -dependencies = [ - "once_cell", - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "brotli" version = "8.0.3" @@ -472,40 +402,6 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" -[[package]] -name = "byte-unit" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" -dependencies = [ - "rust_decimal", - "schemars 1.2.1", - "serde", - "utf8-width", -] - -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "bytemuck" version = "1.25.0" @@ -1173,16 +1069,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "env_filter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" -dependencies = [ - "log", - "regex", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1246,15 +1132,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "fern" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" -dependencies = [ - "log", -] - [[package]] name = "field-offset" version = "0.3.6" @@ -1341,12 +1218,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures-channel" version = "0.3.32" @@ -1733,9 +1604,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] [[package]] name = "hashbrown" @@ -2318,9 +2186,6 @@ name = "log" version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" -dependencies = [ - "value-bag", -] [[package]] name = "lru-slab" @@ -2530,15 +2395,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - [[package]] name = "objc2" version = "0.6.4" @@ -3130,26 +2986,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "quick-xml" version = "0.37.5" @@ -3245,12 +3081,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.6" @@ -3371,9 +3201,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.4" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -3394,18 +3224,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.11" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -3455,35 +3276,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rkyv" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "rust-ini" version = "0.21.3" @@ -3494,23 +3286,6 @@ dependencies = [ "ordered-multimap", ] -[[package]] -name = "rust_decimal" -version = "1.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c5108e3d4d903e21aac27f12ba5377b6b34f9f44b325e4894c7924169d06995" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand 0.8.6", - "rkyv", - "serde", - "serde_json", - "wasm-bindgen", -] - [[package]] name = "rustc-hash" version = "2.1.2" @@ -3694,12 +3469,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - [[package]] name = "security-framework" version = "3.7.0" @@ -3955,12 +3724,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - [[package]] name = "siphasher" version = "1.0.3" @@ -4115,7 +3878,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", "unicode-ident", ] @@ -4214,12 +3976,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "target-lexicon" version = "0.12.16" @@ -4419,28 +4175,6 @@ dependencies = [ "windows-result 0.3.4", ] -[[package]] -name = "tauri-plugin-log" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" -dependencies = [ - "android_logger", - "byte-unit", - "fern", - "log", - "objc2", - "objc2-foundation", - "serde", - "serde_json", - "serde_repr", - "swift-rs", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", - "time", -] - [[package]] name = "tauri-plugin-notification" version = "2.3.3" @@ -4660,11 +4394,10 @@ dependencies = [ "tauri-plugin-app-events", "tauri-plugin-barcode-scanner", "tauri-plugin-deep-link", - "tauri-plugin-log", "tauri-plugin-notification", "tauri-plugin-opener", "ttp-core", - "ttp-tauri", + "ttp-native", ] [[package]] @@ -4715,9 +4448,7 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", - "libc", "num-conv", - "num_threads", "powerfmt", "serde_core", "time-core", @@ -5042,7 +4773,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ttp-core" version = "0.1.0" -source = "git+https://git.methanium.net/tensamin/ttp.git#0ccc2b808c1f8ff38ede906491ef703a51bd7786" +source = "git+https://git.methanium.net/tensamin/ttp.git#23438fa8f884e6ad0d32ca1004c0dedcce0cc8d2" dependencies = [ "base64 0.22.1", "byteorder", @@ -5055,7 +4786,7 @@ dependencies = [ [[package]] name = "ttp-native" version = "0.1.0" -source = "git+https://git.methanium.net/tensamin/ttp.git#0ccc2b808c1f8ff38ede906491ef703a51bd7786" +source = "git+https://git.methanium.net/tensamin/ttp.git#23438fa8f884e6ad0d32ca1004c0dedcce0cc8d2" dependencies = [ "quinn", "rustls", @@ -5063,25 +4794,9 @@ dependencies = [ "thiserror 2.0.18", "tokio", "ttp-core", - "webpki-roots", "wtransport", ] -[[package]] -name = "ttp-tauri" -version = "0.1.0" -source = "git+https://git.methanium.net/tensamin/ttp.git#0ccc2b808c1f8ff38ede906491ef703a51bd7786" -dependencies = [ - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", - "tokio", - "ttp-core", - "ttp-native", -] - [[package]] name = "typeid" version = "1.0.3" @@ -5207,12 +4922,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf8-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -5231,12 +4940,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" - [[package]] name = "version-compare" version = "0.2.1" @@ -5490,15 +5193,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "webpki-roots" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "webview2-com" version = "0.38.2" @@ -6194,15 +5888,6 @@ dependencies = [ "url", ] -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "x11" version = "2.21.0" diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index 556bb8b..ba011c1 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -24,8 +24,7 @@ serde_json = "1" tauri-plugin-deep-link = "2" tauri-plugin-notification = "2" ttp-core = { git = "https://git.methanium.net/tensamin/ttp.git", package = "ttp-core" } -ttp-tauri = { git = "https://git.methanium.net/tensamin/ttp.git", package = "ttp-tauri" } -tauri-plugin-log = "2" +ttp-native = { git = "https://git.methanium.net/tensamin/ttp.git", package = "ttp-native" } [target.'cfg(target_os = "android")'.dependencies.tauri] version = "2" diff --git a/apps/tauri/src-tauri/capabilities/default.json b/apps/tauri/src-tauri/capabilities/default.json index 352eda0..02d2627 100644 --- a/apps/tauri/src-tauri/capabilities/default.json +++ b/apps/tauri/src-tauri/capabilities/default.json @@ -15,11 +15,6 @@ "core:window:allow-minimize", "core:event:default", "deep-link:default", - "notification:default", - "log:default", - "ttp-tauri:allow-connect", - "ttp-tauri:allow-send", - "ttp-tauri:allow-close", - "ttp-tauri:allow-ready-state" + "notification:default" ] -} +} \ No newline at end of file diff --git a/apps/tauri/src-tauri/capabilities/mobile.json b/apps/tauri/src-tauri/capabilities/mobile.json index d9914a6..ceee8f7 100644 --- a/apps/tauri/src-tauri/capabilities/mobile.json +++ b/apps/tauri/src-tauri/capabilities/mobile.json @@ -13,11 +13,6 @@ "barcode-scanner:default", "barcode-scanner:allow-scan", "barcode-scanner:allow-cancel", - "notification:default", - "log:default", - "ttp-tauri:allow-connect", - "ttp-tauri:allow-send", - "ttp-tauri:allow-close", - "ttp-tauri:allow-ready-state" + "notification:default" ] -} +} \ No newline at end of file diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index cfa496a..f01b2ad 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -1,9 +1,6 @@ #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let builder = tauri::Builder::default() - .plugin(tauri_plugin_log::Builder::new().level(tauri_plugin_log::log::LevelFilter::Info).build()) - .plugin(tauri_plugin_notification::init()) - .plugin(ttp_tauri::init()); + let builder = tauri::Builder::default().plugin(tauri_plugin_notification::init()); let builder = builder .plugin(tauri_plugin_deep_link::init()) diff --git a/apps/tauri/src-tauri/src/main.rs b/apps/tauri/src-tauri/src/main.rs index e78a26a..a9346de 100644 --- a/apps/tauri/src-tauri/src/main.rs +++ b/apps/tauri/src-tauri/src/main.rs @@ -1,9 +1,6 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use lib::log; - fn main() { - mobile_lib::run(); - log("Test test 123") + mobile_lib::run() } diff --git a/apps/web/package.json b/apps/web/package.json index 5cbfb60..04d731f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,7 +12,7 @@ "preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .." }, "dependencies": { - "@fontsource-variable/public-sans": "^5.2.7", + "@fontsource-variable/inter": "^5.2.8", "@tailwindcss/vite": "^4.2.4", "@tanstack/react-router": "^1.169.1", "@tanstack/react-virtual": "^3.13.24", diff --git a/apps/web/src/features/settings/components.tsx b/apps/web/src/features/settings/components.tsx index 491b839..b9f2967 100644 --- a/apps/web/src/features/settings/components.tsx +++ b/apps/web/src/features/settings/components.tsx @@ -1,17 +1,8 @@ -import { - Button, - Checkbox, - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, - Input, - Label, - Switch as UISwitch, -} from "@tensamin/ui"; +import { Label } from "@tensamin/ui"; +import { Switch as UISwitch } from "@tensamin/ui"; import { useEffect, useState } from "react"; -import { storageDefaults, type Storage } from "@tensamin/shared/data"; +import type { Storage } from "@tensamin/shared/data"; import { settingsStorageDefaults } from "@tensamin/shared/settings"; import { useStorage } from "@tensamin/storage/context"; @@ -19,13 +10,6 @@ type BooleanStorageKey = { [K in keyof Storage]: Storage[K] extends boolean ? K : never; }[keyof Storage]; -type ListStorageKey = { - [K in keyof Storage]: Storage[K] extends (string | number)[] ? K : never; -}[keyof Storage]; - -type ListStorageItem = - Storage[K] extends Array ? Item : never; - export function Switch({ label, id, @@ -54,125 +38,3 @@ export function Switch({ ); } - -export function List({ - label, - id, -}: { - label: React.ReactNode; - id: K; -}) { - const { save, load } = useStorage(); - const [items, setItems] = useState(storageDefaults[id]); - const [inputValue, setInputValue] = useState(""); - const [selectedItems, setSelectedItems] = useState>(new Set()); - - useEffect(() => { - load(id).then((value) => setItems(value)); - }, [id, load]); - - const persistItems = (nextItems: Storage[K]) => { - setItems(nextItems); - save(id, nextItems); - }; - - const toStorageItem = (value: string): ListStorageItem => { - const referenceItem = items[0] ?? storageDefaults[id][0]; - - if (typeof referenceItem === "number") { - return Number(value) as ListStorageItem; - } - - return value as ListStorageItem; - }; - - const addItem = () => { - const trimmedValue = inputValue.trim(); - if (!trimmedValue) return; - - const nextItem = toStorageItem(trimmedValue); - if (typeof nextItem === "number" && Number.isNaN(nextItem)) return; - - persistItems([...items, nextItem] as Storage[K]); - setInputValue(""); - }; - - const deleteItems = (indexes: Set) => { - const nextItems = items.filter( - (_, index) => !indexes.has(index), - ) as Storage[K]; - - setItems(nextItems); - setSelectedItems(new Set()); - save(id, nextItems); - }; - - return ( -
- -
-
- setInputValue(event.target.value)} - onKeyDown={(event) => { - if (event.key === "Enter") addItem(); - }} - /> - -
-
- {items.map((item, index) => { - const labelId = `${String(id)}-${index}`; - const selected = selectedItems.has(index); - const deletingSelectedItems = selectedItems.size > 1; - - return ( - - - { - setSelectedItems((previous) => { - const nextSelected = new Set(previous); - - if (checked) { - nextSelected.add(index); - } else { - nextSelected.delete(index); - } - - return nextSelected; - }); - }} - /> - -
-
- } - /> - - - deleteItems( - deletingSelectedItems - ? selectedItems - : new Set([index]), - ) - } - > - {deletingSelectedItems ? "Delete Selected" : "Delete"} - - -
- ); - })} -
-
-
- ); -} diff --git a/apps/web/src/index.css b/apps/web/src/index.css index f7dcb91..925d401 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1,13 +1,13 @@ @import "tailwindcss"; @import "tailwind-scrollbar-hide/v4"; @import "tw-animate-css"; -@import "@fontsource-variable/public-sans"; +@import "@fontsource-variable/inter"; @source "./**/*.{ts,tsx}"; @source "../../../packages/**/src/**/*.{ts,tsx}"; @theme { - --font-sans: "Public Sans Variable", sans-serif; + --font-sans: "Inter Variable", sans-serif; --animate-wiggle: wiggle 0.5s ease-in-out infinite; @@ -31,7 +31,7 @@ body, } body { - font-family: "Public Sans Variable", sans-serif; + font-family: "Inter Variable", sans-serif; } #root { diff --git a/apps/web/src/routes/settings/chat.tsx b/apps/web/src/routes/settings/chat.tsx index 9f070df..f67a2c6 100644 --- a/apps/web/src/routes/settings/chat.tsx +++ b/apps/web/src/routes/settings/chat.tsx @@ -1,4 +1,4 @@ -import { List, Switch } from "@/features/settings/components"; +import { Switch } from "@/features/settings/components"; import { Kbd } from "@tensamin/ui"; export default function Page() { @@ -25,7 +25,6 @@ export default function Page() { label="Sidebar message preview" id="settings.show_start_of_last_message_in_sidebar" /> - ); } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 71fb4ac..f9d51eb 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -96,9 +96,6 @@ export default defineConfig({ }, }, envPrefix: ["VITE_", "TAURI_ENV_*"], - optimizeDeps: { - exclude: ["@tensamin/ttp-core"], - }, build: { minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false, sourcemap: !!process.env.TAURI_ENV_DEBUG, diff --git a/bun.lock b/bun.lock index 5e2b865..dc7cf4b 100644 --- a/bun.lock +++ b/bun.lock @@ -43,7 +43,6 @@ "@tauri-apps/api": "^2", "@tauri-apps/plugin-barcode-scanner": "~2", "@tauri-apps/plugin-deep-link": "~2", - "@tauri-apps/plugin-log": "~2", "@tauri-apps/plugin-notification": "~2", "@tensamin/shared": "workspace:*", "@tensamin/ui": "*", @@ -59,7 +58,7 @@ "name": "@tensamin/web", "version": "0.0.0", "dependencies": { - "@fontsource-variable/public-sans": "^5.2.7", + "@fontsource-variable/inter": "^5.2.8", "@tailwindcss/vite": "^4.2.4", "@tanstack/react-router": "^1.169.1", "@tanstack/react-virtual": "^3.13.24", @@ -256,7 +255,7 @@ }, }, "overrides": { - "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.24.tar.gz", + "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.20.tar.gz", "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.36.tar.gz", }, "packages": { @@ -472,8 +471,6 @@ "@fontsource-variable/inter": ["@fontsource-variable/inter@5.2.8", "", {}, "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ=="], - "@fontsource-variable/public-sans": ["@fontsource-variable/public-sans@5.2.7", "", {}, "sha512-4mvade2J3slKkvwRkS+p8T3szet/0vhWoSnuUJTVU81Uo2pRpSZY/Y8bSLRqpSwzIPxjVmRJ53oq6JKP/l/PSg=="], - "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], @@ -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-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"], @@ -748,7 +743,7 @@ "@tensamin/ttp": ["@tensamin/ttp@workspace:packages/ttp"], - "@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.24.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@tauri-apps/api": "^2.9.0", "@typescript-eslint/parser": "^8.59.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.3", "typescript-eslint": "^8.59.1", "zod": "^4.4.1" } }, "sha512-Db94CDOTYvzJSQDZGDgziUXRvXx9PiEfYDQt6sYe8Nyg+KD/aiKnyQx7NeEiG03TfYE3ogNxXOsrcsip4di8+g=="], + "@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.20.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.59.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.3", "typescript-eslint": "^8.59.1", "zod": "^4.4.1" } }, "sha512-08pYuWTivuDWNiBoSSY2P8rIZz4FvuvLQZMjaQnf7KE/Pz9bGO69XHFcCHQXIz1SWsT2TyjochT0sPEGAta15g=="], "@tensamin/ui": ["@tensamin/ui@https://git.methanium.net/tensamin/ui/archive/0.0.36.tar.gz", { "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/inter": "^5.2.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^1.8.0", "next-themes": "^0.4.6", "react-day-picker": "^9.14.0", "react-resizable-panels": "^4.10.0", "recharts": "3.8.0", "shadcn": "^3.5.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.3.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.2.0", "react-dom": "^19.2.0" } }, "sha512-5zql8LtBKVn7WuQDCIdLbthKvLEyxjWShAceXCYbk+kwO41VVILhS1EWuDvjgvv2BBdyuxoVWvdblPIW4BcdSQ=="], diff --git a/flake.nix b/flake.nix index b70fd6b..c1ae625 100644 --- a/flake.nix +++ b/flake.nix @@ -6,8 +6,8 @@ outputs = {nixpkgs, ...}: let systems = ["x86_64-linux"]; forAllSystems = nixpkgs.lib.genAttrs systems; - version = "0.0.9"; - x86_64DebHash = "sha256-8rVb12SGGSac6l1NVPGyRLsAKAGDAOJVUwG8l+hoRCU="; + version = "0.0.8"; + x86_64DebHash = "sha256-xC8YHVk+JpeMvXHB1dZihYLFyzFDXne4bkV6TZiOFrE="; forgejoBaseUrl = "https://git.methanium.net/tensamin/client/releases/download/${version}"; in { packages = forAllSystems (system: let diff --git a/package.json b/package.json index e1c8a5f..86c8924 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ }, "overrides": { "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.36.tar.gz", - "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.24.tar.gz" + "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.20.tar.gz" }, "dependencies": { "@tensamin/ttp-core": "*", diff --git a/packages/chat/src/components/media.tsx b/packages/chat/src/components/media.tsx deleted file mode 100644 index a800897..0000000 --- a/packages/chat/src/components/media.tsx +++ /dev/null @@ -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([]); - const [hidden, setHidden] = useState(true); - const hostname = useMemo(() => new URL(link).hostname, [link]); - - useEffect(() => { - load("chat_trusted_domains").then(setTrustedDomains); - }, [load]); - - return trustedDomains.includes(hostname) ? ( - <> - {hidden && } - setHidden(false)} - className="max-h-70 max-w-70" - /> - - ) : ( -
- - } - /> - - Link embeds can expose your ip address. You can configure trusted - domains in the settings. - - - -
- ); -} diff --git a/packages/chat/src/components/message.tsx b/packages/chat/src/components/message.tsx index 51705ff..60afa75 100644 --- a/packages/chat/src/components/message.tsx +++ b/packages/chat/src/components/message.tsx @@ -2,8 +2,6 @@ import * as React from "react"; import type { RawMessage } from "../values"; import Text from "@tensamin/markdown/text"; import { AlertTriangle } from "lucide-react"; -import { useEffect, useState } from "react"; -import type { User } from "@tensamin/user/context"; import { Avatar, @@ -14,54 +12,26 @@ import { TooltipContent, TooltipTrigger, } from "@tensamin/ui"; +import Wrapper from "@tensamin/user/wrapper"; +import { useChat } from "../context"; import MessageContextMenu from "./messageContextMenu"; -import Media from "./media"; function MessageComponent({ grouped, message, - user, }: { grouped: boolean; message: RawMessage & { failed?: boolean; }; - user: User | null; }) { + const { userId } = useChat(); const actuallyFailed = message.failed && message.message_state === "awaiting"; - const [hasFadedIn, setHasFadedIn] = useState(false); - const opacityClass = !hasFadedIn - ? "opacity-0" - : actuallyFailed || message.message_state === "awaiting" - ? "opacity-50" - : "opacity-100"; - - const [isValidURL, setIsValidURL] = useState(false); - useEffect(() => { - const timeout = window.setTimeout(() => { - setHasFadedIn(true); - }, 100); - - return () => { - window.clearTimeout(timeout); - }; - }, []); - - useEffect(() => { - try { - if (message.content.split(" ").length > 1) throw new Error(); - - new URL(message.content); - setIsValidURL(true); - } catch { - setIsValidURL(false); - } - }, [message.content]); return (
- {user ? ( - <> - {grouped ? ( -

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

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

Failed to send message

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

{user.display}

-

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

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

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

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

Failed to send message

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

{user.display}

+

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

+
+ )} + +
+ + )} + />
@@ -136,8 +104,6 @@ export default React.memo(MessageComponent, (prev, next) => { prev.message.height === next.message.height && prev.message.sent_by_self === next.message.sent_by_self && prev.message.message_state === next.message.message_state && - prev.message.failed === next.message.failed && - prev.grouped === next.grouped && - prev.user === next.user + prev.message.failed === next.message.failed ); }); diff --git a/packages/chat/src/screen.tsx b/packages/chat/src/screen.tsx index 97ae804..44e581b 100644 --- a/packages/chat/src/screen.tsx +++ b/packages/chat/src/screen.tsx @@ -1,69 +1,56 @@ import * as React from "react"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useVirtualizer } from "@tanstack/react-virtual"; -import { failedUser } from "@tensamin/shared/data"; -import { useStorage } from "@tensamin/storage/context"; -import { useUser, type User } from "@tensamin/user/context"; +import { useChat } from "./context"; import InputComponent from "./components/input"; import Message from "./components/message"; -import { useChat } from "./context"; -import { - PAGE_SIZE, - FALLBACK_MESSAGE_HEIGHT, - MESSAGES_PER_VIRTUAL_ROW, -} from "./values"; -function getEstimatedMessageHeight(message: { height?: number }) { - return typeof message.height === "number" && Number.isFinite(message.height) - ? Math.max(1, Math.ceil(message.height)) - : FALLBACK_MESSAGE_HEIGHT; +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 shouldFetchPreviousPage({ - entry, - hasNextPage, - isFetchingNextPage, - userScrolledUp, -}: { - entry: IntersectionObserverEntry | undefined; - hasNextPage: boolean; - isFetchingNextPage: boolean; - userScrolledUp: boolean; -}) { - return [ - entry?.isIntersecting === true, - userScrolledUp, - hasNextPage, - !isFetchingNextPage, - ].every(Boolean); -} +const FALLBACK_MESSAGE_HEIGHT = 56; /** * Renders the chat screen with virtualized history and live message updates. * @returns Chat screen JSX. */ export default function Screen() { - const { getMessages, liveMessages, clearLiveMessages, userId, sharedSecret } = - useChat(); - const { get: getUser } = useUser(); - const { load } = useStorage(); + const { + getMessages, + liveMessages, + clearLiveMessages, + userId, + sharedSecret, + inputBoxRef, + } = useChat(); const scrollRef = React.useRef(null); - const topSentinelRef = React.useRef(null); - const didInitialScrollRef = React.useRef(false); - const userScrolledUpRef = React.useRef(false); - const isAtBottomRef = React.useRef(true); - const smoothScrollFrameRef = React.useRef(null); - const smoothScrollTargetRef = React.useRef(0); + const isMobile = useIsMobile(); + const stickyBottomThreshold = isMobile ? 220 : 140; - const [messageUsers, setMessageUsers] = React.useState< - Record - >({}); + const [hasScrolledToBottomInitially, setHasScrolledToBottomInitially] = + React.useState(false); const [lastLiveMessageCount, setLastLiveMessageCount] = React.useState(0); - const [didInitialScroll, setDidInitialScroll] = React.useState(false); - const [viewportHeight, setViewportHeight] = React.useState(0); - const [value, setValue] = React.useState(""); + const [prependAnchor, setPrependAnchor] = React.useState<{ + totalSize: number; + scrollTop: number; + } | null>(null); + const [scrollWidth, setScrollWidth] = React.useState(0); + const [scrollHeight, setScrollHeight] = React.useState(0); + const [inputHeight, setInputHeight] = React.useState(0); + const [measuredHeights, setMeasuredHeights] = React.useState< + Record + >({}); + const measurementRefs = React.useRef(new Map()); + const shouldRestoreBottomOnResizeRef = React.useRef(false); + const isAtBottomRef = React.useRef(true); + const shouldRestoreBottomOnFocusResizeRef = React.useRef(false); const hasValidChatUser = Number.isSafeInteger(userId) && userId > 0; const hasSharedSecret = sharedSecret.length > 0; @@ -82,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, - ) => { - 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(() => { @@ -147,67 +97,51 @@ 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 = - messageChunks.length + (shouldShowConversationStart ? 1 : 0); + messages.length + (shouldShowConversationStart ? 1 : 0); const getItemKey = React.useCallback( (index: number) => { - if (shouldShowConversationStart && index === messageChunks.length) { + if (shouldShowConversationStart && index === 0) { return "conversation-start"; } - return messageChunks[index]?.key ?? index; + const messageIndex = shouldShowConversationStart ? index - 1 : index; + return messages[messageIndex]?.send_time ?? index; }, - [messageChunks, shouldShowConversationStart], + [shouldShowConversationStart, messages], ); - const estimateSize = React.useCallback( - (index: number) => { - const isConversationStart = - shouldShowConversationStart && index === messageChunks.length; - if (isConversationStart) { - return FALLBACK_MESSAGE_HEIGHT; + const setMeasurementRef = React.useCallback( + (sendTime: number, element: HTMLDivElement | null) => { + if (!element) { + measurementRefs.current.delete(sendTime); + return; } - const chunk = messageChunks[index]; - - if (!chunk) { - return FALLBACK_MESSAGE_HEIGHT; - } - - return chunk.messages.reduce( - (total, message) => total + getEstimatedMessageHeight(message), - 0, - ); + measurementRefs.current.set(sendTime, element); }, - [messageChunks, shouldShowConversationStart], + [], + ); + + const estimateMessageSize = React.useCallback( + (index: number) => { + if (shouldShowConversationStart && index === 0) { + return FALLBACK_MESSAGE_HEIGHT; + } + + const messageIndex = shouldShowConversationStart ? index - 1 : index; + const sendTime = messages[messageIndex]?.send_time; + + if (sendTime === undefined) { + return FALLBACK_MESSAGE_HEIGHT; + } + + return measuredHeights[sendTime] ?? FALLBACK_MESSAGE_HEIGHT; + }, + [measuredHeights, messages, shouldShowConversationStart], ); // eslint-disable-next-line react-hooks/incompatible-library @@ -215,12 +149,9 @@ export default function Screen() { count: virtualRowCount, getScrollElement: () => scrollRef.current, getItemKey, - estimateSize, - overscan: 2, + estimateSize: estimateMessageSize, + overscan: isMobile ? 10 : 6, }); - const totalSize = virtualizer.getTotalSize(); - const contentHeight = Math.max(totalSize, viewportHeight); - const verticalOffset = Math.max(0, viewportHeight - totalSize); React.useLayoutEffect(() => { const element = scrollRef.current; @@ -228,13 +159,14 @@ export default function Screen() { return; } - const updateViewportHeight = () => { - setViewportHeight(element.clientHeight); + const updateDimensions = () => { + setScrollWidth(element.clientWidth); + setScrollHeight(element.clientHeight); }; - updateViewportHeight(); + updateDimensions(); - const observer = new ResizeObserver(updateViewportHeight); + const observer = new ResizeObserver(updateDimensions); observer.observe(element); return () => { @@ -243,169 +175,277 @@ export default function Screen() { }, []); React.useLayoutEffect(() => { - if (didInitialScrollRef.current || virtualRowCount === 0) { + const frame = requestAnimationFrame(() => { + virtualizer.measure(); + }); + + return () => { + cancelAnimationFrame(frame); + }; + }, [inputHeight, measuredHeights, scrollWidth, virtualizer]); + + React.useLayoutEffect(() => { + if (typeof ResizeObserver === "undefined") { return; } - requestAnimationFrame(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = 0; + const measureHeights = () => { + setMeasuredHeights((prev) => { + let changed = false; + const next: Record = {}; + + for (const message of messages) { + const element = measurementRefs.current.get(message.send_time); + const measuredHeight = element?.getBoundingClientRect().height; + const nextHeight = + typeof measuredHeight === "number" && + Number.isFinite(measuredHeight) + ? Math.ceil(measuredHeight) + : (prev[message.send_time] ?? FALLBACK_MESSAGE_HEIGHT); + + next[message.send_time] = nextHeight; + + if (prev[message.send_time] !== nextHeight) { + changed = true; + } + } + + if (!changed && Object.keys(prev).length === messages.length) { + return prev; + } + + return next; + }); + }; + + measureHeights(); + + const observer = new ResizeObserver(() => { + measureHeights(); + }); + + for (const message of messages) { + const element = measurementRefs.current.get(message.send_time); + if (element) { + observer.observe(element); } + } + + return () => { + observer.disconnect(); + }; + }, [messages, scrollWidth]); + + React.useEffect(() => { + if (typeof window === "undefined") { + return; + } + + const restoreBottomAfterResize = () => { + shouldRestoreBottomOnResizeRef.current = + isAtBottomRef.current || shouldRestoreBottomOnFocusResizeRef.current; requestAnimationFrame(() => { - didInitialScrollRef.current = true; - setDidInitialScroll(true); + virtualizer.measure(); + + requestAnimationFrame(() => { + if (!scrollRef.current || !shouldRestoreBottomOnResizeRef.current) { + return; + } + + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + isAtBottomRef.current = true; + shouldRestoreBottomOnFocusResizeRef.current = false; + shouldRestoreBottomOnResizeRef.current = false; + }); }); + }; + + window.addEventListener("resize", restoreBottomAfterResize); + window.visualViewport?.addEventListener("resize", restoreBottomAfterResize); + + return () => { + window.removeEventListener("resize", restoreBottomAfterResize); + window.visualViewport?.removeEventListener( + "resize", + restoreBottomAfterResize, + ); + }; + }, [virtualizer]); + + React.useLayoutEffect(() => { + const element = inputBoxRef.current; + if (!element || typeof ResizeObserver === "undefined") { + return; + } + + const updateHeight = () => { + setInputHeight(element.getBoundingClientRect().height); + }; + + updateHeight(); + + const observer = new ResizeObserver(updateHeight); + observer.observe(element); + + return () => { + observer.disconnect(); + }; + }, [inputBoxRef]); + + React.useEffect(() => { + const element = inputBoxRef.current; + if (!element) { + return; + } + + const handleFocusIn = () => { + if (!scrollRef.current) { + return; + } + + shouldRestoreBottomOnFocusResizeRef.current = + getDistanceFromBottom(scrollRef.current) <= stickyBottomThreshold; + }; + + const handleFocusOut = () => { + shouldRestoreBottomOnFocusResizeRef.current = false; + }; + + element.addEventListener("focusin", handleFocusIn); + element.addEventListener("focusout", handleFocusOut); + + return () => { + element.removeEventListener("focusin", handleFocusIn); + element.removeEventListener("focusout", handleFocusOut); + }; + }, [inputBoxRef, stickyBottomThreshold]); + + /** + * Loads the next page when the scroll container reaches the top. + * @returns Promise that resolves once pagination handling completes. + */ + const onScroll = React.useCallback(async () => { + if (!scrollRef.current) { + return; + } + + if (scrollRef.current.scrollTop > 96) { + return; + } + + if (messagesQuery.isFetchingNextPage || !messagesQuery.hasNextPage) { + return; + } + + setPrependAnchor({ + totalSize: virtualizer.getTotalSize(), + scrollTop: scrollRef.current.scrollTop, }); - }, [virtualizer, virtualRowCount]); + + await messagesQuery.fetchNextPage(); + }, [messagesQuery, virtualizer]); + + React.useLayoutEffect(() => { + if ( + !scrollRef.current || + hasScrolledToBottomInitially || + virtualRowCount === 0 + ) { + return; + } + + let rafId: number; + let stableFrames = 0; + let lastScrollHeight = 0; + + const settleToBottom = () => { + if (!scrollRef.current || hasScrolledToBottomInitially) { + return; + } + + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + const currentScrollHeight = scrollRef.current.scrollHeight; + + if (currentScrollHeight === lastScrollHeight) { + stableFrames++; + } else { + stableFrames = 0; + lastScrollHeight = currentScrollHeight; + } + + if (stableFrames >= 2) { + isAtBottomRef.current = true; + setHasScrolledToBottomInitially(true); + } else { + rafId = requestAnimationFrame(settleToBottom); + } + }; + + settleToBottom(); + + return () => { + cancelAnimationFrame(rafId); + }; + }, [hasScrolledToBottomInitially, virtualRowCount]); React.useLayoutEffect(() => { const count = liveMessagesSnapshot.length; - if (count > lastLiveMessageCount && isAtBottomRef.current) { - requestAnimationFrame(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = 0; - } - }); + if (!scrollRef.current) { + return; + } + + if (count > lastLiveMessageCount) { + const shouldStickToBottom = + getDistanceFromBottom(scrollRef.current) <= stickyBottomThreshold; + + if (shouldStickToBottom) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + isAtBottomRef.current = true; + } } setLastLiveMessageCount(count); }, [ lastLiveMessageCount, liveMessagesSnapshot.length, - virtualRowCount, - virtualizer, + stickyBottomThreshold, ]); - React.useEffect(() => { - if ( - viewportHeight === 0 || - totalSize > viewportHeight || - messagesQuery.isFetchingNextPage || - !messagesQuery.hasNextPage - ) { - return; - } - - void messagesQuery.fetchNextPage(); - }, [didInitialScroll, messagesQuery, totalSize, viewportHeight]); - React.useLayoutEffect(() => { if ( - !didInitialScroll || - userScrolledUpRef.current || - virtualRowCount === 0 + !scrollRef.current || + !prependAnchor || + messagesQuery.isFetchingNextPage ) { return; } - requestAnimationFrame(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = 0; - } - }); - }, [didInitialScroll, virtualRowCount]); + const delta = virtualizer.getTotalSize() - prependAnchor.totalSize; + scrollRef.current.scrollTop = prependAnchor.scrollTop + delta; + setPrependAnchor(null); + }, [messagesQuery.isFetchingNextPage, prependAnchor, virtualizer]); - React.useEffect(() => { - const root = scrollRef.current; - const sentinel = topSentinelRef.current; - - if (!root || !sentinel || !didInitialScroll) { - return; + /** + * Triggers asynchronous scroll pagination without returning a promise to JSX. + * @returns Void. + */ + const handleContainerScroll = React.useCallback((): void => { + if (scrollRef.current) { + isAtBottomRef.current = + getDistanceFromBottom(scrollRef.current) <= stickyBottomThreshold; } - const observer = new IntersectionObserver( - (entries) => { - const entry = entries[0]; - if ( - shouldFetchPreviousPage({ - entry, - hasNextPage: messagesQuery.hasNextPage, - isFetchingNextPage: messagesQuery.isFetchingNextPage, - userScrolledUp: userScrolledUpRef.current, - }) - ) { - void messagesQuery.fetchNextPage(); - } - }, - { root, rootMargin: "240px 0px 0px 0px" }, - ); + void onScroll(); + }, [onScroll, stickyBottomThreshold]); - observer.observe(sentinel); - - return () => { - observer.disconnect(); - }; - }, [didInitialScroll, messagesQuery, virtualRowCount]); - - const handleContainerScroll = React.useCallback(() => { - if (!scrollRef.current) { - return; - } - - isAtBottomRef.current = scrollRef.current.scrollTop <= 140; - - if (didInitialScrollRef.current && scrollRef.current.scrollTop > 140) { - userScrolledUpRef.current = true; - } - }, []); - - React.useEffect(() => { - const element = scrollRef.current; - if (!element) { - return; - } - - smoothScrollTargetRef.current = element.scrollTop; - - const animateScroll = () => { - const distance = smoothScrollTargetRef.current - element.scrollTop; - if (Math.abs(distance) < 0.5) { - element.scrollTop = smoothScrollTargetRef.current; - smoothScrollFrameRef.current = null; - handleContainerScroll(); - return; - } - - element.scrollTop += distance * 0.35; - handleContainerScroll(); - smoothScrollFrameRef.current = requestAnimationFrame(animateScroll); - }; - - const 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]); + const [value, setValue] = React.useState(""); + const totalSize = virtualizer.getTotalSize(); + const verticalOffset = Math.max(0, scrollHeight - totalSize); + const contentHeight = Math.max(totalSize, scrollHeight); + // Render if (!hasValidChatUser) { return (
@@ -422,24 +462,31 @@ export default function Screen() { className="min-h-0 flex-1 overflow-y-auto" style={{ overflowAnchor: "none", - paddingTop: "22px", - transform: "scaleY(-1)", + paddingBottom: "22px", }} onScroll={handleContainerScroll} > + {messagesQuery.isPending && ( +
+

+ + Loading... +

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