Skip to content

Commit 0b0536f

Browse files
Merge branch 'main' into feat/fields-v2-redesign
2 parents 0d709d2 + 49cdfe0 commit 0b0536f

31 files changed

Lines changed: 673 additions & 461 deletions

packages/shared/src/components/CustomFeedEmptyScreen.tsx

Lines changed: 25 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import type { Dispatch, ReactElement, SetStateAction } from 'react';
1+
import type { Dispatch, ReactElement, ReactNode, SetStateAction } from 'react';
22
import React from 'react';
33
import { EmptyScreenIcon } from './EmptyScreen';
4-
import { DevPlusIcon, HashtagIcon } from './icons';
4+
import { HashtagIcon } from './icons';
55
import { PageContainer, SharedFeedPage } from './utilities';
6-
import { ButtonSize, ButtonVariant } from './buttons/common';
7-
import { plusUrl } from '../lib/constants';
86
import {
97
DEFAULT_ALGORITHM_INDEX,
108
DEFAULT_ALGORITHM_KEY,
@@ -14,23 +12,16 @@ import usePersistentContext from '../hooks/usePersistentContext';
1412
import {
1513
Typography,
1614
TypographyColor,
17-
TypographyTag,
1815
TypographyType,
1916
} from './typography/Typography';
20-
import { LogEvent, TargetId } from '../lib/log';
21-
import { Button } from './buttons/Button';
22-
import { useConditionalFeature, usePlusSubscription } from '../hooks';
23-
import { IconSize } from './Icon';
24-
import { featurePlusApiLanding } from '../lib/featureManagement';
25-
import Link from './utilities/Link';
2617

27-
export const CustomFeedEmptyScreen = (): ReactElement => {
28-
const { logSubscriptionEvent, isPlus } = usePlusSubscription();
29-
const { value: isApiLanding } = useConditionalFeature({
30-
feature: featurePlusApiLanding,
31-
shouldEvaluate: !isPlus,
32-
});
33-
const plusCta = isApiLanding ? 'Get API Access' : 'Level Up with Plus';
18+
type CustomFeedEmptyScreenProps = {
19+
chips?: ReactNode;
20+
};
21+
22+
export const CustomFeedEmptyScreen = ({
23+
chips,
24+
}: CustomFeedEmptyScreenProps = {}): ReactElement => {
3425
const [selectedAlgo, setSelectedAlgo] = usePersistentContext(
3526
DEFAULT_ALGORITHM_KEY,
3627
DEFAULT_ALGORITHM_INDEX,
@@ -48,10 +39,11 @@ export const CustomFeedEmptyScreen = (): ReactElement => {
4839

4940
return (
5041
<div className="flex w-full flex-col">
51-
<div className="mr-auto mt-0 flex gap-3 tablet:mr-0 tablet:mt-2 laptop:mr-auto laptop:w-auto">
42+
<div className="mt-0 flex w-full gap-3 tablet:mt-2">
5243
<SearchControlHeader
5344
algoState={algoState}
5445
feedName={SharedFeedPage.Custom}
46+
chips={chips}
5547
/>
5648
</div>
5749
<PageContainer className="mx-auto">
@@ -60,69 +52,20 @@ export const CustomFeedEmptyScreen = (): ReactElement => {
6052
className={EmptyScreenIcon.className}
6153
style={EmptyScreenIcon.style}
6254
/>
63-
{!isPlus ? (
64-
<>
65-
<Typography
66-
tag={TypographyTag.Span}
67-
type={TypographyType.Caption1}
68-
className="flex gap-0.5 rounded-4 bg-action-plus-float p-0.5 pr-1"
69-
color={TypographyColor.Plus}
70-
>
71-
<DevPlusIcon size={IconSize.Size16} /> Plus
72-
</Typography>
73-
<Typography
74-
type={TypographyType.Title1}
75-
color={TypographyColor.Primary}
76-
bold
77-
>
78-
Custom feeds got a massive upgrade!
79-
</Typography>
80-
<Typography
81-
type={TypographyType.Callout}
82-
color={TypographyColor.Tertiary}
83-
>
84-
{`Custom Feeds is now more powerful than ever before, with
85-
advanced filters, extensive customization options, and complete
86-
feed control. Upgrade to Plus to unlock this ultimate tool for
87-
tailoring your content.`}
88-
</Typography>
89-
<Link href={plusUrl} passHref>
90-
<Button
91-
className="mt-10"
92-
tag="a"
93-
type="button"
94-
variant={ButtonVariant.Primary}
95-
size={ButtonSize.Medium}
96-
icon={<DevPlusIcon className="text-action-plus-default" />}
97-
onClick={() => {
98-
logSubscriptionEvent({
99-
event_name: LogEvent.UpgradeSubscription,
100-
target_id: TargetId.CustomFeed,
101-
});
102-
}}
103-
>
104-
{plusCta}
105-
</Button>
106-
</Link>
107-
</>
108-
) : (
109-
<>
110-
<Typography
111-
type={TypographyType.Title1}
112-
color={TypographyColor.Primary}
113-
bold
114-
>
115-
Your feed filters are too specific.
116-
</Typography>
117-
<Typography
118-
type={TypographyType.Callout}
119-
color={TypographyColor.Tertiary}
120-
>
121-
We couldn&apos;t fetch enough posts based on your selected tags.
122-
Try adding more tags using the feed settings.
123-
</Typography>
124-
</>
125-
)}
55+
<Typography
56+
type={TypographyType.Title1}
57+
color={TypographyColor.Primary}
58+
bold
59+
>
60+
Your feed filters are too specific.
61+
</Typography>
62+
<Typography
63+
type={TypographyType.Callout}
64+
color={TypographyColor.Tertiary}
65+
>
66+
We couldn&apos;t fetch enough posts based on your selected tags. Try
67+
adding more tags using the feed settings.
68+
</Typography>
12669
</div>
12770
</PageContainer>
12871
</div>

packages/shared/src/components/Feed.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -335,11 +335,10 @@ export default function Feed<T>({
335335
} = useReaderModalEligibility();
336336
const { isOptedOut: isLegacyLayoutOptedOut } = useLegacyPostLayoutOptOut();
337337
const isTabletViewport = useViewSize(ViewSize.Tablet);
338+
// Viewport gating lives in useReaderModalEligibility (isReaderEligible is
339+
// already tablet-or-larger), so no separate isTabletViewport check here.
338340
const isReaderModalOn =
339-
isReaderEligible &&
340-
readerModalFromGrowthBook &&
341-
!isLegacyLayoutOptedOut &&
342-
isTabletViewport;
341+
isReaderEligible && readerModalFromGrowthBook && !isLegacyLayoutOptedOut;
343342
const isReaderModalFeatureReady = !isReaderFeatureLoading;
344343
const readerEligiblePostTypes = useMemo(
345344
() =>

packages/shared/src/components/MainFeedLayout.tsx

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ReactElement, ReactNode, SetStateAction } from 'react';
22
import React, {
3+
cloneElement,
34
useCallback,
45
useContext,
56
useEffect,
@@ -14,7 +15,7 @@ import Feed from './Feed';
1415
import { FeedPageLayoutMobile } from './utilities/common';
1516
import { ExploreChipsBar } from './feeds/ExploreChipsBar';
1617
import { buildPersonalizedCategories } from './feeds/exploreCategories';
17-
import { useFeedTagsList } from '../hooks/useFeedTagsList';
18+
import { useFeeds } from '../hooks/feed/useFeeds';
1819
import ReadingReminderHero from './marketing/banners/ReadingReminderHero';
1920
import { WebappShortcutsRow } from '../features/shortcuts/components/WebappShortcutsRow';
2021
import { LiveStandupsStrip } from './liveRooms/LiveStandupsStrip';
@@ -60,7 +61,8 @@ import {
6061
customFeedVersion,
6162
discussedFeedVersion,
6263
feature,
63-
featureFeedTagChips,
64+
featureFeedChips,
65+
FeedChipsVariant,
6466
followingFeedVersion,
6567
latestFeedVersion,
6668
popularFeedVersion,
@@ -323,29 +325,33 @@ export default function MainFeedLayout({
323325
});
324326

325327
const isChipStripPage =
326-
router.pathname === '/' || router.pathname === '/explore/[tag]';
327-
const { value: isFeedTagChipsEnabled } = useConditionalFeature({
328-
feature: featureFeedTagChips,
328+
router.pathname === '/' ||
329+
router.pathname === '/my-feed' ||
330+
router.pathname === '/explore/[tag]' ||
331+
router.pathname === '/feeds/[slugOrId]' ||
332+
router.pathname === '/feeds/[slugOrId]/edit';
333+
const { value: feedChipsVariant } = useConditionalFeature({
334+
feature: featureFeedChips,
329335
shouldEvaluate: !!user && isLaptop && isChipStripPage,
330336
});
337+
const isFeedChipsEnabled = feedChipsVariant === FeedChipsVariant.V2;
331338
const showExploreChips =
332-
!!user && isLaptop && isChipStripPage && isFeedTagChipsEnabled;
333-
const { tags: feedTags, isPending: isFeedTagsPending } = useFeedTagsList({
334-
enabled: showExploreChips,
335-
});
339+
!!user && isLaptop && isChipStripPage && isFeedChipsEnabled;
340+
const { feeds } = useFeeds();
336341
const exploreCategories = useMemo(
337-
() => buildPersonalizedCategories(feedTags),
338-
[feedTags],
342+
() =>
343+
buildPersonalizedCategories(feeds?.edges ?? [], {
344+
defaultFeedId,
345+
isCustomDefaultFeed,
346+
}),
347+
[feeds?.edges, defaultFeedId, isCustomDefaultFeed],
339348
);
340349
const chipsNode = useMemo(
341350
() =>
342351
showExploreChips ? (
343-
<ExploreChipsBar
344-
categories={exploreCategories}
345-
isPending={isFeedTagsPending}
346-
/>
352+
<ExploreChipsBar categories={exploreCategories} isPending={!feeds} />
347353
) : null,
348-
[showExploreChips, exploreCategories, isFeedTagsPending],
354+
[showExploreChips, exploreCategories, feeds],
349355
);
350356

351357
const { isSearchPageLaptop } = useSearchResultsLayout();
@@ -505,6 +511,14 @@ export default function MainFeedLayout({
505511
return null;
506512
}
507513

514+
const baseEmptyScreen = propsByFeed[feedName]?.emptyScreen || (
515+
<FeedEmptyScreen />
516+
);
517+
const emptyScreenWithChips = cloneElement(
518+
baseEmptyScreen as ReactElement<{ chips?: ReactNode }>,
519+
{ chips: chipsNode },
520+
);
521+
508522
if (feedNameProp === 'default' && isCustomDefaultFeed) {
509523
if (!defaultFeedId) {
510524
return null;
@@ -522,11 +536,14 @@ export default function MainFeedLayout({
522536
feedId: defaultFeedId,
523537
feedName: SharedFeedPage.Custom,
524538
},
525-
emptyScreen: propsByFeed[feedName]?.emptyScreen || <FeedEmptyScreen />,
539+
emptyScreen: emptyScreenWithChips,
526540
actionButtons: feedWithActions && (
527541
<SearchControlHeader
528542
algoState={[selectedAlgo, handleSelectedAlgoChange]}
529-
feedName={feedName}
543+
// On `/` with a custom default feed the rendered feed is the
544+
// custom feed (not MyFeed) — pass `Custom` so derived flags
545+
// (isSortableFeed, etc.) reflect that, not the outer 'default'.
546+
feedName={SharedFeedPage.Custom}
530547
chips={shouldUseListFeedLayout ? undefined : chipsNode}
531548
/>
532549
),
@@ -602,7 +619,7 @@ export default function MainFeedLayout({
602619
),
603620
query: config.query,
604621
variables,
605-
emptyScreen: propsByFeed[feedName]?.emptyScreen || <FeedEmptyScreen />,
622+
emptyScreen: emptyScreenWithChips,
606623
actionButtons: feedWithActions && (
607624
<SearchControlHeader
608625
algoState={[selectedAlgo, handleSelectedAlgoChange]}

packages/shared/src/components/feeds/ExploreChipsBar.tsx

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import React, { useEffect, useMemo, useRef } from 'react';
33
import classNames from 'classnames';
44
import { useRouter } from 'next/router';
55
import Link from '../utilities/Link';
6+
import { PlusIcon } from '../icons';
67
import { webappUrl } from '../../lib/constants';
78
import useCustomDefaultFeed from '../../hooks/feed/useCustomDefaultFeed';
89
import { ElementPlaceholder } from '../ElementPlaceholder';
910
import { useLogContext } from '../../contexts/LogContext';
1011
import type { ExploreCategory } from './exploreCategories';
12+
import { findActiveChipId } from './exploreCategories';
1113
import { LogEvent } from '../../lib/log';
14+
import { NewStripCta } from './NewStripCta';
1215

1316
interface ExploreChipsBarProps {
1417
categories: ExploreCategory[];
@@ -20,14 +23,6 @@ const PLACEHOLDER_WIDTHS = ['w-20', 'w-16', 'w-24', 'w-20', 'w-28', 'w-16'];
2023

2124
const FOR_YOU_CATEGORY_ID = 'foryou';
2225

23-
const normalizePath = (p: string): string => {
24-
const noQuery = p.split('?')[0];
25-
if (!noQuery || noQuery === '/') {
26-
return '/';
27-
}
28-
return noQuery.replace(/\/$/, '');
29-
};
30-
3126
export function ExploreChipsBar({
3227
categories,
3328
isPending,
@@ -37,24 +32,34 @@ export function ExploreChipsBar({
3732
const { isCustomDefaultFeed } = useCustomDefaultFeed();
3833
const { logEvent } = useLogContext();
3934

40-
const forYouCategory: ExploreCategory = useMemo(
41-
() => ({
35+
const forYouCategory: ExploreCategory = useMemo(() => {
36+
const path = isCustomDefaultFeed ? `${webappUrl}my-feed` : webappUrl;
37+
return {
4238
id: FOR_YOU_CATEGORY_ID,
4339
label: 'For you',
44-
path: isCustomDefaultFeed ? `${webappUrl}my-feed` : webappUrl,
45-
}),
46-
[isCustomDefaultFeed],
47-
);
48-
const activePath = useMemo(
49-
() => normalizePath(router.asPath),
50-
[router.asPath],
51-
);
40+
path,
41+
// When a custom feed is the default, `/` shows that feed (not "For you"
42+
// content) — restrict matching to `/my-feed`. Without a custom default
43+
// `/` is MyFeed, so include both.
44+
matchPaths: isCustomDefaultFeed
45+
? [`${webappUrl}my-feed`]
46+
: [path, webappUrl, `${webappUrl}my-feed`],
47+
};
48+
}, [isCustomDefaultFeed]);
5249

5350
const allCategories = useMemo(
5451
() => [forYouCategory, ...categories],
5552
[forYouCategory, categories],
5653
);
5754

55+
const activeId = useMemo(
56+
() =>
57+
findActiveChipId(allCategories, router.asPath, {
58+
preferId: FOR_YOU_CATEGORY_ID,
59+
}),
60+
[allCategories, router.asPath],
61+
);
62+
5863
const scrollRef = useRef<HTMLDivElement>(null);
5964
useEffect(() => {
6065
const active = scrollRef.current?.querySelector<HTMLElement>(
@@ -64,25 +69,17 @@ export function ExploreChipsBar({
6469
return;
6570
}
6671
active.scrollIntoView({ block: 'nearest', inline: 'center' });
67-
}, [activePath, allCategories]);
72+
}, [activeId, allCategories]);
6873

6974
return (
7075
<div className={classNames('relative', className)}>
7176
<div
7277
ref={scrollRef}
7378
className="no-scrollbar flex items-center gap-2 overflow-x-auto pr-12"
7479
>
80+
<NewStripCta className="h-10 rounded-12 px-3" />
7581
{allCategories.map((category) => {
76-
// For You owns the homepage. Match it against both `/` and `/my-feed`
77-
// so the user's default custom feed (also at `/`) doesn't steal the
78-
// active state.
79-
const isForYou = category.id === FOR_YOU_CATEGORY_ID;
80-
const candidates = isForYou
81-
? [category.path, webappUrl, `${webappUrl}my-feed`]
82-
: [category.path];
83-
const isActive = candidates.some(
84-
(candidate) => normalizePath(candidate) === activePath,
85-
);
82+
const isActive = category.id === activeId;
8683
return (
8784
<Link key={category.id} href={category.path}>
8885
<a
@@ -121,6 +118,15 @@ export function ExploreChipsBar({
121118
className={classNames('h-10 shrink-0 rounded-12', width)}
122119
/>
123120
))}
121+
<Link href={`${webappUrl}feeds/new`}>
122+
<a
123+
href={`${webappUrl}feeds/new`}
124+
aria-label="New feed"
125+
className="inline-flex h-10 shrink-0 items-center justify-center rounded-12 border border-transparent bg-background-subtle px-3 text-text-tertiary transition-colors hover:bg-surface-hover hover:text-text-primary"
126+
>
127+
<PlusIcon />
128+
</a>
129+
</Link>
124130
</div>
125131
<div
126132
aria-hidden

0 commit comments

Comments
 (0)