diff --git a/packages/shared/src/components/history/ReadingHistory.spec.tsx b/packages/shared/src/components/history/ReadingHistory.spec.tsx
index d25c8bee114..c3dce291631 100644
--- a/packages/shared/src/components/history/ReadingHistory.spec.tsx
+++ b/packages/shared/src/components/history/ReadingHistory.spec.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { subDays } from 'date-fns';
import type { RenderResult } from '@testing-library/react';
-import { fireEvent, render, screen } from '@testing-library/react';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import nock from 'nock';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { PostItemCardProps } from '../post/PostItemCard';
@@ -15,6 +15,9 @@ import user from '../../../__tests__/fixture/loggedUser';
import { getLabel } from '../../lib/dateFormat.spec';
import post from '../../../__tests__/fixture/post';
import { SourceType } from '../../graphql/sources';
+import { TestBootProvider } from '../../../__tests__/helpers/boot';
+import { LogEvent, Origin } from '../../lib/log';
+import { ShareProvider } from '../../lib/share';
beforeEach(() => {
nock.cleanAll();
@@ -199,6 +202,42 @@ describe('PostItemCard component', () => {
);
});
+ it('should copy the post link and log it as a share from history', async () => {
+ const writeText = jest.fn().mockResolvedValue(undefined);
+ Object.assign(navigator, { clipboard: { writeText } });
+ const logEvent = jest.fn();
+ const onRowClick = jest.fn();
+
+ render(
+
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
+
+
+
+ ,
+ );
+
+ fireEvent.click(await screen.findByLabelText('Copy link'));
+
+ await waitFor(() =>
+ expect(writeText).toHaveBeenCalledWith(post.commentsPermalink),
+ );
+ expect(onRowClick).not.toHaveBeenCalled();
+ expect(logEvent).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event_name: LogEvent.SharePost,
+ target_id: post.id,
+ extra: expect.stringContaining(
+ `"provider":"${ShareProvider.CopyLink}","origin":"${Origin.History}"`,
+ ),
+ }),
+ );
+ });
+
it('should call onHide on close button clicked', async () => {
renderCard({ onHide });
const button = (await screen.findAllByRole('button'))[0];
diff --git a/packages/shared/src/components/history/ReadingHistoryList.tsx b/packages/shared/src/components/history/ReadingHistoryList.tsx
index cf17c826d9b..befe9b2b9f2 100644
--- a/packages/shared/src/components/history/ReadingHistoryList.tsx
+++ b/packages/shared/src/components/history/ReadingHistoryList.tsx
@@ -23,37 +23,43 @@ export default function ReadHistoryList({
let currentDate: Date;
return data?.pages.map((page, pageIndex) =>
- page.readHistory.edges.reduce((dom, { node: history }, edgeIndex) => {
- const { timestamp } = history;
- const date = new Date(timestamp);
+ page.readHistory.edges.reduce(
+ (dom, { node: history }, edgeIndex) => {
+ const { timestamp } = history;
+ // Optional only because PostItem is shared with surfaces that carry
+ // no timestamp; every reading-history edge has one.
+ const date = new Date(timestamp as Date);
+
+ if (!currentDate || !isDateOnlyEqual(currentDate, date)) {
+ currentDate = date;
+ dom.push(
+ ,
+ );
+ }
+
+ const indexes = { page: pageIndex, edge: edgeIndex };
- if (!currentDate || !isDateOnlyEqual(currentDate, date)) {
- currentDate = date;
dom.push(
- onHide({ ...params, ...indexes })}
+ showVoteActions
+ showCopyLink
+ logOrigin={Origin.History}
/>,
);
- }
-
- const indexes = { page: pageIndex, edge: edgeIndex };
-
- dom.push(
- onHide({ ...params, ...indexes })}
- showVoteActions
- logOrigin={Origin.History}
- />,
- );
- return dom;
- }, []),
+ return dom;
+ },
+ [],
+ ),
);
// @NOTE see https://dailydotdev.atlassian.net/l/cp/dK9h1zoM
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/packages/shared/src/components/imageShare/SnapshotButton.tsx b/packages/shared/src/components/imageShare/SnapshotButton.tsx
index 40a15690dec..a178eec68e5 100644
--- a/packages/shared/src/components/imageShare/SnapshotButton.tsx
+++ b/packages/shared/src/components/imageShare/SnapshotButton.tsx
@@ -73,6 +73,9 @@ export function SnapshotButton({
// Every placement sits inside a clickable card, row or link.
event.preventDefault();
event.stopPropagation();
+ if (isCapturing) {
+ return;
+ }
setIsFlashing(true);
flashTimeout.current = setTimeout(
() => setIsFlashing(false),
@@ -115,7 +118,15 @@ export function SnapshotButton({
setIsCapturing(false);
}
},
- [captureOptions, displayToast, filename, onCapture, onResult, target],
+ [
+ captureOptions,
+ displayToast,
+ filename,
+ isCapturing,
+ onCapture,
+ onResult,
+ target,
+ ],
);
return (
@@ -133,7 +144,6 @@ export function SnapshotButton({
size={size}
variant={variant}
loading={isCapturing}
- disabled={isCapturing}
icon={}
onClick={onSnapshot}
>
diff --git a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.spec.tsx b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.spec.tsx
index 50d99767be4..0f0f9bef6d9 100644
--- a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.spec.tsx
+++ b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.spec.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { act, fireEvent, render, screen } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { HotTake } from '../../../graphql/user/userHotTake';
import { useDiscoverHotTakes } from '../../../hooks/useDiscoverHotTakes';
import { useVoteHotTake } from '../../../hooks/vote/useVoteHotTake';
@@ -48,11 +49,13 @@ const createHotTake = (id = 'take-1'): HotTake => ({
const renderComponent = (onRequestClose = jest.fn()) => {
render(
- ,
+
+
+ ,
);
return { onRequestClose };
@@ -253,6 +256,53 @@ describe('HotAndColdModal', () => {
expect(onRequestClose).toHaveBeenCalledTimes(1);
});
+ it('should offer a snapshot on the top card only', () => {
+ mockedUseDiscoverHotTakes.mockReturnValue({
+ hotTakes: [createHotTake('top'), createHotTake('behind')],
+ currentTake: createHotTake('top'),
+ nextTake: createHotTake('behind'),
+ isEmpty: false,
+ isLoading: false,
+ dismissCurrent,
+ });
+
+ renderComponent();
+
+ // The card behind is rendered too, and a second control would capture a
+ // take the reader has not reached yet.
+ expect(screen.getAllByLabelText('Snapshot')).toHaveLength(1);
+ });
+
+ it('should not swipe the card when a drag starts on the snapshot button', () => {
+ const currentTake = createHotTake('snapshot-drag');
+ mockedUseDiscoverHotTakes.mockReturnValue({
+ hotTakes: [currentTake],
+ currentTake,
+ nextTake: null,
+ isEmpty: false,
+ isLoading: false,
+ dismissCurrent,
+ });
+
+ renderComponent();
+
+ const swipeRight = (from: Element) =>
+ act(() => {
+ fireEvent.touchStart(from, { touches: [{ clientX: 0, clientY: 0 }] });
+ fireEvent.touchMove(from, { touches: [{ clientX: 200, clientY: 0 }] });
+ fireEvent.touchEnd(from, { touches: [] });
+ });
+
+ swipeRight(screen.getByLabelText('Snapshot'));
+ expect(toggleUpvote).not.toHaveBeenCalled();
+
+ swipeRight(screen.getByText(currentTake.title));
+ expect(toggleUpvote).toHaveBeenCalledWith({
+ payload: currentTake,
+ origin: Origin.HotAndCold,
+ });
+ });
+
it('should keep subtitle visible even when title is very long', () => {
const currentTake = {
...createHotTake('long-text'),
diff --git a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx
index 51f9d8628af..f6768c04a5c 100644
--- a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx
+++ b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx
@@ -26,6 +26,7 @@ import {
TypographyColor,
} from '../../typography/Typography';
import { ProfilePicture, ProfileImageSize } from '../../ProfilePicture';
+import { HotTakeSnapshotButton } from '../../../features/snapshot/HotTakeSnapshotButton';
import { ReputationUserBadge } from '../../ReputationUserBadge';
import { VerifiedCompanyUserBadge } from '../../VerifiedCompanyUserBadge';
import { PlusUserBadge } from '../../PlusUserBadge';
@@ -1324,18 +1325,27 @@ const HotTakeCard = ({
)}
- {hotTake.upvotes > 0 && (
-
-
-
- {hotTake.upvotes}
-
-
- )}
+
+ {hotTake.upvotes > 0 && (
+
+
+
+ {hotTake.upvotes}
+
+
+ )}
+ {isTop && (
+
+ )}
+
{hotTake.user && (
@@ -1716,6 +1726,7 @@ const HotAndColdModal = ({
const dismissTimerRef = useRef | null>(null);
const [skipDelta, setSkipDelta] = useState(0);
const swipeDeltaYRef = useRef(0);
+ const swipeStartedOnButtonRef = useRef(false);
const [internalDismissedCardIds, setInternalDismissedCardIds] = useState<
Set
>(() => new Set());
@@ -2182,8 +2193,12 @@ const HotAndColdModal = ({
};
const handlers = useSwipeable({
+ onTouchStartOrOnMouseDown: ({ event }) => {
+ swipeStartedOnButtonRef.current =
+ event.target instanceof Element && !!event.target.closest('button');
+ },
onSwiping: (e) => {
- if (!isAnimating) {
+ if (!isAnimating && !swipeStartedOnButtonRef.current) {
if (isOnboardingMode && e.event.cancelable) {
e.event.preventDefault();
}
diff --git a/packages/shared/src/components/post/PostItemCard.tsx b/packages/shared/src/components/post/PostItemCard.tsx
index 4a49c05a0ed..0f4955847be 100644
--- a/packages/shared/src/components/post/PostItemCard.tsx
+++ b/packages/shared/src/components/post/PostItemCard.tsx
@@ -6,6 +6,7 @@ import type { HidePostItemCardProps } from '../../graphql/users';
import type { PostItem } from '../../graphql/posts';
import { UserVote, isVideoPost } from '../../graphql/posts';
import { MiniCloseIcon as XIcon, UpvoteIcon, DownvoteIcon } from '../icons';
+import { LinkIcon } from '../icons/Link';
import classed from '../../lib/classed';
import PostMetadata from '../cards/common/PostMetadata';
import { ProfileImageSize, ProfilePicture } from '../ProfilePicture';
@@ -13,7 +14,7 @@ import { Image } from '../image/Image';
import ConditionalWrapper from '../ConditionalWrapper';
import { cloudinaryPostImageCoverPlaceholder } from '../../lib/image';
import { useReadHistoryVotePost } from '../../hooks';
-import { Origin } from '../../lib/log';
+import { LogEvent, Origin } from '../../lib/log';
import {
Button,
ButtonColor,
@@ -24,6 +25,13 @@ import { isSourceUserSource } from '../../graphql/sources';
import { ReadingHistoryOptionsMenu } from '../history/ReadingHistoryOptionsMenu';
import type { QueryIndexes } from '../../hooks/useReadingHistory';
+import { useCopyPostLink } from '../../hooks/useCopyPostLink';
+import { CopyStateIcon } from '../share/CopyStateIcon';
+import { Tooltip } from '../tooltip/Tooltip';
+import { useLogContext } from '../../contexts/LogContext';
+import { postLogEvent } from '../../lib/feed';
+import { ReferralCampaignKey } from '../../lib/referral';
+import { ShareProvider } from '../../lib/share';
export interface PostItemCardProps {
className?: string;
@@ -32,6 +40,7 @@ export interface PostItemCardProps {
clickable?: boolean;
onHide?: (params: HidePostItemCardProps) => Promise;
showVoteActions?: boolean;
+ showCopyLink?: boolean;
logOrigin?: Origin;
indexes?: QueryIndexes;
}
@@ -48,6 +57,7 @@ export default function PostItemCard({
onHide,
className,
showVoteActions = false,
+ showCopyLink = false,
logOrigin = Origin.Feed,
indexes,
}: PostItemCardProps): ReactElement {
@@ -66,6 +76,23 @@ export default function PostItemCard({
const isUserSource = isSourceUserSource(source);
const { toggleUpvote, toggleDownvote } = useReadHistoryVotePost();
+ const [linkCopied, copyLink] = useCopyPostLink();
+ const { logEvent } = useLogContext();
+
+ const onCopyLink = (e: MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ logEvent(
+ postLogEvent(LogEvent.SharePost, post, {
+ extra: { provider: ShareProvider.CopyLink, origin: logOrigin },
+ }),
+ );
+ copyLink({
+ link: post.commentsPermalink,
+ shorten: true,
+ cid: ReferralCampaignKey.SharePost,
+ });
+ };
const classes = classNames(
'relative flex w-full flex-row py-3 pl-9 pr-5',
@@ -185,6 +212,17 @@ export default function PostItemCard({
onClick={onHideClick}
/>
)}
+ {showButtons && showCopyLink && (
+
+ }
+ onClick={onCopyLink}
+ />
+
+ )}
{showButtons && (
void;
onDelete?: (item: HotTake) => void;
@@ -31,6 +36,7 @@ interface HotTakeItemProps {
function HotTakeItemV1({
item,
+ author,
isOwner,
onEdit,
onDelete,
@@ -93,6 +99,13 @@ function HotTakeItemV1({
)}
)}
+
{onUpvoteClick && (
void;
onDelete?: (item: HotTake) => void;
@@ -27,6 +32,7 @@ interface HotTakeItemProps {
export function HotTakeItem({
item,
+ author,
isOwner,
onEdit,
onDelete,
@@ -89,6 +95,13 @@ export function HotTakeItem({
)}
)}
+
{onUpvoteClick && (
{
expect(screen.getByText('Hot take 1')).toBeVisible();
});
+ it('offers a snapshot on every hot take, visitors included', () => {
+ mockHotTakes({
+ hotTakes: [createHotTake(1), createHotTake(2)],
+ isOwner: false,
+ });
+
+ renderProfileUserHotTakes();
+
+ expect(screen.getAllByLabelText('Snapshot')).toHaveLength(2);
+ });
+
+ it("credits the profile's owner on a hot take's snapshot", () => {
+ mockHotTakes({ hotTakes: [createHotTake(1)], isOwner: false });
+
+ renderProfileUserHotTakes();
+ fireEvent.pointerEnter(screen.getByLabelText('Snapshot'));
+
+ // The list is fetched without a user on each take.
+ expect(screen.getByText(user.name)).toBeInTheDocument();
+ });
+
it('renders the hot takes anchor while loading for visitors', () => {
mockHotTakes({
isOwner: false,
diff --git a/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx b/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx
index a3b7a676a32..1a8985ded40 100644
--- a/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx
+++ b/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx
@@ -247,6 +247,7 @@ export function ProfileUserHotTakes({
{hotTakes.map((item) => (
({
+ captureShareImage: jest.fn(),
+}));
+jest.mock('../../lib/imageShare/copyShareImage', () => ({
+ copyShareImage: jest.fn(),
+}));
+
+const hotTake: HotTake = {
+ id: 'take-1',
+ emoji: '🔥',
+ title: 'Tabs won',
+ subtitle: 'Prettier just hid the bodies',
+ position: 0,
+ createdAt: '2026-09-01T00:00:00.000Z',
+ upvotes: 12,
+};
+
+const logEvent = jest.fn();
+
+const renderButton = (take = hotTake) =>
+ render(
+
+
+ ,
+ );
+
+const cardCopies = () =>
+ screen.queryAllByText('Tabs won Prettier just hid the bodies').length;
+
+beforeEach(() => {
+ jest.clearAllMocks();
+ jest
+ .mocked(captureShareImage)
+ .mockResolvedValue(new Blob(['png'], { type: 'image/png' }));
+ jest.mocked(copyShareImage).mockResolvedValue(true);
+});
+
+describe('HotTakeSnapshotButton', () => {
+ it('keeps the card out of the page until the reader reaches for it', () => {
+ renderButton();
+
+ // Every take in a profile list carries a button, and each card is 1080px.
+ expect(cardCopies()).toBe(0);
+
+ fireEvent.pointerEnter(screen.getByLabelText('Snapshot'));
+
+ expect(cardCopies()).toBe(1);
+ });
+
+ it('credits the author so a shared take is not read as the sharer', () => {
+ renderButton({
+ ...hotTake,
+ user: {
+ id: 'user-1',
+ name: 'Ada Lovelace',
+ username: 'ada',
+ image: 'https://media.daily.dev/ada.png',
+ createdAt: '2026-01-01T00:00:00.000Z',
+ reputation: 10,
+ permalink: 'https://app.daily.dev/ada',
+ },
+ });
+ fireEvent.pointerEnter(screen.getByLabelText('Snapshot'));
+
+ expect(screen.getByText('Ada Lovelace')).toBeInTheDocument();
+ expect(document.querySelector('img')).toHaveAttribute(
+ 'src',
+ 'https://media.daily.dev/ada.png',
+ );
+ });
+
+ it('leaves the credit off a take without an author', () => {
+ renderButton();
+ fireEvent.pointerEnter(screen.getByLabelText('Snapshot'));
+
+ expect(cardCopies()).toBe(1);
+ expect(document.querySelector('img')).not.toBeInTheDocument();
+ });
+
+ it('logs the snapshot as a hot take share with its placement', async () => {
+ renderButton();
+ const button = screen.getByLabelText('Snapshot');
+ fireEvent.pointerEnter(button);
+ fireEvent.click(button);
+
+ await waitFor(() =>
+ expect(logEvent).toHaveBeenCalledWith({
+ event_name: LogEvent.ShareHotTake,
+ target_id: hotTake.id,
+ extra: JSON.stringify({
+ provider: ShareProvider.Snapshot,
+ origin: Origin.HotTakeList,
+ result: 'clipboard',
+ }),
+ }),
+ );
+ });
+
+ it('stays filled while capturing and ignores a second press', async () => {
+ jest.mocked(copyShareImage).mockReturnValue(new Promise(() => {}));
+ renderButton();
+ const button = screen.getByLabelText('Snapshot');
+ fireEvent.pointerEnter(button);
+ fireEvent.click(button);
+
+ // A disabled button paints the Primary fill grey and the spinner with it.
+ await waitFor(() => expect(button).toHaveAttribute('aria-busy', 'true'));
+ expect(button).toBeEnabled();
+
+ fireEvent.click(button);
+ expect(captureShareImage).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/shared/src/features/snapshot/HotTakeSnapshotButton.tsx b/packages/shared/src/features/snapshot/HotTakeSnapshotButton.tsx
new file mode 100644
index 00000000000..50a9bf507d0
--- /dev/null
+++ b/packages/shared/src/features/snapshot/HotTakeSnapshotButton.tsx
@@ -0,0 +1,93 @@
+import type { ReactElement } from 'react';
+import React, { useCallback, useRef } from 'react';
+import { createPortal } from 'react-dom';
+import type {
+ ButtonSize,
+ ButtonVariant,
+} from '../../components/buttons/common';
+import type { SnapshotResult } from '../../components/imageShare/SnapshotButton';
+import { SnapshotButton } from '../../components/imageShare/SnapshotButton';
+import { useLogContext } from '../../contexts/LogContext';
+import type { HotTake } from '../../graphql/user/userHotTake';
+import type { Origin } from '../../lib/log';
+import { LogEvent } from '../../lib/log';
+import { ShareProvider } from '../../lib/share';
+import type { SnapshotCreditProps } from './SnapshotCredit';
+import { HotTakeSnapshotCard } from './HotTakeSnapshotCard';
+import { getSnapshotCaptureOptions } from './snapshotCapture';
+import { useArmedCard } from './useArmedCard';
+
+/**
+ * A hot take is a self-contained opinion with nowhere to link to, so the card
+ * is the whole share. It is portalled to the body: the swipe card it sits on
+ * is transformed while it moves, which would carry a fixed child with it.
+ */
+export function HotTakeSnapshotButton({
+ author,
+ hotTake,
+ origin,
+ showLabel,
+ size,
+ variant,
+}: {
+ /**
+ * Credited on the card. Defaults to the take's own user; a profile's list
+ * fetches its takes without one, since the profile already names them.
+ */
+ author?: SnapshotCreditProps;
+ hotTake: HotTake;
+ /** Which placement this is, for the snapshot's share event. */
+ origin: Origin;
+ showLabel?: boolean;
+ size?: ButtonSize;
+ variant?: ButtonVariant;
+}): ReactElement {
+ const cardRef = useRef(null);
+ const { isArmed, armProps } = useArmedCard();
+ const { logEvent } = useLogContext();
+
+ const onResult = useCallback(
+ (result: SnapshotResult) =>
+ logEvent({
+ event_name: LogEvent.ShareHotTake,
+ target_id: hotTake.id,
+ extra: JSON.stringify({
+ provider: ShareProvider.Snapshot,
+ origin,
+ result,
+ }),
+ }),
+ [hotTake.id, logEvent, origin],
+ );
+
+ return (
+ <>
+
+ getSnapshotCaptureOptions(cardRef.current)}
+ filename={`hot-take-${hotTake.id}`}
+ onResult={onResult}
+ showLabel={showLabel}
+ size={size}
+ target={cardRef}
+ variant={variant}
+ />
+
+ {isArmed &&
+ typeof document !== 'undefined' &&
+ createPortal(
+
+
+
,
+ document.body,
+ )}
+ >
+ );
+}
diff --git a/packages/shared/src/features/snapshot/HotTakeSnapshotCard.tsx b/packages/shared/src/features/snapshot/HotTakeSnapshotCard.tsx
new file mode 100644
index 00000000000..a5c96b9cee2
--- /dev/null
+++ b/packages/shared/src/features/snapshot/HotTakeSnapshotCard.tsx
@@ -0,0 +1,71 @@
+import type { ReactElement } from 'react';
+import React, { forwardRef } from 'react';
+import type { HotTake } from '../../graphql/user/userHotTake';
+import colors from '../../styles/colors';
+import type { SnapshotCreditProps } from './SnapshotCredit';
+import { SnapshotCredit } from './SnapshotCredit';
+import { SnapshotEyebrow } from './SnapshotEyebrow';
+import { SnapshotFrame } from './SnapshotFrame';
+
+const MUTED = colors.salt['90'];
+
+/** Fire, for a take that ran hot: yellow core through orange into red. */
+export const HOT_TAKE_EYEBROW_GRADIENT = `linear-gradient(100deg, ${colors.cheese['40']} 0%, ${colors.ketchup['10']} 48%, ${colors.ketchup['50']} 100%)`;
+
+interface HotTakeSnapshotCardProps {
+ take: HotTake;
+ /**
+ * Who wrote the take. Anyone can share it, so without the credit the image
+ * reads as the sharer's own opinion.
+ */
+ author?: SnapshotCreditProps;
+}
+
+/**
+ * The take is the whole payload, so the card carries the opinion rather than
+ * the row it was read in, and the upvote count reads as agreement rather than
+ * a score. Title and subtitle are set as one statement: split across two type
+ * styles they read as two voices arguing the same point.
+ */
+function HotTakeSnapshotCardComponent(
+ { take, author }: HotTakeSnapshotCardProps,
+ ref: React.Ref,
+): ReactElement {
+ return (
+
+ }
+ ref={ref}
+ seed={take.id}
+ watermark={take.emoji}
+ >
+