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
21 changes: 17 additions & 4 deletions packages/shared/src/components/ShareBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { useGetShortUrl } from '../hooks';
import { ReferralCampaignKey } from '../lib';
import { ProfileImageSize } from './ProfilePicture';
import { useAuthContext } from '../contexts/AuthContext';
import { useSharePostPage } from '../hooks/useSharePostPage';
import { ShareModuleHeader } from './post/share/ShareModuleHeader';

interface ShareBarProps {
post: Post;
Expand All @@ -41,6 +43,7 @@ export default function ShareBar({ post }: ShareBarProps): ReactElement {
const { openModal } = useLazyModal();
const { logOpts } = useContext(ActiveFeedContext);
const { squads } = useAuthContext();
const isSharePostPageEnabled = useSharePostPage();

const shareableSquadsCount = useMemo(
() => getShareableSquads(squads).length,
Expand Down Expand Up @@ -91,13 +94,23 @@ export default function ShareBar({ post }: ShareBarProps): ReactElement {

return (
<WidgetContainer className="hidden flex-col !border-0 p-3 laptop:flex">
<h4 className="mb-4 font-bold text-text-primary typo-callout">
Would you recommend this post?
</h4>
{isSharePostPageEnabled ? (
<ShareModuleHeader className="mb-4" />
) : (
<h4 className="mb-4 font-bold text-text-primary typo-callout">
Would you recommend this post?
</h4>
)}
<div className="grid grid-cols-4 gap-2 gap-y-4">
<SocialShareButton
size={ButtonSize.Medium}
variant={ButtonVariant.Tertiary}
// Copy link is the action that actually travels, so promote it above
// the network tiles once the redesigned module is on.
variant={
isSharePostPageEnabled
? ButtonVariant.Primary
: ButtonVariant.Tertiary
}
onClick={logAndCopyLink}
pressed={copying}
icon={
Expand Down
32 changes: 32 additions & 0 deletions packages/shared/src/components/ShareMobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import type { Post } from '../graphql/posts';
import { useLogContext } from '../contexts/LogContext';
import { useSharePost } from '../hooks/useSharePost';
import type { UsePostContent } from '../hooks/usePostContent';
import { useSharePostPage } from '../hooks/useSharePostPage';
import { ShareModuleHeader } from './post/share/ShareModuleHeader';

export interface ShareMobileProps {
post: Post;
Expand All @@ -37,6 +39,7 @@ export function ShareMobile({
const { logEvent } = useLogContext();
const { logOpts } = useContext(ActiveFeedContext);
const showCopyIcon = useShareCopyIcon();
const isSharePostPageEnabled = useSharePostPage();

const onShare = () => {
logEvent(
Expand All @@ -47,6 +50,35 @@ export function ShareMobile({
openSharePost({ post });
};

// Redesigned module: a reason to share up top, then two full-width buttons
// with a clear primary — instead of two same-weight coloured text links.
if (isSharePostPageEnabled) {
return (
<WidgetContainer className="flex flex-col items-start gap-2 p-3 laptop:hidden">
<ShareModuleHeader className="mb-1" />
<Button
className="w-full"
size={ButtonSize.Small}
onClick={onCopyPostLink}
pressed={copying}
icon={showCopyIcon ? <CopyIcon secondary={copying} /> : <LinkIcon />}
variant={ButtonVariant.Primary}
>
{copying ? 'Copied!' : 'Copy link'}
</Button>
<Button
className="w-full"
size={ButtonSize.Small}
onClick={onShare}
icon={<ShareIcon />}
variant={ButtonVariant.Secondary}
>
Share with your friends
</Button>
</WidgetContainer>
);
}

return (
<WidgetContainer className="flex flex-col items-start gap-2 p-3 laptop:hidden">
<Button
Expand Down
10 changes: 10 additions & 0 deletions packages/shared/src/components/post/PostContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { useSmartTitle } from '../../hooks/post/useSmartTitle';
import { PostTagList } from './tags/PostTagList';
import PostSourceInfo from './PostSourceInfo';
import { useReaderInstallPromptGate } from '../../hooks/useReaderInstallPromptGate';
import { useSharePostPage } from '../../hooks/useSharePostPage';
import { CopySummaryButton } from './share/CopySummaryButton';

type PostContentRawProps = Omit<PostContentProps, 'post'> & { post: Post };

Expand Down Expand Up @@ -103,6 +105,7 @@ export function PostContentRaw({
onReadArticle();
};
const showCodeSnippets = useFeature(feature.showCodeSnippets);
const isSharePostPageEnabled = useSharePostPage(!!post.summary);
const { title } = useSmartTitle(post);
const hasNavigation = !!onPreviousPost || !!onNextPost;
const isVideoType = isVideoPost(post);
Expand Down Expand Up @@ -199,6 +202,13 @@ export function PostContentRaw({
>
{post.summary}
</p>
{isSharePostPageEnabled && (
<CopySummaryButton
className="mt-3"
post={post}
summary={post.summary}
/>
)}
</div>
)}
<PostTagList post={post} />
Expand Down
11 changes: 11 additions & 0 deletions packages/shared/src/components/post/PostEngagements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AuthTriggers } from '../../lib/auth';
import type { NewCommentRef } from './NewComment';
import { NewComment } from './NewComment';
import { PostActions } from './PostActions';
import { usePostActions } from '../../hooks/post/usePostActions';
import { PostComments } from './PostComments';
import { PostUpvotesCommentsCount } from './PostUpvotesCommentsCount';
import type { Comment } from '../../graphql/comments';
Expand All @@ -32,6 +33,8 @@ import { usePlusSubscription } from '../../hooks/usePlusSubscription';
import SocialBar from '../cards/socials/SocialBar';
import { PostContentReminder } from './common/PostContentReminder';
import { useSettingsContext } from '../../contexts/SettingsContext';
import { useSharePostPage } from '../../hooks/useSharePostPage';
import { ShareWithTeamStrip } from './share/ShareWithTeamStrip';

const AuthorOnboarding = dynamic(
() => import(/* webpackChunkName: "authorOnboarding" */ './AuthorOnboarding'),
Expand Down Expand Up @@ -77,6 +80,8 @@ function PostEngagements({
false,
);
const [linkClicked, setLinkClicked] = useState(false);
const isSharePostPageEnabled = useSharePostPage();
const { interaction } = usePostActions({ post });

const handleLinkClick = () => {
setLinkClicked(true);
Expand Down Expand Up @@ -126,6 +131,12 @@ function PostEngagements({
/>
<PostContentReminder post={post} />
<PostContentShare post={post} />
{/* PostContentShare owns the engagement column right after an upvote, so
* the team strip stands down until that module is dismissed — otherwise
* two full-width share modules stack back-to-back. */}
{isSharePostPageEnabled && interaction !== 'upvote' && (
<ShareWithTeamStrip className="mt-6" post={post} />
)}
{linkClicked && <SocialBar post={post} className="mt-6" />}
<span className="mt-3 flex flex-row items-center">
<Typography type={TypographyType.Callout}>Sort:</Typography>
Expand Down
48 changes: 37 additions & 11 deletions packages/shared/src/components/post/focus/PostFocusCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ import { PostMenuOptions } from '../PostMenuOptions';
import { FocusCardActionBar } from './FocusCardActionBar';
import { PostDiscussionPanel } from './PostDiscussionPanel';
import { CollectionSources } from './CollectionSources';
import { useSharePostPage } from '../../../hooks/useSharePostPage';
import { CopySummaryButton } from '../share/CopySummaryButton';
import { ShareWithTeamStrip } from '../share/ShareWithTeamStrip';

const PostCodeSnippets = dynamic(() =>
import(/* webpackChunkName: "postCodeSnippets" */ '../PostCodeSnippets').then(
Expand Down Expand Up @@ -250,6 +253,7 @@ export const PostFocusCard = ({
const { isReaderEnabled } = useReaderModalEligibility();
const isReaderVariant = isReaderEnabled && post.type === PostType.Article;
const showCodeSnippets = useFeature(feature.showCodeSnippets);
const isSharePostPageEnabled = useSharePostPage();
const focusCommentRef = useRef<() => void>(() => {});
const discussionRef = useRef<HTMLDivElement>(null);
// The video is a small floating preview on tablet/desktop and expands to the
Expand Down Expand Up @@ -324,6 +328,36 @@ export const PostFocusCard = ({
</Button>
) : null;

// Freeform/welcome posts render `contentHtml` instead and never carry a
// summary, so the copy action is absent there by construction.
const renderSummary = (): ReactElement | null => {
if (!article.summary) {
return null;
}

const summary = isVideoType ? (
<VideoSummary summary={article.summary} />
) : (
<p
className="select-text break-words text-text-secondary typo-markdown"
data-testid="tldr-container"
>
{article.summary}
</p>
);

if (!isSharePostPageEnabled) {
return summary;
}

return (
<div className="flex flex-col items-start gap-3">
{summary}
<CopySummaryButton post={post} summary={article.summary} />
</div>
);
};

return (
<article
className="flex w-full flex-col rounded-24 bg-background-default"
Expand Down Expand Up @@ -524,17 +558,7 @@ export const PostFocusCard = ({
<ContentEmbeds embeds={article.contentEmbeds} variant="post" />
</>
) : (
article.summary &&
(isVideoType ? (
<VideoSummary summary={article.summary} />
) : (
<p
className="select-text break-words text-text-secondary typo-markdown"
data-testid="tldr-container"
>
{article.summary}
</p>
))
renderSummary()
)}

<PostTagList post={article} />
Expand Down Expand Up @@ -570,6 +594,8 @@ export const PostFocusCard = ({
className="-mt-2"
/>

{isSharePostPageEnabled && <ShareWithTeamStrip post={post} />}

<div ref={discussionRef} className="scroll-mt-16">
<PostDiscussionPanel
showMetaBar={false}
Expand Down
79 changes: 79 additions & 0 deletions packages/shared/src/components/post/share/CopySummaryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { ReactElement } from 'react';
import React from 'react';
import type { Post } from '../../../graphql/posts';
import { Button } from '../../buttons/Button';
import { ButtonSize, ButtonVariant } from '../../buttons/common';
import { Tooltip } from '../../tooltip/Tooltip';
import { CopyIcon } from '../../icons';
import { useCopyText } from '../../../hooks/useCopy';
import { useGetShortUrl } from '../../../hooks';
import { ReferralCampaignKey } from '../../../lib/referral';
import { useLogContext } from '../../../contexts/LogContext';
import { postLogEvent } from '../../../lib/feed';
import { LogEvent, Origin } from '../../../lib/log';
import { ShareProvider } from '../../../lib/share';

interface CopySummaryButtonProps {
post: Post;
/**
* The TL;DR text. Freeform/welcome posts have no summary, so the button is
* correctly absent there — pass whatever the surface rendered.
*/
summary?: string;
className?: string;
}

/**
* Secondary action at the end of the TL;DR block: copies the summary plus a
* short link back to the post, so pasting into a chat keeps the attribution.
* Deliberately a quiet tertiary button — "Read post" stays the primary CTA.
*/
export const CopySummaryButton = ({
post,
summary,
className,
}: CopySummaryButtonProps): ReactElement | null => {
const [copying, copyText] = useCopyText();
const { getShortUrl } = useGetShortUrl();
const { logEvent } = useLogContext();

if (!summary) {
return null;
}

const onCopySummary = async () => {
const shortLink = await getShortUrl(
post.commentsPermalink,
ReferralCampaignKey.SharePost,
);
await copyText({
textToCopy: `${summary}\n\n${shortLink}`,
message: '✅ Copied summary to clipboard',
});
logEvent(
postLogEvent(LogEvent.SharePost, post, {
extra: {
provider: ShareProvider.CopyLink,
origin: Origin.PostSummary,
},
}),
);
};

return (
<Tooltip content="Copies the TL;DR and a link back to the post">
<Button
aria-label="Copy summary"
className={className}
icon={<CopyIcon secondary={copying} />}
onClick={onCopySummary}
pressed={copying}
size={ButtonSize.Small}
type="button"
variant={ButtonVariant.Tertiary}
>
{copying ? 'Copied!' : 'Copy summary'}
</Button>
</Tooltip>
);
};
Loading
Loading