Bluesky app fork with some witchin' additions 馃挮
at main 258 lines 8.5 kB view raw
1import {useState} from 'react' 2import {View} from 'react-native' 3import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' 4import {useSafeAreaInsets} from 'react-native-safe-area-context' 5import {type ComAtprotoAdminDefs, ToolsOzoneReportDefs} from '@atproto/api' 6import {msg} from '@lingui/core/macro' 7import {useLingui} from '@lingui/react' 8import {Trans} from '@lingui/react/macro' 9import {useMutation} from '@tanstack/react-query' 10import {countGraphemes} from 'unicode-segmenter/grapheme' 11 12import { 13 BLUESKY_MOD_SERVICE_HEADERS, 14 MAX_REPORT_REASON_GRAPHEME_LENGTH, 15} from '#/lib/constants' 16import {cleanError} from '#/lib/strings/errors' 17import {useAgent, useSession, useSessionApi} from '#/state/session' 18import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' 19import {Logo} from '#/view/icons/Logo' 20import {atoms as a, useBreakpoints, useTheme} from '#/alf' 21import {Button, ButtonIcon, ButtonText} from '#/components/Button' 22import * as TextField from '#/components/forms/TextField' 23import {SimpleInlineLinkText} from '#/components/Link' 24import {Loader} from '#/components/Loader' 25import {P, Text} from '#/components/Typography' 26import {IS_WEB} from '#/env' 27 28const COL_WIDTH = 400 29 30export function Takendown() { 31 const {_} = useLingui() 32 const t = useTheme() 33 const insets = useSafeAreaInsets() 34 const {gtMobile} = useBreakpoints() 35 const {currentAccount} = useSession() 36 const {logoutCurrentAccount} = useSessionApi() 37 const agent = useAgent() 38 const [isAppealling, setIsAppealling] = useState(false) 39 const [reason, setReason] = useState('') 40 41 const reasonGraphemeLength = countGraphemes(reason) 42 const isOverMaxLength = 43 reasonGraphemeLength > MAX_REPORT_REASON_GRAPHEME_LENGTH 44 45 const { 46 mutate: submitAppeal, 47 isPending, 48 isSuccess, 49 error, 50 } = useMutation({ 51 mutationFn: async (appealText: string) => { 52 if (!currentAccount) throw new Error('No session') 53 await agent.com.atproto.moderation.createReport( 54 { 55 reasonType: ToolsOzoneReportDefs.REASONAPPEAL, 56 subject: { 57 $type: 'com.atproto.admin.defs#repoRef', 58 did: currentAccount.did, 59 } satisfies ComAtprotoAdminDefs.RepoRef, 60 reason: appealText, 61 }, 62 { 63 encoding: 'application/json', 64 headers: BLUESKY_MOD_SERVICE_HEADERS, 65 }, 66 ) 67 }, 68 onSuccess: () => setReason(''), 69 }) 70 71 const primaryBtn = 72 isAppealling && !isSuccess ? ( 73 <Button 74 color="primary" 75 size="large" 76 label={_(msg`Submit appeal`)} 77 onPress={() => submitAppeal(reason)} 78 disabled={isPending || isOverMaxLength}> 79 <ButtonText> 80 <Trans>Submit Appeal</Trans> 81 </ButtonText> 82 {isPending && <ButtonIcon icon={Loader} />} 83 </Button> 84 ) : ( 85 <Button 86 size="large" 87 color="secondary_inverted" 88 label={_(msg`Sign out`)} 89 onPress={() => logoutCurrentAccount('Takendown')}> 90 <ButtonText> 91 <Trans>Sign Out</Trans> 92 </ButtonText> 93 </Button> 94 ) 95 96 const secondaryBtn = isAppealling ? ( 97 !isSuccess && ( 98 <Button 99 variant="ghost" 100 size="large" 101 color="secondary" 102 label={_(msg`Cancel`)} 103 onPress={() => setIsAppealling(false)}> 104 <ButtonText> 105 <Trans>Cancel</Trans> 106 </ButtonText> 107 </Button> 108 ) 109 ) : ( 110 <Button 111 variant="ghost" 112 size="large" 113 color="secondary" 114 label={_(msg`Appeal suspension`)} 115 onPress={() => setIsAppealling(true)}> 116 <ButtonText> 117 <Trans>Appeal Suspension</Trans> 118 </ButtonText> 119 </Button> 120 ) 121 122 const webLayout = IS_WEB && gtMobile 123 124 return ( 125 <View style={[a.util_screen_outer, a.flex_1]}> 126 <KeyboardAwareScrollView style={[a.flex_1, t.atoms.bg]} centerContent> 127 <View 128 style={[ 129 a.flex_row, 130 a.justify_center, 131 gtMobile ? a.pt_4xl : [a.px_xl, a.pt_4xl], 132 ]}> 133 <View style={[a.flex_1, {maxWidth: COL_WIDTH, minHeight: COL_WIDTH}]}> 134 <View style={[a.pb_xl]}> 135 <Logo width={64} /> 136 </View> 137 138 <Text style={[a.text_4xl, a.font_bold, a.pb_md]}> 139 {isAppealling ? ( 140 <Trans>Appeal suspension</Trans> 141 ) : ( 142 <Trans>Your account has been suspended</Trans> 143 )} 144 </Text> 145 146 {isAppealling ? ( 147 <View style={[a.relative, a.w_full, a.mt_xl]}> 148 {isSuccess ? ( 149 <P style={[t.atoms.text_contrast_medium, a.text_center]}> 150 <Trans> 151 Your appeal has been submitted. If your appeal succeeds, 152 you will receive an email. 153 </Trans> 154 </P> 155 ) : ( 156 <> 157 <TextField.LabelText> 158 <Trans>Reason for appeal</Trans> 159 </TextField.LabelText> 160 <TextField.Root 161 isInvalid={ 162 reasonGraphemeLength > 163 MAX_REPORT_REASON_GRAPHEME_LENGTH || !!error 164 }> 165 <TextField.Input 166 label={_(msg`Reason for appeal`)} 167 defaultValue={reason} 168 onChangeText={setReason} 169 placeholder={_(msg`Why are you appealing?`)} 170 multiline 171 numberOfLines={5} 172 autoFocus 173 style={{paddingBottom: 40, minHeight: 150}} 174 maxLength={MAX_REPORT_REASON_GRAPHEME_LENGTH * 10} 175 /> 176 </TextField.Root> 177 <View 178 style={[ 179 a.absolute, 180 a.flex_row, 181 a.align_center, 182 a.pr_md, 183 a.pb_sm, 184 { 185 bottom: 0, 186 right: 0, 187 }, 188 ]}> 189 <CharProgress 190 count={reasonGraphemeLength} 191 max={MAX_REPORT_REASON_GRAPHEME_LENGTH} 192 /> 193 </View> 194 </> 195 )} 196 {error && ( 197 <Text 198 style={[ 199 a.text_md, 200 a.leading_snug, 201 {color: t.palette.negative_500}, 202 a.mt_lg, 203 ]}> 204 {cleanError(error)} 205 </Text> 206 )} 207 </View> 208 ) : ( 209 <P style={[t.atoms.text_contrast_medium, a.leading_snug]}> 210 <Trans> 211 Your account was found to be in violation of the{' '} 212 <SimpleInlineLinkText 213 label={_(msg`Bluesky Social Terms of Service`)} 214 to="https://bsky.social/about/support/tos" 215 style={[a.text_md, a.leading_snug]}> 216 Bluesky Social Terms of Service 217 </SimpleInlineLinkText> 218 . You have been sent an email outlining the specific violation 219 and suspension period, if applicable. You can appeal this 220 decision if you believe it was made in error. 221 </Trans> 222 </P> 223 )} 224 225 {webLayout && ( 226 <View 227 style={[ 228 a.w_full, 229 a.flex_row, 230 a.justify_between, 231 a.pt_5xl, 232 {paddingBottom: 200}, 233 ]}> 234 {secondaryBtn} 235 {primaryBtn} 236 </View> 237 )} 238 </View> 239 </View> 240 </KeyboardAwareScrollView> 241 242 {!webLayout && ( 243 <View 244 style={[ 245 a.align_center, 246 t.atoms.bg, 247 gtMobile ? a.px_5xl : a.px_xl, 248 {paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom)}, 249 ]}> 250 <View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}> 251 {primaryBtn} 252 {secondaryBtn} 253 </View> 254 </View> 255 )} 256 </View> 257 ) 258}