An ATproto social media client -- with an independent Appview.

i18n interests, allow for fallbacks (#2692)

authored by

Eric Bailey and committed by
GitHub
bb7ce215 40581746

+103 -56
+3
src/lib/strings/capitalize.ts
···
··· 1 + export function capitalize(str: string) { 2 + return str.charAt(0).toUpperCase() + str.slice(1) 3 + }
+4 -2
src/screens/Onboarding/StepInterests/InterestButton.tsx
··· 4 import {useTheme, atoms as a, native} from '#/alf' 5 import * as Toggle from '#/components/forms/Toggle' 6 import {Text} from '#/components/Typography' 7 8 - import {INTEREST_TO_DISPLAY_NAME} from '#/screens/Onboarding/StepInterests/data' 9 10 export function InterestButton({interest}: {interest: string}) { 11 const t = useTheme() 12 const ctx = Toggle.useItemContext() 13 14 const styles = React.useMemo(() => { ··· 72 native({paddingTop: 2}), 73 ctx.selected ? styles.textSelected : {}, 74 ]}> 75 - {INTEREST_TO_DISPLAY_NAME[interest]} 76 </Text> 77 </View> 78 )
··· 4 import {useTheme, atoms as a, native} from '#/alf' 5 import * as Toggle from '#/components/forms/Toggle' 6 import {Text} from '#/components/Typography' 7 + import {capitalize} from '#/lib/strings/capitalize' 8 9 + import {Context} from '#/screens/Onboarding/state' 10 11 export function InterestButton({interest}: {interest: string}) { 12 const t = useTheme() 13 + const {interestsDisplayNames} = React.useContext(Context) 14 const ctx = Toggle.useItemContext() 15 16 const styles = React.useMemo(() => { ··· 74 native({paddingTop: 2}), 75 ctx.selected ? styles.textSelected : {}, 76 ]}> 77 + {interestsDisplayNames[interest] || capitalize(interest)} 78 </Text> 79 </View> 80 )
-36
src/screens/Onboarding/StepInterests/data.ts
··· 1 - export const INTEREST_TO_DISPLAY_NAME: { 2 - [key: string]: string 3 - } = { 4 - news: 'News', 5 - journalism: 'Journalism', 6 - nature: 'Nature', 7 - art: 'Art', 8 - comics: 'Comics', 9 - writers: 'Writers', 10 - culture: 'Culture', 11 - sports: 'Sports', 12 - pets: 'Pets', 13 - animals: 'Animals', 14 - books: 'Books', 15 - education: 'Education', 16 - climate: 'Climate', 17 - science: 'Science', 18 - politics: 'Politics', 19 - fitness: 'Fitness', 20 - tech: 'Tech', 21 - dev: 'Software Dev', 22 - comedy: 'Comedy', 23 - gaming: 'Video Games', 24 - food: 'Food', 25 - cooking: 'Cooking', 26 - } 27 - 28 - export type ApiResponseMap = { 29 - interests: string[] 30 - suggestedAccountDids: { 31 - [key: string]: string[] 32 - } 33 - suggestedFeedUris: { 34 - [key: string]: string[] 35 - } 36 - }
···
+6 -7
src/screens/Onboarding/StepInterests/index.tsx
··· 17 import {useAnalytics} from '#/lib/analytics/analytics' 18 import {Text} from '#/components/Typography' 19 import {useOnboardingDispatch} from '#/state/shell' 20 21 - import {Context} from '#/screens/Onboarding/state' 22 import { 23 Title, 24 Description, 25 OnboardingControls, 26 } from '#/screens/Onboarding/Layout' 27 - import { 28 - ApiResponseMap, 29 - INTEREST_TO_DISPLAY_NAME, 30 - } from '#/screens/Onboarding/StepInterests/data' 31 import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton' 32 import {IconCircle} from '#/screens/Onboarding/IconCircle' 33 ··· 36 const t = useTheme() 37 const {track} = useAnalytics() 38 const {gtMobile} = useBreakpoints() 39 - const {state, dispatch} = React.useContext(Context) 40 const [saving, setSaving] = React.useState(false) 41 const [interests, setInterests] = React.useState<string[]>( 42 state.interestsStepResults.selectedInterests.map(i => i), ··· 202 <Toggle.Item 203 key={interest} 204 name={interest} 205 - label={INTEREST_TO_DISPLAY_NAME[interest]}> 206 <InterestButton interest={interest} /> 207 </Toggle.Item> 208 ))}
··· 17 import {useAnalytics} from '#/lib/analytics/analytics' 18 import {Text} from '#/components/Typography' 19 import {useOnboardingDispatch} from '#/state/shell' 20 + import {capitalize} from '#/lib/strings/capitalize' 21 22 + import {Context, ApiResponseMap} from '#/screens/Onboarding/state' 23 import { 24 Title, 25 Description, 26 OnboardingControls, 27 } from '#/screens/Onboarding/Layout' 28 import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton' 29 import {IconCircle} from '#/screens/Onboarding/IconCircle' 30 ··· 33 const t = useTheme() 34 const {track} = useAnalytics() 35 const {gtMobile} = useBreakpoints() 36 + const {state, dispatch, interestsDisplayNames} = React.useContext(Context) 37 const [saving, setSaving] = React.useState(false) 38 const [interests, setInterests] = React.useState<string[]>( 39 state.interestsStepResults.selectedInterests.map(i => i), ··· 199 <Toggle.Item 200 key={interest} 201 name={interest} 202 + label={ 203 + interestsDisplayNames[interest] || capitalize(interest) 204 + }> 205 <InterestButton interest={interest} /> 206 </Toggle.Item> 207 ))}
+4 -4
src/screens/Onboarding/StepSuggestedAccounts/index.tsx
··· 14 import * as Toggle from '#/components/forms/Toggle' 15 import {useModerationOpts} from '#/state/queries/preferences' 16 import {useAnalytics} from '#/lib/analytics/analytics' 17 18 import {Context} from '#/screens/Onboarding/state' 19 import { ··· 25 SuggestedAccountCard, 26 SuggestedAccountCardPlaceholder, 27 } from '#/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard' 28 - import {INTEREST_TO_DISPLAY_NAME} from '#/screens/Onboarding/StepInterests/data' 29 import {aggregateInterestItems} from '#/screens/Onboarding/util' 30 import {IconCircle} from '#/screens/Onboarding/IconCircle' 31 ··· 70 export function StepSuggestedAccounts() { 71 const {_} = useLingui() 72 const {track} = useAnalytics() 73 - const {state, dispatch} = React.useContext(Context) 74 const {gtMobile} = useBreakpoints() 75 const suggestedDids = React.useMemo(() => { 76 return aggregateInterestItems( ··· 93 94 const interestsText = React.useMemo(() => { 95 const i = state.interestsStepResults.selectedInterests.map( 96 - i => INTEREST_TO_DISPLAY_NAME[i], 97 ) 98 return i.join(', ') 99 - }, [state.interestsStepResults.selectedInterests]) 100 101 const handleContinue = React.useCallback(async () => { 102 setSaving(true)
··· 14 import * as Toggle from '#/components/forms/Toggle' 15 import {useModerationOpts} from '#/state/queries/preferences' 16 import {useAnalytics} from '#/lib/analytics/analytics' 17 + import {capitalize} from '#/lib/strings/capitalize' 18 19 import {Context} from '#/screens/Onboarding/state' 20 import { ··· 26 SuggestedAccountCard, 27 SuggestedAccountCardPlaceholder, 28 } from '#/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard' 29 import {aggregateInterestItems} from '#/screens/Onboarding/util' 30 import {IconCircle} from '#/screens/Onboarding/IconCircle' 31 ··· 70 export function StepSuggestedAccounts() { 71 const {_} = useLingui() 72 const {track} = useAnalytics() 73 + const {state, dispatch, interestsDisplayNames} = React.useContext(Context) 74 const {gtMobile} = useBreakpoints() 75 const suggestedDids = React.useMemo(() => { 76 return aggregateInterestItems( ··· 93 94 const interestsText = React.useMemo(() => { 95 const i = state.interestsStepResults.selectedInterests.map( 96 + i => interestsDisplayNames[i] || capitalize(i), 97 ) 98 return i.join(', ') 99 + }, [state.interestsStepResults.selectedInterests, interestsDisplayNames]) 100 101 const handleContinue = React.useCallback(async () => { 102 setSaving(true)
+4 -4
src/screens/Onboarding/StepTopicalFeeds.tsx
··· 10 import * as Toggle from '#/components/forms/Toggle' 11 import {Loader} from '#/components/Loader' 12 import {useAnalytics} from '#/lib/analytics/analytics' 13 14 import {Context} from '#/screens/Onboarding/state' 15 import { ··· 18 OnboardingControls, 19 } from '#/screens/Onboarding/Layout' 20 import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard' 21 - import {INTEREST_TO_DISPLAY_NAME} from '#/screens/Onboarding/StepInterests/data' 22 import {aggregateInterestItems} from '#/screens/Onboarding/util' 23 import {IconCircle} from '#/screens/Onboarding/IconCircle' 24 25 export function StepTopicalFeeds() { 26 const {_} = useLingui() 27 const {track} = useAnalytics() 28 - const {state, dispatch} = React.useContext(Context) 29 const [selectedFeedUris, setSelectedFeedUris] = React.useState<string[]>([]) 30 const [saving, setSaving] = React.useState(false) 31 const suggestedFeedUris = React.useMemo(() => { ··· 38 39 const interestsText = React.useMemo(() => { 40 const i = state.interestsStepResults.selectedInterests.map( 41 - i => INTEREST_TO_DISPLAY_NAME[i], 42 ) 43 return i.join(', ') 44 - }, [state.interestsStepResults.selectedInterests]) 45 46 const saveFeeds = React.useCallback(async () => { 47 setSaving(true)
··· 10 import * as Toggle from '#/components/forms/Toggle' 11 import {Loader} from '#/components/Loader' 12 import {useAnalytics} from '#/lib/analytics/analytics' 13 + import {capitalize} from '#/lib/strings/capitalize' 14 15 import {Context} from '#/screens/Onboarding/state' 16 import { ··· 19 OnboardingControls, 20 } from '#/screens/Onboarding/Layout' 21 import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard' 22 import {aggregateInterestItems} from '#/screens/Onboarding/util' 23 import {IconCircle} from '#/screens/Onboarding/IconCircle' 24 25 export function StepTopicalFeeds() { 26 const {_} = useLingui() 27 const {track} = useAnalytics() 28 + const {state, dispatch, interestsDisplayNames} = React.useContext(Context) 29 const [selectedFeedUris, setSelectedFeedUris] = React.useState<string[]>([]) 30 const [saving, setSaving] = React.useState(false) 31 const suggestedFeedUris = React.useMemo(() => { ··· 38 39 const interestsText = React.useMemo(() => { 40 const i = state.interestsStepResults.selectedInterests.map( 41 + i => interestsDisplayNames[i] || capitalize(i), 42 ) 43 return i.join(', ') 44 + }, [state.interestsStepResults.selectedInterests, interestsDisplayNames]) 45 46 const saveFeeds = React.useCallback(async () => { 47 setSaving(true)
+34 -1
src/screens/Onboarding/index.tsx
··· 1 import React from 'react' 2 3 import {Portal} from '#/components/Portal' 4 ··· 13 import {StepModeration} from '#/screens/Onboarding/StepModeration' 14 15 export function Onboarding() { 16 const [state, dispatch] = React.useReducer(reducer, {...initialState}) 17 18 return ( 19 <Portal> 20 <OnboardingControls.Provider> 21 <Context.Provider 22 - value={React.useMemo(() => ({state, dispatch}), [state, dispatch])}> 23 <Layout> 24 {state.activeStep === 'interests' && <StepInterests />} 25 {state.activeStep === 'suggestedAccounts' && (
··· 1 import React from 'react' 2 + import {useLingui} from '@lingui/react' 3 + import {msg} from '@lingui/macro' 4 5 import {Portal} from '#/components/Portal' 6 ··· 15 import {StepModeration} from '#/screens/Onboarding/StepModeration' 16 17 export function Onboarding() { 18 + const {_} = useLingui() 19 const [state, dispatch] = React.useReducer(reducer, {...initialState}) 20 21 + const interestsDisplayNames = React.useMemo(() => { 22 + return { 23 + news: _(msg`News`), 24 + journalism: _(msg`Journalism`), 25 + nature: _(msg`Nature`), 26 + art: _(msg`Art`), 27 + comics: _(msg`Comics`), 28 + writers: _(msg`Writers`), 29 + culture: _(msg`Culture`), 30 + sports: _(msg`Sports`), 31 + pets: _(msg`Pets`), 32 + animals: _(msg`Animals`), 33 + books: _(msg`Books`), 34 + education: _(msg`Education`), 35 + climate: _(msg`Climate`), 36 + science: _(msg`Science`), 37 + politics: _(msg`Politics`), 38 + fitness: _(msg`Fitness`), 39 + tech: _(msg`Tech`), 40 + dev: _(msg`Software Dev`), 41 + comedy: _(msg`Comedy`), 42 + gaming: _(msg`Video Games`), 43 + food: _(msg`Food`), 44 + cooking: _(msg`Cooking`), 45 + } 46 + }, [_]) 47 + 48 return ( 49 <Portal> 50 <OnboardingControls.Provider> 51 <Context.Provider 52 + value={React.useMemo( 53 + () => ({state, dispatch, interestsDisplayNames}), 54 + [state, dispatch, interestsDisplayNames], 55 + )}> 56 <Layout> 57 {state.activeStep === 'interests' && <StepInterests />} 58 {state.activeStep === 'suggestedAccounts' && (
+39 -1
src/screens/Onboarding/state.ts
··· 1 import React from 'react' 2 3 - import {ApiResponseMap} from '#/screens/Onboarding/StepInterests/data' 4 import {logger} from '#/logger' 5 6 export type OnboardingState = { ··· 59 feedUris: string[] 60 } 61 62 export const initialState: OnboardingState = { 63 hasPrev: false, 64 totalSteps: 7, ··· 84 }, 85 } 86 87 export const Context = React.createContext<{ 88 state: OnboardingState 89 dispatch: React.Dispatch<OnboardingAction> 90 }>({ 91 state: {...initialState}, 92 dispatch: () => {}, 93 }) 94 95 export function reducer(
··· 1 import React from 'react' 2 3 import {logger} from '#/logger' 4 5 export type OnboardingState = { ··· 58 feedUris: string[] 59 } 60 61 + export type ApiResponseMap = { 62 + interests: string[] 63 + suggestedAccountDids: { 64 + [key: string]: string[] 65 + } 66 + suggestedFeedUris: { 67 + [key: string]: string[] 68 + } 69 + } 70 + 71 export const initialState: OnboardingState = { 72 hasPrev: false, 73 totalSteps: 7, ··· 93 }, 94 } 95 96 + export const INTEREST_TO_DISPLAY_NAME_DEFAULTS: { 97 + [key: string]: string 98 + } = { 99 + news: 'News', 100 + journalism: 'Journalism', 101 + nature: 'Nature', 102 + art: 'Art', 103 + comics: 'Comics', 104 + writers: 'Writers', 105 + culture: 'Culture', 106 + sports: 'Sports', 107 + pets: 'Pets', 108 + animals: 'Animals', 109 + books: 'Books', 110 + education: 'Education', 111 + climate: 'Climate', 112 + science: 'Science', 113 + politics: 'Politics', 114 + fitness: 'Fitness', 115 + tech: 'Tech', 116 + dev: 'Software Dev', 117 + comedy: 'Comedy', 118 + gaming: 'Video Games', 119 + food: 'Food', 120 + cooking: 'Cooking', 121 + } 122 + 123 export const Context = React.createContext<{ 124 state: OnboardingState 125 dispatch: React.Dispatch<OnboardingAction> 126 + interestsDisplayNames: {[key: string]: string} 127 }>({ 128 state: {...initialState}, 129 dispatch: () => {}, 130 + interestsDisplayNames: INTEREST_TO_DISPLAY_NAME_DEFAULTS, 131 }) 132 133 export function reducer(
+9 -1
src/screens/Onboarding/util.ts
··· 31 const selected = interests.length 32 const all = interests 33 .map(i => { 34 - const suggestions = shuffle(map[i]) 35 36 if (selected === 1) { 37 return suggestions // return all
··· 31 const selected = interests.length 32 const all = interests 33 .map(i => { 34 + // suggestions from server 35 + const rawSuggestions = map[i] 36 + 37 + // safeguard against a missing interest->suggestion mapping 38 + if (!rawSuggestions || !rawSuggestions.length) { 39 + return [] 40 + } 41 + 42 + const suggestions = shuffle(rawSuggestions) 43 44 if (selected === 1) { 45 return suggestions // return all