Skip to content

feat(social): visibility discoverability for import/copy + first-publish seal (E150)#1310

Open
georgepapagapitos wants to merge 1 commit into
mainfrom
e150-visibility-flows
Open

feat(social): visibility discoverability for import/copy + first-publish seal (E150)#1310
georgepapagapitos wants to merge 1 commit into
mainfrom
e150-visibility-flows

Conversation

@georgepapagapitos

Copy link
Copy Markdown
Owner

Summary

E150 (board): extends the creation-time Private/Public visibility choice shipped in #1278 (DeckNewPage's fieldset) to the two flows that create decks without that form — import and copy — and wires the first-publish seal moment (E114-family liveliness) at the point publication is actually confirmed.

Design calls

Import (ImportDeckDialog) — fieldset, not a nudge. The single-deck paths (paste, or a staged multi-file batch explicitly merged into one deck) are form-like surfaces already asking Format/Name before a single "Create"/"Import" action, so they get the same Visibility fieldset DeckNewPage uses — same .share-audience ladder classes, same copy. A "separate decks" batch (N results) has no single editor to land a choice on; those decks stay private by default (unchanged behavior) and are publishable per-deck afterward via the already-shipped editor chip. A batch that happens to land on exactly one deck gets the lighter nudge instead (see below) rather than duplicating the fieldset into the multi-draft review screen.

Copy (CopyDeckButton) — a post-create nudge, not a fieldset. One-tap by design; forcing a Public/Private decision into that tap would add friction the component is explicitly built to avoid. Lands on /decks/:id with a one-shot promptVisibility router-state flag (mirrors the existing justGenerated pattern) that shows a new DeckPublishNudge — one row, dismissible, opens the existing ShareDialog on tap. Never displaces page content, renders nothing for guests (who can't publish), and self-retires once engaged with.

Shared choke point. Both fieldset consumers route through a new usePublishOnCreate hook (extracted from DeckNewPage, which now consumes it too) so the guest/offline gating, display_name_required inline substep, and success toast live in exactly one place instead of forking.

First-publish seal — two firing sites, one shared guard. ShareDialog.doPublish() fires useSealMoment() directly since neither of its two callers (the editor chip, the new nudge) navigate away on publish. The creation-time hook can't fire directly — every caller navigates to the new deck's editor the instant publish resolves, which would unmount the portal mid-animation — so it hands {isFirstPublish} to onSettled, which threads it through router state (justPublished) to DeckEditorPage, which fires on landing instead. Both sites share one dedup function (shouldCelebrateFirstPublish, lib/first-publish-celebration.ts) so a deck's first publish celebrates exactly once regardless of entry surface, and the server's isFirstPublish flag (derived from 201-vs-200) means a republish never re-celebrates without even touching the guard.

Bug found and fixed via live validation

Browser-testing the create→publish flow surfaced a real, 100%-reproducible race predating this PR: a publish attempt fired immediately after createDeck() reaches the server before the deck's own fire-and-forget local persist does (the store's sync subscriber is void-fired, and — for a signed-in web session — writes straight through with no debounce to even flush), 404ing "Deck not found." on effectively every brand-new deck. A unit test with a mocked publishDeck can't reproduce this. Fixed with a typed DeckNotSyncedYetError + one bounded retry in usePublishOnCreate, localized entirely to this hook rather than coupling to sync.ts internals (per the documented E22 fragility).

Test plan

  • npm run typecheck / lint / format:check / test:coverage (frontend) — all green, 6831 passed, coverage gate passed
  • New/updated co-located vitest: first-publish-celebration.test.ts (dedup logic), use-publish-on-create.test.ts (13 tests: gating, success, display_name_required substep, the sync-race retry, republish-never-celebrates), ImportDeckDialog.visibility.test.tsx, DeckPublishNudge.test.tsx, plus updated assertions in DeckNewPage.test.tsx, CopyDeckButton.test.tsx, ShareDialog.test.tsx, publications-client.test.ts
  • Live browser validation against localhost:3737 + dev/spellcontrol: import fieldset at 320/360/600/768/1024/1440, light+dark; display_name_required substep (dev account had no display name); publish success + seal moment caught live twice; copy flow + nudge at 320/360/1440, light+dark; nudge → Share dialog → confirm-public step
  • polish:responsive-audit — clean, no critical/warning findings in the diff
  • Dev account fully restored: 2 live decks (unchanged), 0 publications, display name reset to null

…ish seal (E150)

Extends the creation-time Private/Public choice (#1278) to the two flows
that skip DeckNewPage's own fieldset:

- ImportDeckDialog's single-deck path (paste, or a staged batch merged
  into one) gets the same fieldset, driven by a new shared
  usePublishOnCreate hook so the network/error/display-name-substep
  dance isn't forked between the two consumers. A multi-file "separate
  decks" batch has no single editor to land the choice on, so those
  decks stay private by default (unchanged) and a single-result landing
  gets a lighter nudge instead.
- CopyDeckButton's one-tap copy and a batch-import landing on one deck
  both get a new DeckPublishNudge — a dismissible, non-displacing
  one-row banner pointing at the existing DeckVisibilityChip/ShareDialog,
  rather than a fieldset a single click has no natural pause for.

First-publish seal moment: ShareDialog's doPublish() fires it directly
(the chip and the new nudge both stay mounted through a publish), while
the creation-time hook can't — every caller navigates away the instant
publish resolves, which would unmount the portal mid-animation — so it
hands the outcome to DeckEditorPage's `justPublished` router state,
which fires on landing instead. Both paths share one dedup guard
(shouldCelebrateFirstPublish) so a deck's first publish celebrates
exactly once regardless of entry surface, and a republish never
re-celebrates.

Also fixes a real bug this surfaced under live browser validation: a
publish attempt fired immediately after createDeck() can reach the
server before the deck's own fire-and-forget local persist does,
404ing ("Deck not found.") on effectively every brand-new deck. A
typed DeckNotSyncedYetError + one bounded retry in usePublishOnCreate
closes the gap without coupling to sync.ts's internals.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant