diff --git a/packages/shared/src/components/InteractionCounter.tsx b/packages/shared/src/components/InteractionCounter.tsx
index 84831e54f02..ed0c3e3e1e7 100644
--- a/packages/shared/src/components/InteractionCounter.tsx
+++ b/packages/shared/src/components/InteractionCounter.tsx
@@ -7,20 +7,23 @@ import { largeNumberFormat } from '../lib';
export type InteractionCounterProps = {
className?: string;
value: number | null;
+ /** Override the number formatter (defaults to `largeNumberFormat`). */
+ format?: (value: number | null) => string | null;
};
export default function InteractionCounter({
className,
value,
+ format = largeNumberFormat,
...props
}: InteractionCounterProps): ReactElement {
const [shownValue, setShownValue] = useState(value);
const [animate, setAnimate] = useState(false);
useEffect(() => {
- const formattedValue = largeNumberFormat(value);
- const formattedShownValue = largeNumberFormat(shownValue);
+ const formattedValue = format(value);
+ const formattedShownValue = format(shownValue);
if (formattedValue !== formattedShownValue) {
- if (value < shownValue) {
+ if ((value ?? 0) < (shownValue ?? 0)) {
setShownValue(value);
} else {
setAnimate(false);
@@ -37,9 +40,15 @@ export default function InteractionCounter({
);
if (shownValue === value) {
+ // Center the number within the fixed-height (h-5) box. Without this the text
+ // is top-aligned, so smaller type (e.g. typo-caption1 on mobile/tablet, whose
+ // 1rem line-height is shorter than h-5) sits visibly higher than the icon.
return (
-
- {largeNumberFormat(shownValue)}
+
+ {format(shownValue)}
);
}
@@ -49,8 +58,10 @@ export default function InteractionCounter({
setShownValue(value);
};
+ // leading-5 makes each rolling slice's line box fill its h-5 height so the
+ // digits stay centered during the roll (matches the resting state above).
const childClassName =
- 'h-5 inline-block transition-[opacity,transform] ease-in-out duration-300 will-change-[opacity,transform]';
+ 'h-5 leading-5 inline-block transition-[opacity,transform] ease-in-out duration-300 will-change-[opacity,transform]';
return (
@@ -60,7 +71,7 @@ export default function InteractionCounter({
animate ? '-translate-y-full opacity-0' : 'translate-y-0 opacity-100',
)}
>
- {largeNumberFormat(shownValue)}
+ {format(shownValue)}
- {largeNumberFormat(value)}
+ {format(value)}
);
diff --git a/packages/shared/src/components/buttons/CardAction.tsx b/packages/shared/src/components/buttons/CardAction.tsx
index f5e64df8b7d..a3e97475eb7 100644
--- a/packages/shared/src/components/buttons/CardAction.tsx
+++ b/packages/shared/src/components/buttons/CardAction.tsx
@@ -46,6 +46,8 @@ type CardActionBaseProps = CardActionPassthroughProps & {
iconPressed?: IconElement;
label: string;
count?: number | null;
+ /** Override the counter formatter (defaults to `largeNumberFormat`). */
+ countFormat?: (value: number | null) => string | null;
color?: ColorName;
pressed?: boolean;
loading?: boolean;
@@ -64,6 +66,7 @@ function CardActionComponent(
iconPressed,
label,
count,
+ countFormat,
color,
pressed,
loading,
@@ -121,7 +124,9 @@ function CardActionComponent(
{showLabel && (
{label}
)}
- {showCount && }
+ {showCount && (
+
+ )}
)}
diff --git a/packages/shared/src/components/buttons/QuaternaryButton.tsx b/packages/shared/src/components/buttons/QuaternaryButton.tsx
index 84aa5c2c179..32a846d19ff 100644
--- a/packages/shared/src/components/buttons/QuaternaryButton.tsx
+++ b/packages/shared/src/components/buttons/QuaternaryButton.tsx
@@ -80,7 +80,9 @@ function QuaternaryButtonComponent(
htmlFor={id}
{...labelProps}
className={classNames(
- 'flex cursor-pointer items-center pl-1 font-bold typo-callout',
+ // Medium weight to match the button typography guideline (the
+ // Button base and CardAction both use font-medium).
+ 'flex cursor-pointer items-center pl-1 font-medium typo-callout',
{ readOnly: props.disabled },
labelClassName,
)}
diff --git a/packages/shared/src/components/cards/article/ArticleFeaturedWideGridCard.tsx b/packages/shared/src/components/cards/article/ArticleFeaturedWideGridCard.tsx
index 8c9b93c65b1..74a10ce9d5e 100644
--- a/packages/shared/src/components/cards/article/ArticleFeaturedWideGridCard.tsx
+++ b/packages/shared/src/components/cards/article/ArticleFeaturedWideGridCard.tsx
@@ -25,6 +25,7 @@ import { FeedCardGlassActions } from '../common/FeedCardGlassActions';
import { FeedbackGrid } from './feedback/FeedbackGrid';
import { ClickbaitShield } from '../common/ClickbaitShield';
import { useSmartTitle } from '../../../hooks/post/useSmartTitle';
+import { useFitFontSize } from '../../../hooks/useFitFontSize';
import { useFeedCardGlassActions } from '../../../hooks/useFeedCardGlassActions';
import { usePostImage } from '../../../hooks/post/usePostImage';
import { useCardCover } from '../../../hooks/feed/useCardCover';
@@ -51,6 +52,12 @@ const IMAGE_COL_SPAN: Record = {
4: 'col-span-3',
};
+// The hero title shrinks (title1 → title3) to stay within three lines so the
+// summary below always has room, rather than spilling onto a fourth line and
+// pushing the TLDR off the card.
+const HERO_TITLE_SIZE_CLASSES = ['typo-title1', 'typo-title2', 'typo-title3'];
+const HERO_TITLE_MAX_LINES = 3;
+
const HighlightChip = ({
significance,
className,
@@ -148,6 +155,15 @@ export const ArticleFeaturedWideGridCard = forwardRef(
const { pinnedAt } = post;
const { showFeedback } = usePostFeedback({ post });
const { title } = useSmartTitle(post);
+ const {
+ ref: titleRef,
+ sizeClass: titleSizeClass,
+ isClamped: titleClamped,
+ } = useFitFontSize({
+ text: title,
+ sizeClasses: HERO_TITLE_SIZE_CLASSES,
+ maxLines: HERO_TITLE_MAX_LINES,
+ });
const isVideoType = isVideoPost(post);
const image = usePostImage(post);
const { overlay } = useCardCover({ post, onShare });
@@ -276,9 +292,11 @@ export const ArticleFeaturedWideGridCard = forwardRef(
showFeedback={false}
/>
{title}
diff --git a/packages/shared/src/components/cards/common/ActionButtons.tsx b/packages/shared/src/components/cards/common/ActionButtons.tsx
index 83e2a65b5b2..b3db5c93866 100644
--- a/packages/shared/src/components/cards/common/ActionButtons.tsx
+++ b/packages/shared/src/components/cards/common/ActionButtons.tsx
@@ -5,12 +5,13 @@ import type { Post } from '../../../graphql/posts';
import InteractionCounter from '../../InteractionCounter';
import { QuaternaryButton } from '../../buttons/QuaternaryButton';
import {
+ AnalyticsIcon,
DiscussIcon as CommentIcon,
LinkIcon,
DownvoteIcon,
} from '../../icons';
import { ButtonColor, ButtonSize, ButtonVariant } from '../../buttons/Button';
-import { useFeedPreviewMode } from '../../../hooks';
+import { useFeedPreviewMode, useViewSize, ViewSize } from '../../../hooks';
import { UpvoteButtonIcon } from './UpvoteButtonIcon';
import { BookmarkButton } from '../../buttons';
import { IconSize } from '../../Icon';
@@ -21,6 +22,9 @@ import { PostTagsPanel } from '../../post/block/PostTagsPanel';
import { LinkWithTooltip } from '../../tooltips/LinkWithTooltip';
import { useCardActions } from '../../../hooks/cards/useCardActions';
import { useBrandSponsorship } from '../../../hooks/useBrandSponsorship';
+import { usePostImpressionsModal } from '../../../hooks/post/usePostImpressionsModal';
+import { usePostImpressions } from '../../../hooks/post/usePostImpressions';
+import { formatImpressions } from '../../../lib/impressions';
import { useEngagementBarV2 } from '../../../hooks/useEngagementBarV2';
import ActionButtonsV2 from './ActionButtons.v2';
@@ -78,6 +82,14 @@ const ActionButtonsV1 = ({
}: ActionButtonsProps): ReactElement | null => {
const config = variantConfig[variant];
const isFeedPreview = useFeedPreviewMode();
+ const isLaptop = useViewSize(ViewSize.Laptop);
+ const { buttonSize, iconSize } = config;
+ // On mobile/tablet keep full-size icons but shrink the count so the icon
+ // reads as the primary affordance and the number as a subtle stat.
+ const counterClassName = classNames(
+ 'tabular-nums',
+ isLaptop ? variant === 'grid' && 'typo-footnote' : 'typo-caption1',
+ );
const { getUpvoteAnimation } = useBrandSponsorship();
const {
@@ -114,6 +126,13 @@ const ActionButtonsV1 = ({
};
}, [getUpvoteAnimation, post.tags]);
+ const onImpressionsClick = usePostImpressionsModal(post);
+ const {
+ enabled: impressionsEnabled,
+ showImpressions,
+ impressions,
+ } = usePostImpressions(post);
+
if (isFeedPreview) {
return null;
}
@@ -135,13 +154,16 @@ const ActionButtonsV1 = ({
href={post.commentsPermalink}
pressed={post.commented}
variant={ButtonVariant.Tertiary}
- size={config.buttonSize}
- icon={}
+ size={buttonSize}
+ icon={}
onClick={() => onCommentClick?.(post)}
>
{commentCount > 0 && (
)}
@@ -152,16 +174,16 @@ const ActionButtonsV1 = ({
}
+ icon={}
pressed={post.commented}
onClick={() => onCommentClick?.(post)}
- size={config.buttonSize}
+ size={buttonSize}
className="btn-tertiary-blueCheese"
>
{commentCount > 0 && (
}
>
{upvoteCount > 0 && (
)}
+ {commentButton}
{showDownvoteAction && (
+
}
pressed={isDownvoteActive}
onClick={onToggleDownvote}
variant={ButtonVariant.Tertiary}
- size={config.buttonSize}
+ size={buttonSize}
/>
)}
- {commentButton}
- {showAwardAction && (
-
+ {/* When impressions are enabled, drop awards below laptop to make room
+ for the extra action; with the flag off, awards stay on every
+ viewport (unchanged from control). */}
+ {showAwardAction && (!impressionsEnabled || isLaptop) && (
+
)}
}
+ size={buttonSize}
+ icon={}
onClick={onCopyLink}
variant={ButtonVariant.Tertiary}
color={ButtonColor.Cabbage}
className={variant === 'list' ? 'pointer-events-auto' : undefined}
/>
+ {showImpressions && (
+
+ }
+ onClick={onImpressionsClick}
+ variant={ButtonVariant.Tertiary}
+ color={ButtonColor.Cheese}
+ className={classNames(
+ 'btn-tertiary-cheese',
+ variant === 'list' && 'pointer-events-auto',
+ )}
+ >
+
+
+
+ )}
);
diff --git a/packages/shared/src/components/cards/common/ActionButtons.v2.tsx b/packages/shared/src/components/cards/common/ActionButtons.v2.tsx
index 9c5063f5fa2..3bc933fe3ad 100644
--- a/packages/shared/src/components/cards/common/ActionButtons.v2.tsx
+++ b/packages/shared/src/components/cards/common/ActionButtons.v2.tsx
@@ -5,12 +5,13 @@ import type { Post } from '../../../graphql/posts';
import { CardAction } from '../../buttons/CardAction';
import { CardActionBar } from '../../buttons/CardActionBar';
import {
+ AnalyticsIcon,
DiscussIcon as CommentIcon,
LinkIcon,
DownvoteIcon,
} from '../../icons';
import { ButtonColor } from '../../buttons/ButtonV2';
-import { useFeedPreviewMode } from '../../../hooks';
+import { useFeedPreviewMode, useViewSize, ViewSize } from '../../../hooks';
import { UpvoteButtonIcon } from './UpvoteButtonIcon';
import { BookmarkButton } from '../../buttons/BookmarkButton.v2';
import { Tooltip } from '../../tooltip/Tooltip';
@@ -20,6 +21,9 @@ import { PostTagsPanel } from '../../post/block/PostTagsPanel';
import { LinkWithTooltip } from '../../tooltips/LinkWithTooltip';
import { useCardActions } from '../../../hooks/cards/useCardActions';
import { useBrandSponsorship } from '../../../hooks/useBrandSponsorship';
+import { usePostImpressionsModal } from '../../../hooks/post/usePostImpressionsModal';
+import { usePostImpressions } from '../../../hooks/post/usePostImpressions';
+import { formatImpressions } from '../../../lib/impressions';
export type ActionButtonsVariant = 'grid' | 'list' | 'signal';
@@ -70,6 +74,9 @@ const ActionButtons = ({
}: ActionButtonsProps): ReactElement | null => {
const config = variantConfig[variant];
const isFeedPreview = useFeedPreviewMode();
+ // When impressions are enabled, awards are hidden below laptop (tablet +
+ // mobile) to make room for the extra action.
+ const isLaptop = useViewSize(ViewSize.Laptop);
const { getUpvoteAnimation } = useBrandSponsorship();
const {
@@ -105,6 +112,13 @@ const ActionButtons = ({
};
}, [getUpvoteAnimation, post.tags]);
+ const onImpressionsClick = usePostImpressionsModal(post);
+ const {
+ enabled: impressionsEnabled,
+ showImpressions,
+ impressions,
+ } = usePostImpressions(post);
+
if (isFeedPreview) {
return null;
}
@@ -175,6 +189,7 @@ const ActionButtons = ({
buttonClassName="pointer-events-auto"
/>
+ {commentButton}
{showDownvoteAction && (
)}
- {commentButton}
- {showAwardAction && (
+ {showAwardAction && (!impressionsEnabled || isLaptop) && (
)}
+ {showImpressions && (
+
+ }
+ label="Impressions"
+ count={impressions}
+ countFormat={formatImpressions}
+ onClick={onImpressionsClick}
+ color={ButtonColor.Cheese}
+ buttonClassName={classNames(
+ variant === 'list' && 'pointer-events-auto',
+ )}
+ />
+
+ )}
);
diff --git a/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx b/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx
index fc1510c4d09..f2565e822fd 100644
--- a/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx
+++ b/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx
@@ -7,6 +7,7 @@ import InteractionCounter from '../../InteractionCounter';
import { QuaternaryButton } from '../../buttons/QuaternaryButton';
import { BookmarkButton } from '../../buttons/BookmarkButton';
import {
+ AnalyticsIcon,
DiscussIcon as CommentIcon,
DownvoteIcon,
LinkIcon,
@@ -16,6 +17,9 @@ import { IconSize } from '../../Icon';
import { Tooltip } from '../../tooltip/Tooltip';
import { useFeedPreviewMode } from '../../../hooks/useFeedPreviewMode';
import { useCardActions } from '../../../hooks/cards/useCardActions';
+import { usePostImpressionsModal } from '../../../hooks/post/usePostImpressionsModal';
+import { usePostImpressions } from '../../../hooks/post/usePostImpressions';
+import { formatImpressions } from '../../../lib/impressions';
// Full-bleed cover: drop side padding/bottom margin and round only the bottom
// corners so the image meets the card edges. Height/crop are untouched.
@@ -32,10 +36,14 @@ const outerClasses = 'pointer-events-none absolute inset-x-2 bottom-2 z-1';
// actions evenly across the pill: the icons keep equal gaps and a long counter
// (e.g. 900 upvotes / 900 comments) just grows its own button instead of
// clipping or shoving a neighbour off its mark.
-// `px-1` matches the 4px the h-10 pill leaves above/below its h-8 buttons, so
-// the padding is equal on all four sides.
+// The pill stays h-10 (its original height) so it reads as a comfortable bar,
+// while the buttons themselves are the smaller XSmall size.
+// Asymmetric `pl-1 pr-2.5`: the left edge holds the upvote icon while the right
+// edge holds the impressions number — a number reads tighter against the edge
+// than an icon, so it needs more padding to look optically balanced (per
+// jakub.kr "details that make interfaces feel better").
const pillClasses = classNames(
- 'pointer-events-auto flex h-10 w-full items-center justify-between gap-1 overflow-hidden px-1',
+ 'pointer-events-auto flex h-10 w-full items-center justify-between overflow-hidden pl-1 pr-2.5',
'rounded-12 border border-border-subtlest-tertiary',
'text-text-primary backdrop-blur-xl backdrop-saturate-150',
'[&_.btn-quaternary]:[--button-default-color:var(--theme-text-primary)]',
@@ -49,9 +57,10 @@ const pillClasses = classNames(
const glassBackground =
'linear-gradient(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.08)), var(--theme-blur-blur-bg)';
-// Keep the counter compact: monospaced digits so it never jitters, and the
-// `pr-1` breathing room mirrors the standard grid action bar.
-const countLabelClasses = '!pl-0.5 pr-1';
+// Keep the counter compact: monospaced digits so it never jitters, and a hair
+// of padding on each side so six actions (incl. impressions) still fit a narrow
+// card without the trailing icon clipping.
+const countLabelClasses = '!pl-0.5 pr-0.5';
const countClasses = 'tabular-nums typo-footnote';
// Dark glow behind the pill so it stays readable over busy cover images. Fixed
@@ -86,6 +95,8 @@ export function FeedCardGlassActions({
onBookmarkClick,
onCopyLinkClick,
});
+ const onImpressionsClick = usePostImpressionsModal(post);
+ const { showImpressions, impressions } = usePostImpressions(post);
if (isFeedPreview) {
return null;
@@ -117,11 +128,11 @@ export function FeedCardGlassActions({
pressed={isUpvoteActive}
onClick={onToggleUpvote}
variant={ButtonVariant.Tertiary}
- size={ButtonSize.Small}
+ size={ButtonSize.XSmall}
icon={
}
>
@@ -140,12 +151,12 @@ export function FeedCardGlassActions({
icon={
}
pressed={post.commented}
onClick={() => onCommentClick?.(post)}
- size={ButtonSize.Small}
+ size={ButtonSize.XSmall}
className="btn-tertiary-blueCheese pointer-events-auto"
>
{commentCount > 0 && (
@@ -168,13 +179,13 @@ export function FeedCardGlassActions({
icon={
}
pressed={isDownvoteActive}
onClick={onToggleDownvote}
variant={ButtonVariant.Tertiary}
- size={ButtonSize.Small}
+ size={ButtonSize.XSmall}
/>
)}
@@ -184,22 +195,42 @@ export function FeedCardGlassActions({
buttonProps={{
id: `post-${post.id}-bookmark-btn`,
onClick: onToggleBookmark,
- size: ButtonSize.Small,
+ size: ButtonSize.XSmall,
className: 'btn-tertiary-bun pointer-events-auto',
}}
- iconSize={IconSize.XSmall}
+ iconSize={IconSize.Size16}
/>
}
+ size={ButtonSize.XSmall}
+ icon={}
onClick={onCopyLink}
variant={ButtonVariant.Tertiary}
color={ButtonColor.Cabbage}
className="pointer-events-auto"
/>
+ {showImpressions && (
+
+ }
+ size={ButtonSize.XSmall}
+ variant={ButtonVariant.Tertiary}
+ color={ButtonColor.Cheese}
+ onClick={onImpressionsClick}
+ className="btn-tertiary-cheese pointer-events-auto"
+ >
+
+
+
+ )}
>
diff --git a/packages/shared/src/components/modals/common.tsx b/packages/shared/src/components/modals/common.tsx
index 199f5dfcce1..2934626328c 100644
--- a/packages/shared/src/components/modals/common.tsx
+++ b/packages/shared/src/components/modals/common.tsx
@@ -512,6 +512,13 @@ const ReaderPreviewLazyModal = dynamic(
),
);
+const PostImpressionsModal = dynamic(
+ () =>
+ import(
+ /* webpackChunkName: "postImpressionsModal" */ './post/PostImpressionsModal'
+ ),
+);
+
export const modals = {
[LazyModal.SquadMember]: SquadMemberModal,
[LazyModal.UpvotedPopup]: UpvotedPopupModal,
@@ -595,6 +602,7 @@ export const modals = {
[LazyModal.ReaderInstallPrompt]: ReaderInstallPromptModal,
[LazyModal.ReaderExtensionInstall]: ReaderExtensionInstallModal,
[LazyModal.ReaderPreview]: ReaderPreviewLazyModal,
+ [LazyModal.PostImpressions]: PostImpressionsModal,
};
type GetComponentProps = T extends
diff --git a/packages/shared/src/components/modals/common/types.ts b/packages/shared/src/components/modals/common/types.ts
index dfda6a0d09c..f28b066f111 100644
--- a/packages/shared/src/components/modals/common/types.ts
+++ b/packages/shared/src/components/modals/common/types.ts
@@ -107,6 +107,7 @@ export enum LazyModal {
ReaderInstallPrompt = 'readerInstallPrompt',
ReaderExtensionInstall = 'readerExtensionInstall',
ReaderPreview = 'readerPreview',
+ PostImpressions = 'postImpressions',
}
export type ModalTabItem = {
diff --git a/packages/shared/src/components/modals/post/PostImpressionsModal.tsx b/packages/shared/src/components/modals/post/PostImpressionsModal.tsx
new file mode 100644
index 00000000000..2233fbc9f4e
--- /dev/null
+++ b/packages/shared/src/components/modals/post/PostImpressionsModal.tsx
@@ -0,0 +1,69 @@
+import type { ReactElement } from 'react';
+import React from 'react';
+import { Modal } from '../common/Modal';
+import type { LazyModalCommonProps } from '../common/Modal';
+import { ModalClose } from '../common/ModalClose';
+import { Button, ButtonSize, ButtonVariant } from '../../buttons/Button';
+import {
+ Typography,
+ TypographyTag,
+ TypographyType,
+ TypographyColor,
+} from '../../typography/Typography';
+import { AnalyticsIcon } from '../../icons';
+import { IconSize } from '../../Icon';
+import { docs } from '../../../lib/constants';
+import { anchorDefaultRel } from '../../../lib/strings';
+
+/**
+ * X/Twitter-style "Views" explainer shown to non-authors when they tap the
+ * impressions stat — daily.dev's own modal styling, X-like messaging.
+ */
+function PostImpressionsModal(props: LazyModalCommonProps): ReactElement {
+ const { onRequestClose } = props;
+
+ return (
+
+
+
+
+
+ Impressions
+
+
+ The number of times this post was seen across daily.dev. To learn
+ more, visit our{' '}
+
+ docs
+
+ .
+
+
+
+
+ );
+}
+
+export default PostImpressionsModal;
diff --git a/packages/shared/src/components/post/PostUpvotesCommentsCount.tsx b/packages/shared/src/components/post/PostUpvotesCommentsCount.tsx
index 7e0ca893eb6..c7f63c105c7 100644
--- a/packages/shared/src/components/post/PostUpvotesCommentsCount.tsx
+++ b/packages/shared/src/components/post/PostUpvotesCommentsCount.tsx
@@ -14,12 +14,21 @@ import Link from '../utilities/Link';
import { Button, ButtonSize } from '../buttons/Button';
import { AnalyticsIcon } from '../icons';
import { webappUrl } from '../../lib/constants';
+import { getPostImpressions } from '../../lib/impressions';
+import { useConditionalFeature } from '../../hooks/useConditionalFeature';
+import { featureCardImpressions } from '../../lib/featureManagement';
+import { usePostImpressionsModal } from '../../hooks/post/usePostImpressionsModal';
const DEFAULT_REPOSTS_PER_PAGE = 20;
type PostUpvotesCommentsCountPost = Pick<
Post,
- 'analytics' | 'numAwards' | 'numComments' | 'numReposts' | 'numUpvotes'
+ | 'analytics'
+ | 'numAwards'
+ | 'numComments'
+ | 'numReposts'
+ | 'numUpvotes'
+ | 'views'
> &
Partial> & {
author?: Pick, 'id'>;
@@ -43,7 +52,9 @@ interface PostUpvotesCommentsCountProps {
type PostUpvotesCommentsCountContentProps = PostUpvotesCommentsCountProps & {
onRepostsClick?: () => unknown;
onAwardsClick?: () => unknown;
+ onImpressionsClick?: () => void;
showPostAnalytics?: boolean;
+ impressionsEnabled?: boolean;
};
const PostUpvotesCommentsCountContent = ({
@@ -52,7 +63,9 @@ const PostUpvotesCommentsCountContent = ({
onCommentsClick,
onRepostsClick,
onAwardsClick,
+ onImpressionsClick,
showPostAnalytics = false,
+ impressionsEnabled = false,
className,
compact = false,
passive = false,
@@ -63,6 +76,18 @@ const PostUpvotesCommentsCountContent = ({
const reposts = post.numReposts || 0;
const getText = ({ count, label }: { count: number; label: string }) =>
`${largeNumberFormat(count)} ${label}${count > 1 ? 's' : ''}`;
+ // Flag on: a single impressions stat next to the other counts (links to the
+ // analytics page). Flag off: keep main's behaviour — the author/team-only
+ // `analytics.impressions` line plus the "Post analytics" button.
+ const impressions = getPostImpressions(post);
+ const impressionsLabel =
+ impressionsEnabled &&
+ !compact &&
+ !!post.id &&
+ impressions !== null &&
+ impressions > 0
+ ? getText({ count: impressions, label: 'Impression' })
+ : null;
const renderText = ({
key,
@@ -95,7 +120,7 @@ const PostUpvotesCommentsCountContent = ({
)}
data-testid="statsBar"
>
- {!!post.analytics?.impressions && (
+ {!impressionsEnabled && !!post.analytics?.impressions && (
{getText({ count: post.analytics.impressions, label: 'Impression' })}
@@ -112,6 +137,16 @@ const PostUpvotesCommentsCountContent = ({
onClick: onCommentsClick,
children: getText({ count: comments, label: 'Comment' }),
})}
+ {/* Flag on: impressions sit right after comments and look like the other
+ stats. Tapping routes the owner/team to the analytics page and
+ everyone else to the explainer popup (same handler as the feed cards).
+ Shown on the post page/modal strip only (not the compact embed). */}
+ {impressionsLabel &&
+ renderText({
+ key: 'impressions',
+ onClick: onImpressionsClick,
+ children: impressionsLabel,
+ })}
{reposts > 0 &&
renderText({
key: 'reposts',
@@ -136,7 +171,7 @@ const PostUpvotesCommentsCountContent = ({
),
})}
- {showPostAnalytics && (
+ {!impressionsEnabled && showPostAnalytics && (