add timeline paging for older messages

This commit is contained in:
2026-09-30 12:49:37 +07:00
parent de862637f3
commit d621b125a9
4 changed files with 164 additions and 6 deletions
+33 -1
View File
@@ -136,6 +136,12 @@ GroupsRegistry ──ensure relay──► Group entity ◄──ingest── ba
UI later: GroupPanel (Vec<Message>), JoinDialog (naddr)
```
A group's subscriptions are the standing `grp-<hash>` REQ (timeline + state +
membership, the timeline replayed up to 100 events) and, on demand, a
`pg-<hash>` REQ that re-asks the same timeline filter with `until` set to the
oldest event hydrated so far. Both are recognised by the same pump and land in
the same envelope cache, so paging needs no separate read path.
## Phase 0 — `crates/nip29/src/protocol.rs` (pure, no I/O)
The protocol is expressed as types with `impl` blocks: a parsed thing is a struct
@@ -437,7 +443,32 @@ group's relay only, and treat the relay's `OK` as the truth.
`metadata_tags` re-emits them — `9002` requires it, an edit that drops a
child is rejected. `parent`/`children` are per-relay, so assembling the tree
from `Group::metadata()` is UI work; there is no backend tree helper.
4. Range backfill: page the timeline with `until` for "load more".
4. Range backfill: page the timeline with `until` for "load more". **Done.**
`GroupId::timeline_page(limit, until)` is `timeline_filter` plus `.until(..)`,
and `Group::load_page` sends it as a `pg-<hash>` subscription (auto-close on
EOSE, 10s cap) alongside the standing `grp-<hash>` one, which keeps running so
paging cannot replace the live tail. The read path is the usual one: `pg-`
joins `is_own_subscription`, the id is registered in `routes` against the same
key, and `handle_event` therefore caches each page event and `reload`s the
group, so it arrives through the envelope cache and the timeline like any
other event. `GroupsRegistry::load_more(key, cx)` is the entry point: it does
nothing while a page is in flight (`Group::loading_more`) or before anything is
cached, and registers the route before `Group::load_page` sets the flag.
`handle_eose` and a page `CLOSED` remove the route and call
`Group::page_finished`, which clears the flag and emits `GroupEvent::Updated`
so a panel's spinner can settle. The cursor is `Group::oldest()`, the oldest
`h`-tagged event ever hydrated, updated by `track` next to `window.push` —
*not* read from `TimelineWindow`, which keeps only the newest 50 events, so a
cursor taken from it would never move and would page the same window forever.
`until` is inclusive, so a page repeats the event it starts from; `hydrate`
dedups by id, so that costs one redundant fetch, never a gap. `forget` also
drops and unsubscribes the `pg-` route (`reset` already sweeps `routes`). Two
gaps left for the UI phase: a relay that accepts the REQ but never EOSEs leaves
`loading_more` true (the auto-close timeout closes the subscription locally and
emits no notification — the same stickiness as `Browse::loading`), and a group
whose next page shares the `until` second cannot advance the cursor, so it can
re-fetch the same page. Neither loops on its own; both need a "load older
messages" button bound to `Group::loading_more()` to be observable at all.
5. LiveKit AV (`livekit` tag, `39004`, the `/.well-known/nip29/livekit` token
endpoint with a NIP-98 `27235` authorization event) and fork/migration
detection through the admins' `10009`.
@@ -449,6 +480,7 @@ group's relay only, and treat the relay's `OK` as the truth.
| Paste an naddr and join (or retry a refused join) | `GroupReference::parse` → `GroupsRegistry::join` |
| Open a group | `GroupsRegistry::group(&key, cx)` → panel subscribes to `GroupEvent` |
| Timeline | `Group::messages()` (already `chat::Message`s) |
| Load older messages | `GroupsRegistry::load_more(&key, cx)`; spinner from `Group::loading_more()` |
| Send / react | `Group::send_message` / `send_reaction` |
| Members, admins, roles, flags | `Group::members/admins/roles/metadata` |
| Membership badge (member, pending, refused, removed) | `Group::membership()` |