Skip to content

feat(snapshot): snapshot an expanded highlight on Happening Now - #6570

Open
tomeredlich wants to merge 15 commits into
mainfrom
snapshot-happening-now
Open

feat(snapshot): snapshot an expanded highlight on Happening Now#6570
tomeredlich wants to merge 15 commits into
mainfrom
snapshot-happening-now

Conversation

@tomeredlich

@tomeredlich tomeredlich commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Share controls for Happening Now, built on the snapshot foundation from #6556 and the cards from #6544. There is no feature flag: every control below ships to everyone on merge.

What ships

  • Expanded highlight (HighlightItem): copy link and a Snapshot button sit opposite "Read more". Snapshot captures the TLDR as a 1080-wide HighlightTextSnapshotCard with a "Happening now" eyebrow on its logo row and the post's source credited under a rule, and puts the image on the clipboard (download as the fallback). The card mounts through useArmedCard on hover, touch or focus, never one per row at render. Collapsed rows are unchanged.
  • Quote bar over the TLDR: selecting part of an expanded TLDR opens main's selection bar (Snapshot, copy link, copy text). Its card carries the same "Happening now" eyebrow and source credit as the expanded-highlight card.
  • Page header: a copy link for the Happening Now page, right of the title. The title keeps its intrinsic width, so the animated wordmark gradient is not stretched.
  • Feed card (HighlightGrid/HighlightList): a copy link in the header for the page, and one per row for that highlight. Both appear on hover or keyboard focus, and take no taps while hidden. On touch they stay hidden; the page header and the expanded row cover touch.

Links

Every copy link puts an absolute URL on the clipboard and goes through useCopyLink's shorten path with a campaign, as PostMenuOptions does: the long link is written inside the click (Safari refuses a write after an awaited shortener), then the short link carrying userid and cid replaces it.

Control Copies cid
Expanded row, feed card row, quote bar the highlight's post permalink share_post
Page header, feed card header /highlights resolved against the current origin share_highlights (new)

One highlight, one link: the post. Before, a feed card row copied the highlights deep link (/highlights?highlight=<id>) while the expanded row copied the post, both logged as share post. The deep link is the weaker destination for whoever receives it:

  • /highlights is statically generated, so a crawler gets the same page for every highlight value: title "Highlights | daily.dev", the generic page description, the default daily.dev image, and og:url pointing at https://daily.dev/highlights. Checked against production with a Slackbot user agent.
  • The highlight param only expands a row that is still in the page's loaded list, so an older highlight lands on the plain page.
  • The post permalink previews with the headline as og:title, the TLDR as og:description and the post's own og:image, and it is what the share post event already names as the target.

The page links used webappUrl, which is a bare / on the webapp, so the clipboard got /highlights. They now resolve the path against location.origin the way agentShareLink does (in the extension webappUrl is already absolute and passes through unchanged), which also stops the tracked-link step from throwing on new URL('/highlights').

Where to look first

  1. features/snapshot/SelectionSnapshotBar.tsx: the bar's body is now SelectionShareBar (link, seed, source, label, share callback). SelectionSnapshotBar wraps it for posts with the same props and events as before, and the quote bar here uses the generic one. With snapshotSource's wider parameter (item 5), these are the only changes to feat(snapshot): share placements for the post page #6556 files.
  2. components/highlights/CopyHighlightsLink.tsx: the one copy control every surface uses, and the link and campaign choice above.
  3. features/snapshot/HighlightShareActions.tsx: the expanded row's controls and quote bar, the happening now eyebrow gradient, and the credit passed to both cards.
  4. features/snapshot/useLogHighlightShare.ts: the event shape below.
  5. graphql/highlights.ts: the feed fragment now asks for the post's source { name image } and domain, and the shared post's, since a share's TLDR comes from the shared post. snapshotSource takes any { name, image } source instead of the full Source, which is all it ever read.
  6. components/cards/highlight/common.tsx: the feed card header, where the copy link carries ml-auto so the options menu stays on the right.

Events

A highlight points at a post, so its shares are SharePost on that post (target_id = post id, target_type = post) with highlight_id in extra, the same shape as the post page's placements, and every one of them copies that post's link. The page link has no post, so it logs a new share highlights event and copies the page.

Action Event provider origin Extra Link
Expanded row: copy link share post copy link happening now highlight highlight_id post
Expanded row: Snapshot share post snapshot happening now highlight highlight_id, result
Quote bar: Snapshot share post snapshot happening now selection highlight_id, result
Quote bar: copy link share post copy link happening now selection highlight_id post
Quote bar: copy text share post copy text happening now selection highlight_id
Page header: copy link share highlights copy link happening now page
Feed card header: copy link share highlights copy link highlights card page
Feed card row: copy link share post copy link highlights card highlight_id post

result is clipboard, download or error. The post page's quote bar still logs origin: text selection, unchanged. Visits through a copied link carry cid=share_post or cid=share_highlights with the sharer's userid.

Experiment

None. The controls ship to everyone with no flag; the events above are how their use is read.

Testing

  • node ./scripts/typecheck-strict-changed.js origin/main: passed.
  • Full webapp tsc --noEmit: no errors outside the known __tests__ backlog.
  • ESLint on every changed file: clean.
  • pnpm --filter shared test: 408 suites, 2960 tests passed (the script exits 1 afterwards on the known log-after-teardown warning; NODE_ENV=test npx jest --runInBand --silent exits 0 with the same counts).
  • pnpm --filter webapp test: 86 suites, 689 tests passed.
  • New tests:
    • The feed card header copies http://localhost/highlights with webappUrl mocked to / (the webapp's value), and a row copies the post permalink without opening the highlight.
    • With a logged-in sharer and the shortener unreachable, the link swapped in after the click is absolute and carries cid (share_highlights for the page, share_post for a row) and the sharer's userid.
    • The expanded row copies the same post link and logs SharePost with the highlight's post and origin.
    • The armed TLDR card and the quote card both render the "Happening now" eyebrow and the source's name.
    • Carried over: a collapsed row offers nothing; an expanded row offers snapshot and copy link beside Read more; the card header's copy link logs share highlights. The existing SelectionSnapshotBar tests pass unchanged against the refactor.

To try it on the preview, visit /highlights, copy the header link and a row's link, and expand a headline to snapshot its TLDR or a quote from it.

Preview domain

https://snapshot-happening-now.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 Building Building Preview Sep 10, 2026 2:35pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
storybook Ignored Ignored Sep 10, 2026 2:35pm UTC

Request Review

@tomeredlich
tomeredlich force-pushed the snapshot-happening-now branch from e11ab2f to 220744a Compare September 2, 2026 15:07
@tomeredlich
tomeredlich force-pushed the snapshot-happening-now branch from 220744a to bbe1546 Compare September 3, 2026 09:30
@tomeredlich
tomeredlich force-pushed the snapshot-happening-now branch from e0216a8 to cc74e39 Compare September 3, 2026 13:09
@tomeredlich
tomeredlich force-pushed the snapshot-happening-now branch from cc74e39 to 1269d52 Compare September 3, 2026 13:52
@tomeredlich
tomeredlich marked this pull request as ready for review September 3, 2026 13:57
tomeredlich added a commit that referenced this pull request Sep 3, 2026
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.
tomeredlich added a commit that referenced this pull request Sep 3, 2026
#6570 extends CopyStateIcon for the same reason — a copy control that rests on
a link glyph — and lands on a green VIcon that fades in, with the resting
glyph passed as `icon`. This branch had an avocado UpvoteIcon spinning 360°
through a `copy-confirm` keyframe, under a prop called `idle`.

Both PRs touch the same file, so this takes #6570's component and spec
verbatim and renames the call sites. The keyframe goes with the spin: nothing
references it once the check lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every highlight is a self-contained claim with sources behind it, and
today none of them can be lifted out. The page also has the shortest
shelf life we publish, which is why the image beats the link: a URL sends
someone to a page that has already moved on.

Snapshot and copy link sit opposite Read more once a highlight is open.
Expansion is the intent signal, so a scan of the feed stays a scan.
Behind snapshot_highlight_expanded, off.

The card takes its timestamp from the formatter the row already renders,
so the two cannot disagree about how old the claim is, and it no longer
prints the channel, which is a slug rather than the tab's display name.

/dev/snapshot-happening-now renders the production row with the flag
pinned through the harness context, so the review surface exercises the
shipped component instead of restating its markup.

Mockup-to-eng-pass: 1
Snapshot on an expanded highlight now matches the copy link beside it —
tertiary and icon-only, so the footer reads as two peers rather than one
button and one afterthought.

Marking text inside a highlight's TLDR raises the same selection bar the
post page uses: snapshot the quote, copy it, or copy the link. The bar,
the card and the selection hook come over from the post page branch;
whichever lands second drops its copy.

Copy link joins three more places, each pointing at what the reader is
looking at: the feed card header on hover, a headline row on hover
(carrying that highlight's id), and the page header.

The flag becomes happening_now_share, since it no longer gates only the
expanded snapshot. Still off by default.

Mockup-to-eng-pass: 2
Every copy control on Happening Now swapped its glyph for nothing: the
toast was the only acknowledgement, and it lands in a corner away from
the press. The icon now crossfades to a green check and back after a
second, so the confirmation arrives where the eye already is.

CopyStateIcon comes from the split share button work, widened to take
the resting glyph so a link button can use it too. It ships there as
well; whichever lands second drops its copy.

Mockup-to-eng-pass: 3
The square held the copy to whatever fitted it: type stepped down as a
claim grew, so a long TLDR and a short one shipped as visibly different
cards, and a short one padded the rest out with dead gradient.

The frame now grows to fit and stops at 9:16, the card widens to 1008
with tighter padding, and the copy sets at one size throughout. The
Happening Now wordmark moves onto the logo row as an eyebrow, and the
headline comes off the card — the TLDR already says what it says, at
more length. The selection card loses its display quote marks and takes
the same treatment.

Capture height is measured at the press rather than assumed, since a
frame that grows only has one once it is mounted.

Ported from #6544. Its SnapshotButton rewrite is left there: this branch
keeps its own trigger.

Mockup-to-eng-pass: 4
Take the merged captureShareImage from #6426 with the width, height and
capture timeout the snapshot cards need. The designed cards carry their
own logo, so the capture options opt out of the frame's logo bar.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Main now owns the snapshot foundation this branch vendored (#6556 and
#6544), so every add/add conflict takes main's version: SnapshotButton,
SnapshotFrame, HighlightTextSnapshotCard, snapshotGradient, snapshotText,
useTextSelection, captureShareImage, copyShareImage and its spec.
CopyStateIcon, SnapshotCredit, SnapshotEyebrow, snapshotCapture, the icon
and the shutter CSS merged cleanly because they already matched main.

The highlight placement is ported onto main's APIs in the commits that
follow.
…hot parts

The branch carried its own copies of what #6556 and #6544 shipped: a
second selection bar, a second highlight card and a card-mounting button.
The placement now runs on main's versions.

- The expanded row's Snapshot mounts its card through useArmedCard, on
  hover, touch or focus, instead of one off-screen 1080px card per
  expanded highlight at render. The card is HighlightTextSnapshotCard
  with the "Happening now" eyebrow on its logo row, and the wordmark
  gradient lives next to it in shared (Storybook keeps its own copy).
- The quote bar over the TLDR is main's selection bar. Its body is
  SelectionShareBar now, taking the link, seed, source and a share
  callback, and SelectionSnapshotBar wraps it for posts, so the post page
  keeps its API and events unchanged.
- The clipboard gets the image only, as #6556 decided; the old button
  wrote the link beside it.
- Copy link on the expanded row goes through useCopyPostLink with
  `shorten`, like the post page's copy placements, so the write stays in
  the gesture and signed-in shares carry the referral campaign.
- Every action now logs `SharePost` on the highlight's post with
  `provider`, a distinct `origin` and the highlight id (plus `result` for
  a snapshot). Nothing was logged before. New origins: `happening now
  highlight` for the row and `happening now selection` for the quote bar.
- The flag is read with useConditionalFeature, evaluated only once a row
  is expanded.
…copy links

- With the flag off, the copy link rendered nothing but still owned the
  `ml-auto` that the feed card's options menu used to carry, so the menu
  slid left against the title for everyone. The menu keeps `ml-auto`
  unless the copy link is there to take it.
- The page title took `flex-1` to push the copy link right. Its wordmark
  gradient is sized to the element's box, so a full-width h1 showed only
  the blue end of it, again for everyone. The title keeps its width and
  the copy link takes `ml-auto`.
- The card's hover-revealed copy links were only transparent, so on touch
  screens they were invisible buttons that swallowed taps on the header
  and on each row's timestamp. They take pointer events only while the
  card or row is hovered; focus still reveals them for keyboard users.
- The flag is read once per card and once per page with
  useConditionalFeature, and a disabled placement renders nothing, so the
  card no longer needs a query client when the flag is off.
- Each copy now logs. A row's link is a share of that highlight's post
  (`SharePost` with the highlight id); the page link has no post, so it
  logs `ShareHighlights`. Origins: `highlights card` for the feed card,
  `happening now` for the page header.
- getHighlightsUrl moved to lib/links for the copy link; the card module
  no longer re-exports it and its one other caller imports it directly.
… forcing

#6556 took these out of the snapshot foundation and they came back with
this branch:

- useSharePlacement and isPreviewHost turned every share placement on
  for anyone on a `.preview.app.daily.dev` host, bypassing GrowthBook.
  The placements read `happening_now_share` with useConditionalFeature,
  and a preview is reviewed by opening the flag like any other.
- shutterSound.ts and public/sounds/shutter.mp3 had no caller left once
  SnapshotButton came from main.
- pages/dev/snapshot-happening-now was a review harness with stubbed
  auth, logging and flags; `/dev/*` pages do not ship.

The flag comment now lists every control it gates.
…hips

The surface story drew a fake Happening Now page to argue for the
expanded-row placement. The placement is real now, so the live page is
the reference, the same way #6556 retired the post page mockup. The
overview row points at this PR.
Product decided the sharing placements go out to everyone, so
happening_now_share is gone along with every read of it. The page
header's copy link, the feed card's copy links, and an expanded
highlight's copy link, snapshot and quote bar now always render.

The feed card's copy link carries ml-auto, so the options menu stays
pinned to the right with no conditional class, and the page h1 keeps its
intrinsic width so the wordmark gradient does not stretch.

The flag-off test is dropped. The card and row specs render inside
TestBootProvider, since the controls need the query client and log
context wherever the card or an expanded row mounts.
The page header link on /highlights, the feed card's header link and its
per-row links all built their URL from `webappUrl`, which is a bare `/` on
the webapp, so the clipboard got `/highlights` or
`/highlights?highlight=<id>`: a path that means nothing once pasted
anywhere else.

A row now copies the highlight's post permalink, the same link the expanded
row on /highlights already copied and the one both of them log as
`share post`. The deep link was the weaker target: /highlights is
statically generated, so a crawler gets the generic page title, the default
image and `og:url` pointing at /highlights whatever the query says, and the
`highlight` param only expands a row that is still in the loaded list. The
post page previews with the headline, the TLDR and the post's own image.

The page links resolve `/highlights` against the current origin the way
`agentShareLink` does, since the share pipeline runs `new URL(link)` and
threw on the bare path. Both go through `useCopyLink`'s `shorten` path with
a campaign, as PostMenuOptions does: the long link is written inside the
click, then the short link carrying `userid` and `cid` replaces it. The page
gets its own `share_highlights` campaign, added to the join page's campaign
map, which has to list every key.
The expanded-highlight card carried the "Happening now" eyebrow but no
credit, and the quote card over the TLDR carried neither, where the briefing
and post-page cards name the source under a rule. An image passed around
without a byline reads as daily.dev's own words.

The highlights feed fragment now asks for the post's `source` and `domain`
(and the shared post's, which is where a share's TLDR comes from), and both
cards take `snapshotSource` of the same post the TLDR is read from.
`snapshotSource` accepts any `{ name, image }` source rather than the full
`Source`, which is all it ever read. The quote bar passes the eyebrow through
to its card so both images carry the same label.

The expanded row's copy link is now the same control as the feed card's
rows, so one highlight is copied, tracked and logged the same way on every
surface.
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