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