diff options
Diffstat (limited to 'src/components/dialogs/MutedWords.tsx')
-rw-r--r-- | src/components/dialogs/MutedWords.tsx | 373 |
1 files changed, 283 insertions, 90 deletions
diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx index 526652be9..38273aad5 100644 --- a/src/components/dialogs/MutedWords.tsx +++ b/src/components/dialogs/MutedWords.tsx @@ -1,5 +1,5 @@ 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' @@ -24,6 +24,7 @@ import * as Dialog from '#/components/Dialog' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {Divider} from '#/components/Divider' import * as Toggle from '#/components/forms/Toggle' +import {useFormatDistance} from '#/components/hooks/dates' import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' @@ -32,6 +33,8 @@ 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 ( @@ -53,16 +56,32 @@ 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' + + const now = Date.now() + const rawDuration = durations.at(0) + // undefined evaluates to 'forever' + let duration: string | undefined + + if (rawDuration === '24_hours') { + duration = new Date(now + ONE_DAY).toISOString() + } else if (rawDuration === '7_days') { + duration = new Date(now + 7 * ONE_DAY).toISOString() + } else if (rawDuration === '30_days') { + duration = new Date(now + 30 * ONE_DAY).toISOString() + } - if (!sanitizedValue || !targets.length) { + if (!sanitizedValue || !surfaces.length) { setField('') setError(_(msg`Please enter a valid word, tag, or phrase to mute`)) return @@ -70,28 +89,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]) + }, [_, field, targets, addMutedWord, setField, durations, excludeFollowing]) return ( <Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}> - <View onTouchStart={Keyboard.dismiss}> + <View> <Text style={[a.text_md, a.font_bold, a.pb_sm, t.atoms.text_contrast_high]}> <Trans>Add muted words and tags</Trans> </Text> <Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}> <Trans> - 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. </Trans> </Text> - <View style={[a.pb_lg]}> + <View style={[a.pb_sm]}> <Dialog.Input autoCorrect={false} autoCapitalize="none" @@ -107,30 +135,135 @@ function MutedWordsInner() { }} onSubmitEditing={submit} /> + </View> + <View style={[a.pb_xl, a.gap_sm]}> <Toggle.Group - label={_(msg`Toggle between muted word options.`)} + label={_(msg`Select how long to mute this word for.`)} type="radio" - values={options} - onChange={setOptions}> + values={durations} + onChange={setDurations}> + <Text + style={[ + a.pb_xs, + a.text_sm, + a.font_bold, + t.atoms.text_contrast_medium, + ]}> + <Trans>Duration:</Trans> + </Text> + <View style={[ - a.pt_sm, - a.py_sm, - a.flex_row, - a.align_center, + gtMobile && [a.flex_row, a.align_center, a.justify_start], a.gap_sm, - a.flex_wrap, ]}> + <View + style={[ + a.flex_1, + a.flex_row, + a.justify_start, + a.align_center, + a.gap_sm, + ]}> + <Toggle.Item + label={_(msg`Mute this word until you unmute it`)} + name="forever" + style={[a.flex_1]}> + <TargetToggle> + <View + style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> + <Toggle.Radio /> + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> + <Trans>Forever</Trans> + </Toggle.LabelText> + </View> + </TargetToggle> + </Toggle.Item> + + <Toggle.Item + label={_(msg`Mute this word for 24 hours`)} + name="24_hours" + style={[a.flex_1]}> + <TargetToggle> + <View + style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> + <Toggle.Radio /> + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> + <Trans>24 hours</Trans> + </Toggle.LabelText> + </View> + </TargetToggle> + </Toggle.Item> + </View> + + <View + style={[ + a.flex_1, + a.flex_row, + a.justify_start, + a.align_center, + a.gap_sm, + ]}> + <Toggle.Item + label={_(msg`Mute this word for 7 days`)} + name="7_days" + style={[a.flex_1]}> + <TargetToggle> + <View + style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> + <Toggle.Radio /> + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> + <Trans>7 days</Trans> + </Toggle.LabelText> + </View> + </TargetToggle> + </Toggle.Item> + + <Toggle.Item + label={_(msg`Mute this word for 30 days`)} + name="30_days" + style={[a.flex_1]}> + <TargetToggle> + <View + style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> + <Toggle.Radio /> + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> + <Trans>30 days</Trans> + </Toggle.LabelText> + </View> + </TargetToggle> + </Toggle.Item> + </View> + </View> + </Toggle.Group> + + <Toggle.Group + label={_(msg`Select what content this mute word should apply to.`)} + type="radio" + values={targets} + onChange={setTargets}> + <Text + style={[ + a.pb_xs, + a.text_sm, + a.font_bold, + t.atoms.text_contrast_medium, + ]}> + <Trans>Mute in:</Trans> + </Text> + + <View style={[a.flex_row, a.align_center, a.gap_sm, a.flex_wrap]}> <Toggle.Item label={_(msg`Mute this word in post text and tags`)} name="content" - style={[a.flex_1, !gtMobile && [a.w_full, a.flex_0]]}> + style={[a.flex_1]}> <TargetToggle> - <View style={[a.flex_row, a.align_center, a.gap_sm]}> + <View + style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> <Toggle.Radio /> - <Toggle.LabelText> - <Trans>Mute in text & tags</Trans> + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> + <Trans>Text & tags</Trans> </Toggle.LabelText> </View> <PageText size="sm" /> @@ -140,34 +273,64 @@ function MutedWordsInner() { <Toggle.Item label={_(msg`Mute this word in tags only`)} name="tag" - style={[a.flex_1, !gtMobile && [a.w_full, a.flex_0]]}> + style={[a.flex_1]}> <TargetToggle> - <View style={[a.flex_row, a.align_center, a.gap_sm]}> + <View + style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> <Toggle.Radio /> - <Toggle.LabelText> - <Trans>Mute in tags only</Trans> + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> + <Trans>Tags only</Trans> </Toggle.LabelText> </View> <Hashtag size="sm" /> </TargetToggle> </Toggle.Item> - - <Button - disabled={isPending || !field} - label={_(msg`Add mute word for configured settings`)} - size="small" - color="primary" - variant="solid" - style={[!gtMobile && [a.w_full, a.flex_0]]} - onPress={submit}> - <ButtonText> - <Trans>Add</Trans> - </ButtonText> - <ButtonIcon icon={isPending ? Loader : Plus} /> - </Button> </View> </Toggle.Group> + <View> + <Text + style={[ + a.pb_xs, + a.text_sm, + a.font_bold, + t.atoms.text_contrast_medium, + ]}> + <Trans>Options:</Trans> + </Text> + <Toggle.Item + label={_(msg`Do not apply this mute word to users you follow`)} + name="exclude_following" + style={[a.flex_row, a.justify_between]} + value={excludeFollowing} + onChange={setExcludeFollowing}> + <TargetToggle> + <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> + <Toggle.Checkbox /> + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> + <Trans>Exclude users you follow</Trans> + </Toggle.LabelText> + </View> + </TargetToggle> + </Toggle.Item> + </View> + + <View style={[a.pt_xs]}> + <Button + disabled={isPending || !field} + label={_(msg`Add mute word for configured settings`)} + size="medium" + color="primary" + variant="solid" + style={[]} + onPress={submit}> + <ButtonText> + <Trans>Add</Trans> + </ButtonText> + <ButtonIcon icon={isPending ? Loader : Plus} position="right" /> + </Button> + </View> + {error && ( <View style={[ @@ -191,20 +354,6 @@ function MutedWordsInner() { </Text> </View> )} - - <Text - style={[ - a.pt_xs, - a.text_sm, - a.italic, - a.leading_snug, - t.atoms.text_contrast_medium, - ]}> - <Trans> - We recommend avoiding common words that appear in many posts, - since it can result in no posts being shown. - </Trans> - </Text> </View> <Divider /> @@ -268,6 +417,9 @@ 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 formatDistance = useFormatDistance() const remove = React.useCallback(async () => { control.close() @@ -280,7 +432,7 @@ function MutedWordRow({ control={control} title={_(msg`Are you sure?`)} description={_( - msg`This will delete ${word.value} from your muted words. You can always add it back later.`, + msg`This will delete "${word.value}" from your muted words. You can always add it back later.`, )} onConfirm={remove} confirmButtonCta={_(msg`Remove`)} @@ -289,53 +441,94 @@ function MutedWordRow({ <View style={[ - a.py_md, - a.px_lg, a.flex_row, - a.align_center, a.justify_between, + a.py_md, + a.px_lg, a.rounded_md, a.gap_md, style, ]}> - <Text - style={[ - a.flex_1, - a.leading_snug, - a.w_full, - a.font_bold, - t.atoms.text_contrast_high, - web({ - overflowWrap: 'break-word', - wordBreak: 'break-word', - }), - ]}> - {word.value} - </Text> + <View style={[a.flex_1, a.gap_xs]}> + <View style={[a.flex_row, a.align_center, a.gap_sm]}> + <Text + style={[ + a.flex_1, + a.leading_snug, + a.font_bold, + web({ + overflowWrap: 'break-word', + wordBreak: 'break-word', + }), + ]}> + {word.targets.find(t => t === 'content') ? ( + <Trans comment="Pattern: {wordValue} in text, tags"> + {word.value}{' '} + <Text style={[a.font_normal, t.atoms.text_contrast_medium]}> + in{' '} + <Text style={[a.font_bold, t.atoms.text_contrast_medium]}> + text & tags + </Text> + </Text> + </Trans> + ) : ( + <Trans comment="Pattern: {wordValue} in tags"> + {word.value}{' '} + <Text style={[a.font_normal, t.atoms.text_contrast_medium]}> + in{' '} + <Text style={[a.font_bold, t.atoms.text_contrast_medium]}> + tags + </Text> + </Text> + </Trans> + )} + </Text> + </View> - <View style={[a.flex_row, a.align_center, a.justify_end, a.gap_sm]}> - {word.targets.map(target => ( - <View - key={target} - style={[a.py_xs, a.px_sm, a.rounded_sm, t.atoms.bg_contrast_100]}> + {(expiryDate || word.actorTarget === 'exclude-following') && ( + <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> <Text - style={[a.text_xs, a.font_bold, t.atoms.text_contrast_medium]}> - {target === 'content' ? _(msg`text`) : _(msg`tag`)} + style={[ + a.flex_1, + a.text_xs, + a.leading_snug, + t.atoms.text_contrast_medium, + ]}> + {expiryDate && ( + <> + {isExpired ? ( + <Trans>Expired</Trans> + ) : ( + <Trans> + Expires{' '} + {formatDistance(expiryDate, new Date(), { + addSuffix: true, + })} + </Trans> + )} + </> + )} + {word.actorTarget === 'exclude-following' && ( + <> + {' • '} + <Trans>Excludes users you follow</Trans> + </> + )} </Text> </View> - ))} - - <Button - label={_(msg`Remove mute word from your list`)} - size="tiny" - shape="round" - variant="ghost" - color="secondary" - onPress={() => control.open()} - style={[a.ml_sm]}> - <ButtonIcon icon={isPending ? Loader : X} /> - </Button> + )} </View> + + <Button + label={_(msg`Remove mute word from your list`)} + size="tiny" + shape="round" + variant="outline" + color="secondary" + onPress={() => control.open()} + style={[a.ml_sm]}> + <ButtonIcon icon={isPending ? Loader : X} /> + </Button> </View> </> ) |