[🐴] Report message dialog (#3941)
* message report dialog * report chat prompt * typo * 100% height sheet on android * messages-specific report options * restore unwanted sexual content * chat -> conversationzio/stable
parent
7370bebf07
commit
ab21aafc28
|
@ -25,9 +25,12 @@ import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {ReportDialogProps} from './types'
|
import {ReportDialogProps} from './types'
|
||||||
|
|
||||||
|
type ParamsWithMessages = ReportDialogProps['params'] | {type: 'message'}
|
||||||
|
|
||||||
export function SelectReportOptionView({
|
export function SelectReportOptionView({
|
||||||
...props
|
...props
|
||||||
}: ReportDialogProps & {
|
}: {
|
||||||
|
params: ParamsWithMessages
|
||||||
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
|
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
|
||||||
onSelectReportOption: (reportOption: ReportOption) => void
|
onSelectReportOption: (reportOption: ReportOption) => void
|
||||||
goBack: () => void
|
goBack: () => void
|
||||||
|
@ -54,6 +57,9 @@ export function SelectReportOptionView({
|
||||||
} else if (props.params.type === 'feedgen') {
|
} else if (props.params.type === 'feedgen') {
|
||||||
title = _(msg`Report this feed`)
|
title = _(msg`Report this feed`)
|
||||||
description = _(msg`Why should this feed be reviewed?`)
|
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 {
|
return {
|
||||||
|
|
|
@ -50,6 +50,7 @@ let ConvoMenu = ({
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const leaveConvoControl = Prompt.usePromptControl()
|
const leaveConvoControl = Prompt.usePromptControl()
|
||||||
|
const reportControl = Prompt.usePromptControl()
|
||||||
const {mutate: markAsRead} = useMarkAsReadMutation()
|
const {mutate: markAsRead} = useMarkAsReadMutation()
|
||||||
|
|
||||||
const {data: convo} = useConvoQuery(initialConvo)
|
const {data: convo} = useConvoQuery(initialConvo)
|
||||||
|
@ -147,7 +148,7 @@ let ConvoMenu = ({
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Group>
|
</Menu.Group>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
{/* TODO(samuel): implement these */}
|
{/* TODO(samuel): implement this */}
|
||||||
<Menu.Group>
|
<Menu.Group>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
label={_(msg`Block account`)}
|
label={_(msg`Block account`)}
|
||||||
|
@ -161,11 +162,10 @@ let ConvoMenu = ({
|
||||||
/>
|
/>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
label={_(msg`Report account`)}
|
label={_(msg`Report conversation`)}
|
||||||
onPress={() => {}}
|
onPress={reportControl.open}>
|
||||||
disabled>
|
|
||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
<Trans>Report account</Trans>
|
<Trans>Report conversation</Trans>
|
||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={Flag} />
|
<Menu.ItemIcon icon={Flag} />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
@ -194,9 +194,21 @@ let ConvoMenu = ({
|
||||||
confirmButtonColor="negative"
|
confirmButtonColor="negative"
|
||||||
onConfirm={() => leaveConvo()}
|
onConfirm={() => leaveConvo()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Prompt.Basic
|
||||||
|
control={reportControl}
|
||||||
|
title={_(msg`Report conversation`)}
|
||||||
|
description={_(
|
||||||
|
msg`To report a conversation, please report one of its messages via the conversation screen. This lets our moderators understand the context of your issue.`,
|
||||||
|
)}
|
||||||
|
confirmButtonCta={_(msg`I understand`)}
|
||||||
|
onConfirm={noop}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ConvoMenu = React.memo(ConvoMenu)
|
ConvoMenu = React.memo(ConvoMenu)
|
||||||
|
|
||||||
export {ConvoMenu}
|
export {ConvoMenu}
|
||||||
|
|
||||||
|
function noop() {}
|
||||||
|
|
|
@ -193,6 +193,7 @@ let MessageItemMetadata = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageItemMetadata = React.memo(MessageItemMetadata)
|
MessageItemMetadata = React.memo(MessageItemMetadata)
|
||||||
|
export {MessageItemMetadata}
|
||||||
|
|
||||||
function localDateString(date: Date) {
|
function localDateString(date: Date) {
|
||||||
// can't use toISOString because it should be in local time
|
// can't use toISOString because it should be in local time
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {LayoutAnimation, Pressable, View} from 'react-native'
|
import {LayoutAnimation, Pressable, View} from 'react-native'
|
||||||
import * as Clipboard from 'expo-clipboard'
|
import * as Clipboard from 'expo-clipboard'
|
||||||
|
import {RichText} from '@atproto/api'
|
||||||
import {ChatBskyConvoDefs} from '@atproto-labs/api'
|
import {ChatBskyConvoDefs} from '@atproto-labs/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
import {useConvo} from 'state/messages/convo'
|
import {useConvo} from 'state/messages/convo'
|
||||||
import {ConvoStatus} from 'state/messages/convo/types'
|
import {ConvoStatus} from 'state/messages/convo/types'
|
||||||
|
@ -18,6 +20,7 @@ import * as Menu from '#/components/Menu'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {usePromptControl} from '#/components/Prompt'
|
import {usePromptControl} from '#/components/Prompt'
|
||||||
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '../icons/Clipboard'
|
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '../icons/Clipboard'
|
||||||
|
import {MessageReportDialog} from './MessageReportDialog'
|
||||||
|
|
||||||
export let MessageMenu = ({
|
export let MessageMenu = ({
|
||||||
message,
|
message,
|
||||||
|
@ -35,16 +38,22 @@ export let MessageMenu = ({
|
||||||
const convo = useConvo()
|
const convo = useConvo()
|
||||||
const deleteControl = usePromptControl()
|
const deleteControl = usePromptControl()
|
||||||
const retryDeleteControl = usePromptControl()
|
const retryDeleteControl = usePromptControl()
|
||||||
|
const reportControl = usePromptControl()
|
||||||
|
|
||||||
const isFromSelf = message.sender?.did === currentAccount?.did
|
const isFromSelf = message.sender?.did === currentAccount?.did
|
||||||
|
|
||||||
const onCopyPostText = React.useCallback(() => {
|
const onCopyPostText = React.useCallback(() => {
|
||||||
// use when we have rich text
|
const str = richTextToString(
|
||||||
// const str = richTextToString(richText, true)
|
new RichText({
|
||||||
|
text: message.text,
|
||||||
|
facets: message.facets,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
Clipboard.setStringAsync(message.text)
|
Clipboard.setStringAsync(str)
|
||||||
Toast.show(_(msg`Copied to clipboard`))
|
Toast.show(_(msg`Copied to clipboard`))
|
||||||
}, [_, message.text])
|
}, [_, message.text, message.facets])
|
||||||
|
|
||||||
const onDelete = React.useCallback(() => {
|
const onDelete = React.useCallback(() => {
|
||||||
if (convo.status !== ConvoStatus.Ready) return
|
if (convo.status !== ConvoStatus.Ready) return
|
||||||
|
@ -56,10 +65,6 @@ export let MessageMenu = ({
|
||||||
.catch(() => retryDeleteControl.open())
|
.catch(() => retryDeleteControl.open())
|
||||||
}, [_, convo, message.id, retryDeleteControl])
|
}, [_, convo, message.id, retryDeleteControl])
|
||||||
|
|
||||||
const onReport = React.useCallback(() => {
|
|
||||||
// TODO report the message
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Menu.Root control={control}>
|
<Menu.Root control={control}>
|
||||||
|
@ -104,7 +109,7 @@ export let MessageMenu = ({
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="messageDropdownReportBtn"
|
testID="messageDropdownReportBtn"
|
||||||
label={_(msg`Report message`)}
|
label={_(msg`Report message`)}
|
||||||
onPress={onReport}>
|
onPress={reportControl.open}>
|
||||||
<Menu.ItemText>{_(msg`Report`)}</Menu.ItemText>
|
<Menu.ItemText>{_(msg`Report`)}</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={Warning} position="right" />
|
<Menu.ItemIcon icon={Warning} position="right" />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
@ -113,6 +118,8 @@ export let MessageMenu = ({
|
||||||
</Menu.Outer>
|
</Menu.Outer>
|
||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
|
|
||||||
|
<MessageReportDialog message={message} control={reportControl} />
|
||||||
|
|
||||||
<Prompt.Basic
|
<Prompt.Basic
|
||||||
control={deleteControl}
|
control={deleteControl}
|
||||||
title={_(msg`Delete message`)}
|
title={_(msg`Delete message`)}
|
||||||
|
|
|
@ -0,0 +1,254 @@
|
||||||
|
import React, {memo, useMemo, useState} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {RichText as RichTextAPI} from '@atproto/api'
|
||||||
|
import {
|
||||||
|
ChatBskyConvoDefs,
|
||||||
|
ComAtprotoModerationCreateReport,
|
||||||
|
} from '@atproto-labs/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'
|
||||||
|
|
||||||
|
let MessageReportDialog = ({
|
||||||
|
control,
|
||||||
|
message,
|
||||||
|
}: {
|
||||||
|
control: Dialog.DialogControlProps
|
||||||
|
message: ChatBskyConvoDefs.MessageView
|
||||||
|
}): React.ReactNode => {
|
||||||
|
const {_} = useLingui()
|
||||||
|
return (
|
||||||
|
<Dialog.Outer
|
||||||
|
control={control}
|
||||||
|
nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
<Dialog.ScrollableInner label={_(msg`Report this message`)}>
|
||||||
|
<DialogInner message={message} />
|
||||||
|
<Dialog.Close />
|
||||||
|
</Dialog.ScrollableInner>
|
||||||
|
</Dialog.Outer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
MessageReportDialog = memo(MessageReportDialog)
|
||||||
|
export {MessageReportDialog}
|
||||||
|
|
||||||
|
function DialogInner({message}: {message: ChatBskyConvoDefs.MessageView}) {
|
||||||
|
const [reportOption, setReportOption] = useState<ReportOption | null>(null)
|
||||||
|
|
||||||
|
return reportOption ? (
|
||||||
|
<SubmitStep
|
||||||
|
message={message}
|
||||||
|
reportOption={reportOption}
|
||||||
|
goBack={() => setReportOption(null)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ReasonStep setReportOption={setReportOption} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReasonStep({
|
||||||
|
setReportOption,
|
||||||
|
}: {
|
||||||
|
setReportOption: (reportOption: ReportOption) => void
|
||||||
|
}) {
|
||||||
|
const control = Dialog.useDialogContext()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectReportOptionView
|
||||||
|
labelers={[]}
|
||||||
|
goBack={control.close}
|
||||||
|
params={{type: 'message'}}
|
||||||
|
onSelectReportOption={setReportOption}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<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]}>
|
||||||
|
<Trans>Report this message</Trans>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<PreviewMessage message={message} />
|
||||||
|
|
||||||
|
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Reason: {reportOption.title}</Trans>
|
||||||
|
</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
|
||||||
|
message={message}
|
||||||
|
isLastInGroup
|
||||||
|
style={[a.text_left, a.mb_0]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ interface ReportOptions {
|
||||||
list: ReportOption[]
|
list: ReportOption[]
|
||||||
feedgen: ReportOption[]
|
feedgen: ReportOption[]
|
||||||
other: ReportOption[]
|
other: ReportOption[]
|
||||||
|
message: ReportOption[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useReportOptions(): ReportOptions {
|
export function useReportOptions(): ReportOptions {
|
||||||
|
@ -72,6 +73,19 @@ export function useReportOptions(): ReportOptions {
|
||||||
},
|
},
|
||||||
...common,
|
...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: [
|
list: [
|
||||||
{
|
{
|
||||||
reason: ComAtprotoModerationDefs.REASONVIOLATION,
|
reason: ComAtprotoModerationDefs.REASONVIOLATION,
|
||||||
|
|
Loading…
Reference in New Issue