mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {memo, useMemo, useState} from 'react'
2import {View} from 'react-native'
3import {
4 $Typed,
5 AppBskyActorDefs,
6 ChatBskyConvoDefs,
7 ComAtprotoModerationCreateReport,
8 RichText as RichTextAPI,
9} from '@atproto/api'
10import {msg, Trans} from '@lingui/macro'
11import {useLingui} from '@lingui/react'
12import {StackActions, useNavigation} from '@react-navigation/native'
13import {useMutation} from '@tanstack/react-query'
14
15import {ReportOption} from '#/lib/moderation/useReportOptions'
16import {NavigationProp} from '#/lib/routes/types'
17import {isNative} from '#/platform/detection'
18import {useProfileShadow} from '#/state/cache/profile-shadow'
19import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
20import {
21 useProfileBlockMutationQueue,
22 useProfileQuery,
23} from '#/state/queries/profile'
24import {useAgent} from '#/state/session'
25import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
26import * as Toast from '#/view/com/util/Toast'
27import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf'
28import {Button, ButtonIcon, ButtonText} from '#/components/Button'
29import * as Dialog from '#/components/Dialog'
30import {Divider} from '#/components/Divider'
31import * as Toggle from '#/components/forms/Toggle'
32import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
33import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane'
34import {Loader} from '#/components/Loader'
35import {SelectReportOptionView} from '#/components/ReportDialog/SelectReportOptionView'
36import {RichText} from '#/components/RichText'
37import {Text} from '#/components/Typography'
38import {MessageItemMetadata} from './MessageItem'
39
40type ReportDialogParams = {
41 type: 'convoMessage'
42 convoId: string
43 message: ChatBskyConvoDefs.MessageView
44}
45
46let ReportDialog = ({
47 control,
48 params,
49 currentScreen,
50}: {
51 control: Dialog.DialogControlProps
52 params: ReportDialogParams
53 currentScreen: 'list' | 'conversation'
54}): React.ReactNode => {
55 const {_} = useLingui()
56 return (
57 <Dialog.Outer control={control}>
58 <Dialog.Handle />
59 <Dialog.ScrollableInner label={_(msg`Report this message`)}>
60 <DialogInner params={params} currentScreen={currentScreen} />
61 <Dialog.Close />
62 </Dialog.ScrollableInner>
63 </Dialog.Outer>
64 )
65}
66ReportDialog = memo(ReportDialog)
67export {ReportDialog}
68
69function DialogInner({
70 params,
71 currentScreen,
72}: {
73 params: ReportDialogParams
74 currentScreen: 'list' | 'conversation'
75}) {
76 const {data: profile, isError} = useProfileQuery({
77 did: params.message.sender.did,
78 })
79 const [reportOption, setReportOption] = useState<ReportOption | null>(null)
80 const [done, setDone] = useState(false)
81 const control = Dialog.useDialogContext()
82
83 return done ? (
84 profile ? (
85 <DoneStep
86 convoId={params.convoId}
87 currentScreen={currentScreen}
88 profile={profile}
89 />
90 ) : (
91 <View style={[a.w_full, a.py_5xl, a.align_center]}>
92 <Loader />
93 </View>
94 )
95 ) : reportOption ? (
96 <SubmitStep
97 params={params}
98 reportOption={reportOption}
99 goBack={() => setReportOption(null)}
100 onComplete={() => {
101 if (isError) {
102 control.close()
103 } else {
104 setDone(true)
105 }
106 }}
107 />
108 ) : (
109 <ReasonStep params={params} setReportOption={setReportOption} />
110 )
111}
112
113function ReasonStep({
114 setReportOption,
115}: {
116 setReportOption: (reportOption: ReportOption) => void
117 params: ReportDialogParams
118}) {
119 const control = Dialog.useDialogContext()
120
121 return (
122 <SelectReportOptionView
123 labelers={[]}
124 goBack={control.close}
125 params={{
126 type: 'convoMessage',
127 }}
128 onSelectReportOption={setReportOption}
129 />
130 )
131}
132
133function SubmitStep({
134 params,
135 reportOption,
136 goBack,
137 onComplete,
138}: {
139 params: ReportDialogParams
140 reportOption: ReportOption
141 goBack: () => void
142 onComplete: () => void
143}) {
144 const {_} = useLingui()
145 const {gtMobile} = useBreakpoints()
146 const t = useTheme()
147 const [details, setDetails] = useState('')
148 const agent = useAgent()
149
150 const {
151 mutate: submit,
152 error,
153 isPending: submitting,
154 } = useMutation({
155 mutationFn: async () => {
156 if (params.type === 'convoMessage') {
157 const {convoId, message} = params
158 const subject: $Typed<ChatBskyConvoDefs.MessageRef> = {
159 $type: 'chat.bsky.convo.defs#messageRef',
160 messageId: message.id,
161 convoId,
162 did: message.sender.did,
163 }
164
165 const report = {
166 reasonType: reportOption.reason,
167 subject,
168 reason: details,
169 } satisfies ComAtprotoModerationCreateReport.InputSchema
170
171 await agent.createModerationReport(report)
172 }
173 },
174 onSuccess: onComplete,
175 })
176
177 const copy = useMemo(() => {
178 return {
179 convoMessage: {
180 title: _(msg`Report this message`),
181 },
182 }[params.type]
183 }, [_, params])
184
185 return (
186 <View style={a.gap_lg}>
187 <Button
188 size="small"
189 variant="solid"
190 color="secondary"
191 shape="round"
192 label={_(msg`Go back to previous step`)}
193 onPress={goBack}>
194 <ButtonIcon icon={Chevron} />
195 </Button>
196
197 <View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
198 <Text style={[a.text_2xl, a.font_bold]}>{copy.title}</Text>
199 <Text style={[a.text_md, t.atoms.text_contrast_medium]}>
200 <Trans>
201 Your report will be sent to the Bluesky Moderation Service
202 </Trans>
203 </Text>
204 </View>
205
206 {params.type === 'convoMessage' && (
207 <PreviewMessage message={params.message} />
208 )}
209
210 <Text style={[a.text_md, t.atoms.text_contrast_medium]}>
211 <Text style={[a.font_bold, a.text_md, t.atoms.text_contrast_medium]}>
212 <Trans>Reason:</Trans>
213 </Text>{' '}
214 <Text style={[a.font_bold, a.text_md]}>{reportOption.title}</Text>
215 </Text>
216
217 <Divider />
218
219 <View style={[a.gap_md]}>
220 <Text style={[t.atoms.text_contrast_medium]}>
221 <Trans>Optionally provide additional information below:</Trans>
222 </Text>
223
224 <View style={[a.relative, a.w_full]}>
225 <Dialog.Input
226 multiline
227 defaultValue={details}
228 onChangeText={setDetails}
229 label={_(msg`Text field`)}
230 style={{paddingRight: 60}}
231 numberOfLines={5}
232 />
233 <View
234 style={[
235 a.absolute,
236 a.flex_row,
237 a.align_center,
238 a.pr_md,
239 a.pb_sm,
240 {
241 bottom: 0,
242 right: 0,
243 },
244 ]}>
245 <CharProgress count={details?.length || 0} />
246 </View>
247 </View>
248 </View>
249
250 <View style={[a.flex_row, a.align_center, a.justify_end, a.gap_lg]}>
251 {error && (
252 <Text
253 style={[
254 a.flex_1,
255 a.italic,
256 a.leading_snug,
257 t.atoms.text_contrast_medium,
258 ]}>
259 <Trans>
260 There was an issue sending your report. Please check your internet
261 connection.
262 </Trans>
263 </Text>
264 )}
265
266 <Button
267 testID="sendReportBtn"
268 size="large"
269 variant="solid"
270 color="negative"
271 label={_(msg`Send report`)}
272 onPress={() => submit()}>
273 <ButtonText>
274 <Trans>Send report</Trans>
275 </ButtonText>
276 <ButtonIcon icon={submitting ? Loader : SendIcon} />
277 </Button>
278 </View>
279 </View>
280 )
281}
282
283function DoneStep({
284 convoId,
285 currentScreen,
286 profile,
287}: {
288 convoId: string
289 currentScreen: 'list' | 'conversation'
290 profile: AppBskyActorDefs.ProfileViewDetailed
291}) {
292 const {_} = useLingui()
293 const navigation = useNavigation<NavigationProp>()
294 const control = Dialog.useDialogContext()
295 const {gtMobile} = useBreakpoints()
296 const t = useTheme()
297 const [actions, setActions] = useState<string[]>(['block', 'leave'])
298 const shadow = useProfileShadow(profile)
299 const [queueBlock] = useProfileBlockMutationQueue(shadow)
300
301 const {mutate: leaveConvo} = useLeaveConvo(convoId, {
302 onMutate: () => {
303 if (currentScreen === 'conversation') {
304 navigation.dispatch(
305 StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}),
306 )
307 }
308 },
309 onError: () => {
310 Toast.show(_(msg`Could not leave chat`), 'xmark')
311 },
312 })
313
314 let btnText = _(msg`Done`)
315 let toastMsg: string | undefined
316 if (actions.includes('leave') && actions.includes('block')) {
317 btnText = _(msg`Block and Delete`)
318 toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'}))
319 } else if (actions.includes('leave')) {
320 btnText = _(msg`Delete Conversation`)
321 toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'}))
322 } else if (actions.includes('block')) {
323 btnText = _(msg`Block User`)
324 toastMsg = _(msg({message: 'User blocked', context: 'toast'}))
325 }
326
327 const onPressPrimaryAction = () => {
328 control.close(() => {
329 if (actions.includes('block')) {
330 queueBlock()
331 }
332 if (actions.includes('leave')) {
333 leaveConvo()
334 }
335 if (toastMsg) {
336 Toast.show(toastMsg, 'check')
337 }
338 })
339 }
340
341 return (
342 <View style={a.gap_2xl}>
343 <View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
344 <Text style={[a.text_2xl, a.font_bold]}>
345 <Trans>Report submitted</Trans>
346 </Text>
347 <Text style={[a.text_md, t.atoms.text_contrast_medium]}>
348 <Trans>Our moderation team has received your report.</Trans>
349 </Text>
350 </View>
351 <Toggle.Group
352 label={_(msg`Block and/or delete this conversation`)}
353 values={actions}
354 onChange={setActions}>
355 <View style={[a.gap_md]}>
356 <Toggle.Item name="block" label={_(msg`Block user`)}>
357 <Toggle.Checkbox />
358 <Toggle.LabelText style={[a.text_md]}>
359 <Trans>Block user</Trans>
360 </Toggle.LabelText>
361 </Toggle.Item>
362 <Toggle.Item name="leave" label={_(msg`Delete conversation`)}>
363 <Toggle.Checkbox />
364 <Toggle.LabelText style={[a.text_md]}>
365 <Trans>Delete conversation</Trans>
366 </Toggle.LabelText>
367 </Toggle.Item>
368 </View>
369 </Toggle.Group>
370
371 <View style={[a.gap_md, web([a.flex_row_reverse])]}>
372 <Button
373 label={btnText}
374 onPress={onPressPrimaryAction}
375 size="large"
376 variant="solid"
377 color={actions.length > 0 ? 'negative' : 'primary'}>
378 <ButtonText>{btnText}</ButtonText>
379 </Button>
380 <Button
381 label={_(msg`Close`)}
382 onPress={() => control.close()}
383 size={platform({native: 'small', web: 'large'})}
384 variant={platform({
385 native: 'ghost',
386 web: 'solid',
387 })}
388 color="secondary">
389 <ButtonText>
390 <Trans>Close</Trans>
391 </ButtonText>
392 </Button>
393 </View>
394 </View>
395 )
396}
397
398function PreviewMessage({message}: {message: ChatBskyConvoDefs.MessageView}) {
399 const t = useTheme()
400 const rt = useMemo(() => {
401 return new RichTextAPI({text: message.text, facets: message.facets})
402 }, [message.text, message.facets])
403
404 return (
405 <View style={a.align_start}>
406 <View
407 style={[
408 a.py_sm,
409 a.my_2xs,
410 a.rounded_md,
411 {
412 paddingLeft: 14,
413 paddingRight: 14,
414 backgroundColor: t.palette.contrast_50,
415 borderRadius: 17,
416 },
417 {borderBottomLeftRadius: 2},
418 ]}>
419 <RichText
420 value={rt}
421 style={[a.text_md, a.leading_snug]}
422 interactiveStyle={a.underline}
423 enableTags
424 />
425 </View>
426 <MessageItemMetadata
427 item={{
428 type: 'message',
429 message,
430 key: '',
431 nextMessage: null,
432 prevMessage: null,
433 }}
434 style={[a.text_left, a.mb_0]}
435 />
436 </View>
437 )
438}