Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
f6d9fbe
feat: add Pinterest-style post discovery mockup
tsahimatsliah Jun 2, 2026
7508344
feat: render discovery experience on the real post page behind flag
tsahimatsliah Jun 2, 2026
5d62e66
fix: show post discovery preview by default
tsahimatsliah Jun 2, 2026
a26f1f7
fix: align discovery post layout with platform reading surface
tsahimatsliah Jun 2, 2026
bbc7988
fix: remove duplicate discovery read action
tsahimatsliah Jun 2, 2026
bdce1ca
test: keep classic post page tests on classic layout
tsahimatsliah Jun 2, 2026
5611539
fix: force discovery feed to render as card grid
tsahimatsliah Jun 2, 2026
cd76374
fix: reuse production post actions in discovery layout
tsahimatsliah Jun 2, 2026
6b6ac31
fix: let discovery feed use full page width
tsahimatsliah Jun 2, 2026
5e02970
fix: reuse production post details in discovery layout
tsahimatsliah Jun 2, 2026
7788b4f
fix: use reader source strip in discovery hero
tsahimatsliah Jun 2, 2026
b97f254
fix: render discovery feed across available page width
tsahimatsliah Jun 2, 2026
edb1437
fix: restore production post details column structure
tsahimatsliah Jun 2, 2026
f83318d
fix: polish discovery discussion sidebar card
tsahimatsliah Jun 2, 2026
74dcd4d
fix: render more-like-this as three-column card grid
tsahimatsliah Jun 2, 2026
d49c6cf
fix: tighten discovery actions and comment rail spacing
tsahimatsliah Jun 2, 2026
41eeabe
fix: place action bar at top of discussion card
tsahimatsliah Jun 2, 2026
368a480
fix: remove discovery post column separator
tsahimatsliah Jun 2, 2026
d3e492d
fix: simplify discussion card actions and sharing
tsahimatsliah Jun 2, 2026
a9122fd
fix: remove duplicate discovery stats row
tsahimatsliah Jun 2, 2026
b65975b
fix: let related discovery feed use main feed columns
tsahimatsliah Jun 2, 2026
94cae4c
fix: align discovery header action icon sizes
tsahimatsliah Jun 2, 2026
3826f85
fix: make discovery toc compact and collapsible
tsahimatsliah Jun 2, 2026
ff0cb4c
fix: align keep-exploring feed spacing
tsahimatsliah Jun 2, 2026
e6677df
fix: polish discovery post details and video layout
tsahimatsliah Jun 2, 2026
6e5a6f1
fix: refine discovery left column and discussion composer
tsahimatsliah Jun 2, 2026
82f9df6
fix: set discovery columns to 768px content and 340px discussion
tsahimatsliah Jun 2, 2026
59b4482
feat: move discovery stats strip to left column above action bar
tsahimatsliah Jun 2, 2026
2393af8
fix: make discovery left content 768px inside column padding
tsahimatsliah Jun 2, 2026
1bb35a9
feat: redesign discovery composer and share section
tsahimatsliah Jun 2, 2026
eaafc74
feat: medium-style header order for discovery left column
tsahimatsliah Jun 2, 2026
66aa313
fix: drop separator borders around discovery comment composer
tsahimatsliah Jun 2, 2026
572dce6
fix: simplify discovery composer footer (drop markdown hint + send icon)
tsahimatsliah Jun 2, 2026
b5cd095
fix: remove top gap above first comment in discovery panel
tsahimatsliah Jun 2, 2026
ed0137c
fix: match share row icons to comment action bar style
tsahimatsliah Jun 2, 2026
ba87fbb
fix: move source strip above title, metadata below media in discovery
tsahimatsliah Jun 2, 2026
368f700
fix: replace action bar box border with bottom separator in discovery
tsahimatsliah Jun 2, 2026
477df20
fix: move clickbait shield to right of engagement strip in discovery
tsahimatsliah Jun 2, 2026
91757fe
feat: center content column with ghost spacer opposite comments rail
tsahimatsliah Jun 2, 2026
d8f9c83
feat: redesign discovery action bar with counts, icon-only utilities,…
tsahimatsliah Jun 2, 2026
dc8ec92
feat: anchor follow button to source name and show full read button i…
tsahimatsliah Jun 2, 2026
49fad96
style: increase spacing and button size in discovery action bar
tsahimatsliah Jun 2, 2026
01d3998
style: enlarge action bar icons in discovery
tsahimatsliah Jun 2, 2026
48ad616
style: subtle follow button and primary read button in discovery
tsahimatsliah Jun 2, 2026
0de36c0
fix: align discovery feed gutter and gap to home feed layout
tsahimatsliah Jun 2, 2026
ae3f2b3
feat: icon-only clickbait shield and reordered discovery action bar u…
tsahimatsliah Jun 2, 2026
997b251
style: add vertical padding around discovery post title
tsahimatsliah Jun 2, 2026
a9a099d
fix: center post details and comments together in discovery feed area
tsahimatsliah Jun 2, 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
30 changes: 20 additions & 10 deletions packages/shared/src/components/ShareBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,24 @@ import { useAuthContext } from '../contexts/AuthContext';

interface ShareBarProps {
post: Post;
visibleRows?: number;
}

const visibleRows = 2;
const columns = 4;
const fixedOptions = 4;
const maxVisibleOptions = visibleRows * columns;
const maxVisibleSquadsWhenCollapsed = maxVisibleOptions - fixedOptions;

export default function ShareBar({ post }: ShareBarProps): ReactElement {
export default function ShareBar({
post,
visibleRows = 2,
}: ShareBarProps): ReactElement {
const [isExpanded, setIsExpanded] = useState(false);
const maxVisibleOptions = visibleRows * columns;
const maxVisibleSquadsWhenCollapsed = Math.max(
maxVisibleOptions - fixedOptions,
0,
);
const shouldShowSquadOptions =
isExpanded || maxVisibleSquadsWhenCollapsed > 0;
const href = post.commentsPermalink;
const cid = ReferralCampaignKey.SharePost;
const { getShortUrl } = useGetShortUrl();
Expand Down Expand Up @@ -130,12 +138,14 @@ export default function ShareBar({ post }: ShareBarProps): ReactElement {
onClick={() => onClick(ShareProvider.Twitter)}
label="X"
/>
<SquadsToShare
size={ButtonSize.Medium}
squadAvatarSize={ProfileImageSize.Large}
maxItems={isExpanded ? undefined : maxVisibleSquadsWhenCollapsed}
onClick={(_, squad) => onShareToSquad(squad)}
/>
{shouldShowSquadOptions && (
<SquadsToShare
size={ButtonSize.Medium}
squadAvatarSize={ProfileImageSize.Large}
maxItems={isExpanded ? undefined : maxVisibleSquadsWhenCollapsed}
onClick={(_, squad) => onShareToSquad(squad)}
/>
)}
</div>
{shouldShowToggle && (
<Button
Expand Down
14 changes: 12 additions & 2 deletions packages/shared/src/components/post/PostComments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ interface PostCommentsProps {
onClickUpvote?: (commentId: string, upvotes: number) => unknown;
className?: CommentClassName;
onCommented?: MainCommentProps['onCommented'];
/**
* Drop the list's top margin. Use when comments are the first element in
* their container (e.g. the discovery discussion panel) so they don't get an
* extra gap above the first item.
*/
removeTopSpacing?: boolean;
}

const noopShare = (): void => {};
Expand All @@ -61,6 +67,7 @@ export function PostComments({
joinNotificationCommentId,
className = {},
onCommented,
removeTopSpacing = false,
}: PostCommentsProps): ReactElement {
const { id } = post;
const container = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -119,9 +126,12 @@ export function PostComments({
isModalThread
? classNames(
'mb-12 flex flex-col gap-4',
isComposerOpen ? 'mt-2' : 'mt-5',
!removeTopSpacing && (isComposerOpen ? 'mt-2' : 'mt-5'),
)
: classNames(
'-mx-4 mb-12 flex flex-col gap-4 mobileL:mx-0',
!removeTopSpacing && 'mt-6',
)
: '-mx-4 mb-12 mt-6 flex flex-col gap-4 mobileL:mx-0'
}
ref={container}
>
Expand Down
16 changes: 10 additions & 6 deletions packages/shared/src/components/post/PostHeaderActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export function PostHeaderActions({
isFixedNavigation,
buttonSize,
hideSubscribeAction,
hideMenuOptions,
readButtonVariant,
...props
}: PostHeaderActionsProps): ReactElement {
const { openNewTab } = useContext(SettingsContext);
Expand Down Expand Up @@ -110,7 +112,7 @@ export function PostHeaderActions({
variant={
isFixedNavigation || isMobile
? ButtonVariant.Tertiary
: ButtonVariant.Secondary
: readButtonVariant ?? ButtonVariant.Secondary
}
tag="a"
href={readHref}
Expand All @@ -133,11 +135,13 @@ export function PostHeaderActions({
{isCollection && !hideSubscribeAction && (
<CollectionSubscribeButton post={post} />
)}
<PostMenuOptions
post={post}
origin={Origin.ArticleModal}
buttonSize={buttonSize}
/>
{!hideMenuOptions && (
<PostMenuOptions
post={post}
origin={Origin.ArticleModal}
buttonSize={buttonSize}
/>
)}
</Container>
);
}
6 changes: 5 additions & 1 deletion packages/shared/src/components/post/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {
UsePostContent,
UsePostContentProps,
} from '../../hooks/usePostContent';
import type { ButtonSize } from '../buttons/common';
import type { ButtonSize, ButtonVariant } from '../buttons/common';

export interface PostContentClassName {
container?: string;
Expand Down Expand Up @@ -69,6 +69,10 @@ export interface PostHeaderActionsProps {
isFixedNavigation?: boolean;
buttonSize?: ButtonSize;
hideSubscribeAction?: boolean;
/** Hides the trailing "..." menu (e.g. when it is surfaced elsewhere). */
hideMenuOptions?: boolean;
/** Overrides the read button variant on desktop (defaults to Secondary). */
readButtonVariant?: ButtonVariant;
}

export interface PostContentProps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '../../../hooks';
import { useLazyModal } from '../../../hooks/useLazyModal';
import { LazyModal } from '../../modals/common/types';
import { IconSize } from '../../Icon';

import { useSmartTitle } from '../../../hooks/post/useSmartTitle';
import type { Post } from '../../../graphql/posts';
Expand All @@ -23,7 +24,13 @@ import { Typography, TypographyType } from '../../typography/Typography';
import { PostUpgradeToPlus } from '../../plus/PostUpgradeToPlus';
import { TargetId } from '../../../lib/log';

export const PostClickbaitShield = ({ post }: { post: Post }): ReactElement => {
export const PostClickbaitShield = ({
post,
iconOnly = false,
}: {
post: Post;
iconOnly?: boolean;
}): ReactElement => {
const { openModal } = useLazyModal();
const { isPlus } = usePlusSubscription();
const { fetchSmartTitle, fetchedSmartTitle, shieldActive } =
Expand All @@ -33,6 +40,78 @@ export const PostClickbaitShield = ({ post }: { post: Post }): ReactElement => {
const { user } = useAuthContext();
const { hasUsedFreeTrial, triesLeft } = useClickbaitTries();

if (iconOnly) {
const isActive = isPlus ? shieldActive : fetchedSmartTitle;
const handleIconClick = async () => {
if (isPlus || !hasUsedFreeTrial) {
await fetchSmartTitle();
return;
}

if (isMobile) {
openModal({ type: LazyModal.ClickbaitShield });
return;
}

if (!user) {
throw new Error(
'PostClickbaitShield requires an authenticated user to edit feed settings',
);
}

router.push(
`${webappUrl}feeds/${user.id}/edit?dview=${FeedSettingsMenu.AI}`,
);
};

const tooltipContent = (() => {
if (isActive) {
return 'Click to see the original title';
}
return isPlus
? 'Click to see the optimized title'
: 'Optimize this title with Clickbait Shield';
})();

const renderIcon = () => {
if (isActive) {
return (
<ShieldCheckIcon
className="text-status-success"
size={IconSize.Large}
/>
);
}
if (isPlus) {
return <ShieldIcon size={IconSize.Large} />;
}
return (
<ShieldWarningIcon
className={
hasUsedFreeTrial
? 'text-accent-ketchup-default'
: 'text-accent-cheese-default'
}
size={IconSize.Large}
/>
);
};

return (
<Tooltip content={tooltipContent}>
<Button
aria-label="Clickbait Shield"
icon={renderIcon()}
iconSecondaryOnHover
onClick={handleIconClick}
size={ButtonSize.Medium}
type="button"
variant={ButtonVariant.Tertiary}
/>
</Tooltip>
);
}

if (!isPlus) {
return (
<>
Expand Down
106 changes: 106 additions & 0 deletions packages/shared/src/components/post/discovery/DiscussionMetaBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { ReactElement, ReactNode } from 'react';
import React from 'react';
import classNames from 'classnames';
import type { Post } from '../../../graphql/posts';
import { useAuthContext } from '../../../contexts/AuthContext';
import { useUpvoteQuery } from '../../../hooks/useUpvoteQuery';
import { useSettingsContext } from '../../../contexts/SettingsContext';
import { Button, ButtonSize, ButtonVariant } from '../../buttons/Button';
import { TimeSortIcon } from '../../icons/Sort/Time';
import { AnalyticsIcon } from '../../icons';
import { SortCommentsBy } from '../../../graphql/comments';
import { ClickableText } from '../../buttons/ClickableText';
import Link from '../../utilities/Link';
import { largeNumberFormat } from '../../../lib';
import { canViewPostAnalytics } from '../../../lib/user';
import { webappUrl } from '../../../lib/constants';

interface DiscussionMetaBarProps {
post: Post;
className?: string;
/** Optional content rendered on the right edge of the strip (e.g. clickbait shield). */
rightSlot?: ReactNode;
}

/**
* Post stats + comment sort strip. Lives in the left content column above the
* action bar; the sort toggle updates the shared settings context, so the
* discussion panel re-sorts its comments accordingly.
*/
export const DiscussionMetaBar = ({
post,
className,
rightSlot,
}: DiscussionMetaBarProps): ReactElement => {
const { user } = useAuthContext();
const { onShowUpvoted } = useUpvoteQuery();
const { sortCommentsBy: sortBy, updateSortCommentsBy: setSortBy } =
useSettingsContext();
const upvotes = post.numUpvotes || 0;
const comments = post.numComments || 0;
const canSeeAnalytics = canViewPostAnalytics({ user, post });
const isNewestFirst = sortBy === SortCommentsBy.NewestFirst;
const sortLabel = isNewestFirst ? 'Sort: Newest first' : 'Sort: Oldest first';

return (
<div
className={classNames(
'flex min-w-0 flex-wrap items-center justify-between gap-x-4 gap-y-2 text-text-tertiary typo-callout',
className,
)}
>
<div className="flex min-w-0 flex-wrap items-center gap-x-4">
{upvotes > 0 && (
<ClickableText onClick={() => onShowUpvoted(post.id, upvotes)}>
{largeNumberFormat(upvotes)} Upvote{upvotes > 1 ? 's' : ''}
</ClickableText>
)}
<span>
{largeNumberFormat(comments)} Comment{comments === 1 ? '' : 's'}
</span>
{canSeeAnalytics && (
<Link
href={`${webappUrl}posts/${post.id}/analytics`}
passHref
prefetch={false}
>
<ClickableText
tag="a"
className="gap-1"
textClassName="text-text-tertiary"
>
<AnalyticsIcon />
Analytics
</ClickableText>
</Link>
)}
<Button
type="button"
size={ButtonSize.XSmall}
variant={ButtonVariant.Tertiary}
icon={
<TimeSortIcon
secondary
className={isNewestFirst ? undefined : 'rotate-180'}
/>
}
onClick={() =>
setSortBy(
isNewestFirst
? SortCommentsBy.OldestFirst
: SortCommentsBy.NewestFirst,
)
}
aria-label={sortLabel}
title={sortLabel}
className="!text-text-tertiary"
/>
</div>
{rightSlot && (
<div className="flex shrink-0 items-center [&_.mr-2]:!mr-0 [&_.mt-4]:!mt-0 [&_.mt-6]:!mt-0">
{rightSlot}
</div>
)}
</div>
);
};
Loading
Loading