diff options
author | Hailey <me@haileyok.com> | 2024-01-31 14:14:37 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-31 14:14:37 -0800 |
commit | 5db56277c05c6c8a6357f0e463d893baabd80b03 (patch) | |
tree | 0266bd50dd232dc16e1cc7029cf286c8aad7bd57 | |
parent | a4ff29076993401935ac65c47b4d284c6c6d16fe (diff) | |
download | voidsky-5db56277c05c6c8a6357f0e463d893baabd80b03.tar.zst |
Onboarding moderation improvements (#2713)
* create separate label group arrays * render adult and other label groups separately * animate in/out the additional settings * improve toggle logic * support animations on all platforms * remove debug * update notice, prevent running animations on mount * reorg imports
-rw-r--r-- | src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx | 125 | ||||
-rw-r--r-- | src/screens/Onboarding/StepModeration/ModerationOption.tsx | 49 | ||||
-rw-r--r-- | src/screens/Onboarding/StepModeration/index.tsx | 47 | ||||
-rw-r--r-- | src/state/queries/preferences/types.ts | 10 |
4 files changed, 130 insertions, 101 deletions
diff --git a/src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx b/src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx index bc4c0387f..6b456de80 100644 --- a/src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx +++ b/src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx @@ -2,19 +2,17 @@ import React from 'react' import {View} from 'react-native' import {useLingui} from '@lingui/react' import {msg, Trans} from '@lingui/macro' +import {UseMutateFunction} from '@tanstack/react-query' -import {isIOS} from '#/platform/detection' import * as Toast from '#/view/com/util/Toast' import {atoms as a, useTheme} from '#/alf' -import { - usePreferencesQuery, - usePreferencesSetAdultContentMutation, -} from '#/state/queries/preferences' +import {usePreferencesQuery} from '#/state/queries/preferences' import {logger} from '#/logger' import {Text} from '#/components/Typography' -import {InlineLink} from '#/components/Link' import * as Toggle from '#/components/forms/Toggle' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' +import * as Prompt from '#/components/Prompt' +import {isIOS} from '#/platform/detection' function Card({children}: React.PropsWithChildren<{}>) { const t = useTheme() @@ -36,16 +34,25 @@ function Card({children}: React.PropsWithChildren<{}>) { ) } -export function AdultContentEnabledPref() { +export function AdultContentEnabledPref({ + mutate, + variables, +}: { + mutate: UseMutateFunction<void, unknown, {enabled: boolean}, unknown> + variables: {enabled: boolean} | undefined +}) { const {_} = useLingui() const t = useTheme() + const prompt = Prompt.usePromptControl() // Reuse logic here form ContentFilteringSettings.tsx const {data: preferences} = usePreferencesQuery() - const {mutate, variables} = usePreferencesSetAdultContentMutation() const onToggleAdultContent = React.useCallback(async () => { - if (isIOS) return + if (isIOS) { + prompt.open() + return + } try { mutate({ @@ -57,15 +64,33 @@ export function AdultContentEnabledPref() { ) logger.error('Failed to update preferences with server', {error: e}) } - }, [variables, preferences, mutate, _]) + }, [variables, preferences, mutate, _, prompt]) if (!preferences) return null - if (isIOS) { - if (preferences?.adultContentEnabled === true) { - return null - } else { - return ( + return ( + <> + {preferences.userAge && preferences.userAge >= 18 ? ( + <View style={[a.w_full, a.px_xs]}> + <Toggle.Item + name={_(msg`Enable adult content in your feeds`)} + label={_(msg`Enable adult content in your feeds`)} + value={variables?.enabled ?? preferences?.adultContentEnabled} + onChange={onToggleAdultContent}> + <View + style={[ + a.flex_row, + a.w_full, + a.justify_between, + a.align_center, + a.py_md, + ]}> + <Text style={[a.font_bold]}>Enable Adult Content</Text> + <Toggle.Switch /> + </View> + </Toggle.Item> + </View> + ) : ( <Card> <CircleInfo size="sm" fill={t.palette.contrast_500} /> <Text @@ -75,61 +100,23 @@ export function AdultContentEnabledPref() { a.leading_snug, {paddingTop: 1}, ]}> - <Trans> - Adult content can only be enabled via the Web at{' '} - <InlineLink style={[a.leading_snug]} to="https://bsky.app"> - bsky.app - </InlineLink> - . - </Trans> + <Trans>You must be 18 years or older to enable adult content</Trans> </Text> </Card> - ) - } - } else { - if (preferences?.userAge) { - if (preferences.userAge >= 18) { - return ( - <View style={[a.w_full]}> - <Toggle.Item - name={_(msg`Enable adult content in your feeds`)} - label={_(msg`Enable adult content in your feeds`)} - value={variables?.enabled ?? preferences?.adultContentEnabled} - onChange={onToggleAdultContent}> - <View - style={[ - a.flex_row, - a.w_full, - a.justify_between, - a.align_center, - a.py_md, - ]}> - <Text style={[a.font_bold]}>Enable Adult Content</Text> - <Toggle.Switch /> - </View> - </Toggle.Item> - </View> - ) - } else { - return ( - <Card> - <CircleInfo size="sm" fill={t.palette.contrast_500} /> - <Text - style={[ - a.flex_1, - t.atoms.text_contrast_700, - a.leading_snug, - {paddingTop: 1}, - ]}> - <Trans> - You must be 18 years or older to enable adult content - </Trans> - </Text> - </Card> - ) - } - } + )} - return null - } + <Prompt.Outer control={prompt}> + <Prompt.Title>Adult Content</Prompt.Title> + <Prompt.Description> + <Trans> + Due to Apple policies, adult content can only be enabled on the web + after completing sign up. + </Trans> + </Prompt.Description> + <Prompt.Actions> + <Prompt.Action onPress={prompt.close}>OK</Prompt.Action> + </Prompt.Actions> + </Prompt.Outer> + </> + ) } diff --git a/src/screens/Onboarding/StepModeration/ModerationOption.tsx b/src/screens/Onboarding/StepModeration/ModerationOption.tsx index 904c47299..d216692d0 100644 --- a/src/screens/Onboarding/StepModeration/ModerationOption.tsx +++ b/src/screens/Onboarding/StepModeration/ModerationOption.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native' import {LabelPreference} from '@atproto/api' import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' +import Animated, {Easing, Layout, FadeIn} from 'react-native-reanimated' import { CONFIGURABLE_LABEL_GROUPS, @@ -16,8 +17,10 @@ import * as ToggleButton from '#/components/forms/ToggleButton' export function ModerationOption({ labelGroup, + isMounted, }: { labelGroup: ConfigurableLabelGroup + isMounted: React.MutableRefObject<boolean> }) { const {_} = useLingui() const t = useTheme() @@ -41,7 +44,7 @@ export function ModerationOption({ } return ( - <View + <Animated.View style={[ a.flex_row, a.justify_between, @@ -49,7 +52,9 @@ export function ModerationOption({ a.py_xs, a.px_xs, a.align_center, - ]}> + ]} + layout={Layout.easing(Easing.ease).duration(200)} + entering={isMounted.current ? FadeIn : undefined}> <View style={[a.gap_xs, {width: '50%'}]}> <Text style={[a.font_bold]}>{groupInfo.title}</Text> <Text style={[t.atoms.text_contrast_700, a.leading_snug]}> @@ -57,29 +62,23 @@ export function ModerationOption({ </Text> </View> <View style={[a.justify_center, {minHeight: 35}]}> - {!preferences?.adultContentEnabled && groupInfo.isAdultImagery ? ( - <View style={[a.justify_center, {minHeight: 40}]}> - <Text style={[a.font_bold]}>{labels.hide}</Text> - </View> - ) : ( - <ToggleButton.Group - label={_( - msg`Configure content filtering setting for category: ${groupInfo.title.toLowerCase()}`, - )} - values={[visibility ?? 'hide']} - onChange={onChange}> - <ToggleButton.Button name="hide" label={labels.hide}> - {labels.hide} - </ToggleButton.Button> - <ToggleButton.Button name="warn" label={labels.warn}> - {labels.warn} - </ToggleButton.Button> - <ToggleButton.Button name="ignore" label={labels.show}> - {labels.show} - </ToggleButton.Button> - </ToggleButton.Group> - )} + <ToggleButton.Group + label={_( + msg`Configure content filtering setting for category: ${groupInfo.title.toLowerCase()}`, + )} + values={[visibility ?? 'hide']} + onChange={onChange}> + <ToggleButton.Button name="hide" label={labels.hide}> + {labels.hide} + </ToggleButton.Button> + <ToggleButton.Button name="warn" label={labels.warn}> + {labels.warn} + </ToggleButton.Button> + <ToggleButton.Button name="ignore" label={labels.show}> + {labels.show} + </ToggleButton.Button> + </ToggleButton.Group> </View> - </View> + </Animated.View> ) } diff --git a/src/screens/Onboarding/StepModeration/index.tsx b/src/screens/Onboarding/StepModeration/index.tsx index 18ac037c8..c831b6880 100644 --- a/src/screens/Onboarding/StepModeration/index.tsx +++ b/src/screens/Onboarding/StepModeration/index.tsx @@ -2,9 +2,14 @@ import React from 'react' import {View} from 'react-native' import {useLingui} from '@lingui/react' import {msg, Trans} from '@lingui/macro' +import Animated, {Easing, Layout} from 'react-native-reanimated' import {atoms as a} from '#/alf' -import {configurableLabelGroups} from 'state/queries/preferences' +import { + configurableAdultLabelGroups, + configurableOtherLabelGroups, + usePreferencesSetAdultContentMutation, +} from 'state/queries/preferences' import {Divider} from '#/components/Divider' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' @@ -23,11 +28,32 @@ import {AdultContentEnabledPref} from '#/screens/Onboarding/StepModeration/Adult import {Context} from '#/screens/Onboarding/state' import {IconCircle} from '#/screens/Onboarding/IconCircle' +function AnimatedDivider() { + return ( + <Animated.View layout={Layout.easing(Easing.ease).duration(200)}> + <Divider /> + </Animated.View> + ) +} + export function StepModeration() { const {_} = useLingui() const {track} = useAnalytics() const {state, dispatch} = React.useContext(Context) const {data: preferences} = usePreferencesQuery() + const {mutate, variables} = usePreferencesSetAdultContentMutation() + + // We need to know if the screen is mounted so we know if we want to run entering animations + // https://github.com/software-mansion/react-native-reanimated/discussions/2513 + const isMounted = React.useRef(false) + React.useLayoutEffect(() => { + isMounted.current = true + }, []) + + const adultContentEnabled = !!( + (variables && variables.enabled) || + (!variables && preferences?.adultContentEnabled) + ) const onContinue = React.useCallback(() => { dispatch({type: 'next'}) @@ -57,14 +83,23 @@ export function StepModeration() { </View> ) : ( <> - <AdultContentEnabledPref /> + <AdultContentEnabledPref mutate={mutate} variables={variables} /> <View style={[a.gap_sm, a.w_full]}> - {configurableLabelGroups.map((g, index) => ( + {adultContentEnabled && + configurableAdultLabelGroups.map((g, index) => ( + <React.Fragment key={index}> + {index === 0 && <AnimatedDivider />} + <ModerationOption labelGroup={g} isMounted={isMounted} /> + <AnimatedDivider /> + </React.Fragment> + ))} + + {configurableOtherLabelGroups.map((g, index) => ( <React.Fragment key={index}> - {index === 0 && <Divider />} - <ModerationOption labelGroup={g} /> - <Divider /> + {!adultContentEnabled && index === 0 && <AnimatedDivider />} + <ModerationOption labelGroup={g} isMounted={isMounted} /> + <AnimatedDivider /> </React.Fragment> ))} </View> diff --git a/src/state/queries/preferences/types.ts b/src/state/queries/preferences/types.ts index cd9a2e8f9..45c9eed7d 100644 --- a/src/state/queries/preferences/types.ts +++ b/src/state/queries/preferences/types.ts @@ -5,15 +5,23 @@ import { BskyFeedViewPreference, } from '@atproto/api' -export const configurableLabelGroups = [ +export const configurableAdultLabelGroups = [ 'nsfw', 'nudity', 'suggestive', 'gore', +] as const + +export const configurableOtherLabelGroups = [ 'hate', 'spam', 'impersonation', ] as const + +export const configurableLabelGroups = [ + ...configurableAdultLabelGroups, + ...configurableOtherLabelGroups, +] as const export type ConfigurableLabelGroup = (typeof configurableLabelGroups)[number] export type LabelGroup = |