From 1de5b94536f11890ddfca9d712e51f7f63b83d44 Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:59:30 +0300 Subject: [PATCH 1/5] feat: collection card enhancements behind feature flag Gate new collection card behavior behind the `collection_card_enhancements` GrowthBook flag: - Show number of sources after the read time on grid and list cards - Use `updatedAt` with an "Updated" label when the post was edited after creation - Surface a `HIGHLIGHT` raised label on cards featured in Happening Now - Mirror the same source count, "Last updated" prefix, and "Featured in Happening Now" pill on the collection post page The flag is evaluated once in `Feed.tsx` and propagated through `FeedCardContext` so feed cards do not re-evaluate per render. The `majorHeadlines` query only runs when the flag is enabled. Made-with: Cursor --- packages/shared/src/components/Feed.tsx | 41 +++++++++++ .../cards/collection/CollectionGrid.tsx | 18 ++++- .../cards/collection/CollectionList.tsx | 24 ++++++- .../cards/common/FeedItemContainer.tsx | 41 +++++++++-- .../components/cards/common/PostMetadata.tsx | 23 +++++- .../components/cards/common/RaisedLabel.tsx | 22 +++++- .../cards/common/list/FeedItemContainer.tsx | 48 ++++++++++--- .../cards/common/list/PostCardHeader.tsx | 3 + .../cards/common/list/PostMetadata.tsx | 29 ++++++-- .../post/collection/CollectionPostContent.tsx | 70 +++++++++++++++++-- .../src/features/posts/FeedCardContext.tsx | 2 + packages/shared/src/lib/featureManagement.ts | 4 ++ 12 files changed, 288 insertions(+), 37 deletions(-) diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index bcb5d10181f..cabb810942b 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -10,6 +10,7 @@ import React, { import dynamic from 'next/dynamic'; import { useRouter } from 'next/router'; import type { QueryKey } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; import type { PostItem, UseFeedOptionalParams } from '../hooks/useFeed'; import useFeed, { isBoostedPostAd } from '../hooks/useFeed'; import type { Ad, Post } from '../graphql/posts'; @@ -61,9 +62,12 @@ import { useFeedContentPreferenceMutationSubscription } from './feeds/useFeedCon import { useFeedBookmarkPost } from '../hooks/bookmark/useFeedBookmarkPost'; import usePlusEntry from '../hooks/usePlusEntry'; import { FeedCardContext } from '../features/posts/FeedCardContext'; +import { FeedItemType } from './cards/common/common'; +import { majorHeadlinesQueryOptions } from '../graphql/highlights'; import { briefCardFeedFeature, briefFeedEntrypointPage, + featureCollectionCardEnhancements, featureFeedAdTemplate, } from '../lib/featureManagement'; import type { AwardProps } from '../graphql/njord'; @@ -454,6 +458,38 @@ export default function Feed({ 'go to link', ); + const { value: isCollectionEnhancementsEnabled } = useConditionalFeature({ + feature: featureCollectionCardEnhancements, + }); + + const { data: majorHeadlinesData } = useQuery({ + ...majorHeadlinesQueryOptions({}), + enabled: isCollectionEnhancementsEnabled, + }); + + const highlightedPostIds = useMemo(() => { + const set = new Set(); + if (!isCollectionEnhancementsEnabled) { + return set; + } + items.forEach((feedItem) => { + if (feedItem.type !== FeedItemType.Highlight) { + return; + } + feedItem.highlights.forEach((highlight) => { + if (highlight.post?.id) { + set.add(highlight.post.id); + } + }); + }); + majorHeadlinesData?.majorHeadlines.edges.forEach(({ node }) => { + if (node.post?.id) { + set.add(node.post.id); + } + }); + return set; + }, [items, majorHeadlinesData, isCollectionEnhancementsEnabled]); + const trackFinishFeed = useCallback(() => { if (!canFetchMore) { logEvent({ @@ -641,6 +677,11 @@ export default function Feed({ boostedBy: isBoostedPostAd(item) ? item.ad.data?.post?.author || item.ad.data?.post?.scout : undefined, + highlighted: + item.type === FeedItemType.Post && + highlightedPostIds.has(item.post.id), + collectionEnhancementsEnabled: + isCollectionEnhancementsEnabled, }} > {showPromoBanner && index === indexWhenShowingPromoBanner && ( diff --git a/packages/shared/src/components/cards/collection/CollectionGrid.tsx b/packages/shared/src/components/cards/collection/CollectionGrid.tsx index 51d4b05eed2..b9462ff2cb8 100644 --- a/packages/shared/src/components/cards/collection/CollectionGrid.tsx +++ b/packages/shared/src/components/cards/collection/CollectionGrid.tsx @@ -16,6 +16,7 @@ import PostMetadata from '../common/PostMetadata'; import { usePostImage } from '../../../hooks/post/usePostImage'; import CardOverlay from '../common/CardOverlay'; import PostTags from '../common/PostTags'; +import { useFeedCardContext } from '../../../features/posts/FeedCardContext'; export const CollectionGrid = forwardRef(function CollectionCard( { @@ -35,6 +36,11 @@ export const CollectionGrid = forwardRef(function CollectionCard( ) { const { pinnedAt, trending } = post; const image = usePostImage(post); + const { highlighted, collectionEnhancementsEnabled } = useFeedCardContext(); + const wasUpdated = + collectionEnhancementsEnabled && + !!post.updatedAt && + post.updatedAt !== post.createdAt; const onPostCardClick = () => onPostClick(post); const onPostCardAuxClick = () => onPostAuxClick(post); @@ -45,7 +51,11 @@ export const CollectionGrid = forwardRef(function CollectionCard( className: getPostClassNames(post, domProps.className, 'min-h-card'), }} ref={ref} - flagProps={{ pinnedAt, trending }} + flagProps={{ + pinnedAt, + trending, + highlighted: collectionEnhancementsEnabled && highlighted, + }} bookmarked={post.bookmarked} > diff --git a/packages/shared/src/components/cards/collection/CollectionList.tsx b/packages/shared/src/components/cards/collection/CollectionList.tsx index ffbbf9085bd..a000cb3bc11 100644 --- a/packages/shared/src/components/cards/collection/CollectionList.tsx +++ b/packages/shared/src/components/cards/collection/CollectionList.tsx @@ -18,6 +18,7 @@ import { useTruncatedSummary, useViewSize, ViewSize } from '../../../hooks'; import PostTags from '../common/PostTags'; import { CardCoverList } from '../common/list/CardCover'; import { HIGH_PRIORITY_IMAGE_PROPS } from '../../image/Image'; +import { useFeedCardContext } from '../../../features/posts/FeedCardContext'; export const CollectionList = forwardRef(function CollectionCard( { @@ -38,6 +39,11 @@ export const CollectionList = forwardRef(function CollectionCard( const isMobile = useViewSize(ViewSize.MobileL); const image = usePostImage(post); const { title } = useTruncatedSummary(post?.title); + const { highlighted, collectionEnhancementsEnabled } = useFeedCardContext(); + const wasUpdated = + collectionEnhancementsEnabled && + !!post.updatedAt && + post.updatedAt !== post.createdAt; const actionButtons = ( onPostClick(post), @@ -69,7 +80,16 @@ export const CollectionList = forwardRef(function CollectionCard( bookmarked={post.bookmarked} > - + { listMode?: boolean; + highlighted?: boolean; } interface FeedItemContainerProps { @@ -22,6 +23,35 @@ interface FeedItemContainerProps { bookmarked?: boolean; } +const getRaisedLabelType = ({ + pinnedAt, + highlighted, +}: Pick): RaisedLabelType => { + if (pinnedAt) { + return RaisedLabelType.Pinned; + } + if (highlighted) { + return RaisedLabelType.Highlight; + } + return RaisedLabelType.Hot; +}; + +const getRaisedLabelDescription = ({ + type, + trending, +}: { + type: RaisedLabelType; + trending?: number; +}): string | undefined => { + if (type === RaisedLabelType.Hot) { + return `${trending} devs read it last hour`; + } + if (type === RaisedLabelType.Highlight) { + return 'Featured in Happening Now'; + } + return undefined; +}; + function FeedItemContainer( { flagProps = {}, children, domProps, bookmarked }: FeedItemContainerProps, ref?: Ref, @@ -29,17 +59,14 @@ function FeedItemContainer( const { highlightBookmarkedPost } = useBookmarkProvider({ bookmarked: bookmarked ?? false, }); - const { listMode, pinnedAt, trending } = flagProps; - const type = pinnedAt ? RaisedLabelType.Pinned : RaisedLabelType.Hot; - const description = - type === RaisedLabelType.Hot - ? `${trending} devs read it last hour` - : undefined; + const { listMode, pinnedAt, trending, highlighted } = flagProps; + const type = getRaisedLabelType({ pinnedAt, highlighted }); + const description = getRaisedLabelDescription({ type, trending }); const isFeedPreview = useFeedPreviewMode(); return ( ( {component} diff --git a/packages/shared/src/components/cards/common/PostMetadata.tsx b/packages/shared/src/components/cards/common/PostMetadata.tsx index 137e5e314b5..9318a433a1d 100644 --- a/packages/shared/src/components/cards/common/PostMetadata.tsx +++ b/packages/shared/src/components/cards/common/PostMetadata.tsx @@ -20,6 +20,8 @@ interface PostMetadataProps isVideoType?: boolean; domain?: ReactNode; pollMetadata?: PollMetadataProps; + numSources?: number; + dateLabel?: string; } export default function PostMetadata({ @@ -32,6 +34,8 @@ export default function PostMetadata({ isVideoType, domain, pollMetadata, + numSources, + dateLabel, }: PostMetadataProps): ReactElement { const hasUpvoteCount = typeof numUpvotes === 'number'; const upvoteCount = numUpvotes ?? 0; @@ -62,7 +66,13 @@ export default function PostMetadata({ !!createdAt && !boostedBy && { key: 'date', - node: , + node: ( + + ), }, showReadTime && { key: 'readTime', @@ -72,6 +82,15 @@ export default function PostMetadata({ ), }, + !!numSources && + numSources > 0 && { + key: 'sources', + node: ( + + {numSources} {numSources === 1 ? 'source' : 'sources'} + + ), + }, !!showReadTime && domain && { key: 'domain', node: domain }, hasUpvoteCount && upvoteCount > 0 && { @@ -87,7 +106,7 @@ export default function PostMetadata({ return (
diff --git a/packages/shared/src/components/cards/common/RaisedLabel.tsx b/packages/shared/src/components/cards/common/RaisedLabel.tsx index 3036a6253d1..6919947e4f5 100644 --- a/packages/shared/src/components/cards/common/RaisedLabel.tsx +++ b/packages/shared/src/components/cards/common/RaisedLabel.tsx @@ -10,12 +10,23 @@ export enum RaisedLabelType { Hot = 'Hot', Pinned = 'Pinned', Beta = 'Beta', + Highlight = 'Highlight', } const typeToClassName: Record = { [RaisedLabelType.Hot]: 'bg-status-error', [RaisedLabelType.Pinned]: 'bg-status-warning', [RaisedLabelType.Beta]: 'bg-raw-cabbage-40', + [RaisedLabelType.Highlight]: 'bg-theme-overlay-float-blueCheese', +}; + +const typeToTextClassName: Partial> = { + [RaisedLabelType.Highlight]: + 'feed-highlights-title-gradient text-[0.5rem] leading-none', +}; + +const typeToContainerClassName: Partial> = { + [RaisedLabelType.Highlight]: 'pt-1', }; export interface RaisedLabelProps { @@ -63,8 +74,9 @@ export function RaisedLabel({ >
{type} @@ -94,12 +106,18 @@ export function RaisedLabel({ 'flex items-center px-1', styles.flag, typeToClassName[type], + typeToContainerClassName[type], listMode ? 'h-5 w-full justify-center rounded-l-4 mouse:translate-x-9' : 'h-full flex-col rounded-t-4 mouse:translate-y-4', )} > - + {type}
diff --git a/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx b/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx index 04562185537..a7895ed8567 100644 --- a/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx +++ b/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx @@ -27,8 +27,38 @@ interface FlagProps extends Omit, 'type'> { type?: Post['type'] | ReactElement | string; adAttribution?: ReactElement | string; + highlighted?: boolean; } +const getListRaisedLabelType = ({ + pinnedAt, + highlighted, +}: Pick): RaisedLabelType => { + if (pinnedAt) { + return RaisedLabelType.Pinned; + } + if (highlighted) { + return RaisedLabelType.Highlight; + } + return RaisedLabelType.Hot; +}; + +const getListRaisedLabelDescription = ({ + type, + trending, +}: { + type: RaisedLabelType; + trending?: number; +}): string | undefined => { + if (type === RaisedLabelType.Hot && trending && trending > 0) { + return `${trending} devs read it last hour`; + } + if (type === RaisedLabelType.Highlight) { + return 'Featured in Happening Now'; + } + return undefined; +}; + function FeedItemContainer( { flagProps, @@ -42,16 +72,16 @@ function FeedItemContainer( const { highlightBookmarkedPost } = useBookmarkProvider({ bookmarked: bookmarked ?? false, }); - const { adAttribution, pinnedAt, trending, type } = flagProps ?? {}; - const raisedLabelType = pinnedAt - ? RaisedLabelType.Pinned - : RaisedLabelType.Hot; - const description = - [RaisedLabelType.Hot].includes(raisedLabelType) && trending > 0 - ? `${trending} devs read it last hour` - : undefined; + const { adAttribution, pinnedAt, trending, type, highlighted } = + flagProps ?? {}; + const raisedLabelType = getListRaisedLabelType({ pinnedAt, highlighted }); + const description = getListRaisedLabelDescription({ + type: raisedLabelType, + trending, + }); const isFeedPreview = useFeedPreviewMode(); - const showFlag = (!!pinnedAt || !!trending) && !isFeedPreview; + const showFlag = + (!!pinnedAt || !!highlighted || !!trending) && !isFeedPreview; const showTypeLabel = !!adAttribution || !!type; const [focus, setFocus] = useState(false); diff --git a/packages/shared/src/components/cards/common/list/PostCardHeader.tsx b/packages/shared/src/components/cards/common/list/PostCardHeader.tsx index 73d9a06c22b..ea3df48ef20 100644 --- a/packages/shared/src/components/cards/common/list/PostCardHeader.tsx +++ b/packages/shared/src/components/cards/common/list/PostCardHeader.tsx @@ -42,6 +42,9 @@ interface CardHeaderProps { topLabel?: PostMetadataProps['topLabel']; bottomLabel?: PostMetadataProps['bottomLabel']; dateFirst?: PostMetadataProps['dateFirst']; + createdAt?: PostMetadataProps['createdAt']; + dateLabel?: PostMetadataProps['dateLabel']; + numSources?: PostMetadataProps['numSources']; }; } diff --git a/packages/shared/src/components/cards/common/list/PostMetadata.tsx b/packages/shared/src/components/cards/common/list/PostMetadata.tsx index 242f56020fb..30164d12377 100644 --- a/packages/shared/src/components/cards/common/list/PostMetadata.tsx +++ b/packages/shared/src/components/cards/common/list/PostMetadata.tsx @@ -19,6 +19,8 @@ export interface PostMetadataProps { bottomLabel?: ReactElement | string; createdAt?: string; dateFirst?: boolean; + dateLabel?: string; + numSources?: number; } export default function PostMetadata({ @@ -27,11 +29,26 @@ export default function PostMetadata({ topLabel, bottomLabel, dateFirst, + dateLabel, + numSources, }: PostMetadataProps): ReactElement { const { boostedBy } = useFeedCardContext(); const promotedText = useScrambler( boostedBy ? `Promoted by @${boostedBy.username}` : undefined, ); + const hasSources = !!numSources && numSources > 0; + const dateNode = !!createdAt && ( + + ); + const sourcesNode = hasSources && ( + + {numSources} {numSources === 1 ? 'source' : 'sources'} + + ); return (
@@ -51,9 +68,7 @@ export default function PostMetadata({ {!!boostedBy && !!bottomLabel && } {dateFirst ? ( <> - {!!createdAt && ( - - )} + {dateNode} {!!createdAt && !!bottomLabel && } {bottomLabel} @@ -61,11 +76,13 @@ export default function PostMetadata({ <> {bottomLabel} {(!!bottomLabel || !!boostedBy) && !!createdAt && } - {!!createdAt && ( - - )} + {dateNode} )} + {(!!createdAt || !!bottomLabel || !!boostedBy) && sourcesNode && ( + + )} + {sourcesNode}
); diff --git a/packages/shared/src/components/post/collection/CollectionPostContent.tsx b/packages/shared/src/components/post/collection/CollectionPostContent.tsx index 60c969b443f..77737b06293 100644 --- a/packages/shared/src/components/post/collection/CollectionPostContent.tsx +++ b/packages/shared/src/components/post/collection/CollectionPostContent.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; import type { ReactElement } from 'react'; -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; import Link from '../../utilities/Link'; import { LazyImage } from '../../LazyImage'; import { ToastSubject, useToastNotification } from '../../../hooks'; @@ -24,6 +25,9 @@ import { withPostById } from '../withPostById'; import { PostTagList } from '../tags/PostTagList'; import { CollectionPostHeaderActions } from './CollectionPostHeaderActions'; import type { Post } from '../../../graphql/posts'; +import { majorHeadlinesQueryOptions } from '../../../graphql/highlights'; +import { useConditionalFeature } from '../../../hooks/useConditionalFeature'; +import { featureCollectionCardEnhancements } from '../../../lib/featureManagement'; type CollectionPostContentRawProps = Omit & { post: Post; @@ -53,9 +57,33 @@ const CollectionPostContentRaw = ({ origin, post, }); - const { updatedAt, contentHtml, image } = post; + const { value: isCollectionEnhancementsEnabled } = useConditionalFeature({ + feature: featureCollectionCardEnhancements, + }); + const { updatedAt, createdAt, contentHtml, image, numCollectionSources } = + post; + const wasUpdated = + isCollectionEnhancementsEnabled && !!updatedAt && updatedAt !== createdAt; + const dateToShow = wasUpdated ? updatedAt : createdAt; + const hasSources = + isCollectionEnhancementsEnabled && + !!numCollectionSources && + numCollectionSources > 0; const { onCopyPostLink, onReadArticle } = engagementActions; + const { data: highlightsData } = useQuery({ + ...majorHeadlinesQueryOptions({}), + enabled: isCollectionEnhancementsEnabled, + }); + const highlightForPost = useMemo(() => { + if (!isCollectionEnhancementsEnabled) { + return undefined; + } + return highlightsData?.majorHeadlines.edges.find( + (edge) => edge.node.post.id === post.id, + )?.node; + }, [highlightsData, post.id, isCollectionEnhancementsEnabled]); + const hasNavigation = !!onPreviousPost || !!onNextPost; const containerClass = classNames( 'laptop:flex-row laptop:pb-0', @@ -130,7 +158,7 @@ const CollectionPostContentRaw = ({ >
-
+
+ {highlightForPost && ( + + + Featured in Happening Now + + } + className="bg-theme-overlay-float-blueCheese" + /> + + )} - {!!updatedAt && ( -
- Last updated - + {!!dateToShow && ( +
+ + {hasSources && ( + <> + + + {numCollectionSources}{' '} + {numCollectionSources === 1 ? 'source' : 'sources'} + + + )}
)} {image && ( diff --git a/packages/shared/src/features/posts/FeedCardContext.tsx b/packages/shared/src/features/posts/FeedCardContext.tsx index 91695e3eb35..f75ebcaa382 100644 --- a/packages/shared/src/features/posts/FeedCardContext.tsx +++ b/packages/shared/src/features/posts/FeedCardContext.tsx @@ -4,6 +4,8 @@ import type { Author } from '../../graphql/comments'; interface FeedCardContextData { // a boosted post can surface organically, and we want to show the boosted label only if the post surfaced as an ad boostedBy?: Author; + highlighted?: boolean; + collectionEnhancementsEnabled?: boolean; } export const FeedCardContext = createContext({}); diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts index 1de697c7b30..ec07c174086 100644 --- a/packages/shared/src/lib/featureManagement.ts +++ b/packages/shared/src/lib/featureManagement.ts @@ -33,6 +33,10 @@ export const discussedFeedVersion = new Feature('discussed_feed_version', 2); export const latestFeedVersion = new Feature('latest_feed_version', 2); export const customFeedVersion = new Feature('custom_feed_version', 2); export const featureFeedV2Highlights = new Feature('feed_v2_highlights', false); +export const featureCollectionCardEnhancements = new Feature( + 'collection_card_enhancements', + false, +); // @ts-expect-error stale feature without default export const plusTakeoverContent = new Feature<{ From 2c5d9fa24542b4285e0a700dd02972f673a7c4da Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:13:44 +0300 Subject: [PATCH 2/5] fix: satisfy strict typecheck on touched collection/list files Resolve pre-existing strict-mode errors in files modified by the collection card enhancements so `typecheck_strict_changed` passes on CI. Made-with: Cursor --- .../src/components/cards/collection/CollectionGrid.tsx | 10 +++++++--- .../src/components/cards/collection/CollectionList.tsx | 8 ++++---- .../shared/src/components/cards/common/RaisedLabel.tsx | 4 ++-- .../components/cards/common/list/FeedItemContainer.tsx | 9 +++++---- .../components/cards/common/list/PostCardHeader.tsx | 4 ++-- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/shared/src/components/cards/collection/CollectionGrid.tsx b/packages/shared/src/components/cards/collection/CollectionGrid.tsx index b9462ff2cb8..e6cc72ad742 100644 --- a/packages/shared/src/components/cards/collection/CollectionGrid.tsx +++ b/packages/shared/src/components/cards/collection/CollectionGrid.tsx @@ -41,14 +41,18 @@ export const CollectionGrid = forwardRef(function CollectionCard( collectionEnhancementsEnabled && !!post.updatedAt && post.updatedAt !== post.createdAt; - const onPostCardClick = () => onPostClick(post); - const onPostCardAuxClick = () => onPostAuxClick(post); + const onPostCardClick = () => onPostClick?.(post); + const onPostCardAuxClick = () => onPostAuxClick?.(post); return ( onPostClick(post), + onClick: () => onPostClick?.(post), href: post.commentsPermalink, }} bookmarked={post.bookmarked} @@ -95,8 +95,8 @@ export const CollectionList = forwardRef(function CollectionCard( main: classNames(!!post.collectionSources?.length && '-my-0.5'), avatar: 'group-hover:border-background-subtle', }} - sources={post.collectionSources} - totalSources={post.numCollectionSources} + sources={post.collectionSources ?? []} + totalSources={post.numCollectionSources ?? 0} alwaysShowSources /> diff --git a/packages/shared/src/components/cards/common/RaisedLabel.tsx b/packages/shared/src/components/cards/common/RaisedLabel.tsx index 6919947e4f5..8e55c2849fa 100644 --- a/packages/shared/src/components/cards/common/RaisedLabel.tsx +++ b/packages/shared/src/components/cards/common/RaisedLabel.tsx @@ -67,7 +67,7 @@ export function RaisedLabel({ )} > 0} + condition={!!description?.length} wrapper={(children) => ( {children} )} @@ -96,7 +96,7 @@ export function RaisedLabel({ )} > 0} + condition={!!description?.length} wrapper={(children) => ( {children} )} diff --git a/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx b/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx index a7895ed8567..9ae1cdc4888 100644 --- a/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx +++ b/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx @@ -82,7 +82,8 @@ function FeedItemContainer( const isFeedPreview = useFeedPreviewMode(); const showFlag = (!!pinnedAt || !!highlighted || !!trending) && !isFeedPreview; - const showTypeLabel = !!adAttribution || !!type; + const typeLabelValue = adAttribution ?? type; + const showTypeLabel = !!typeLabelValue; const [focus, setFocus] = useState(false); return ( @@ -100,7 +101,7 @@ function FeedItemContainer( ...(highlightBookmarkedPost && { background: bookmarkProviderListBg }), }} > - {linkProps && ( + {linkProps?.href && ( - {showTypeLabel && ( + {typeLabelValue && ( )} diff --git a/packages/shared/src/components/cards/common/list/PostCardHeader.tsx b/packages/shared/src/components/cards/common/list/PostCardHeader.tsx index ea3df48ef20..ea50564bb58 100644 --- a/packages/shared/src/components/cards/common/list/PostCardHeader.tsx +++ b/packages/shared/src/components/cards/common/list/PostCardHeader.tsx @@ -116,11 +116,11 @@ export const PostCardHeader = ({ <> {showCTA && ( } - href={postLink} + href={postLink ?? ''} onClick={onReadArticleClick} openNewTab={openNewTab} /> From 01c3a5f22cda6adcbe0e0db6d4ac5c5db3de7c70 Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:13:58 +0300 Subject: [PATCH 3/5] chore: drop feature flag and highlight badge from collection cards - Remove featureCollectionCardEnhancements flag; collection card/date/source enhancements are always on. - Drop highlight cross-referencing in Feed.tsx and the majorHeadlines query on the collection post page. This removes two extra API calls. - Remove RaisedLabelType.Highlight variant and the "Featured in Happening Now" pill on the collection post page. - Extract isPostUpdated helper in graphql/posts for the updated-date check. - Reuse shared pluralize helper for the "N source(s)" label. Made-with: Cursor --- packages/shared/src/components/Feed.tsx | 41 -------------- .../cards/collection/CollectionGrid.tsx | 18 ++----- .../cards/collection/CollectionList.tsx | 13 ++--- .../cards/common/FeedItemContainer.tsx | 41 +++----------- .../components/cards/common/PostMetadata.tsx | 3 +- .../components/cards/common/RaisedLabel.tsx | 22 +------- .../cards/common/list/FeedItemContainer.tsx | 48 ++++------------- .../cards/common/list/PostMetadata.tsx | 3 +- .../post/collection/CollectionPostContent.tsx | 53 +++---------------- .../src/features/posts/FeedCardContext.tsx | 2 - packages/shared/src/graphql/posts.ts | 4 ++ packages/shared/src/lib/featureManagement.ts | 4 -- 12 files changed, 40 insertions(+), 212 deletions(-) diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index cabb810942b..bcb5d10181f 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -10,7 +10,6 @@ import React, { import dynamic from 'next/dynamic'; import { useRouter } from 'next/router'; import type { QueryKey } from '@tanstack/react-query'; -import { useQuery } from '@tanstack/react-query'; import type { PostItem, UseFeedOptionalParams } from '../hooks/useFeed'; import useFeed, { isBoostedPostAd } from '../hooks/useFeed'; import type { Ad, Post } from '../graphql/posts'; @@ -62,12 +61,9 @@ import { useFeedContentPreferenceMutationSubscription } from './feeds/useFeedCon import { useFeedBookmarkPost } from '../hooks/bookmark/useFeedBookmarkPost'; import usePlusEntry from '../hooks/usePlusEntry'; import { FeedCardContext } from '../features/posts/FeedCardContext'; -import { FeedItemType } from './cards/common/common'; -import { majorHeadlinesQueryOptions } from '../graphql/highlights'; import { briefCardFeedFeature, briefFeedEntrypointPage, - featureCollectionCardEnhancements, featureFeedAdTemplate, } from '../lib/featureManagement'; import type { AwardProps } from '../graphql/njord'; @@ -458,38 +454,6 @@ export default function Feed({ 'go to link', ); - const { value: isCollectionEnhancementsEnabled } = useConditionalFeature({ - feature: featureCollectionCardEnhancements, - }); - - const { data: majorHeadlinesData } = useQuery({ - ...majorHeadlinesQueryOptions({}), - enabled: isCollectionEnhancementsEnabled, - }); - - const highlightedPostIds = useMemo(() => { - const set = new Set(); - if (!isCollectionEnhancementsEnabled) { - return set; - } - items.forEach((feedItem) => { - if (feedItem.type !== FeedItemType.Highlight) { - return; - } - feedItem.highlights.forEach((highlight) => { - if (highlight.post?.id) { - set.add(highlight.post.id); - } - }); - }); - majorHeadlinesData?.majorHeadlines.edges.forEach(({ node }) => { - if (node.post?.id) { - set.add(node.post.id); - } - }); - return set; - }, [items, majorHeadlinesData, isCollectionEnhancementsEnabled]); - const trackFinishFeed = useCallback(() => { if (!canFetchMore) { logEvent({ @@ -677,11 +641,6 @@ export default function Feed({ boostedBy: isBoostedPostAd(item) ? item.ad.data?.post?.author || item.ad.data?.post?.scout : undefined, - highlighted: - item.type === FeedItemType.Post && - highlightedPostIds.has(item.post.id), - collectionEnhancementsEnabled: - isCollectionEnhancementsEnabled, }} > {showPromoBanner && index === indexWhenShowingPromoBanner && ( diff --git a/packages/shared/src/components/cards/collection/CollectionGrid.tsx b/packages/shared/src/components/cards/collection/CollectionGrid.tsx index e6cc72ad742..d40afca7fbd 100644 --- a/packages/shared/src/components/cards/collection/CollectionGrid.tsx +++ b/packages/shared/src/components/cards/collection/CollectionGrid.tsx @@ -16,7 +16,7 @@ import PostMetadata from '../common/PostMetadata'; import { usePostImage } from '../../../hooks/post/usePostImage'; import CardOverlay from '../common/CardOverlay'; import PostTags from '../common/PostTags'; -import { useFeedCardContext } from '../../../features/posts/FeedCardContext'; +import { isPostUpdated } from '../../../graphql/posts'; export const CollectionGrid = forwardRef(function CollectionCard( { @@ -36,11 +36,7 @@ export const CollectionGrid = forwardRef(function CollectionCard( ) { const { pinnedAt, trending } = post; const image = usePostImage(post); - const { highlighted, collectionEnhancementsEnabled } = useFeedCardContext(); - const wasUpdated = - collectionEnhancementsEnabled && - !!post.updatedAt && - post.updatedAt !== post.createdAt; + const wasUpdated = isPostUpdated(post); const onPostCardClick = () => onPostClick?.(post); const onPostCardAuxClick = () => onPostAuxClick?.(post); @@ -55,11 +51,7 @@ export const CollectionGrid = forwardRef(function CollectionCard( ), }} ref={ref} - flagProps={{ - pinnedAt, - trending, - highlighted: collectionEnhancementsEnabled && highlighted, - }} + flagProps={{ pinnedAt, trending }} bookmarked={post.bookmarked} > diff --git a/packages/shared/src/components/cards/collection/CollectionList.tsx b/packages/shared/src/components/cards/collection/CollectionList.tsx index dd657143fce..ac1043de172 100644 --- a/packages/shared/src/components/cards/collection/CollectionList.tsx +++ b/packages/shared/src/components/cards/collection/CollectionList.tsx @@ -18,7 +18,7 @@ import { useTruncatedSummary, useViewSize, ViewSize } from '../../../hooks'; import PostTags from '../common/PostTags'; import { CardCoverList } from '../common/list/CardCover'; import { HIGH_PRIORITY_IMAGE_PROPS } from '../../image/Image'; -import { useFeedCardContext } from '../../../features/posts/FeedCardContext'; +import { isPostUpdated } from '../../../graphql/posts'; export const CollectionList = forwardRef(function CollectionCard( { @@ -39,11 +39,7 @@ export const CollectionList = forwardRef(function CollectionCard( const isMobile = useViewSize(ViewSize.MobileL); const image = usePostImage(post); const { title } = useTruncatedSummary(post?.title ?? ''); - const { highlighted, collectionEnhancementsEnabled } = useFeedCardContext(); - const wasUpdated = - collectionEnhancementsEnabled && - !!post.updatedAt && - post.updatedAt !== post.createdAt; + const wasUpdated = isPostUpdated(post); const actionButtons = ( { listMode?: boolean; - highlighted?: boolean; } interface FeedItemContainerProps { @@ -23,35 +22,6 @@ interface FeedItemContainerProps { bookmarked?: boolean; } -const getRaisedLabelType = ({ - pinnedAt, - highlighted, -}: Pick): RaisedLabelType => { - if (pinnedAt) { - return RaisedLabelType.Pinned; - } - if (highlighted) { - return RaisedLabelType.Highlight; - } - return RaisedLabelType.Hot; -}; - -const getRaisedLabelDescription = ({ - type, - trending, -}: { - type: RaisedLabelType; - trending?: number; -}): string | undefined => { - if (type === RaisedLabelType.Hot) { - return `${trending} devs read it last hour`; - } - if (type === RaisedLabelType.Highlight) { - return 'Featured in Happening Now'; - } - return undefined; -}; - function FeedItemContainer( { flagProps = {}, children, domProps, bookmarked }: FeedItemContainerProps, ref?: Ref, @@ -59,14 +29,17 @@ function FeedItemContainer( const { highlightBookmarkedPost } = useBookmarkProvider({ bookmarked: bookmarked ?? false, }); - const { listMode, pinnedAt, trending, highlighted } = flagProps; - const type = getRaisedLabelType({ pinnedAt, highlighted }); - const description = getRaisedLabelDescription({ type, trending }); + const { listMode, pinnedAt, trending } = flagProps; + const type = pinnedAt ? RaisedLabelType.Pinned : RaisedLabelType.Hot; + const description = + type === RaisedLabelType.Hot + ? `${trending} devs read it last hour` + : undefined; const isFeedPreview = useFeedPreviewMode(); return ( ( {component} diff --git a/packages/shared/src/components/cards/common/PostMetadata.tsx b/packages/shared/src/components/cards/common/PostMetadata.tsx index 9318a433a1d..1fb3479d077 100644 --- a/packages/shared/src/components/cards/common/PostMetadata.tsx +++ b/packages/shared/src/components/cards/common/PostMetadata.tsx @@ -6,6 +6,7 @@ import { Separator } from './common'; import type { Post } from '../../../graphql/posts'; import { formatReadTime, DateFormat } from '../../utilities'; import { largeNumberFormat } from '../../../lib'; +import { pluralize } from '../../../lib/strings'; import { useFeedCardContext } from '../../../features/posts/FeedCardContext'; import { Tooltip } from '../../tooltip/Tooltip'; import type { PollMetadataProps } from './PollMetadata'; @@ -87,7 +88,7 @@ export default function PostMetadata({ key: 'sources', node: ( - {numSources} {numSources === 1 ? 'source' : 'sources'} + {numSources} {pluralize('source', numSources)} ), }, diff --git a/packages/shared/src/components/cards/common/RaisedLabel.tsx b/packages/shared/src/components/cards/common/RaisedLabel.tsx index 8e55c2849fa..e7141b4efcc 100644 --- a/packages/shared/src/components/cards/common/RaisedLabel.tsx +++ b/packages/shared/src/components/cards/common/RaisedLabel.tsx @@ -10,23 +10,12 @@ export enum RaisedLabelType { Hot = 'Hot', Pinned = 'Pinned', Beta = 'Beta', - Highlight = 'Highlight', } const typeToClassName: Record = { [RaisedLabelType.Hot]: 'bg-status-error', [RaisedLabelType.Pinned]: 'bg-status-warning', [RaisedLabelType.Beta]: 'bg-raw-cabbage-40', - [RaisedLabelType.Highlight]: 'bg-theme-overlay-float-blueCheese', -}; - -const typeToTextClassName: Partial> = { - [RaisedLabelType.Highlight]: - 'feed-highlights-title-gradient text-[0.5rem] leading-none', -}; - -const typeToContainerClassName: Partial> = { - [RaisedLabelType.Highlight]: 'pt-1', }; export interface RaisedLabelProps { @@ -74,9 +63,8 @@ export function RaisedLabel({ >
{type} @@ -106,18 +94,12 @@ export function RaisedLabel({ 'flex items-center px-1', styles.flag, typeToClassName[type], - typeToContainerClassName[type], listMode ? 'h-5 w-full justify-center rounded-l-4 mouse:translate-x-9' : 'h-full flex-col rounded-t-4 mouse:translate-y-4', )} > - + {type}
diff --git a/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx b/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx index 9ae1cdc4888..5148d2df4e7 100644 --- a/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx +++ b/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx @@ -27,38 +27,8 @@ interface FlagProps extends Omit, 'type'> { type?: Post['type'] | ReactElement | string; adAttribution?: ReactElement | string; - highlighted?: boolean; } -const getListRaisedLabelType = ({ - pinnedAt, - highlighted, -}: Pick): RaisedLabelType => { - if (pinnedAt) { - return RaisedLabelType.Pinned; - } - if (highlighted) { - return RaisedLabelType.Highlight; - } - return RaisedLabelType.Hot; -}; - -const getListRaisedLabelDescription = ({ - type, - trending, -}: { - type: RaisedLabelType; - trending?: number; -}): string | undefined => { - if (type === RaisedLabelType.Hot && trending && trending > 0) { - return `${trending} devs read it last hour`; - } - if (type === RaisedLabelType.Highlight) { - return 'Featured in Happening Now'; - } - return undefined; -}; - function FeedItemContainer( { flagProps, @@ -72,16 +42,16 @@ function FeedItemContainer( const { highlightBookmarkedPost } = useBookmarkProvider({ bookmarked: bookmarked ?? false, }); - const { adAttribution, pinnedAt, trending, type, highlighted } = - flagProps ?? {}; - const raisedLabelType = getListRaisedLabelType({ pinnedAt, highlighted }); - const description = getListRaisedLabelDescription({ - type: raisedLabelType, - trending, - }); + const { adAttribution, pinnedAt, trending, type } = flagProps ?? {}; + const raisedLabelType = pinnedAt + ? RaisedLabelType.Pinned + : RaisedLabelType.Hot; + const description = + raisedLabelType === RaisedLabelType.Hot && trending && trending > 0 + ? `${trending} devs read it last hour` + : undefined; const isFeedPreview = useFeedPreviewMode(); - const showFlag = - (!!pinnedAt || !!highlighted || !!trending) && !isFeedPreview; + const showFlag = (!!pinnedAt || !!trending) && !isFeedPreview; const typeLabelValue = adAttribution ?? type; const showTypeLabel = !!typeLabelValue; const [focus, setFocus] = useState(false); diff --git a/packages/shared/src/components/cards/common/list/PostMetadata.tsx b/packages/shared/src/components/cards/common/list/PostMetadata.tsx index 30164d12377..e9ceedd4e88 100644 --- a/packages/shared/src/components/cards/common/list/PostMetadata.tsx +++ b/packages/shared/src/components/cards/common/list/PostMetadata.tsx @@ -12,6 +12,7 @@ import { TypographyColor, } from '../../../typography/Typography'; import { useScrambler } from '../../../../hooks/useScrambler'; +import { pluralize } from '../../../../lib/strings'; export interface PostMetadataProps { className?: string; @@ -46,7 +47,7 @@ export default function PostMetadata({ ); const sourcesNode = hasSources && ( - {numSources} {numSources === 1 ? 'source' : 'sources'} + {numSources} {pluralize('source', numSources)} ); diff --git a/packages/shared/src/components/post/collection/CollectionPostContent.tsx b/packages/shared/src/components/post/collection/CollectionPostContent.tsx index 77737b06293..39ca30d61bf 100644 --- a/packages/shared/src/components/post/collection/CollectionPostContent.tsx +++ b/packages/shared/src/components/post/collection/CollectionPostContent.tsx @@ -1,7 +1,6 @@ import classNames from 'classnames'; import type { ReactElement } from 'react'; -import React, { useEffect, useMemo } from 'react'; -import { useQuery } from '@tanstack/react-query'; +import React, { useEffect } from 'react'; import Link from '../../utilities/Link'; import { LazyImage } from '../../LazyImage'; import { ToastSubject, useToastNotification } from '../../../hooks'; @@ -24,10 +23,8 @@ import { DateFormat } from '../../utilities'; import { withPostById } from '../withPostById'; import { PostTagList } from '../tags/PostTagList'; import { CollectionPostHeaderActions } from './CollectionPostHeaderActions'; -import type { Post } from '../../../graphql/posts'; -import { majorHeadlinesQueryOptions } from '../../../graphql/highlights'; -import { useConditionalFeature } from '../../../hooks/useConditionalFeature'; -import { featureCollectionCardEnhancements } from '../../../lib/featureManagement'; +import { isPostUpdated, type Post } from '../../../graphql/posts'; +import { pluralize } from '../../../lib/strings'; type CollectionPostContentRawProps = Omit & { post: Post; @@ -57,33 +54,13 @@ const CollectionPostContentRaw = ({ origin, post, }); - const { value: isCollectionEnhancementsEnabled } = useConditionalFeature({ - feature: featureCollectionCardEnhancements, - }); - const { updatedAt, createdAt, contentHtml, image, numCollectionSources } = + const { createdAt, updatedAt, contentHtml, image, numCollectionSources } = post; - const wasUpdated = - isCollectionEnhancementsEnabled && !!updatedAt && updatedAt !== createdAt; + const wasUpdated = isPostUpdated(post); const dateToShow = wasUpdated ? updatedAt : createdAt; - const hasSources = - isCollectionEnhancementsEnabled && - !!numCollectionSources && - numCollectionSources > 0; + const hasSources = !!numCollectionSources && numCollectionSources > 0; const { onCopyPostLink, onReadArticle } = engagementActions; - const { data: highlightsData } = useQuery({ - ...majorHeadlinesQueryOptions({}), - enabled: isCollectionEnhancementsEnabled, - }); - const highlightForPost = useMemo(() => { - if (!isCollectionEnhancementsEnabled) { - return undefined; - } - return highlightsData?.majorHeadlines.edges.find( - (edge) => edge.node.post.id === post.id, - )?.node; - }, [highlightsData, post.id, isCollectionEnhancementsEnabled]); - const hasNavigation = !!onPreviousPost || !!onNextPost; const containerClass = classNames( 'laptop:flex-row laptop:pb-0', @@ -166,22 +143,6 @@ const CollectionPostContentRaw = ({ className="bg-theme-overlay-float-cabbage text-brand-default" /> - {highlightForPost && ( - - - Featured in Happening Now - - } - className="bg-theme-overlay-float-blueCheese" - /> - - )} {numCollectionSources}{' '} - {numCollectionSources === 1 ? 'source' : 'sources'} + {pluralize('source', numCollectionSources)} )} diff --git a/packages/shared/src/features/posts/FeedCardContext.tsx b/packages/shared/src/features/posts/FeedCardContext.tsx index f75ebcaa382..91695e3eb35 100644 --- a/packages/shared/src/features/posts/FeedCardContext.tsx +++ b/packages/shared/src/features/posts/FeedCardContext.tsx @@ -4,8 +4,6 @@ import type { Author } from '../../graphql/comments'; interface FeedCardContextData { // a boosted post can surface organically, and we want to show the boosted label only if the post surfaced as an ad boostedBy?: Author; - highlighted?: boolean; - collectionEnhancementsEnabled?: boolean; } export const FeedCardContext = createContext({}); diff --git a/packages/shared/src/graphql/posts.ts b/packages/shared/src/graphql/posts.ts index 5871afd723e..96da9429755 100644 --- a/packages/shared/src/graphql/posts.ts +++ b/packages/shared/src/graphql/posts.ts @@ -92,6 +92,10 @@ export const isPostOrSharedPostTwitter = ( ): boolean => isSocialTwitterPost(post) || isSocialTwitterPost(post?.sharedPost as Post); +export const isPostUpdated = ( + post: Pick, +): boolean => !!post.updatedAt && post.updatedAt !== post.createdAt; + /** * For social:twitter quote posts, resolve to the top tweet (the post itself) * rather than the referenced/shared tweet. For all other post types, fall back diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts index ec07c174086..1de697c7b30 100644 --- a/packages/shared/src/lib/featureManagement.ts +++ b/packages/shared/src/lib/featureManagement.ts @@ -33,10 +33,6 @@ export const discussedFeedVersion = new Feature('discussed_feed_version', 2); export const latestFeedVersion = new Feature('latest_feed_version', 2); export const customFeedVersion = new Feature('custom_feed_version', 2); export const featureFeedV2Highlights = new Feature('feed_v2_highlights', false); -export const featureCollectionCardEnhancements = new Feature( - 'collection_card_enhancements', - false, -); // @ts-expect-error stale feature without default export const plusTakeoverContent = new Feature<{ From d22f3a7dec5e79175b336eb3c0f76cebbd2aa502 Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:17:03 +0300 Subject: [PATCH 4/5] chore: drop items-center tweak no longer needed on RaisedLabel Made-with: Cursor --- packages/shared/src/components/cards/common/RaisedLabel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/components/cards/common/RaisedLabel.tsx b/packages/shared/src/components/cards/common/RaisedLabel.tsx index e7141b4efcc..611f4f7f45b 100644 --- a/packages/shared/src/components/cards/common/RaisedLabel.tsx +++ b/packages/shared/src/components/cards/common/RaisedLabel.tsx @@ -63,7 +63,7 @@ export function RaisedLabel({ >
From 90809b852584cb5d6e7716dc1b83ce2646b296eb Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:26:20 +0300 Subject: [PATCH 5/5] feat: show relative time for recent collection updates Use comment-style short format (e.g. "11h ago", "45m ago") for collection "Updated" timestamps that fall on today or yesterday. Older updates keep the absolute date format. Applies to collection grid/list cards and the collection post page. Made-with: Cursor --- .../cards/collection/CollectionGrid.tsx | 2 ++ .../cards/collection/CollectionList.tsx | 4 ++++ .../components/cards/common/PostMetadata.tsx | 4 +++- .../cards/common/list/PostCardHeader.tsx | 1 + .../cards/common/list/PostMetadata.tsx | 4 +++- .../post/collection/CollectionPostContent.tsx | 6 +++++- packages/shared/src/lib/dateFormat.ts | 19 +++++++++++++++++++ 7 files changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/components/cards/collection/CollectionGrid.tsx b/packages/shared/src/components/cards/collection/CollectionGrid.tsx index d40afca7fbd..1f7ee126c6e 100644 --- a/packages/shared/src/components/cards/collection/CollectionGrid.tsx +++ b/packages/shared/src/components/cards/collection/CollectionGrid.tsx @@ -17,6 +17,7 @@ import { usePostImage } from '../../../hooks/post/usePostImage'; import CardOverlay from '../common/CardOverlay'; import PostTags from '../common/PostTags'; import { isPostUpdated } from '../../../graphql/posts'; +import { TimeFormatType } from '../../../lib/dateFormat'; export const CollectionGrid = forwardRef(function CollectionCard( { @@ -78,6 +79,7 @@ export const CollectionGrid = forwardRef(function CollectionCard( diff --git a/packages/shared/src/components/cards/common/PostMetadata.tsx b/packages/shared/src/components/cards/common/PostMetadata.tsx index 1fb3479d077..54b438d08fd 100644 --- a/packages/shared/src/components/cards/common/PostMetadata.tsx +++ b/packages/shared/src/components/cards/common/PostMetadata.tsx @@ -23,6 +23,7 @@ interface PostMetadataProps pollMetadata?: PollMetadataProps; numSources?: number; dateLabel?: string; + dateType?: TimeFormatType; } export default function PostMetadata({ @@ -37,6 +38,7 @@ export default function PostMetadata({ pollMetadata, numSources, dateLabel, + dateType = TimeFormatType.Post, }: PostMetadataProps): ReactElement { const hasUpvoteCount = typeof numUpvotes === 'number'; const upvoteCount = numUpvotes ?? 0; @@ -70,7 +72,7 @@ export default function PostMetadata({ node: ( ), diff --git a/packages/shared/src/components/cards/common/list/PostCardHeader.tsx b/packages/shared/src/components/cards/common/list/PostCardHeader.tsx index ea50564bb58..716f16f5edb 100644 --- a/packages/shared/src/components/cards/common/list/PostCardHeader.tsx +++ b/packages/shared/src/components/cards/common/list/PostCardHeader.tsx @@ -45,6 +45,7 @@ interface CardHeaderProps { createdAt?: PostMetadataProps['createdAt']; dateLabel?: PostMetadataProps['dateLabel']; numSources?: PostMetadataProps['numSources']; + dateType?: PostMetadataProps['dateType']; }; } diff --git a/packages/shared/src/components/cards/common/list/PostMetadata.tsx b/packages/shared/src/components/cards/common/list/PostMetadata.tsx index e9ceedd4e88..053a1ebb491 100644 --- a/packages/shared/src/components/cards/common/list/PostMetadata.tsx +++ b/packages/shared/src/components/cards/common/list/PostMetadata.tsx @@ -22,6 +22,7 @@ export interface PostMetadataProps { dateFirst?: boolean; dateLabel?: string; numSources?: number; + dateType?: TimeFormatType; } export default function PostMetadata({ @@ -32,6 +33,7 @@ export default function PostMetadata({ dateFirst, dateLabel, numSources, + dateType = TimeFormatType.Post, }: PostMetadataProps): ReactElement { const { boostedBy } = useFeedCardContext(); const promotedText = useScrambler( @@ -41,7 +43,7 @@ export default function PostMetadata({ const dateNode = !!createdAt && ( ); diff --git a/packages/shared/src/components/post/collection/CollectionPostContent.tsx b/packages/shared/src/components/post/collection/CollectionPostContent.tsx index 39ca30d61bf..728702d0f3b 100644 --- a/packages/shared/src/components/post/collection/CollectionPostContent.tsx +++ b/packages/shared/src/components/post/collection/CollectionPostContent.tsx @@ -162,7 +162,11 @@ const CollectionPostContentRaw = ({
{hasSources && ( diff --git a/packages/shared/src/lib/dateFormat.ts b/packages/shared/src/lib/dateFormat.ts index 1c319328e9a..1f73e214458 100644 --- a/packages/shared/src/lib/dateFormat.ts +++ b/packages/shared/src/lib/dateFormat.ts @@ -73,6 +73,7 @@ export const publishTimeLiveTimer: typeof publishTimeRelativeShort = ( export enum TimeFormatType { Post = 'post', + PostUpdated = 'postUpdated', Comment = 'comment', ReadHistory = 'readHistory', TopReaderBadge = 'topReaderBadge', @@ -114,6 +115,20 @@ export function postDateFormat( return date.toLocaleString('en-US', options); } +export function postUpdatedDateFormat( + value: Date | number | string, + now = new Date(), +): string { + const date = new Date(value); + + if (isSameDay(date, now) || isSameDay(date, subDays(now, 1))) { + const relative = publishTimeRelativeShort(value, now); + return relative === 'now' ? relative : `${relative} ago`; + } + + return postDateFormat(value, now); +} + export function commentDateFormat( value: Date | number | string, now = new Date(), @@ -287,6 +302,10 @@ export const formatDate = ({ value, type, now }: FormatDateProps): string => { return postDateFormat(date); } + if (type === TimeFormatType.PostUpdated) { + return postUpdatedDateFormat(date); + } + if (type === TimeFormatType.Comment) { return publishTimeRelativeShort(date); }