Bluesky app fork with some witchin' additions 馃挮
at linkat-integration 239 lines 7.3 kB view raw
1import {useMemo, useState} from 'react' 2import {type TextStyle, View, type ViewStyle} from 'react-native' 3import {msg, Trans} from '@lingui/macro' 4import {useLingui} from '@lingui/react' 5import {type NativeStackScreenProps} from '@react-navigation/native-stack' 6import {useQueryClient} from '@tanstack/react-query' 7import debounce from 'lodash.debounce' 8 9import { 10 type Interest, 11 interests as allInterests, 12 useInterestsDisplayNames, 13} from '#/lib/interests' 14import {type CommonNavigatorParams} from '#/lib/routes/types' 15import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 16import { 17 preferencesQueryKey, 18 usePreferencesQuery, 19} from '#/state/queries/preferences' 20import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types' 21import {createGetSuggestedFeedsQueryKey} from '#/state/queries/trending/useGetSuggestedFeedsQuery' 22import {createGetSuggestedUsersQueryKey} from '#/state/queries/trending/useGetSuggestedUsersQuery' 23import {createSuggestedStarterPacksQueryKey} from '#/state/queries/useSuggestedStarterPacksQuery' 24import {useAgent} from '#/state/session' 25import * as Toast from '#/view/com/util/Toast' 26import {atoms as a, useGutters, useTheme} from '#/alf' 27import {Admonition} from '#/components/Admonition' 28import {Divider} from '#/components/Divider' 29import * as Toggle from '#/components/forms/Toggle' 30import * as Layout from '#/components/Layout' 31import {Loader} from '#/components/Loader' 32import {Text} from '#/components/Typography' 33 34type Props = NativeStackScreenProps<CommonNavigatorParams, 'InterestsSettings'> 35export function InterestsSettingsScreen({}: Props) { 36 const t = useTheme() 37 const gutters = useGutters(['base']) 38 const {data: preferences} = usePreferencesQuery() 39 const [isSaving, setIsSaving] = useState(false) 40 41 return ( 42 <Layout.Screen> 43 <Layout.Header.Outer> 44 <Layout.Header.BackButton /> 45 <Layout.Header.Content> 46 <Layout.Header.TitleText> 47 <Trans>Your interests</Trans> 48 </Layout.Header.TitleText> 49 </Layout.Header.Content> 50 <Layout.Header.Slot>{isSaving && <Loader />}</Layout.Header.Slot> 51 </Layout.Header.Outer> 52 <Layout.Content> 53 <View style={[gutters, a.gap_lg]}> 54 <Text 55 style={[ 56 a.flex_1, 57 a.text_sm, 58 a.leading_snug, 59 t.atoms.text_contrast_medium, 60 ]}> 61 <Trans> 62 Your selected interests help us serve you content you care about. 63 </Trans> 64 </Text> 65 66 <Divider /> 67 68 {preferences ? ( 69 <Inner preferences={preferences} setIsSaving={setIsSaving} /> 70 ) : ( 71 <View style={[a.flex_row, a.justify_center, a.p_lg]}> 72 <Loader size="xl" /> 73 </View> 74 )} 75 </View> 76 </Layout.Content> 77 </Layout.Screen> 78 ) 79} 80 81function Inner({ 82 preferences, 83 setIsSaving, 84}: { 85 preferences: UsePreferencesQueryResponse 86 setIsSaving: (isSaving: boolean) => void 87}) { 88 const {_} = useLingui() 89 const agent = useAgent() 90 const qc = useQueryClient() 91 const interestsDisplayNames = useInterestsDisplayNames() 92 const preselectedInterests = useMemo( 93 () => preferences.interests.tags || [], 94 [preferences.interests.tags], 95 ) 96 const [interests, setInterests] = useState<string[]>(preselectedInterests) 97 98 const saveInterests = useMemo(() => { 99 return debounce(async (interests: string[]) => { 100 const noEdits = 101 interests.length === preselectedInterests.length && 102 preselectedInterests.every(pre => { 103 return interests.find(int => int === pre) 104 }) 105 106 if (noEdits) return 107 108 setIsSaving(true) 109 110 try { 111 await agent.setInterestsPref({tags: interests}) 112 qc.setQueriesData( 113 {queryKey: preferencesQueryKey}, 114 (old?: UsePreferencesQueryResponse) => { 115 if (!old) return old 116 old.interests.tags = interests 117 return old 118 }, 119 ) 120 await Promise.all([ 121 qc.resetQueries({queryKey: createSuggestedStarterPacksQueryKey()}), 122 qc.resetQueries({queryKey: createGetSuggestedFeedsQueryKey()}), 123 qc.resetQueries({queryKey: createGetSuggestedUsersQueryKey({})}), 124 ]) 125 126 Toast.show( 127 _( 128 msg({ 129 message: 'Your interests have been updated!', 130 context: 'toast', 131 }), 132 ), 133 ) 134 } catch (error) { 135 Toast.show( 136 _( 137 msg({ 138 message: 'Failed to save your interests.', 139 context: 'toast', 140 }), 141 ), 142 'xmark', 143 ) 144 } finally { 145 setIsSaving(false) 146 } 147 }, 1500) 148 }, [_, agent, setIsSaving, qc, preselectedInterests]) 149 150 const onChangeInterests = async (interests: string[]) => { 151 setInterests(interests) 152 saveInterests(interests) 153 } 154 155 return ( 156 <> 157 {interests.length === 0 && ( 158 <Admonition type="tip"> 159 <Trans>We recommend selecting at least two interests.</Trans> 160 </Admonition> 161 )} 162 163 <Toggle.Group 164 values={interests} 165 onChange={onChangeInterests} 166 label={_(msg`Select your interests from the options below`)}> 167 <View style={[a.flex_row, a.flex_wrap, a.gap_sm]}> 168 {allInterests.map(interest => { 169 const name = interestsDisplayNames[interest] 170 if (!name) return null 171 return ( 172 <Toggle.Item 173 key={interest} 174 name={interest} 175 label={interestsDisplayNames[interest]}> 176 <InterestButton interest={interest} /> 177 </Toggle.Item> 178 ) 179 })} 180 </View> 181 </Toggle.Group> 182 </> 183 ) 184} 185 186export function InterestButton({interest}: {interest: Interest}) { 187 const t = useTheme() 188 const interestsDisplayNames = useInterestsDisplayNames() 189 const ctx = Toggle.useItemContext() 190 191 const enableSquareButtons = useEnableSquareButtons() 192 193 const styles = useMemo(() => { 194 const hovered: ViewStyle[] = [t.atoms.bg_contrast_100] 195 const focused: ViewStyle[] = [] 196 const pressed: ViewStyle[] = [] 197 const selected: ViewStyle[] = [t.atoms.bg_contrast_900] 198 const selectedHover: ViewStyle[] = [t.atoms.bg_contrast_975] 199 const textSelected: TextStyle[] = [t.atoms.text_inverted] 200 201 return { 202 hovered, 203 focused, 204 pressed, 205 selected, 206 selectedHover, 207 textSelected, 208 } 209 }, [t]) 210 211 return ( 212 <View 213 style={[ 214 enableSquareButtons ? a.rounded_sm : a.rounded_full, 215 a.py_md, 216 a.px_xl, 217 t.atoms.bg_contrast_50, 218 ctx.hovered ? styles.hovered : {}, 219 ctx.focused ? styles.hovered : {}, 220 ctx.pressed ? styles.hovered : {}, 221 ctx.selected ? styles.selected : {}, 222 ctx.selected && (ctx.hovered || ctx.focused || ctx.pressed) 223 ? styles.selectedHover 224 : {}, 225 ]}> 226 <Text 227 selectable={false} 228 style={[ 229 { 230 color: t.palette.contrast_900, 231 }, 232 a.font_semi_bold, 233 ctx.selected ? styles.textSelected : {}, 234 ]}> 235 {interestsDisplayNames[interest]} 236 </Text> 237 </View> 238 ) 239}