diff --git a/src/components/Admonition.tsx b/src/components/Admonition.tsx index 8846bb208d..ed644cd374 100644 --- a/src/components/Admonition.tsx +++ b/src/components/Admonition.tsx @@ -7,13 +7,14 @@ import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {Text as BaseText, type TextProps} from '#/components/Typography' +import {EmojiSad_Stroke2_Corner0_Rounded as EmojiSadIcon} from './icons/Emoji' export const colors = { warning: '#FFC404', } type Context = { - type: 'info' | 'tip' | 'warning' | 'error' + type: 'info' | 'tip' | 'warning' | 'error' | 'apology' } const Context = createContext({ @@ -29,12 +30,14 @@ export function Icon() { tip: CircleInfoIcon, warning: WarningIcon, error: CircleXIcon, + apology: EmojiSadIcon, }[type] const fill = { info: t.atoms.text_contrast_medium.color, tip: t.palette.primary_500, warning: colors.warning, error: t.palette.negative_500, + apology: t.atoms.text_contrast_medium.color, }[type] return } @@ -109,6 +112,7 @@ export function Outer({ tip: t.palette.primary_500, warning: colors.warning, error: t.palette.negative_500, + apology: t.atoms.border_contrast_high.borderColor, }[type] return ( diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx index 3dd308e83e..18afa462af 100644 --- a/src/components/ProgressGuide/FollowDialog.tsx +++ b/src/components/ProgressGuide/FollowDialog.tsx @@ -4,6 +4,7 @@ import {type ModerationOpts} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {popularInterests, useInterestsDisplayNames} from '#/lib/interests' import {logEvent} from '#/lib/statsig/statsig' import {isWeb} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -13,10 +14,6 @@ import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggeste import {useSession} from '#/state/session' import {type Follow10ProgressGuide} from '#/state/shell/progress-guide' import {type ListMethods} from '#/view/com/util/List' -import { - popularInterests, - useInterestsDisplayNames, -} from '#/screens/Onboarding/state' import { atoms as a, native, diff --git a/src/lib/interests.ts b/src/lib/interests.ts new file mode 100644 index 0000000000..0d1618fffb --- /dev/null +++ b/src/lib/interests.ts @@ -0,0 +1,78 @@ +import {useMemo} from 'react' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +export const interests = [ + 'animals', + 'art', + 'books', + 'comedy', + 'comics', + 'culture', + 'dev', + 'education', + 'finance', + 'food', + 'gaming', + 'journalism', + 'movies', + 'music', + 'nature', + 'news', + 'pets', + 'photography', + 'politics', + 'science', + 'sports', + 'tech', + 'tv', + 'writers', +] as const +export type Interest = (typeof interests)[number] + +// most popular selected interests +export const popularInterests = [ + 'art', + 'gaming', + 'sports', + 'comics', + 'music', + 'politics', + 'photography', + 'science', + 'news', +] satisfies Interest[] + +export function useInterestsDisplayNames() { + const {_} = useLingui() + + return useMemo>(() => { + return { + // Keep this alphabetized + animals: _(msg`Animals`), + art: _(msg`Art`), + books: _(msg`Books`), + comedy: _(msg`Comedy`), + comics: _(msg`Comics`), + culture: _(msg`Culture`), + dev: _(msg`Software Dev`), + education: _(msg`Education`), + finance: _(msg`Finance`), + food: _(msg`Food`), + gaming: _(msg`Video Games`), + journalism: _(msg`Journalism`), + movies: _(msg`Movies`), + music: _(msg`Music`), + nature: _(msg`Nature`), + news: _(msg`News`), + pets: _(msg`Pets`), + photography: _(msg`Photography`), + politics: _(msg`Politics`), + science: _(msg`Science`), + sports: _(msg`Sports`), + tech: _(msg`Tech`), + tv: _(msg`TV`), + writers: _(msg`Writers`), + } satisfies Record + }, [_]) +} diff --git a/src/screens/Onboarding/StepInterests/InterestButton.tsx b/src/screens/Onboarding/StepInterests/InterestButton.tsx index 66e84bc098..d41e7837c0 100644 --- a/src/screens/Onboarding/StepInterests/InterestButton.tsx +++ b/src/screens/Onboarding/StepInterests/InterestButton.tsx @@ -1,13 +1,13 @@ import React from 'react' import {type TextStyle, View, type ViewStyle} from 'react-native' +import {type Interest, useInterestsDisplayNames} from '#/lib/interests' import {capitalize} from '#/lib/strings/capitalize' -import {useInterestsDisplayNames} from '#/screens/Onboarding/state' import {atoms as a, native, useTheme} from '#/alf' import * as Toggle from '#/components/forms/Toggle' import {Text} from '#/components/Typography' -export function InterestButton({interest}: {interest: string}) { +export function InterestButton({interest}: {interest: Interest}) { const t = useTheme() const interestsDisplayNames = useInterestsDisplayNames() const ctx = Toggle.useItemContext() diff --git a/src/screens/Onboarding/StepInterests/index.tsx b/src/screens/Onboarding/StepInterests/index.tsx index cba7c81734..3e51409757 100644 --- a/src/screens/Onboarding/StepInterests/index.tsx +++ b/src/screens/Onboarding/StepInterests/index.tsx @@ -2,97 +2,35 @@ import React from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useQuery} from '@tanstack/react-query' +import {interests, useInterestsDisplayNames} from '#/lib/interests' import {logEvent} from '#/lib/statsig/statsig' import {capitalize} from '#/lib/strings/capitalize' import {logger} from '#/logger' -import {useAgent} from '#/state/session' -import {useOnboardingDispatch} from '#/state/shell' import { DescriptionText, OnboardingControls, TitleText, } from '#/screens/Onboarding/Layout' -import { - type ApiResponseMap, - Context, - useInterestsDisplayNames, -} from '#/screens/Onboarding/state' +import {Context} from '#/screens/Onboarding/state' import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {atoms as a} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Toggle from '#/components/forms/Toggle' import {IconCircle} from '#/components/IconCircle' -import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwise} from '#/components/icons/ArrowRotateCounterClockwise' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' -import {EmojiSad_Stroke2_Corner0_Rounded as EmojiSad} from '#/components/icons/Emoji' import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' export function StepInterests() { const {_} = useLingui() - const t = useTheme() - const {gtMobile} = useBreakpoints() const interestsDisplayNames = useInterestsDisplayNames() const {state, dispatch} = React.useContext(Context) const [saving, setSaving] = React.useState(false) - const [interests, setInterests] = React.useState( + const [selectedInterests, setSelectedInterests] = React.useState( state.interestsStepResults.selectedInterests.map(i => i), ) - const onboardDispatch = useOnboardingDispatch() - const agent = useAgent() - const {isLoading, isError, error, data, refetch, isFetching} = useQuery({ - queryKey: ['interests'], - queryFn: async () => { - try { - const {data} = await agent.app.bsky.unspecced.getTaggedSuggestions() - return data.suggestions.reduce( - (agg, s) => { - const {tag, subject, subjectType} = s - const isDefault = tag === 'default' - - if (!agg.interests.includes(tag) && !isDefault) { - agg.interests.push(tag) - } - - if (subjectType === 'user') { - agg.suggestedAccountDids[tag] = - agg.suggestedAccountDids[tag] || [] - agg.suggestedAccountDids[tag].push(subject) - } - - if (subjectType === 'feed') { - // agg all feeds into defaults - if (isDefault) { - agg.suggestedFeedUris[tag] = agg.suggestedFeedUris[tag] || [] - } else { - agg.suggestedFeedUris[tag] = agg.suggestedFeedUris[tag] || [] - agg.suggestedFeedUris[tag].push(subject) - agg.suggestedFeedUris.default.push(subject) - } - } - - return agg - }, - { - interests: [], - suggestedAccountDids: {}, - suggestedFeedUris: {}, - } as ApiResponseMap, - ) - } catch (e: any) { - logger.info( - `onboarding: getTaggedSuggestions fetch or processing failed`, - ) - logger.error(e) - - throw new Error(`a network error occurred`) - } - }, - }) const saveInterests = React.useCallback(async () => { setSaving(true) @@ -101,164 +39,62 @@ export function StepInterests() { setSaving(false) dispatch({ type: 'setInterestsStepResults', - apiResponse: data!, - selectedInterests: interests, + selectedInterests, }) dispatch({type: 'next'}) logEvent('onboarding:interests:nextPressed', { - selectedInterests: interests, - selectedInterestsLength: interests.length, + selectedInterests, + selectedInterestsLength: selectedInterests.length, }) } catch (e: any) { logger.info(`onboading: error saving interests`) logger.error(e) } - }, [interests, data, setSaving, dispatch]) - - const skipOnboarding = React.useCallback(() => { - onboardDispatch({type: 'finish'}) - dispatch({type: 'finish'}) - }, [onboardDispatch, dispatch]) - - const title = isError ? ( - Oh no! Something went wrong. - ) : ( - What are your interests? - ) - const description = isError ? ( - - We weren't able to connect. Please try again to continue setting up your - account. If it continues to fail, you can skip this flow. - - ) : ( - We'll use this to help customize your experience. - ) + }, [selectedInterests, setSaving, dispatch]) return ( - + - {title} - {description} + + What are your interests? + + + We'll use this to help customize your experience. + - {isLoading ? ( - - + + + {interests.map(interest => ( + + + + ))} - ) : isError || !data ? ( - - - - Error:{' '} - - {error?.message || _(msg`an unknown error occurred`)} - - - ) : ( - - - {data.interests.map(interest => ( - - - - ))} - - - )} + - {isError ? ( - - - - - ) : ( - - )} + ) diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx index 4bca81fedb..456b52e5ca 100644 --- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx @@ -7,6 +7,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query' import * as bcp47Match from 'bcp-47-match' import {wait} from '#/lib/async/wait' +import {popularInterests, useInterestsDisplayNames} from '#/lib/interests' import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' @@ -16,11 +17,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useAgent, useSession} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import {OnboardingControls} from '#/screens/Onboarding/Layout' -import { - Context, - popularInterests, - useInterestsDisplayNames, -} from '#/screens/Onboarding/state' +import {Context} from '#/screens/Onboarding/state' import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers' import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf' import {Admonition} from '#/components/Admonition' @@ -77,6 +74,8 @@ export function StepSuggestedAccounts() { }) const isError = !!error + const isEmpty = + !isLoading && suggestedUsers && suggestedUsers.actors.length === 0 const skipOnboarding = useCallback(() => { onboardDispatch({type: 'finish'}) @@ -171,13 +170,21 @@ export function StepSuggestedAccounts() { ) : isError ? ( - + An error occurred while fetching suggested accounts. + ) : isEmpty ? ( + + + + Sorry, we're unable to load account suggestions at this time. + + + ) : ( >(() => { - return { - // Keep this alphabetized - animals: _(msg`Animals`), - art: _(msg`Art`), - books: _(msg`Books`), - comedy: _(msg`Comedy`), - comics: _(msg`Comics`), - culture: _(msg`Culture`), - dev: _(msg`Software Dev`), - education: _(msg`Education`), - food: _(msg`Food`), - gaming: _(msg`Video Games`), - journalism: _(msg`Journalism`), - movies: _(msg`Movies`), - music: _(msg`Music`), - nature: _(msg`Nature`), - news: _(msg`News`), - pets: _(msg`Pets`), - photography: _(msg`Photography`), - politics: _(msg`Politics`), - science: _(msg`Science`), - sports: _(msg`Sports`), - tech: _(msg`Tech`), - tv: _(msg`TV`), - writers: _(msg`Writers`), - } - }, [_]) -} - export const initialState: OnboardingState = { hasPrev: false, totalSteps: 3, @@ -142,11 +82,6 @@ export const initialState: OnboardingState = { interestsStepResults: { selectedInterests: [], - apiResponse: { - interests: [], - suggestedAccountDids: {}, - suggestedFeedUris: {}, - }, }, profileStepResults: { isCreatedAvatar: false, @@ -212,7 +147,6 @@ export function reducer( case 'setInterestsStepResults': { next.interestsStepResults = { selectedInterests: a.selectedInterests, - apiResponse: a.apiResponse, } break } diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx index 4624826180..dd5772895d 100644 --- a/src/screens/Search/Explore.tsx +++ b/src/screens/Search/Explore.tsx @@ -10,6 +10,7 @@ import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' import * as bcp47Match from 'bcp-47-match' +import {popularInterests, useInterestsDisplayNames} from '#/lib/interests' import {cleanError} from '#/lib/strings/errors' import {sanitizeHandle} from '#/lib/strings/handles' import {logger} from '#/logger' @@ -41,10 +42,6 @@ import {ViewFullThread} from '#/view/com/posts/ViewFullThread' import {List} from '#/view/com/util/List' import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' -import { - popularInterests, - useInterestsDisplayNames, -} from '#/screens/Onboarding/state' import { StarterPackCard, StarterPackCardSkeleton, diff --git a/src/screens/Search/modules/ExploreInterestsCard.tsx b/src/screens/Search/modules/ExploreInterestsCard.tsx index b5e7b7d2ed..012f1d6e1f 100644 --- a/src/screens/Search/modules/ExploreInterestsCard.tsx +++ b/src/screens/Search/modules/ExploreInterestsCard.tsx @@ -3,9 +3,9 @@ import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useInterestsDisplayNames} from '#/lib/interests' import {Nux, useSaveNux} from '#/state/queries/nuxs' import {usePreferencesQuery} from '#/state/queries/preferences' -import {useInterestsDisplayNames} from '#/screens/Onboarding/state' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Shapes_Stroke2_Corner0_Rounded as Shapes} from '#/components/icons/Shapes' diff --git a/src/screens/Search/modules/ExploreSuggestedAccounts.tsx b/src/screens/Search/modules/ExploreSuggestedAccounts.tsx index 71bfd65472..ea91b1c355 100644 --- a/src/screens/Search/modules/ExploreSuggestedAccounts.tsx +++ b/src/screens/Search/modules/ExploreSuggestedAccounts.tsx @@ -5,13 +5,10 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {type InfiniteData} from '@tanstack/react-query' +import {popularInterests, useInterestsDisplayNames} from '#/lib/interests' import {logger} from '#/logger' import {usePreferencesQuery} from '#/state/queries/preferences' import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' -import { - popularInterests, - useInterestsDisplayNames, -} from '#/screens/Onboarding/state' import {useTheme} from '#/alf' import {atoms as a} from '#/alf' import {boostInterests, InterestTabs} from '#/components/InterestTabs' diff --git a/src/screens/Search/util/useSuggestedUsers.ts b/src/screens/Search/util/useSuggestedUsers.ts index 9ca2c558a7..b1f9064096 100644 --- a/src/screens/Search/util/useSuggestedUsers.ts +++ b/src/screens/Search/util/useSuggestedUsers.ts @@ -1,8 +1,8 @@ import {useMemo} from 'react' +import {useInterestsDisplayNames} from '#/lib/interests' import {useActorSearchPaginated} from '#/state/queries/actor-search' import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery' -import {useInterestsDisplayNames} from '#/screens/Onboarding/state' /** * Conditional hook, used in case a user is a non-english speaker, in which diff --git a/src/screens/Settings/InterestsSettings.tsx b/src/screens/Settings/InterestsSettings.tsx index 4ecaba2884..074a423ec4 100644 --- a/src/screens/Settings/InterestsSettings.tsx +++ b/src/screens/Settings/InterestsSettings.tsx @@ -6,6 +6,11 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {useQueryClient} from '@tanstack/react-query' import debounce from 'lodash.debounce' +import { + type Interest, + interests as allInterests, + useInterestsDisplayNames, +} from '#/lib/interests' import {type CommonNavigatorParams} from '#/lib/routes/types' import { preferencesQueryKey, @@ -17,7 +22,6 @@ import {createGetSuggestedUsersQueryKey} from '#/state/queries/trending/useGetSu import {createSuggestedStarterPacksQueryKey} from '#/state/queries/useSuggestedStarterPacksQuery' import {useAgent} from '#/state/session' import * as Toast from '#/view/com/util/Toast' -import {useInterestsDisplayNames} from '#/screens/Onboarding/state' import {atoms as a, useGutters, useTheme} from '#/alf' import {Admonition} from '#/components/Admonition' import {Divider} from '#/components/Divider' @@ -160,7 +164,7 @@ function Inner({ onChange={onChangeInterests} label={_(msg`Select your interests from the options below`)}> - {INTERESTS.map(interest => { + {allInterests.map(interest => { const name = interestsDisplayNames[interest] if (!name) return null return ( @@ -178,7 +182,7 @@ function Inner({ ) } -export function InterestButton({interest}: {interest: string}) { +export function InterestButton({interest}: {interest: Interest}) { const t = useTheme() const interestsDisplayNames = useInterestsDisplayNames() const ctx = Toggle.useItemContext() @@ -230,29 +234,3 @@ export function InterestButton({interest}: {interest: string}) { ) } - -const INTERESTS = [ - 'animals', - 'art', - 'books', - 'comedy', - 'comics', - 'culture', - 'dev', - 'education', - 'food', - 'gaming', - 'journalism', - 'movies', - 'music', - 'nature', - 'news', - 'pets', - 'photography', - 'politics', - 'science', - 'sports', - 'tech', - 'tv', - 'writers', -] diff --git a/src/state/queries/trending/useGetSuggestedUsersQuery.ts b/src/state/queries/trending/useGetSuggestedUsersQuery.ts index 4ecee2986d..547d7f44fd 100644 --- a/src/state/queries/trending/useGetSuggestedUsersQuery.ts +++ b/src/state/queries/trending/useGetSuggestedUsersQuery.ts @@ -8,6 +8,7 @@ import { aggregateUserInterests, createBskyTopicsHeader, } from '#/lib/api/feed/utils' +import {logger} from '#/logger' import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' @@ -38,7 +39,13 @@ export function useGetSuggestedUsersQuery(props: QueryProps) { queryKey: createGetSuggestedUsersQueryKey(props), queryFn: async () => { const contentLangs = getContentLanguages().join(',') - const interests = aggregateUserInterests(preferences) + const userInterests = aggregateUserInterests(preferences) + + const interests = + props.overrideInterests && props.overrideInterests.length > 0 + ? props.overrideInterests.join(',') + : userInterests + const {data} = await agent.app.bsky.unspecced.getSuggestedUsers( { category: props.category ?? undefined, @@ -46,15 +53,30 @@ export function useGetSuggestedUsersQuery(props: QueryProps) { }, { headers: { - ...createBskyTopicsHeader( - props.overrideInterests && props.overrideInterests.length > 0 - ? props.overrideInterests.join(',') - : interests, - ), + ...createBskyTopicsHeader(interests), 'Accept-Language': contentLangs, }, }, ) + // FALLBACK: if no results for 'all', try again with no interests specified + if (!props.category && data.actors.length === 0) { + logger.error( + `Did not get any suggested users, falling back - interests: ${interests}`, + ) + const {data: fallbackData} = + await agent.app.bsky.unspecced.getSuggestedUsers( + { + category: props.category ?? undefined, + limit: props.limit || 10, + }, + { + headers: { + 'Accept-Language': contentLangs, + }, + }, + ) + return fallbackData + } return data },