Bluesky app fork with some witchin' additions 馃挮
fork

Configure Feed

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

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