forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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, Trans} from '@lingui/macro'
7import {useLingui} from '@lingui/react'
8import {useMutation} from '@tanstack/react-query'
9import {countGraphemes} from 'unicode-segmenter/grapheme'
10
11import {
12 BLUESKY_MOD_SERVICE_HEADERS,
13 MAX_REPORT_REASON_GRAPHEME_LENGTH,
14} from '#/lib/constants'
15import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController'
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 useEnableKeyboardController(true)
125
126 return (
127 <View style={[a.util_screen_outer, a.flex_1]}>
128 <KeyboardAwareScrollView style={[a.flex_1, t.atoms.bg]} centerContent>
129 <View
130 style={[
131 a.flex_row,
132 a.justify_center,
133 gtMobile ? a.pt_4xl : [a.px_xl, a.pt_4xl],
134 ]}>
135 <View style={[a.flex_1, {maxWidth: COL_WIDTH, minHeight: COL_WIDTH}]}>
136 <View style={[a.pb_xl]}>
137 <Logo width={64} />
138 </View>
139
140 <Text style={[a.text_4xl, a.font_bold, a.pb_md]}>
141 {isAppealling ? (
142 <Trans>Appeal suspension</Trans>
143 ) : (
144 <Trans>Your account has been suspended</Trans>
145 )}
146 </Text>
147
148 {isAppealling ? (
149 <View style={[a.relative, a.w_full, a.mt_xl]}>
150 {isSuccess ? (
151 <P style={[t.atoms.text_contrast_medium, a.text_center]}>
152 <Trans>
153 Your appeal has been submitted. If your appeal succeeds,
154 you will receive an email.
155 </Trans>
156 </P>
157 ) : (
158 <>
159 <TextField.LabelText>
160 <Trans>Reason for appeal</Trans>
161 </TextField.LabelText>
162 <TextField.Root
163 isInvalid={
164 reasonGraphemeLength >
165 MAX_REPORT_REASON_GRAPHEME_LENGTH || !!error
166 }>
167 <TextField.Input
168 label={_(msg`Reason for appeal`)}
169 defaultValue={reason}
170 onChangeText={setReason}
171 placeholder={_(msg`Why are you appealing?`)}
172 multiline
173 numberOfLines={5}
174 autoFocus
175 style={{paddingBottom: 40, minHeight: 150}}
176 maxLength={MAX_REPORT_REASON_GRAPHEME_LENGTH * 10}
177 />
178 </TextField.Root>
179 <View
180 style={[
181 a.absolute,
182 a.flex_row,
183 a.align_center,
184 a.pr_md,
185 a.pb_sm,
186 {
187 bottom: 0,
188 right: 0,
189 },
190 ]}>
191 <CharProgress
192 count={reasonGraphemeLength}
193 max={MAX_REPORT_REASON_GRAPHEME_LENGTH}
194 />
195 </View>
196 </>
197 )}
198 {error && (
199 <Text
200 style={[
201 a.text_md,
202 a.leading_snug,
203 {color: t.palette.negative_500},
204 a.mt_lg,
205 ]}>
206 {cleanError(error)}
207 </Text>
208 )}
209 </View>
210 ) : (
211 <P style={[t.atoms.text_contrast_medium, a.leading_snug]}>
212 <Trans>
213 Your account was found to be in violation of the{' '}
214 <SimpleInlineLinkText
215 label={_(msg`Bluesky Social Terms of Service`)}
216 to="https://bsky.social/about/support/tos"
217 style={[a.text_md, a.leading_snug]}>
218 Bluesky Social Terms of Service
219 </SimpleInlineLinkText>
220 . You have been sent an email outlining the specific violation
221 and suspension period, if applicable. You can appeal this
222 decision if you believe it was made in error.
223 </Trans>
224 </P>
225 )}
226
227 {webLayout && (
228 <View
229 style={[
230 a.w_full,
231 a.flex_row,
232 a.justify_between,
233 a.pt_5xl,
234 {paddingBottom: 200},
235 ]}>
236 {secondaryBtn}
237 {primaryBtn}
238 </View>
239 )}
240 </View>
241 </View>
242 </KeyboardAwareScrollView>
243
244 {!webLayout && (
245 <View
246 style={[
247 a.align_center,
248 t.atoms.bg,
249 gtMobile ? a.px_5xl : a.px_xl,
250 {paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom)},
251 ]}>
252 <View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}>
253 {primaryBtn}
254 {secondaryBtn}
255 </View>
256 </View>
257 )}
258 </View>
259 )
260}