tangled mirror of catsky-🐱 Soothing soft social-app fork with all the niche toggles! (Unofficial); for issues and PRs please put them on github:NekoDrone/catsky-social

Move interests clientside and add `finance` (#9176)

* move interests clientside

* add finance

* try and ensure there's always data for the "all" category

* add empty state for suggested accounts

* simplify error wording

authored by samuel.fm and committed by GitHub be53a168 b56d0c41

Changed files
+180 -330
src
components
lib
screens
state
queries
+5 -1
src/components/Admonition.tsx
··· 7 7 import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX' 8 8 import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 9 9 import {Text as BaseText, type TextProps} from '#/components/Typography' 10 + import {EmojiSad_Stroke2_Corner0_Rounded as EmojiSadIcon} from './icons/Emoji' 10 11 11 12 export const colors = { 12 13 warning: '#FFC404', 13 14 } 14 15 15 16 type Context = { 16 - type: 'info' | 'tip' | 'warning' | 'error' 17 + type: 'info' | 'tip' | 'warning' | 'error' | 'apology' 17 18 } 18 19 19 20 const Context = createContext<Context>({ ··· 29 30 tip: CircleInfoIcon, 30 31 warning: WarningIcon, 31 32 error: CircleXIcon, 33 + apology: EmojiSadIcon, 32 34 }[type] 33 35 const fill = { 34 36 info: t.atoms.text_contrast_medium.color, 35 37 tip: t.palette.primary_500, 36 38 warning: colors.warning, 37 39 error: t.palette.negative_500, 40 + apology: t.atoms.text_contrast_medium.color, 38 41 }[type] 39 42 return <Icon fill={fill} size="md" /> 40 43 } ··· 109 112 tip: t.palette.primary_500, 110 113 warning: colors.warning, 111 114 error: t.palette.negative_500, 115 + apology: t.atoms.border_contrast_high.borderColor, 112 116 }[type] 113 117 return ( 114 118 <Context.Provider value={{type}}>
+1 -4
src/components/ProgressGuide/FollowDialog.tsx
··· 4 4 import {msg, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 7 + import {popularInterests, useInterestsDisplayNames} from '#/lib/interests' 7 8 import {logEvent} from '#/lib/statsig/statsig' 8 9 import {isWeb} from '#/platform/detection' 9 10 import {useModerationOpts} from '#/state/preferences/moderation-opts' ··· 13 14 import {useSession} from '#/state/session' 14 15 import {type Follow10ProgressGuide} from '#/state/shell/progress-guide' 15 16 import {type ListMethods} from '#/view/com/util/List' 16 - import { 17 - popularInterests, 18 - useInterestsDisplayNames, 19 - } from '#/screens/Onboarding/state' 20 17 import { 21 18 atoms as a, 22 19 native,
+78
src/lib/interests.ts
··· 1 + import {useMemo} from 'react' 2 + import {msg} from '@lingui/macro' 3 + import {useLingui} from '@lingui/react' 4 + 5 + export const interests = [ 6 + 'animals', 7 + 'art', 8 + 'books', 9 + 'comedy', 10 + 'comics', 11 + 'culture', 12 + 'dev', 13 + 'education', 14 + 'finance', 15 + 'food', 16 + 'gaming', 17 + 'journalism', 18 + 'movies', 19 + 'music', 20 + 'nature', 21 + 'news', 22 + 'pets', 23 + 'photography', 24 + 'politics', 25 + 'science', 26 + 'sports', 27 + 'tech', 28 + 'tv', 29 + 'writers', 30 + ] as const 31 + export type Interest = (typeof interests)[number] 32 + 33 + // most popular selected interests 34 + export const popularInterests = [ 35 + 'art', 36 + 'gaming', 37 + 'sports', 38 + 'comics', 39 + 'music', 40 + 'politics', 41 + 'photography', 42 + 'science', 43 + 'news', 44 + ] satisfies Interest[] 45 + 46 + export function useInterestsDisplayNames() { 47 + const {_} = useLingui() 48 + 49 + return useMemo<Record<string, string>>(() => { 50 + return { 51 + // Keep this alphabetized 52 + animals: _(msg`Animals`), 53 + art: _(msg`Art`), 54 + books: _(msg`Books`), 55 + comedy: _(msg`Comedy`), 56 + comics: _(msg`Comics`), 57 + culture: _(msg`Culture`), 58 + dev: _(msg`Software Dev`), 59 + education: _(msg`Education`), 60 + finance: _(msg`Finance`), 61 + food: _(msg`Food`), 62 + gaming: _(msg`Video Games`), 63 + journalism: _(msg`Journalism`), 64 + movies: _(msg`Movies`), 65 + music: _(msg`Music`), 66 + nature: _(msg`Nature`), 67 + news: _(msg`News`), 68 + pets: _(msg`Pets`), 69 + photography: _(msg`Photography`), 70 + politics: _(msg`Politics`), 71 + science: _(msg`Science`), 72 + sports: _(msg`Sports`), 73 + tech: _(msg`Tech`), 74 + tv: _(msg`TV`), 75 + writers: _(msg`Writers`), 76 + } satisfies Record<Interest, string> 77 + }, [_]) 78 + }
+2 -2
src/screens/Onboarding/StepInterests/InterestButton.tsx
··· 1 1 import React from 'react' 2 2 import {type TextStyle, View, type ViewStyle} from 'react-native' 3 3 4 + import {type Interest, useInterestsDisplayNames} from '#/lib/interests' 4 5 import {capitalize} from '#/lib/strings/capitalize' 5 - import {useInterestsDisplayNames} from '#/screens/Onboarding/state' 6 6 import {atoms as a, native, useTheme} from '#/alf' 7 7 import * as Toggle from '#/components/forms/Toggle' 8 8 import {Text} from '#/components/Typography' 9 9 10 - export function InterestButton({interest}: {interest: string}) { 10 + export function InterestButton({interest}: {interest: Interest}) { 11 11 const t = useTheme() 12 12 const interestsDisplayNames = useInterestsDisplayNames() 13 13 const ctx = Toggle.useItemContext()
+42 -206
src/screens/Onboarding/StepInterests/index.tsx
··· 2 2 import {View} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 - import {useQuery} from '@tanstack/react-query' 6 5 6 + import {interests, useInterestsDisplayNames} from '#/lib/interests' 7 7 import {logEvent} from '#/lib/statsig/statsig' 8 8 import {capitalize} from '#/lib/strings/capitalize' 9 9 import {logger} from '#/logger' 10 - import {useAgent} from '#/state/session' 11 - import {useOnboardingDispatch} from '#/state/shell' 12 10 import { 13 11 DescriptionText, 14 12 OnboardingControls, 15 13 TitleText, 16 14 } from '#/screens/Onboarding/Layout' 17 - import { 18 - type ApiResponseMap, 19 - Context, 20 - useInterestsDisplayNames, 21 - } from '#/screens/Onboarding/state' 15 + import {Context} from '#/screens/Onboarding/state' 22 16 import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton' 23 - import {atoms as a, useBreakpoints, useTheme} from '#/alf' 17 + import {atoms as a} from '#/alf' 24 18 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 25 19 import * as Toggle from '#/components/forms/Toggle' 26 20 import {IconCircle} from '#/components/IconCircle' 27 - import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwise} from '#/components/icons/ArrowRotateCounterClockwise' 28 21 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 29 - import {EmojiSad_Stroke2_Corner0_Rounded as EmojiSad} from '#/components/icons/Emoji' 30 22 import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' 31 23 import {Loader} from '#/components/Loader' 32 - import {Text} from '#/components/Typography' 33 24 34 25 export function StepInterests() { 35 26 const {_} = useLingui() 36 - const t = useTheme() 37 - const {gtMobile} = useBreakpoints() 38 27 const interestsDisplayNames = useInterestsDisplayNames() 39 28 40 29 const {state, dispatch} = React.useContext(Context) 41 30 const [saving, setSaving] = React.useState(false) 42 - const [interests, setInterests] = React.useState<string[]>( 31 + const [selectedInterests, setSelectedInterests] = React.useState<string[]>( 43 32 state.interestsStepResults.selectedInterests.map(i => i), 44 33 ) 45 - const onboardDispatch = useOnboardingDispatch() 46 - const agent = useAgent() 47 - const {isLoading, isError, error, data, refetch, isFetching} = useQuery({ 48 - queryKey: ['interests'], 49 - queryFn: async () => { 50 - try { 51 - const {data} = await agent.app.bsky.unspecced.getTaggedSuggestions() 52 - return data.suggestions.reduce( 53 - (agg, s) => { 54 - const {tag, subject, subjectType} = s 55 - const isDefault = tag === 'default' 56 - 57 - if (!agg.interests.includes(tag) && !isDefault) { 58 - agg.interests.push(tag) 59 - } 60 - 61 - if (subjectType === 'user') { 62 - agg.suggestedAccountDids[tag] = 63 - agg.suggestedAccountDids[tag] || [] 64 - agg.suggestedAccountDids[tag].push(subject) 65 - } 66 - 67 - if (subjectType === 'feed') { 68 - // agg all feeds into defaults 69 - if (isDefault) { 70 - agg.suggestedFeedUris[tag] = agg.suggestedFeedUris[tag] || [] 71 - } else { 72 - agg.suggestedFeedUris[tag] = agg.suggestedFeedUris[tag] || [] 73 - agg.suggestedFeedUris[tag].push(subject) 74 - agg.suggestedFeedUris.default.push(subject) 75 - } 76 - } 77 - 78 - return agg 79 - }, 80 - { 81 - interests: [], 82 - suggestedAccountDids: {}, 83 - suggestedFeedUris: {}, 84 - } as ApiResponseMap, 85 - ) 86 - } catch (e: any) { 87 - logger.info( 88 - `onboarding: getTaggedSuggestions fetch or processing failed`, 89 - ) 90 - logger.error(e) 91 - 92 - throw new Error(`a network error occurred`) 93 - } 94 - }, 95 - }) 96 34 97 35 const saveInterests = React.useCallback(async () => { 98 36 setSaving(true) ··· 101 39 setSaving(false) 102 40 dispatch({ 103 41 type: 'setInterestsStepResults', 104 - apiResponse: data!, 105 - selectedInterests: interests, 42 + selectedInterests, 106 43 }) 107 44 dispatch({type: 'next'}) 108 45 logEvent('onboarding:interests:nextPressed', { 109 - selectedInterests: interests, 110 - selectedInterestsLength: interests.length, 46 + selectedInterests, 47 + selectedInterestsLength: selectedInterests.length, 111 48 }) 112 49 } catch (e: any) { 113 50 logger.info(`onboading: error saving interests`) 114 51 logger.error(e) 115 52 } 116 - }, [interests, data, setSaving, dispatch]) 117 - 118 - const skipOnboarding = React.useCallback(() => { 119 - onboardDispatch({type: 'finish'}) 120 - dispatch({type: 'finish'}) 121 - }, [onboardDispatch, dispatch]) 122 - 123 - const title = isError ? ( 124 - <Trans>Oh no! Something went wrong.</Trans> 125 - ) : ( 126 - <Trans>What are your interests?</Trans> 127 - ) 128 - const description = isError ? ( 129 - <Trans> 130 - We weren't able to connect. Please try again to continue setting up your 131 - account. If it continues to fail, you can skip this flow. 132 - </Trans> 133 - ) : ( 134 - <Trans>We'll use this to help customize your experience.</Trans> 135 - ) 53 + }, [selectedInterests, setSaving, dispatch]) 136 54 137 55 return ( 138 56 <View style={[a.align_start]} testID="onboardingInterests"> 139 - <IconCircle 140 - icon={isError ? EmojiSad : Hashtag} 141 - style={[ 142 - a.mb_2xl, 143 - isError 144 - ? { 145 - backgroundColor: t.palette.negative_50, 146 - } 147 - : {}, 148 - ]} 149 - iconStyle={[ 150 - isError 151 - ? { 152 - color: t.palette.negative_900, 153 - } 154 - : {}, 155 - ]} 156 - /> 57 + <IconCircle icon={Hashtag} style={[a.mb_2xl]} /> 157 58 158 - <TitleText>{title}</TitleText> 159 - <DescriptionText>{description}</DescriptionText> 59 + <TitleText> 60 + <Trans>What are your interests?</Trans> 61 + </TitleText> 62 + <DescriptionText> 63 + <Trans>We'll use this to help customize your experience.</Trans> 64 + </DescriptionText> 160 65 161 66 <View style={[a.w_full, a.pt_2xl]}> 162 - {isLoading ? ( 163 - <View 164 - style={[ 165 - a.flex_1, 166 - a.mt_md, 167 - a.align_center, 168 - a.justify_center, 169 - {minHeight: 400}, 170 - ]}> 171 - <Loader size="xl" /> 172 - </View> 173 - ) : isError || !data ? ( 174 - <View 175 - style={[ 176 - a.w_full, 177 - a.p_lg, 178 - a.rounded_md, 179 - { 180 - backgroundColor: t.palette.negative_50, 181 - }, 182 - ]}> 183 - <Text style={[a.text_md]}> 184 - <Text 185 - style={[ 186 - a.text_md, 187 - a.font_semi_bold, 188 - { 189 - color: t.palette.negative_900, 190 - }, 191 - ]}> 192 - <Trans>Error:</Trans>{' '} 193 - </Text> 194 - {error?.message || _(msg`an unknown error occurred`)} 195 - </Text> 67 + <Toggle.Group 68 + values={selectedInterests} 69 + onChange={setSelectedInterests} 70 + label={_(msg`Select your interests from the options below`)}> 71 + <View style={[a.flex_row, a.gap_md, a.flex_wrap]}> 72 + {interests.map(interest => ( 73 + <Toggle.Item 74 + key={interest} 75 + name={interest} 76 + label={interestsDisplayNames[interest] || capitalize(interest)}> 77 + <InterestButton interest={interest} /> 78 + </Toggle.Item> 79 + ))} 196 80 </View> 197 - ) : ( 198 - <Toggle.Group 199 - values={interests} 200 - onChange={setInterests} 201 - label={_(msg`Select your interests from the options below`)}> 202 - <View style={[a.flex_row, a.gap_md, a.flex_wrap]}> 203 - {data.interests.map(interest => ( 204 - <Toggle.Item 205 - key={interest} 206 - name={interest} 207 - label={ 208 - interestsDisplayNames[interest] || capitalize(interest) 209 - }> 210 - <InterestButton interest={interest} /> 211 - </Toggle.Item> 212 - ))} 213 - </View> 214 - </Toggle.Group> 215 - )} 81 + </Toggle.Group> 216 82 </View> 217 83 218 84 <OnboardingControls.Portal> 219 - {isError ? ( 220 - <View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}> 221 - <Button 222 - disabled={isFetching} 223 - variant="solid" 224 - color="secondary" 225 - size="large" 226 - label={_(msg`Retry`)} 227 - onPress={() => refetch()}> 228 - <ButtonText> 229 - <Trans>Retry</Trans> 230 - </ButtonText> 231 - <ButtonIcon icon={ArrowRotateCounterClockwise} position="right" /> 232 - </Button> 233 - <Button 234 - variant="outline" 235 - color="secondary" 236 - size="large" 237 - label={_(msg`Skip this flow`)} 238 - onPress={skipOnboarding}> 239 - <ButtonText> 240 - <Trans>Skip</Trans> 241 - </ButtonText> 242 - </Button> 243 - </View> 244 - ) : ( 245 - <Button 246 - disabled={saving || !data} 247 - testID="onboardingContinue" 248 - variant="solid" 249 - color="primary" 250 - size="large" 251 - label={_(msg`Continue to next step`)} 252 - onPress={saveInterests}> 253 - <ButtonText> 254 - <Trans>Continue</Trans> 255 - </ButtonText> 256 - <ButtonIcon 257 - icon={saving ? Loader : ChevronRight} 258 - position="right" 259 - /> 260 - </Button> 261 - )} 85 + <Button 86 + disabled={saving} 87 + testID="onboardingContinue" 88 + variant="solid" 89 + color="primary" 90 + size="large" 91 + label={_(msg`Continue to next step`)} 92 + onPress={saveInterests}> 93 + <ButtonText> 94 + <Trans>Continue</Trans> 95 + </ButtonText> 96 + <ButtonIcon icon={saving ? Loader : ChevronRight} /> 97 + </Button> 262 98 </OnboardingControls.Portal> 263 99 </View> 264 100 )
+13 -6
src/screens/Onboarding/StepSuggestedAccounts/index.tsx
··· 7 7 import * as bcp47Match from 'bcp-47-match' 8 8 9 9 import {wait} from '#/lib/async/wait' 10 + import {popularInterests, useInterestsDisplayNames} from '#/lib/interests' 10 11 import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted' 11 12 import {logger} from '#/logger' 12 13 import {isWeb} from '#/platform/detection' ··· 16 17 import {useAgent, useSession} from '#/state/session' 17 18 import {useOnboardingDispatch} from '#/state/shell' 18 19 import {OnboardingControls} from '#/screens/Onboarding/Layout' 19 - import { 20 - Context, 21 - popularInterests, 22 - useInterestsDisplayNames, 23 - } from '#/screens/Onboarding/state' 20 + import {Context} from '#/screens/Onboarding/state' 24 21 import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers' 25 22 import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf' 26 23 import {Admonition} from '#/components/Admonition' ··· 77 74 }) 78 75 79 76 const isError = !!error 77 + const isEmpty = 78 + !isLoading && suggestedUsers && suggestedUsers.actors.length === 0 80 79 81 80 const skipOnboarding = useCallback(() => { 82 81 onboardDispatch({type: 'finish'}) ··· 171 170 <Loader size="xl" /> 172 171 </View> 173 172 ) : isError ? ( 174 - <View style={[a.flex_1, a.px_xl, a.pt_5xl]}> 173 + <View style={[a.flex_1, a.px_xl, a.pt_2xl]}> 175 174 <Admonition type="error"> 176 175 <Trans> 177 176 An error occurred while fetching suggested accounts. 177 + </Trans> 178 + </Admonition> 179 + </View> 180 + ) : isEmpty ? ( 181 + <View style={[a.flex_1, a.px_xl, a.pt_2xl]}> 182 + <Admonition type="apology"> 183 + <Trans> 184 + Sorry, we're unable to load account suggestions at this time. 178 185 </Trans> 179 186 </Admonition> 180 187 </View>
-66
src/screens/Onboarding/state.ts
··· 1 1 import React from 'react' 2 - import {msg} from '@lingui/macro' 3 - import {useLingui} from '@lingui/react' 4 2 5 3 import {logger} from '#/logger' 6 4 import { ··· 22 20 23 21 interestsStepResults: { 24 22 selectedInterests: string[] 25 - apiResponse: ApiResponseMap 26 23 } 27 24 profileStepResults: { 28 25 isCreatedAvatar: boolean ··· 61 58 | { 62 59 type: 'setInterestsStepResults' 63 60 selectedInterests: string[] 64 - apiResponse: ApiResponseMap 65 61 } 66 62 | { 67 63 type: 'setProfileStepResults' ··· 77 73 | undefined 78 74 } 79 75 80 - export type ApiResponseMap = { 81 - interests: string[] 82 - suggestedAccountDids: { 83 - [key: string]: string[] 84 - } 85 - suggestedFeedUris: { 86 - [key: string]: string[] 87 - } 88 - } 89 - 90 - // most popular selected interests 91 - export const popularInterests = [ 92 - 'art', 93 - 'gaming', 94 - 'sports', 95 - 'comics', 96 - 'music', 97 - 'politics', 98 - 'photography', 99 - 'science', 100 - 'news', 101 - ] 102 - 103 - export function useInterestsDisplayNames() { 104 - const {_} = useLingui() 105 - 106 - return React.useMemo<Record<string, string>>(() => { 107 - return { 108 - // Keep this alphabetized 109 - animals: _(msg`Animals`), 110 - art: _(msg`Art`), 111 - books: _(msg`Books`), 112 - comedy: _(msg`Comedy`), 113 - comics: _(msg`Comics`), 114 - culture: _(msg`Culture`), 115 - dev: _(msg`Software Dev`), 116 - education: _(msg`Education`), 117 - food: _(msg`Food`), 118 - gaming: _(msg`Video Games`), 119 - journalism: _(msg`Journalism`), 120 - movies: _(msg`Movies`), 121 - music: _(msg`Music`), 122 - nature: _(msg`Nature`), 123 - news: _(msg`News`), 124 - pets: _(msg`Pets`), 125 - photography: _(msg`Photography`), 126 - politics: _(msg`Politics`), 127 - science: _(msg`Science`), 128 - sports: _(msg`Sports`), 129 - tech: _(msg`Tech`), 130 - tv: _(msg`TV`), 131 - writers: _(msg`Writers`), 132 - } 133 - }, [_]) 134 - } 135 - 136 76 export const initialState: OnboardingState = { 137 77 hasPrev: false, 138 78 totalSteps: 3, ··· 142 82 143 83 interestsStepResults: { 144 84 selectedInterests: [], 145 - apiResponse: { 146 - interests: [], 147 - suggestedAccountDids: {}, 148 - suggestedFeedUris: {}, 149 - }, 150 85 }, 151 86 profileStepResults: { 152 87 isCreatedAvatar: false, ··· 212 147 case 'setInterestsStepResults': { 213 148 next.interestsStepResults = { 214 149 selectedInterests: a.selectedInterests, 215 - apiResponse: a.apiResponse, 216 150 } 217 151 break 218 152 }
+1 -4
src/screens/Search/Explore.tsx
··· 10 10 import {useQueryClient} from '@tanstack/react-query' 11 11 import * as bcp47Match from 'bcp-47-match' 12 12 13 + import {popularInterests, useInterestsDisplayNames} from '#/lib/interests' 13 14 import {cleanError} from '#/lib/strings/errors' 14 15 import {sanitizeHandle} from '#/lib/strings/handles' 15 16 import {logger} from '#/logger' ··· 41 42 import {List} from '#/view/com/util/List' 42 43 import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 43 44 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 44 - import { 45 - popularInterests, 46 - useInterestsDisplayNames, 47 - } from '#/screens/Onboarding/state' 48 45 import { 49 46 StarterPackCard, 50 47 StarterPackCardSkeleton,
+1 -1
src/screens/Search/modules/ExploreInterestsCard.tsx
··· 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 6 + import {useInterestsDisplayNames} from '#/lib/interests' 6 7 import {Nux, useSaveNux} from '#/state/queries/nuxs' 7 8 import {usePreferencesQuery} from '#/state/queries/preferences' 8 - import {useInterestsDisplayNames} from '#/screens/Onboarding/state' 9 9 import {atoms as a, useTheme} from '#/alf' 10 10 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 11 11 import {Shapes_Stroke2_Corner0_Rounded as Shapes} from '#/components/icons/Shapes'
+1 -4
src/screens/Search/modules/ExploreSuggestedAccounts.tsx
··· 5 5 import {useLingui} from '@lingui/react' 6 6 import {type InfiniteData} from '@tanstack/react-query' 7 7 8 + import {popularInterests, useInterestsDisplayNames} from '#/lib/interests' 8 9 import {logger} from '#/logger' 9 10 import {usePreferencesQuery} from '#/state/queries/preferences' 10 11 import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' 11 - import { 12 - popularInterests, 13 - useInterestsDisplayNames, 14 - } from '#/screens/Onboarding/state' 15 12 import {useTheme} from '#/alf' 16 13 import {atoms as a} from '#/alf' 17 14 import {boostInterests, InterestTabs} from '#/components/InterestTabs'
+1 -1
src/screens/Search/util/useSuggestedUsers.ts
··· 1 1 import {useMemo} from 'react' 2 2 3 + import {useInterestsDisplayNames} from '#/lib/interests' 3 4 import {useActorSearchPaginated} from '#/state/queries/actor-search' 4 5 import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery' 5 - import {useInterestsDisplayNames} from '#/screens/Onboarding/state' 6 6 7 7 /** 8 8 * Conditional hook, used in case a user is a non-english speaker, in which
+7 -29
src/screens/Settings/InterestsSettings.tsx
··· 6 6 import {useQueryClient} from '@tanstack/react-query' 7 7 import debounce from 'lodash.debounce' 8 8 9 + import { 10 + type Interest, 11 + interests as allInterests, 12 + useInterestsDisplayNames, 13 + } from '#/lib/interests' 9 14 import {type CommonNavigatorParams} from '#/lib/routes/types' 10 15 import { 11 16 preferencesQueryKey, ··· 17 22 import {createSuggestedStarterPacksQueryKey} from '#/state/queries/useSuggestedStarterPacksQuery' 18 23 import {useAgent} from '#/state/session' 19 24 import * as Toast from '#/view/com/util/Toast' 20 - import {useInterestsDisplayNames} from '#/screens/Onboarding/state' 21 25 import {atoms as a, useGutters, useTheme} from '#/alf' 22 26 import {Admonition} from '#/components/Admonition' 23 27 import {Divider} from '#/components/Divider' ··· 160 164 onChange={onChangeInterests} 161 165 label={_(msg`Select your interests from the options below`)}> 162 166 <View style={[a.flex_row, a.flex_wrap, a.gap_sm]}> 163 - {INTERESTS.map(interest => { 167 + {allInterests.map(interest => { 164 168 const name = interestsDisplayNames[interest] 165 169 if (!name) return null 166 170 return ( ··· 178 182 ) 179 183 } 180 184 181 - export function InterestButton({interest}: {interest: string}) { 185 + export function InterestButton({interest}: {interest: Interest}) { 182 186 const t = useTheme() 183 187 const interestsDisplayNames = useInterestsDisplayNames() 184 188 const ctx = Toggle.useItemContext() ··· 230 234 </View> 231 235 ) 232 236 } 233 - 234 - const INTERESTS = [ 235 - 'animals', 236 - 'art', 237 - 'books', 238 - 'comedy', 239 - 'comics', 240 - 'culture', 241 - 'dev', 242 - 'education', 243 - 'food', 244 - 'gaming', 245 - 'journalism', 246 - 'movies', 247 - 'music', 248 - 'nature', 249 - 'news', 250 - 'pets', 251 - 'photography', 252 - 'politics', 253 - 'science', 254 - 'sports', 255 - 'tech', 256 - 'tv', 257 - 'writers', 258 - ]
+28 -6
src/state/queries/trending/useGetSuggestedUsersQuery.ts
··· 8 8 aggregateUserInterests, 9 9 createBskyTopicsHeader, 10 10 } from '#/lib/api/feed/utils' 11 + import {logger} from '#/logger' 11 12 import {getContentLanguages} from '#/state/preferences/languages' 12 13 import {STALE} from '#/state/queries' 13 14 import {usePreferencesQuery} from '#/state/queries/preferences' ··· 38 39 queryKey: createGetSuggestedUsersQueryKey(props), 39 40 queryFn: async () => { 40 41 const contentLangs = getContentLanguages().join(',') 41 - const interests = aggregateUserInterests(preferences) 42 + const userInterests = aggregateUserInterests(preferences) 43 + 44 + const interests = 45 + props.overrideInterests && props.overrideInterests.length > 0 46 + ? props.overrideInterests.join(',') 47 + : userInterests 48 + 42 49 const {data} = await agent.app.bsky.unspecced.getSuggestedUsers( 43 50 { 44 51 category: props.category ?? undefined, ··· 46 53 }, 47 54 { 48 55 headers: { 49 - ...createBskyTopicsHeader( 50 - props.overrideInterests && props.overrideInterests.length > 0 51 - ? props.overrideInterests.join(',') 52 - : interests, 53 - ), 56 + ...createBskyTopicsHeader(interests), 54 57 'Accept-Language': contentLangs, 55 58 }, 56 59 }, 57 60 ) 61 + // FALLBACK: if no results for 'all', try again with no interests specified 62 + if (!props.category && data.actors.length === 0) { 63 + logger.error( 64 + `Did not get any suggested users, falling back - interests: ${interests}`, 65 + ) 66 + const {data: fallbackData} = 67 + await agent.app.bsky.unspecced.getSuggestedUsers( 68 + { 69 + category: props.category ?? undefined, 70 + limit: props.limit || 10, 71 + }, 72 + { 73 + headers: { 74 + 'Accept-Language': contentLangs, 75 + }, 76 + }, 77 + ) 78 + return fallbackData 79 + } 58 80 59 81 return data 60 82 },