forked from
jollywhoppers.com/witchsky.app
fork
Configure Feed
Select the types of activity you want to include in your feed.
Bluesky app fork with some witchin' additions 馃挮
fork
Configure Feed
Select the types of activity you want to include in your feed.
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}