diff --git a/src/components/ReportDialog/SelectReportOptionView.tsx b/src/components/ReportDialog/SelectReportOptionView.tsx
index 8219b20951..da3c434401 100644
--- a/src/components/ReportDialog/SelectReportOptionView.tsx
+++ b/src/components/ReportDialog/SelectReportOptionView.tsx
@@ -25,9 +25,12 @@ import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from
import {Text} from '#/components/Typography'
import {ReportDialogProps} from './types'
+type ParamsWithMessages = ReportDialogProps['params'] | {type: 'message'}
+
export function SelectReportOptionView({
...props
-}: ReportDialogProps & {
+}: {
+ params: ParamsWithMessages
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
onSelectReportOption: (reportOption: ReportOption) => void
goBack: () => void
@@ -54,6 +57,9 @@ export function SelectReportOptionView({
} else if (props.params.type === 'feedgen') {
title = _(msg`Report this feed`)
description = _(msg`Why should this feed be reviewed?`)
+ } else if (props.params.type === 'message') {
+ title = _(msg`Report this message`)
+ description = _(msg`Why should this message be reviewed?`)
}
return {
diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx
index 8c8e7ed486..68d8150747 100644
--- a/src/components/dms/ConvoMenu.tsx
+++ b/src/components/dms/ConvoMenu.tsx
@@ -50,6 +50,7 @@ let ConvoMenu = ({
const {_} = useLingui()
const t = useTheme()
const leaveConvoControl = Prompt.usePromptControl()
+ const reportControl = Prompt.usePromptControl()
const {mutate: markAsRead} = useMarkAsReadMutation()
const {data: convo} = useConvoQuery(initialConvo)
@@ -147,7 +148,7 @@ let ConvoMenu = ({
- {/* TODO(samuel): implement these */}
+ {/* TODO(samuel): implement this */}
{}}
- disabled>
+ label={_(msg`Report conversation`)}
+ onPress={reportControl.open}>
- Report account
+ Report conversation
@@ -194,9 +194,21 @@ let ConvoMenu = ({
confirmButtonColor="negative"
onConfirm={() => leaveConvo()}
/>
+
+
>
)
}
ConvoMenu = React.memo(ConvoMenu)
export {ConvoMenu}
+
+function noop() {}
diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx
index faf0c88cd8..e162e40ee8 100644
--- a/src/components/dms/MessageItem.tsx
+++ b/src/components/dms/MessageItem.tsx
@@ -193,6 +193,7 @@ let MessageItemMetadata = ({
}
MessageItemMetadata = React.memo(MessageItemMetadata)
+export {MessageItemMetadata}
function localDateString(date: Date) {
// can't use toISOString because it should be in local time
diff --git a/src/components/dms/MessageMenu.tsx b/src/components/dms/MessageMenu.tsx
index 75807f8187..c6abd51106 100644
--- a/src/components/dms/MessageMenu.tsx
+++ b/src/components/dms/MessageMenu.tsx
@@ -1,10 +1,12 @@
import React from 'react'
import {LayoutAnimation, Pressable, View} from 'react-native'
import * as Clipboard from 'expo-clipboard'
+import {RichText} from '@atproto/api'
import {ChatBskyConvoDefs} from '@atproto-labs/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {isWeb} from 'platform/detection'
import {useConvo} from 'state/messages/convo'
import {ConvoStatus} from 'state/messages/convo/types'
@@ -18,6 +20,7 @@ import * as Menu from '#/components/Menu'
import * as Prompt from '#/components/Prompt'
import {usePromptControl} from '#/components/Prompt'
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '../icons/Clipboard'
+import {MessageReportDialog} from './MessageReportDialog'
export let MessageMenu = ({
message,
@@ -35,16 +38,22 @@ export let MessageMenu = ({
const convo = useConvo()
const deleteControl = usePromptControl()
const retryDeleteControl = usePromptControl()
+ const reportControl = usePromptControl()
const isFromSelf = message.sender?.did === currentAccount?.did
const onCopyPostText = React.useCallback(() => {
- // use when we have rich text
- // const str = richTextToString(richText, true)
+ const str = richTextToString(
+ new RichText({
+ text: message.text,
+ facets: message.facets,
+ }),
+ true,
+ )
- Clipboard.setStringAsync(message.text)
+ Clipboard.setStringAsync(str)
Toast.show(_(msg`Copied to clipboard`))
- }, [_, message.text])
+ }, [_, message.text, message.facets])
const onDelete = React.useCallback(() => {
if (convo.status !== ConvoStatus.Ready) return
@@ -56,10 +65,6 @@ export let MessageMenu = ({
.catch(() => retryDeleteControl.open())
}, [_, convo, message.id, retryDeleteControl])
- const onReport = React.useCallback(() => {
- // TODO report the message
- }, [])
-
return (
<>
@@ -104,7 +109,7 @@ export let MessageMenu = ({
+ onPress={reportControl.open}>
{_(msg`Report`)}
@@ -113,6 +118,8 @@ export let MessageMenu = ({
+
+
{
+ const {_} = useLingui()
+ return (
+
+
+
+
+
+
+
+ )
+}
+MessageReportDialog = memo(MessageReportDialog)
+export {MessageReportDialog}
+
+function DialogInner({message}: {message: ChatBskyConvoDefs.MessageView}) {
+ const [reportOption, setReportOption] = useState(null)
+
+ return reportOption ? (
+ setReportOption(null)}
+ />
+ ) : (
+
+ )
+}
+
+function ReasonStep({
+ setReportOption,
+}: {
+ setReportOption: (reportOption: ReportOption) => void
+}) {
+ const control = Dialog.useDialogContext()
+
+ return (
+
+ )
+}
+
+function SubmitStep({
+ message,
+ reportOption,
+ goBack,
+}: {
+ message: ChatBskyConvoDefs.MessageView
+ 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 () => {
+ const report = {
+ reasonType: reportOption.reason,
+ subject: {
+ $type: 'chat.bsky.convo.defs#messageRef',
+ messageId: message.id,
+ did: message.sender!.did,
+ } satisfies ChatBskyConvoDefs.MessageRef,
+ reason: details,
+ } satisfies ComAtprotoModerationCreateReport.InputSchema
+
+ await getAgent().createModerationReport(report)
+ },
+ onSuccess: () => {
+ control.close(() => {
+ Toast.show(_(msg`Thank you. Your report has been sent.`))
+ })
+ },
+ })
+
+ return (
+
+
+
+
+
+ Report this message
+
+
+
+ Your report will be sent to the Bluesky Moderation Service
+
+
+
+
+
+
+
+ Reason: {reportOption.title}
+
+
+
+
+
+
+ Optionally provide additional information below:
+
+
+
+
+
+
+
+
+
+
+
+
+ {error && (
+
+
+ There was an issue sending your report. Please check your internet
+ connection.
+
+
+ )}
+
+
+
+
+ )
+}
+
+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 (
+
+
+
+
+
+
+ )
+}
diff --git a/src/lib/moderation/useReportOptions.ts b/src/lib/moderation/useReportOptions.ts
index a22386b991..c96f302a63 100644
--- a/src/lib/moderation/useReportOptions.ts
+++ b/src/lib/moderation/useReportOptions.ts
@@ -15,6 +15,7 @@ interface ReportOptions {
list: ReportOption[]
feedgen: ReportOption[]
other: ReportOption[]
+ message: ReportOption[]
}
export function useReportOptions(): ReportOptions {
@@ -72,6 +73,19 @@ export function useReportOptions(): ReportOptions {
},
...common,
],
+ message: [
+ {
+ reason: ComAtprotoModerationDefs.REASONSPAM,
+ title: _(msg`Spam`),
+ description: _(msg`Excessive or unwanted messages`),
+ },
+ {
+ reason: ComAtprotoModerationDefs.REASONSEXUAL,
+ title: _(msg`Unwanted Sexual Content`),
+ description: _(msg`Unwanted sexual content`),
+ },
+ ...common,
+ ],
list: [
{
reason: ComAtprotoModerationDefs.REASONVIOLATION,