forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}