add browse panel for discovering relay groups
This commit is contained in:
+55
-28
@@ -58,16 +58,20 @@ subscribes to `GroupsEvent` and renders `groups()` — pending and refused group
|
||||
are in that list alongside members, and their `Group::membership()` says which.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GroupsEvent {
|
||||
Updated,
|
||||
Error(String),
|
||||
Browsed(Box<GroupCandidate>),
|
||||
BrowseSettled(RelayUrl),
|
||||
}
|
||||
```
|
||||
|
||||
`Updated` fires on any change to `groups()` or to a browse result; `Error` is only
|
||||
emitted when publishing a `9007` (create) is refused, and carries the relay's
|
||||
message.
|
||||
`Updated` fires on any change to `groups()`; `Error` is only emitted when
|
||||
publishing a `9007` (create) is refused, and carries the relay's message.
|
||||
`Browsed` and `BrowseSettled` carry a browse: one group the relay announced, and
|
||||
the end of the answer (see [Browsing a relay](#browsing-a-relay)). They are
|
||||
fan-out, not stored — whatever opened the browse owns the candidates.
|
||||
|
||||
## Value types
|
||||
|
||||
@@ -435,23 +439,35 @@ check runs with the relay gone, just at the next tick.
|
||||
## Browsing a relay
|
||||
|
||||
```rust
|
||||
use nip29::Browse;
|
||||
|
||||
registry.update(cx, |registry, cx| registry.browse(relay_url.clone(), cx));
|
||||
let browse: Option<&Browse> = registry.read(cx).browsed(&relay_url);
|
||||
|
||||
// then, in a GroupsEvent handler:
|
||||
match event {
|
||||
GroupsEvent::Browsed(candidate) => push(candidate),
|
||||
GroupsEvent::BrowseSettled(relay) => settled(relay),
|
||||
_ => {}
|
||||
}
|
||||
```
|
||||
|
||||
`Browse` is `{ relay, loading, groups: Vec<GroupCandidate> }`, where a candidate
|
||||
is a `GroupKey` plus its parsed `GroupMetadata`. `browse` connects the relay,
|
||||
sends one auto-closing `39000` REQ (limit 500, EOSE or 10s) and returns
|
||||
immediately; the pump folds each relay-signed metadata event into the result. Read
|
||||
it back in a `GroupsEvent::Updated` handler, which also fires when the browse
|
||||
settles (`loading` flips false on EOSE or a refusal). Join a result with
|
||||
`join_key(candidate.key, None, cx)`.
|
||||
A `GroupCandidate` is a `GroupKey` plus its parsed `GroupMetadata`; `display_name`
|
||||
falls back to the id and `display_image` is the picture, the same pair `Group`
|
||||
offers. `browse` connects the relay, sends one auto-closing `39000` REQ (limit
|
||||
500, EOSE or 10s) and returns immediately; the pump parses each relay-signed
|
||||
metadata event and re-emits it as `Browsed`.
|
||||
|
||||
One caveat: `Browse::loading` can stick if the relay accepts the REQ and never
|
||||
EOSEs, because the auto-close timeout closes the subscription locally with no
|
||||
notification. Treat it as "still trying", not as an error.
|
||||
**The registry stores no browse result.** It keeps only which relay each `brw-`
|
||||
REQ belongs to — the same routing bookkeeping as `routes`, and the reason
|
||||
`Browsed` can carry a boxed candidate instead of the registry holding a `Vec`:
|
||||
the only copy is the caller's. A browse is transient by nature, so the surface
|
||||
that asked for it owns it (the browse panel keeps `relay`, `candidates` and
|
||||
`loading` in its own state, and drops them with the panel).
|
||||
|
||||
`BrowseSettled` is driven by the subscribe task itself, not by `EOSE`/`CLOSED`
|
||||
notifications: it is sent once `subscribe(..).await` returns, which happens on
|
||||
EOSE, on a refusal, or when the auto-close timeout fires. So the answer always
|
||||
ends, even against a relay that never EOSEs. A candidate can still arrive after
|
||||
the settle — the pump and the subscribe task are different producers — which is
|
||||
why the relay stays mapped. Join a result with `join_key(candidate.key, None, cx)`.
|
||||
|
||||
## Creating a group
|
||||
|
||||
@@ -496,14 +512,22 @@ impl GroupsRegistry {
|
||||
}
|
||||
```
|
||||
|
||||
- `GroupsRegistry` holds `groups: Vec<Entity<Group>>`, the `10009` `list`, the
|
||||
live `browse` results, `routes` (subscription id → `GroupKey`), one
|
||||
`Subscription` per group, the fork state, and its tasks.
|
||||
- `GroupsRegistry` holds `groups: Vec<Entity<Group>>`, the `10009` `list`,
|
||||
`routes` (subscription id → `GroupKey`), the relay behind each live `brw-` REQ,
|
||||
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`
|
||||
window, `loading`/`loading_more`/`refused`.
|
||||
- Views render `Entity<Group>` and re-read on `GroupEvent`; no protocol state
|
||||
lives in a view.
|
||||
- The registry re-broadcasts every `GroupEvent::Updated` as `GroupsEvent::Updated`
|
||||
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
|
||||
renders the timeline watches the `Group` itself.
|
||||
|
||||
### Foreground and background
|
||||
|
||||
@@ -545,20 +569,23 @@ before they reach the consumer, so a replay cannot double-hydrate.
|
||||
|
||||
## Not wired up yet
|
||||
|
||||
- **Browsing, details and administration are not built.** The sidebar's `Groups`
|
||||
tab lists `GroupsRegistry::groups()`, opens a `GroupPanel`, and offers the
|
||||
new-group and join dialogs, but `crates/workspace/src/panels/browse.rs` is still
|
||||
a stub, and the group details, moderation and pin surfaces are still to be built.
|
||||
- **Details and administration are not built.** The sidebar's `Groups` tab lists
|
||||
`GroupsRegistry::groups()`, opens a `GroupPanel`, and offers the new-group and
|
||||
join dialogs; `crates/workspace/src/panels/browse.rs` browses a relay and joins
|
||||
from a result. The group details, moderation and pin surfaces are still to be
|
||||
built.
|
||||
- **Remote deletions are not folded in.** A `9005` from another admin never reaches the
|
||||
standing REQ, so only our own accepted deletes evict the row.
|
||||
- **The LiveKit response shape is a guess.** The NIP names no fields, so
|
||||
`LivekitSession::parse` reads `token` (or `jwt`) and `url` (or `server_url`) and
|
||||
fails loudly on anything else. Re-check it against the relay actually targeted
|
||||
before building the AV UI, or the join button fails silently.
|
||||
- **A relay that never EOSEs sticks a spinner.** `Browse::loading` and
|
||||
`Group::loading_more` both rely on EOSE or CLOSED, and the auto-close timeout
|
||||
closes the subscription with no notification at all. Bind "load more" to
|
||||
`Group::loading_more()` and give the user a way out.
|
||||
- **A relay that never EOSEs bounds how long "load more" spins.**
|
||||
`Group::loading_more` relies on EOSE or CLOSED, and the auto-close timeout
|
||||
closes the subscription with no notification at all; bind "load more" to
|
||||
`Group::loading_more()` and give the user a way out. A browse is fine by
|
||||
contrast: `BrowseSettled` comes from the subscribe task returning, so it
|
||||
always lands.
|
||||
- **Subgroups are parsed, not assembled.** `parent`/`children` are on
|
||||
`GroupMetadata` and the writes carry them; there is no tree builder, because
|
||||
the tree is scoped to a relay and that is a view concern.
|
||||
|
||||
Reference in New Issue
Block a user