Skip to content

feat(share): copy-link control on profile headers - #6354

Merged
tsahimatsliah merged 12 commits into
claude/website-sharing-visibility-be6b32from
claude/share-profile
Jul 28, 2026
Merged

feat(share): copy-link control on profile headers#6354
tsahimatsliah merged 12 commits into
claude/website-sharing-visibility-be6b32from
claude/share-profile

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Jul 22, 2026

Copy link
Copy Markdown
Member

Part of the sharing visibility initiative. Builds on PR 1 (#6343) — base branch is claude/website-sharing-visibility-be6b32, not main.

What changed

The share action on a profile lived only inside the ... options menu, and the owner's only share affordance (the Public profile & URL widget) is laptop:hidden, so a signed-in user on desktop had no way to share their own profile at all. This promotes it to a real control.

ProfileHeader.tsx — the canonical placement (all breakpoints, both profile types)

  • Own profile: copy-link button next to Edit profile.
  • Public profile: the button fills the slot the edit button used to reserve (it renders invisible there today purely to hold the row's height), so the layout height is unchanged and no dead placeholder is left behind.

profile/Header.tsx — the pinned mobile bar

  • Shows the same control, but only while pinned (sticky). Unpinned, the profile card directly below owns it; rendering both would put two identical copy buttons on one screen.
  • Share is promoted out of the ... menu here.

ProfileActions.tsx — share promoted out of its ... menu too; the new button sits directly above it on the same card.

CustomFeedOptionsMenu.tsx — new optional hideShare prop drops just the Share entry. Default false, so no other call site changes. This is the only shared-component touch outside profile/.

New

  • profile/ProfileShareButton.tsx — the control itself. One click copies the profile link (shortened via dly.to with ReferralCampaignKey.ShareProfile) and confirms twice: the glyph flips to a green check for a second (VIcon in text-status-success) and a toast names what was copied. No network picker — choosing a destination is a second decision and the clipboard covers every destination. On mobile, where the platform offers a native share sheet, a tap still opens that.
  • useShareOrCopyLink gains an optional copyMessage so the toast can name the profile instead of the generic "Copied link to clipboard". Backwards compatible.

Deliberately not using PR 1's ShareActions

The primitive opens a network popover. That is a second click before anything useful happens, so this control drives useShareOrCopyLink directly instead. ShareActions is untouched by this PR and stays PR 1's for the initiative's other surfaces.

Research principle #6 — share stays distinct from Follow

Share is an icon control in the header's top-right utility slot; Follow is the filled primary CTA further down the card, inside ProfileActions. Different region, different weight — they never sit in the same control group. In the pinned bar, ml-1 separates the utility icon from the Follow group.

Logging

Reuses the existing LogEvent.ShareProfile (the event already emitted by the menu item and the owner share widget, so metrics stay comparable) with target_type: TargetType.ProfilePage and extra: { provider, origin: Origin.Profile }. No new Origin value was needed.

No flag — ships to everyone

Per product decision this is unconditional: share_profile and useShareProfileEnabled are gone, so there is no ramp and no kill-switch short of a revert.

This PR simply stops consuming PR 1's sharing_visibility master gate — it does not delete it. share_copy_icon and the surfaces it touches (feed card action bar, brief post header, mobile share widget) are PR 1's and are untouched here.

Header control design

Share and Edit are both Subtle Small and bottom-aligned to the avatar: the row is a fixed h-10 with items-end rather than spaced off the cover, so alignment holds at any button height. The glyph is LinkIcon, the same copy-link glyph the feed card already uses.

OG image — deferred, on purpose

A per-profile unfurl card (mini dev-card with handle + stats) would make a shared profile land much harder. It is not in this PR:

  • The repo has no OG-image library. The pattern is a server-screenshotted Next.js page under packages/webapp/pages/image-generator/ (devcards/[userId].tsx, badges/[badgeId].tsx, id="screenshot_wrapper", ISR).
  • A new image-generator/profiles/[userId].tsx route is only half the work: the backend screenshot service needs a route registered for it, and getProfileSeoDefaults needs to emit the resulting og:image. That is a cross-repo dependency with its own rollout.

Shipping the affordance now and the image as a follow-up keeps this PR reviewable, per the brief's explicit guidance to defer rather than half-build.

Backend dependencies

None for this PR. The deferred OG image needs a screenshot-service route (see above).

Verification

  • node ./scripts/typecheck-strict-changed.js — clean for every file this PR adds or rewrites. The remaining reported errors (CustomFeedOptionsMenu.tsx:51 feeds.edges.length, ProfileActions.tsx user.username) are pre-existing on the base branch; confirmed by reading the same expressions out of origin/claude/website-sharing-visibility-be6b32, untouched by this diff.
  • pnpm --filter shared lint — pass.
  • NODE_ENV=test pnpm --filter shared test — 297 suites / 1997 tests pass.
  • NODE_ENV=test pnpm --filter webapp test — 44 suites / 297 tests pass.
  • pnpm --filter storybook lint not run (missing eslint binary, pre-existing repo issue).
  • build not run, per instructions.

Tests added

  • ProfileShareButton.spec.tsx — copy writes the permalink to the clipboard and raises the ✅ Copied link to clipboard toast; native share calls navigator.share with the share text + link and never touches the clipboard; desktop reveals the network list; both labels (Share your profile / Share @handle's profile) are asserted for a11y.
  • ProfileHeader.spec.tsx — the control fills the slot on public profiles (the invisible edit placeholder is gone), sits beside Edit on the owner's, and both render at the same size and variant.
  • ProfileShareButton.spec.tsx — both labels, the copied link, the toast wording for each, the green-check transition, the native-share path, and that no popover opens.
  • CustomFeedOptionsMenu.spec.tsxShare is listed with shareProps and dropped without them.

Storybook

Components/Share/ProfileShareButton — public/own labels, network list, copying state, and the button in place in both header variants.

Components/Share/Overview → States And Variants — a review page putting every surface and state on one scrollable page: the primitive's variants, both profile headers, the pinned mobile bar (embedded as real 390px story frames, not mock-ups), the menu with and without hideShare, the glyph's idle/copied states, and the copy inventory.

Open questions

  1. Is the pinned-only rule on the mobile bar right? The alternative is showing it in the bar at all times and accepting two copy buttons at the top of a mobile profile. Happy to flip it.
  2. Removing Share from the ... menus — intentional per the brief ("promote out of the menu where it makes sense"), but it does mean the menu loses an entry some users may have learned. Easy to keep both if we'd rather not move anyone's cheese mid-experiment.
  3. Share text for a public profile is Check out {name}'s profile on daily.dev (matching the current menu copy) and Check out my profile on daily.dev for the owner. Worth a copy pass?

🤖 Generated with Claude Code

Preview domain

https://claude-share-profile.preview.app.daily.dev

Adds a dedicated share control to the profile header instead of burying it
in the "..." options menu.

- Own profile: sits next to "Edit profile".
- Public profile: fills the slot the (inapplicable) edit button used to
  reserve, so the layout height is unchanged.
- Stays well away from Follow (different card region, Float icon vs. the
  filled primary CTA) so the two intents don't read as one control group.
- The pinned mobile profile header shows it only while pinned, where the
  profile card has scrolled away — otherwise there would be two identical
  copy buttons on screen.

Built on the shared `ShareActions` primitive from PR 1, so mobile taps
straight through to the native share sheet and desktop opens the network
popover.

Gated by `share_profile` plus the `sharing_visibility` master flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Jul 28, 2026 11:32am
storybook Building Building Preview Jul 28, 2026 11:32am

Request Review

Reconciles the CustomFeedOptionsMenu API with the tags/sources branch:
shareProps stays required (no empty-link fallback into useShareOrCopyLink)
and surfaces that promote share to a visible control pass hideShare instead.
File is now byte-identical on both branches so the stack merges cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Evaluate the per-topic flag only once sharing_visibility passes, so control
users are never bucketed into the experiment. Matches the other topic gates.
Also types the menu spec helper now that shareProps is required again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tsahimatsliah and others added 5 commits July 27, 2026 16:38
Puts every surface the two share PRs touch on one scrollable page —
flag-off next to flag-on — so the experiment can be reviewed without
running the app or flipping GrowthBook.

- stories/share/Overview.stories.tsx: 11 sections covering the flag
  matrix, the ShareActions primitive (icon/inline/hover, the four
  button variant+size pairs actually used), both profile headers, the
  pinned mobile bar, the menu before/after, the icon swap and the copy
  inventory. Mobile-only branches are embedded as real 390px story
  frames rather than mocked.
- stories/share/shareStoryContext.tsx: the providers, mock profile and
  flag-on/flag-off scope, previously duplicated in both share story
  files, now defined once and reused by all three.
- stories/share/reviewLayout.tsx: presentational scaffolding for the
  page, themed off CSS variables so it follows the light/dark toggle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ShareActions trigger rendered CopyIcon; the copy-link affordance
everywhere else in the product is the chain-link glyph, so the profile
share control now matches it. Both branches of the trigger (desktop
popover, mobile native-share) swap together.

Flags a conflict in the review page: `share_copy_icon` swaps the same
glyph the other way (LinkIcon -> CopyIcon) on the feed card, brief
header and mobile share widget. That's a call to settle before either
ships.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Share and Edit sit side by side in the profile header card, so they now
share a variant. Float there read as two unrelated ghost icons.

- ProfileShareButton defaults to Subtle (the header card is its canonical
  placement).
- ProfileHeader's Edit button follows, but only when the share control is
  actually rendered — control users, who get no share button, keep the
  Float button that ships today, so flag-off stays identical to main.
- The pinned mobile bar overrides back to Float: there the control sits
  among Float icons (award, options) and a bordered button would read as
  a different kind of action.

Storybook: the review page's variant grid documents both pairings, and
its embedded 390px frames now follow the light/dark toggle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Medium (40px) icon buttons dominated the header card next to the Small
(32px) options menu below them. Share and Edit are both Small now.

ProfileShareButton defaults to Small, which is what the pinned mobile
bar already passed explicitly, so that override goes away. The Edit
button only shrinks when the share control renders — control users keep
today's Medium Float button.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The control row was spaced off the cover with mt-2, so where it landed
depended on the button height — at Medium it overshot the avatar's
bottom edge by 8px, and at Small it happened to line up by luck.

Anchor it instead: the cover is h-36 (9rem) and the avatar hangs to
11.5rem, so a fixed 2.5rem row with items-end sits the controls on the
avatar's bottom edge at any button height. Control keeps the centred
row that ships today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removes the gating so the copy-link control on profile headers is
permanent, per product decision — there is no ramp and no kill-switch
short of a revert.

Deleted `share_profile`, `sharing_visibility` and `share_copy_icon`
along with useShareProfileEnabled / useSharingVisibility /
useShareCopyIcon and their spec. Every gated path becomes the kept
behaviour:

- ProfileHeader always renders the share control; the invisible edit
  placeholder public profiles used to hold the row height is gone, and
  Edit is unconditionally Subtle + Small beside it.
- The pinned mobile bar shows the control whenever it is sticky, and
  both profile menus always pass hideShare.
- share_copy_icon's CopyIcon arm is the discarded path: the feed card
  action bar, brief header and mobile share widget keep the LinkIcon
  they ship today, which is also the glyph the new control uses. Those
  three files are back to main apart from a Copy link aria-label.

Also fixes the pre-existing strict-null errors the changed-file
typecheck guard flags in the two files this PR touches (user.username
is optional; feeds.edges may be undefined), which had CI red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Clicking the profile control opened a network popover — a second decision
before anything useful happened. It now copies straight away and confirms
twice: the glyph flips to a green check for a second (VIcon in
text-status-success, the same check the product uses elsewhere) and a
toast names what landed on the clipboard.

- ProfileShareButton drops ShareActions and drives useShareOrCopyLink
  directly, so it keeps the dly.to shortening, the ShareProfile referral
  campaign and the ShareProfile log event with its provider.
- Mobile is unchanged: where the platform offers a native share sheet a
  tap still opens it, and the check/toast only fire on the copy path.
- Labels follow the behaviour: "Copy link to your profile" /
  "Copy link to @user's profile", replacing the old "Share …".
- useShareOrCopyLink takes an optional copyMessage so the toast can name
  the profile instead of the generic "Copied link to clipboard".

ShareActions itself is untouched and still owned by PR 1 (#6343) for the
other surfaces in the initiative.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review pass: this PR was reaching into files PR 1 (#6343) owns, even
though none of them are needed for the profile control.

Restored to the base branch, unchanged by this PR:
- ShareActions.tsx — the profile control drives useShareOrCopyLink
  directly, so it has no reason to touch the primitive's glyph.
- ShareMobile / ActionButtons.v2 / BriefPostHeaderActions and
  useShareCopyIcon — the share_copy_icon experiment is PR 1's call about
  a different set of surfaces, not this PR's to unwind.
- useSharingVisibility and the sharing_visibility flag — this PR stops
  consuming the initiative-wide kill-switch, but deleting infrastructure
  later surfaces may still want belongs in PR 1.
- ShareActions.stories.tsx — reverted the decorator refactor.

The Storybook review page documents this PR's control instead of PR 1's
primitive: the variant/size grid now renders ProfileShareButton with the
two combinations actually used.

Net diff is 13 files, all profile-share.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tsahimatsliah tsahimatsliah changed the title feat(share): surface a copy-link control on profile headers feat(share): copy-link control on profile headers Jul 28, 2026
Verification pass on the tracking. The emitted event now has exact-match
assertions instead of objectContaining, on both the component and the
real header surface, so the payload can't drift away from what the
dashboards already match:

  event_name: share profile
  target_id:  <user id>
  target_type: profile page
  extra:      {"provider":"copy link"|"native","origin":"profile"}

Also covers the cancelled-native-share case, which must not log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tsahimatsliah
tsahimatsliah marked this pull request as ready for review July 28, 2026 12:36
@tsahimatsliah
tsahimatsliah merged commit 4579d30 into claude/website-sharing-visibility-be6b32 Jul 28, 2026
14 checks passed
@tsahimatsliah
tsahimatsliah deleted the claude/share-profile branch July 28, 2026 12:38
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