mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {Keyboard, View} from 'react-native'
3import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api'
4import {msg, Trans} from '@lingui/macro'
5import {useLingui} from '@lingui/react'
6
7import {logger} from '#/logger'
8import {isNative} from '#/platform/detection'
9import {
10 usePreferencesQuery,
11 useRemoveMutedWordMutation,
12 useUpsertMutedWordsMutation,
13} from '#/state/queries/preferences'
14import {
15 atoms as a,
16 native,
17 useBreakpoints,
18 useTheme,
19 ViewStyleProp,
20 web,
21} from '#/alf'
22import {Button, ButtonIcon, ButtonText} from '#/components/Button'
23import * as Dialog from '#/components/Dialog'
24import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
25import {Divider} from '#/components/Divider'
26import * as Toggle from '#/components/forms/Toggle'
27import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
28import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText'
29import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
30import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
31import {KeyboardPadding} from '#/components/KeyboardPadding'
32import {Loader} from '#/components/Loader'
33import * as Prompt from '#/components/Prompt'
34import {Text} from '#/components/Typography'
35
36export function MutedWordsDialog() {
37 const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext()
38 return (
39 <Dialog.Outer control={control}>
40 <Dialog.Handle />
41 <MutedWordsInner />
42 </Dialog.Outer>
43 )
44}
45
46function MutedWordsInner() {
47 const t = useTheme()
48 const {_} = useLingui()
49 const {gtMobile} = useBreakpoints()
50 const {
51 isLoading: isPreferencesLoading,
52 data: preferences,
53 error: preferencesError,
54 } = usePreferencesQuery()
55 const {isPending, mutateAsync: addMutedWord} = useUpsertMutedWordsMutation()
56 const [field, setField] = React.useState('')
57 const [options, setOptions] = React.useState(['content'])
58 const [error, setError] = React.useState('')
59
60 const submit = React.useCallback(async () => {
61 const sanitizedValue = sanitizeMutedWordValue(field)
62 const targets = ['tag', options.includes('content') && 'content'].filter(
63 Boolean,
64 ) as AppBskyActorDefs.MutedWord['targets']
65
66 if (!sanitizedValue || !targets.length) {
67 setField('')
68 setError(_(msg`Please enter a valid word, tag, or phrase to mute`))
69 return
70 }
71
72 try {
73 // send raw value and rely on SDK as sanitization source of truth
74 await addMutedWord([{value: field, targets}])
75 setField('')
76 } catch (e: any) {
77 logger.error(`Failed to save muted word`, {message: e.message})
78 setError(e.message)
79 }
80 }, [_, field, options, addMutedWord, setField])
81
82 return (
83 <Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}>
84 <View onTouchStart={Keyboard.dismiss}>
85 <Text
86 style={[a.text_md, a.font_bold, a.pb_sm, t.atoms.text_contrast_high]}>
87 <Trans>Add muted words and tags</Trans>
88 </Text>
89 <Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}>
90 <Trans>
91 Posts can be muted based on their text, their tags, or both.
92 </Trans>
93 </Text>
94
95 <View style={[a.pb_lg]}>
96 <Dialog.Input
97 autoCorrect={false}
98 autoCapitalize="none"
99 autoComplete="off"
100 label={_(msg`Enter a word or tag`)}
101 placeholder={_(msg`Enter a word or tag`)}
102 value={field}
103 onChangeText={value => {
104 if (error) {
105 setError('')
106 }
107 setField(value)
108 }}
109 onSubmitEditing={submit}
110 />
111
112 <Toggle.Group
113 label={_(msg`Toggle between muted word options.`)}
114 type="radio"
115 values={options}
116 onChange={setOptions}>
117 <View
118 style={[
119 a.pt_sm,
120 a.py_sm,
121 a.flex_row,
122 a.align_center,
123 a.gap_sm,
124 a.flex_wrap,
125 ]}>
126 <Toggle.Item
127 label={_(msg`Mute this word in post text and tags`)}
128 name="content"
129 style={[a.flex_1, !gtMobile && [a.w_full, a.flex_0]]}>
130 <TargetToggle>
131 <View style={[a.flex_row, a.align_center, a.gap_sm]}>
132 <Toggle.Radio />
133 <Toggle.LabelText>
134 <Trans>Mute in text & tags</Trans>
135 </Toggle.LabelText>
136 </View>
137 <PageText size="sm" />
138 </TargetToggle>
139 </Toggle.Item>
140
141 <Toggle.Item
142 label={_(msg`Mute this word in tags only`)}
143 name="tag"
144 style={[a.flex_1, !gtMobile && [a.w_full, a.flex_0]]}>
145 <TargetToggle>
146 <View style={[a.flex_row, a.align_center, a.gap_sm]}>
147 <Toggle.Radio />
148 <Toggle.LabelText>
149 <Trans>Mute in tags only</Trans>
150 </Toggle.LabelText>
151 </View>
152 <Hashtag size="sm" />
153 </TargetToggle>
154 </Toggle.Item>
155
156 <Button
157 disabled={isPending || !field}
158 label={_(msg`Add mute word for configured settings`)}
159 size="small"
160 color="primary"
161 variant="solid"
162 style={[!gtMobile && [a.w_full, a.flex_0]]}
163 onPress={submit}>
164 <ButtonText>
165 <Trans>Add</Trans>
166 </ButtonText>
167 <ButtonIcon icon={isPending ? Loader : Plus} />
168 </Button>
169 </View>
170 </Toggle.Group>
171
172 {error && (
173 <View
174 style={[
175 a.mb_lg,
176 a.flex_row,
177 a.rounded_sm,
178 a.p_md,
179 a.mb_xs,
180 t.atoms.bg_contrast_25,
181 {
182 backgroundColor: t.palette.negative_400,
183 },
184 ]}>
185 <Text
186 style={[
187 a.italic,
188 {color: t.palette.white},
189 native({marginTop: 2}),
190 ]}>
191 {error}
192 </Text>
193 </View>
194 )}
195
196 <Text
197 style={[
198 a.pt_xs,
199 a.text_sm,
200 a.italic,
201 a.leading_snug,
202 t.atoms.text_contrast_medium,
203 ]}>
204 <Trans>
205 We recommend avoiding common words that appear in many posts,
206 since it can result in no posts being shown.
207 </Trans>
208 </Text>
209 </View>
210
211 <Divider />
212
213 <View style={[a.pt_2xl]}>
214 <Text
215 style={[
216 a.text_md,
217 a.font_bold,
218 a.pb_md,
219 t.atoms.text_contrast_high,
220 ]}>
221 <Trans>Your muted words</Trans>
222 </Text>
223
224 {isPreferencesLoading ? (
225 <Loader />
226 ) : preferencesError || !preferences ? (
227 <View
228 style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}>
229 <Text style={[a.italic, t.atoms.text_contrast_high]}>
230 <Trans>
231 We're sorry, but we weren't able to load your muted words at
232 this time. Please try again.
233 </Trans>
234 </Text>
235 </View>
236 ) : preferences.moderationPrefs.mutedWords.length ? (
237 [...preferences.moderationPrefs.mutedWords]
238 .reverse()
239 .map((word, i) => (
240 <MutedWordRow
241 key={word.value + i}
242 word={word}
243 style={[i % 2 === 0 && t.atoms.bg_contrast_25]}
244 />
245 ))
246 ) : (
247 <View
248 style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}>
249 <Text style={[a.italic, t.atoms.text_contrast_high]}>
250 <Trans>You haven't muted any words or tags yet</Trans>
251 </Text>
252 </View>
253 )}
254 </View>
255
256 {isNative && <View style={{height: 20}} />}
257 </View>
258
259 <Dialog.Close />
260 <KeyboardPadding maxHeight={100} />
261 </Dialog.ScrollableInner>
262 )
263}
264
265function MutedWordRow({
266 style,
267 word,
268}: ViewStyleProp & {word: AppBskyActorDefs.MutedWord}) {
269 const t = useTheme()
270 const {_} = useLingui()
271 const {isPending, mutateAsync: removeMutedWord} = useRemoveMutedWordMutation()
272 const control = Prompt.usePromptControl()
273
274 const remove = React.useCallback(async () => {
275 control.close()
276 removeMutedWord(word)
277 }, [removeMutedWord, word, control])
278
279 return (
280 <>
281 <Prompt.Basic
282 control={control}
283 title={_(msg`Are you sure?`)}
284 description={_(
285 msg`This will delete ${word.value} from your muted words. You can always add it back later.`,
286 )}
287 onConfirm={remove}
288 confirmButtonCta={_(msg`Remove`)}
289 confirmButtonColor="negative"
290 />
291
292 <View
293 style={[
294 a.py_md,
295 a.px_lg,
296 a.flex_row,
297 a.align_center,
298 a.justify_between,
299 a.rounded_md,
300 a.gap_md,
301 style,
302 ]}>
303 <Text
304 style={[
305 a.flex_1,
306 a.leading_snug,
307 a.w_full,
308 a.font_bold,
309 t.atoms.text_contrast_high,
310 web({
311 overflowWrap: 'break-word',
312 wordBreak: 'break-word',
313 }),
314 ]}>
315 {word.value}
316 </Text>
317
318 <View style={[a.flex_row, a.align_center, a.justify_end, a.gap_sm]}>
319 {word.targets.map(target => (
320 <View
321 key={target}
322 style={[a.py_xs, a.px_sm, a.rounded_sm, t.atoms.bg_contrast_100]}>
323 <Text
324 style={[a.text_xs, a.font_bold, t.atoms.text_contrast_medium]}>
325 {target === 'content' ? _(msg`text`) : _(msg`tag`)}
326 </Text>
327 </View>
328 ))}
329
330 <Button
331 label={_(msg`Remove mute word from your list`)}
332 size="tiny"
333 shape="round"
334 variant="ghost"
335 color="secondary"
336 onPress={() => control.open()}
337 style={[a.ml_sm]}>
338 <ButtonIcon icon={isPending ? Loader : X} />
339 </Button>
340 </View>
341 </View>
342 </>
343 )
344}
345
346function TargetToggle({children}: React.PropsWithChildren<{}>) {
347 const t = useTheme()
348 const ctx = Toggle.useItemContext()
349 const {gtMobile} = useBreakpoints()
350 return (
351 <View
352 style={[
353 a.flex_row,
354 a.align_center,
355 a.justify_between,
356 a.gap_xs,
357 a.flex_1,
358 a.py_sm,
359 a.px_sm,
360 gtMobile && a.px_md,
361 a.rounded_sm,
362 t.atoms.bg_contrast_50,
363 (ctx.hovered || ctx.focused) && t.atoms.bg_contrast_100,
364 ctx.selected && [
365 {
366 backgroundColor:
367 t.name === 'light' ? t.palette.primary_50 : t.palette.primary_975,
368 },
369 ],
370 ctx.disabled && {
371 opacity: 0.8,
372 },
373 ]}>
374 {children}
375 </View>
376 )
377}