Repository navigation
refactor(communities): extract the join owner and add the pre-join relay lookup - #1815
Conversation
d47e7c2 to
2415a08
Compare
5cc2c69 to
b9ea475
Compare
2415a08 to
600aa3a
Compare
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Three non-blocking observations on the preparatory lookup surface and its tests. No demonstrated regression in the existing join flow; native host integration remains unverified.
| * does, and leaving it out keeps Join a disabled placeholder. While `onJoin` is | ||
| * pending the page holds still; what its failure means is the caller's to show. | ||
| */ | ||
| export function FindCommunity({ |
There was a problem hiding this comment.
🤖 [P3] Introduce the lookup surface with its first production caller
The production caller search over src/ found no rendering caller for this component or consumer of the lookup flow; the no-publication completion branch and related preparatory APIs likewise await later stack layers. This fixes callback and lifecycle contracts before their integration exercises them, contrary to the repository requirement for a current caller. Consider moving this surface and its unused supporting branches into the layer that first consumes them, keeping this layer focused on the existing join extraction. This is a scope/maintainability concern, not a demonstrated live-path failure.
There was a problem hiding this comment.
🤖 Keeping the lookup in this layer. The stack is reviewed as one feature, and the callers are in #1820: Welcome.tsx renders for a typed community URL and for sign-in discovery, and calls completeJoin without publish when the relay or local default already holds a profile, or on Skip. FoundCommunity is also imported by #1817 and #1819, so the lookup can't move to its rendering caller, and moving FindCommunity.tsx into #1820 would grow the largest, FOUNDATION-touching layer. (If the optional change lands:) I did remove the one branch no layer reaches: with neither onJoin nor onFound, Join was a disabled "Coming soon" placeholder; the props now require exactly one.
…lay lookup
Everything the in-app Add-a-community dialog and a future first-run caller
share, with no reference to first run. Behaviour of the dialog is unchanged:
`native-join.test.tsx` and `CommunityDialog.test.tsx` are byte-identical to
main and green.
Join owner (`join.ts`, new): the dialog's two async blocks move out verbatim.
`admitCommunity({ community, journal?, code, policy?, consent, current })`
is the access step — a pending journal entry is read before it is reclaimed
(a denied read tolerated), an admitted key skips the claim, otherwise
`journal.begin` before any remote effect, then `accept-policy`
(`age_confirmed: consent.adult`) → `claim` (refusals mapped through
`claimRefusal`, anything else rethrown, a status other than
`joined`/`already_member` ⇒ "Membership was not confirmed") → the signed
profile read. `completeJoin({ community, membership, found, journal?,
entry?, publish?, communities, current })` is the profile step — fresh read,
publish only if different, readback or "Your profile change is not
current…", then `communities.joined(membership, profileDefault(…))` last and
`journal.finish`. With `publish` omitted there is no readback: the
membership is saved with the relay's profile or the local default. The
dialog never omits it; that branch's caller arrives with the Welcome flow.
`CLAIM_REFUSALS`/`claimRefusal` live here; `CommunityDialog.tsx` shrinks to
call the two functions.
Join policy (`join-policy.ts`, new): the one validator of `/api/join-policy`
answers (`JoinPolicy`, `joinPolicy()`), moved out of `native-api.ts`, whose
`info` route behaves as before; `CommunityInfo.policy` in `api.ts` names the
type.
Pre-join lookup (`find-community.ts` + `FindCommunity.tsx`, new): two
unauthenticated `relay_http` GETs in parallel, `GET /` (NIP-11) and
`GET /api/join-policy`, nothing signed. Errors are classified, never echoed:
network/timeout/5xx ⇒ "Could not reach the relay…" with Retry; non-JSON or a
non-Buzz `software`/`name` ⇒ "This address isn't a Buzz community."; a Buzz
relay without `read_state_snapshot`/`icon`/`buzz_v1` ⇒ "No community is set
up at this address."; a policy 404 means "none" only after NIP-11 confirmed
a Buzz relay, and a malformed policy is the retryable error, never a silent
consent waiver. NIP-11 `self` is copied only when it is 64 lowercase hex
(`relaySelf()` in `relay/native.ts`, now shared with
`connectNativeTransport`). The page shows the hostname and Cancel at once
with a `LoadingRegion`-delayed "Looking up {host}…" status (DESIGN.md: initial
reads stay silent for one second), then the found community — icon, mono
hostname, the community's own consent boxes ("I agree to this community’s
Terms of Service and Privacy Notice.", "I confirm that I am at least 18
years old.") with links to `{origin}/api/join-policy/terms|privacy` — and
Cancel-left/Join-right; `onFound(found)` hands a found community to the
caller instead, `onJoin(found, consent)` may return a promise and shows
"Joining…" while pending. Cancel aborts the lookup so a late answer never
lands.
Also: `enteredRelayOrigin()` in `destination.ts` prefixes `wss://` for
scheme-less human input and delegates to the strict `relayOrigin()` (saved
IDs and aliases never resolve); `JournalError` for the join journal's two
storage messages so a caller can show them as is; `ProfileFields` gains an
optional `autoFocus` (display name only).
Note for the reviewer: `FindCommunity.tsx` lives in `communities/`, not
`identity/`, because `join.ts` imports its `JoinConsent` type and the page
is the dialog's lookup made standalone.
Tests (new, under `src/features/communities`): `join.test.ts` (journal write
precedes `accept-policy` → `claim` → read with exact bodies; a throwing
`setItem` sends nothing; pending entry read before reclaim; refusal mapping;
"not confirmed"; fences; `completeJoin` publish/readback/no-publish paths),
`find-community.test.ts` (the NIP-11 and policy classification matrix,
parallel issue with the NIP-11 verdict first, `self` extraction, abort
pass-through), `FindCommunity.test.tsx` (finding → found with and without a
policy, the delayed status under fake timers, consent gating, `onFound`,
`onJoin` pending/rejected, Cancel discarding a late answer, Retry, the
Cancel-only errors), `destination.test.ts` (+33, the secure-origin rule for
entered addresses).
Stack: PR 02 of the onboarding stack, based on main; depends on nothing.
04–07 build on it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
… the save order The pre-join lookup added its own 12 s `AbortSignal.timeout` on top of the caller's signal. On the native path `nativeRelayRequest` only checks the signal after `relay_http` returns, and IPC cannot cancel reqwest, so the bound never shortened the wait. Instead, a relay that answered between 12 s and the native limit (10 s connect, 30 s total) kept "Looking up…" on screen until it answered, then had a valid answer reported as "Could not reach the relay…". The lookup now passes the caller's signal through and relies on the native limit; Cancel still aborts it at any time. A new `find-community.test.ts` case drives `nativeRelayFetch` with a mocked `relay_http` held for 20 s under a controlled `AbortSignal.timeout`: it fails with `unreachable` before this change and finds the community after. `join.test.ts`'s call sequence now records `communities.joined`, so the three successful-completion tests assert the membership is saved before `journal.finish`. Swapping those two calls in `join.ts` fails all three; it passed before. Addresses kalvinnchau's review comments 1 and 3 on #1815. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
600aa3a to
c172562
Compare
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Secondary review: the lookup deadline and membership-ordering test fixes address the earlier findings, and the shared API scope is supported by concrete stack consumers. No blocking defects found. The unused placeholder branch remains a non-blocking cleanup; native host integration remains unverified.
…ndCommunity `FindCommunity` showed a disabled "Join — Coming soon" button when a caller passed neither `onFound` nor `onJoin`. That placeholder was only reachable before the first-run Welcome page could join; since then every caller passes exactly one (`Welcome.tsx`'s typed-URL page passes `onJoin`, its landing lookup `onFound`), and only tests rendered the fallback. The props are now a union, so passing neither or both callbacks fails typecheck. The "Coming soon" label, the `!onJoin` guards in `disabled` and `join()`, and the docstring's placeholder sentence are gone. No reachable behaviour changes. Tests: the no-policy case renders once with `onJoin` and asserts no checkbox, no icon and an enabled Join; the `onFound` hand-over drops its now-invalid `onJoin`; the lookup and failure cases pass `onJoin`. Addresses kalvinnchau's review comment r4407169798 on #1815. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Onboarding stack (merge in order, top to bottom):
Depends on: nothing (based on 01 only to keep the stack linear). PRs 04–07 build on it.
Diff: production +620/−137; tests +904/−0
What this layer is
Everything the in-app Add-a-community dialog and a future first-run caller share, with no reference to first run.
join.tsextracts the dialog's admission (admitCommunity: journal-before-effect, read-before-reclaim,accept-policy→claim→ signed read,CLAIM_REFUSALS) and completion (completeJoin: fresh read, publish if different, readback,joined()last) verbatim;CommunityDialog.tsxshrinks to call them withnative-join.test.tsxandCommunityDialog.test.tsxbyte-identical tomainand green.join-policy.tsis the one join-policy validator, used bynative-api.ts(behaviour unchanged) and typed intoapi.ts.find-community.ts+FindCommunity.tsxare the unauthenticated lookup (NIP-11GET /+GET /api/join-policy, classified errors,LoadingRegion-delayed status, the community's own consent copy,onFound/onJoin(found, consent)), plusenteredRelayOrigin()(bare host ⇒wss://),relaySelf()shared withconnectNativeTransport,JournalError, andProfileFields'autoFocusprop.Review focus
join.tsagainst the removed blocks ofCommunityDialog.tsx: the move must be verbatim — same request order, same fences (current), same journal begin/finish points.completeJoinwithoutpublishhas no caller until PR 07 (stated in the file).find-community.ts: the error classification is deliberate — a policy 404 is "none" only after NIP-11 confirmed a Buzz relay; a malformed policy is retryable, never a silent consent waiver; nothing is signed.FindCommunity.tsxlives incommunities/, notidentity/, becausejoin.tsimports itsJoinConsenttype. Design:LoadingRegion,Checkbox,Button,Avatar, role classes only; the consent copy isCommunityDialog's verbatim.relay/native.tsrelaySelf():connectNativeTransportnow uses it for its existing 64-hex check — no behaviour change intended.Checks run on this branch's tree
bin/pnpm tsc --noEmit✓biome checkon the 15 changed TS/TSX files ✓pnpm design:check✓bin/pnpm vitest run src/features/communities src/features/relay: 2523 passed / 10 failed / 4 skipped of 2537 (141 files). All 10 failures are 5 s timeouts and abort errors under load in four relay store files this layer does not touch (read-state,store-discovery,store-system-discovery,unread-startup); this layer's only relay change isrelay/native.ts. Not rerun locally — CI is the verification.Native evidence
None on this tree. The native first-run recordings and the hands-on checks on the branch were made on the full tree, which only the DO-NOT-MERGE PR 09 reproduces (it adds the isolated debug Keychain slot). Treat every layer below 09 as unit/integration-tested only.
🤖 Generated with Claude Code