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