diff --git a/Cargo.lock b/Cargo.lock index 5ba7bca52..e304d2308 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -176,17 +176,18 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "attohttpc" -version = "0.28.5" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07a9b245ba0739fc90935094c29adbaee3f977218b5fb95e822e261cda7f56a3" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ - "http 1.4.0", + "base64", + "http", "log", - "rustls 0.23.37", + "rustls", "serde", "serde_json", "url", - "webpki-roots 0.26.11", + "webpki-roots 1.0.6", ] [[package]] @@ -197,9 +198,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-creds" -version = "0.37.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f84143206b9c72b3c5cb65415de60c7539c79cd1559290fddec657939131be0" +checksum = "ca3b85155d265df828f84e53886ed9e427aed979dd8a39f5b8b2162c77e142d7" dependencies = [ "attohttpc", "home", @@ -207,7 +208,7 @@ dependencies = [ "quick-xml", "rust-ini", "serde", - "thiserror 1.0.69", + "thiserror", "time", "url", ] @@ -236,11 +237,11 @@ dependencies = [ [[package]] name = "aws-region" -version = "0.25.5" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aed3f9c7eac9be28662fdb3b0f4d1951e812f7c64fed4f0327ba702f459b3b" +checksum = "838b36c8dc927b6db1b6c6b8f5d05865f2213550b9e83bf92fa99ed6525472c0" dependencies = [ - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -251,14 +252,14 @@ checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", "axum-macros", - "base64 0.22.1", + "base64", "bytes", "form_urlencoded", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-util", "itoa", "matchit", @@ -288,8 +289,8 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "mime", "pin-project-lite", @@ -329,12 +330,6 @@ dependencies = [ "bitcoin_hashes", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -481,6 +476,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cbc" version = "0.1.2" @@ -560,7 +564,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -649,6 +653,19 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "compression-codecs" version = "0.4.37" @@ -701,16 +718,6 @@ dependencies = [ "tiny-keccak", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -1275,7 +1282,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http", "indexmap", "slab", "tokio", @@ -1378,17 +1385,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.4.0" @@ -1399,17 +1395,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -1417,7 +1402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http", ] [[package]] @@ -1428,8 +1413,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -1445,29 +1430,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.8.1" @@ -1479,8 +1441,8 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -1491,34 +1453,20 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "rustls 0.21.12", - "tokio", - "tokio-rustls 0.24.1", -] - [[package]] name = "hyper-rustls" version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.4.0", - "hyper 1.8.1", + "http", + "hyper", "hyper-util", - "rustls 0.23.37", - "rustls-native-certs 0.8.3", + "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower-service", "webpki-roots 1.0.6", ] @@ -1529,13 +1477,13 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "hyper 1.8.1", + "http", + "http-body", + "hyper", "ipnet", "libc", "percent-encoding", @@ -1558,7 +1506,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -1824,7 +1772,7 @@ version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64 0.22.1", + "base64", "js-sys", "pem", "ring", @@ -1947,9 +1895,9 @@ dependencies = [ [[package]] name = "md5" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" [[package]] name = "memchr" @@ -1973,18 +1921,18 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3589659543c04c7dc5526ec858591015b87cd8746583b51b48ef4353f99dbcda" dependencies = [ - "base64 0.22.1", + "base64", "http-body-util", - "hyper 1.8.1", - "hyper-rustls 0.27.7", + "hyper", + "hyper-rustls", "hyper-util", "indexmap", "ipnet", "metrics", "metrics-util", "quanta", - "rustls 0.23.37", - "thiserror 2.0.18", + "rustls", + "thiserror", "tokio", "tracing", ] @@ -2013,9 +1961,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minidom" -version = "0.15.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f45614075738ce1b77a1768912a60c0227525971b03e09122a05b8a34a2a6278" +checksum = "e394a0e3c7ccc2daea3dffabe82f09857b6b510cb25af87d54bf3e910ac1642d" dependencies = [ "rxml", ] @@ -2104,7 +2052,7 @@ checksum = "14ad56c1d9a59f4edc46b17bc64a217b38b99baefddc0080f85ad98a0855336d" dependencies = [ "aes", "async-trait", - "base64 0.22.1", + "base64", "bech32", "bip39", "bitcoin", @@ -2128,6 +2076,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2209,6 +2166,25 @@ dependencies = [ "libc", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2227,12 +2203,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-probe" version = "0.2.1" @@ -2275,7 +2245,7 @@ dependencies = [ "libc", "redox_syscall 0.5.18", "smallvec", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -2311,7 +2281,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64 0.22.1", + "base64", "serde_core", ] @@ -2477,9 +2447,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.32.0" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", "serde", @@ -2497,9 +2467,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.37", + "rustls", "socket2 0.6.3", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "web-time", @@ -2517,10 +2487,10 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.37", + "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror", "tinyvec", "tracing", "web-time", @@ -2725,34 +2695,37 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "futures-util", + "http", + "http-body", "http-body-util", - "hyper 1.8.1", - "hyper-rustls 0.27.7", + "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.37", + "rustls", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots 1.0.6", ] @@ -2778,7 +2751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba6b9d2f0efe2258b23767f1f9e0054cfbcac9c2d6f81a031214143096d7864f" dependencies = [ "async-trait", - "base64 0.22.1", + "base64", "chrono", "futures", "pastey", @@ -2787,7 +2760,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -2838,38 +2811,35 @@ dependencies = [ [[package]] name = "rust-s3" -version = "0.35.1" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3df3f353b1f4209dcf437d777cda90279c397ab15a0cd6fd06bd32c88591533" +checksum = "4af74047374528b627109d579ce86b23ccf6ffba7ff363c807126c1aff69e1bb" dependencies = [ "async-trait", "aws-creds", "aws-region", - "base64 0.22.1", + "base64", "bytes", "cfg-if", - "futures", + "futures-util", "hex", "hmac", - "http 0.2.12", - "hyper 0.14.32", - "hyper-rustls 0.24.2", + "http", "log", "maybe-async", "md5", "minidom", "percent-encoding", "quick-xml", - "rustls 0.21.12", - "rustls-native-certs 0.6.3", + "reqwest", "serde", "serde_derive", "serde_json", "sha2", - "thiserror 1.0.69", + "sysinfo", + "thiserror", "time", "tokio", - "tokio-rustls 0.24.1", "tokio-stream", "url", ] @@ -2880,18 +2850,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.23.37" @@ -2903,42 +2861,21 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.9", + "rustls-webpki", "subtle", "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe 0.1.6", - "rustls-pemfile", - "schannel", - "security-framework 2.11.1", -] - [[package]] name = "rustls-native-certs" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.1", + "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.7.0", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", + "security-framework", ] [[package]] @@ -2953,19 +2890,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -2981,20 +2908,22 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rxml" -version = "0.9.1" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a98f186c7a2f3abbffb802984b7f1dfd65dac8be1aafdaabbca4137f53f0dff7" +checksum = "65bc94b580d0f5a6b7a2d604e597513d3c673154b52ddeccd1d5c32360d945ee" dependencies = [ "bytes", "rxml_validation", - "smartstring", ] [[package]] name = "rxml_validation" -version = "0.9.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a197350ece202f19a166d1ad6d9d6de145e1d2a8ef47db299abe164dbd7530" +checksum = "826e80413b9a35e9d33217b3dcac04cf95f6559d15944b93887a08be5496c4a4" +dependencies = [ + "compact_str", +] [[package]] name = "ryu" @@ -3064,16 +2993,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "secp256k1" version = "0.29.1" @@ -3095,19 +3014,6 @@ dependencies = [ "cc", ] -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - [[package]] name = "security-framework" version = "3.7.0" @@ -3115,7 +3021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -3314,7 +3220,7 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.18", + "thiserror", "time", ] @@ -3339,17 +3245,6 @@ dependencies = [ "serde", ] -[[package]] -name = "smartstring" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" -dependencies = [ - "autocfg", - "static_assertions", - "version_check", -] - [[package]] name = "socket2" version = "0.5.10" @@ -3400,11 +3295,11 @@ dependencies = [ "futures-util", "nostr", "reqwest", - "rustls 0.23.37", + "rustls", "serde", "serde_json", "sprout-core", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-tungstenite 0.26.2", "toml", @@ -3440,7 +3335,7 @@ dependencies = [ "sha2", "sprout-core", "sqlx", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "uuid", @@ -3461,7 +3356,7 @@ dependencies = [ "sha2", "sprout-core", "subtle", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -3478,7 +3373,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "uuid", ] @@ -3494,7 +3389,7 @@ dependencies = [ "sha2", "sprout-core", "sqlx", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "uuid", @@ -3513,7 +3408,7 @@ dependencies = [ "serde_json", "sha2", "sprout-core", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "uuid", @@ -3528,12 +3423,12 @@ dependencies = [ "nostr", "reqwest", "rmcp", - "rustls 0.23.37", + "rustls", "schemars", "serde", "serde_json", "sprout-core", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-tungstenite 0.26.2", "tracing", @@ -3560,7 +3455,7 @@ dependencies = [ "serde_json", "sha2", "sprout-core", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", ] @@ -3582,7 +3477,7 @@ dependencies = [ "serde_json", "sha2", "sprout-core", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-tungstenite 0.26.2", "tower-http", @@ -3605,7 +3500,7 @@ dependencies = [ "serde_json", "sprout-auth", "sprout-core", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "uuid", @@ -3617,7 +3512,7 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", - "base64 0.22.1", + "base64", "bytes", "chrono", "dashmap", @@ -3643,7 +3538,7 @@ dependencies = [ "sprout-search", "sprout-workflow", "sqlx", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tower", @@ -3664,7 +3559,7 @@ dependencies = [ "serde", "serde_json", "sprout-core", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "uuid", @@ -3675,20 +3570,20 @@ name = "sprout-test-client" version = "0.1.0" dependencies = [ "anyhow", - "base64 0.22.1", + "base64", "chrono", "futures-util", "hex", "nostr", "rand 0.8.5", "reqwest", - "rustls 0.23.37", + "rustls", "serde", "serde_json", "sha2", "sprout-core", "sprout-mcp", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-tungstenite 0.26.2", "tracing", @@ -3713,7 +3608,7 @@ dependencies = [ "serde_yaml", "sprout-core", "sprout-db", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "uuid", @@ -3738,7 +3633,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "chrono", "crc", @@ -3756,12 +3651,12 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rustls 0.23.37", + "rustls", "serde", "serde_json", "sha2", "smallvec", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-stream", "tracing", @@ -3815,7 +3710,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64 0.22.1", + "base64", "bitflags", "byteorder", "bytes", @@ -3846,7 +3741,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror", "tracing", "uuid", "whoami", @@ -3859,7 +3754,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64 0.22.1", + "base64", "bitflags", "byteorder", "chrono", @@ -3885,7 +3780,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror", "tracing", "uuid", "whoami", @@ -3911,7 +3806,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.18", + "thiserror", "tracing", "url", "uuid", @@ -3983,39 +3878,33 @@ dependencies = [ "syn", ] +[[package]] +name = "sysinfo" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + [[package]] name = "tagptr" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -4130,23 +4019,13 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.37", + "rustls", "tokio", ] @@ -4169,10 +4048,10 @@ checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" dependencies = [ "futures-util", "log", - "rustls 0.23.37", + "rustls", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tungstenite 0.26.2", "webpki-roots 0.26.11", ] @@ -4271,8 +4150,8 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "iri-string", "pin-project-lite", @@ -4385,14 +4264,14 @@ checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http", "httparse", "log", "rand 0.9.2", - "rustls 0.23.37", + "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.18", + "thiserror", "utf-8", ] @@ -4404,12 +4283,12 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http", "httparse", "log", "rand 0.9.2", "sha1", - "thiserror 2.0.18", + "thiserror", "utf-8", ] @@ -4655,6 +4534,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -4743,6 +4635,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -4751,9 +4678,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", ] [[package]] @@ -4778,19 +4716,53 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -4799,7 +4771,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -4835,7 +4807,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -4875,7 +4847,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -4886,6 +4858,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" diff --git a/crates/sprout-media/Cargo.toml b/crates/sprout-media/Cargo.toml index 93c6d2e04..2f6b05d31 100644 --- a/crates/sprout-media/Cargo.toml +++ b/crates/sprout-media/Cargo.toml @@ -19,7 +19,7 @@ sha2 = { workspace = true } hex = { workspace = true } chrono = { workspace = true } axum = { workspace = true } -s3 = { version = "0.35", package = "rust-s3", default-features = false, features = ["tokio-rustls-tls", "fail-on-err", "tags"] } +s3 = { version = "0.37", package = "rust-s3", default-features = false, features = ["tokio-rustls-tls", "fail-on-err", "tags"] } infer = "0.16" image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "webp"] } blurhash = "0.2" diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index e080b41a3..30875b9e5 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -50,6 +50,7 @@ import { relayClient } from "@/shared/api/relayClient"; import { getEventById, joinChannel } from "@/shared/api/tauri"; import { useIdentityQuery } from "@/shared/api/hooks"; import type { Channel, RelayEvent, SearchHit } from "@/shared/api/types"; +import { ChannelNavigationProvider } from "@/shared/context/ChannelNavigationContext"; import { SidebarInset, SidebarProvider, @@ -58,7 +59,6 @@ import { type AppView = "home" | "channel" | "settings" | "agents"; type MainView = Exclude; - export function AppShell() { const [selectedView, setSelectedView] = React.useState("home"); const [settingsSection, setSettingsSection] = React.useState( @@ -217,6 +217,14 @@ export function AppShell() { .filter((value) => value && value.trim().length > 0) .join(" ") || "Channel details and activity." : "Connect to the relay to browse channels and read messages."; + const contentPaneKey = + selectedView === "home" + ? "home" + : selectedView === "agents" + ? "agents" + : selectedView === "settings" + ? "settings" + : `channel:${activeChannel?.id ?? "none"}`; const shouldLoadTimeline = activeChannel !== null && activeChannel.channelType !== "forum"; const isTimelineLoading = @@ -357,7 +365,6 @@ export function AppShell() { isCancelled = true; }; }, []); - React.useEffect(() => { if (previousActiveChannelIdRef.current === activeChannelId) { return; @@ -367,7 +374,6 @@ export function AppShell() { setReplyTargetId(null); requestedAncestorIdsRef.current.clear(); }, [activeChannelId]); - React.useEffect(() => { if (selectedView === "settings") { return; @@ -375,13 +381,11 @@ export function AppShell() { lastNonSettingsViewRef.current = selectedView; }, [selectedView]); - React.useEffect(() => { if (replyTargetId && !replyTargetMessage) { setReplyTargetId(null); } }, [replyTargetId, replyTargetMessage]); - React.useEffect(() => { if (!activeChannel || activeChannel.channelType === "forum") { return; @@ -473,265 +477,273 @@ export function AppShell() { }, [handleCloseSettings, handleOpenSettings, selectedView]); return ( - - {selectedView === "settings" ? ( -
- -
- ) : ( - - - { - const createdChannel = await createChannelMutation.mutateAsync({ - name, - description, - channelType: "stream", - visibility, - }); + + + {selectedView === "settings" ? ( +
+ +
+ ) : ( + + + { + const createdChannel = await createChannelMutation.mutateAsync({ + name, + description, + channelType: "stream", + visibility, + }); - openChannelView(createdChannel.id); - }} - onCreateForum={async ({ description, name, visibility }) => { - const createdForum = await createForumMutation.mutateAsync({ - name, - description, - channelType: "forum", - visibility, - }); + openChannelView(createdChannel.id); + }} + onCreateForum={async ({ description, name, visibility }) => { + const createdForum = await createForumMutation.mutateAsync({ + name, + description, + channelType: "forum", + visibility, + }); - openChannelView(createdForum.id); - }} - onOpenBrowseChannels={() => { - setBrowseDialogType("stream"); - void refetchChannels(); - }} - onOpenBrowseForums={() => { - setBrowseDialogType("forum"); - void refetchChannels(); - }} - onOpenSearch={() => { - setIsSearchOpen(true); - void refetchChannels(); - }} - onOpenDm={async ({ pubkeys }) => { - const directMessage = await openDmMutation.mutateAsync({ - pubkeys, - }); - openChannelView(directMessage.id); - }} - onSelectAgents={() => { - React.startTransition(() => { - setSelectedView("agents"); - }); - }} - onSelectHome={() => { - React.startTransition(() => { - setSelectedView("home"); - }); + openChannelView(createdForum.id); + }} + onOpenBrowseChannels={() => { + setBrowseDialogType("stream"); + void refetchChannels(); + }} + onOpenBrowseForums={() => { + setBrowseDialogType("forum"); + void refetchChannels(); + }} + onOpenSearch={() => { + setIsSearchOpen(true); + void refetchChannels(); + }} + onOpenDm={async ({ pubkeys }) => { + const directMessage = await openDmMutation.mutateAsync({ + pubkeys, + }); + openChannelView(directMessage.id); + }} + onSelectAgents={() => { + React.startTransition(() => { + setSelectedView("agents"); + }); + }} + onSelectHome={() => { + React.startTransition(() => { + setSelectedView("home"); + }); - void homeFeedQuery.refetch(); - }} - onSelectChannel={handleOpenChannel} - onSelectSettings={handleOpenSettings} - profile={profileQuery.data} - selectedChannelId={selectedChannel?.id ?? null} - selectedView={selectedView} - unreadChannelIds={unreadChannelIds} - /> + void homeFeedQuery.refetch(); + }} + onSelectChannel={handleOpenChannel} + onSelectSettings={handleOpenSettings} + profile={profileQuery.data} + selectedChannelId={selectedChannel?.id ?? null} + selectedView={selectedView} + unreadChannelIds={unreadChannelIds} + /> - - {selectedView === "home" ? ( - - ) : selectedView === "agents" ? ( - - ) : ( - { - setIsChannelManagementOpen(true); - }} - /> - ) : null - } - channelType={activeChannel?.channelType} - description={channelDescription} - statusBadge={ - activeChannel?.channelType === "dm" && - activeDmPresenceStatus ? ( - - ) : null - } - title={activeChannelTitle} - /> - )} - -
+ {selectedView === "home" ? ( - { - void homeFeedQuery.refetch(); - }} + ) : selectedView === "agents" ? ( - - ) : activeChannel?.channelType === "forum" ? ( - ) : ( - { - setReplyTargetId(null); - }} - onReply={(message) => { - setReplyTargetId((current) => - current === message.id ? null : message.id, - ); - }} - onSend={async (content, mentionPubkeys, mediaTags) => { - await sendMessageMutation.mutateAsync({ - content, - mentionPubkeys, - parentEventId: replyTargetId, - mediaTags, - }); - setReplyTargetId(null); - }} - onTargetReached={(messageId) => { - setSearchAnchor((current) => - current?.eventId === messageId ? null : current, - ); - }} - onToggleReaction={ - activeChannel && activeChannel.archivedAt === null - ? async (message, emoji, remove) => { - await toggleReactionMutation.mutateAsync({ - emoji, - eventId: message.id, - remove, - }); - } - : undefined + { + setIsChannelManagementOpen(true); + }} + /> + ) : null } - profiles={messageProfiles} - replyTargetId={replyTargetId} - replyTargetMessage={replyTargetMessage} - targetMessageId={ - activeChannel && - searchAnchor?.channelId === activeChannel.id - ? searchAnchor.eventId - : null + channelType={activeChannel?.channelType} + description={channelDescription} + statusBadge={ + activeChannel?.channelType === "dm" && + activeDmPresenceStatus ? ( + + ) : null } - typingPubkeys={typingPubkeys} + title={activeChannelTitle} /> )} -
-
-
- )} - +
+ {selectedView === "home" ? ( + { + void homeFeedQuery.refetch(); + }} + /> + ) : selectedView === "agents" ? ( + + ) : activeChannel?.channelType === "forum" ? ( + + ) : ( + { + setReplyTargetId(null); + }} + onReply={(message) => { + setReplyTargetId((current) => + current === message.id ? null : message.id, + ); + }} + onSend={async (content, mentionPubkeys, mediaTags) => { + await sendMessageMutation.mutateAsync({ + content, + mentionPubkeys, + parentEventId: replyTargetId, + mediaTags, + }); + setReplyTargetId(null); + }} + onTargetReached={(messageId) => { + setSearchAnchor((current) => + current?.eventId === messageId ? null : current, + ); + }} + onToggleReaction={ + activeChannel && activeChannel.archivedAt === null + ? async (message, emoji, remove) => { + await toggleReactionMutation.mutateAsync({ + emoji, + eventId: message.id, + remove, + }); + } + : undefined + } + profiles={messageProfiles} + replyTargetId={replyTargetId} + replyTargetMessage={replyTargetMessage} + targetMessageId={ + activeChannel && + searchAnchor?.channelId === activeChannel.id + ? searchAnchor.eventId + : null + } + typingPubkeys={typingPubkeys} + /> + )} +
+ +
+ )} - + - { - React.startTransition(() => { - setIsChannelManagementOpen(false); - setSelectedView("home"); - }); - }} - onOpenChange={setIsChannelManagementOpen} - open={isChannelManagementOpen && activeChannel !== null} - /> -
+ + + { + React.startTransition(() => { + setIsChannelManagementOpen(false); + setSelectedView("home"); + }); + }} + onOpenChange={setIsChannelManagementOpen} + open={isChannelManagementOpen && activeChannel !== null} + /> + + ); } diff --git a/desktop/src/features/forum/ui/ForumComposer.tsx b/desktop/src/features/forum/ui/ForumComposer.tsx index c64ad33ba..c376404df 100644 --- a/desktop/src/features/forum/ui/ForumComposer.tsx +++ b/desktop/src/features/forum/ui/ForumComposer.tsx @@ -1,7 +1,9 @@ import { Send } from "lucide-react"; import * as React from "react"; +import { useChannelLinks } from "@/features/messages/lib/useChannelLinks"; import { useMentions } from "@/features/messages/lib/useMentions"; +import { ChannelAutocomplete } from "@/features/messages/ui/ChannelAutocomplete"; import { MentionAutocomplete } from "@/features/messages/ui/MentionAutocomplete"; import { Button } from "@/shared/ui/button"; import { Textarea } from "@/shared/ui/textarea"; @@ -29,6 +31,7 @@ export function ForumComposer({ const textareaRef = React.useRef(null); const mentions = useMentions(channelId); + const channelLinks = useChannelLinks(); function handleSubmit(event: React.FormEvent) { event.preventDefault(); @@ -38,15 +41,34 @@ export function ForumComposer({ onSubmit(trimmed, pubkeys); setValue(""); mentions.clearMentions(); + channelLinks.clearChannels(); } function handleChange(event: React.ChangeEvent) { const next = event.target.value; setValue(next); mentions.updateMentionQuery(next, event.target.selectionStart); + channelLinks.updateChannelQuery(next, event.target.selectionStart); } function handleKeyDown(event: React.KeyboardEvent) { + const channelResult = channelLinks.handleChannelKeyDown(event); + if (channelResult.handled) { + if (channelResult.suggestion) { + const textarea = textareaRef.current; + const result = channelLinks.insertChannel( + channelResult.suggestion, + value, + textarea?.selectionEnd ?? value.length, + ); + setValue(result.nextContent); + requestAnimationFrame(() => { + textarea?.setSelectionRange(result.nextCursor, result.nextCursor); + }); + } + return; + } + const { handled, suggestion } = mentions.handleMentionKeyDown(event); if (handled) { if (suggestion) { @@ -76,12 +98,32 @@ export function ForumComposer({ onSubmit(trimmed, pubkeys); setValue(""); mentions.clearMentions(); + channelLinks.clearChannels(); } } } return (
+ { + const textarea = textareaRef.current; + const result = channelLinks.insertChannel( + suggestion, + value, + textarea?.selectionEnd ?? value.length, + ); + setValue(result.nextContent); + requestAnimationFrame(() => { + textarea?.setSelectionRange(result.nextCursor, result.nextCursor); + textarea?.focus(); + }); + }} + selectedIndex={channelLinks.channelSelectedIndex} + suggestions={ + channelLinks.isChannelOpen ? channelLinks.channelSuggestions : [] + } + /> { const textarea = textareaRef.current; diff --git a/desktop/src/features/messages/lib/useChannelLinks.ts b/desktop/src/features/messages/lib/useChannelLinks.ts new file mode 100644 index 000000000..26976286b --- /dev/null +++ b/desktop/src/features/messages/lib/useChannelLinks.ts @@ -0,0 +1,153 @@ +import * as React from "react"; + +import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext"; + +export type ChannelSuggestion = { + id: string; + name: string; + channelType: "stream" | "forum"; +}; + +function detectChannelQuery( + value: string, + cursorPosition: number, +): { query: string; startIndex: number } | null { + const beforeCursor = value.slice(0, cursorPosition); + const match = beforeCursor.match(/(?:^|[\s])#([^\s]*)$/); + if (!match) { + return null; + } + + const query = match[1]; + const startIndex = beforeCursor.length - query.length - 1; // -1 for # + return { query, startIndex }; +} + +export function useChannelLinks() { + const { channels } = useChannelNavigation(); + + const [channelQuery, setChannelQuery] = React.useState(null); + const [channelStartIndex, setChannelStartIndex] = React.useState(0); + const [channelSelectedIndex, setChannelSelectedIndex] = React.useState(0); + + const channelSuggestions = React.useMemo(() => { + if (channelQuery === null) { + return []; + } + + const lowerQuery = channelQuery.toLowerCase(); + return channels + .filter( + (ch) => + ch.channelType !== "dm" && ch.name.toLowerCase().includes(lowerQuery), + ) + .slice(0, 8) + .map((ch) => ({ + id: ch.id, + name: ch.name, + channelType: ch.channelType as "stream" | "forum", + })); + }, [channels, channelQuery]); + + const isChannelOpen = channelQuery !== null && channelSuggestions.length > 0; + + const insertChannel = React.useCallback( + ( + suggestion: ChannelSuggestion, + content: string, + selectionEnd: number, + ): { nextContent: string; nextCursor: number } => { + const before = content.slice(0, channelStartIndex); + const after = content.slice(selectionEnd); + const inserted = `#${suggestion.name} `; + const nextContent = `${before}${inserted}${after}`; + const nextCursor = before.length + inserted.length; + + setChannelQuery(null); + setChannelSelectedIndex(0); + + return { nextContent, nextCursor }; + }, + [channelStartIndex], + ); + + const updateChannelQuery = React.useCallback( + (value: string, cursorPosition: number) => { + const channel = detectChannelQuery(value, cursorPosition); + if (channel) { + setChannelQuery(channel.query); + setChannelStartIndex(channel.startIndex); + setChannelSelectedIndex(0); + } else { + setChannelQuery(null); + } + }, + [], + ); + + const clearChannels = React.useCallback(() => { + setChannelQuery(null); + setChannelSelectedIndex(0); + }, []); + + const handleChannelKeyDown = React.useCallback( + ( + event: React.KeyboardEvent, + ): { handled: boolean; suggestion?: ChannelSuggestion } => { + if (!isChannelOpen) { + return { handled: false }; + } + + if (event.key === "ArrowDown") { + event.preventDefault(); + setChannelSelectedIndex((current) => + current < channelSuggestions.length - 1 ? current + 1 : 0, + ); + return { handled: true }; + } + + if (event.key === "ArrowUp") { + event.preventDefault(); + setChannelSelectedIndex((current) => + current > 0 ? current - 1 : channelSuggestions.length - 1, + ); + return { handled: true }; + } + + if ( + event.key === "Tab" || + (event.key === "Enter" && + !event.ctrlKey && + !event.metaKey && + !event.altKey && + !event.shiftKey) + ) { + event.preventDefault(); + return { + handled: true, + suggestion: channelSuggestions[channelSelectedIndex], + }; + } + + if (event.key === "Escape") { + event.preventDefault(); + setChannelQuery(null); + return { handled: true }; + } + + return { handled: false }; + }, + [isChannelOpen, channelSelectedIndex, channelSuggestions], + ); + + return { + channelQuery, + channelSelectedIndex, + channelSuggestions, + clearChannels, + handleChannelKeyDown, + insertChannel, + isChannelOpen, + updateChannelQuery, + }; +} diff --git a/desktop/src/features/messages/ui/ChannelAutocomplete.tsx b/desktop/src/features/messages/ui/ChannelAutocomplete.tsx new file mode 100644 index 000000000..12654626b --- /dev/null +++ b/desktop/src/features/messages/ui/ChannelAutocomplete.tsx @@ -0,0 +1,61 @@ +import * as React from "react"; + +import type { ChannelSuggestion } from "@/features/messages/lib/useChannelLinks"; +import { cn } from "@/shared/lib/cn"; + +type ChannelAutocompleteProps = { + suggestions: ChannelSuggestion[]; + selectedIndex: number; + onSelect: (suggestion: ChannelSuggestion) => void; +}; + +export function ChannelAutocomplete({ + suggestions, + selectedIndex, + onSelect, +}: ChannelAutocompleteProps) { + const listRef = React.useRef(null); + + React.useEffect(() => { + const activeItem = listRef.current?.children[selectedIndex] as + | HTMLElement + | undefined; + activeItem?.scrollIntoView({ block: "nearest" }); + }, [selectedIndex]); + + if (suggestions.length === 0) { + return null; + } + + return ( +
+
+ {suggestions.map((suggestion, index) => ( + + ))} +
+
+ ); +} diff --git a/desktop/src/features/messages/ui/MessageComposer.tsx b/desktop/src/features/messages/ui/MessageComposer.tsx index a391b952d..350885ddd 100644 --- a/desktop/src/features/messages/ui/MessageComposer.tsx +++ b/desktop/src/features/messages/ui/MessageComposer.tsx @@ -1,7 +1,8 @@ import * as React from "react"; -import { useManagedAgentsQuery } from "@/features/agents/hooks"; -import { useChannelMembersQuery } from "@/features/channels/hooks"; +import { useChannelLinks } from "@/features/messages/lib/useChannelLinks"; +import type { ChannelSuggestion } from "@/features/messages/lib/useChannelLinks"; +import { useMentions } from "@/features/messages/lib/useMentions"; import { type BlobDescriptor, pickAndUploadMedia, @@ -9,6 +10,7 @@ import { } from "@/shared/api/tauri"; import { Button } from "@/shared/ui/button"; import { Textarea } from "@/shared/ui/textarea"; +import { ChannelAutocomplete } from "./ChannelAutocomplete"; import { MentionAutocomplete, type MentionSuggestion, @@ -67,10 +69,8 @@ export function MessageComposer({ const draftSelectionRef = React.useRef({ end: 0, start: 0 }); const [isEmojiPickerOpen, setIsEmojiPickerOpen] = React.useState(false); - const [mentionQuery, setMentionQuery] = React.useState(null); - const [mentionStartIndex, setMentionStartIndex] = React.useState(0); - const [mentionSelectedIndex, setMentionSelectedIndex] = React.useState(0); - const mentionMapRef = React.useRef>(new Map()); + const mentions = useMentions(channelId); + const channelLinks = useChannelLinks(); const [uploadState, setUploadState] = React.useState<{ status: "idle" | "uploading" | "error"; @@ -78,127 +78,51 @@ export function MessageComposer({ }>({ status: "idle" }); const [pendingImeta, setPendingImeta] = React.useState([]); - // biome-ignore lint/correctness/useExhaustiveDependencies: channelId is the sole trigger — reset all composer state on channel switch to prevent draft/upload/mention leaks + // biome-ignore lint/correctness/useExhaustiveDependencies: channelId is the sole trigger — reset all composer state on channel switch to prevent draft/upload/autocomplete leaks React.useEffect(() => { setContent(""); setPendingImeta([]); setUploadState({ status: "idle" }); - setMentionQuery(null); - setMentionStartIndex(0); - setMentionSelectedIndex(0); setIsEmojiPickerOpen(false); - mentionMapRef.current.clear(); + mentions.clearMentions(); + channelLinks.clearChannels(); draftSelectionRef.current = { end: 0, start: 0 }; pendingSelectionRef.current = null; }, [channelId]); - - const membersQuery = useChannelMembersQuery(channelId); - const members = membersQuery.data ?? []; - const managedAgentsQuery = useManagedAgentsQuery(); - const managedAgentNamesByPubkey = React.useMemo( - () => - new Map( - (managedAgentsQuery.data ?? []).map((agent) => [ - agent.pubkey.toLowerCase(), - agent.name, - ]), - ), - [managedAgentsQuery.data], - ); - - const suggestions = React.useMemo(() => { - if (mentionQuery === null) { - return []; - } - - const lowerQuery = mentionQuery.toLowerCase(); - return members - .map((member) => { - const fallbackName = - managedAgentNamesByPubkey.get(member.pubkey.toLowerCase()) ?? - member.pubkey.slice(0, 8); - - return { - member, - label: member.displayName ?? fallbackName, - }; - }) - .filter( - ({ label, member }) => - label.toLowerCase().includes(lowerQuery) || - member.pubkey.toLowerCase().includes(lowerQuery), - ) - .slice(0, 8) - .map(({ member, label }) => ({ - pubkey: member.pubkey, - displayName: label, - role: member.role === "admin" ? "admin" : null, - })); - }, [managedAgentNamesByPubkey, members, mentionQuery]); - - const isMentionOpen = mentionQuery !== null && suggestions.length > 0; - - const insertMention = React.useCallback( + const applyMentionInsert = React.useCallback( (suggestion: MentionSuggestion) => { const textarea = textareaRef.current; - if (!textarea) { - return; - } - - const displayName = suggestion.displayName; - const before = content.slice(0, mentionStartIndex); - const after = content.slice(textarea.selectionEnd); - const inserted = `@${displayName} `; - const nextContent = `${before}${inserted}${after}`; - const nextCursor = before.length + inserted.length; - - mentionMapRef.current.set(displayName, suggestion.pubkey); + const result = mentions.insertMention( + suggestion, + content, + textarea?.selectionEnd ?? content.length, + ); draftSelectionRef.current = { - end: nextCursor, - start: nextCursor, + end: result.nextCursor, + start: result.nextCursor, }; - pendingSelectionRef.current = nextCursor; - setContent(nextContent); - setMentionQuery(null); - setMentionSelectedIndex(0); + pendingSelectionRef.current = result.nextCursor; + setContent(result.nextContent); }, - [content, mentionStartIndex], + [content, mentions.insertMention], ); - const extractMentionPubkeys = React.useCallback( - (text: string): string[] => { - const pubkeys: string[] = []; - - const hasMention = (name: string): boolean => { - const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const pattern = new RegExp( - `(?:^|\\s)@${escaped}(?=[\\s,;.!?:)\\]}]|$)`, - "i", - ); - return pattern.test(text); + const applyChannelInsert = React.useCallback( + (suggestion: ChannelSuggestion) => { + const textarea = textareaRef.current; + const result = channelLinks.insertChannel( + suggestion, + content, + textarea?.selectionEnd ?? content.length, + ); + draftSelectionRef.current = { + end: result.nextCursor, + start: result.nextCursor, }; - - for (const [displayName, pubkey] of mentionMapRef.current) { - if (hasMention(displayName)) { - pubkeys.push(pubkey); - } - } - - for (const member of members) { - if (pubkeys.includes(member.pubkey)) { - continue; - } - const name = - member.displayName ?? - managedAgentNamesByPubkey.get(member.pubkey.toLowerCase()); - if (name && hasMention(name)) { - pubkeys.push(member.pubkey); - } - } - - return [...new Set(pubkeys)]; + pendingSelectionRef.current = result.nextCursor; + setContent(result.nextContent); }, - [members, managedAgentNamesByPubkey], + [content, channelLinks.insertChannel], ); const updateDraftSelection = React.useCallback( @@ -230,10 +154,9 @@ export function MessageComposer({ pendingSelectionRef.current = nextCursor; setContent(nextContent); setIsEmojiPickerOpen(false); - setMentionQuery(null); - setMentionSelectedIndex(0); + mentions.clearMentions(); }, - [content], + [content, mentions.clearMentions], ); const openMentionPicker = React.useCallback(() => { @@ -245,9 +168,7 @@ export function MessageComposer({ const cursorPosition = textarea.selectionStart ?? content.length; const existingMention = detectMentionQuery(content, cursorPosition); if (existingMention) { - setMentionStartIndex(existingMention.startIndex); - setMentionQuery(existingMention.query); - setMentionSelectedIndex(0); + mentions.updateMentionQuery(content, cursorPosition); textarea.focus(); return; } @@ -271,10 +192,8 @@ export function MessageComposer({ pendingSelectionRef.current = nextCursor; setContent(nextContent); setIsEmojiPickerOpen(false); - setMentionStartIndex(mentionIndex); - setMentionQuery(""); - setMentionSelectedIndex(0); - }, [content]); + mentions.updateMentionQuery(nextContent, nextCursor); + }, [content, mentions.updateMentionQuery]); const onUploaded = React.useCallback((descriptor: BlobDescriptor) => { const markdown = `\n![image](${descriptor.url})\n`; @@ -374,7 +293,7 @@ export function MessageComposer({ return; } - const pubkeys = extractMentionPubkeys(trimmed); + const pubkeys = mentions.extractMentionPubkeys(trimmed); const mediaTags = pendingImeta.length > 0 @@ -396,8 +315,8 @@ export function MessageComposer({ setContent(""); draftSelectionRef.current = { end: 0, start: 0 }; setPendingImeta([]); - mentionMapRef.current.clear(); - setMentionQuery(null); + mentions.clearMentions(); + channelLinks.clearChannels(); setIsEmojiPickerOpen(false); try { @@ -411,7 +330,9 @@ export function MessageComposer({ disabled, isSending, onSend, - extractMentionPubkeys, + mentions.extractMentionPubkeys, + mentions.clearMentions, + channelLinks.clearChannels, pendingImeta, ]); @@ -429,56 +350,32 @@ export function MessageComposer({ const cursorPos = event.target.selectionStart; setContent(nextContent); updateDraftSelection(event.target); - - const mention = detectMentionQuery(nextContent, cursorPos); - if (mention) { - setMentionQuery(mention.query); - setMentionStartIndex(mention.startIndex); - setMentionSelectedIndex(0); - } else { - setMentionQuery(null); - } + mentions.updateMentionQuery(nextContent, cursorPos); + channelLinks.updateChannelQuery(nextContent, cursorPos); }, - [updateDraftSelection], + [ + updateDraftSelection, + mentions.updateMentionQuery, + channelLinks.updateChannelQuery, + ], ); const handleKeyDown = React.useCallback( (event: React.KeyboardEvent) => { - if (isMentionOpen) { - if (event.key === "ArrowDown") { - event.preventDefault(); - setMentionSelectedIndex((current) => - current < suggestions.length - 1 ? current + 1 : 0, - ); - return; + const channelResult = channelLinks.handleChannelKeyDown(event); + if (channelResult.handled) { + if (channelResult.suggestion) { + applyChannelInsert(channelResult.suggestion); } + return; + } - if (event.key === "ArrowUp") { - event.preventDefault(); - setMentionSelectedIndex((current) => - current > 0 ? current - 1 : suggestions.length - 1, - ); - return; - } - - if ( - event.key === "Tab" || - (event.key === "Enter" && - !event.ctrlKey && - !event.metaKey && - !event.altKey && - !event.shiftKey) - ) { - event.preventDefault(); - insertMention(suggestions[mentionSelectedIndex]); - return; - } - - if (event.key === "Escape") { - event.preventDefault(); - setMentionQuery(null); - return; + const { handled, suggestion } = mentions.handleMentionKeyDown(event); + if (handled) { + if (suggestion) { + applyMentionInsert(suggestion); } + return; } if (event.key !== "Enter" || event.nativeEvent.isComposing) { @@ -508,10 +405,10 @@ export function MessageComposer({ void submitMessage(); }, [ - isMentionOpen, - suggestions, - mentionSelectedIndex, - insertMention, + channelLinks.handleChannelKeyDown, + applyChannelInsert, + mentions.handleMentionKeyDown, + applyMentionInsert, submitMessage, ], ); @@ -568,10 +465,17 @@ export function MessageComposer({ handleSubmit(event); }} > + {replyTarget ? ( diff --git a/desktop/src/shared/context/ChannelNavigationContext.tsx b/desktop/src/shared/context/ChannelNavigationContext.tsx new file mode 100644 index 000000000..cabdf88e6 --- /dev/null +++ b/desktop/src/shared/context/ChannelNavigationContext.tsx @@ -0,0 +1,39 @@ +import * as React from "react"; + +import type { Channel } from "@/shared/api/types"; + +type ChannelNavigationContextValue = { + channels: Channel[]; + onOpenChannel: (channelId: string) => void; +}; + +const ChannelNavigationContext = + React.createContext({ + channels: [], + onOpenChannel: () => {}, + }); + +export function ChannelNavigationProvider({ + channels, + children, + onOpenChannel, +}: { + channels: Channel[]; + children: React.ReactNode; + onOpenChannel: (channelId: string) => void; +}) { + const value = React.useMemo( + () => ({ channels, onOpenChannel }), + [channels, onOpenChannel], + ); + + return ( + + {children} + + ); +} + +export function useChannelNavigation() { + return React.useContext(ChannelNavigationContext); +} diff --git a/desktop/src/shared/lib/remarkChannelLinks.ts b/desktop/src/shared/lib/remarkChannelLinks.ts new file mode 100644 index 000000000..5c11fd777 --- /dev/null +++ b/desktop/src/shared/lib/remarkChannelLinks.ts @@ -0,0 +1,73 @@ +/** + * Remark plugin that detects #channel-name patterns in text nodes and wraps them + * in custom HAST `channel-link` elements for styled rendering via react-markdown. + */ +export default function remarkChannelLinks() { + // biome-ignore lint/suspicious/noExplicitAny: remark tree types are not available + return (tree: any) => { + walkChildren(tree); + }; +} + +// biome-ignore lint/suspicious/noExplicitAny: remark tree types are not available +function walkChildren(node: any) { + if (!node?.children || !Array.isArray(node.children)) { + return; + } + + for (let i = node.children.length - 1; i >= 0; i--) { + const child = node.children[i]; + + if (child.type === "text") { + const parts = splitChannelLinks(child.value); + if (parts.length > 1) { + node.children.splice(i, 1, ...parts); + } + } else { + walkChildren(child); + } + } +} + +function splitChannelLinks(text: string) { + const channelPattern = /#[a-zA-Z0-9][\w-]*/g; + // biome-ignore lint/suspicious/noExplicitAny: building mdast-compatible nodes + const parts: any[] = []; + let lastIndex = 0; + let match: RegExpExecArray | null = null; + + while (true) { + match = channelPattern.exec(text); + if (!match) { + break; + } + + if (match.index > lastIndex) { + parts.push({ type: "text", value: text.slice(lastIndex, match.index) }); + } + + const channelName = match[0].slice(1); + + parts.push({ + type: "channel-link", + value: match[0], + data: { + hName: "channel-link", + hChildren: [{ type: "text", value: match[0] }], + channelName, + }, + }); + + lastIndex = match.index + match[0].length; + } + + if (parts.length === 0) { + return [{ type: "text", value: text }]; + } + + if (lastIndex < text.length) { + parts.push({ type: "text", value: text.slice(lastIndex) }); + } + + return parts; +} diff --git a/desktop/src/shared/ui/markdown.tsx b/desktop/src/shared/ui/markdown.tsx index 649719bd6..7a6813305 100644 --- a/desktop/src/shared/ui/markdown.tsx +++ b/desktop/src/shared/ui/markdown.tsx @@ -4,7 +4,10 @@ import remarkBreaks from "remark-breaks"; import remarkGfm from "remark-gfm"; import { cn } from "@/shared/lib/cn"; +import remarkChannelLinks from "@/shared/lib/remarkChannelLinks"; import remarkMentions from "@/shared/lib/remarkMentions"; +import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext"; +import type { Channel } from "@/shared/api/types"; type MarkdownProps = { className?: string; @@ -15,7 +18,11 @@ type MarkdownProps = { type MarkdownVariant = "default" | "compact" | "tight"; -function createMarkdownComponents(variant: MarkdownVariant): Components { +function createMarkdownComponents( + variant: MarkdownVariant, + channels: Channel[], + onOpenChannel: (channelId: string) => void, +): Components { const paragraphClassName = variant === "tight" ? "leading-5" @@ -139,6 +146,36 @@ function createMarkdownComponents(variant: MarkdownVariant): Components { {children} ), + "channel-link": ({ children }: { children?: React.ReactNode }) => { + const text = String(children ?? ""); + const channelName = text.startsWith("#") ? text.slice(1) : text; + const channel = channels.find( + (c) => + c.channelType !== "dm" && + c.name.toLowerCase() === channelName.toLowerCase(), + ); + + if (channel) { + return ( + + ); + } + + return ( + + {children} + + ); + }, } as Components; } @@ -149,6 +186,7 @@ export function Markdown({ tight = false, }: MarkdownProps) { const variant = tight ? "tight" : compact ? "compact" : "default"; + const { channels, onOpenChannel } = useChannelNavigation(); let processedContent = content; if (/^(?:\s{2}\n)+/.test(content)) { @@ -171,8 +209,13 @@ export function Markdown({ )} > {processedContent}