about summary refs log tree commit diff
path: root/src/components/dms/ReportDialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/dms/ReportDialog.tsx')
-rw-r--r--src/components/dms/ReportDialog.tsx310
1 files changed, 310 insertions, 0 deletions
diff --git a/src/components/dms/ReportDialog.tsx b/src/components/dms/ReportDialog.tsx
new file mode 100644
index 000000000..e8ac0ed2f
--- /dev/null
+++ b/src/components/dms/ReportDialog.tsx
@@ -0,0 +1,310 @@
+import React, {memo, useMemo, useState} from 'react'
+import {View} from 'react-native'
+import {
+  ChatBskyConvoDefs,
+  ComAtprotoModerationCreateReport,
+  RichText as RichTextAPI,
+} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useMutation} from '@tanstack/react-query'
+
+import {ReportOption} from '#/lib/moderation/useReportOptions'
+import {isAndroid} from '#/platform/detection'
+import {useAgent} from '#/state/session'
+import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
+import * as Toast from '#/view/com/util/Toast'
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import * as Dialog from '#/components/Dialog'
+import {Button, ButtonIcon, ButtonText} from '../Button'
+import {Divider} from '../Divider'
+import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron'
+import {Loader} from '../Loader'
+import {SelectReportOptionView} from '../ReportDialog/SelectReportOptionView'
+import {RichText} from '../RichText'
+import {Text} from '../Typography'
+import {MessageItemMetadata} from './MessageItem'
+
+type ReportDialogParams =
+  | {
+      type: 'convoAccount'
+      did: string
+      convoId: string
+    }
+  | {
+      type: 'convoMessage'
+      convoId: string
+      message: ChatBskyConvoDefs.MessageView
+    }
+
+let ReportDialog = ({
+  control,
+  params,
+}: {
+  control: Dialog.DialogControlProps
+  params: ReportDialogParams
+}): React.ReactNode => {
+  const {_} = useLingui()
+  return (
+    <Dialog.Outer
+      control={control}
+      nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}>
+      <Dialog.Handle />
+      <Dialog.ScrollableInner label={_(msg`Report this message`)}>
+        <DialogInner params={params} />
+        <Dialog.Close />
+      </Dialog.ScrollableInner>
+    </Dialog.Outer>
+  )
+}
+ReportDialog = memo(ReportDialog)
+export {ReportDialog}
+
+function DialogInner({params}: {params: ReportDialogParams}) {
+  const [reportOption, setReportOption] = useState<ReportOption | null>(null)
+
+  return reportOption ? (
+    <SubmitStep
+      params={params}
+      reportOption={reportOption}
+      goBack={() => setReportOption(null)}
+    />
+  ) : (
+    <ReasonStep params={params} setReportOption={setReportOption} />
+  )
+}
+
+function ReasonStep({
+  setReportOption,
+  params,
+}: {
+  setReportOption: (reportOption: ReportOption) => void
+  params: ReportDialogParams
+}) {
+  const control = Dialog.useDialogContext()
+
+  return (
+    <SelectReportOptionView
+      labelers={[]}
+      goBack={control.close}
+      params={
+        params.type === 'convoMessage'
+          ? {
+              type: 'convoMessage',
+            }
+          : {
+              type: 'convoAccount',
+            }
+      }
+      onSelectReportOption={setReportOption}
+    />
+  )
+}
+
+function SubmitStep({
+  params,
+  reportOption,
+  goBack,
+}: {
+  params: ReportDialogParams
+  reportOption: ReportOption
+  goBack: () => void
+}) {
+  const {_} = useLingui()
+  const {gtMobile} = useBreakpoints()
+  const t = useTheme()
+  const [details, setDetails] = useState('')
+  const control = Dialog.useDialogContext()
+  const {getAgent} = useAgent()
+
+  const {
+    mutate: submit,
+    error,
+    isPending: submitting,
+  } = useMutation({
+    mutationFn: async () => {
+      if (params.type === 'convoMessage') {
+        const {convoId, message} = params
+
+        const report = {
+          reasonType: reportOption.reason,
+          subject: {
+            $type: 'chat.bsky.convo.defs#messageRef',
+            messageId: message.id,
+            convoId,
+            did: message.sender.did,
+          } satisfies ChatBskyConvoDefs.MessageRef,
+          reason: details,
+        } satisfies ComAtprotoModerationCreateReport.InputSchema
+
+        await getAgent().createModerationReport(report)
+      } else if (params.type === 'convoAccount') {
+        const {convoId, did} = params
+
+        await getAgent().createModerationReport({
+          reasonType: reportOption.reason,
+          subject: {
+            $type: 'com.atproto.admin.defs#repoRef',
+            did,
+          },
+          reason: details + ` — from:dms:${convoId}`,
+        })
+      }
+    },
+    onSuccess: () => {
+      control.close(() => {
+        Toast.show(_(msg`Thank you. Your report has been sent.`))
+      })
+    },
+  })
+
+  const copy = useMemo(() => {
+    return {
+      convoMessage: {
+        title: _(msg`Report this message`),
+      },
+      convoAccount: {
+        title: _(msg`Report this account`),
+      },
+    }[params.type]
+  }, [_, params])
+
+  return (
+    <View style={a.gap_lg}>
+      <Button
+        size="small"
+        variant="solid"
+        color="secondary"
+        shape="round"
+        label={_(msg`Go back to previous step`)}
+        onPress={goBack}>
+        <ButtonIcon icon={Chevron} />
+      </Button>
+
+      <View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
+        <Text style={[a.text_2xl, a.font_bold]}>{copy.title}</Text>
+        <Text style={[a.text_md, t.atoms.text_contrast_medium]}>
+          <Trans>
+            Your report will be sent to the Bluesky Moderation Service
+          </Trans>
+        </Text>
+      </View>
+
+      {params.type === 'convoMessage' && (
+        <PreviewMessage message={params.message} />
+      )}
+
+      <Text style={[a.text_md, t.atoms.text_contrast_medium]}>
+        <Text style={[a.font_bold, a.text_md, t.atoms.text_contrast_medium]}>
+          <Trans>Reason:</Trans>
+        </Text>{' '}
+        <Text style={[a.font_bold, a.text_md]}>{reportOption.title}</Text>
+      </Text>
+
+      <Divider />
+
+      <View style={[a.gap_md]}>
+        <Text style={[t.atoms.text_contrast_medium]}>
+          <Trans>Optionally provide additional information below:</Trans>
+        </Text>
+
+        <View style={[a.relative, a.w_full]}>
+          <Dialog.Input
+            multiline
+            value={details}
+            onChangeText={setDetails}
+            label="Text field"
+            style={{paddingRight: 60}}
+            numberOfLines={6}
+          />
+
+          <View
+            style={[
+              a.absolute,
+              a.flex_row,
+              a.align_center,
+              a.pr_md,
+              a.pb_sm,
+              {
+                bottom: 0,
+                right: 0,
+              },
+            ]}>
+            <CharProgress count={details?.length || 0} />
+          </View>
+        </View>
+      </View>
+
+      <View style={[a.flex_row, a.align_center, a.justify_end, a.gap_lg]}>
+        {error && (
+          <Text
+            style={[
+              a.flex_1,
+              a.italic,
+              a.leading_snug,
+              t.atoms.text_contrast_medium,
+            ]}>
+            <Trans>
+              There was an issue sending your report. Please check your internet
+              connection.
+            </Trans>
+          </Text>
+        )}
+
+        <Button
+          testID="sendReportBtn"
+          size="large"
+          variant="solid"
+          color="negative"
+          label={_(msg`Send report`)}
+          onPress={() => submit()}>
+          <ButtonText>
+            <Trans>Send report</Trans>
+          </ButtonText>
+          {submitting && <ButtonIcon icon={Loader} />}
+        </Button>
+      </View>
+    </View>
+  )
+}
+
+function PreviewMessage({message}: {message: ChatBskyConvoDefs.MessageView}) {
+  const t = useTheme()
+  const rt = useMemo(() => {
+    return new RichTextAPI({text: message.text, facets: message.facets})
+  }, [message.text, message.facets])
+
+  return (
+    <View style={a.align_start}>
+      <View
+        style={[
+          a.py_sm,
+          a.my_2xs,
+          a.rounded_md,
+          {
+            paddingLeft: 14,
+            paddingRight: 14,
+            backgroundColor: t.palette.contrast_50,
+            borderRadius: 17,
+          },
+          {borderBottomLeftRadius: 2},
+        ]}>
+        <RichText
+          value={rt}
+          style={[a.text_md, a.leading_snug]}
+          interactiveStyle={a.underline}
+          enableTags
+        />
+      </View>
+      <MessageItemMetadata
+        item={{
+          type: 'message',
+          message,
+          key: '',
+          nextMessage: null,
+        }}
+        style={[a.text_left, a.mb_0]}
+      />
+    </View>
+  )
+}