diff options
author | Eric Bailey <git@esb.lol> | 2024-08-01 10:29:27 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-01 10:29:27 -0500 |
commit | b0e130a4d85f2056bddcbf210aa7ea4068d41686 (patch) | |
tree | 8ddff0edd9a564c952daccb58c79d092ef35ba25 | |
parent | d2e88cc623b2df5fe40280618fe9598334df8241 (diff) | |
download | voidsky-b0e130a4d85f2056bddcbf210aa7ea4068d41686.tar.zst |
Update muted words dialog with `expiresAt` and `actorTarget` (#4801)
* WIP not working dropdown * Update MutedWords dialog * Add i18n formatDistance * Comments * Handle text wrapping * Update label copy Co-authored-by: Hailey <me@haileyok.com> * Fix alignment * Improve translation output * Revert toggle changes * Better types for useFormatDistance * Tweaks * Integrate new sdk version into TagMenu * Use ampersand Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Bump SDK --------- Co-authored-by: Hailey <me@haileyok.com> Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/components/TagMenu/index.tsx | 56 | ||||
-rw-r--r-- | src/components/TagMenu/index.web.tsx | 36 | ||||
-rw-r--r-- | src/components/dialogs/MutedWords.tsx | 373 | ||||
-rw-r--r-- | src/components/hooks/dates.ts | 69 | ||||
-rw-r--r-- | src/state/queries/preferences/index.ts | 15 | ||||
-rw-r--r-- | yarn.lock | 8 |
7 files changed, 432 insertions, 127 deletions
diff --git a/package.json b/package.json index 91b427ae9..3d053bc83 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" }, "dependencies": { - "@atproto/api": "0.12.25", + "@atproto/api": "^0.12.26", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", diff --git a/src/components/TagMenu/index.tsx b/src/components/TagMenu/index.tsx index 0ed703667..2c6a0b674 100644 --- a/src/components/TagMenu/index.tsx +++ b/src/components/TagMenu/index.tsx @@ -1,27 +1,27 @@ import React from 'react' import {View} from 'react-native' -import {useNavigation} from '@react-navigation/native' -import {useLingui} from '@lingui/react' import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' -import {atoms as a, native, useTheme} from '#/alf' -import * as Dialog from '#/components/Dialog' -import {Text} from '#/components/Typography' -import {Button, ButtonText} from '#/components/Button' -import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' -import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' -import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' -import {Divider} from '#/components/Divider' -import {Link} from '#/components/Link' import {makeSearchLink} from '#/lib/routes/links' import {NavigationProp} from '#/lib/routes/types' +import {isInvalidHandle} from '#/lib/strings/handles' import { usePreferencesQuery, + useRemoveMutedWordsMutation, useUpsertMutedWordsMutation, - useRemoveMutedWordMutation, } from '#/state/queries/preferences' +import {atoms as a, native, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Divider} from '#/components/Divider' +import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' +import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' +import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' +import {Link} from '#/components/Link' import {Loader} from '#/components/Loader' -import {isInvalidHandle} from '#/lib/strings/handles' +import {Text} from '#/components/Typography' export function useTagMenuControl() { return Dialog.useDialogControl() @@ -52,10 +52,10 @@ export function TagMenu({ reset: resetUpsert, } = useUpsertMutedWordsMutation() const { - mutateAsync: removeMutedWord, + mutateAsync: removeMutedWords, variables: optimisticRemove, reset: resetRemove, - } = useRemoveMutedWordMutation() + } = useRemoveMutedWordsMutation() const displayTag = '#' + tag const isMuted = Boolean( @@ -65,9 +65,20 @@ export function TagMenu({ optimisticUpsert?.find( m => m.value === tag && m.targets.includes('tag'), )) && - !(optimisticRemove?.value === tag), + !optimisticRemove?.find(m => m?.value === tag), ) + /* + * Mute word records that exactly match the tag in question. + */ + const removeableMuteWords = React.useMemo(() => { + return ( + preferences?.moderationPrefs.mutedWords?.filter(word => { + return word.value === tag + }) || [] + ) + }, [tag, preferences?.moderationPrefs?.mutedWords]) + return ( <> {children} @@ -212,13 +223,16 @@ export function TagMenu({ control.close(() => { if (isMuted) { resetUpsert() - removeMutedWord({ - value: tag, - targets: ['tag'], - }) + removeMutedWords(removeableMuteWords) } else { resetRemove() - upsertMutedWord([{value: tag, targets: ['tag']}]) + upsertMutedWord([ + { + value: tag, + targets: ['tag'], + actorTarget: 'all', + }, + ]) } }) }}> diff --git a/src/components/TagMenu/index.web.tsx b/src/components/TagMenu/index.web.tsx index 433622386..b6c306439 100644 --- a/src/components/TagMenu/index.web.tsx +++ b/src/components/TagMenu/index.web.tsx @@ -3,16 +3,16 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {isInvalidHandle} from '#/lib/strings/handles' -import {EventStopper} from '#/view/com/util/EventStopper' -import {NativeDropdown} from '#/view/com/util/forms/NativeDropdown' import {NavigationProp} from '#/lib/routes/types' +import {isInvalidHandle} from '#/lib/strings/handles' +import {enforceLen} from '#/lib/strings/helpers' import { usePreferencesQuery, + useRemoveMutedWordsMutation, useUpsertMutedWordsMutation, - useRemoveMutedWordMutation, } from '#/state/queries/preferences' -import {enforceLen} from '#/lib/strings/helpers' +import {EventStopper} from '#/view/com/util/EventStopper' +import {NativeDropdown} from '#/view/com/util/forms/NativeDropdown' import {web} from '#/alf' import * as Dialog from '#/components/Dialog' @@ -47,8 +47,8 @@ export function TagMenu({ const {data: preferences} = usePreferencesQuery() const {mutateAsync: upsertMutedWord, variables: optimisticUpsert} = useUpsertMutedWordsMutation() - const {mutateAsync: removeMutedWord, variables: optimisticRemove} = - useRemoveMutedWordMutation() + const {mutateAsync: removeMutedWords, variables: optimisticRemove} = + useRemoveMutedWordsMutation() const isMuted = Boolean( (preferences?.moderationPrefs.mutedWords?.find( m => m.value === tag && m.targets.includes('tag'), @@ -56,10 +56,21 @@ export function TagMenu({ optimisticUpsert?.find( m => m.value === tag && m.targets.includes('tag'), )) && - !(optimisticRemove?.value === tag), + !optimisticRemove?.find(m => m?.value === tag), ) const truncatedTag = '#' + enforceLen(tag, 15, true, 'middle') + /* + * Mute word records that exactly match the tag in question. + */ + const removeableMuteWords = React.useMemo(() => { + return ( + preferences?.moderationPrefs.mutedWords?.filter(word => { + return word.value === tag + }) || [] + ) + }, [tag, preferences?.moderationPrefs?.mutedWords]) + const dropdownItems = React.useMemo(() => { return [ { @@ -105,9 +116,11 @@ export function TagMenu({ : _(msg`Mute ${truncatedTag}`), onPress() { if (isMuted) { - removeMutedWord({value: tag, targets: ['tag']}) + removeMutedWords(removeableMuteWords) } else { - upsertMutedWord([{value: tag, targets: ['tag']}]) + upsertMutedWord([ + {value: tag, targets: ['tag'], actorTarget: 'all'}, + ]) } }, testID: 'tagMenuMute', @@ -129,7 +142,8 @@ export function TagMenu({ tag, truncatedTag, upsertMutedWord, - removeMutedWord, + removeMutedWords, + removeableMuteWords, ]) return ( 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> </> ) diff --git a/src/components/hooks/dates.ts b/src/components/hooks/dates.ts new file mode 100644 index 000000000..b0f94133b --- /dev/null +++ b/src/components/hooks/dates.ts @@ -0,0 +1,69 @@ +/** + * Hooks for date-fns localized formatters. + * + * Our app supports some languages that are not included in date-fns by + * default, in which case it will fall back to English. + * + * {@link https://github.com/date-fns/date-fns/blob/main/docs/i18n.md} + */ + +import React from 'react' +import {formatDistance, Locale} from 'date-fns' +import { + ca, + de, + es, + fi, + fr, + hi, + id, + it, + ja, + ko, + ptBR, + tr, + uk, + zhCN, + zhTW, +} from 'date-fns/locale' + +import {AppLanguage} from '#/locale/languages' +import {useLanguagePrefs} from '#/state/preferences' + +/** + * {@link AppLanguage} + */ +const locales: Record<AppLanguage, Locale | undefined> = { + en: undefined, + ca, + de, + es, + fi, + fr, + ga: undefined, + hi, + id, + it, + ja, + ko, + ['pt-BR']: ptBR, + tr, + uk, + ['zh-CN']: zhCN, + ['zh-TW']: zhTW, +} + +/** + * Returns a localized `formatDistance` function. + * {@link formatDistance} + */ +export function useFormatDistance() { + const {appLanguage} = useLanguagePrefs() + return React.useCallback<typeof formatDistance>( + (date, baseDate, options) => { + const locale = locales[appLanguage as AppLanguage] + return formatDistance(date, baseDate, {...options, locale: locale}) + }, + [appLanguage], + ) +} diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index 9bb57fcaf..6991f8647 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -343,6 +343,21 @@ export function useRemoveMutedWordMutation() { }) } +export function useRemoveMutedWordsMutation() { + const queryClient = useQueryClient() + const agent = useAgent() + + return useMutation({ + mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => { + await agent.removeMutedWords(mutedWords) + // triggers a refetch + await queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + }, + }) +} + export function useQueueNudgesMutation() { const queryClient = useQueryClient() const agent = useAgent() diff --git a/yarn.lock b/yarn.lock index 675fda4c2..6fa880512 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,10 +34,10 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@atproto/api@0.12.25": - version "0.12.25" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.25.tgz#9eeb51484106a5e07f89f124e505674a3574f93b" - integrity sha512-IV3vGPnDw9bmyP/JOd8YKbm8fOpRAgJpEUVnIZNVb/Vo8v+WOroOjrJxtzdHOcXTL9IEcTTyXSCc7yE7kwhN2A== +"@atproto/api@^0.12.26": + version "0.12.26" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.26.tgz#940888466522cc9ff8c03d8164dc39221b29d9ca" + integrity sha512-RH0ymOGbDfT8IL8eNzzY+hwtyTgknHfkzUVqRd0sstNblvTf8WGpDR2FSTveiiMR3OpVO6zG8fRYVzBfmY1+pA== dependencies: "@atproto/common-web" "^0.3.0" "@atproto/lexicon" "^0.4.0" |