Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
53c0502
docs(snapshot): document post page share placements in Storybook
tomeredlich Aug 31, 2026
4111522
docs(snapshot): show the exported image on the post page
tomeredlich Aug 31, 2026
1a03b1f
feat(snapshot): put the selection snapshot on the post page
tomeredlich Aug 31, 2026
0d3b11f
docs(snapshot): restore the full post page page
tomeredlich Aug 31, 2026
c8a0bef
feat(snapshot): make every post page placement testable
tomeredlich Aug 31, 2026
b953a12
feat(snapshot): ship the post page share placements behind flags
tomeredlich Aug 31, 2026
2866cbc
style(snapshot): format the poll payload spec
tomeredlich Aug 31, 2026
bc4e3ea
feat(snapshot): open the placements on branch previews
tomeredlich Aug 31, 2026
67c2fe2
fix(snapshot): offer copy summary when ads segment the TLDR
tomeredlich Aug 31, 2026
7d23414
fix(snapshot): put the placements on the redesigned post page too
tomeredlich Aug 31, 2026
34b78b0
fix(snapshot): raise the selection bar in the post modal
tomeredlich Aug 31, 2026
d42d20f
feat(snapshot): lead the selection bar with Snapshot, and copy the li…
tomeredlich Sep 1, 2026
1b57671
feat(snapshot): match the poll card and the share prompts to the product
tomeredlich Sep 1, 2026
1c513d4
feat(share): put both prompts on the split copy-link band
tomeredlich Sep 1, 2026
f05db7c
fix(share): even the air around the prompt, and let it stay after a copy
tomeredlich Sep 1, 2026
412cbcd
fix(share): copy the tracked link everywhere, as the rest of the page…
tomeredlich Sep 1, 2026
c88f709
fix(share): stop the upvote prompt sharing an empty link
tomeredlich Sep 1, 2026
03a601d
refactor(snapshot): confirm a copy with the arrow, and cut the page t…
tomeredlich Sep 3, 2026
31b84ff
feat(share): confirm every copy with the arrow, not just the split bu…
tomeredlich Sep 3, 2026
51bb98b
fix(share): match the copy confirmation to #6570
tomeredlich Sep 3, 2026
913330f
fix(snapshot): keep the selection bar beside the text it belongs to
tomeredlich Sep 3, 2026
1b237f1
feat(share): copy the link from every post header
tomeredlich Sep 3, 2026
a6db62e
fix(share): give the header spec the contextMenuId it requires
tomeredlich Sep 3, 2026
98cc880
fix(share): hang the copy link off the menu, where every post type ha…
tomeredlich Sep 6, 2026
b198aa0
feat(share): copy any paragraph of a collection body
tomeredlich Sep 6, 2026
5aed479
feat(briefing): share controls on the briefing header and list rows
tomeredlich Sep 2, 2026
b506790
feat(briefing): share a highlight, a bullet, or the Must know section
tomeredlich Sep 2, 2026
f9e2e64
feat(briefing): confirm every copy on the control that was pressed
tomeredlich Sep 3, 2026
b4ea89a
chore(briefing): cut what the reviewer does not need
tomeredlich Sep 6, 2026
7e319da
fix(briefing): draw the Must know card the way the library draws it
tomeredlich Sep 6, 2026
f5ab917
feat(briefing): take #6544's current frame for the Must know card
tomeredlich Sep 8, 2026
46e5677
feat(snapshot): take #6544's card design for the saved image
tomeredlich Sep 8, 2026
a2f5201
fix(snapshot): do not credit the API's placeholder source
tomeredlich Sep 8, 2026
81a6521
Merge branch 'main' into snapshot-post-page-variations
tomeredlich Sep 9, 2026
0ab7a66
Merge branch 'snapshot-post-page-variations' into claude/snapshot-bri…
tomeredlich Sep 9, 2026
829c0c0
Merge branch 'main' into claude/snapshot-briefing-preview
idoshamun Sep 10, 2026
e78bb7e
fix(briefing): gate the list and header controls with useConditionalF…
idoshamun Sep 10, 2026
4e42418
fix(briefing): rebuild the in-body controls on main's snapshot placem…
idoshamun Sep 10, 2026
56fbf24
chore(storybook): drop the briefing surface mockup
idoshamun Sep 10, 2026
9127aeb
fix(briefing): keep contextMenuId off the header actions' DOM node
idoshamun Sep 10, 2026
1eebef2
feat(briefing): ship the share controls without a flag
idoshamun Sep 10, 2026
fe84bb2
Merge branch 'main' into claude/snapshot-briefing-preview
idoshamun Sep 10, 2026
2cd974f
fix(briefing): tighten the block and Must know snapshots
idoshamun Sep 10, 2026
3fb5112
Merge branch 'main' into claude/snapshot-briefing-preview
idoshamun Sep 10, 2026
d671f8a
fix(briefing): name a bullet's snapshot after its claim
idoshamun Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/shared/__tests__/fixture/brief.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* A brief's `contentHtml` as the API renders it: markdown-it over the TLDR and
* one `## title` plus a bullet list per section, each bullet closing with a
* link to the post it came from, or to a feed of them when there are several.
*/
export const briefContentHtml = `<p>The U.S. government has accused Chinese AI firms of industrial-scale model distillation, while Shopify has acquired Tailwind CSS to ensure its long-term stability.</p>
<h2>Must know</h2>
<ul>
<li><strong>US intelligence labels Chinese AI distillation a national security threat</strong>: A joint advisory accuses six Chinese firms of systematic distillation campaigns against U.S. frontier models. U.S. labs are now being advised to serve subtly degraded responses to suspected distillers to protect their model weights. <a href="https://daily.dev/posts/DMdgOqvwb" target="_blank" rel="noopener nofollow ugc">Read more</a></li>
<li><strong>Shopify acquires Tailwind Labs to anchor the CSS framework</strong>: Tailwind CSS creator Adam Wathan announced that the project and its parent company are joining Shopify to ensure long-term maintenance. <a href="https://daily.dev/posts/5wTLa8J7j" target="_blank" rel="noopener nofollow ugc">Read more</a></li>
<li><strong>Microsoft and Cisco hit by record AI discovered vulnerabilities</strong>: Microsoft released a massive patch batch this month, a surge researchers attribute to AI-assisted discovery tools. <a href="https://daily.dev/feed-by-ids?id=b8aoT91D7&amp;id=KZMXaFvNS" target="_blank" rel="noopener nofollow ugc">Read more</a></li>
</ul>
<h2>Good to know</h2>
<ul>
<li><strong>GitHub Copilot for JetBrains adds enterprise sandbox policies</strong>: Administrators can now centrally control filesystem and network access for Copilot, overriding local developer configs. <a href="https://daily.dev/posts/Qp8dGd3xK" target="_blank" rel="noopener nofollow ugc">Read more</a></li>
</ul>`;

/** The same brief when the briefing service sent no Must know section. */
export const briefContentHtmlWithoutMustKnow = briefContentHtml.replace(
/<h2>Must know<\/h2>\n<ul>[\s\S]*?<\/ul>\n/,
'',
);
39 changes: 32 additions & 7 deletions packages/shared/src/components/brief/BriefListItem.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BriefListItem } from './BriefListItem';
import type { Post } from '../../graphql/posts';
import { LogEvent, Origin, TargetId } from '../../lib/log';

const mockOnPostClick = jest.fn();
const mockLogEvent = jest.fn();
const mockCopyLink = jest.fn();
const mockOpenSharePost = jest.fn();

jest.mock('../../hooks/useOnPostClick', () => ({
__esModule: true,
Expand All @@ -20,6 +23,13 @@ jest.mock('../../hooks/usePlusSubscription', () => ({
usePlusSubscription: () => ({ isPlus: true }),
}));

jest.mock('../../hooks/useSharePost', () => ({
useSharePost: () => ({
copyLink: mockCopyLink,
openSharePost: mockOpenSharePost,
}),
}));

const post = {
id: 'brief-1',
slug: 'brief-1',
Expand All @@ -30,13 +40,15 @@ const post = {

const renderComponent = (onClick = jest.fn()) =>
render(
<BriefListItem
post={post}
title={post.title}
onClick={onClick}
origin={Origin.BriefPage}
targetId={TargetId.List}
/>,
<QueryClientProvider client={new QueryClient()}>
<BriefListItem
post={post}
title={post.title}
onClick={onClick}
origin={Origin.BriefPage}
targetId={TargetId.List}
/>
</QueryClientProvider>,
);

describe('BriefListItem', () => {
Expand Down Expand Up @@ -87,4 +99,17 @@ describe('BriefListItem', () => {
expect(mockOnPostClick).toHaveBeenCalledWith({ post });
expect(mockLogEvent).toHaveBeenCalledTimes(1);
});

it('copies and shares the brief link without opening the brief', () => {
const onClick = jest.fn();
renderComponent(onClick);

fireEvent.click(screen.getByRole('button', { name: 'Copy link' }));
fireEvent.click(screen.getByRole('button', { name: 'Share briefing' }));

expect(mockCopyLink).toHaveBeenCalledWith({ post });
expect(mockOpenSharePost).toHaveBeenCalledWith({ post });
expect(onClick).not.toHaveBeenCalled();
expect(mockOnPostClick).not.toHaveBeenCalled();
});
});
36 changes: 34 additions & 2 deletions packages/shared/src/components/brief/BriefListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import type { PillProps } from '../Pill';
import { Pill } from '../Pill';
import { IconSize } from '../Icon';
import { BriefGradientIcon, LockIcon } from '../icons';
import { LinkIcon } from '../icons/Link';
import { ShareIcon } from '../icons/Share';
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
import { Tooltip } from '../tooltip/Tooltip';
import type { Origin, TargetId } from '../../lib/log';
import { LogEvent } from '../../lib/log';
import useOnPostClick from '../../hooks/useOnPostClick';
Expand All @@ -22,6 +26,8 @@ import { anchorDefaultRel } from '../../lib/strings';
import Link from '../utilities/Link';
import { useLogContext } from '../../contexts/LogContext';
import { usePlusSubscription } from '../../hooks/usePlusSubscription';
import { useSharePost } from '../../hooks/useSharePost';
import { CopyStateIcon } from '../share/CopyStateIcon';

export type BriefListItemProps = {
className?: string;
Expand Down Expand Up @@ -55,6 +61,7 @@ export const BriefListItem = ({
const { isPlus } = usePlusSubscription();
const { logEvent } = useLogContext();
const onPostClick = useOnPostClick({ origin });
const { copyLink, isCopying, openSharePost } = useSharePost(origin);

const trackBriefClick = () => {
onPostClick({ post });
Expand Down Expand Up @@ -86,14 +93,17 @@ export const BriefListItem = ({
<div className="hidden items-center mobileXL:flex">
<BriefGradientIcon secondary={!isRead} size={IconSize.Size48} />
</div>
<div className="flex w-full flex-col gap-1">
<div className="flex items-center gap-2">
{/* `w-full` would claim the whole card and push the controls past its
border. */}
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex min-w-0 items-center gap-2">
<Typography
type={TypographyType.Title3}
bold
color={
isRead ? TypographyColor.Quaternary : TypographyColor.Primary
}
truncate
>
{title}
</Typography>
Expand Down Expand Up @@ -150,6 +160,28 @@ export const BriefListItem = ({
onAuxClick={(event) => event.button === 1 && trackBriefClick()}
/>
</Link>
{/* After the CardLink and above it: the overlay covers the whole row,
so anything rendered before it never receives the click. */}
<div className="relative z-1 flex shrink-0 items-center gap-1">
<Tooltip content={isCopying ? 'Copied!' : 'Copy link'}>
<Button
aria-label="Copy link"
icon={<CopyStateIcon copied={isCopying} icon={LinkIcon} />}
size={ButtonSize.Small}
variant={ButtonVariant.Tertiary}
onClick={() => copyLink({ post })}
/>
</Tooltip>
<Tooltip content="Share">
<Button
aria-label="Share briefing"
icon={<ShareIcon />}
size={ButtonSize.Small}
variant={ButtonVariant.Tertiary}
onClick={() => openSharePost({ post })}
/>
</Tooltip>
</div>
</article>
);
};
9 changes: 8 additions & 1 deletion packages/shared/src/components/imageShare/SnapshotButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export interface SnapshotButtonProps {
target: CaptureTarget;
filename?: string;
label?: string;
/**
* The name a screen reader announces, when the label alone does not say
* what this captures: a body with a control on every paragraph. The visible
* label and tooltip stay the label.
*/
ariaLabel?: string;
showLabel?: boolean;
size?: ButtonSize;
variant?: ButtonVariant;
Expand All @@ -46,6 +52,7 @@ export function SnapshotButton({
target,
filename = 'daily-snapshot',
label = SNAPSHOT_LABEL,
ariaLabel,
showLabel = true,
captureOptions,
onCapture,
Expand Down Expand Up @@ -122,7 +129,7 @@ export function SnapshotButton({
<Tooltip content={label} visible={!showLabel}>
<Button
type="button"
aria-label={label}
aria-label={ariaLabel ?? label}
className={classNames(
'relative shrink-0 overflow-hidden',
// A pseudo-element rather than a child: Button reads its children to
Expand Down
36 changes: 33 additions & 3 deletions packages/shared/src/components/post/brief/BriefPostContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import classNames from 'classnames';
import type { ReactElement } from 'react';
import React, { useMemo, useEffect, useState } from 'react';
import React, { useMemo, useEffect, useRef, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import {
Expand Down Expand Up @@ -45,7 +45,7 @@ import {
ButtonSize,
ButtonVariant,
} from '../../buttons/Button';
import { LogEvent, TargetId } from '../../../lib/log';
import { LogEvent, Origin, TargetId } from '../../../lib/log';
import { featurePlusCtaCopy } from '../../../lib/featureManagement';
import { LottieAnimation } from '../../LottieAnimation';
import { briefFeatureList, PlusList } from '../../plus/PlusList';
Expand All @@ -65,6 +65,15 @@ import { getFirstName } from '../../../lib/user';
import Link from '../../utilities/Link';
import { ActionType } from '../../../graphql/actions';
import { BriefUpgradeAlert } from '../../../features/briefing/components/BriefUpgradeAlert';
import { BriefShareBand } from '../../../features/briefing/components/BriefShareBand';
import { BriefMustKnowSnapshotButton } from '../../../features/briefing/components/BriefMustKnowSnapshotButton';
import {
BRIEF_BLOCK_SELECTOR,
BRIEF_SOURCE_LINK_SELECTOR,
getBriefBlockLabel,
} from '../../../features/briefing/briefBodyBlocks';
import { SelectionSnapshotBar } from '../../../features/snapshot/SelectionSnapshotBar';
import { ParagraphSnapshotButtons } from '../../../features/snapshot/ParagraphSnapshotButtons';
import type { BriefPostHeaderProps } from '../../../features/briefing/components/BriefPostHeader';
import { BriefPostHeader } from '../../../features/briefing/components/BriefPostHeader';
import type { NotificationChannel } from '../../../hooks/notifications/useNotificationSettings';
Expand Down Expand Up @@ -135,6 +144,7 @@ const BriefPostContentRaw = ({
unsubscribePersonalizedDigest,
} = usePersonalizedDigest();
const [digestTimeIndex, setDigestTimeIndex] = useState<number | undefined>(8);
const briefBodyRef = useRef<HTMLDivElement>(null);

const briefDigest = getPersonalizedDigest(UserPersonalizedDigestType.Brief);

Expand Down Expand Up @@ -390,7 +400,27 @@ const BriefPostContentRaw = ({
</Typography>
</div>
</div>
<Markdown content={contentHtml} />
<div ref={briefBodyRef}>
<Markdown content={contentHtml} />
</div>
<SelectionSnapshotBar
containerRef={briefBodyRef}
origin={Origin.BriefTextSelection}
post={post}
/>
<ParagraphSnapshotButtons
ariaLabel={getBriefBlockLabel}
containerRef={briefBodyRef}
omit={BRIEF_SOURCE_LINK_SELECTOR}
origin={Origin.BriefParagraph}
post={post}
selector={BRIEF_BLOCK_SELECTOR}
/>
<BriefMustKnowSnapshotButton
containerRef={briefBodyRef}
post={post}
/>
<BriefShareBand post={post} />
{isNotPlus && (
<div className="flex w-full rounded-12 border border-white bg-transparent">
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { fireEvent, render, screen } from '@testing-library/react';
import { BriefPostHeaderActions } from './BriefPostHeaderActions';
import type { Post } from '../../../graphql/posts';
import { Origin } from '../../../lib/log';

const mockCopyLink = jest.fn();
const mockOpenSharePost = jest.fn();

jest.mock('../../../hooks/useSharePost', () => ({
useSharePost: () => ({
copyLink: mockCopyLink,
openSharePost: mockOpenSharePost,
}),
}));

const post = { id: 'brief-1', slug: 'brief-1' } as Post;

const renderComponent = (showShareButton = true) =>
render(
<QueryClientProvider client={new QueryClient()}>
<BriefPostHeaderActions
showShareButton={showShareButton}
contextMenuId="post-widgets-context"
origin={Origin.BriefPage}
post={post}
/>
</QueryClientProvider>,
);

describe('BriefPostHeaderActions', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('shows share at every width and leaves the copy link to laptop', () => {
renderComponent();

expect(
screen.getByRole('button', { name: 'Share briefing' }),
).not.toHaveClass('hidden');
expect(screen.getByRole('button', { name: 'Copy link' })).toHaveClass(
'hidden',
'laptop:flex',
);
});

it('renders no share controls where the share button is off', () => {
renderComponent(false);

expect(
screen.queryByRole('button', { name: 'Copy link' }),
).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: 'Share briefing' }),
).not.toBeInTheDocument();
});

it('copies the brief link and opens the share modal', () => {
renderComponent();

fireEvent.click(screen.getByRole('button', { name: 'Copy link' }));
fireEvent.click(screen.getByRole('button', { name: 'Share briefing' }));

expect(mockCopyLink).toHaveBeenCalledWith({ post });
expect(mockOpenSharePost).toHaveBeenCalledWith({ post });
});
});
Loading
Loading