diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-01-23 22:33:28 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-23 22:33:28 +0000 |
commit | f1d120d416bc262b5fd63e3407f9e0b09b13cd74 (patch) | |
tree | 5191aec11ef4367e7be299e3711cad2dcad29697 /src | |
parent | 084d11c63aebd0afc516931a11bddac9b24b3b18 (diff) | |
download | voidsky-f1d120d416bc262b5fd63e3407f9e0b09b13cd74.tar.zst |
Takendown state + in-app takedown appeals (#7566)
* takendown screen * add form, move button inline * expect type error * display error * disable submit if too long * move around all the ctas * typos, rm layoutanimation, fix link * use REASONAPPEAL
Diffstat (limited to 'src')
-rw-r--r-- | src/components/Link.tsx | 20 | ||||
-rw-r--r-- | src/components/dms/ReportDialog.tsx | 3 | ||||
-rw-r--r-- | src/lib/constants.ts | 2 | ||||
-rw-r--r-- | src/lib/statsig/events.ts | 7 | ||||
-rw-r--r-- | src/screens/SignupQueued.tsx | 45 | ||||
-rw-r--r-- | src/screens/Takendown.tsx | 263 | ||||
-rw-r--r-- | src/state/session/agent.ts | 7 | ||||
-rw-r--r-- | src/state/session/index.tsx | 2 | ||||
-rw-r--r-- | src/view/shell/createNativeStackNavigatorWithAuth.tsx | 4 |
9 files changed, 317 insertions, 36 deletions
diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 50e741ea7..26cea5968 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -79,8 +79,10 @@ export function useLink({ onPress: outerOnPress, onLongPress: outerOnLongPress, shareOnLongPress, + overridePresentation, }: BaseLinkProps & { displayText: string + overridePresentation?: boolean }) { const navigation = useNavigationDeduped() const {href} = useLinkProps<AllNavigatorParams>({ @@ -116,7 +118,7 @@ export function useLink({ }) } else { if (isExternal) { - openLink(href) + openLink(href, overridePresentation) } else { const shouldOpenInNewTab = shouldClickOpenNewTab(e) @@ -158,6 +160,7 @@ export function useLink({ closeModal, action, navigation, + overridePresentation, ], ) @@ -254,12 +257,13 @@ export function Link({ export type InlineLinkProps = React.PropsWithChildren< BaseLinkProps & TextStyleProp & - Pick<TextProps, 'selectable' | 'numberOfLines'> -> & - Pick<ButtonProps, 'label' | 'accessibilityHint'> & { - disableUnderline?: boolean - title?: TextProps['title'] - } + Pick<TextProps, 'selectable' | 'numberOfLines'> & + Pick<ButtonProps, 'label' | 'accessibilityHint'> & { + disableUnderline?: boolean + title?: TextProps['title'] + overridePresentation?: boolean + } +> export function InlineLinkText({ children, @@ -274,6 +278,7 @@ export function InlineLinkText({ label, shareOnLongPress, disableUnderline, + overridePresentation, ...rest }: InlineLinkProps) { const t = useTheme() @@ -286,6 +291,7 @@ export function InlineLinkText({ onPress: outerOnPress, onLongPress: outerOnLongPress, shareOnLongPress, + overridePresentation, }) const { state: hovered, diff --git a/src/components/dms/ReportDialog.tsx b/src/components/dms/ReportDialog.tsx index a67ac47f2..4f9bc23ca 100644 --- a/src/components/dms/ReportDialog.tsx +++ b/src/components/dms/ReportDialog.tsx @@ -224,11 +224,10 @@ function SubmitStep({ multiline defaultValue={details} onChangeText={setDetails} - label="Text field" + label={_(msg`Text field`)} style={{paddingRight: 60}} numberOfLines={5} /> - <View style={[ a.absolute, diff --git a/src/lib/constants.ts b/src/lib/constants.ts index aa7ff2928..c03439f56 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -52,6 +52,8 @@ export const MAX_DM_GRAPHEME_LENGTH = 1000 // but increasing limit per user feedback export const MAX_ALT_TEXT = 2000 +export const MAX_REPORT_REASON_GRAPHEME_LENGTH = 2000 + export function IS_TEST_USER(handle?: string) { return handle && handle?.endsWith('.test') } diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index af759e94e..189153a10 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -13,7 +13,12 @@ export type LogEvents = { withPassword: boolean } 'account:loggedOut': { - logContext: 'SwitchAccount' | 'Settings' | 'SignupQueued' | 'Deactivated' + logContext: + | 'SwitchAccount' + | 'Settings' + | 'SignupQueued' + | 'Deactivated' + | 'Takendown' scope: 'current' | 'every' } 'notifications:openApp': {} diff --git a/src/screens/SignupQueued.tsx b/src/screens/SignupQueued.tsx index f1c36a69c..61c387875 100644 --- a/src/screens/SignupQueued.tsx +++ b/src/screens/SignupQueued.tsx @@ -85,6 +85,21 @@ export function SignupQueued() { </Button> ) + const logoutBtn = ( + <Button + variant="ghost" + size="large" + color="primary" + label={_(msg`Log out`)} + onPress={() => logoutCurrentAccount('SignupQueued')}> + <ButtonText> + <Trans>Log out</Trans> + </ButtonText> + </Button> + ) + + const webLayout = isWeb && gtMobile + return ( <Modal visible @@ -108,7 +123,7 @@ export function SignupQueued() { <Logo width={120} /> </View> - <Text style={[a.text_4xl, a.font_bold, a.pb_sm]}> + <Text style={[a.text_4xl, a.font_heavy, a.pb_sm]}> <Trans>You're in line</Trans> </Text> <P style={[t.atoms.text_contrast_medium]}> @@ -153,7 +168,7 @@ export function SignupQueued() { </P> </View> - {isWeb && gtMobile && ( + {webLayout && ( <View style={[ a.w_full, @@ -162,15 +177,7 @@ export function SignupQueued() { a.pt_5xl, {paddingBottom: 200}, ]}> - <Button - variant="ghost" - size="large" - label={_(msg`Log out`)} - onPress={() => logoutCurrentAccount('SignupQueued')}> - <ButtonText style={[{color: t.palette.primary_500}]}> - <Trans>Log out</Trans> - </ButtonText> - </Button> + {logoutBtn} {checkBtn} </View> )} @@ -178,27 +185,17 @@ export function SignupQueued() { </View> </ScrollView> - {(!isWeb || !gtMobile) && ( + {!webLayout && ( <View style={[ a.align_center, t.atoms.bg, gtMobile ? a.px_5xl : a.px_xl, - { - paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom), - }, + {paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom)}, ]}> <View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}> {checkBtn} - <Button - variant="ghost" - size="large" - label={_(msg`Log out`)} - onPress={() => logoutCurrentAccount('SignupQueued')}> - <ButtonText style={[{color: t.palette.primary_500}]}> - <Trans>Log out</Trans> - </ButtonText> - </Button> + {logoutBtn} </View> </View> )} diff --git a/src/screens/Takendown.tsx b/src/screens/Takendown.tsx new file mode 100644 index 000000000..5eb787e80 --- /dev/null +++ b/src/screens/Takendown.tsx @@ -0,0 +1,263 @@ +import {useMemo, useState} from 'react' +import {Modal, View} from 'react-native' +import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {StatusBar} from 'expo-status-bar' +import {ComAtprotoAdminDefs, ComAtprotoModerationDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useMutation} from '@tanstack/react-query' +import Graphemer from 'graphemer' + +import {MAX_REPORT_REASON_GRAPHEME_LENGTH} from '#/lib/constants' +import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController' +import {cleanError} from '#/lib/strings/errors' +import {isIOS, isWeb} from '#/platform/detection' +import {useAgent, useSession, useSessionApi} from '#/state/session' +import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' +import {Logo} from '#/view/icons/Logo' +import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as TextField from '#/components/forms/TextField' +import {InlineLinkText} from '#/components/Link' +import {Loader} from '#/components/Loader' +import {P, Text} from '#/components/Typography' + +const COL_WIDTH = 400 + +export function Takendown() { + const {_} = useLingui() + const t = useTheme() + const insets = useSafeAreaInsets() + const {gtMobile} = useBreakpoints() + const {currentAccount} = useSession() + const {logoutCurrentAccount} = useSessionApi() + const agent = useAgent() + const [isAppealling, setIsAppealling] = useState(false) + const [reason, setReason] = useState('') + const graphemer = useMemo(() => new Graphemer(), []) + + const reasonGraphemeLength = useMemo(() => { + return graphemer.countGraphemes(reason) + }, [graphemer, reason]) + + const { + mutate: submitAppeal, + isPending, + isSuccess, + error, + } = useMutation({ + mutationFn: async (appealText: string) => { + if (!currentAccount) throw new Error('No session') + await agent.com.atproto.moderation.createReport({ + reasonType: ComAtprotoModerationDefs.REASONAPPEAL, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: currentAccount.did, + } satisfies ComAtprotoAdminDefs.RepoRef, + reason: appealText, + }) + }, + onSuccess: () => setReason(''), + }) + + const primaryBtn = + isAppealling && !isSuccess ? ( + <Button + variant="solid" + color="primary" + size="large" + label={_(msg`Submit appeal`)} + onPress={() => submitAppeal(reason)} + disabled={ + isPending || reasonGraphemeLength > MAX_REPORT_REASON_GRAPHEME_LENGTH + }> + <ButtonText> + <Trans>Submit Appeal</Trans> + </ButtonText> + {isPending && <ButtonIcon icon={Loader} />} + </Button> + ) : ( + <Button + variant="solid" + size="large" + color="secondary_inverted" + label={_(msg`Log out`)} + onPress={() => logoutCurrentAccount('Takendown')}> + <ButtonText> + <Trans>Log Out</Trans> + </ButtonText> + </Button> + ) + + const secondaryBtn = isAppealling ? ( + !isSuccess && ( + <Button + variant="ghost" + size="large" + color="secondary" + label={_(msg`Cancel`)} + onPress={() => setIsAppealling(false)}> + <ButtonText> + <Trans>Cancel</Trans> + </ButtonText> + </Button> + ) + ) : ( + <Button + variant="ghost" + size="large" + color="secondary" + label={_(msg`Appeal suspension`)} + onPress={() => setIsAppealling(true)}> + <ButtonText> + <Trans>Appeal Suspension</Trans> + </ButtonText> + </Button> + ) + + const webLayout = isWeb && gtMobile + + useEnableKeyboardController(true) + + return ( + <Modal + visible + animationType={native('slide')} + presentationStyle="formSheet" + style={[web(a.util_screen_outer)]}> + {isIOS && <StatusBar style="light" />} + <KeyboardAwareScrollView style={[a.flex_1, t.atoms.bg]} centerContent> + <View + style={[ + a.flex_row, + a.justify_center, + gtMobile ? a.pt_4xl : [a.px_xl, a.pt_4xl], + ]}> + <View style={[a.flex_1, {maxWidth: COL_WIDTH, minHeight: COL_WIDTH}]}> + <View style={[a.pb_xl]}> + <Logo width={64} /> + </View> + + <Text style={[a.text_4xl, a.font_heavy, a.pb_md]}> + {isAppealling ? ( + <Trans>Appeal suspension</Trans> + ) : ( + <Trans>Your account has been suspended</Trans> + )} + </Text> + + {isAppealling ? ( + <View style={[a.relative, a.w_full, a.mt_xl]}> + {isSuccess ? ( + <P style={[t.atoms.text_contrast_medium, a.text_center]}> + <Trans> + Your appeal has been submitted. If your appeal succeeds, + you will receive an email. + </Trans> + </P> + ) : ( + <> + <TextField.LabelText> + <Trans>Reason for appeal</Trans> + </TextField.LabelText> + <TextField.Root + isInvalid={ + reasonGraphemeLength > + MAX_REPORT_REASON_GRAPHEME_LENGTH || !!error + }> + <TextField.Input + label={_(msg`Reason for appeal`)} + defaultValue={reason} + onChangeText={setReason} + placeholder={_(msg`Why are you appealing?`)} + multiline + numberOfLines={5} + autoFocus + style={{paddingBottom: 40, minHeight: 150}} + maxLength={MAX_REPORT_REASON_GRAPHEME_LENGTH * 10} + /> + </TextField.Root> + <View + style={[ + a.absolute, + a.flex_row, + a.align_center, + a.pr_md, + a.pb_sm, + { + bottom: 0, + right: 0, + }, + ]}> + <CharProgress + count={reasonGraphemeLength} + max={MAX_REPORT_REASON_GRAPHEME_LENGTH} + /> + </View> + </> + )} + {error && ( + <Text + style={[ + a.text_md, + a.leading_normal, + {color: t.palette.negative_500}, + a.mt_lg, + ]}> + {cleanError(error)} + </Text> + )} + </View> + ) : ( + <P style={[t.atoms.text_contrast_medium]}> + <Trans> + Your account was found to be in violation of the{' '} + <InlineLinkText + label={_(msg`Bluesky Social Terms of Service`)} + to="https://bsky.social/about/support/tos" + style={[a.text_md, a.leading_normal]} + overridePresentation> + Bluesky Social Terms of Service + </InlineLinkText> + . You have been sent an email outlining the specific violation + and suspension period, if applicable. You can appeal this + decision if you believe it was made in error. + </Trans> + </P> + )} + + {webLayout && ( + <View + style={[ + a.w_full, + a.flex_row, + a.justify_between, + a.pt_5xl, + {paddingBottom: 200}, + ]}> + {secondaryBtn} + {primaryBtn} + </View> + )} + </View> + </View> + </KeyboardAwareScrollView> + + {!webLayout && ( + <View + style={[ + a.align_center, + t.atoms.bg, + gtMobile ? a.px_5xl : a.px_xl, + {paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom)}, + ]}> + <View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}> + {primaryBtn} + {secondaryBtn} + </View> + </View> + )} + </Modal> + ) +} diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index 84c816d44..ba0c14c1a 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -83,7 +83,12 @@ export async function createAgentAndLogin( ) => void, ) { const agent = new BskyAppAgent({service}) - await agent.login({identifier, password, authFactorToken}) + await agent.login({ + identifier, + password, + authFactorToken, + allowTakendown: true, + }) const account = agentToSessionAccountOrThrow(agent) const gates = tryFetchGates(account.did, 'prefer-fresh-gates') diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 48b258863..03a8a936a 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -258,7 +258,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ], ) - // @ts-ignore + // @ts-expect-error window type is not declared, debug only if (__DEV__ && isWeb) window.agent = state.currentAgentState.agent const agent = state.currentAgentState.agent as BskyAppAgent diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx index 9bcb91b7a..35a46b427 100644 --- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx +++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx @@ -34,6 +34,7 @@ import {LoggedOut} from '#/view/com/auth/LoggedOut' import {Deactivated} from '#/screens/Deactivated' import {Onboarding} from '#/screens/Onboarding' import {SignupQueued} from '#/screens/SignupQueued' +import {Takendown} from '#/screens/Takendown' import {atoms as a} from '#/alf' import {BottomBarWeb} from './bottom-bar/BottomBarWeb' import {DesktopLeftNav} from './desktop/LeftNav' @@ -107,6 +108,9 @@ function NativeStackNavigator({ if (hasSession && currentAccount?.signupQueued) { return <SignupQueued /> } + if (hasSession && currentAccount?.status === 'takendown') { + return <Takendown /> + } if (showLoggedOut) { return <LoggedOut onDismiss={() => setShowLoggedOut(false)} /> } |