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