Skip to content

Commit cd0770b

Browse files
authored
feat: add a no-ai toggle to my feed (#5832)
1 parent e964d83 commit cd0770b

13 files changed

Lines changed: 688 additions & 49 deletions

File tree

packages/shared/src/components/MainFeedLayout.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import { isDevelopment, isProductionAPI, webappUrl } from '../lib/constants';
7171
import { useReadingReminderHero } from '../hooks/notifications/useReadingReminderHero';
7272
import { useTrackQuestClientEvent } from '../hooks/useTrackQuestClientEvent';
7373
import { useReadingReminderVariation } from '../hooks/notifications/useReadingReminderVariation';
74+
import { useNoAiFeed } from '../hooks/useNoAiFeed';
7475

7576
const FeedExploreHeader = dynamic(
7677
() =>
@@ -220,6 +221,8 @@ export default function MainFeedLayout({
220221
hasUser: !!user,
221222
});
222223
const { isCustomDefaultFeed, defaultFeedId } = useCustomDefaultFeed();
224+
const shouldEvaluateNoAi =
225+
feedName === SharedFeedPage.MyFeed && !isCustomDefaultFeed;
223226
const isLaptop = useViewSize(ViewSize.Laptop);
224227
const feedVersion = useFeature(feature.feedVersion);
225228
const { time, contentCurationFilter } = useSearchContextProvider();
@@ -289,6 +292,14 @@ export default function MainFeedLayout({
289292
feature: customFeedVersion,
290293
shouldEvaluate: feedName === SharedFeedPage.Custom,
291294
});
295+
const {
296+
isNoAi,
297+
isNoAiAvailable,
298+
isLoaded: isNoAiLoaded,
299+
toggleNoAi,
300+
} = useNoAiFeed({
301+
shouldEvaluate: shouldEvaluateNoAi,
302+
});
292303

293304
const { isSearchPageLaptop } = useSearchResultsLayout();
294305

@@ -352,6 +363,7 @@ export default function MainFeedLayout({
352363
variables: {
353364
...feedConfig.variables,
354365
...dynamicFeedConfig?.variables,
366+
...(shouldEvaluateNoAi && isNoAi ? { noAi: true } : {}),
355367
version:
356368
isDevelopment && !isProductionAPI
357369
? 1
@@ -372,6 +384,8 @@ export default function MainFeedLayout({
372384
customFeedV,
373385
tokenRefreshed,
374386
feedVersion,
387+
isNoAi,
388+
shouldEvaluateNoAi,
375389
]);
376390

377391
const [selectedAlgo, setSelectedAlgo, loadedAlgo] = usePersistentContext(
@@ -424,6 +438,10 @@ export default function MainFeedLayout({
424438
return null;
425439
}
426440

441+
if (shouldEvaluateNoAi && !isNoAiLoaded) {
442+
return null;
443+
}
444+
427445
if (feedNameProp === 'default' && isCustomDefaultFeed) {
428446
if (!defaultFeedId) {
429447
return null;
@@ -446,6 +464,15 @@ export default function MainFeedLayout({
446464
<SearchControlHeader
447465
algoState={[selectedAlgo, handleSelectedAlgoChange]}
448466
feedName={feedName}
467+
noAiState={
468+
shouldEvaluateNoAi
469+
? {
470+
isAvailable: isNoAiAvailable,
471+
isEnabled: isNoAi,
472+
onToggle: toggleNoAi,
473+
}
474+
: undefined
475+
}
449476
/>
450477
),
451478
};
@@ -525,6 +552,15 @@ export default function MainFeedLayout({
525552
<SearchControlHeader
526553
algoState={[selectedAlgo, handleSelectedAlgoChange]}
527554
feedName={feedName}
555+
noAiState={
556+
shouldEvaluateNoAi
557+
? {
558+
isAvailable: isNoAiAvailable,
559+
isEnabled: isNoAi,
560+
onToggle: toggleNoAi,
561+
}
562+
: undefined
563+
}
528564
/>
529565
),
530566
};
@@ -556,6 +592,11 @@ export default function MainFeedLayout({
556592
isLaptop,
557593
loadedAlgo,
558594
tokenRefreshed,
595+
shouldEvaluateNoAi,
596+
isNoAiLoaded,
597+
isNoAiAvailable,
598+
isNoAi,
599+
toggleNoAi,
559600
]);
560601

561602
useEffect(() => {

packages/shared/src/components/feeds/FeedSettings/sections/FeedSettingsContentPreferencesSection.tsx

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import React, { useContext, useMemo } from 'react';
33
import { FeedSettingsEditContext } from '../FeedSettingsEditContext';
44
import useFeedSettings from '../../../../hooks/useFeedSettings';
55
import { useAdvancedSettings } from '../../../../hooks/feed/useAdvancedSettings';
6+
import { useConditionalFeature, useToastNotification } from '../../../../hooks';
7+
import { useLogContext } from '../../../../contexts/LogContext';
8+
import { useSettingsContext } from '../../../../contexts/SettingsContext';
69
import {
710
getAdvancedContentTypes,
811
getContentCurationList,
@@ -14,21 +17,33 @@ import {
1417
TypographyType,
1518
} from '../../../typography/Typography';
1619
import { FilterCheckbox } from '../../../fields/FilterCheckbox';
20+
import { Switch } from '../../../fields/Switch';
1721
import { FeedType } from '../../../../graphql/feed';
22+
import { featureNoAiFeed } from '../../../../lib/featureManagement';
23+
import { SidebarSettingsFlags } from '../../../../graphql/settings';
24+
import { labels } from '../../../../lib/labels';
25+
import { LogEvent, Origin, TargetId } from '../../../../lib/log';
1826

1927
export const TOGGLEABLE_TYPES = ['Videos', 'Polls', 'Social'];
2028
const CUSTOM_FEEDS_ONLY = ['Article'];
2129
const ADVANCED_SETTINGS_KEY = 'advancedSettings';
2230

2331
export const FeedSettingsContentPreferencesSection = (): ReactElement => {
2432
const { feed, editFeedSettings } = useContext(FeedSettingsEditContext);
33+
const { flags, updateFlag } = useSettingsContext();
34+
const { displayToast } = useToastNotification();
35+
const { logEvent } = useLogContext();
2536
const { advancedSettings } = useFeedSettings({ feedId: feed?.id });
2637
const {
2738
selectedSettings,
2839
onToggleSettings,
2940
checkSourceBlocked,
3041
onToggleSource,
3142
} = useAdvancedSettings({ feedId: feed?.id });
43+
const { value: isNoAiFeatureEnabled } = useConditionalFeature({
44+
feature: featureNoAiFeed,
45+
shouldEvaluate: feed?.type === FeedType.Main,
46+
});
3247
const toggleableTypes = useMemo(
3348
() =>
3449
getAdvancedContentTypes(
@@ -63,7 +78,12 @@ export const FeedSettingsContentPreferencesSection = (): ReactElement => {
6378
</Typography>
6479
</div>
6580
<div className="flex flex-col">
66-
{toggleableTypes.map(({ id, title, defaultEnabledState }) => {
81+
{toggleableTypes.map((setting) => {
82+
if (!setting) {
83+
return null;
84+
}
85+
86+
const { id, title, defaultEnabledState } = setting;
6787
const isDisabled =
6888
CUSTOM_FEEDS_ONLY.includes(title) &&
6989
feed?.type !== FeedType.Custom;
@@ -93,6 +113,47 @@ export const FeedSettingsContentPreferencesSection = (): ReactElement => {
93113
})}
94114
</div>
95115
</div>
116+
{feed?.type === FeedType.Main && isNoAiFeatureEnabled && (
117+
<div className="flex flex-col gap-4">
118+
<div className="flex flex-col gap-1">
119+
<Typography bold type={TypographyType.Body}>
120+
No AI mode
121+
</Typography>
122+
<Typography
123+
type={TypographyType.Callout}
124+
color={TypographyColor.Tertiary}
125+
>
126+
Keep AI topics filtered out across My Feed. You can hide the
127+
homepage toggle once this is set.
128+
</Typography>
129+
</div>
130+
<Switch
131+
inputId="no-ai-feed-preference-switch"
132+
name="no_ai_feed_preference"
133+
compact={false}
134+
checked={flags?.noAiFeedEnabled ?? false}
135+
onClick={() => {
136+
const newState = !(flags?.noAiFeedEnabled ?? false);
137+
138+
editFeedSettings(() =>
139+
updateFlag(SidebarSettingsFlags.NoAiFeedEnabled, newState),
140+
);
141+
displayToast(
142+
newState ? labels.feed.noAi.hidden : labels.feed.noAi.visible,
143+
);
144+
logEvent({
145+
event_name: LogEvent.ToggleNoAiFeed,
146+
target_id: newState ? TargetId.On : TargetId.Off,
147+
extra: JSON.stringify({
148+
origin: Origin.Settings,
149+
}),
150+
});
151+
}}
152+
>
153+
Keep AI topics filtered out
154+
</Switch>
155+
</div>
156+
)}
96157
<div className="flex flex-col gap-4">
97158
<div className="flex flex-col gap-1">
98159
<Typography bold type={TypographyType.Body}>
@@ -105,20 +166,28 @@ export const FeedSettingsContentPreferencesSection = (): ReactElement => {
105166
Pick the categories of content you&apos;d like to see in your feed.
106167
</Typography>
107168
</div>
108-
{contentSourceList?.map(({ id, title, description, options }) => (
109-
<FilterCheckbox
110-
key={id}
111-
name={`${ADVANCED_SETTINGS_KEY}-${id}`}
112-
checked={!checkSourceBlocked(options.source)}
113-
description={description}
114-
onToggleCallback={() =>
115-
editFeedSettings(() => onToggleSource(options.source))
116-
}
117-
descriptionClassName="text-text-tertiary"
118-
>
119-
<Typography bold>{title}</Typography>
120-
</FilterCheckbox>
121-
))}
169+
{contentSourceList?.map(({ id, title, description, options }) => {
170+
if (!options?.source) {
171+
return null;
172+
}
173+
174+
const { source } = options;
175+
176+
return (
177+
<FilterCheckbox
178+
key={id}
179+
name={`${ADVANCED_SETTINGS_KEY}-${id}`}
180+
checked={!checkSourceBlocked(source)}
181+
description={description}
182+
onToggleCallback={() =>
183+
editFeedSettings(() => onToggleSource(source))
184+
}
185+
descriptionClassName="text-text-tertiary"
186+
>
187+
<Typography bold>{title}</Typography>
188+
</FilterCheckbox>
189+
);
190+
})}
122191
{contentCurationList?.map(
123192
({ id, title, description, defaultEnabledState }) => (
124193
<FilterCheckbox

0 commit comments

Comments
 (0)