Skip to content

Commit bca3471

Browse files
authored
feat: post highlight new cards (#6101)
1 parent 0f8e491 commit bca3471

11 files changed

Lines changed: 1034 additions & 115 deletions

File tree

packages/shared/src/components/Feed.tsx

Lines changed: 121 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import useFeedInfiniteScroll, {
2424
InfiniteScrollScreenOffset,
2525
} from '../hooks/feed/useFeedInfiniteScroll';
2626
import FeedItemComponent, { getFeedItemKey } from './FeedItemComponent';
27+
import { computeColSpans } from '../lib/feedHighlightColSpan';
28+
import type { FeaturedWideColSpan } from './cards/article/ArticleFeaturedWideGridCard';
2729
import { useLogContext } from '../contexts/LogContext';
2830
import { feedLogExtra, postLogEvent } from '../lib/feed';
2931
import { usePostModalNavigation } from '../hooks/usePostModalNavigation';
@@ -67,6 +69,7 @@ import {
6769
briefCardFeedFeature,
6870
briefFeedEntrypointPage,
6971
featureFeedAdTemplate,
72+
featurePostHighlightCards,
7073
} from '../lib/featureManagement';
7174
import { useNewD1ExperienceFeature } from '../hooks/useNewD1ExperienceFeature';
7275
import type { AwardProps } from '../graphql/njord';
@@ -372,6 +375,59 @@ export default function Feed<T>({
372375
firstSlotOffset: Number(showProfileCompletionCard || showBriefCard),
373376
});
374377

378+
const isMobileViewport = !isTabletViewport;
379+
const isListContext = useList || shouldUseListFeedLayout;
380+
const canRenderHighlightCards =
381+
!isMobileViewport && !isListContext && virtualizedNumCards > 1;
382+
const { value: isPostHighlightCardsEnabled } = useConditionalFeature({
383+
feature: featurePostHighlightCards,
384+
shouldEvaluate: canRenderHighlightCards,
385+
});
386+
const isHighlightCardLayoutEnabled =
387+
canRenderHighlightCards && isPostHighlightCardsEnabled;
388+
const currentPageSize = pageSize ?? currentSettings.pageSize;
389+
const showPromoBanner = !!briefBannerPage;
390+
const columnsDiffWithPage = currentPageSize % virtualizedNumCards;
391+
const indexWhenShowingPromoBanner =
392+
currentPageSize * Number(briefBannerPage) - // number of items at that page
393+
columnsDiffWithPage * Number(briefBannerPage) - // cards let out of rows * page number
394+
Number(showFirstSlotCard);
395+
396+
const fullRowInsertionBeforeIndex = useMemo(() => {
397+
const set = new Set<number>();
398+
if (showPromoBanner) {
399+
set.add(indexWhenShowingPromoBanner);
400+
}
401+
if (shouldShowInFeedHero) {
402+
set.add(adjustedHeroInsertIndex);
403+
}
404+
return set;
405+
}, [
406+
showPromoBanner,
407+
indexWhenShowingPromoBanner,
408+
shouldShowInFeedHero,
409+
adjustedHeroInsertIndex,
410+
]);
411+
412+
const itemColSpans = useMemo(
413+
() =>
414+
computeColSpans(items, {
415+
numCards: virtualizedNumCards,
416+
isMobile: isMobileViewport,
417+
isList: isListContext,
418+
isEnabled: isHighlightCardLayoutEnabled,
419+
fullRowInsertionBeforeIndex,
420+
}),
421+
[
422+
items,
423+
virtualizedNumCards,
424+
isMobileViewport,
425+
isListContext,
426+
isHighlightCardLayoutEnabled,
427+
fullRowInsertionBeforeIndex,
428+
],
429+
);
430+
375431
useMutationSubscription({
376432
matcher: ({ mutation }) => {
377433
const [requestKey] = Array.isArray(mutation.options.mutationKey)
@@ -638,14 +694,6 @@ export default function Feed<T>({
638694
feedName as SharedFeedPage,
639695
);
640696

641-
const currentPageSize = pageSize ?? currentSettings.pageSize;
642-
const showPromoBanner = !!briefBannerPage;
643-
const columnsDiffWithPage = currentPageSize % virtualizedNumCards;
644-
const indexWhenShowingPromoBanner =
645-
currentPageSize * Number(briefBannerPage) - // number of items at that page
646-
columnsDiffWithPage * Number(briefBannerPage) - // cards let out of rows * page number
647-
Number(showFirstSlotCard);
648-
649697
const FeedWrapperComponent = isSearchPageLaptop
650698
? SearchResultsLayout
651699
: FeedContainer;
@@ -697,45 +745,14 @@ export default function Feed<T>({
697745
}}
698746
/>
699747
)}
700-
{items.map((item, index) => (
701-
<FeedCardContext.Provider
702-
key={getFeedItemKey(item, index)}
703-
value={{
704-
boostedBy: isBoostedPostAd(item)
705-
? item.ad.data?.post?.author || item.ad.data?.post?.scout
706-
: undefined,
707-
}}
708-
>
709-
{showPromoBanner && index === indexWhenShowingPromoBanner && (
710-
<BriefBannerFeed
711-
style={{
712-
gridColumn: !shouldUseListFeedLayout
713-
? `span ${virtualizedNumCards}`
714-
: undefined,
715-
}}
716-
/>
717-
)}
718-
{shouldShowInFeedHero && index === adjustedHeroInsertIndex && (
719-
<div
720-
style={{
721-
gridColumn: !shouldUseListFeedLayout
722-
? `span ${virtualizedNumCards}`
723-
: undefined,
724-
}}
725-
>
726-
<TopHero
727-
className="pt-0"
728-
title={readingReminderTitle}
729-
subtitle={readingReminderSubtitle}
730-
onCtaClick={() =>
731-
onEnableHero(NotificationCtaPlacement.InFeedHero)
732-
}
733-
onClose={() =>
734-
onDismissHero(NotificationCtaPlacement.InFeedHero)
735-
}
736-
/>
737-
</div>
738-
)}
748+
{items.map((item, index) => {
749+
const colSpan = itemColSpans[index] ?? 1;
750+
const isWidened = colSpan > 1;
751+
const wideColSpan =
752+
isWidened && (colSpan === 2 || colSpan === 3 || colSpan === 4)
753+
? (colSpan as FeaturedWideColSpan)
754+
: undefined;
755+
const itemNode = (
739756
<FeedItemComponent
740757
item={item}
741758
index={index}
@@ -758,9 +775,64 @@ export default function Feed<T>({
758775
onReadArticleClick={onReadArticleClick}
759776
virtualizedNumCards={virtualizedNumCards}
760777
disableAdRefresh={disableAdRefresh}
778+
wideColSpan={wideColSpan}
761779
/>
762-
</FeedCardContext.Provider>
763-
))}
780+
);
781+
782+
return (
783+
<FeedCardContext.Provider
784+
key={getFeedItemKey(item, index)}
785+
value={{
786+
boostedBy: isBoostedPostAd(item)
787+
? item.ad.data?.post?.author || item.ad.data?.post?.scout
788+
: undefined,
789+
}}
790+
>
791+
{showPromoBanner && index === indexWhenShowingPromoBanner && (
792+
<BriefBannerFeed
793+
style={{
794+
gridColumn: !shouldUseListFeedLayout
795+
? `span ${virtualizedNumCards}`
796+
: undefined,
797+
}}
798+
/>
799+
)}
800+
{shouldShowInFeedHero &&
801+
index === adjustedHeroInsertIndex && (
802+
<div
803+
style={{
804+
gridColumn: !shouldUseListFeedLayout
805+
? `span ${virtualizedNumCards}`
806+
: undefined,
807+
}}
808+
>
809+
<TopHero
810+
className="pt-0"
811+
title={readingReminderTitle}
812+
subtitle={readingReminderSubtitle}
813+
onCtaClick={() =>
814+
onEnableHero(NotificationCtaPlacement.InFeedHero)
815+
}
816+
onClose={() =>
817+
onDismissHero(NotificationCtaPlacement.InFeedHero)
818+
}
819+
/>
820+
</div>
821+
)}
822+
{isWidened ? (
823+
<div
824+
className="flex h-full w-full [&>*]:h-full [&>*]:w-full"
825+
style={{ gridColumn: `span ${colSpan}` }}
826+
data-testid="feedItemColSpanWrapper"
827+
>
828+
{itemNode}
829+
</div>
830+
) : (
831+
itemNode
832+
)}
833+
</FeedCardContext.Provider>
834+
);
835+
})}
764836
{!isFetching && !isInitialLoading && !isHorizontal && (
765837
<InfiniteScrollScreenOffset ref={infiniteScrollRef} />
766838
)}

packages/shared/src/components/FeedItemComponent.tsx

Lines changed: 69 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { FreeformList } from './cards/Freeform/FreeformList';
2828
import type { PostClick } from '../lib/click';
2929
import { ArticleList } from './cards/article/ArticleList';
3030
import { ArticleGrid } from './cards/article/ArticleGrid';
31+
import { ArticleFeaturedWideGridCard } from './cards/article/ArticleFeaturedWideGridCard';
32+
import type { FeaturedWideColSpan } from './cards/article/ArticleFeaturedWideGridCard';
3133
import { ShareGrid } from './cards/share/ShareGrid';
3234
import { ShareList } from './cards/share/ShareList';
3335
import { CollectionGrid } from './cards/collection';
@@ -98,6 +100,12 @@ export type FeedItemComponentProps = {
98100
) => unknown;
99101
virtualizedNumCards: number;
100102
disableAdRefresh?: boolean;
103+
/**
104+
* When set, render the post as a wide featured highlight card spanning
105+
* the given number of grid columns. Only used for article-like post
106+
* types with an active `postHighlight`.
107+
*/
108+
wideColSpan?: FeaturedWideColSpan;
101109
} & Pick<UseVotePost, 'toggleUpvote' | 'toggleDownvote'> &
102110
Pick<UseBookmarkPost, 'toggleBookmark'>;
103111

@@ -280,6 +288,7 @@ function FeedItemComponent({
280288
onCommentClick,
281289
onReadArticleClick,
282290
virtualizedNumCards,
291+
wideColSpan,
283292
}: FeedItemComponentProps): ReactElement | null {
284293
const { logEvent } = useLogContext();
285294
const inViewRef = useLogImpression(
@@ -290,6 +299,7 @@ function FeedItemComponent({
290299
row,
291300
feedName,
292301
ranking,
302+
wideColSpan,
293303
);
294304

295305
const { shouldUseListFeedLayout, shouldUseListMode } = useFeedLayout();
@@ -395,74 +405,67 @@ function FeedItemComponent({
395405
return null;
396406
}
397407

408+
const postCardProps = {
409+
enableSourceHeader:
410+
feedName !== 'squad' && isSourceSquadOrMachine(itemPost.source),
411+
ref: inViewRef,
412+
post: { ...itemPost },
413+
'data-testid': 'postItem',
414+
onUpvoteClick: (post: Post, origin = Origin.Feed) => {
415+
toggleUpvote({
416+
payload: post,
417+
origin,
418+
opts: { columns, column, row },
419+
});
420+
},
421+
onDownvoteClick: (post: Post, origin = Origin.Feed) => {
422+
toggleDownvote({
423+
payload: post,
424+
origin,
425+
opts: { columns, column, row },
426+
});
427+
},
428+
onPostClick: (post: Post, event?: React.MouseEvent) =>
429+
onPostClick(post, index, row, column, false, event),
430+
onPostAuxClick: (post: Post) =>
431+
onPostClick(post, index, row, column, true),
432+
onReadArticleClick: () =>
433+
onReadArticleClick(itemPost, index, row, column),
434+
onShare: (post: Post) => onShare(post, row, column),
435+
onBookmarkClick: (post: Post, origin = Origin.Feed) => {
436+
toggleBookmark({
437+
post,
438+
origin,
439+
opts: { columns, column, row },
440+
});
441+
},
442+
openNewTab,
443+
enableMenu: !!user,
444+
onMenuClick: (event: React.MouseEvent) =>
445+
onMenuClick(event, index, row, column),
446+
onCopyLinkClick: (event: React.MouseEvent, post: Post) =>
447+
onCopyLinkClick(event, post, index, row, column),
448+
menuOpened: postMenuIndex === index,
449+
onCommentClick: (post: Post) =>
450+
onCommentClick(post, index, row, column, !!boostedBy),
451+
eagerLoadImage: row === 0 && column === 0,
452+
};
453+
454+
const isWidenedFeaturedPost =
455+
item.type === FeedItemType.Post && !!wideColSpan && wideColSpan > 1;
456+
398457
return (
399458
<ActivePostContextProvider post={itemPost}>
400-
<PostTag
401-
enableSourceHeader={
402-
feedName !== 'squad' && isSourceSquadOrMachine(itemPost.source)
403-
}
404-
ref={inViewRef}
405-
post={{ ...itemPost }}
406-
data-testid="postItem"
407-
onUpvoteClick={(post: Post, origin = Origin.Feed) => {
408-
toggleUpvote({
409-
payload: post,
410-
origin,
411-
opts: {
412-
columns,
413-
column,
414-
row,
415-
},
416-
});
417-
}}
418-
onDownvoteClick={(post: Post, origin = Origin.Feed) => {
419-
toggleDownvote({
420-
payload: post,
421-
origin,
422-
opts: {
423-
columns,
424-
column,
425-
row,
426-
},
427-
});
428-
}}
429-
onPostClick={(post: Post, event) =>
430-
onPostClick(post, index, row, column, false, event)
431-
}
432-
onPostAuxClick={(post: Post) =>
433-
onPostClick(post, index, row, column, true)
434-
}
435-
onReadArticleClick={() =>
436-
onReadArticleClick(itemPost, index, row, column)
437-
}
438-
onShare={(post: Post) => onShare(post, row, column)}
439-
onBookmarkClick={(post: Post, origin = Origin.Feed) => {
440-
toggleBookmark({
441-
post,
442-
origin,
443-
opts: {
444-
columns,
445-
column,
446-
row,
447-
},
448-
});
449-
}}
450-
openNewTab={openNewTab}
451-
enableMenu={!!user}
452-
onMenuClick={(event: React.MouseEvent) =>
453-
onMenuClick(event, index, row, column)
454-
}
455-
onCopyLinkClick={(event: React.MouseEvent, post: Post) =>
456-
onCopyLinkClick(event, post, index, row, column)
457-
}
458-
menuOpened={postMenuIndex === index}
459-
onCommentClick={(post: Post) =>
460-
onCommentClick(post, index, row, column, !!boostedBy)
461-
}
462-
eagerLoadImage={row === 0 && column === 0}
463-
>
464-
{item.type === FeedItemType.Ad && <AdPixel pixel={item.ad.pixel} />}
465-
</PostTag>
459+
{isWidenedFeaturedPost ? (
460+
<ArticleFeaturedWideGridCard
461+
{...postCardProps}
462+
wideColSpan={wideColSpan}
463+
/>
464+
) : (
465+
<PostTag {...postCardProps}>
466+
{item.type === FeedItemType.Ad && <AdPixel pixel={item.ad.pixel} />}
467+
</PostTag>
468+
)}
466469
</ActivePostContextProvider>
467470
);
468471
}

0 commit comments

Comments
 (0)