atagen

malice

public
Star0
Watch0

Commit 249e439c27e5

tarball

atagen <boss@atagen.co>2026-10-25 13:44 UTC

29 files changed+1661-326
macltok: adapt to e521a71

Event context, search and thread lists are now sessions in macltok, so the old room-keyed context routes could never page and are replaced by commands carrying context_id, and search and thread lists close what they open. The foldout prefers the timeline thread summary over the thread list count because the list currently double counts replies. Mention coverage dropped incomplete_history, which made every mentions page fail to parse.
Modified file: Cargo.lock+506 additions−12 deletions
@@ -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"
Modified file: Cargo.toml+1 additions−1 deletions
@@ -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"
Modified file: crates/malice-core/src/error.rs+6 additions−3 deletions
@@ -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::NotFound
57- }
55+ ErrorKind::RoomUnknown
56+ | ErrorKind::EventNotFound
57+ | ErrorKind::MediaUnknown
58+ | ErrorKind::MediaUnavailable
59+ | ErrorKind::EventContextNotOpen
60+ | ErrorKind::SearchNotOpen => Failure::NotFound,
5861 ErrorKind::MediaTooLarge => Failure::TooLarge,
5962 ErrorKind::Forbidden => Failure::Forbidden,
6063 ErrorKind::Invalid | ErrorKind::EmptyBody => Failure::BadRequest,
Modified file: crates/malice-core/src/feed.rs+51 additions−3 deletions
@@ -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: state
175+ .thread_lists
176+ .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(
Modified file: crates/malice-core/src/request.rs+123 additions−19 deletions
@@ -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 handle
615- .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 = handle
637- .event_context(&room_id, &event_id, before_limit, after_limit)
687+ let session = handle
688+ .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 = handle
703+ .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 = handle
713+ .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+ handle
727+ .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 = handle
741+ .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 = handle
751+ .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 = handle
661764 .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 handle
11181222 .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 = handle
1225- .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 = handle
1242- .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 = handle
1259- .clone_pack_with_usage(
1363+ .clone_pack(
12601364 pack_text(from_pack, "pack_id")?,
12611365 to.source()?,
12621366 display_name,
Modified file: crates/malice-server/Cargo.toml+1 additions−1 deletions
@@ -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"
Modified file: crates/malice-server/src/routes.rs+12 additions−96 deletions
@@ -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::RecoverMentions
587- | 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)),
Modified file: crates/malice-server/src/state.rs+3 additions
@@ -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 { .. }
Modified file: crates/malice-worker/Cargo.lock+526 additions−10 deletions
@@ -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"
Modified file: crates/malice-worker/Cargo.toml+2 additions−2 deletions
@@ -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"] }
Modified file: crates/malice-worker/src/account.rs+3 additions
@@ -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 { .. }
Modified file: web/src/App.svelte+16 additions−8 deletions
@@ -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 <RoomThreads
21202123 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 <RoomThreads
21752180 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}
Modified file: web/src/lib/ContextTimeline.svelte+42 additions−9 deletions
@@ -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();
Modified file: web/src/lib/MentionsInbox.svelte+18 additions−3 deletions
@@ -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>
Modified file: web/src/lib/PackManager.stories.ts+1 additions
@@ -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,
Modified file: web/src/lib/RoomThreads.svelte+11 additions−1 deletions
@@ -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>
Modified file: web/src/lib/RoomTimelineRows.svelte+7 additions−10 deletions
@@ -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}
Modified file: web/src/lib/SearchView.svelte+25 additions−6 deletions
@@ -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;
Modified file: web/src/lib/ThreadPane.svelte+3 additions−1 deletions
@@ -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}
Modified file: web/src/lib/api.ts+47 additions−19 deletions
@@ -1,6 +1,7 @@
11 // Backend-agnostic API surface. Every call forwards to whichever transport
22 // 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);
Modified file: web/src/lib/mentions-model.ts+3 additions−3 deletions
@@ -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 {
Modified file: web/src/lib/search-model.ts+63 additions−16 deletions
@@ -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'] === true
95+ 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'] : 0
65106 };
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[]>();
Modified file: web/src/lib/store.ts+34 additions
@@ -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 VerificationState
3135 } 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'];
Modified file: web/src/lib/thread-index.ts+33 additions−6 deletions
@@ -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- pinned
32+ pinned,
33+ unread: 0,
34+ mentions: 0
3135 };
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 the
40+ * 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
Modified file: web/src/lib/threads.ts+36 additions−5 deletions
@@ -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({});
Modified file: web/src/lib/transport-http.ts+1 additions−36 deletions
@@ -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 }
Modified file: web/src/lib/transport-worker.ts+1 additions−27 deletions
@@ -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 }
Modified file: web/src/lib/transport.ts+12 additions−10 deletions
@@ -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 | null
119126 ): 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>;
Modified file: web/src/lib/types.ts+74 additions−19 deletions
@@ -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'] === true
770+ 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;