Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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: 21 additions & 1 deletion packages/shared/src/components/history/ReadingHistory.spec.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -199,6 +199,26 @@ describe('PostItemCard component', () => {
);
});

it('should copy the post link and confirm on the button itself', async () => {
const writeText = jest.fn().mockResolvedValue(undefined);
Object.assign(navigator, { clipboard: { writeText } });

renderCard({ showCopyLink: true });

fireEvent.click(await screen.findByLabelText('Copy link'));

await waitFor(() =>
expect(writeText).toHaveBeenCalledWith(post.commentsPermalink),
);
await screen.findByLabelText('Link copied');
});

it('should not render the copy link button by default', async () => {
renderCard();
await screen.findByText(postTitle);
expect(screen.queryByLabelText('Copy link')).not.toBeInTheDocument();
});

it('should call onHide on close button clicked', async () => {
renderCard({ onHide });
const button = (await screen.findAllByRole('button'))[0];
Expand Down
58 changes: 32 additions & 26 deletions packages/shared/src/components/history/ReadingHistoryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReactElement[]>(
(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(
<DateFormat
key={date.toISOString()}
date={date}
type={TimeFormatType.ReadHistory}
className="my-3 px-6 text-text-tertiary typo-body first:mt-0"
/>,
);
}

const indexes = { page: pageIndex, edge: edgeIndex };

if (!currentDate || !isDateOnlyEqual(currentDate, date)) {
currentDate = date;
dom.push(
<DateFormat
key={date.toISOString()}
date={date}
type={TimeFormatType.ReadHistory}
className="my-3 px-6 text-text-tertiary typo-body first:mt-0"
<PostItemCard
key={`${history.post.id}-${timestamp}`}
postItem={history}
indexes={indexes}
onHide={(params) => onHide({ ...params, ...indexes })}
showVoteActions
showCopyLink
logOrigin={Origin.History}
/>,
);
}

const indexes = { page: pageIndex, edge: edgeIndex };

dom.push(
<PostItemCard
key={`${history.post.id}-${timestamp}`}
postItem={history}
indexes={indexes}
onHide={(params) => 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
Expand Down
13 changes: 13 additions & 0 deletions packages/shared/src/components/icons/Snapshot/filled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions packages/shared/src/components/icons/Snapshot/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ReactElement } from 'react';
import React from 'react';
import type { IconProps } from '../../Icon';
import Icon from '../../Icon';
import OutlinedIcon from './outlined.svg';
import FilledIcon from './filled.svg';

export const SnapshotIcon = (props: IconProps): ReactElement => (
<Icon {...props} IconPrimary={OutlinedIcon} IconSecondary={FilledIcon} />
);
11 changes: 11 additions & 0 deletions packages/shared/src/components/icons/Snapshot/outlined.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/shared/src/components/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export * from './Shortcuts';
export * from './Sidebar';
export * from './Sites';
export * from './Slack';
export * from './Snapshot';
export * from './Sort';
export * from './Source';
export * from './Sparkle';
Expand Down
18 changes: 18 additions & 0 deletions packages/shared/src/components/post/PostItemCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ 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';

export interface PostItemCardProps {
className?: string;
Expand All @@ -32,6 +34,7 @@ export interface PostItemCardProps {
clickable?: boolean;
onHide?: (params: HidePostItemCardProps) => Promise<unknown>;
showVoteActions?: boolean;
showCopyLink?: boolean;
logOrigin?: Origin;
indexes?: QueryIndexes;
}
Expand All @@ -48,6 +51,7 @@ export default function PostItemCard({
onHide,
className,
showVoteActions = false,
showCopyLink = false,
logOrigin = Origin.Feed,
indexes,
}: PostItemCardProps): ReactElement {
Expand All @@ -66,6 +70,7 @@ export default function PostItemCard({
const isUserSource = isSourceUserSource(source);

const { toggleUpvote, toggleDownvote } = useReadHistoryVotePost();
const [copying, copyLink] = useCopyPostLink(post.commentsPermalink);

const classes = classNames(
'relative flex w-full flex-row py-3 pl-9 pr-5',
Expand Down Expand Up @@ -185,6 +190,19 @@ export default function PostItemCard({
onClick={onHideClick}
/>
)}
{showButtons && showCopyLink && (
<Button
size={ButtonSize.Small}
variant={ButtonVariant.Tertiary}
aria-label={copying ? 'Link copied' : 'Copy link'}
icon={<CopyStateIcon copied={copying} />}
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
copyLink();
}}
/>
)}
{showButtons && (
<ReadingHistoryOptionsMenu
post={post}
Expand Down
46 changes: 46 additions & 0 deletions packages/shared/src/components/share/CopyStateIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { ReactElement } from 'react';
import React from 'react';
import classNames from 'classnames';
import { CopyIcon, VIcon } from '../icons';
import type { IconProps } from '../Icon';

/**
* easeOutExpo — the curve the design-system dropdown animates on. It
* decelerates into the target with no overshoot, which is what keeps a swap
* from reading as a wobble.
*/
export const EASE_OUT_EXPO = 'ease-[cubic-bezier(0.16,1,0.3,1)]';

/**
* Both glyphs share one grid cell so nothing beside them shifts mid-swap, and
* the transition collapses to an instant swap under `prefers-reduced-motion`.
*/
export const CopyStateIcon = ({
copied,
className,
...props
}: IconProps & { copied: boolean }): ReactElement => {
const layer = classNames(
className,
'col-start-1 row-start-1 transition-[opacity,transform,filter] duration-200 motion-reduce:transition-none',
EASE_OUT_EXPO,
);

return (
<span className="inline-grid">
<CopyIcon
{...props}
className={classNames(layer, copied && 'scale-50 opacity-0 blur-[2px]')}
/>
<VIcon
{...props}
secondary
className={classNames(
layer,
'text-status-success',
!copied && 'scale-50 opacity-0 blur-[2px]',
)}
/>
</span>
);
};
Loading
Loading