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} 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}