Skip to content
Draft
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
11 changes: 11 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "storybook",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["--filter", "storybook", "dev"],
"port": 6006
}
]
}
70 changes: 70 additions & 0 deletions packages/shared/src/components/CustomFeedOptionsMenu.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { ComponentProps } from 'react';
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { fireEvent, render, screen } from '@testing-library/react';
import CustomFeedOptionsMenu from './CustomFeedOptionsMenu';
import AuthContext from '../contexts/AuthContext';
import type { AuthContextData } from '../contexts/AuthContext';

jest.mock('../hooks', () => ({
...jest.requireActual('../hooks'),
useFeeds: () => ({ feeds: { edges: [] } }),
}));

const shareProps = {
text: "Check out Ido Shamun's profile on daily.dev",
link: 'https://app.daily.dev/idoshamun',
};

const renderMenu = (
props: Partial<ComponentProps<typeof CustomFeedOptionsMenu>> = {},
) => {
const client = new QueryClient({
defaultOptions: { queries: { retry: false, gcTime: 0 } },
});

return render(
<QueryClientProvider client={client}>
<AuthContext.Provider
value={
{
user: null,
isAuthReady: true,
tokenRefreshed: true,
squads: [],
} as unknown as AuthContextData
}
>
<CustomFeedOptionsMenu
onAdd={jest.fn()}
shareProps={shareProps}
{...props}
/>
</AuthContext.Provider>
</QueryClientProvider>,
);
};

describe('CustomFeedOptionsMenu', () => {
it('should list the share option by default', async () => {
renderMenu({ shareProps });

// Radix opens the menu on keydown; jsdom lacks the pointer-event support
// its click path relies on.
fireEvent.keyDown(screen.getByRole('button'), { key: 'Enter' });

expect(await screen.findByText('Share')).toBeInTheDocument();
expect(screen.getByText('Add to custom feed')).toBeInTheDocument();
});

it('should drop the share option when the surface promotes it elsewhere', async () => {
renderMenu({ shareProps, hideShare: true });

// Radix opens the menu on keydown; jsdom lacks the pointer-event support
// its click path relies on.
fireEvent.keyDown(screen.getByRole('button'), { key: 'Enter' });

expect(await screen.findByText('Add to custom feed')).toBeInTheDocument();
expect(screen.queryByText('Share')).not.toBeInTheDocument();
});
});
19 changes: 13 additions & 6 deletions packages/shared/src/components/CustomFeedOptionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type CustomFeedOptionsMenuProps = {
buttonVariant?: ButtonVariant;
shareProps: UseShareOrCopyLinkProps;
additionalOptions?: MenuItemProps[];
/** Drop the in-menu share entry when the surface renders a visible one. */
hideShare?: boolean;
};

const CustomFeedOptionsMenu = ({
Expand All @@ -38,13 +40,14 @@ const CustomFeedOptionsMenu = ({
onCreateNewFeed,
additionalOptions = [],
buttonVariant = ButtonVariant.Float,
hideShare = false,
}: CustomFeedOptionsMenuProps): ReactElement => {
const { openModal } = useLazyModal();
const [, onShareOrCopyLink] = useShareOrCopyLink(shareProps);
const { feeds } = useFeeds();

const handleOpenModal = () => {
if (feeds?.edges?.length > 0) {
if ((feeds?.edges?.length ?? 0) > 0) {
return openModal({
type: LazyModal.AddToCustomFeed,
props: {
Expand All @@ -59,11 +62,15 @@ const CustomFeedOptionsMenu = ({
};

const options: MenuItemProps[] = [
{
icon: <MenuIcon Icon={ShareIcon} />,
label: 'Share',
action: () => onShareOrCopyLink(),
},
...(hideShare
? []
: [
{
icon: <MenuIcon Icon={ShareIcon} />,
label: 'Share',
action: () => onShareOrCopyLink(),
},
]),
{
icon: <MenuIcon Icon={HashtagIcon} />,
label: 'Add to custom feed',
Expand Down
6 changes: 4 additions & 2 deletions packages/shared/src/components/ShareMobile.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { ReactElement } from 'react';
import React, { useContext } from 'react';
import { LinkIcon, ShareIcon } from './icons';
import { CopyIcon, LinkIcon, ShareIcon } from './icons';
import { useCopyPostLink } from '../hooks/useCopyPostLink';
import { useShareCopyIcon } from '../hooks/useShareCopyIcon';
import {
Button,
ButtonColor,
Expand Down Expand Up @@ -35,6 +36,7 @@ export function ShareMobile({
const { openSharePost } = useSharePost(origin);
const { logEvent } = useLogContext();
const { logOpts } = useContext(ActiveFeedContext);
const showCopyIcon = useShareCopyIcon();

const onShare = () => {
logEvent(
Expand All @@ -51,7 +53,7 @@ export function ShareMobile({
size={ButtonSize.Small}
onClick={onCopyPostLink}
pressed={copying}
icon={<LinkIcon />}
icon={showCopyIcon ? <CopyIcon secondary={copying} /> : <LinkIcon />}
variant={ButtonVariant.Tertiary}
color={ButtonColor.Avocado}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CardActionBar } from '../../buttons/CardActionBar';
import {
AnalyticsIcon,
DiscussIcon as CommentIcon,
CopyIcon,
LinkIcon,
DownvoteIcon,
} from '../../icons';
Expand All @@ -21,6 +22,7 @@ import { PostTagsPanel } from '../../post/block/PostTagsPanel';
import { LinkWithTooltip } from '../../tooltips/LinkWithTooltip';
import { useCardActions } from '../../../hooks/cards/useCardActions';
import { useBrandSponsorship } from '../../../hooks/useBrandSponsorship';
import { useShareCopyIcon } from '../../../hooks/useShareCopyIcon';
import { usePostImpressionsModal } from '../../../hooks/post/usePostImpressionsModal';
import { usePostImpressions } from '../../../hooks/post/usePostImpressions';

Expand Down Expand Up @@ -73,6 +75,7 @@ const ActionButtons = ({
}: ActionButtonsProps): ReactElement | null => {
const config = variantConfig[variant];
const isFeedPreview = useFeedPreviewMode();
const showCopyIcon = useShareCopyIcon();
// When impressions are enabled, awards are hidden below laptop (tablet +
// mobile) to make room for the extra action.
const isLaptop = useViewSize(ViewSize.Laptop);
Expand Down Expand Up @@ -227,7 +230,7 @@ const ActionButtons = ({
<CardAction
id="copy-post-btn"
density={FEED_CARD_DENSITY}
icon={<LinkIcon />}
icon={showCopyIcon ? <CopyIcon /> : <LinkIcon />}
label="Copy link"
onClick={onCopyLink}
color={ButtonColor.Cabbage}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import type { PostHeaderActionsProps } from '../common';
import Link from '../../utilities/Link';
import { Button, ButtonSize } from '../../buttons/Button';
import { settingsUrl } from '../../../lib/constants';
import { LinkIcon, SettingsIcon } from '../../icons';
import { CopyIcon, LinkIcon, SettingsIcon } from '../../icons';
import { useSharePost } from '../../../hooks/useSharePost';
import { useShareCopyIcon } from '../../../hooks/useShareCopyIcon';
import type { Origin } from '../../../lib/log';

const Container = classed('div', 'flex flex-row items-center');
Expand All @@ -27,15 +28,17 @@ export const BriefPostHeaderActions = ({
showShareButton?: boolean;
}): ReactElement => {
const { copyLink } = useSharePost(origin);
const showCopyIcon = useShareCopyIcon();

return (
<Container {...props} className={classNames('gap-2', className)}>
<div className="hidden laptop:block">
{showShareButton && (
<Button
icon={<LinkIcon />}
icon={showCopyIcon ? <CopyIcon /> : <LinkIcon />}
size={ButtonSize.Medium}
onClick={() => copyLink({ post })}
aria-label="Copy link"
/>
)}
<Link passHref href={`${settingsUrl}/notifications`}>
Expand Down
17 changes: 17 additions & 0 deletions packages/shared/src/components/profile/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
import Link from '../utilities/Link';
import type { MenuItemProps } from '../dropdown/common';
import { ProfileMobileBackButton } from './ProfileBackButton';
import { ProfileShareButton } from './ProfileShareButton';

export interface HeaderProps {
user: PublicProfile;
Expand Down Expand Up @@ -218,6 +219,20 @@ export function Header({
variant={ButtonVariant.Float}
/>
)}
{/* Only while pinned: unpinned, the profile card right below owns the
share control, and two identical copy buttons on one screen read as
a mistake. `ml-1` keeps this utility icon out of the Follow group. */}
{sticky && (
<ProfileShareButton
user={user}
isSameUser={isSameUser}
// Float, not the header card's Subtle: in this bar the control
// sits among Float icons (award, options) and a bordered button
// would read as a different kind of action.
buttonVariant={ButtonVariant.Float}
className="ml-1"
/>
)}
{!isSameUser && (
<CustomFeedOptionsMenu
onAdd={(feedId) =>
Expand All @@ -241,6 +256,8 @@ export function Header({
`/feeds/new?entityId=${user.id}&entityType=${ContentPreferenceType.User}`,
)
}
// Promoted out of the menu into the dedicated control above.
hideShare
shareProps={{
text: `Check out ${user.name}'s profile on daily.dev`,
link: user.permalink,
Expand Down
12 changes: 7 additions & 5 deletions packages/shared/src/components/profile/ProfileActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const ProfileActions = ({ user, isPreviewMode }: HeaderProps): ReactElement => {
props: {
offendingUser: {
id: user.id,
username: user.username,
username: user.username || '',
},
defaultBlockUser: defaultBlocked,
},
Expand All @@ -87,12 +87,12 @@ const ProfileActions = ({ user, isPreviewMode }: HeaderProps): ReactElement => {
? unblock({
id: user.id,
entity: ContentPreferenceType.User,
entityName: user.username,
entityName: user.username || '',
})
: block({
id: user.id,
entity: ContentPreferenceType.User,
entityName: user.username,
entityName: user.username || '',
}),
},
{
Expand Down Expand Up @@ -178,15 +178,15 @@ const ProfileActions = ({ user, isPreviewMode }: HeaderProps): ReactElement => {
follow({
id: user.id,
entity: ContentPreferenceType.User,
entityName: user.username,
entityName: user.username || '',
feedId,
})
}
onUndo={(feedId) =>
unfollow({
id: user.id,
entity: ContentPreferenceType.User,
entityName: user.username,
entityName: user.username || '',
feedId,
})
}
Expand All @@ -195,6 +195,8 @@ const ProfileActions = ({ user, isPreviewMode }: HeaderProps): ReactElement => {
`/feeds/new?entityId=${user.id}&entityType=${ContentPreferenceType.User}`,
)
}
// Promoted out of the menu into the header's share control.
hideShare
shareProps={{
text: `Check out ${user.name}'s profile on daily.dev`,
link: user.permalink,
Expand Down
Loading