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