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 && (