mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {View} from 'react-native'
3import {ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api'
4import {msg, Trans} from '@lingui/macro'
5import {useLingui} from '@lingui/react'
6import {useMutation} from '@tanstack/react-query'
7
8import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
9import {useLabelSubject} from '#/lib/moderation'
10import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
11import {makeProfileLink} from '#/lib/routes/links'
12import {sanitizeHandle} from '#/lib/strings/handles'
13import {logger} from '#/logger'
14import {isAndroid} from '#/platform/detection'
15import {useAgent, useSession} from '#/state/session'
16import * as Toast from '#/view/com/util/Toast'
17import {atoms as a, useBreakpoints, useTheme} from '#/alf'
18import {Button, ButtonIcon, ButtonText} from '#/components/Button'
19import * as Dialog from '#/components/Dialog'
20import {InlineLinkText} from '#/components/Link'
21import {Text} from '#/components/Typography'
22import {Divider} from '../Divider'
23import {Loader} from '../Loader'
24
25export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog'
26
27export interface LabelsOnMeDialogProps {
28 control: Dialog.DialogOuterProps['control']
29 labels: ComAtprotoLabelDefs.Label[]
30 type: 'account' | 'content'
31}
32
33export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) {
34 return (
35 <Dialog.Outer control={props.control}>
36 <Dialog.Handle />
37 <LabelsOnMeDialogInner {...props} />
38 </Dialog.Outer>
39 )
40}
41
42function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
43 const {_} = useLingui()
44 const {currentAccount} = useSession()
45 const [appealingLabel, setAppealingLabel] = React.useState<
46 ComAtprotoLabelDefs.Label | undefined
47 >(undefined)
48 const {labels} = props
49 const isAccount = props.type === 'account'
50 const containsSelfLabel = React.useMemo(
51 () => labels.some(l => l.src === currentAccount?.did),
52 [currentAccount?.did, labels],
53 )
54
55 return (
56 <Dialog.ScrollableInner
57 label={
58 isAccount
59 ? _(msg`The following labels were applied to your account.`)
60 : _(msg`The following labels were applied to your content.`)
61 }>
62 {appealingLabel ? (
63 <AppealForm
64 label={appealingLabel}
65 control={props.control}
66 onPressBack={() => setAppealingLabel(undefined)}
67 />
68 ) : (
69 <>
70 <Text style={[a.text_2xl, a.font_heavy, a.pb_xs, a.leading_tight]}>
71 {isAccount ? (
72 <Trans>Labels on your account</Trans>
73 ) : (
74 <Trans>Labels on your content</Trans>
75 )}
76 </Text>
77 <Text style={[a.text_md, a.leading_snug]}>
78 {containsSelfLabel ? (
79 <Trans>
80 You may appeal non-self labels if you feel they were placed in
81 error.
82 </Trans>
83 ) : (
84 <Trans>
85 You may appeal these labels if you feel they were placed in
86 error.
87 </Trans>
88 )}
89 </Text>
90
91 <View style={[a.py_lg, a.gap_md]}>
92 {labels.map(label => (
93 <Label
94 key={`${label.val}-${label.src}`}
95 label={label}
96 isSelfLabel={label.src === currentAccount?.did}
97 control={props.control}
98 onPressAppeal={setAppealingLabel}
99 />
100 ))}
101 </View>
102 </>
103 )}
104 <Dialog.Close />
105 </Dialog.ScrollableInner>
106 )
107}
108
109function Label({
110 label,
111 isSelfLabel,
112 control,
113 onPressAppeal,
114}: {
115 label: ComAtprotoLabelDefs.Label
116 isSelfLabel: boolean
117 control: Dialog.DialogOuterProps['control']
118 onPressAppeal: (label: ComAtprotoLabelDefs.Label) => void
119}) {
120 const t = useTheme()
121 const {_} = useLingui()
122 const {labeler, strings} = useLabelInfo(label)
123 const sourceName = labeler
124 ? sanitizeHandle(labeler.creator.handle, '@')
125 : label.src
126 const timeDiff = useGetTimeAgo({future: true})
127 return (
128 <View
129 style={[
130 a.border,
131 t.atoms.border_contrast_low,
132 a.rounded_sm,
133 a.overflow_hidden,
134 ]}>
135 <View style={[a.p_md, a.gap_sm, a.flex_row]}>
136 <View style={[a.flex_1, a.gap_xs]}>
137 <Text emoji style={[a.font_bold, a.text_md]}>
138 {strings.name}
139 </Text>
140 <Text emoji style={[t.atoms.text_contrast_medium, a.leading_snug]}>
141 {strings.description}
142 </Text>
143 </View>
144 {!isSelfLabel && (
145 <View>
146 <Button
147 variant="solid"
148 color="secondary"
149 size="small"
150 label={_(msg`Appeal`)}
151 onPress={() => onPressAppeal(label)}>
152 <ButtonText>
153 <Trans>Appeal</Trans>
154 </ButtonText>
155 </Button>
156 </View>
157 )}
158 </View>
159
160 <Divider />
161
162 <View style={[a.px_md, a.py_sm, t.atoms.bg_contrast_25]}>
163 {isSelfLabel ? (
164 <Text style={[t.atoms.text_contrast_medium]}>
165 <Trans>This label was applied by you.</Trans>
166 </Text>
167 ) : (
168 <View
169 style={[
170 a.flex_row,
171 a.justify_between,
172 a.gap_xl,
173 {paddingBottom: 1},
174 ]}>
175 <Text
176 style={[a.flex_1, a.leading_snug, t.atoms.text_contrast_medium]}
177 numberOfLines={1}>
178 <Trans>
179 Source:{' '}
180 <InlineLinkText
181 label={sourceName}
182 to={makeProfileLink(
183 labeler ? labeler.creator : {did: label.src, handle: ''},
184 )}
185 onPress={() => control.close()}>
186 {sourceName}
187 </InlineLinkText>
188 </Trans>
189 </Text>
190 {label.exp && (
191 <View>
192 <Text
193 style={[
194 a.leading_snug,
195 a.text_sm,
196 a.italic,
197 t.atoms.text_contrast_medium,
198 ]}>
199 <Trans>Expires in {timeDiff(Date.now(), label.exp)}</Trans>
200 </Text>
201 </View>
202 )}
203 </View>
204 )}
205 </View>
206 </View>
207 )
208}
209
210function AppealForm({
211 label,
212 control,
213 onPressBack,
214}: {
215 label: ComAtprotoLabelDefs.Label
216 control: Dialog.DialogOuterProps['control']
217 onPressBack: () => void
218}) {
219 const {_} = useLingui()
220 const {labeler, strings} = useLabelInfo(label)
221 const {gtMobile} = useBreakpoints()
222 const [details, setDetails] = React.useState('')
223 const {subject} = useLabelSubject({label})
224 const isAccountReport = 'did' in subject
225 const agent = useAgent()
226 const sourceName = labeler
227 ? sanitizeHandle(labeler.creator.handle, '@')
228 : label.src
229
230 const {mutate, isPending} = useMutation({
231 mutationFn: async () => {
232 const $type = !isAccountReport
233 ? 'com.atproto.repo.strongRef'
234 : 'com.atproto.admin.defs#repoRef'
235 await agent.createModerationReport(
236 {
237 reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
238 subject: {
239 $type,
240 ...subject,
241 },
242 reason: details,
243 },
244 {
245 encoding: 'application/json',
246 headers: {
247 'atproto-proxy': `${label.src}#atproto_labeler`,
248 },
249 },
250 )
251 },
252 onError: err => {
253 logger.error('Failed to submit label appeal', {message: err})
254 Toast.show(_(msg`Failed to submit appeal, please try again.`), 'xmark')
255 },
256 onSuccess: () => {
257 control.close()
258 Toast.show(_(msg({message: 'Appeal submitted', context: 'toast'})))
259 },
260 })
261
262 const onSubmit = React.useCallback(() => mutate(), [mutate])
263
264 return (
265 <>
266 <View>
267 <Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}>
268 <Trans>Appeal "{strings.name}" label</Trans>
269 </Text>
270 <Text style={[a.text_md, a.leading_snug]}>
271 <Trans>
272 This appeal will be sent to{' '}
273 <InlineLinkText
274 label={sourceName}
275 to={makeProfileLink(
276 labeler ? labeler.creator : {did: label.src, handle: ''},
277 )}
278 onPress={() => control.close()}
279 style={[a.text_md, a.leading_snug]}>
280 {sourceName}
281 </InlineLinkText>
282 .
283 </Trans>
284 </Text>
285 </View>
286 <View style={[a.my_md]}>
287 <Dialog.Input
288 label={_(msg`Text input field`)}
289 placeholder={_(
290 msg`Please explain why you think this label was incorrectly applied by ${
291 labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src
292 }`,
293 )}
294 value={details}
295 onChangeText={setDetails}
296 autoFocus={true}
297 numberOfLines={3}
298 multiline
299 maxLength={300}
300 />
301 </View>
302
303 <View
304 style={
305 gtMobile
306 ? [a.flex_row, a.justify_between]
307 : [{flexDirection: 'column-reverse'}, a.gap_sm]
308 }>
309 <Button
310 testID="backBtn"
311 variant="solid"
312 color="secondary"
313 size="large"
314 onPress={onPressBack}
315 label={_(msg`Back`)}>
316 <ButtonText>{_(msg`Back`)}</ButtonText>
317 </Button>
318 <Button
319 testID="submitBtn"
320 variant="solid"
321 color="primary"
322 size="large"
323 onPress={onSubmit}
324 label={_(msg`Submit`)}>
325 <ButtonText>{_(msg`Submit`)}</ButtonText>
326 {isPending && <ButtonIcon icon={Loader} />}
327 </Button>
328 </View>
329 {isAndroid && <View style={{height: 300}} />}
330 </>
331 )
332}