feat(social): visibility discoverability for import/copy + first-publish seal (E150)#1310
Open
georgepapagapitos wants to merge 1 commit into
Open
feat(social): visibility discoverability for import/copy + first-publish seal (E150)#1310georgepapagapitos wants to merge 1 commit into
georgepapagapitos wants to merge 1 commit into
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-audienceladder 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/:idwith a one-shotpromptVisibilityrouter-state flag (mirrors the existingjustGeneratedpattern) that shows a newDeckPublishNudge— one row, dismissible, opens the existingShareDialogon 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
usePublishOnCreatehook (extracted from DeckNewPage, which now consumes it too) so the guest/offline gating,display_name_requiredinline substep, and success toast live in exactly one place instead of forking.First-publish seal — two firing sites, one shared guard.
ShareDialog.doPublish()firesuseSealMoment()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}toonSettled, which threads it through router state (justPublished) toDeckEditorPage, 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'sisFirstPublishflag (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 isvoid-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 mockedpublishDeckcan't reproduce this. Fixed with a typedDeckNotSyncedYetError+ one bounded retry inusePublishOnCreate, localized entirely to this hook rather than coupling tosync.tsinternals (per the documented E22 fragility).Test plan
npm run typecheck/lint/format:check/test:coverage(frontend) — all green, 6831 passed, coverage gate passedfirst-publish-celebration.test.ts(dedup logic),use-publish-on-create.test.ts(13 tests: gating, success,display_name_requiredsubstep, the sync-race retry, republish-never-celebrates),ImportDeckDialog.visibility.test.tsx,DeckPublishNudge.test.tsx, plus updated assertions inDeckNewPage.test.tsx,CopyDeckButton.test.tsx,ShareDialog.test.tsx,publications-client.test.tsdisplay_name_requiredsubstep (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 steppolish:responsive-audit— clean, no critical/warning findings in the diff