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