add group activity rows to nip29 timeline
This commit is contained in:
+33
-14
@@ -178,7 +178,7 @@ use nip29::{Group, GroupEvent};
|
||||
let Some(group) = registry.read(cx).group(&key, cx) else {
|
||||
return;
|
||||
};
|
||||
let messages = group.read(cx).messages(); // &[chat::Message]
|
||||
let rows = group.read(cx).rows(); // &[nip29::Row]
|
||||
```
|
||||
|
||||
```rust
|
||||
@@ -194,17 +194,26 @@ cx.subscribe(&group, |this, group, event, cx| match event {
|
||||
|
||||
| Variant | Means |
|
||||
| --- | --- |
|
||||
| `Incoming(Event)` | a fresh renderable message was folded in (use it for system notifications when the window is inactive) |
|
||||
| `Incoming(Event)` | a fresh renderable row was folded in (use it for system notifications when the window is inactive) |
|
||||
| `Sent(Event)` | our own event was accepted by the relay; the registry re-reads the group, the panel normally ignores it |
|
||||
| `Updated` | metadata, state, membership, pins or a finished page changed |
|
||||
| `Eose` | the standing subscription settled |
|
||||
| `Refused(String)` | the relay `CLOSED` the standing subscription, with its message |
|
||||
|
||||
The reads are all accessors: `key`, `metadata`, `members`, `admins`, `roles`,
|
||||
`participants`, `pins`, `membership`, `elsewhere`, `messages`, `loading`,
|
||||
`participants`, `pins`, `membership`, `elsewhere`, `rows`, `loading`,
|
||||
`loading_more`, `oldest`, `refused`. `display_name` reads the group's name and
|
||||
falls back to its id; `display_image` is the picture, for a row or avatar.
|
||||
|
||||
`rows` is the whole timeline in `created_at` order, one entry per renderable
|
||||
event. `Row::Message(Box<chat::Message>)` is a chat row; `Row::Activity(Activity)`
|
||||
is a moderation row — who joined (`9021`), who left (`9022`), who was removed
|
||||
(`9001`) and who was given roles (`9000` with roles; a `9000` without roles is the
|
||||
relay echoing a join we already show and is not a row). `Activity::subject()` is
|
||||
the affected person (the `p` tag target, or the author when there is none) and
|
||||
`Activity::roles` the assigned roles, so the view only chooses wording and an
|
||||
icon. `Group::message(id)` looks a chat row up for a pin label.
|
||||
|
||||
### The read model
|
||||
|
||||
There is **no `fetch_events` anywhere** in the crate. A group is read by
|
||||
@@ -213,7 +222,7 @@ and `crates/community`:
|
||||
|
||||
```
|
||||
subscribe grp-<hash> the global pump observes every REQ on the client
|
||||
│ (timeline + state + membership filters, one relay)
|
||||
│ (timeline + membership + one state filter per kind, one relay)
|
||||
▼
|
||||
save_envelope 30078, d = event id, r = "<relay>|<id>", k = kind
|
||||
│ content = the raw event JSON, signed by crate-local keys
|
||||
@@ -224,9 +233,13 @@ load_envelopes query by the r tag, parse, sort by created_at
|
||||
Group::hydrate the only place entity state changes
|
||||
```
|
||||
|
||||
The standing REQ carries three filters under one id: the timeline
|
||||
(`GroupId::timeline_filter(100)`), the relay state (`state_filter`) and our own
|
||||
membership (`membership_filter(me)`). Its id is `grp-` plus 8 hex of a hash of the
|
||||
The standing REQ carries the timeline (`GroupId::timeline_filter(100)`), our own
|
||||
membership (`membership_filter(me)`) and `state_filters()` — **one filter per
|
||||
state kind**, because a relay that serves group metadata is allowed to refuse a
|
||||
filter that mixes metadata kinds with other kinds (`groups.hzrd149.com` answers
|
||||
`blocked: it's not allowed to mix metadata kinds with others` and drops the whole
|
||||
REQ, which is what hid a group's name and picture until the filters were split).
|
||||
Its id is `grp-` plus 8 hex of a hash of the
|
||||
key — NIP-01 caps ids at 64 characters, and the pump needs a prefix to recognise
|
||||
its own subscriptions, so the real key is held in the registry's `routes`, not
|
||||
encoded in the id.
|
||||
@@ -242,7 +255,10 @@ page, a live tail and a local send all take the same path.
|
||||
- `STATE_KINDS` → `metadata`, `admins`, `members`, `roles`, `participants`,
|
||||
`pins` (relay-signed, read-only, addressable by `d`).
|
||||
- `MEMBERSHIP_KINDS` → our membership only (see below).
|
||||
- `RENDER_KINDS` → a `chat::Message`, inserted in `created_at` order.
|
||||
- `RENDER_KINDS` → a `Row::Message`, inserted in `created_at` order.
|
||||
- `ACTIVITY_KINDS` → a `Row::Activity`, inserted in `created_at` order. A `9001`
|
||||
that answers a `9022` from the same subject within five minutes is dropped, so a
|
||||
plain leave reads once.
|
||||
- everything else is kept only for the `previous` window.
|
||||
|
||||
Every `h`-tagged event feeds the window regardless of kind. The window is the
|
||||
@@ -517,7 +533,7 @@ impl GroupsRegistry {
|
||||
one `Subscription` per group, the fork state, and its tasks. It holds no browse
|
||||
result: a browse is fan-out, and the caller keeps the candidates.
|
||||
- `Group` owns everything the panel draws: metadata, admins/members/roles,
|
||||
participants, pins, membership, the `Vec<Message>` timeline, the `previous`
|
||||
participants, pins, membership, the `Vec<Row>` timeline, the `previous`
|
||||
window, `loading`/`loading_more`/`refused`.
|
||||
- Views render `Entity<Group>` and re-read on `GroupEvent`; no protocol state
|
||||
lives in a view.
|
||||
@@ -525,8 +541,8 @@ impl GroupsRegistry {
|
||||
and notifies itself, so a view that watches only the registry (the sidebar,
|
||||
which renders `groups()` as rows) stays in sync when a group's metadata,
|
||||
membership, pins or fork state change. `GroupEvent::Updated` fires for state and
|
||||
membership only, never for an arriving message — that is `GroupEvent::Incoming`,
|
||||
and it is why a rebuild of the row list does not happen per message. A view that
|
||||
membership only, never for an arriving row — that is `GroupEvent::Incoming`,
|
||||
and it is why a rebuild of the row list does not happen per row. A view that
|
||||
renders the timeline watches the `Group` itself.
|
||||
|
||||
### Foreground and background
|
||||
@@ -550,14 +566,14 @@ notifications() ──sig──► flume ──recv──► this.update(cx, …
|
||||
|
||||
### Subscriptions and routing
|
||||
|
||||
One `Client` and one notification stream are shared with `chat` and `community`,
|
||||
so every subscription the crate owns is identified by a prefix and nothing else:
|
||||
One `Client` and one notification stream are shared with `chat`, so every
|
||||
subscription the crate owns is identified by a prefix and nothing else:
|
||||
|
||||
| Id | Meaning |
|
||||
| --- | --- |
|
||||
| `nip29-list` | the account's `10009` |
|
||||
| `nip29-forks` | the admins' `10009` |
|
||||
| `grp-<hash>` | a group's standing REQ (timeline + state + membership) |
|
||||
| `grp-<hash>` | a group's standing REQ (timeline + membership + state filters) |
|
||||
| `pg-<hash>` | a "load more" page |
|
||||
| `brw-<hash>` | a relay browse |
|
||||
|
||||
@@ -575,6 +591,9 @@ before they reach the consumer, so a replay cannot double-hydrate.
|
||||
from a result. The panel has a read-only details dialog, a header `Manage` menu
|
||||
(edit metadata, invite link, members and roles) shown to admins, a message menu
|
||||
with reactions and admin pin/delete, a pins strip, a fork banner, and leave.
|
||||
Opening a group also fills the sidebar with the group itself — avatar, banner
|
||||
and the collapsible `Admins`/`Members` lists — with a back button in the title
|
||||
bar, the same shape the sidebar used for a community before the migration.
|
||||
What a given account may actually do is the relay's call; the client shows the
|
||||
refusal verbatim.
|
||||
- **Remote deletions are not folded in.** A `9005` from another admin never reaches the
|
||||
|
||||
Reference in New Issue
Block a user