Commit 249e439c27e5
tarball29 files changed+1661-326
@@ -51,6 +51,12 @@ dependencies = [
5151
]5252
5353
[[package]]54
+name = "allocator-api2"55
+version = "0.2.21"56
+source = "registry+https://github.com/rust-lang/crates.io-index"57
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"58
+59
+[[package]]5460
name = "android_system_properties"5561
version = "0.1.6"5662
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -86,6 +92,15 @@ dependencies = [
8692
]8793
8894
[[package]]95
+name = "arc-swap"96
+version = "1.9.3"97
+source = "registry+https://github.com/rust-lang/crates.io-index"98
+checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e"99
+dependencies = [100
+ "rustversion",101
+]102
+103
+[[package]]89104
name = "archery"90105
version = "1.2.3"91106
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -361,6 +376,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
361376
checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6"362377
363378
[[package]]379
+name = "bitpacking"380
+version = "0.9.3"381
+source = "registry+https://github.com/rust-lang/crates.io-index"382
+checksum = "96a7139abd3d9cebf8cd6f920a389cf3dc9576172e32f4563f188cae3c3eb019"383
+dependencies = [384
+ "crunchy",385
+]386
+387
+[[package]]364388
name = "blake3"365389
version = "1.8.7"366390
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -392,6 +416,29 @@ dependencies = [
392416
]393417
394418
[[package]]419
+name = "bon"420
+version = "3.10.3"421
+source = "registry+https://github.com/rust-lang/crates.io-index"422
+checksum = "a05442cd7005532d0cfedec676b5b9333d2b4c7c3afabe2b1a9ce2742fbe2ee6"423
+dependencies = [424
+ "bon-macros",425
+]426
+427
+[[package]]428
+name = "bon-macros"429
+version = "3.10.3"430
+source = "registry+https://github.com/rust-lang/crates.io-index"431
+checksum = "93833e97ac7c46ba97045afcf247238c27057538fcf2f06344662ddd5f567be6"432
+dependencies = [433
+ "darling",434
+ "ident_case",435
+ "prettyplease",436
+ "proc-macro2",437
+ "quote",438
+ "syn 3.0.6",439
+]440
+441
+[[package]]395442
name = "bs58"396443
version = "0.5.1"397444
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -464,6 +511,12 @@ dependencies = [
464511
]465512
466513
[[package]]514
+name = "census"515
+version = "0.4.2"516
+source = "registry+https://github.com/rust-lang/crates.io-index"517
+checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0"518
+519
+[[package]]467520
name = "cfg-if"468521
version = "1.0.5"469522
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -668,12 +721,46 @@ dependencies = [
668721
]669722
670723
[[package]]724
+name = "crossbeam-channel"725
+version = "0.5.17"726
+source = "registry+https://github.com/rust-lang/crates.io-index"727
+checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"728
+dependencies = [729
+ "crossbeam-utils",730
+]731
+732
+[[package]]733
+name = "crossbeam-deque"734
+version = "0.8.8"735
+source = "registry+https://github.com/rust-lang/crates.io-index"736
+checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"737
+dependencies = [738
+ "crossbeam-epoch",739
+ "crossbeam-utils",740
+]741
+742
+[[package]]743
+name = "crossbeam-epoch"744
+version = "0.9.21"745
+source = "registry+https://github.com/rust-lang/crates.io-index"746
+checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"747
+dependencies = [748
+ "crossbeam-utils",749
+]750
+751
+[[package]]671752
name = "crossbeam-utils"672753
version = "0.8.23"673754
source = "registry+https://github.com/rust-lang/crates.io-index"674755
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"675756
676757
[[package]]758
+name = "crunchy"759
+version = "0.2.4"760
+source = "registry+https://github.com/rust-lang/crates.io-index"761
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"762
+763
+[[package]]677764
name = "crypto-common"678765
version = "0.1.7"679766
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -732,6 +819,46 @@ dependencies = [
732819
]733820
734821
[[package]]822
+name = "darling"823
+version = "0.24.1"824
+source = "registry+https://github.com/rust-lang/crates.io-index"825
+checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"826
+dependencies = [827
+ "darling_core",828
+ "darling_macro",829
+]830
+831
+[[package]]832
+name = "darling_core"833
+version = "0.24.1"834
+source = "registry+https://github.com/rust-lang/crates.io-index"835
+checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"836
+dependencies = [837
+ "ident_case",838
+ "proc-macro2",839
+ "quote",840
+ "strsim",841
+ "syn 3.0.6",842
+]843
+844
+[[package]]845
+name = "darling_macro"846
+version = "0.24.1"847
+source = "registry+https://github.com/rust-lang/crates.io-index"848
+checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"849
+dependencies = [850
+ "darling_core",851
+ "quote",852
+ "syn 3.0.6",853
+]854
+855
+[[package]]856
+name = "datasketches"857
+version = "0.2.0"858
+source = "registry+https://github.com/rust-lang/crates.io-index"859
+checksum = "c286de4e81ea2590afc24d754e0f83810c566f50a1388fa75ebd57928c0d9745"860
+861
+[[package]]735862
name = "date_header"736863
version = "1.0.5"737864
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -818,6 +945,9 @@ name = "deranged"
818945
version = "0.5.8"819946
source = "registry+https://github.com/rust-lang/crates.io-index"820947
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"948
+dependencies = [949
+ "serde_core",950
+]821951
822952
[[package]]823953
name = "derivative"@@ -895,6 +1025,12 @@ dependencies = [
8951025
]8961026
8971027
[[package]]1028
+name = "downcast-rs"1029
+version = "2.0.2"1030
+source = "registry+https://github.com/rust-lang/crates.io-index"1031
+checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc"1032
+1033
+[[package]]8981034
name = "dunce"8991035
version = "1.0.5"9001036
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -969,6 +1105,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
9691105
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"9701106
9711107
[[package]]1108
+name = "erased-serde"1109
+version = "0.4.11"1110
+source = "registry+https://github.com/rust-lang/crates.io-index"1111
+checksum = "1e59b668157f32d3459d4bd634861a06ffa2503a461ca5f0916b9f0252605e66"1112
+dependencies = [1113
+ "serde",1114
+ "serde_core",1115
+ "typeid",1116
+]1117
+1118
+[[package]]9721119
name = "errno"9731120
version = "0.3.14"9741121
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -1063,6 +1210,12 @@ dependencies = [
10631210
]10641211
10651212
[[package]]1213
+name = "fastdivide"1214
+version = "0.4.2"1215
+source = "registry+https://github.com/rust-lang/crates.io-index"1216
+checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471"1217
+1218
+[[package]]10661219
name = "fastrand"10671220
version = "2.5.0"10681221
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -1113,6 +1266,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
11131266
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"11141267
11151268
[[package]]1269
+name = "foldhash"1270
+version = "0.2.0"1271
+source = "registry+https://github.com/rust-lang/crates.io-index"1272
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"1273
+1274
+[[package]]11161275
name = "form_urlencoded"11171276
version = "1.2.2"11181277
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -1362,7 +1521,18 @@ version = "0.15.5"
13621521
source = "registry+https://github.com/rust-lang/crates.io-index"13631522
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"13641523
dependencies = [1365
- "foldhash",1524
+ "foldhash 0.1.5",1525
+]1526
+1527
+[[package]]1528
+name = "hashbrown"1529
+version = "0.16.1"1530
+source = "registry+https://github.com/rust-lang/crates.io-index"1531
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"1532
+dependencies = [1533
+ "allocator-api2",1534
+ "equivalent",1535
+ "foldhash 0.2.0",13661536
]13671537
13681538
[[package]]@@ -1421,6 +1591,12 @@ dependencies = [
14211591
]14221592
14231593
[[package]]1594
+name = "htmlescape"1595
+version = "0.3.1"1596
+source = "registry+https://github.com/rust-lang/crates.io-index"1597
+checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"1598
+1599
+[[package]]14241600
name = "http"14251601
version = "1.5.0"14261602
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -1639,6 +1815,12 @@ dependencies = [
16391815
]16401816
16411817
[[package]]1818
+name = "ident_case"1819
+version = "1.0.1"1820
+source = "registry+https://github.com/rust-lang/crates.io-index"1821
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"1822
+1823
+[[package]]16421824
name = "idna"16431825
version = "1.1.0"16441826
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -1779,6 +1961,15 @@ dependencies = [
17791961
]17801962
17811963
[[package]]1964
+name = "inventory"1965
+version = "0.3.25"1966
+source = "registry+https://github.com/rust-lang/crates.io-index"1967
+checksum = "6928282826c822ad91bf1c9a1cb90a30ba1c26770749929b4656cd6be829cd7c"1968
+dependencies = [1969
+ "rustversion",1970
+]1971
+1972
+[[package]]17821973
name = "ipnet"17831974
version = "2.12.2"17841975
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -1919,6 +2110,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
19192110
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"19202111
19212112
[[package]]2113
+name = "levenshtein_automata"2114
+version = "0.2.1"2115
+source = "registry+https://github.com/rust-lang/crates.io-index"2116
+checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25"2117
+2118
+[[package]]19222119
name = "libc"19232120
version = "0.2.189"19242121
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -1992,15 +2189,30 @@ dependencies = [
19922189
]19932190
19942191
[[package]]2192
+name = "lru"2193
+version = "0.16.4"2194
+source = "registry+https://github.com/rust-lang/crates.io-index"2195
+checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"2196
+dependencies = [2197
+ "hashbrown 0.16.1",2198
+]2199
+2200
+[[package]]19952201
name = "lru-slab"19962202
version = "0.1.3"19972203
source = "registry+https://github.com/rust-lang/crates.io-index"19982204
checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"19992205
20002206
[[package]]2207
+name = "lz4_flex"2208
+version = "0.13.1"2209
+source = "registry+https://github.com/rust-lang/crates.io-index"2210
+checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e"2211
+2212
+[[package]]20012213
name = "macltok"20022214
version = "0.1.0"2003
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/macltok?rev=8fd8dcf69c68a45d7ceb5d553230e1df3efada9d#8fd8dcf69c68a45d7ceb5d553230e1df3efada9d"2215
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/macltok?rev=e521a7177da3a9606f06df063e7095e5a1f00447#e521a7177da3a9606f06df063e7095e5a1f00447"20042216
dependencies = [20052217
"futures-util",20062218
"getrandom 0.2.17",@@ -2008,6 +2220,7 @@ dependencies = [
20082220
"getrandom 0.4.3",20092221
"http",20102222
"imbl",2223
+ "log",20112224
"matrix-sdk",20122225
"matrix-sdk-base",20132226
"matrix-sdk-crypto",@@ -2020,6 +2233,8 @@ dependencies = [
20202233
"serde",20212234
"serde_json",20222235
"sha2",2236
+ "tantivy",2237
+ "tantivy-common",20232238
"thiserror 2.0.21",20242239
"tokio",20252240
"tracing",@@ -2232,7 +2447,7 @@ dependencies = [
22322447
[[package]]22332448
name = "matrix-sdk"22342449
version = "0.18.0"2235
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2450
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"22362451
dependencies = [22372452
"anymap2",22382453
"aquamarine",@@ -2295,7 +2510,7 @@ dependencies = [
22952510
[[package]]22962511
name = "matrix-sdk-base"22972512
version = "0.18.0"2298
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2513
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"22992514
dependencies = [23002515
"as_variant",23012516
"assert_matches",@@ -2325,7 +2540,7 @@ dependencies = [
23252540
[[package]]23262541
name = "matrix-sdk-common"23272542
version = "0.18.0"2328
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2543
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"23292544
dependencies = [23302545
"eyeball-im",23312546
"futures-core",@@ -2348,7 +2563,7 @@ dependencies = [
23482563
[[package]]23492564
name = "matrix-sdk-crypto"23502565
version = "0.18.0"2351
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2566
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"23522567
dependencies = [23532568
"aes",23542569
"aquamarine",@@ -2389,7 +2604,7 @@ dependencies = [
23892604
[[package]]23902605
name = "matrix-sdk-indexeddb"23912606
version = "0.18.0"2392
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2607
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"23932608
dependencies = [23942609
"async-trait",23952610
"base64 0.22.1",@@ -2420,7 +2635,7 @@ dependencies = [
24202635
[[package]]24212636
name = "matrix-sdk-sqlite"24222637
version = "0.18.0"2423
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2638
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"24242639
dependencies = [24252640
"as_variant",24262641
"async-trait",@@ -2447,7 +2662,7 @@ dependencies = [
24472662
[[package]]24482663
name = "matrix-sdk-store-encryption"24492664
version = "0.18.0"2450
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2665
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"24512666
dependencies = [24522667
"base64 0.22.1",24532668
"blake3",@@ -2468,7 +2683,7 @@ dependencies = [
24682683
[[package]]24692684
name = "matrix-sdk-test"24702685
version = "0.18.0"2471
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2686
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"24722687
dependencies = [24732688
"as_variant",24742689
"ctor",@@ -2491,7 +2706,7 @@ dependencies = [
24912706
[[package]]24922707
name = "matrix-sdk-test-macros"24932708
version = "0.18.0"2494
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2709
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"24952710
dependencies = [24962711
"quote",24972712
"syn 2.0.119",@@ -2500,7 +2715,7 @@ dependencies = [
25002715
[[package]]25012716
name = "matrix-sdk-ui"25022717
version = "0.18.0"2503
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2718
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"25042719
dependencies = [25052720
"as_variant",25062721
"async-rx",@@ -2575,6 +2790,15 @@ dependencies = [
25752790
]25762791
25772792
[[package]]2793
+name = "measure_time"2794
+version = "0.9.0"2795
+source = "registry+https://github.com/rust-lang/crates.io-index"2796
+checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e"2797
+dependencies = [2798
+ "log",2799
+]2800
+2801
+[[package]]25782802
name = "memchr"25792803
version = "2.8.3"25802804
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -2613,6 +2837,12 @@ dependencies = [
26132837
]26142838
26152839
[[package]]2840
+name = "minimal-lexical"2841
+version = "0.2.1"2842
+source = "registry+https://github.com/rust-lang/crates.io-index"2843
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"2844
+2845
+[[package]]26162846
name = "miniz_oxide"26172847
version = "0.8.9"26182848
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -2698,12 +2928,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
26982928
checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"26992929
27002930
[[package]]2931
+name = "murmurhash32"2932
+version = "0.3.1"2933
+source = "registry+https://github.com/rust-lang/crates.io-index"2934
+checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b"2935
+2936
+[[package]]27012937
name = "new_debug_unreachable"27022938
version = "1.0.6"27032939
source = "registry+https://github.com/rust-lang/crates.io-index"27042940
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"27052941
27062942
[[package]]2943
+name = "nom"2944
+version = "7.1.3"2945
+source = "registry+https://github.com/rust-lang/crates.io-index"2946
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"2947
+dependencies = [2948
+ "memchr",2949
+ "minimal-lexical",2950
+]2951
+2952
+[[package]]27072953
name = "nu-ansi-term"27082954
version = "0.50.3"27092955
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -2774,6 +3020,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
27743020
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"27753021
27763022
[[package]]3023
+name = "oneshot"3024
+version = "0.1.13"3025
+source = "registry+https://github.com/rust-lang/crates.io-index"3026
+checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107"3027
+3028
+[[package]]27773029
name = "oorandom"27783030
version = "11.1.5"27793031
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -2792,6 +3044,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
27923044
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"27933045
27943046
[[package]]3047
+name = "ordered-float"3048
+version = "5.5.0"3049
+source = "registry+https://github.com/rust-lang/crates.io-index"3050
+checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0"3051
+dependencies = [3052
+ "num-traits",3053
+]3054
+3055
+[[package]]3056
+name = "ownedbytes"3057
+version = "0.9.0"3058
+source = "registry+https://github.com/rust-lang/crates.io-index"3059
+checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e"3060
+dependencies = [3061
+ "stable_deref_trait",3062
+]3063
+3064
+[[package]]27953065
name = "parking"27963066
version = "2.2.1"27973067
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -2993,6 +3263,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
29933263
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"29943264
29953265
[[package]]3266
+name = "prettyplease"3267
+version = "0.3.0"3268
+source = "registry+https://github.com/rust-lang/crates.io-index"3269
+checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0"3270
+dependencies = [3271
+ "proc-macro2",3272
+ "syn 3.0.6",3273
+]3274
+3275
+[[package]]29963276
name = "proc-macro-crate"29973277
version = "3.5.0"29983278
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -3239,6 +3519,26 @@ dependencies = [
32393519
]32403520
32413521
[[package]]3522
+name = "rayon"3523
+version = "1.12.0"3524
+source = "registry+https://github.com/rust-lang/crates.io-index"3525
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"3526
+dependencies = [3527
+ "either",3528
+ "rayon-core",3529
+]3530
+3531
+[[package]]3532
+name = "rayon-core"3533
+version = "1.13.0"3534
+source = "registry+https://github.com/rust-lang/crates.io-index"3535
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"3536
+dependencies = [3537
+ "crossbeam-deque",3538
+ "crossbeam-utils",3539
+]3540
+3541
+[[package]]32423542
name = "readlock"32433543
version = "0.1.11"32443544
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -3900,6 +4200,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
39004200
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"39014201
39024202
[[package]]4203
+name = "sketches-ddsketch"4204
+version = "0.4.1"4205
+source = "registry+https://github.com/rust-lang/crates.io-index"4206
+checksum = "513c3f5f732bfd6fbb187619c2dfe9d2f25f1a2976f01d575f0fd329d565df56"4207
+dependencies = [4208
+ "serde",4209
+]4210
+4211
+[[package]]39034212
name = "slab"39044213
version = "0.4.12"39054214
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -3971,6 +4280,12 @@ dependencies = [
39714280
]39724281
39734282
[[package]]4283
+name = "strsim"4284
+version = "0.11.1"4285
+source = "registry+https://github.com/rust-lang/crates.io-index"4286
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"4287
+4288
+[[package]]39744289
name = "subtle"39754290
version = "2.6.1"39764291
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -4030,6 +4345,149 @@ dependencies = [
40304345
]40314346
40324347
[[package]]4348
+name = "tantivy"4349
+version = "0.26.2"4350
+source = "registry+https://github.com/rust-lang/crates.io-index"4351
+checksum = "861facfabd71044968f364837f9a083b56464ba5a59079f88706ee5c451ca069"4352
+dependencies = [4353
+ "aho-corasick",4354
+ "arc-swap",4355
+ "base64 0.22.1",4356
+ "bitpacking",4357
+ "bon",4358
+ "byteorder",4359
+ "census",4360
+ "crc32fast",4361
+ "crossbeam-channel",4362
+ "datasketches",4363
+ "downcast-rs",4364
+ "fastdivide",4365
+ "fnv",4366
+ "htmlescape",4367
+ "itertools 0.14.0",4368
+ "levenshtein_automata",4369
+ "log",4370
+ "lru",4371
+ "lz4_flex",4372
+ "measure_time",4373
+ "once_cell",4374
+ "oneshot",4375
+ "rayon",4376
+ "regex",4377
+ "rustc-hash",4378
+ "serde",4379
+ "serde_json",4380
+ "sketches-ddsketch",4381
+ "smallvec",4382
+ "tantivy-bitpacker",4383
+ "tantivy-columnar",4384
+ "tantivy-common",4385
+ "tantivy-fst",4386
+ "tantivy-query-grammar",4387
+ "tantivy-stacker",4388
+ "tantivy-tokenizer-api",4389
+ "thiserror 2.0.21",4390
+ "time",4391
+ "typetag",4392
+ "uuid",4393
+ "winapi",4394
+]4395
+4396
+[[package]]4397
+name = "tantivy-bitpacker"4398
+version = "0.10.0"4399
+source = "registry+https://github.com/rust-lang/crates.io-index"4400
+checksum = "4fed3d674429bcd2de5d0a6d1aa5495fed8afd9c5ecce993019caf7615f53fa4"4401
+dependencies = [4402
+ "bitpacking",4403
+]4404
+4405
+[[package]]4406
+name = "tantivy-columnar"4407
+version = "0.7.0"4408
+source = "registry+https://github.com/rust-lang/crates.io-index"4409
+checksum = "c57166f5bcfd478f370ab8445afb4678dce44801fa5ce5c451aaf8595583c5dc"4410
+dependencies = [4411
+ "downcast-rs",4412
+ "fastdivide",4413
+ "itertools 0.14.0",4414
+ "serde",4415
+ "tantivy-bitpacker",4416
+ "tantivy-common",4417
+ "tantivy-sstable",4418
+ "tantivy-stacker",4419
+]4420
+4421
+[[package]]4422
+name = "tantivy-common"4423
+version = "0.11.0"4424
+source = "registry+https://github.com/rust-lang/crates.io-index"4425
+checksum = "bbf10915aa75da3c3b0d58b58853d2e889efbaf32d4982a4c3715dde6bba23e5"4426
+dependencies = [4427
+ "async-trait",4428
+ "byteorder",4429
+ "ownedbytes",4430
+ "serde",4431
+ "time",4432
+]4433
+4434
+[[package]]4435
+name = "tantivy-fst"4436
+version = "0.5.0"4437
+source = "registry+https://github.com/rust-lang/crates.io-index"4438
+checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18"4439
+dependencies = [4440
+ "byteorder",4441
+ "regex-syntax",4442
+ "utf8-ranges",4443
+]4444
+4445
+[[package]]4446
+name = "tantivy-query-grammar"4447
+version = "0.26.0"4448
+source = "registry+https://github.com/rust-lang/crates.io-index"4449
+checksum = "dfadb8526b6da90704feb293b0701a6aae62ea14983143344be2dc5ce30f1d82"4450
+dependencies = [4451
+ "fnv",4452
+ "nom",4453
+ "ordered-float",4454
+ "serde",4455
+ "serde_json",4456
+]4457
+4458
+[[package]]4459
+name = "tantivy-sstable"4460
+version = "0.7.0"4461
+source = "registry+https://github.com/rust-lang/crates.io-index"4462
+checksum = "8a2cfc3ac5164cbadc28965ffb145a8f47582a60ae5897859ad8d4316596c606"4463
+dependencies = [4464
+ "futures-util",4465
+ "itertools 0.14.0",4466
+ "tantivy-bitpacker",4467
+ "tantivy-common",4468
+ "tantivy-fst",4469
+]4470
+4471
+[[package]]4472
+name = "tantivy-stacker"4473
+version = "0.7.0"4474
+source = "registry+https://github.com/rust-lang/crates.io-index"4475
+checksum = "6cbb051742da9d53ca9e8fff43a9b10e319338b24e2c0e15d0372df19ffeb951"4476
+dependencies = [4477
+ "murmurhash32",4478
+ "tantivy-common",4479
+]4480
+4481
+[[package]]4482
+name = "tantivy-tokenizer-api"4483
+version = "0.7.0"4484
+source = "registry+https://github.com/rust-lang/crates.io-index"4485
+checksum = "eac258c2c6390673f2685813afeeafcb8c4e0ee7de8dd3fc46838dcc37263f98"4486
+dependencies = [4487
+ "serde",4488
+]4489
+4490
+[[package]]40334491
name = "tempfile"40344492
version = "3.27.0"40354493
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -4379,12 +4837,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
43794837
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"43804838
43814839
[[package]]4840
+name = "typeid"4841
+version = "1.0.3"4842
+source = "registry+https://github.com/rust-lang/crates.io-index"4843
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"4844
+4845
+[[package]]43824846
name = "typenum"43834847
version = "1.20.1"43844848
source = "registry+https://github.com/rust-lang/crates.io-index"43854849
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"43864850
43874851
[[package]]4852
+name = "typetag"4853
+version = "0.2.23"4854
+source = "registry+https://github.com/rust-lang/crates.io-index"4855
+checksum = "c90e86058a30d42a1a928dfb4b49bb33c98c3a2b4909492e6b0881cd94798ec2"4856
+dependencies = [4857
+ "erased-serde",4858
+ "inventory",4859
+ "once_cell",4860
+ "serde",4861
+ "typetag-impl",4862
+]4863
+4864
+[[package]]4865
+name = "typetag-impl"4866
+version = "0.2.23"4867
+source = "registry+https://github.com/rust-lang/crates.io-index"4868
+checksum = "f153acc4e99a5f2a5aefa09fb078be54e26271b2813f6041200b224c098d8328"4869
+dependencies = [4870
+ "proc-macro2",4871
+ "quote",4872
+ "syn 3.0.6",4873
+]4874
+4875
+[[package]]43884876
name = "typewit"43894877
version = "1.15.2"43904878
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -4475,6 +4963,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
44754963
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"44764964
44774965
[[package]]4966
+name = "utf8-ranges"4967
+version = "1.0.5"4968
+source = "registry+https://github.com/rust-lang/crates.io-index"4969
+checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"4970
+4971
+[[package]]44784972
name = "utf8_iter"44794973
version = "1.0.4"44804974
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -3,7 +3,7 @@ resolver = "3"
33
members = ["crates/malice-core", "crates/malice-server", "crates/malice-static"]44
55
[workspace.dependencies]6
-macltok = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/macltok", rev = "8fd8dcf69c68a45d7ceb5d553230e1df3efada9d" }6
+macltok = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/macltok", rev = "e521a7177da3a9606f06df063e7095e5a1f00447" }77
ruma = { version = "0.16", default-features = false }88
serde = { version = "1", features = ["derive"] }99
serde_json = "1"@@ -52,9 +52,12 @@ impl From<macltok::Error> for CoreError {
5252
);5353
}5454
ErrorKind::QueueFull | ErrorKind::RoomBusy | ErrorKind::RateLimited => Failure::Busy,55
- ErrorKind::RoomUnknown | ErrorKind::EventNotFound | ErrorKind::MediaUnknown | ErrorKind::MediaUnavailable => {56
- Failure::NotFound57
- }55
+ ErrorKind::RoomUnknown56
+ | ErrorKind::EventNotFound57
+ | ErrorKind::MediaUnknown58
+ | ErrorKind::MediaUnavailable59
+ | ErrorKind::EventContextNotOpen60
+ | ErrorKind::SearchNotOpen => Failure::NotFound,5861
ErrorKind::MediaTooLarge => Failure::TooLarge,5962
ErrorKind::Forbidden => Failure::Forbidden,6063
ErrorKind::Invalid | ErrorKind::EmptyBody => Failure::BadRequest,@@ -2,9 +2,10 @@ use std::collections::HashMap;
22
33
use futures_util::{Stream, StreamExt, stream};44
use macltok::{5
- ConnectionState, EncryptionState, ImagePacks, MentionsPage, MentionsUpdate, MirrorHandle,6
- RecentHistoryState, RoomSummary, SessionStatus, State, StateChange, TimelineChange, TimelineId,7
- TimelineItem, UploadOutcome, VerificationState, imbl::Vector,5
+ ConnectionState, EncryptionState, HistoryState, ImagePacks, MentionsPage, MentionsUpdate,6
+ MirrorHandle, RecentHistoryState, RoomSummary, SessionStatus, State, StateChange,7
+ ThreadListChange, ThreadListEntry, ThreadListState, TimelineChange, TimelineId, TimelineItem,8
+ UploadOutcome, VerificationState, imbl::Vector,89
};910
use serde::Serialize;1011
@@ -49,6 +50,23 @@ pub enum WireEvent {
4950
room_id: String,5051
root_event_id: String,5152
},53
+ ThreadList {54
+ room_id: String,55
+ entries: Vector<ThreadListEntry>,56
+ },57
+ ThreadListHistory {58
+ room_id: String,59
+ state: HistoryState,60
+ },61
+ ThreadListClosed {62
+ room_id: String,63
+ },64
+ EventContextClosed {65
+ context_id: String,66
+ },67
+ SearchClosed {68
+ search_id: String,69
+ },5270
Connection {5371
state: String,5472
},@@ -75,6 +93,7 @@ pub struct Snapshot {
7593
pub timelines: HashMap<String, Vector<TimelineItem>>,7694
pub pinned: HashMap<String, Vector<TimelineItem>>,7795
pub threads: Vec<ThreadItems>,96
+ pub thread_lists: HashMap<String, ThreadListState>,7897
pub connection: String,7998
pub encryption: Option<EncryptionState>,8099
pub verification: Option<VerificationState>,@@ -152,6 +171,11 @@ pub fn snapshot_event(state: &State) -> WireEvent {
152171
timelines,153172
pinned,154173
threads,174
+ thread_lists: state175
+ .thread_lists176
+ .iter()177
+ .map(|(room_id, list)| (room_id.as_str().to_owned(), list.clone()))178
+ .collect(),155179
connection: connection_label(&state.connection),156180
encryption: state.encryption.clone(),157181
verification: state.verification.clone(),@@ -256,6 +280,30 @@ pub fn wire_events(change: &StateChange) -> Vec<WireEvent> {
256280
state: state.clone(),257281
}],258282
StateChange::NotificationSettings => vec![WireEvent::NotificationSettings],283
+ StateChange::ThreadList { room_id, change } => {284
+ let room_id = room_id.as_str().to_owned();285
+ match change {286
+ ThreadListChange::Reset { entries } | ThreadListChange::Changed { entries, .. } => {287
+ vec![WireEvent::ThreadList {288
+ room_id,289
+ entries: entries.clone(),290
+ }]291
+ }292
+ ThreadListChange::History(state) => vec![WireEvent::ThreadListHistory {293
+ room_id,294
+ state: state.clone(),295
+ }],296
+ ThreadListChange::Closed(_) => vec![WireEvent::ThreadListClosed { room_id }],297
+ }298
+ }299
+ StateChange::EventContextClosed { context_id, .. } => {300
+ vec![WireEvent::EventContextClosed {301
+ context_id: context_id.clone(),302
+ }]303
+ }304
+ StateChange::SearchClosed { search_id, .. } => vec![WireEvent::SearchClosed {305
+ search_id: search_id.clone(),306
+ }],259307
StateChange::Failed(failure) => vec![error(&failure.message)],260308
StateChange::SessionSaveFailed(_) => {261309
vec![error(@@ -2,7 +2,8 @@ use macltok::ruma::{OwnedTransactionId, events::GlobalAccountDataEventType, serd
22
use macltok::{33
AttachmentUpload, ClientHandle, CreateRoomOptions, ImagePackSource, MediaId, MediaSize,44
MemberFilter, MentionKey, MentionsBoundary, MessageContent, MessageEditContent, MirrorHandle,5
- PackImageInfo, PackUsage, Patch, RichText, TimelineId, VerificationAction, VerificationTarget,5
+ PackImageInfo, PackUsage, Patch, RichText, SearchScope, TimelineId, VerificationAction,6
+ VerificationTarget,67
};78
use serde::Deserialize;89
use serde_json::json;@@ -113,7 +114,7 @@ pub enum Request {
113114
#[serde(default)]114115
thread_root: Option<String>,115116
},116
- StartRecentHistory {117
+ RetryRecentHistory {117118
room_id: String,118119
},119120
FillHistoryGap {@@ -132,7 +133,7 @@ pub enum Request {
132133
},133134
EventContextBefore {134135
#[serde(default)]135
- room_id: String,136
+ context_id: String,136137
#[serde(default)]137138
token: String,138139
#[serde(default)]@@ -140,12 +141,44 @@ pub enum Request {
140141
},141142
EventContextAfter {142143
#[serde(default)]143
- room_id: String,144
+ context_id: String,144145
#[serde(default)]145146
token: String,146147
#[serde(default)]147148
limit: u32,148149
},150
+ CloseEventContext {151
+ context_id: String,152
+ },153
+ OpenThreadList {154
+ room_id: String,155
+ },156
+ PaginateThreadList {157
+ room_id: String,158
+ #[serde(default)]159
+ limit: u32,160
+ },161
+ CloseThreadList {162
+ room_id: String,163
+ },164
+ Search {165
+ query: String,166
+ scope: SearchScope,167
+ #[serde(default)]168
+ limit: u32,169
+ },170
+ SearchMore {171
+ search_id: String,172
+ cursor: String,173
+ #[serde(default)]174
+ limit: u32,175
+ },176
+ CloseSearch {177
+ search_id: String,178
+ },179
+ SearchCoverage {180
+ scope: SearchScope,181
+ },149182
MentionsPage {150183
#[serde(default)]151184
cursor: Option<String>,@@ -358,6 +391,8 @@ pub enum Request {
358391
},359392
SendSticker {360393
room_id: String,394
+ #[serde(default)]395
+ thread_root: Option<String>,361396
pack_id: String,362397
shortcode: String,363398
},@@ -477,6 +512,22 @@ fn pack_text(value: String, what: &str) -> Result<String, CoreError> {
477512
Ok(value)478513
}479514
515
+/// A page of zero is macltok's Invalid, so an omitted limit means a full page instead.516
+fn page_limit(limit: u32) -> Result<u16, CoreError> {517
+ match limit {518
+ 0 => Ok(50),519
+ 1..=50 => Ok(limit as u16),520
+ _ => Err(CoreError::bad_request("limit must be 1..50")),521
+ }522
+}523
+524
+fn context_limit(limit: u32) -> Result<u16, CoreError> {525
+ if limit > 50 {526
+ return Err(CoreError::bad_request("limit must be 0..50"));527
+ }528
+ Ok(limit as u16)529
+}530
+480531
fn clone_usage(usage: Option<Vec<PackUsage>>) -> Result<Option<Vec<PackUsage>>, CoreError> {481532
if usage.as_ref().is_some_and(Vec::is_empty) {482533
return Err(CoreError::bad_request(@@ -610,9 +661,9 @@ pub async fn dispatch(
610661
.await?;611662
Ok(Reply::Done)612663
}613
- Request::StartRecentHistory { room_id } => {664
+ Request::RetryRecentHistory { room_id } => {614665
handle615
- .start_recent_history(parse_room_id(&room_id)?)666
+ .retry_recent_history(parse_room_id(&room_id)?)616667
.await?;617668
Ok(Reply::Done)618669
}@@ -633,29 +684,81 @@ pub async fn dispatch(
633684
} => {634685
let room_id = parse_room_id(&room_id)?;635686
let event_id = parse_event_id(&event_id)?;636
- let context = handle637
- .event_context(&room_id, &event_id, before_limit, after_limit)687
+ let session = handle688
+ .event_context(689
+ &room_id,690
+ &event_id,691
+ context_limit(before_limit)?,692
+ context_limit(after_limit)?,693
+ )638694
.await?;639
- Ok(Reply::Json(json!(context)))695
+ Ok(Reply::Json(json!(session)))640696
}641697
Request::EventContextBefore {642
- room_id,698
+ context_id,643699
token,644700
limit,645701
} => {646
- let room_id = parse_room_id(&room_id)?;647
- let page = handle.event_context_before(&room_id, &token, limit).await?;702
+ let page = handle703
+ .event_context_before(&context_id, &token, page_limit(limit)?)704
+ .await?;648705
Ok(Reply::Json(json!(page)))649706
}650707
Request::EventContextAfter {651
- room_id,708
+ context_id,652709
token,653710
limit,654711
} => {655
- let room_id = parse_room_id(&room_id)?;656
- let page = handle.event_context_after(&room_id, &token, limit).await?;712
+ let page = handle713
+ .event_context_after(&context_id, &token, page_limit(limit)?)714
+ .await?;715
+ Ok(Reply::Json(json!(page)))716
+ }717
+ Request::CloseEventContext { context_id } => {718
+ handle.close_event_context(&context_id).await?;719
+ Ok(Reply::Done)720
+ }721
+ Request::OpenThreadList { room_id } => {722
+ handle.open_thread_list(parse_room_id(&room_id)?).await?;723
+ Ok(Reply::Done)724
+ }725
+ Request::PaginateThreadList { room_id, limit } => {726
+ handle727
+ .paginate_thread_list(parse_room_id(&room_id)?, page_limit(limit)?)728
+ .await?;729
+ Ok(Reply::Done)730
+ }731
+ Request::CloseThreadList { room_id } => {732
+ handle.close_thread_list(parse_room_id(&room_id)?).await?;733
+ Ok(Reply::Done)734
+ }735
+ Request::Search {736
+ query,737
+ scope,738
+ limit,739
+ } => {740
+ let session = handle741
+ .search_messages(&query, scope, page_limit(limit)?)742
+ .await?;743
+ Ok(Reply::Json(json!(session)))744
+ }745
+ Request::SearchMore {746
+ search_id,747
+ cursor,748
+ limit,749
+ } => {750
+ let page = handle751
+ .search_more(&search_id, &cursor, page_limit(limit)?)752
+ .await?;657753
Ok(Reply::Json(json!(page)))658754
}755
+ Request::CloseSearch { search_id } => {756
+ handle.close_search(&search_id).await?;757
+ Ok(Reply::Done)758
+ }759
+ Request::SearchCoverage { scope } => {760
+ Ok(Reply::Json(json!(handle.search_coverage(scope).await?)))761
+ }659762
Request::MentionsPage { cursor, limit } => {660763
let page = handle661764
.mentions_page(cursor, if limit == 0 { 50 } else { limit.clamp(1, 50) })@@ -1111,12 +1214,13 @@ pub async fn dispatch(
11111214
}11121215
Request::SendSticker {11131216
room_id,1217
+ thread_root,11141218
pack_id,11151219
shortcode,11161220
} => {11171221
handle11181222
.send_sticker(1119
- parse_room_id(&room_id)?,1223
+ timeline_for(&room_id, thread_root.as_deref())?,11201224
pack_text(pack_id, "pack_id")?,11211225
pack_text(shortcode, "shortcode")?,11221226
)@@ -1222,7 +1326,7 @@ pub async fn dispatch(
12221326
usage,12231327
} => {12241328
let shortcode = handle1225
- .clone_pack_image_with_usage(1329
+ .clone_pack_image(12261330
pack_text(from_pack, "pack_id")?,12271331
pack_text(shortcode, "shortcode")?,12281332
to.source()?,@@ -1239,7 +1343,7 @@ pub async fn dispatch(
12391343
usage,12401344
} => {12411345
let shortcode = handle1242
- .clone_sticker_with_usage(1346
+ .clone_sticker(12431347
parse_room_id(&room_id)?,12441348
parse_event_id(&event_id)?,12451349
to.source()?,@@ -1256,7 +1360,7 @@ pub async fn dispatch(
12561360
usage,12571361
} => {12581362
let skipped = handle1259
- .clone_pack_with_usage(1363
+ .clone_pack(12601364
pack_text(from_pack, "pack_id")?,12611365
to.source()?,12621366
display_name,@@ -31,7 +31,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
3131
[dev-dependencies]3232
http-body-util = "0.1"3333
tower = { version = "0.5", features = ["util"] }34
-matrix-sdk = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "9b2c17fb10703d90b177b02588912a34cecfa11f", default-features = false, features = ["testing"] }34
+matrix-sdk = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd", default-features = false, features = ["testing"] }3535
reqwest = { version = "0.13", default-features = false, features = ["json", "query", "rustls"] }3636
tempfile = "3"3737
wiremock = "0.6"@@ -60,28 +60,6 @@ struct PaginateBody {
6060
}6161
6262
#[derive(Debug, Deserialize)]63
-struct EventContextBody {64
- #[serde(default)]65
- room_id: String,66
- #[serde(default)]67
- event_id: String,68
- #[serde(default)]69
- before_limit: u32,70
- #[serde(default)]71
- after_limit: u32,72
-}73
-74
-#[derive(Debug, Deserialize)]75
-struct EventContextPageBody {76
- #[serde(default)]77
- room_id: String,78
- #[serde(default)]79
- token: String,80
- #[serde(default)]81
- limit: u32,82
-}83
-84
-#[derive(Debug, Deserialize)]8563
struct JoinBody {8664
#[serde(default)]8765
room: String,@@ -584,7 +562,18 @@ async fn post_command(
584562
| Request::SetMentionsRead { .. }585563
| Request::MarkMentionsReadThrough { .. }586564
| Request::RecoverMentions587
- | Request::StartRecentHistory { .. }565
+ | Request::RetryRecentHistory { .. }566
+ | Request::EventContext { .. }567
+ | Request::EventContextBefore { .. }568
+ | Request::EventContextAfter { .. }569
+ | Request::CloseEventContext { .. }570
+ | Request::OpenThreadList { .. }571
+ | Request::PaginateThreadList { .. }572
+ | Request::CloseThreadList { .. }573
+ | Request::Search { .. }574
+ | Request::SearchMore { .. }575
+ | Request::CloseSearch { .. }576
+ | Request::SearchCoverage { .. }588577
| Request::FillHistoryGap { .. }589578
| Request::Kick { .. }590579
| Request::Ban { .. }@@ -720,76 +709,6 @@ async fn rooms_paginate(
720709
)721710
}722711
723
-async fn rooms_context(724
- State(state): State<AppState>,725
- headers: HeaderMap,726
- body: axum::Json<EventContextBody>,727
-) -> Response {728
- let (user, _) = match auth(&state, &headers).await {729
- Ok(pair) => pair,730
- Err(error) => return error.into_response(),731
- };732
- respond(733
- dispatch(734
- &user.handle,735
- &user.mirror,736
- Request::EventContext {737
- room_id: body.0.room_id,738
- event_id: body.0.event_id,739
- before_limit: body.0.before_limit,740
- after_limit: body.0.after_limit,741
- },742
- )743
- .await,744
- )745
-}746
-747
-async fn rooms_context_before(748
- State(state): State<AppState>,749
- headers: HeaderMap,750
- body: axum::Json<EventContextPageBody>,751
-) -> Response {752
- let (user, _) = match auth(&state, &headers).await {753
- Ok(pair) => pair,754
- Err(error) => return error.into_response(),755
- };756
- respond(757
- dispatch(758
- &user.handle,759
- &user.mirror,760
- Request::EventContextBefore {761
- room_id: body.0.room_id,762
- token: body.0.token,763
- limit: body.0.limit,764
- },765
- )766
- .await,767
- )768
-}769
-770
-async fn rooms_context_after(771
- State(state): State<AppState>,772
- headers: HeaderMap,773
- body: axum::Json<EventContextPageBody>,774
-) -> Response {775
- let (user, _) = match auth(&state, &headers).await {776
- Ok(pair) => pair,777
- Err(error) => return error.into_response(),778
- };779
- respond(780
- dispatch(781
- &user.handle,782
- &user.mirror,783
- Request::EventContextAfter {784
- room_id: body.0.room_id,785
- token: body.0.token,786
- limit: body.0.limit,787
- },788
- )789
- .await,790
- )791
-}792
-793712
async fn rooms_join(794713
State(state): State<AppState>,795714
headers: HeaderMap,@@ -1188,9 +1107,6 @@ pub fn router(state: AppState, web_dir: Option<String>) -> Router {
11881107
.route("/rooms/close", post(rooms_close))11891108
.route("/rooms/send", post(rooms_send))11901109
.route("/rooms/paginate", post(rooms_paginate))1191
- .route("/rooms/context", post(rooms_context))1192
- .route("/rooms/context/before", post(rooms_context_before))1193
- .route("/rooms/context/after", post(rooms_context_after))11941110
.route(11951111
"/rooms/upload",11961112
post(rooms_upload).layer(DefaultBodyLimit::max(21 * 1024 * 1024)),@@ -436,6 +436,9 @@ async fn wait_authenticated(mirror: &MirrorHandle) -> Option<String> {
436436
| StateChange::History { .. }437437
| StateChange::HistoryRepair(_)438438
| StateChange::RecentHistory { .. }439
+ | StateChange::ThreadList { .. }440
+ | StateChange::EventContextClosed { .. }441
+ | StateChange::SearchClosed { .. }439442
| StateChange::Typing(_)440443
| StateChange::Upload { .. }441444
| StateChange::UploadFinished { .. }@@ -51,6 +51,12 @@ dependencies = [
5151
]5252
5353
[[package]]54
+name = "allocator-api2"55
+version = "0.2.21"56
+source = "registry+https://github.com/rust-lang/crates.io-index"57
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"58
+59
+[[package]]5460
name = "android_system_properties"5561
version = "0.1.6"5662
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -86,6 +92,15 @@ dependencies = [
8692
]8793
8894
[[package]]95
+name = "arc-swap"96
+version = "1.9.3"97
+source = "registry+https://github.com/rust-lang/crates.io-index"98
+checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e"99
+dependencies = [100
+ "rustversion",101
+]102
+103
+[[package]]89104
name = "archery"90105
version = "1.2.3"91106
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -274,6 +289,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
274289
checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6"275290
276291
[[package]]292
+name = "bitpacking"293
+version = "0.9.3"294
+source = "registry+https://github.com/rust-lang/crates.io-index"295
+checksum = "96a7139abd3d9cebf8cd6f920a389cf3dc9576172e32f4563f188cae3c3eb019"296
+dependencies = [297
+ "crunchy",298
+]299
+300
+[[package]]277301
name = "blake3"278302
version = "1.8.7"279303
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -305,6 +329,29 @@ dependencies = [
305329
]306330
307331
[[package]]332
+name = "bon"333
+version = "3.10.3"334
+source = "registry+https://github.com/rust-lang/crates.io-index"335
+checksum = "a05442cd7005532d0cfedec676b5b9333d2b4c7c3afabe2b1a9ce2742fbe2ee6"336
+dependencies = [337
+ "bon-macros",338
+]339
+340
+[[package]]341
+name = "bon-macros"342
+version = "3.10.3"343
+source = "registry+https://github.com/rust-lang/crates.io-index"344
+checksum = "93833e97ac7c46ba97045afcf247238c27057538fcf2f06344662ddd5f567be6"345
+dependencies = [346
+ "darling",347
+ "ident_case",348
+ "prettyplease",349
+ "proc-macro2",350
+ "quote",351
+ "syn 3.0.6",352
+]353
+354
+[[package]]308355
name = "bs58"309356
version = "0.5.1"310357
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -377,6 +424,12 @@ dependencies = [
377424
]378425
379426
[[package]]427
+name = "census"428
+version = "0.4.2"429
+source = "registry+https://github.com/rust-lang/crates.io-index"430
+checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0"431
+432
+[[package]]380433
name = "cfg-if"381434
version = "1.0.5"382435
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -564,12 +617,46 @@ dependencies = [
564617
]565618
566619
[[package]]620
+name = "crossbeam-channel"621
+version = "0.5.17"622
+source = "registry+https://github.com/rust-lang/crates.io-index"623
+checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"624
+dependencies = [625
+ "crossbeam-utils",626
+]627
+628
+[[package]]629
+name = "crossbeam-deque"630
+version = "0.8.8"631
+source = "registry+https://github.com/rust-lang/crates.io-index"632
+checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"633
+dependencies = [634
+ "crossbeam-epoch",635
+ "crossbeam-utils",636
+]637
+638
+[[package]]639
+name = "crossbeam-epoch"640
+version = "0.9.21"641
+source = "registry+https://github.com/rust-lang/crates.io-index"642
+checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"643
+dependencies = [644
+ "crossbeam-utils",645
+]646
+647
+[[package]]567648
name = "crossbeam-utils"568649
version = "0.8.23"569650
source = "registry+https://github.com/rust-lang/crates.io-index"570651
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"571652
572653
[[package]]654
+name = "crunchy"655
+version = "0.2.4"656
+source = "registry+https://github.com/rust-lang/crates.io-index"657
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"658
+659
+[[package]]573660
name = "crypto-common"574661
version = "0.1.7"575662
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -618,6 +705,46 @@ dependencies = [
618705
]619706
620707
[[package]]708
+name = "darling"709
+version = "0.24.1"710
+source = "registry+https://github.com/rust-lang/crates.io-index"711
+checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"712
+dependencies = [713
+ "darling_core",714
+ "darling_macro",715
+]716
+717
+[[package]]718
+name = "darling_core"719
+version = "0.24.1"720
+source = "registry+https://github.com/rust-lang/crates.io-index"721
+checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"722
+dependencies = [723
+ "ident_case",724
+ "proc-macro2",725
+ "quote",726
+ "strsim",727
+ "syn 3.0.6",728
+]729
+730
+[[package]]731
+name = "darling_macro"732
+version = "0.24.1"733
+source = "registry+https://github.com/rust-lang/crates.io-index"734
+checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"735
+dependencies = [736
+ "darling_core",737
+ "quote",738
+ "syn 3.0.6",739
+]740
+741
+[[package]]742
+name = "datasketches"743
+version = "0.2.0"744
+source = "registry+https://github.com/rust-lang/crates.io-index"745
+checksum = "c286de4e81ea2590afc24d754e0f83810c566f50a1388fa75ebd57928c0d9745"746
+747
+[[package]]621748
name = "date_header"622749
version = "1.0.5"623750
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -686,6 +813,9 @@ name = "deranged"
686813
version = "0.5.8"687814
source = "registry+https://github.com/rust-lang/crates.io-index"688815
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"816
+dependencies = [817
+ "serde_core",818
+]689819
690820
[[package]]691821
name = "derivative"@@ -763,6 +893,12 @@ dependencies = [
763893
]764894
765895
[[package]]896
+name = "downcast-rs"897
+version = "2.0.2"898
+source = "registry+https://github.com/rust-lang/crates.io-index"899
+checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc"900
+901
+[[package]]766902
name = "dunce"767903
version = "1.0.5"768904
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -816,6 +952,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
816952
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"817953
818954
[[package]]955
+name = "erased-serde"956
+version = "0.4.11"957
+source = "registry+https://github.com/rust-lang/crates.io-index"958
+checksum = "1e59b668157f32d3459d4bd634861a06ffa2503a461ca5f0916b9f0252605e66"959
+dependencies = [960
+ "serde",961
+ "serde_core",962
+ "typeid",963
+]964
+965
+[[package]]819966
name = "errno"820967
version = "0.3.14"821968
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -910,6 +1057,12 @@ dependencies = [
9101057
]9111058
9121059
[[package]]1060
+name = "fastdivide"1061
+version = "0.4.2"1062
+source = "registry+https://github.com/rust-lang/crates.io-index"1063
+checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471"1064
+1065
+[[package]]9131066
name = "fastrand"9141067
version = "2.5.0"9151068
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -960,6 +1113,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
9601113
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"9611114
9621115
[[package]]1116
+name = "foldhash"1117
+version = "0.2.0"1118
+source = "registry+https://github.com/rust-lang/crates.io-index"1119
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"1120
+1121
+[[package]]9631122
name = "form_urlencoded"9641123
version = "1.2.2"9651124
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -1193,7 +1352,18 @@ version = "0.15.5"
11931352
source = "registry+https://github.com/rust-lang/crates.io-index"11941353
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"11951354
dependencies = [1196
- "foldhash",1355
+ "foldhash 0.1.5",1356
+]1357
+1358
+[[package]]1359
+name = "hashbrown"1360
+version = "0.16.1"1361
+source = "registry+https://github.com/rust-lang/crates.io-index"1362
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"1363
+dependencies = [1364
+ "allocator-api2",1365
+ "equivalent",1366
+ "foldhash 0.2.0",11971367
]11981368
11991369
[[package]]@@ -1246,6 +1416,12 @@ dependencies = [
12461416
]12471417
12481418
[[package]]1419
+name = "htmlescape"1420
+version = "0.3.1"1421
+source = "registry+https://github.com/rust-lang/crates.io-index"1422
+checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"1423
+1424
+[[package]]12491425
name = "http"12501426
version = "1.5.0"12511427
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -1452,6 +1628,12 @@ dependencies = [
14521628
]14531629
14541630
[[package]]1631
+name = "ident_case"1632
+version = "1.0.1"1633
+source = "registry+https://github.com/rust-lang/crates.io-index"1634
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"1635
+1636
+[[package]]14551637
name = "idna"14561638
version = "1.1.0"14571639
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -1577,6 +1759,15 @@ dependencies = [
15771759
]15781760
15791761
[[package]]1762
+name = "inventory"1763
+version = "0.3.25"1764
+source = "registry+https://github.com/rust-lang/crates.io-index"1765
+checksum = "6928282826c822ad91bf1c9a1cb90a30ba1c26770749929b4656cd6be829cd7c"1766
+dependencies = [1767
+ "rustversion",1768
+]1769
+1770
+[[package]]15801771
name = "ipnet"15811772
version = "2.12.2"15821773
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -1717,6 +1908,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
17171908
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"17181909
17191910
[[package]]1911
+name = "levenshtein_automata"1912
+version = "0.2.1"1913
+source = "registry+https://github.com/rust-lang/crates.io-index"1914
+checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25"1915
+1916
+[[package]]17201917
name = "libc"17211918
version = "0.2.189"17221919
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -1779,15 +1976,30 @@ dependencies = [
17791976
]17801977
17811978
[[package]]1979
+name = "lru"1980
+version = "0.16.4"1981
+source = "registry+https://github.com/rust-lang/crates.io-index"1982
+checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"1983
+dependencies = [1984
+ "hashbrown 0.16.1",1985
+]1986
+1987
+[[package]]17821988
name = "lru-slab"17831989
version = "0.1.3"17841990
source = "registry+https://github.com/rust-lang/crates.io-index"17851991
checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"17861992
17871993
[[package]]1994
+name = "lz4_flex"1995
+version = "0.13.1"1996
+source = "registry+https://github.com/rust-lang/crates.io-index"1997
+checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e"1998
+1999
+[[package]]17882000
name = "macltok"17892001
version = "0.1.0"1790
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/macltok?rev=8fd8dcf69c68a45d7ceb5d553230e1df3efada9d#8fd8dcf69c68a45d7ceb5d553230e1df3efada9d"2002
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/macltok?rev=e521a7177da3a9606f06df063e7095e5a1f00447#e521a7177da3a9606f06df063e7095e5a1f00447"17912003
dependencies = [17922004
"futures-util",17932005
"getrandom 0.2.17",@@ -1796,6 +2008,7 @@ dependencies = [
17962008
"http",17972009
"imbl",17982010
"js-sys",2011
+ "log",17992012
"matrix-sdk",18002013
"matrix-sdk-base",18012014
"matrix-sdk-crypto",@@ -1808,6 +2021,8 @@ dependencies = [
18082021
"serde",18092022
"serde_json",18102023
"sha2",2024
+ "tantivy",2025
+ "tantivy-common",18112026
"thiserror 2.0.21",18122027
"tokio",18132028
"tracing",@@ -1995,7 +2210,7 @@ dependencies = [
19952210
[[package]]19962211
name = "matrix-sdk"19972212
version = "0.18.0"1998
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2213
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"19992214
dependencies = [20002215
"anymap2",20012216
"aquamarine",@@ -2055,7 +2270,7 @@ dependencies = [
20552270
[[package]]20562271
name = "matrix-sdk-base"20572272
version = "0.18.0"2058
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2273
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"20592274
dependencies = [20602275
"as_variant",20612276
"async-trait",@@ -2081,7 +2296,7 @@ dependencies = [
20812296
[[package]]20822297
name = "matrix-sdk-common"20832298
version = "0.18.0"2084
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2299
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"20852300
dependencies = [20862301
"eyeball-im",20872302
"futures-core",@@ -2104,7 +2319,7 @@ dependencies = [
21042319
[[package]]21052320
name = "matrix-sdk-crypto"21062321
version = "0.18.0"2107
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2322
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"21082323
dependencies = [21092324
"aes",21102325
"aquamarine",@@ -2144,7 +2359,7 @@ dependencies = [
21442359
[[package]]21452360
name = "matrix-sdk-indexeddb"21462361
version = "0.18.0"2147
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2362
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"21482363
dependencies = [21492364
"async-trait",21502365
"base64 0.22.1",@@ -2176,7 +2391,7 @@ dependencies = [
21762391
[[package]]21772392
name = "matrix-sdk-sqlite"21782393
version = "0.18.0"2179
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2394
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"21802395
dependencies = [21812396
"as_variant",21822397
"async-trait",@@ -2203,7 +2418,7 @@ dependencies = [
22032418
[[package]]22042419
name = "matrix-sdk-store-encryption"22052420
version = "0.18.0"2206
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2421
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"22072422
dependencies = [22082423
"base64 0.22.1",22092424
"blake3",@@ -2224,7 +2439,7 @@ dependencies = [
22242439
[[package]]22252440
name = "matrix-sdk-ui"22262441
version = "0.18.0"2227
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b2c17fb10703d90b177b02588912a34cecfa11f#9b2c17fb10703d90b177b02588912a34cecfa11f"2442
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd#5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd"22282443
dependencies = [22292444
"as_variant",22302445
"async-rx",@@ -2299,6 +2514,15 @@ dependencies = [
22992514
]23002515
23012516
[[package]]2517
+name = "measure_time"2518
+version = "0.9.0"2519
+source = "registry+https://github.com/rust-lang/crates.io-index"2520
+checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e"2521
+dependencies = [2522
+ "log",2523
+]2524
+2525
+[[package]]23022526
name = "memchr"23032527
version = "2.8.3"23042528
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -2327,6 +2551,12 @@ dependencies = [
23272551
]23282552
23292553
[[package]]2554
+name = "minimal-lexical"2555
+version = "0.2.1"2556
+source = "registry+https://github.com/rust-lang/crates.io-index"2557
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"2558
+2559
+[[package]]23302560
name = "miniz_oxide"23312561
version = "0.8.9"23322562
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -2368,12 +2598,28 @@ dependencies = [
23682598
]23692599
23702600
[[package]]2601
+name = "murmurhash32"2602
+version = "0.3.1"2603
+source = "registry+https://github.com/rust-lang/crates.io-index"2604
+checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b"2605
+2606
+[[package]]23712607
name = "new_debug_unreachable"23722608
version = "1.0.6"23732609
source = "registry+https://github.com/rust-lang/crates.io-index"23742610
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"23752611
23762612
[[package]]2613
+name = "nom"2614
+version = "7.1.3"2615
+source = "registry+https://github.com/rust-lang/crates.io-index"2616
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"2617
+dependencies = [2618
+ "memchr",2619
+ "minimal-lexical",2620
+]2621
+2622
+[[package]]23772623
name = "nu-ansi-term"23782624
version = "0.50.3"23792625
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -2444,6 +2690,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
24442690
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"24452691
24462692
[[package]]2693
+name = "oneshot"2694
+version = "0.1.13"2695
+source = "registry+https://github.com/rust-lang/crates.io-index"2696
+checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107"2697
+2698
+[[package]]24472699
name = "oorandom"24482700
version = "11.1.5"24492701
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -2462,6 +2714,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
24622714
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"24632715
24642716
[[package]]2717
+name = "ordered-float"2718
+version = "5.5.0"2719
+source = "registry+https://github.com/rust-lang/crates.io-index"2720
+checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0"2721
+dependencies = [2722
+ "num-traits",2723
+]2724
+2725
+[[package]]2726
+name = "ownedbytes"2727
+version = "0.9.0"2728
+source = "registry+https://github.com/rust-lang/crates.io-index"2729
+checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e"2730
+dependencies = [2731
+ "stable_deref_trait",2732
+]2733
+2734
+[[package]]24652735
name = "parking"24662736
version = "2.2.1"24672737
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -2621,6 +2891,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
26212891
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"26222892
26232893
[[package]]2894
+name = "prettyplease"2895
+version = "0.3.0"2896
+source = "registry+https://github.com/rust-lang/crates.io-index"2897
+checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0"2898
+dependencies = [2899
+ "proc-macro2",2900
+ "syn 3.0.6",2901
+]2902
+2903
+[[package]]26242904
name = "proc-macro-crate"26252905
version = "3.5.0"26262906
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -2867,6 +3147,26 @@ dependencies = [
28673147
]28683148
28693149
[[package]]3150
+name = "rayon"3151
+version = "1.12.0"3152
+source = "registry+https://github.com/rust-lang/crates.io-index"3153
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"3154
+dependencies = [3155
+ "either",3156
+ "rayon-core",3157
+]3158
+3159
+[[package]]3160
+name = "rayon-core"3161
+version = "1.13.0"3162
+source = "registry+https://github.com/rust-lang/crates.io-index"3163
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"3164
+dependencies = [3165
+ "crossbeam-deque",3166
+ "crossbeam-utils",3167
+]3168
+3169
+[[package]]28703170
name = "readlock"28713171
version = "0.1.11"28723172
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -3491,6 +3791,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
34913791
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"34923792
34933793
[[package]]3794
+name = "sketches-ddsketch"3795
+version = "0.4.1"3796
+source = "registry+https://github.com/rust-lang/crates.io-index"3797
+checksum = "513c3f5f732bfd6fbb187619c2dfe9d2f25f1a2976f01d575f0fd329d565df56"3798
+dependencies = [3799
+ "serde",3800
+]3801
+3802
+[[package]]34943803
name = "slab"34953804
version = "0.4.12"34963805
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -3556,6 +3865,12 @@ dependencies = [
35563865
]35573866
35583867
[[package]]3868
+name = "strsim"3869
+version = "0.11.1"3870
+source = "registry+https://github.com/rust-lang/crates.io-index"3871
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"3872
+3873
+[[package]]35593874
name = "subtle"35603875
version = "2.6.1"35613876
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -3615,6 +3930,149 @@ dependencies = [
36153930
]36163931
36173932
[[package]]3933
+name = "tantivy"3934
+version = "0.26.2"3935
+source = "registry+https://github.com/rust-lang/crates.io-index"3936
+checksum = "861facfabd71044968f364837f9a083b56464ba5a59079f88706ee5c451ca069"3937
+dependencies = [3938
+ "aho-corasick",3939
+ "arc-swap",3940
+ "base64 0.22.1",3941
+ "bitpacking",3942
+ "bon",3943
+ "byteorder",3944
+ "census",3945
+ "crc32fast",3946
+ "crossbeam-channel",3947
+ "datasketches",3948
+ "downcast-rs",3949
+ "fastdivide",3950
+ "fnv",3951
+ "htmlescape",3952
+ "itertools 0.14.0",3953
+ "levenshtein_automata",3954
+ "log",3955
+ "lru",3956
+ "lz4_flex",3957
+ "measure_time",3958
+ "once_cell",3959
+ "oneshot",3960
+ "rayon",3961
+ "regex",3962
+ "rustc-hash",3963
+ "serde",3964
+ "serde_json",3965
+ "sketches-ddsketch",3966
+ "smallvec",3967
+ "tantivy-bitpacker",3968
+ "tantivy-columnar",3969
+ "tantivy-common",3970
+ "tantivy-fst",3971
+ "tantivy-query-grammar",3972
+ "tantivy-stacker",3973
+ "tantivy-tokenizer-api",3974
+ "thiserror 2.0.21",3975
+ "time",3976
+ "typetag",3977
+ "uuid",3978
+ "winapi",3979
+]3980
+3981
+[[package]]3982
+name = "tantivy-bitpacker"3983
+version = "0.10.0"3984
+source = "registry+https://github.com/rust-lang/crates.io-index"3985
+checksum = "4fed3d674429bcd2de5d0a6d1aa5495fed8afd9c5ecce993019caf7615f53fa4"3986
+dependencies = [3987
+ "bitpacking",3988
+]3989
+3990
+[[package]]3991
+name = "tantivy-columnar"3992
+version = "0.7.0"3993
+source = "registry+https://github.com/rust-lang/crates.io-index"3994
+checksum = "c57166f5bcfd478f370ab8445afb4678dce44801fa5ce5c451aaf8595583c5dc"3995
+dependencies = [3996
+ "downcast-rs",3997
+ "fastdivide",3998
+ "itertools 0.14.0",3999
+ "serde",4000
+ "tantivy-bitpacker",4001
+ "tantivy-common",4002
+ "tantivy-sstable",4003
+ "tantivy-stacker",4004
+]4005
+4006
+[[package]]4007
+name = "tantivy-common"4008
+version = "0.11.0"4009
+source = "registry+https://github.com/rust-lang/crates.io-index"4010
+checksum = "bbf10915aa75da3c3b0d58b58853d2e889efbaf32d4982a4c3715dde6bba23e5"4011
+dependencies = [4012
+ "async-trait",4013
+ "byteorder",4014
+ "ownedbytes",4015
+ "serde",4016
+ "time",4017
+]4018
+4019
+[[package]]4020
+name = "tantivy-fst"4021
+version = "0.5.0"4022
+source = "registry+https://github.com/rust-lang/crates.io-index"4023
+checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18"4024
+dependencies = [4025
+ "byteorder",4026
+ "regex-syntax",4027
+ "utf8-ranges",4028
+]4029
+4030
+[[package]]4031
+name = "tantivy-query-grammar"4032
+version = "0.26.0"4033
+source = "registry+https://github.com/rust-lang/crates.io-index"4034
+checksum = "dfadb8526b6da90704feb293b0701a6aae62ea14983143344be2dc5ce30f1d82"4035
+dependencies = [4036
+ "fnv",4037
+ "nom",4038
+ "ordered-float",4039
+ "serde",4040
+ "serde_json",4041
+]4042
+4043
+[[package]]4044
+name = "tantivy-sstable"4045
+version = "0.7.0"4046
+source = "registry+https://github.com/rust-lang/crates.io-index"4047
+checksum = "8a2cfc3ac5164cbadc28965ffb145a8f47582a60ae5897859ad8d4316596c606"4048
+dependencies = [4049
+ "futures-util",4050
+ "itertools 0.14.0",4051
+ "tantivy-bitpacker",4052
+ "tantivy-common",4053
+ "tantivy-fst",4054
+]4055
+4056
+[[package]]4057
+name = "tantivy-stacker"4058
+version = "0.7.0"4059
+source = "registry+https://github.com/rust-lang/crates.io-index"4060
+checksum = "6cbb051742da9d53ca9e8fff43a9b10e319338b24e2c0e15d0372df19ffeb951"4061
+dependencies = [4062
+ "murmurhash32",4063
+ "tantivy-common",4064
+]4065
+4066
+[[package]]4067
+name = "tantivy-tokenizer-api"4068
+version = "0.7.0"4069
+source = "registry+https://github.com/rust-lang/crates.io-index"4070
+checksum = "eac258c2c6390673f2685813afeeafcb8c4e0ee7de8dd3fc46838dcc37263f98"4071
+dependencies = [4072
+ "serde",4073
+]4074
+4075
+[[package]]36184076
name = "tempfile"36194077
version = "3.27.0"36204078
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -3953,12 +4411,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
39534411
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"39544412
39554413
[[package]]4414
+name = "typeid"4415
+version = "1.0.3"4416
+source = "registry+https://github.com/rust-lang/crates.io-index"4417
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"4418
+4419
+[[package]]39564420
name = "typenum"39574421
version = "1.20.1"39584422
source = "registry+https://github.com/rust-lang/crates.io-index"39594423
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"39604424
39614425
[[package]]4426
+name = "typetag"4427
+version = "0.2.23"4428
+source = "registry+https://github.com/rust-lang/crates.io-index"4429
+checksum = "c90e86058a30d42a1a928dfb4b49bb33c98c3a2b4909492e6b0881cd94798ec2"4430
+dependencies = [4431
+ "erased-serde",4432
+ "inventory",4433
+ "once_cell",4434
+ "serde",4435
+ "typetag-impl",4436
+]4437
+4438
+[[package]]4439
+name = "typetag-impl"4440
+version = "0.2.23"4441
+source = "registry+https://github.com/rust-lang/crates.io-index"4442
+checksum = "f153acc4e99a5f2a5aefa09fb078be54e26271b2813f6041200b224c098d8328"4443
+dependencies = [4444
+ "proc-macro2",4445
+ "quote",4446
+ "syn 3.0.6",4447
+]4448
+4449
+[[package]]39624450
name = "typewit"39634451
version = "1.15.2"39644452
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -4037,6 +4525,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
40374525
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"40384526
40394527
[[package]]4528
+name = "utf8-ranges"4529
+version = "1.0.5"4530
+source = "registry+https://github.com/rust-lang/crates.io-index"4531
+checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"4532
+4533
+[[package]]40404534
name = "utf8_iter"40414535
version = "1.0.4"40424536
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -4325,6 +4819,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
43254819
checksum = "29333c3ea1ba8b17211763463ff24ee84e41c78224c16b001cd907e663a38c68"43264820
43274821
[[package]]4822
+name = "winapi"4823
+version = "0.3.9"4824
+source = "registry+https://github.com/rust-lang/crates.io-index"4825
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"4826
+dependencies = [4827
+ "winapi-i686-pc-windows-gnu",4828
+ "winapi-x86_64-pc-windows-gnu",4829
+]4830
+4831
+[[package]]4832
+name = "winapi-i686-pc-windows-gnu"4833
+version = "0.4.0"4834
+source = "registry+https://github.com/rust-lang/crates.io-index"4835
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"4836
+4837
+[[package]]43284838
name = "winapi-util"43294839
version = "0.1.11"43304840
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -4334,6 +4844,12 @@ dependencies = [
43344844
]43354845
43364846
[[package]]4847
+name = "winapi-x86_64-pc-windows-gnu"4848
+version = "0.4.0"4849
+source = "registry+https://github.com/rust-lang/crates.io-index"4850
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"4851
+4852
+[[package]]43374853
name = "windows-core"43384854
version = "0.62.2"43394855
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -14,7 +14,7 @@ publish = false
1414
crate-type = ["cdylib", "rlib"]1515
1616
[target.'cfg(target_family = "wasm")'.dependencies]17
-macltok = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/macltok", rev = "8fd8dcf69c68a45d7ceb5d553230e1df3efada9d", features = ["indexeddb", "serde", "mirror"] }17
+macltok = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/macltok", rev = "e521a7177da3a9606f06df063e7095e5a1f00447", features = ["indexeddb", "serde", "mirror"] }1818
malice-core = { path = "../malice-core" }1919
ruma = { version = "0.16", default-features = false }2020
futures-util = { version = "0.3", default-features = false, features = ["std"] }@@ -27,7 +27,7 @@ wasm-bindgen-futures = "=0.4.78"
2727
js-sys = "=0.3.105"2828
2929
[dev-dependencies]30
-matrix-sdk = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "9b2c17fb10703d90b177b02588912a34cecfa11f", default-features = false }30
+matrix-sdk = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "5ec7600f0a5f648c6e2a34b5be0a1494a4e2eafd", default-features = false }3131
wasm-bindgen-test = "0.3"3232
web-sys = { version = "0.3", features = ["SharedWorkerGlobalScope"] }3333
gloo-timers = { version = "0.3", features = ["futures"] }@@ -113,6 +113,9 @@ async fn wait_authenticated(mirror: &MirrorHandle) -> Option<String> {
113113
| StateChange::History { .. }114114
| StateChange::HistoryRepair(_)115115
| StateChange::RecentHistory { .. }116
+ | StateChange::ThreadList { .. }117
+ | StateChange::EventContextClosed { .. }118
+ | StateChange::SearchClosed { .. }116119
| StateChange::Typing(_)117120
| StateChange::Upload { .. }118121
| StateChange::UploadFinished { .. }@@ -12,7 +12,7 @@
1212
import type { ScopeOption } from './lib/search-model';1313
import { releaseStaged, stageFiles as stageAttachments, type StagedAttachment, type StickerHandlers } from './lib/staging';1414
import RoomThreads from './lib/RoomThreads.svelte';15
- import { loadPinnedThreads, openThread, showThread, threadsByRoom } from './lib/threads';15
+ import { loadOlderThreads, loadPinnedThreads, openThread, showThread, syncThreadLists, threadsByRoom } from './lib/threads';1616
import { threadMatches } from './lib/thread-index';1717
import ContextMenu, { type MenuItem } from './lib/ContextMenu.svelte';1818
import PackManager from './lib/PackManager.svelte';@@ -38,7 +38,7 @@
3838
getSession,3939
getSpaceChildren,4040
fillHistoryGap,41
- startRecentHistory,41
+ retryRecentHistory as requestRecentHistory,4242
joinRoom,4343
leaveRoom,4444
logout,@@ -58,6 +58,7 @@
5858
import { isKnownCommand, parseCommand, splitTarget, unescapeCommand } from './lib/commands';5959
import type { ParsedCommand } from './lib/commands';6060
import {61
+ threadLists,6162
banners,6263
blocked,6364
connectEvents,@@ -1185,7 +1186,7 @@
11851186
if (recentRetrying[roomId]) return;11861187
recentRetrying = { ...recentRetrying, [roomId]: true };11871188
try {1188
- await startRecentHistory(roomId);1189
+ await requestRecentHistory(roomId);11891190
} catch (e) {11901191
pushBanner(e instanceof Error ? e.message : 'Loading recent history failed');11911192
} finally {@@ -1270,6 +1271,8 @@
12701271
if ($selectedRoomId === roomId) openThreadHere(rootEventId);12711272
}12721273
1274
+ $: syncThreadLists([...new Set([...($selectedRoomId ? [$selectedRoomId] : []), ...Object.keys(threadFolds).filter((id) => threadFolds[id])])]);1275
+12731276
function threadFoldOpen(roomId: string, folds: Record<string, boolean>, q: string): boolean {12741277
if (q) return ($threadsByRoom[roomId] ?? []).some((t) => threadMatches(t, q));12751278
return !!folds[roomId];@@ -1491,9 +1494,9 @@
14911494
return $rooms.find((r) => r.room_id === roomId)?.name ?? roomId;14921495
}14931496
1494
- function roomStickers(roomId: string): StickerHandlers {1497
+ function roomStickers(roomId: string, threadRoot: string | null = null): StickerHandlers {14951498
return {1496
- onSend: (pack, image) => void sendSticker(roomId, pack, image),1499
+ onSend: (pack, image) => void sendSticker(roomId, pack, image, threadRoot),14971500
onClone: (pack, image, x, y) => openCloneImageMenu(pack, image, x, y),14981501
onClonePack: (pack, x, y) => openClonePackMenu(pack, x, y),14991502
onManage: () => (dialog = 'packs'),@@ -1501,10 +1504,10 @@
15011504
};15021505
}15031506
1504
- async function sendSticker(roomId: string, pack: ImagePack, image: PackImage): Promise<void> {1505
- stickToBottom = true;1507
+ async function sendSticker(roomId: string, pack: ImagePack, image: PackImage, threadRoot: string | null = null): Promise<void> {1508
+ if (!threadRoot) stickToBottom = true;15061509
try {1507
- await runCommand({ op: 'send_sticker', room_id: roomId, pack_id: pack.id, shortcode: image.shortcode });1510
+ await runCommand({ op: 'send_sticker', room_id: roomId, thread_root: threadRoot, pack_id: pack.id, shortcode: image.shortcode });15081511
if (peeking === roomId) stopPeeking();15091512
} catch (e) {15101513
pushBanner(e instanceof Error ? e.message : 'Could not send the sticker');@@ -2119,6 +2122,8 @@
21192122
<RoomThreads21202123
roomId={room.room_id}21212124
threads={$threadsByRoom[room.room_id] ?? []}2125
+ history={$threadLists[room.room_id]?.history ?? null}2126
+ onOlder={() => void loadOlderThreads(room.room_id)}21222127
{query}21232128
activeRoot={threadView?.room_id === room.room_id ? threadView.root_event_id : null}21242129
onOpen={(root) => void openThreadIn(room.room_id, root)}@@ -2174,6 +2179,8 @@
21742179
<RoomThreads21752180
roomId={room.room_id}21762181
threads={$threadsByRoom[room.room_id] ?? []}2182
+ history={$threadLists[room.room_id]?.history ?? null}2183
+ onOlder={() => void loadOlderThreads(room.room_id)}21772184
{query}21782185
activeRoot={threadView?.room_id === room.room_id ? threadView.root_event_id : null}21792186
onOpen={(root) => void openThreadIn(room.room_id, root)}@@ -2491,6 +2498,7 @@
24912498
onMore={(ev, x, y) => void openMessageMenu(ev, x, y)}24922499
onMentionStart={() => !$membersByRoom[threadView.room_id] && void loadMembers(threadView.room_id)}24932500
focusEventId={threadView.focus_event_id ?? null}2501
+ stickers={roomStickers(threadView.room_id, threadView.root_event_id)}24942502
onClose={() => void showThread(null)}24952503
/>24962504
{/if}@@ -1,6 +1,6 @@
11
<script lang="ts">22
import { onDestroy, onMount, tick } from 'svelte';3
- import { eventContextAfter, eventContextBefore } from './api';3
+ import { closeEventContext, eventContext, eventContextAfter, eventContextBefore } from './api';44
import EmojiText from './EmojiText.svelte';55
import MessageRow from './MessageRow.svelte';66
import { formatDay } from './format';@@ -38,6 +38,8 @@
3838
let alive = true;3939
let identity = '';4040
let centered = false;41
+ let contextId: string | null = null;42
+ let contextFor = '';4143
4244
const stable = (event: TimelineEvent) => event.event_id || event.item_id;4345
const key = (item: TimelineItem) => item.kind === 'event'@@ -76,6 +78,7 @@
7678
if (identity !== nextIdentity) {7779
generation++;7880
identity = nextIdentity;81
+ closeSession();7982
rows = [];8083
beforeToken = null;8184
afterToken = null;@@ -90,7 +93,7 @@
9093
}9194
const request = generation;9295
viewport?.capture();93
- const initial = normalize([...next.events_before, ...(next.event ? [next.event] : []), ...next.events_after]);96
+ const initial = normalize([...next.events_before, next.event, ...next.events_after]);9497
rows = rows.length ? merge(rows, initial) : initial;9598
if (!beforeExtended) {9699
beforeToken = next.start_token;@@ -125,6 +128,29 @@
125128
});126129
}127130
131
+ function closeSession(): void {132
+ if (contextId) void closeEventContext(contextId).catch(() => undefined);133
+ contextId = null;134
+ }135
+136
+ /** The saved capture has no session, so paging opens one on demand and takes over its tokens. */137
+ async function openSession(): Promise<string | null> {138
+ if (contextId && contextFor === identity) return contextId;139
+ const forIdentity = identity;140
+ const opened = await eventContext(roomId, eventId, 20);141
+ if (!alive || forIdentity !== identity) {142
+ void closeEventContext(opened.context_id).catch(() => undefined);143
+ return null;144
+ }145
+ closeSession();146
+ contextId = opened.context_id;147
+ contextFor = forIdentity;148
+ rows = merge(rows, normalize([...opened.context.events_before, opened.context.event, ...opened.context.events_after]));149
+ if (!beforeExtended) beforeToken = opened.context.start_token;150
+ if (!afterExtended) afterToken = opened.context.end_token;151
+ return contextId;152
+ }153
+128154
async function load(direction: 'before' | 'after'): Promise<void> {129155
const token = direction === 'before' ? beforeToken : afterToken;130156
if (!token || !canPaginate || (direction === 'before' ? beforeBusy : afterBusy)) return;@@ -133,22 +159,28 @@
133159
else afterBusy = true;134160
error = '';135161
try {136
- const requestedRoom = roomId;137
- const requestedEvent = eventId;162
+ const session = await openSession();163
+ if (!session || !alive || request !== generation) return;164
+ const from = direction === 'before' ? beforeToken : afterToken;165
+ if (!from) {166
+ if (direction === 'before') beforeEnded = true;167
+ else afterEnded = true;168
+ return;169
+ }138170
const page = direction === 'before'139
- ? await eventContextBefore(requestedRoom, requestedEvent, token, 20)140
- : await eventContextAfter(requestedRoom, requestedEvent, token, 20);141
- if (!alive || request !== generation || requestedRoom !== roomId || requestedEvent !== eventId) return;171
+ ? await eventContextBefore(session, from, 20)172
+ : await eventContextAfter(session, from, 20);173
+ if (!alive || request !== generation) return;142174
viewport?.capture();143175
rows = merge(rows, page.events);144176
if (direction === 'before') {145177
beforeExtended = true;146178
beforeToken = page.next_token;147
- beforeEnded = page.end_reached || !page.next_token;179
+ beforeEnded = !page.next_token;148180
} else {149181
afterExtended = true;150182
afterToken = page.next_token;151
- afterEnded = page.end_reached || !page.next_token;183
+ afterEnded = !page.next_token;152184
}153185
await tick();154186
if (alive && request === generation) viewport?.restore();@@ -166,6 +198,7 @@
166198
if (!viewport?.consumeScroll()) viewport?.capture();167199
}168200
onDestroy(() => {201
+ closeSession();169202
alive = false;170203
generation++;171204
observer?.disconnect();@@ -51,6 +51,22 @@
5151
function roomName(entry: MentionEntry): string {5252
return entry.room_name || joinedRooms.get(entry.key.room_id)?.name || entry.key.room_id;5353
}54
+ const GAP_LABELS: Record<string, string> = {55
+ SyncDiscontinuity: 'Sync was interrupted',56
+ LimitedTimeline: 'Too many messages arrived at once',57
+ RoomLeft: 'You left the room',58
+ ContextUnavailable: 'Context could not be loaded',59
+ NotificationHistoryUnavailable: 'Notification history unavailable',60
+ CandidateExpired: 'A message could not be read',61
+ StoreReset: 'Inbox was reset',62
+ StoreFailed: 'Inbox could not be saved',63
+ HookTimedOut: 'Checking a message timed out'64
+ };65
+66
+ function gapLabel(reason: string): string {67
+ return GAP_LABELS[reason] ?? reason;68
+ }69
+5470
function reasonLabel(reason: string): string {5571
return reason === 'Direct' ? 'Direct mention' : reason === 'Reply' ? 'Reply' : reason === 'Keyword' ? 'Keyword' : 'Room mention';5672
}@@ -424,17 +440,16 @@
424440
{:else}425441
<header><h2>Mentions {#if page}<span>({unreadCount} unread)</span>{/if}</h2><button disabled={!page || unreadCount === 0 || actionBusy} onclick={markAllRead}>Mark all read</button></header>426442
<details class="coverage">427
- <summary>{page?.coverage.recovering ? 'Recovering mention history…' : 'History coverage'}{#if page?.coverage.incomplete_history} · Incomplete{/if}</summary>443
+ <summary>{page?.coverage.recovering ? 'Recovering mention history…' : 'History coverage'}{#if page?.coverage.gaps.length} · Incomplete{/if}</summary>428444
<div class="coverage-body">429445
{#if page}430446
{#if page.coverage.recovering}<p>History recovery is in progress.</p>{/if}431
- {#if page.coverage.incomplete_history}<p>Some mention history could not be checked.</p>{/if}432447
{#if page.coverage.truncated}<p>Results are truncated.</p>{/if}433448
{#if page.coverage.historical_supported === false}<p>Historical mention search is unsupported.</p>{/if}434449
{#if page.coverage.unresolved_candidates}<p>{page.coverage.unresolved_candidates} possible mentions could not be resolved.</p>{/if}435450
{#if page.coverage.gaps.length}<p>{page.coverage.gaps.length} history gaps remain.</p>{/if}436451
{#if page.coverage.earliest_timestamp_ms}<p>Oldest observed event from {new Date(page.coverage.earliest_timestamp_ms).toLocaleString()}.</p>{/if}437
- {#each page.coverage.gaps as gap}<p><EmojiText text={gap.room_id ? joinedRooms.get(gap.room_id)?.name ?? gap.room_id : 'Unknown room'} /> · <EmojiText text={gap.reason} /></p>{/each}452
+ {#each page.coverage.gaps as gap}<p><EmojiText text={gap.room_id ? joinedRooms.get(gap.room_id)?.name ?? gap.room_id : 'Unknown room'} /> · {gapLabel(gap.reason)}</p>{/each}438453
{:else}<p>Coverage is loading.</p>{/if}439454
<button disabled={actionBusy || page?.coverage.recovering} onclick={recover}>Recover history</button>440455
</div>@@ -22,6 +22,7 @@ function room(
2222
unread_messages: 0,2323
unread_mentions: 0,2424
unread_notifications: 0,25
+ thread_unreads: {},2526
encrypted: false,2627
space,2728
direct: false,@@ -5,13 +5,16 @@
55
import { formatClock, formatDay } from './format';66
import { threadMatches } from './thread-index';77
import type { ThreadEntry } from './thread-index';8
+ import type { ListHistoryState } from './types';89
import { togglePinnedThread } from './threads';910
1011
export let roomId: string;1112
export let threads: ThreadEntry[];1213
export let query = '';1314
export let activeRoot: string | null = null;15
+ export let history: ListHistoryState | null = null;1416
export let onOpen: (rootEventId: string) => void;17
+ export let onOlder: () => void;1518
1619
const COLLAPSED_LIMIT = 6;1720
let showAll = false;@@ -19,6 +22,7 @@
1922
$: matching = threads.filter((t) => threadMatches(t, query));2023
$: shown = query || showAll ? matching : matching.slice(0, COLLAPSED_LIMIT);2124
$: hidden = matching.length - shown.length;25
+ $: canLoadOlder = !query && hidden === 0 && history !== null && history !== 'StartReached';2226
2327
function label(t: ThreadEntry): string {2428
return t.preview || (t.pinned ? 'Pinned thread' : 'Thread');@@ -36,7 +40,8 @@
3640
on:click={() => onOpen(t.root_event_id)}3741
>3842
<span class="thread-preview"><EmojiText text={label(t)} /></span>39
- {#if t.reply_count > 0}<span class="thread-meta">{t.reply_count}</span>{/if}43
+ {#if t.unread > 0}<span class="thread-unread" class:mention={t.mentions > 0} aria-label={`${t.unread} unread`}>{t.unread > 99 ? '99+' : t.unread}</span>44
+ {:else if t.reply_count > 0}<span class="thread-meta">{t.reply_count}</span>{/if}4045
{#if t.latest_ms}<time class="thread-meta" datetime={new Date(t.latest_ms).toISOString()}>{formatDay(t.latest_ms)} {formatClock(t.latest_ms)}</time>{/if}4146
</button>4247
<PinButton pinned={t.pinned} label={`thread ${label(t)}`} onToggle={() => void togglePinnedThread(roomId, t.root_event_id)} />@@ -44,6 +49,8 @@
4449
{/each}4550
{#if hidden > 0}4651
<li><button type="button" class="thread-more" on:click={() => (showAll = true)}>{hidden} more</button></li>52
+ {:else if canLoadOlder}53
+ <li><button type="button" class="thread-more" disabled={history === 'Loading'} on:click={onOlder}>{history === 'Loading' ? 'Loading…' : 'Older threads'}</button></li>4754
{/if}4855
</ul>4956
@@ -57,5 +64,8 @@
5764
.thread-preview { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }5865
.thread-meta { flex: none; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }5966
.thread-item:hover :global(.pin-btn), .thread-item :global(.pin-btn.pinned) { opacity: 1; }67
+ .thread-unread { flex: none; min-width: 16px; padding: 0 4px; border-radius: var(--r-sm); background: var(--panel-2); color: var(--text); font-size: 10.5px; font-weight: 600; text-align: center; font-variant-numeric: tabular-nums; }68
+ .thread-unread.mention { background: var(--accent); color: var(--on-accent); }69
+ .thread-more:disabled { color: var(--muted); cursor: default; }6070
.thread-more { border: none; background: transparent; color: var(--accent); font: inherit; font-size: 12px; padding: 2px 6px; cursor: pointer; }6171
</style>@@ -74,16 +74,13 @@
7474
{:else if g.item.kind === 'start'}7575
<p class="start-note">Beginning of history</p>7676
{:else if g.item.kind === 'historyGap'}77
- {#if g.item.gap.continuity !== 'Verified'}78
- {@const gap = g.item.gap}79
- {@const filling = fillingGap === gap.id || gap.lifecycle === 'Fetching'}80
- <div class="history-gap">81
- <span>{gap.failure ?? 'Some history is not loaded'}</span>82
- <button type="button" class="ghost-btn" disabled={!!fillingGap || gap.lifecycle === 'Fetching' || gap.lifecycle === 'Unavailable'} on:click={() => onFillGap(gap.id)}>83
- {filling ? 'Loading…' : gap.lifecycle === 'Unavailable' ? 'History unavailable' : 'Fetch older history'}84
- </button>85
- </div>86
- {/if}77
+ {@const gap = g.item.gap}78
+ <div class="history-gap">79
+ <span>{gap.failure ?? 'Some history is not loaded'}</span>80
+ <button type="button" class="ghost-btn" disabled={!!fillingGap || gap.lifecycle === 'Unavailable'} on:click={() => onFillGap(gap.id)}>81
+ {fillingGap === gap.id ? 'Loading…' : gap.lifecycle === 'Unavailable' ? 'History unavailable' : 'Fetch older history'}82
+ </button>83
+ </div>8784
{:else if g.item.kind === 'unknown'}8885
<p class="unknown-note">Unsupported event ({g.item.label})</p>8986
{:else}@@ -1,11 +1,11 @@
11
<script lang="ts">2
- import { onMount } from 'svelte';2
+ import { onDestroy, onMount } from 'svelte';33
import EmojiText from './EmojiText.svelte';44
import MessageRow from './MessageRow.svelte';5
- import { searchMessages } from './api';5
+ import { closeSearch, searchMessages, searchMore } from './api';66
import { formatDay } from './format';77
import { groupByRoom } from './search-model';8
- import type { ScopeOption, SearchCoverage, SearchHit } from './search-model';8
+ import type { ScopeOption, SearchCoverage, SearchHit, SearchPage } from './search-model';99
1010
export let scopes: ScopeOption[];1111
export let initialScope = 0;@@ -17,18 +17,25 @@
1717
let query = '';1818
let scopeIndex = Math.min(initialScope, scopes.length - 1);1919
let hits: SearchHit[] = [];20
- let coverage: SearchCoverage[] = [];20
+ let coverage: SearchCoverage | null = null;2121
let next: string | null = null;22
+ let searchId: string | null = null;2223
let searched = '';2324
let busy = false;2425
let error = '';2526
let generation = 0;2627
2728
$: groups = groupByRoom(hits);28
- $: partial = coverage.filter((c) => !c.reached_start);29
+ $: partial = coverage?.rooms.filter((c) => !c.start_reached) ?? [];2930
$: oldest = partial.reduce<number | null>((min, c) => (c.oldest_indexed_ms === null ? min : min === null ? c.oldest_indexed_ms : Math.max(min, c.oldest_indexed_ms)), null);3031
3132
onMount(() => input?.focus());33
+ onDestroy(() => dropSearch());34
+35
+ function dropSearch(): void {36
+ if (searchId) void closeSearch(searchId).catch(() => undefined);37
+ searchId = null;38
+ }3239
3340
async function run(more = false): Promise<void> {3441
const text = query.trim();@@ -37,7 +44,19 @@
3744
busy = true;3845
error = '';3946
try {40
- const page = await searchMessages(text, scopes[scopeIndex].scope, more ? next : null);47
+ let page: SearchPage;48
+ if (more && searchId && next) {49
+ page = await searchMore(searchId, next);50
+ } else {51
+ const session = await searchMessages(text, scopes[scopeIndex].scope);52
+ if (run !== generation) {53
+ void closeSearch(session.search_id).catch(() => undefined);54
+ return;55
+ }56
+ dropSearch();57
+ searchId = session.search_id;58
+ page = session.page;59
+ }4160
if (run !== generation) return;4261
hits = more ? [...hits, ...page.hits] : page.hits;4362
coverage = page.coverage;@@ -2,7 +2,7 @@
22
import { sessionUser } from './store';33
import { releaseStaged as release } from './staging';44
import type { Mention } from './mentions';5
- import type { StagedAttachment } from './staging';5
+ import type { StagedAttachment, StickerHandlers } from './staging';66
77
interface ThreadDraft {88
text: string;@@ -49,6 +49,7 @@
4949
export let onUser: ((userId: string, x: number, y: number, menu: boolean) => void) | null = null;5050
export let onMore: ((event: TimelineEvent, x: number, y: number) => void) | null = null;5151
export let onMentionStart: () => void = () => {};52
+ export let stickers: StickerHandlers | null = null;5253
export let focusEventId: string | null = null;5354
export let onClose: () => void;5455
@@ -356,6 +357,7 @@
356357
{self}357358
{emotes}358359
{packs}360
+ {stickers}359361
onSubmit={() => void submit()}360362
onEditLast={editLastOwn}361363
{onMentionStart}@@ -1,6 +1,7 @@
11
// Backend-agnostic API surface. Every call forwards to whichever transport22
// this build was compiled for, hosted (fetch + SSE) or on-device (worker).3
-import type { EventContext, EventContextPage, MemberSummary, RoomSummary } from './types';3
+import { parseEventContextPage, parseEventContextSession } from './types';4
+import type { EventContextPage, EventContextSession, MemberSummary, RoomSummary } from './types';45
import type { Emote } from './emotes';56
import type { MentionKey, MentionsBoundary, MentionsPage, SavedMentionContext } from './mentions-model';67
export { mentionKey, parseMentionsPage, parseSavedMentionContext, parseMentionsUpdate } from './mentions-model';@@ -8,7 +9,7 @@ export type { MentionKey, MentionReason, MentionCaptureState, MentionEntry, Ment
89
import { formatBody } from './markdown';910
import { mentionedUserIds, type Mention } from './mentions';1011
import { HttpTransport } from './transport-http';11
-import { parseSearchPage, scopeWire, type SearchPage, type SearchScope } from './search-model';12
+import { parseSearchPage, parseSearchSession, scopeWire, type SearchPage, type SearchScope, type SearchSession } from './search-model';1213
import type { CommandRequest, SendKind, SessionInfo, Transport, VerifyAction } from './transport';1314
export type { HistoryRequest } from './transport';1415
import { createDeviceTransport } from 'malice:device-transport';@@ -56,32 +57,59 @@ export const uploadAvatar = (file: File) => transport.uploadAvatar(file);
5657
export const uploadRoomAvatar = (room_id: string, file: File) => transport.uploadRoomAvatar(room_id, file);5758
export const uploadMedia = (file: File) => transport.uploadMedia(file);5859
export const paginateRoom = (room_id: string, thread_root: string | null = null) => transport.paginateRoom(room_id, thread_root);59
-export const eventContext = (room_id: string, event_id: string, limit?: number): Promise<EventContext> =>60
- transport.eventContext(room_id, event_id, limit);61
-export const eventContextBefore = (room_id: string, event_id: string, from: string, limit?: number): Promise<EventContextPage> =>62
- transport.eventContextBefore(room_id, event_id, from, limit);63
-export const eventContextAfter = (room_id: string, event_id: string, from: string, limit?: number): Promise<EventContextPage> =>64
- transport.eventContextAfter(room_id, event_id, from, limit);60
+export async function eventContext(room_id: string, event_id: string, limit = 20): Promise<EventContextSession> {61
+ const parsed = parseEventContextSession(62
+ await transport.command({ op: 'event_context', room_id, event_id, before_limit: limit, after_limit: limit })63
+ );64
+ if (!parsed) throw new ApiError(502, 'Invalid event context response');65
+ return parsed;66
+}67
+async function eventContextPage(op: 'event_context_before' | 'event_context_after', context_id: string, token: string, limit: number): Promise<EventContextPage> {68
+ const parsed = parseEventContextPage(await transport.command({ op, context_id, token, limit }));69
+ if (!parsed) throw new ApiError(502, 'Invalid event context page response');70
+ return parsed;71
+}72
+export const eventContextBefore = (context_id: string, token: string, limit = 20): Promise<EventContextPage> =>73
+ eventContextPage('event_context_before', context_id, token, limit);74
+export const eventContextAfter = (context_id: string, token: string, limit = 20): Promise<EventContextPage> =>75
+ eventContextPage('event_context_after', context_id, token, limit);76
+export async function closeEventContext(context_id: string): Promise<void> {77
+ await transport.command({ op: 'close_event_context', context_id });78
+}6579
export const mentionsPage = (cursor: string | null = null, limit = 50): Promise<MentionsPage> => transport.mentionsPage(cursor, limit);6680
export const mentionContext = (room_id: string, event_id: string): Promise<SavedMentionContext> => transport.mentionContext(room_id, event_id);6781
export const retryMentionContext = (room_id: string, event_id: string): Promise<SavedMentionContext> => transport.retryMentionContext(room_id, event_id);6882
export const setMentionsRead = (keys: MentionKey[], read: boolean) => transport.setMentionsRead(keys, read);6983
export const markMentionsReadThrough = (boundary: MentionsBoundary) => transport.markMentionsReadThrough(boundary);7084
export const recoverMentions = () => transport.recoverMentions();71
-export async function searchMessages(query: string, scope: SearchScope, cursor: string | null = null, limit = 30): Promise<SearchPage> {72
- let raw: unknown;73
- try {74
- raw = await transport.command({ op: 'search', query, scope: scopeWire(scope), cursor, limit });75
- } catch (e) {76
- if (e instanceof ApiError && (e.status === 400 || e.status === 422)) throw new ApiError(400, "Search isn't available yet");77
- throw e;78
- }79
- const page = parseSearchPage(raw);85
+export async function searchMessages(query: string, scope: SearchScope, limit = 30): Promise<SearchSession> {86
+ const session = parseSearchSession(await transport.command({ op: 'search', query, scope: scopeWire(scope), limit }));87
+ if (!session) throw new ApiError(502, 'Invalid search response');88
+ return session;89
+}90
+export async function searchMore(search_id: string, cursor: string, limit = 30): Promise<SearchPage> {91
+ const page = parseSearchPage(await transport.command({ op: 'search_more', search_id, cursor, limit }));8092
if (!page) throw new ApiError(502, 'Invalid search response');8193
return page;8294
}83
-export const startRecentHistory = (room_id: string): Promise<void> => transport.startRecentHistory(room_id);84
-export const fillHistoryGap = (room_id: string, gap_id: string): Promise<void> => transport.fillHistoryGap(room_id, gap_id);95
+export async function closeSearch(search_id: string): Promise<void> {96
+ await transport.command({ op: 'close_search', search_id });97
+}98
+export async function retryRecentHistory(room_id: string): Promise<void> {99
+ await transport.command({ op: 'retry_recent_history', room_id });100
+}101
+export async function fillHistoryGap(room_id: string, gap_id: string): Promise<void> {102
+ await transport.command({ op: 'fill_history_gap', room_id, gap_id });103
+}104
+export async function openThreadList(room_id: string): Promise<void> {105
+ await transport.command({ op: 'open_thread_list', room_id });106
+}107
+export async function paginateThreadList(room_id: string, limit = 20): Promise<void> {108
+ await transport.command({ op: 'paginate_thread_list', room_id, limit });109
+}110
+export async function closeThreadList(room_id: string): Promise<void> {111
+ await transport.command({ op: 'close_thread_list', room_id });112
+}85113
export const setTyping = (room_id: string, typing: boolean) => transport.setTyping(room_id, typing);86114
export const getAccountData = (eventType: string) => transport.getAccountData(eventType);87115
export const getUrlPreview = (room_id: string, url: string) => transport.getUrlPreview(room_id, url);@@ -13,7 +13,7 @@ export interface MentionEntry {
1313
}1414
export interface MentionCoverage {1515
earliest_timestamp_ms: number | null; historical_supported: boolean | null; recovering: boolean;16
- incomplete_history: boolean; gaps: MentionGap[]; truncated: boolean; unresolved_candidates: number;16
+ gaps: MentionGap[]; truncated: boolean; unresolved_candidates: number;1717
}1818
export interface MentionGap { room_id: string | null; reason: string; token: string | null }1919
export interface MentionsBoundary { account: string; revision: number }@@ -52,8 +52,8 @@ function page(v: unknown): MentionsPage | null {
5252
if (!obj(v) || !obj(v.coverage) || !Array.isArray(v.entries) || !Array.isArray(v.coverage['gaps'])) return null;5353
const entries=v.entries.map(entry), cov=v.coverage as Obj;5454
const gaps=(cov['gaps'] as unknown[]).map(gap);55
- if (entries.some(x=>!x) || gaps.some(x=>!x) || !(v.next_cursor === null || str(v.next_cursor)) || !bool(v.end_reached) || !(cov['earliest_timestamp_ms'] === null || nat(cov['earliest_timestamp_ms'])) || !(cov['historical_supported'] === null || bool(cov['historical_supported'])) || !bool(cov['recovering']) || !bool(cov['incomplete_history']) || !bool(cov['truncated']) || !nat(cov['unresolved_candidates']) || !nat(v.unread_count) || !nat(v.revision) || !obj(v.boundary) || !str(v.boundary['account']) || !v.boundary['account'] || !nat(v.boundary['revision'])) return null;56
- return { entries:entries as MentionEntry[], next_cursor:v.next_cursor as string|null, end_reached:v.end_reached, coverage:{earliest_timestamp_ms:cov['earliest_timestamp_ms'] as number|null,historical_supported:cov['historical_supported'] as boolean|null,recovering:cov['recovering'],incomplete_history:cov['incomplete_history'],gaps:gaps as MentionGap[],truncated:cov['truncated'],unresolved_candidates:cov['unresolved_candidates']}, unread_count:v.unread_count, revision:v.revision, boundary:{account:v.boundary['account'],revision:v.boundary['revision']} };55
+ if (entries.some(x=>!x) || gaps.some(x=>!x) || !(v.next_cursor === null || str(v.next_cursor)) || !bool(v.end_reached) || !(cov['earliest_timestamp_ms'] === null || nat(cov['earliest_timestamp_ms'])) || !(cov['historical_supported'] === null || bool(cov['historical_supported'])) || !bool(cov['recovering']) || !bool(cov['truncated']) || !nat(cov['unresolved_candidates']) || !nat(v.unread_count) || !nat(v.revision) || !obj(v.boundary) || !str(v.boundary['account']) || !v.boundary['account'] || !nat(v.boundary['revision'])) return null;56
+ return { entries:entries as MentionEntry[], next_cursor:v.next_cursor as string|null, end_reached:v.end_reached, coverage:{earliest_timestamp_ms:cov['earliest_timestamp_ms'] as number|null,historical_supported:cov['historical_supported'] as boolean|null,recovering:cov['recovering'],gaps:gaps as MentionGap[],truncated:cov['truncated'],unresolved_candidates:cov['unresolved_candidates']}, unread_count:v.unread_count, revision:v.revision, boundary:{account:v.boundary['account'],revision:v.boundary['revision']} };5757
}5858
export function parseMentionsPage(raw: unknown): MentionsPage | null { try { return page(raw); } catch { return null; } }5959
export function parseSavedMentionContext(raw: unknown): SavedMentionContext | null {@@ -1,5 +1,5 @@
11
import { isRecord } from './guards';2
-import { parseTimelineItem } from './types';2
+import { parseTimelineEvent } from './types';33
import type { TimelineEvent } from './types';44
55
export type SearchScope = { kind: 'room'; room_id: string } | { kind: 'rooms'; room_ids: string[] } | { kind: 'all' };@@ -17,16 +17,30 @@ export interface SearchHit {
1717
ranges: [number, number][];1818
}1919
20
-export interface SearchCoverage {20
+export type SearchBackfill = 'Waiting' | 'Running' | 'Paused' | 'Complete' | 'Disabled' | { Failed: string };21
+22
+export interface RoomSearchCoverage {2123
room_id: string;2224
oldest_indexed_ms: number | null;23
- reached_start: boolean;25
+ start_reached: boolean;26
+ backfill: SearchBackfill;27
+}28
+29
+export interface SearchCoverage {30
+ rooms: RoomSearchCoverage[];31
+ persistent: boolean;32
+ undecrypted: number;2433
}2534
2635
export interface SearchPage {2736
hits: SearchHit[];2837
next: string | null;29
- coverage: SearchCoverage[];38
+ coverage: SearchCoverage;39
+}40
+41
+export interface SearchSession {42
+ search_id: string;43
+ page: SearchPage;3044
}3145
3246
/** The wire form of a scope, as macltok's externally tagged enum. */@@ -36,32 +50,59 @@ export function scopeWire(scope: SearchScope): unknown {
3650
return 'All';3751
}3852
39
-function parseRanges(raw: unknown): [number, number][] {53
+/** macltok's matches are utf-8 byte ranges, so they're mapped back to string offsets against the body. */54
+function parseRanges(raw: unknown, body: string): [number, number][] {4055
if (!Array.isArray(raw)) return [];56
+ const bytes = new TextEncoder().encode(body);57
+ const decoder = new TextDecoder();58
+ const offset = (byte: number) => decoder.decode(bytes.subarray(0, byte)).length;4159
return raw.flatMap((range) =>42
- Array.isArray(range) && typeof range[0] === 'number' && typeof range[1] === 'number' ? [[range[0], range[1]] as [number, number]] : []60
+ isRecord(range) && typeof range['start'] === 'number' && typeof range['end'] === 'number'61
+ ? [[offset(range['start']), offset(range['end'])] as [number, number]]62
+ : []4363
);4464
}4565
66
+function hitBody(event: TimelineEvent): string {67
+ const content = event.content;68
+ if (content.kind === 'Message') return content.message.body;69
+ return '';70
+}71
+4672
function parseHit(raw: unknown): SearchHit | null {47
- if (!isRecord(raw) || typeof raw['room_id'] !== 'string' || typeof raw['event_id'] !== 'string') return null;48
- const item = parseTimelineItem(raw['preview'] ?? raw['event']);49
- if (item?.kind !== 'event') return null;73
+ if (!isRecord(raw) || typeof raw['room_id'] !== 'string' || typeof raw['event_id'] !== 'string' || !isRecord(raw['event'])) return null;74
+ const event = parseTimelineEvent(raw['event']);5075
return {5176
room_id: raw['room_id'],5277
event_id: raw['event_id'],53
- timestamp_ms: typeof raw['timestamp_ms'] === 'number' ? raw['timestamp_ms'] : item.event.timestamp_ms,54
- event: item.event,55
- ranges: parseRanges(raw['ranges'])78
+ timestamp_ms: typeof raw['timestamp_ms'] === 'number' ? raw['timestamp_ms'] : event.timestamp_ms,79
+ event,80
+ ranges: parseRanges(raw['matches'], hitBody(event))5681
};5782
}5883
59
-function parseCoverage(raw: unknown): SearchCoverage | null {84
+function parseBackfill(raw: unknown): SearchBackfill {85
+ if (raw === 'Waiting' || raw === 'Running' || raw === 'Paused' || raw === 'Complete' || raw === 'Disabled') return raw;86
+ if (isRecord(raw) && isRecord(raw['Failed']) && typeof raw['Failed']['message'] === 'string') return { Failed: raw['Failed']['message'] };87
+ return 'Waiting';88
+}89
+90
+function parseRoomCoverage(raw: unknown): RoomSearchCoverage | null {6091
if (!isRecord(raw) || typeof raw['room_id'] !== 'string') return null;6192
return {6293
room_id: raw['room_id'],6394
oldest_indexed_ms: typeof raw['oldest_indexed_ms'] === 'number' ? raw['oldest_indexed_ms'] : null,64
- reached_start: raw['reached_start'] === true95
+ start_reached: raw['start_reached'] === true,96
+ backfill: parseBackfill(raw['backfill'])97
+ };98
+}99
+100
+function parseCoverage(raw: unknown): SearchCoverage {101
+ if (!isRecord(raw)) return { rooms: [], persistent: false, undecrypted: 0 };102
+ return {103
+ rooms: Array.isArray(raw['rooms']) ? raw['rooms'].flatMap((room) => parseRoomCoverage(room) ?? []) : [],104
+ persistent: raw['persistent'] === true,105
+ undecrypted: typeof raw['undecrypted'] === 'number' ? raw['undecrypted'] : 065106
};66107
}67108
@@ -69,11 +110,17 @@ export function parseSearchPage(raw: unknown): SearchPage | null {
69110
if (!isRecord(raw) || !Array.isArray(raw['hits'])) return null;70111
return {71112
hits: raw['hits'].flatMap((hit) => parseHit(hit) ?? []),72
- next: typeof raw['next'] === 'string' ? raw['next'] : null,73
- coverage: Array.isArray(raw['coverage']) ? raw['coverage'].flatMap((c) => parseCoverage(c) ?? []) : []113
+ next: typeof raw['next_cursor'] === 'string' ? raw['next_cursor'] : null,114
+ coverage: parseCoverage(raw['coverage'])74115
};75116
}76117
118
+export function parseSearchSession(raw: unknown): SearchSession | null {119
+ if (!isRecord(raw) || typeof raw['search_id'] !== 'string') return null;120
+ const page = parseSearchPage(raw['page']);121
+ return page ? { search_id: raw['search_id'], page } : null;122
+}123
+77124
/** Hits grouped by room, keeping the order in which each room first appears. */78125
export function groupByRoom(hits: SearchHit[]): { room_id: string; hits: SearchHit[] }[] {79126
const groups = new Map<string, SearchHit[]>();@@ -13,7 +13,9 @@ import {
1313
EMPTY_IMAGE_PACKS,1414
parseEncryptionState,1515
parseImagePacks,16
+ parseListHistoryState,1617
parseRecentHistoryMap,18
+ parseThreadListEntries,1719
parseRecentHistoryState,1820
parseRoomList,1921
parseTimelineList,@@ -23,9 +25,11 @@ import type {
2325
EncryptionState,2426
EventContext,2527
ImagePacks,28
+ ListHistoryState,2629
MemberSummary,2730
RecentHistoryState,2831
RoomSummary,32
+ ThreadListEntry,2933
TimelineItem,3034
VerificationState3135
} from './types';@@ -49,6 +53,21 @@ export const pinnedTimelines = writable<Record<string, TimelineItem[]>>({});
4953
export const pinnedTimelineVersions = writable<Record<string, number>>({});5054
export const threadTimelines = writable<Record<string, TimelineItem[]>>({});5155
56
+export interface ThreadListView {57
+ entries: ThreadListEntry[];58
+ history: ListHistoryState;59
+}60
+export const threadLists = writable<Record<string, ThreadListView>>({});61
+62
+function parseThreadListMap(raw: unknown): Record<string, ThreadListView> {63
+ const out: Record<string, ThreadListView> = {};64
+ if (!isRecord(raw)) return out;65
+ for (const [roomId, list] of Object.entries(raw)) {66
+ if (isRecord(list)) out[roomId] = { entries: parseThreadListEntries(list['entries']), history: parseListHistoryState(list['history']) };67
+ }68
+ return out;69
+}70
+5271
export function threadKey(roomId: string, rootEventId: string): string {5372
return `${roomId}\u0000${rootEventId}`;5473
}@@ -91,6 +110,7 @@ export function resetClientState(): void {
91110
pinnedTimelines.set({});92111
pinnedTimelineVersions.set({});93112
threadTimelines.set({});113
+ threadLists.set({});94114
recentHistory.set({});95115
eventContexts.set({});96116
connection.set('connecting');@@ -189,6 +209,7 @@ function handlePayload(text: string): void {
189209
}190210
threadTimelines.set(next);191211
}212
+ threadLists.set(parseThreadListMap(data['thread_lists']));192213
const rawPinned = data['pinned'];193214
if (isRecord(rawPinned)) {194215
const next: Record<string, TimelineItem[]> = {};@@ -238,6 +259,19 @@ function handlePayload(text: string): void {
238259
}239260
return;240261
}262
+ if (kind === 'thread_list' || kind === 'thread_list_history' || kind === 'thread_list_closed') {263
+ const roomId = data['room_id'];264
+ if (typeof roomId !== 'string') return;265
+ threadLists.update((all) => {266
+ const { [roomId]: current, ...rest } = all;267
+ if (kind === 'thread_list_closed') return rest;268
+ const base = current ?? { entries: [], history: 'Idle' as const };269
+ return kind === 'thread_list'270
+ ? { ...rest, [roomId]: { ...base, entries: parseThreadListEntries(data['entries']) } }271
+ : { ...rest, [roomId]: { ...base, history: parseListHistoryState(data['state']) } };272
+ });273
+ return;274
+ }241275
if (kind === 'thread' || kind === 'thread_closed') {242276
const roomId = data['room_id'];243277
const root = data['root_event_id'];@@ -1,4 +1,4 @@
1
-import type { TimelineEvent, TimelineItem } from './types';1
+import type { ThreadListEntry, ThreadUnread, TimelineEvent, TimelineItem } from './types';22
33
export interface ThreadEntry {44
room_id: string;@@ -8,6 +8,8 @@ export interface ThreadEntry {
88
reply_count: number;99
latest_ms: number;1010
pinned: boolean;11
+ unread: number;12
+ mentions: number;1113
}1214
1315
function previewOf(event: TimelineEvent): string {@@ -27,12 +29,23 @@ function entryFor(roomId: string, event: TimelineEvent, pinned: boolean): Thread
2729
sender: event.sender.display_name ?? event.sender.user_id,2830
reply_count: event.thread_summary?.reply_count ?? 0,2931
latest_ms: event.thread_summary?.latest_event_timestamp_ms ?? event.timestamp_ms,30
- pinned32
+ pinned,33
+ unread: 0,34
+ mentions: 03135
};3236
}3337
34
-/** Pinned threads first, then by latest activity. Pins whose root isn't loaded still get a row. */35
-export function roomThreads(roomId: string, items: TimelineItem[], pinnedRoots: string[]): ThreadEntry[] {38
+/**39
+ * Pinned threads first, then by latest activity. The server's thread list wins over roots seen in the40
+ * loaded timeline, and pins or unread roots that neither has loaded still get a row.41
+ */42
+export function roomThreads(43
+ roomId: string,44
+ items: TimelineItem[],45
+ pinnedRoots: string[],46
+ listed: ThreadListEntry[] = [],47
+ unreads: Record<string, ThreadUnread> = {}48
+): ThreadEntry[] {3649
const pinned = new Set(pinnedRoots);3750
const found = new Map<string, ThreadEntry>();3851
for (const item of items) {@@ -42,11 +55,25 @@ export function roomThreads(roomId: string, items: TimelineItem[], pinnedRoots:
4255
const entry = entryFor(roomId, item.event, pinned.has(id));4356
if (entry) found.set(id, entry);4457
}45
- for (const root of pinnedRoots) {58
+ for (const listedEntry of listed) {59
+ const entry = entryFor(roomId, listedEntry.root, pinned.has(listedEntry.root_event_id));60
+ if (!entry) continue;61
+ const loaded = found.get(listedEntry.root_event_id);62
+ found.set(listedEntry.root_event_id, {63
+ ...entry,64
+ reply_count: loaded?.reply_count || listedEntry.reply_count,65
+ latest_ms: Math.max(loaded?.latest_ms ?? 0, listedEntry.latest_activity_ms)66
+ });67
+ }68
+ for (const root of new Set([...pinnedRoots, ...Object.keys(unreads)])) {4669
if (!found.has(root)) {47
- found.set(root, { room_id: roomId, root_event_id: root, preview: '', sender: '', reply_count: 0, latest_ms: 0, pinned: true });70
+ found.set(root, { room_id: roomId, root_event_id: root, preview: '', sender: '', reply_count: 0, latest_ms: 0, pinned: pinned.has(root), unread: 0, mentions: 0 });4871
}4972
}73
+ for (const [root, unread] of Object.entries(unreads)) {74
+ const entry = found.get(root);75
+ if (entry) found.set(root, { ...entry, unread: unread.messages, mentions: unread.mentions });76
+ }5077
return [...found.values()].sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.latest_ms - a.latest_ms);5178
}5279
@@ -1,6 +1,6 @@
11
import { derived, get, writable } from 'svelte/store';2
-import { getAccountData, runCommand, setAccountData } from './api';3
-import { pushBanner, sessionUser, timelines } from './store';2
+import { closeThreadList, getAccountData, openThreadList, paginateThreadList, runCommand, setAccountData } from './api';3
+import { pushBanner, rooms, sessionUser, threadLists, timelines } from './store';44
import { roomThreads } from './thread-index';55
import type { ThreadEntry } from './thread-index';66
@@ -15,15 +15,45 @@ export interface OpenThread {
1515
export const openThread = writable<OpenThread | null>(null);1616
export const pinnedThreads = writable<Record<string, string[]>>({});1717
18
-export const threadsByRoom = derived([timelines, pinnedThreads], ([$timelines, $pinned]) => {18
+export const threadsByRoom = derived([timelines, pinnedThreads, threadLists, rooms], ([$timelines, $pinned, $lists, $rooms]) => {19
+ const unreads = new Map($rooms.map((room) => [room.room_id, room.thread_unreads]));1920
const out: Record<string, ThreadEntry[]> = {};20
- for (const roomId of new Set([...Object.keys($timelines), ...Object.keys($pinned)])) {21
- const list = roomThreads(roomId, $timelines[roomId] ?? [], $pinned[roomId] ?? []);21
+ const ids = [...Object.keys($timelines), ...Object.keys($pinned), ...Object.keys($lists), ...unreads.keys()];22
+ for (const roomId of new Set(ids)) {23
+ const list = roomThreads(roomId, $timelines[roomId] ?? [], $pinned[roomId] ?? [], $lists[roomId]?.entries ?? [], unreads.get(roomId) ?? {});2224
if (list.length) out[roomId] = list;2325
}2426
return out;2527
});2628
29
+const THREAD_PAGE = 20;30
+const openLists = new Set<string>();31
+32
+/** Keeps a server thread list open for exactly the rooms in `wanted`, loading a first page for each new one. */33
+export function syncThreadLists(wanted: string[]): void {34
+ const want = new Set(wanted);35
+ for (const roomId of [...openLists]) {36
+ if (want.has(roomId)) continue;37
+ openLists.delete(roomId);38
+ void closeThreadList(roomId).catch(() => undefined);39
+ }40
+ for (const roomId of want) {41
+ if (openLists.has(roomId)) continue;42
+ openLists.add(roomId);43
+ void openThreadList(roomId)44
+ .then(() => paginateThreadList(roomId, THREAD_PAGE))45
+ .catch(() => openLists.delete(roomId));46
+ }47
+}48
+49
+export async function loadOlderThreads(roomId: string): Promise<void> {50
+ try {51
+ await paginateThreadList(roomId, THREAD_PAGE);52
+ } catch (e) {53
+ pushBanner(e instanceof Error ? e.message : 'Could not load older threads');54
+ }55
+}56
+2757
function parsePinned(raw: unknown): Record<string, string[]> {2858
if (typeof raw !== 'object' || raw === null) return {};2959
const rooms = (raw as Record<string, unknown>)['rooms'];@@ -83,6 +113,7 @@ let account: string | null | undefined;
83113
sessionUser.subscribe((user) => {84114
if (user === account) return;85115
account = user;116
+ openLists.clear();86117
opened = null;87118
openThread.set(null);88119
pinnedThreads.set({});@@ -1,5 +1,4 @@
1
-import type { EventContext, EventContextPage, MemberSummary, RoomSummary } from './types';2
-import { parseEventContext, parseEventContextPage } from './types';1
+import type { MemberSummary, RoomSummary } from './types';32
import { parseMentionsPage, parseSavedMentionContext } from './mentions-model';43
import type { MentionKey, MentionsBoundary, MentionsPage, SavedMentionContext } from './mentions-model';54
import { ApiError, type CommandRequest, type SendKind, type SessionInfo, type Transport, type UploadExtras, type VerifyAction } from './transport';@@ -107,17 +106,6 @@ export class HttpTransport implements Transport {
107106
paginateRoom(room_id: string, thread_root: string | null = null): Promise<void> {108107
return this.roomCommand(ROOM_COMMAND_PATHS.paginate, { room_id, thread_root });109108
}110
- async eventContext(room_id: string, event_id: string, limit = 20): Promise<EventContext> {111
- const res = await fetch('/api/rooms/context', {112
- method: 'POST',113
- credentials: 'same-origin',114
- headers: { 'content-type': 'application/json' },115
- body: JSON.stringify({ room_id, event_id, limit })116
- });117
- const parsed = parseEventContext(await json<unknown>(res, 'Could not load event context'));118
- if (!parsed) throw new ApiError(502, 'Invalid event context response');119
- return parsed;120
- }121109
async mentionsPage(cursor: string | null = null, limit = 50): Promise<MentionsPage> {122110
const parsed = parseMentionsPage(await this.command({ op: 'mentions_page', cursor, limit }));123111
if (!parsed) throw new ApiError(502, 'Invalid mentions page response');@@ -141,29 +129,6 @@ export class HttpTransport implements Transport {
141129
await this.command({ op: 'mark_mentions_read_through', boundary });142130
}143131
async recoverMentions(): Promise<void> { await this.command({ op: 'recover_mentions' }); }144
- async startRecentHistory(room_id: string): Promise<void> {145
- await this.command({ op: 'start_recent_history', room_id });146
- }147
- async fillHistoryGap(room_id: string, gap_id: string): Promise<void> {148
- await this.command({ op: 'fill_history_gap', room_id, gap_id });149
- }150
- async eventContextBefore(room_id: string, event_id: string, from: string, limit = 20): Promise<EventContextPage> {151
- return this.eventContextPage('before', room_id, event_id, from, limit);152
- }153
- async eventContextAfter(room_id: string, event_id: string, from: string, limit = 20): Promise<EventContextPage> {154
- return this.eventContextPage('after', room_id, event_id, from, limit);155
- }156
- private async eventContextPage(direction: 'before' | 'after', room_id: string, event_id: string, from: string, limit: number): Promise<EventContextPage> {157
- const res = await fetch(`/api/rooms/context/${direction}`, {158
- method: 'POST',159
- credentials: 'same-origin',160
- headers: { 'content-type': 'application/json' },161
- body: JSON.stringify({ room_id, event_id, from, limit })162
- });163
- const parsed = parseEventContextPage(await json<unknown>(res, 'Could not load event context'));164
- if (!parsed) throw new ApiError(502, 'Invalid event context page response');165
- return parsed;166
- }167132
setTyping(room_id: string, typing: boolean): Promise<void> {168133
return this.roomCommand(ROOM_COMMAND_PATHS.typing, { room_id, typing });169134
}@@ -1,7 +1,6 @@
1
-import { parseEventContext, parseEventContextPage } from './types';21
import { parseMentionsPage, parseSavedMentionContext } from './mentions-model';32
import type { MentionKey, MentionsBoundary, MentionsPage, SavedMentionContext } from './mentions-model';4
-import type { EventContext, EventContextPage, MemberSummary, RoomSummary } from './types';3
+import type { MemberSummary, RoomSummary } from './types';54
import { ApiError, type CommandRequest, type SendKind, type SessionInfo, type SoftLogout, type Transport, type UploadExtras, type VerifyAction } from './transport';65
import { statusForKind } from './wire-status';76
@@ -248,31 +247,6 @@ export class WorkerTransport implements Transport {
248247
async setMentionsRead(keys: MentionKey[], read: boolean): Promise<void> { await this.client.call('set_mentions_read', { keys, read }); }249248
async markMentionsReadThrough(boundary: MentionsBoundary): Promise<void> { await this.client.call('mark_mentions_read_through', { boundary }); }250249
async recoverMentions(): Promise<void> { await this.client.call('recover_mentions'); }251
- async startRecentHistory(room_id: string): Promise<void> {252
- await this.client.call('start_recent_history', { room_id });253
- }254
- async fillHistoryGap(room_id: string, gap_id: string): Promise<void> {255
- await this.client.call('fill_history_gap', { room_id, gap_id });256
- }257
- eventContext(room_id: string, event_id: string, limit = 20): Promise<EventContext> {258
- return this.contextCall('event_context', { room_id, event_id, limit });259
- }260
- eventContextBefore(room_id: string, event_id: string, from: string, limit = 20): Promise<EventContextPage> {261
- return this.contextPageCall('event_context_before', { room_id, event_id, from, limit });262
- }263
- eventContextAfter(room_id: string, event_id: string, from: string, limit = 20): Promise<EventContextPage> {264
- return this.contextPageCall('event_context_after', { room_id, event_id, from, limit });265
- }266
- private async contextCall(op: string, fields: Record<string, unknown>): Promise<EventContext> {267
- const parsed = parseEventContext(await this.client.call(op, fields));268
- if (!parsed) throw new ApiError(502, 'Invalid event context response');269
- return parsed;270
- }271
- private async contextPageCall(op: string, fields: Record<string, unknown>): Promise<EventContextPage> {272
- const parsed = parseEventContextPage(await this.client.call(op, fields));273
- if (!parsed) throw new ApiError(502, 'Invalid event context page response');274
- return parsed;275
- }276250
setTyping(room_id: string, typing: boolean): Promise<void> {277251
return this.client.call('typing', { room_id, typing });278252
}@@ -1,4 +1,4 @@
1
-import type { EventContext, EventContextPage, MemberSummary, PackUsage, RoomSummary } from './types';1
+import type { MemberSummary, PackUsage, RoomSummary } from './types';22
import type { MentionKey, MentionsBoundary, MentionsPage, SavedMentionContext } from './mentions-model';33
44
export class ApiError extends Error {@@ -37,13 +37,17 @@ export type CommandRequest =
3737
| { op: 'set_room_avatar'; room_id: string; mxc: string | null }3838
| { op: 'mute'; room_id: string; user_id: string; muted: boolean }3939
| { op: 'notification_keywords' }40
- | { op: 'search'; query: string; scope: unknown; cursor: string | null; limit: number }40
+ | { op: 'search'; query: string; scope: unknown; limit: number }41
+ | { op: 'search_more'; search_id: string; cursor: string; limit: number }42
+ | { op: 'close_search'; search_id: string }43
+ | { op: 'open_thread_list' | 'close_thread_list'; room_id: string }44
+ | { op: 'paginate_thread_list'; room_id: string; limit: number }4145
| { op: 'add_notification_keyword' | 'remove_notification_keyword'; keyword: string }4246
| { op: 'ignore' | 'unignore'; user_id: string }4347
| { op: 'power_levels'; room_id: string }4448
| { op: 'open_pinned' | 'close_pinned'; room_id: string }4549
| { op: 'set_pinned'; room_id: string; event_id: string; pinned: boolean }46
- | { op: 'send_sticker'; room_id: string; pack_id: string; shortcode: string }50
+ | { op: 'send_sticker'; room_id: string; thread_root?: string | null; pack_id: string; shortcode: string }4751
| { op: 'create_room_pack'; room_id: string; state_key: string; display_name: string }4852
| { op: 'delete_pack'; target: PackTarget }4953
| {@@ -72,8 +76,11 @@ export type CommandRequest =
7276
| MentionsRequest;7377
7478
export type HistoryRequest =75
- | { op: 'start_recent_history'; room_id: string }76
- | { op: 'fill_history_gap'; room_id: string; gap_id: string };79
+ | { op: 'retry_recent_history'; room_id: string }80
+ | { op: 'fill_history_gap'; room_id: string; gap_id: string }81
+ | { op: 'event_context'; room_id: string; event_id: string; before_limit: number; after_limit: number }82
+ | { op: 'event_context_before' | 'event_context_after'; context_id: string; token: string; limit: number }83
+ | { op: 'close_event_context'; context_id: string };7784
7885
export type MentionsRequest =7986
| { op: 'mentions_page'; cursor: string | null; limit: number }@@ -118,17 +125,12 @@ export interface Transport {
118125
thread_root?: string | null119126
): Promise<void>;120127
paginateRoom(room_id: string, thread_root?: string | null): Promise<void>;121
- eventContext(room_id: string, event_id: string, limit?: number): Promise<EventContext>;122
- eventContextBefore(room_id: string, event_id: string, from: string, limit?: number): Promise<EventContextPage>;123
- eventContextAfter(room_id: string, event_id: string, from: string, limit?: number): Promise<EventContextPage>;124128
mentionsPage(cursor?: string | null, limit?: number): Promise<MentionsPage>;125129
mentionContext(room_id: string, event_id: string): Promise<SavedMentionContext>;126130
retryMentionContext(room_id: string, event_id: string): Promise<SavedMentionContext>;127131
setMentionsRead(keys: MentionKey[], read: boolean): Promise<void>;128132
markMentionsReadThrough(boundary: MentionsBoundary): Promise<void>;129133
recoverMentions(): Promise<void>;130
- startRecentHistory(room_id: string): Promise<void>;131
- fillHistoryGap(room_id: string, gap_id: string): Promise<void>;132134
setTyping(room_id: string, typing: boolean): Promise<void>;133135
getAccountData(eventType: string): Promise<unknown | null>;134136
getUrlPreview(room_id: string, url: string): Promise<unknown | null>;@@ -21,6 +21,7 @@ export interface RoomSummary {
2121
unread_messages: number;2222
unread_mentions: number;2323
unread_notifications: number;24
+ thread_unreads: Record<string, ThreadUnread>;2425
encrypted: boolean;2526
space: boolean;2627
direct: boolean;@@ -348,6 +349,7 @@ export function parseRoomSummary(raw: unknown): RoomSummary | null {
348349
unread_messages: numberField(raw, 'unread_messages'),349350
unread_mentions: numberField(raw, 'unread_mentions'),350351
unread_notifications: numberField(raw, 'unread_notifications'),352
+ thread_unreads: parseThreadUnreads(raw['thread_unreads']),351353
encrypted: raw['encrypted'] === true,352354
space: raw['space'] === true,353355
direct: raw['direct'] === true,@@ -630,7 +632,7 @@ function parseReactions(raw: unknown): Reaction[] {
630632
return out;631633
}632634
633
-function parseTimelineEvent(raw: Record<string, unknown>): TimelineEvent {635
+export function parseTimelineEvent(raw: Record<string, unknown>): TimelineEvent {634636
return {635637
item_id: stringField(raw, 'item_id'),636638
event_id: optionalString(raw, 'event_id'),@@ -658,10 +660,10 @@ export type TimelineItem =
658660
| { kind: 'start' }659661
| { kind: 'unknown'; label: string };660662
661
-export type RecentHistoryState = 'Idle' | 'Loading' | 'Loaded' | { Failed: string };663
+export type RecentHistoryState = 'Idle' | 'Loading' | 'Loaded' | 'Cancelled' | { Failed: string };662664
663665
export function parseRecentHistoryState(raw: unknown): RecentHistoryState | null {664
- if (raw === 'Idle' || raw === 'Loading' || raw === 'Loaded') return raw;666
+ if (raw === 'Idle' || raw === 'Loading' || raw === 'Loaded' || raw === 'Cancelled') return raw;665667
if (isRecord(raw) && typeof raw['Failed'] === 'string') return { Failed: raw['Failed'] };666668
return null;667669
}@@ -680,26 +682,26 @@ export interface HistoryGap {
680682
id: string;681683
older_boundary: string | null;682684
newer_boundary: string | null;683
- lifecycle: 'Idle' | 'Fetching' | 'Paused' | 'Failed' | 'Unavailable';684
- continuity: 'Unresolved' | 'Verified' | 'AccessibleStart';685
+ lifecycle: 'Idle' | 'Paused' | 'Failed' | 'Unavailable';685686
failure: string | null;686687
}687688
688689
export interface EventContext {689690
events_before: TimelineItem[];690
- event: TimelineItem | null;691
+ event: TimelineItem;691692
events_after: TimelineItem[];692693
start_token: string | null;693694
end_token: string | null;694695
}695696
696
-export type EventContextDirection = 'Before' | 'After';697
+export interface EventContextSession {698
+ context_id: string;699
+ context: EventContext;700
+}697701
698702
export interface EventContextPage {699
- direction: EventContextDirection;700703
events: TimelineItem[];701704
next_token: string | null;702
- end_reached: boolean;703705
}704706
705707
/** Normalize one TimelineItem from external-tag serde form. */@@ -711,15 +713,13 @@ export function parseTimelineItem(raw: unknown): TimelineItem | null {
711713
if (isRecord(raw['HistoryGap'])) {712714
const gap = raw['HistoryGap'];713715
const lifecycle = gap['lifecycle'];714
- const continuity = gap['continuity'];715716
return {716717
kind: 'historyGap',717718
gap: {718719
id: stringField(gap, 'id'),719720
older_boundary: optionalString(gap, 'older_boundary'),720721
newer_boundary: optionalString(gap, 'newer_boundary'),721
- lifecycle: lifecycle === 'Fetching' || lifecycle === 'Paused' || lifecycle === 'Failed' || lifecycle === 'Unavailable' ? lifecycle : 'Idle',722
- continuity: continuity === 'Verified' || continuity === 'AccessibleStart' ? continuity : 'Unresolved',722
+ lifecycle: lifecycle === 'Paused' || lifecycle === 'Failed' || lifecycle === 'Unavailable' ? lifecycle : 'Idle',723723
failure: optionalString(gap, 'failure')724724
}725725
};@@ -746,28 +746,83 @@ export function parseTimelineList(raw: unknown): TimelineItem[] {
746746
747747
export function parseEventContext(raw: unknown): EventContext | null {748748
if (!isRecord(raw)) return null;749
- const eventRaw = raw['event'];749
+ const event = parseTimelineItem(raw['event']);750
+ if (!event) return null;750751
return {751752
events_before: parseTimelineList(raw['events_before']),752
- event: eventRaw == null ? null : parseTimelineItem(eventRaw),753
+ event,753754
events_after: parseTimelineList(raw['events_after']),754755
start_token: optionalString(raw, 'start_token'),755756
end_token: optionalString(raw, 'end_token')756757
};757758
}758759
760
+export function parseEventContextSession(raw: unknown): EventContextSession | null {761
+ if (!isRecord(raw) || typeof raw['context_id'] !== 'string') return null;762
+ const context = parseEventContext(raw['context']);763
+ return context ? { context_id: raw['context_id'], context } : null;764
+}765
+759766
export function parseEventContextPage(raw: unknown): EventContextPage | null {760767
if (!isRecord(raw)) return null;761
- const direction = raw['direction'];762
- if (direction !== 'Before' && direction !== 'After') return null;763768
return {764
- direction,765769
events: parseTimelineList(raw['events']),766
- next_token: optionalString(raw, 'next_token'),767
- end_reached: raw['end_reached'] === true770
+ next_token: optionalString(raw, 'next_token')771
+ };772
+}773
+774
+export interface ThreadUnread {775
+ messages: number;776
+ mentions: number;777
+ notifications: number;778
+}779
+780
+function parseThreadUnreads(raw: unknown): Record<string, ThreadUnread> {781
+ const out: Record<string, ThreadUnread> = {};782
+ if (!isRecord(raw)) return out;783
+ for (const [root, value] of Object.entries(raw)) {784
+ if (!isRecord(value)) continue;785
+ out[root] = { messages: numberField(value, 'messages'), mentions: numberField(value, 'mentions'), notifications: numberField(value, 'notifications') };786
+ }787
+ return out;788
+}789
+790
+export type ListHistoryState = 'Idle' | 'Loading' | 'StartReached' | { Failed: string };791
+792
+export function parseListHistoryState(raw: unknown): ListHistoryState {793
+ if (raw === 'Loading' || raw === 'StartReached') return raw;794
+ if (isRecord(raw) && typeof raw['Failed'] === 'string') return { Failed: raw['Failed'] };795
+ return 'Idle';796
+}797
+798
+export interface ThreadListEntry {799
+ root_event_id: string;800
+ root: TimelineEvent;801
+ reply_count: number;802
+ latest_reply: TimelineEvent | null;803
+ latest_activity_ms: number;804
+}805
+806
+export function parseThreadListEntry(raw: unknown): ThreadListEntry | null {807
+ if (!isRecord(raw) || typeof raw['root_event_id'] !== 'string') return null;808
+ const root = parseTimelineItem(raw['root']);809
+ if (!root || root.kind !== 'event') return null;810
+ const latest = raw['latest_reply'] == null ? null : parseTimelineItem(raw['latest_reply']);811
+ const summary = isRecord(raw['summary']) ? raw['summary'] : {};812
+ return {813
+ root_event_id: raw['root_event_id'],814
+ root: root.event,815
+ reply_count: numberField(summary, 'reply_count'),816
+ latest_reply: latest && latest.kind === 'event' ? latest.event : null,817
+ latest_activity_ms: numberField(raw, 'latest_activity_ms')768818
};769819
}770820
821
+export function parseThreadListEntries(raw: unknown): ThreadListEntry[] {822
+ if (!Array.isArray(raw)) return [];823
+ return raw.map(parseThreadListEntry).filter((entry): entry is ThreadListEntry => entry !== null);824
+}825
+771826
export type PackUsage = 'Emoticon' | 'Sticker';772827
export interface PackImage {773828
shortcode: string;