Skip to content

feat(sources): offer a copy link on the topic and directory surfaces - #6566

Merged
idoshamun merged 20 commits into
mainfrom
claude/snapshot-surfaces-topic-directory-3b3fd5
Sep 10, 2026
Merged

feat(sources): offer a copy link on the topic and directory surfaces#6566
idoshamun merged 20 commits into
mainfrom
claude/snapshot-surfaces-topic-directory-3b3fd5

Conversation

@tomeredlich

@tomeredlich tomeredlich commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Sharing on the topic and directory surfaces was buried behind an overflow menu. On the squad directory cards that menu is invisible group-hover/menu:visible, so on touch there was no share route at all.

One shared CopyLinkButton (components/share/CopyLinkButton.tsx) now sits on each of them:

Surface Component Placement
Tag page TagTopicPage.tsx Between Block and the overflow menu
Source page SourceActions/index.tsx (opt in with showCopyLink, used by pages/sources/[source].tsx) Beside the follow and notify buttons
Source directory lists SourceTopList.tsx Per row, revealed on hover for a mouse
Squad directory cards SquadGrid.tsx, UnfeaturedSquadGrid.tsx Beside Join
Squad directory rows (below the tablet breakpoint on /squads/discover and the featured and category pages; /squads/discover/my at every size) SquadList.tsx Before Join, revealed on hover for a mouse
Squad page SquadHeaderBar.tsx After the bell, only when the header has no "Invitation link"
Best of index ArchiveIndexPage.tsx Beside the heading
Best of month and year ArchiveFeedPage.tsx Beside the heading

It goes through useShareOrCopyLink: the native share sheet on mobile, a copy on desktop. The confirmation is main's CopyStateIcon (link glyph cross-fading to a check). Where the control is hover-revealed (source rows, unfeatured squad cards, squad list rows) it only hides behind laptop:mouse:, so it is always visible on touch, and it fades by opacity so it stays reachable by keyboard.

Ships unflagged: there is no GrowthBook flag, and it renders on merge.

Review first

  • hooks/useShareOrCopyLink.ts: the copy path used to await the URL shortener before writing to the clipboard. Safari refuses a write once the click task has ended, and useCopy now reports that as an error toast, so signed-in copies would have failed there. It now writes the tracked link inside the click and lets useCopyLink's shorten path swap in the short link, the same approach feat(snapshot): share placements for the post page #6556 took on the post page. This hook is shared, so the fix also reaches ShareActions, CustomFeedOptionsMenu, the profile share widget and the other existing callers. The native share path is unchanged.
  • CopyLinkButton takes a required origin and adds extra: { provider, origin } to the caller's share event, so a press on the new button can be told apart from the overflow menu on the same page, and a copy from a native share.
  • SquadHeaderBar: a member who can invite already has "Invitation link", so the header shows exactly one link control. The copy link renders for non-members of a public squad and for members without the invite permission. The invite permission check moved from SquadInviteButton into the header so both controls read the same condition.
  • SquadList: the copy link sits above the row's overlay CardLink (relative z-0), so a tap copies instead of opening the squad. The text column now shrinks with min-w-0 instead of a max-w sized for one button.
  • ArchiveCopyLinkButton: one place maps the archive scope to its event and campaign, used by both the index and the month/year pages. The global best-of has no tag or source, so it logs share archive with the generic campaign instead of falling through to share source with no target.

Events

Every action logs the entity's existing share event (these are not posts, so not SharePost) with target_id and extra: { provider, origin }. provider is copy link on desktop and native when the OS share sheet is used.

Action Event Origin
Copy link, tag page share tag tag page
Copy link, source page share source source page
Copy link, source directory row share source source directory (new)
Copy link, squad directory card or list row share source squad directory
Copy link, squad page (non-members, members who cannot invite) share source squad page
Copy link, tag or source best-of (index, month, year) share tag / share source archive index (new)
Copy link, global best-of (index, month, year) share archive (new) archive index (new)

The month and year pages share the archive index origin with the index page; the event does not say which of them it came from.

Experiment

None. No flag, no preview-host forcing.

Also here

Two files carried pre-existing strict-type errors that CI surfaces once a branch touches them, so they are fixed rather than worked around:

  • SourceActions passed an optional source.id into follow/unfollow; both now go through one helper that fails fast instead of firing a mutation it cannot form.
  • SquadHeaderBar passed an optional squad.id and a possibly undefined user into useSourceIntegrationQuery and useGetSquadAwardAdmin. Both hooks already guarded for those at runtime, so the prop types are widened to match.

The Storybook "Topic & directory pages" mockup is removed now that the placements ship on the real pages, and the Overview row points at the live pages instead.

Testing

  • CopyLinkButton.spec.tsx: the clipboard write happens inside the click with the tracked link, and the event carries the provider and origin. Both fail against the old hook.
  • SquadList.spec.tsx: the row's copy link writes the tracked squad link, logs share source with the squad directory origin, and does not click the row link.
  • SquadHeaderBar.spec.tsx: a member with the invite permission sees only "Invitation link"; a non-member sees only "Copy link".
  • ArchiveFeedPage.spec.tsx: the copy link on a global monthly best-of page logs share archive with the archive index origin.
  • pnpm --filter shared test: 410 suites, 2957 tests pass.
  • pnpm --filter webapp test: 86 suites, 689 tests pass.
  • node ./scripts/typecheck-strict-changed.js origin/main: passes.
  • Full webapp tsc --noEmit: no errors outside the known __tests__ backlog.
  • eslint clean on every changed file.

Preview pages: /tags/javascript, /sources/theverge, /sources, /squads/discover (cards from the tablet breakpoint, list rows below 656px), /squads/discover/featured, a squad page as a member and as a non-member, /posts/best-of, /posts/best-of/2025, /posts/best-of/2025/8.

Preview domain

https://claude-snapshot-surfaces-topic-d.preview.app.daily.dev

@vercel

vercel Bot commented Sep 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
daily-webapp Ready Ready Preview Sep 10, 2026 2:36pm UTC
storybook Building Building Preview Sep 10, 2026 2:36pm UTC

Request Review

Wraps useShareOrCopyLink so a surface can offer a link directly instead of
burying it in an overflow menu: the native sheet on mobile, a clipboard copy
on desktop. The hook already raises a toast on copy, so the button stays
static rather than swapping in a confirmation icon.

Defaults to Float because it usually sits beside a Block or overflow button;
surfaces whose neighbours are tertiary pass their own variant.
All four of these surfaces ended in a live feed and hid sharing behind the
same overflow menu. On the squad card that menu is `invisible
group-hover/menu:visible`, so touch had no share route at all.

Give each one a copy link in the spot its own layout allows:

- tag page, between Block and the overflow menu
- source page, beside the follow and notify buttons, behind `showCopyLink`
  so PostUsersHighlights keeps its current row
- squad directory card, before Join so it never competes with the conversion
- archive index, beside a heading row that carried no controls
- source directory rows, revealed on hover from laptop up and always present
  below it, using opacity so the button stays focusable

SourceActions passed an optional source.id straight into follow/unfollow,
which never type-checked in strict mode; route both through one helper that
fails fast instead of firing a mutation it cannot form.
One story covering the four surfaces, each showing the placement that ships
across desktop, tablet and mobile, so the layouts can be compared side by
side without opening four pages.

Mockup-to-eng-pass: 1
@tomeredlich
tomeredlich force-pushed the claude/snapshot-surfaces-topic-directory-3b3fd5 branch from 0dd55be to 0132963 Compare September 2, 2026 15:05
@tomeredlich tomeredlich changed the title docs(snapshot): document topic and directory page share placements in Storybook feat(sources): offer a copy link on the topic and directory surfaces Sep 2, 2026
tomeredlich and others added 2 commits September 3, 2026 12:35
The copy link control relied on the toast alone, so a press had no
feedback on the button itself. It now swaps to a filled avocado arrow
and spins through the same 0.4s curve as the upvote button, then
reverts when useCopy clears its 1s window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The directory boxes are SquadGrid and UnfeaturedSquadGrid, not the
SquadEntityCard the earlier pass changed — that one is the hover preview
shown from SourceButton and the post widgets, so the control never appeared
where the directory actually renders. Move it onto the two real cards and
reveal it on hover from laptop up, keeping it visible below that for touch.

Both cards lay a CardLink across the whole surface, so the button renders
after the content with its own z-index rather than relying on stacking order.

The squad page header gets one beside the bell, sharing its order so it
stays put as the row wraps.

SquadHeaderBar passed an optional squad.id and a possibly-undefined user
into hooks typed to require them, which never type-checked in strict mode.
Widen the two hook props to match the guards they already run, and narrow
the id where the component needs it.
Replace the spinning avocado arrow with CopyStateIcon, taken verbatim from
snapshot-happening-now so the two branches cannot drift: the link glyph and a
green check share one grid cell and cross-fade over 200ms on easeOutExpo,
collapsing to an instant swap under prefers-reduced-motion. Nothing resizes
mid-swap, which the spin could not promise.

Drop the copy-confirm keyframes with it, now that nothing references them.

On the squad directory cards the control loses its fill and stroke, and takes
the Medium size SquadActionButton leaves Button on, so it lines up with Join
rather than sitting a step smaller beside it.
It floated over the banner, top right, where it sat on the artwork and read
as part of the image rather than the card's controls. Put it at the bottom
right instead, sharing the row with Join, which keeps every control on the
card in one place. Join flexes to fill what is left, so the row still reads
as one primary action with an affordance beside it.
The featured card carries one squad at full size, so there is room for the
control to simply be there. Hover-gating it also left a gap beside Join where
the transparent button still took its space.

The unfeatured grid keeps the reveal: those cards sit many to a row, where a
visible icon on every one competes with the names.
The squad section documented SquadEntityCard, which is the hover preview card
the post widgets render, not the directory. Redraw it as the two cards the
page actually uses, and add the two surfaces the story never covered: the
squad page header and the source directory rows.

Drop the copy link from SquadEntityCard with it. Nothing asked for a control
on the post page previews, and it reached four surfaces through that card.

Cover the placement on both squad cards, so the control cannot quietly stop
rendering.
…ces-topic-directory-3b3fd5

Brings in #6556 (post page share placements) and #6544 (designed share
cards).

Conflicts: surfaceChrome.tsx and surfaces/Directories.stories.tsx were
added on both sides. Took main's version of both, since main's
surfaceChrome is the shared helper the other surface stories import.
CopyStateIcon and its spec were identical on both sides and merged
cleanly.
The copy link placements logged ShareTag/ShareSource with only a
target, so a press on the new button could not be told apart from the
overflow menu's Share item on the same page, and native share could not
be told apart from a copy.

CopyLinkButton now takes a required origin and adds
extra: { provider, origin } to the caller's event, the same extra shape
#6556 uses for SharePost. Tag, source and squad pages reuse their page
origins; the source directory lists and the best-of archive get new
SourceDirectory and ArchiveIndex values.

The global best-of archive has no tag or source, but fell through to
ShareSource with no target and the share_source campaign. It now logs
a new ShareArchive event with the generic campaign.

Also imports LinkIcon from its own file rather than the icons barrel.
The copy path awaited the URL shortener before writing to the
clipboard. Safari refuses a clipboard write once the task that handled
the click has ended, and useCopy now reports that refusal, so every
signed-in copy from CopyLinkButton (and the other callers of this hook,
including ShareActions) would end in a "blocked the clipboard" toast.

It now writes the tracked link straight away and hands the shortening
to useCopyLink's shorten path, which swaps the short link in through a
pending ClipboardItem, the same approach #6556 took for the post page.
The native share path still resolves the short link first, since the
text it shares has to carry it.

The CopyLinkButton spec pins the synchronous write and the logged
provider and origin. The two squad card tests that only asserted the
button exists are dropped: they catch nothing the new spec does not.
The source directory rows and the unfeatured squad cards hid the copy
link from laptop width up and revealed it on hover. A touch device that
wide (an iPad in landscape, a touch laptop) has no hover, so the control
stayed invisible there. Gating on laptop:mouse:, the pairing the feed
cards already use, keeps it visible on any touch screen. It still fades
by opacity rather than visibility, so it stays reachable by keyboard.

Also imports largeNumberFormat from lib/numberFormat, not the lib
barrel, in the file this touched.
The story drew mock tag, source, squad and archive pages to compare
copy link placements. This PR ships those placements on the real
pages, so the mockup can only drift from them. The Overview row now
points at the live pages, like the post page row does.
SourceActions and TagTopicPage read ReferralCampaignKey, which the copy link's shareProps now depends on, through the hooks barrel. Import it from lib/referral, and the hook beside it from its own file.
Below the MobileL breakpoint the squad directory renders SquadList rows
instead of the grid cards, and those rows only had Join, so phones had
no copy link on /squads/discover even though touch was the reason for
the placement. The same rows back the featured, category and my squads
pages on phones.

The row now carries the same CopyLinkButton as the grid cards (squad
directory origin, share source event, share source campaign). It sits
above the row's overlay link, so a tap copies instead of opening the
squad, and it only hides behind hover for a fine pointer on laptop and
up, like the unfeatured grid card. The text column now shrinks with
min-w-0 instead of a max width sized for a single button.
A member who can invite saw the existing "Invitation link" next to the
new "Copy link", two controls for nearly the same thing. The copy link
now renders only when the invitation link does not: for non-members of
a public squad and for members without the invite permission. The
invite permission check moves up from SquadInviteButton so both
controls read the same condition.
The best-of index pages had a page-level copy link but the month and
year pages they link to did not, for every scope. ArchiveFeedPage now
renders the same control beside its heading.

The scope to event and campaign mapping moves into
ArchiveCopyLinkButton so both pages log the same scheme: share archive
for the global scope, share tag or share source otherwise, with the
archive index origin.
idoshamun added a commit that referenced this pull request Sep 10, 2026
The DevCard's Share copied the bare permalink, with no cid or userid and
no short link, while the profile header's copy link gave a tracked short
link. The share now builds the ShareProfile tracked link without a
request, so it reaches the share sheet and the clipboard inside the
press, and the copy swaps in the short link through useCopyLink's
shorten path once the shortener answers. It no longer goes through
useShareOrCopyLink, which awaits the shortener before writing and loses
Safari's user gesture (#6566 fixes that hook separately).

The header's copy link now writes the tracked link first too, as
useSharePost does, instead of the bare permalink with the tracked one
only in the short link. When the short link cannot be written, the
clipboard keeps an attributed link either way.
@idoshamun
idoshamun merged commit 9de24d2 into main Sep 10, 2026
11 checks passed
@idoshamun
idoshamun deleted the claude/snapshot-surfaces-topic-directory-3b3fd5 branch September 10, 2026 15:22
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.

2 participants