From 7522df9c10f3f7fe1adb836b877544a87133b1ce Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 19 Jul 2024 14:15:21 -0700 Subject: [PATCH] Update MutedWords dialog --- src/components/dialogs/MutedWords.tsx | 423 ++++++++++++++++---------- 1 file changed, 258 insertions(+), 165 deletions(-) diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx index fe12002210..404e3970c3 100644 --- a/src/components/dialogs/MutedWords.tsx +++ b/src/components/dialogs/MutedWords.tsx @@ -1,14 +1,10 @@ import React from 'react' -import {Keyboard, View} from 'react-native' +import {View} from 'react-native' import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select' +import {formatDistance} from 'date-fns' -import { - DropdownItem, - NativeDropdown, -} from '#/view/com/util/forms/NativeDropdown' import {logger} from '#/logger' import {isNative} from '#/platform/detection' import { @@ -33,11 +29,12 @@ import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Has import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' -import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' +const ONE_DAY = 24 * 60 * 60 * 1000 + export function MutedWordsDialog() { const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext() return ( @@ -59,16 +56,30 @@ function MutedWordsInner() { } = usePreferencesQuery() const {isPending, mutateAsync: addMutedWord} = useUpsertMutedWordsMutation() const [field, setField] = React.useState('') - const [options, setOptions] = React.useState(['content']) + const [targets, setTargets] = React.useState(['content']) const [error, setError] = React.useState('') + const [durations, setDurations] = React.useState(['forever']) + const [excludeFollowing, setExcludeFollowing] = React.useState(false) const submit = React.useCallback(async () => { const sanitizedValue = sanitizeMutedWordValue(field) - const targets = ['tag', options.includes('content') && 'content'].filter( + const surfaces = ['tag', targets.includes('content') && 'content'].filter( Boolean, ) as AppBskyActorDefs.MutedWord['targets'] + const actorTarget = excludeFollowing ? 'exclude-following' : 'all' - if (!sanitizedValue || !targets.length) { + const rawDuration = durations.at(0) + let duration: string | undefined + + if (rawDuration === '24_hours') { + duration = new Date(Date.now() + ONE_DAY).toISOString() + } else if (rawDuration === '7_days') { + duration = new Date(Date.now() + 7 * ONE_DAY).toISOString() + } else if (rawDuration === '30_days') { + duration = new Date(Date.now() + 30 * ONE_DAY).toISOString() + } + + if (!sanitizedValue || !surfaces.length) { setField('') setError(_(msg`Please enter a valid word, tag, or phrase to mute`)) return @@ -76,45 +87,37 @@ function MutedWordsInner() { try { // send raw value and rely on SDK as sanitization source of truth - await addMutedWord([{value: field, targets}]) + await addMutedWord([ + { + value: field, + targets: surfaces, + actorTarget, + expiresAt: duration, + }, + ]) setField('') } catch (e: any) { logger.error(`Failed to save muted word`, {message: e.message}) setError(e.message) } - }, [_, field, options, addMutedWord, setField]) - - const onDurationChange = React.useCallback( - (value: Parameters[0]) => { - if (!value) return - }, - [], - ) - - const selectStyle = [ - a.border_0, - a.rounded_sm, - a.py_sm, - a.text_md, - a.px_md, - t.atoms.bg_contrast_25, - t.atoms.text_contrast_medium, - ] + }, [_, field, targets, addMutedWord, setField, durations, excludeFollowing]) return ( - + Add muted words and tags - Posts can be muted based on their text, their tags, or both. + Posts can be muted based on their text, their tags, or both. We + recommend avoiding common words that appear in many posts, since it + can result in no posts being shown. - + + - - } - /> - - + + values={durations} + onChange={setDurations}> + + Duration: + + + + + + + + + Forever + + + + + + + + + + + 24 hours + + + + + + + + + + + + + 7 days + + + + + + + + + + + 30 days + + + + + + + + + + + Mute in: + + + + style={[a.flex_1]}> - Mute in text & tags + Text & tags @@ -219,34 +266,63 @@ function MutedWordsInner() { + style={[a.flex_1]}> - Mute in tags only + Tags only - - + + + Options: + + + + + + + Exclude users you follow + + + + + + + + + + {error && ( )} - - - - We recommend avoiding common words that appear in many posts, - since it can result in no posts being shown. - - @@ -347,6 +409,13 @@ function MutedWordRow({ const {_} = useLingui() const {isPending, mutateAsync: removeMutedWord} = useRemoveMutedWordMutation() const control = Prompt.usePromptControl() + const expiryDate = word.expiresAt ? new Date(word.expiresAt) : undefined + const isExpired = expiryDate && expiryDate < new Date() + const target = word.targets.find(t => t === 'content') ? ( + text, tags + ) : ( + tags + ) const remove = React.useCallback(async () => { control.close() @@ -368,53 +437,77 @@ function MutedWordRow({ - - {word.value} - + + + + {word.value} - - {word.targets.map(target => ( - - - {target === 'content' ? _(msg`text`) : _(msg`tag`)} + + {' '} + in{' '} + + + + {target} + + + + + {(expiryDate || word.actorTarget === 'exclude-following') && ( + + + {expiryDate && ( + <> + {isExpired ? ( + Expired + ) : ( + + Expires{' '} + {formatDistance(expiryDate, new Date(), { + addSuffix: true, + })} + + )} + + )} + {word.actorTarget === 'exclude-following' && ( + <> + {' • '} + Excludes users you follow + + )} - ))} - - + )} + + )