Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[Explore] Add interests card (#8125)

* Add interests card to top of Explore

* Add tip to add interests if they have none or deselect

* Format

* Copy

authored by

Eric Bailey and committed by
GitHub
a0ff9b52 5c59ec14

+195 -27
+24
src/screens/Search/Explore.tsx
··· 21 21 useFeedPreviews, 22 22 } from '#/state/queries/explore-feed-previews' 23 23 import {useGetPopularFeedsQuery} from '#/state/queries/feed' 24 + import {Nux, useNux} from '#/state/queries/nuxs' 24 25 import {usePreferencesQuery} from '#/state/queries/preferences' 25 26 import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' 26 27 import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery' ··· 36 37 StarterPackCard, 37 38 StarterPackCardSkeleton, 38 39 } from '#/screens/Search/components/StarterPackCard' 40 + import {ExploreInterestsCard} from '#/screens/Search/modules/ExploreInterestsCard' 39 41 import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommendations' 40 42 import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics' 41 43 import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos' ··· 181 183 key: string 182 184 } 183 185 | FeedPreviewItem 186 + | { 187 + type: 'interests-card' 188 + key: 'interests-card' 189 + } 184 190 185 191 export function Explore({ 186 192 focusSearchInput, ··· 233 239 error: feedsError, 234 240 fetchNextPage: fetchNextFeedsPage, 235 241 } = useGetPopularFeedsQuery({limit: 10}) 242 + const interestsNux = useNux(Nux.ExploreInterestsCard) 243 + const showInterestsNux = 244 + interestsNux.status === 'ready' && !interestsNux.nux?.completed 236 245 237 246 const profiles: typeof suggestedProfiles & typeof interestProfiles = 238 247 !selectedInterest ? suggestedProfiles : interestProfiles ··· 558 567 return i 559 568 }, [feedPreviewSlices, isFetchingNextPageFeedPreviews]) 560 569 570 + const interestsNuxModule = useMemo<ExploreScreenItems[]>(() => { 571 + if (!showInterestsNux) return [] 572 + return [ 573 + { 574 + type: 'interests-card', 575 + key: 'interests-card', 576 + }, 577 + ] 578 + }, [showInterestsNux]) 579 + 561 580 const isNewUser = guide?.guide === 'follow-10' && !guide.isComplete 562 581 const items = useMemo<ExploreScreenItems[]>(() => { 563 582 const i: ExploreScreenItems[] = [] ··· 565 584 // Dynamic module ordering 566 585 567 586 i.push(topBorder) 587 + i.push(...interestsNuxModule) 568 588 if (isNewUser) { 569 589 i.push(...suggestedFollowsModule) 570 590 i.push(...suggestedStarterPacksModule) ··· 588 608 suggestedFeedsModule, 589 609 trendingTopicsModule, 590 610 feedPreviewsModule, 611 + interestsNuxModule, 591 612 gate, 592 613 ]) 593 614 ··· 853 874 onPress={fetchNextPageFeedPreviews} 854 875 /> 855 876 ) 877 + } 878 + case 'interests-card': { 879 + return <ExploreInterestsCard /> 856 880 } 857 881 } 858 882 },
+128
src/screens/Search/modules/ExploreInterestsCard.tsx
··· 1 + import {useState} from 'react' 2 + import {View} from 'react-native' 3 + import {msg, Trans} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import {Nux, useSaveNux} from '#/state/queries/nuxs' 7 + import {usePreferencesQuery} from '#/state/queries/preferences' 8 + import {useInterestsDisplayNames} from '#/screens/Onboarding/state' 9 + import {atoms as a, useGutters, useTheme} from '#/alf' 10 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 11 + import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 12 + import {Link} from '#/components/Link' 13 + import * as Prompt from '#/components/Prompt' 14 + import {Text} from '#/components/Typography' 15 + 16 + export function ExploreInterestsCard() { 17 + const t = useTheme() 18 + const {_} = useLingui() 19 + const gutters = useGutters([0, 'base']) 20 + const {data: preferences} = usePreferencesQuery() 21 + const interestsDisplayNames = useInterestsDisplayNames() 22 + const {mutateAsync: saveNux} = useSaveNux() 23 + const trendingPrompt = Prompt.usePromptControl() 24 + const [closing, setClosing] = useState(false) 25 + 26 + const onClose = () => { 27 + trendingPrompt.open() 28 + } 29 + const onConfirmClose = () => { 30 + setClosing(true) 31 + // if this fails, they can try again later 32 + saveNux({ 33 + id: Nux.ExploreInterestsCard, 34 + completed: true, 35 + data: undefined, 36 + }).catch(() => {}) 37 + } 38 + 39 + return closing ? null : ( 40 + <> 41 + <Prompt.Basic 42 + control={trendingPrompt} 43 + title={_(msg`Your interests`)} 44 + description={_( 45 + msg`You can adjust your interests at any time from your "Content and media" settings.`, 46 + )} 47 + confirmButtonCta={_( 48 + msg({ 49 + message: `Copy that!`, 50 + comment: `Confirm button text. Can be a short cheeky phrase that means "OK" e.g. "Copy that!"`, 51 + }), 52 + )} 53 + onConfirm={onConfirmClose} 54 + /> 55 + 56 + <View style={[gutters, a.pt_lg, a.pb_2xs]}> 57 + <View 58 + style={[ 59 + a.p_lg, 60 + a.rounded_md, 61 + a.border, 62 + a.gap_sm, 63 + t.atoms.border_contrast_medium, 64 + t.atoms.bg_contrast_25, 65 + ]}> 66 + <Text style={[a.text_md, a.font_bold, a.leading_tight]}> 67 + <Trans>Your interests</Trans> 68 + </Text> 69 + 70 + {preferences?.interests?.tags && 71 + preferences.interests.tags.length > 0 ? ( 72 + <View style={[a.flex_row, a.flex_wrap, {gap: 6}]}> 73 + {preferences.interests.tags.map(tag => ( 74 + <View 75 + key={tag} 76 + style={[ 77 + a.justify_center, 78 + a.align_center, 79 + a.rounded_full, 80 + a.border, 81 + t.atoms.border_contrast_medium, 82 + a.px_lg, 83 + {height: 32}, 84 + ]}> 85 + <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> 86 + {interestsDisplayNames[tag]} 87 + </Text> 88 + </View> 89 + ))} 90 + </View> 91 + ) : null} 92 + 93 + <Text style={[a.text_sm, a.leading_snug, a.pb_xs]}> 94 + <Trans> 95 + Your selected interests help us serve you content you care about. 96 + </Trans> 97 + </Text> 98 + 99 + <Link 100 + label={_(msg`Edit interests`)} 101 + to="/settings/interests" 102 + size="small" 103 + variant="solid" 104 + color="primary" 105 + style={[a.justify_center]}> 106 + <ButtonText> 107 + <Trans>Edit interests</Trans> 108 + </ButtonText> 109 + </Link> 110 + 111 + <Button 112 + label={_(msg`Hide this card`)} 113 + size="small" 114 + variant="solid" 115 + color="secondary" 116 + shape="round" 117 + onPress={onClose} 118 + style={[ 119 + a.absolute, 120 + {top: a.pt_xs.paddingTop, right: a.pr_xs.paddingRight}, 121 + ]}> 122 + <ButtonIcon icon={X} /> 123 + </Button> 124 + </View> 125 + </View> 126 + </> 127 + ) 128 + }
+29 -21
src/screens/Settings/SettingsInterests.tsx
··· 14 14 import * as Toast from '#/view/com/util/Toast' 15 15 import {useInterestsDisplayNames} from '#/screens/Onboarding/state' 16 16 import {atoms as a, useGutters, useTheme} from '#/alf' 17 + import {Admonition} from '#/components/Admonition' 17 18 import {Divider} from '#/components/Divider' 18 19 import * as Toggle from '#/components/forms/Toggle' 19 20 import * as Layout from '#/components/Layout' ··· 47 48 t.atoms.text_contrast_medium, 48 49 ]}> 49 50 <Trans> 50 - Selecting interests from the list below helps us deliver you 51 - higher quality content. 51 + Your selected interests help us serve you content you care about. 52 52 </Trans> 53 53 </Text> 54 54 ··· 124 124 } 125 125 126 126 return ( 127 - <Toggle.Group 128 - values={interests} 129 - onChange={onChangeInterests} 130 - label={_(msg`Select your interests from the options below`)}> 131 - <View style={[a.flex_row, a.flex_wrap, a.gap_sm]}> 132 - {INTERESTS.map(interest => { 133 - const name = interestsDisplayNames[interest] 134 - if (!name) return null 135 - return ( 136 - <Toggle.Item 137 - key={interest} 138 - name={interest} 139 - label={interestsDisplayNames[interest]}> 140 - <InterestButton interest={interest} /> 141 - </Toggle.Item> 142 - ) 143 - })} 144 - </View> 145 - </Toggle.Group> 127 + <> 128 + {interests.length === 0 && ( 129 + <Admonition type="tip"> 130 + <Trans>We recommend selecting at least two interests.</Trans> 131 + </Admonition> 132 + )} 133 + 134 + <Toggle.Group 135 + values={interests} 136 + onChange={onChangeInterests} 137 + label={_(msg`Select your interests from the options below`)}> 138 + <View style={[a.flex_row, a.flex_wrap, a.gap_sm]}> 139 + {INTERESTS.map(interest => { 140 + const name = interestsDisplayNames[interest] 141 + if (!name) return null 142 + return ( 143 + <Toggle.Item 144 + key={interest} 145 + name={interest} 146 + label={interestsDisplayNames[interest]}> 147 + <InterestButton interest={interest} /> 148 + </Toggle.Item> 149 + ) 150 + })} 151 + </View> 152 + </Toggle.Group> 153 + </> 146 154 ) 147 155 } 148 156
+14 -6
src/state/queries/nuxs/definitions.ts
··· 1 - import zod from 'zod' 1 + import type zod from 'zod' 2 2 3 - import {BaseNux} from '#/state/queries/nuxs/types' 3 + import {type BaseNux} from '#/state/queries/nuxs/types' 4 4 5 5 export enum Nux { 6 6 NeueTypography = 'NeueTypography', 7 + ExploreInterestsCard = 'ExploreInterestsCard', 7 8 } 8 9 9 10 export const nuxNames = new Set(Object.values(Nux)) 10 11 11 - export type AppNux = BaseNux<{ 12 - id: Nux.NeueTypography 13 - data: undefined 14 - }> 12 + export type AppNux = BaseNux< 13 + | { 14 + id: Nux.NeueTypography 15 + data: undefined 16 + } 17 + | { 18 + id: Nux.ExploreInterestsCard 19 + data: undefined 20 + } 21 + > 15 22 16 23 export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = { 17 24 [Nux.NeueTypography]: undefined, 25 + [Nux.ExploreInterestsCard]: undefined, 18 26 }