[Reporting] Integrate new ReportDialog into DMs (#9213)

* Update types, fork copy for convo vs message

* Integrate in remaining spots

* Replace DoneStep with separate dialog that opens after reporting

* Handle profile error state by just showing a success message

* Delete old dms ReportDialog

* Chats only go to bsky

* Only open block or delete dialog if submitted successfully

* make close button always large

* prevent expansion of blockordelete dialog

* Delete old report dialog (#9214)e

* Skip labeler selection for Bluesky-only cases

* Rename AfterReportDialog

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
Eric Bailey
2025-11-03 10:46:15 -06:00
parent c87d53c34f
commit f43950929a
16 changed files with 436 additions and 1227 deletions
@@ -1,85 +0,0 @@
import {View} from 'react-native'
import {type AppBskyLabelerDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, useButtonContext} from '#/components/Button'
import {Divider} from '#/components/Divider'
import * as LabelingServiceCard from '#/components/LabelingServiceCard'
import {Text} from '#/components/Typography'
import {type ReportDialogProps} from './types'
export function SelectLabelerView({
...props
}: ReportDialogProps & {
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
onSelectLabeler: (v: string) => void
}) {
const t = useTheme()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
return (
<View style={[a.gap_lg]}>
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
<Text style={[a.text_2xl, a.font_semi_bold]}>
<Trans>Select moderator</Trans>
</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
<Trans>To whom would you like to send this report?</Trans>
</Text>
</View>
<Divider />
<View style={[a.gap_sm]}>
{props.labelers.map(labeler => {
return (
<Button
key={labeler.creator.did}
label={_(msg`Send report to ${labeler.creator.displayName}`)}
onPress={() => props.onSelectLabeler(labeler.creator.did)}>
<LabelerButton labeler={labeler} />
</Button>
)
})}
</View>
</View>
)
}
function LabelerButton({
labeler,
}: {
labeler: AppBskyLabelerDefs.LabelerViewDetailed
}) {
const t = useTheme()
const {hovered, pressed} = useButtonContext()
const interacted = hovered || pressed
return (
<LabelingServiceCard.Outer
style={[
a.p_md,
a.rounded_sm,
t.atoms.bg_contrast_25,
interacted && t.atoms.bg_contrast_50,
]}>
<LabelingServiceCard.Avatar avatar={labeler.creator.avatar} />
<LabelingServiceCard.Content>
<LabelingServiceCard.Title
value={getLabelingServiceTitle({
displayName: labeler.creator.displayName,
handle: labeler.creator.handle,
})}
/>
<Text
style={[t.atoms.text_contrast_medium, a.text_sm, a.font_semi_bold]}>
@{labeler.creator.handle}
</Text>
</LabelingServiceCard.Content>
</LabelingServiceCard.Outer>
)
}
@@ -1,195 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {type AppBskyLabelerDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
type ReportOption,
useReportOptions,
} from '#/lib/moderation/useReportOptions'
import {Link} from '#/components/Link'
import {DMCA_LINK} from '#/components/ReportDialog/const'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {
Button,
ButtonIcon,
ButtonText,
useButtonContext,
} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {
ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft,
ChevronRight_Stroke2_Corner0_Rounded as ChevronRight,
} from '#/components/icons/Chevron'
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight'
import {Text} from '#/components/Typography'
import {type ReportDialogProps} from './types'
export function SelectReportOptionView(props: {
params: ReportDialogProps['params']
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
onSelectReportOption: (reportOption: ReportOption) => void
goBack: () => void
}) {
const t = useTheme()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const allReportOptions = useReportOptions()
const reportOptions = allReportOptions[props.params.type]
const i18n = React.useMemo(() => {
let title = _(msg`Report this content`)
let description = _(msg`Why should this content be reviewed?`)
if (props.params.type === 'account') {
title = _(msg`Report this user`)
description = _(msg`Why should this user be reviewed?`)
} else if (props.params.type === 'post') {
title = _(msg`Report this post`)
description = _(msg`Why should this post be reviewed?`)
} else if (props.params.type === 'list') {
title = _(msg`Report this list`)
description = _(msg`Why should this list be reviewed?`)
} else if (props.params.type === 'feedgen') {
title = _(msg`Report this feed`)
description = _(msg`Why should this feed be reviewed?`)
} else if (props.params.type === 'starterpack') {
title = _(msg`Report this starter pack`)
description = _(msg`Why should this starter pack be reviewed?`)
} else if (props.params.type === 'convoMessage') {
title = _(msg`Report this message`)
description = _(msg`Why should this message be reviewed?`)
}
return {
title,
description,
}
}, [_, props.params.type])
return (
<View style={[a.gap_lg]}>
{props.labelers?.length > 1 ? (
<Button
size="small"
variant="solid"
color="secondary"
shape="round"
label={_(msg`Go back to previous step`)}
onPress={props.goBack}>
<ButtonIcon icon={ChevronLeft} />
</Button>
) : null}
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
<Text style={[a.text_2xl, a.font_semi_bold]}>{i18n.title}</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
{i18n.description}
</Text>
</View>
<Divider />
<View style={[a.gap_sm]}>
{reportOptions.map(reportOption => {
return (
<Button
key={reportOption.reason}
testID={reportOption.reason}
label={_(msg`Create report for ${reportOption.title}`)}
onPress={() => props.onSelectReportOption(reportOption)}>
<ReportOptionButton
title={reportOption.title}
description={reportOption.description}
/>
</Button>
)
})}
{(props.params.type === 'post' || props.params.type === 'account') && (
<View
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.p_md,
a.pl_lg,
a.rounded_md,
t.atoms.bg_contrast_900,
]}>
<Text
style={[
a.flex_1,
t.atoms.text_inverted,
a.italic,
a.leading_snug,
]}>
<Trans>Need to report a copyright violation?</Trans>
</Text>
<Link
to={DMCA_LINK}
label={_(msg`View details for reporting a copyright violation`)}
size="small"
variant="solid"
color="secondary">
<ButtonText>
<Trans>View details</Trans>
</ButtonText>
<ButtonIcon position="right" icon={SquareArrowTopRight} />
</Link>
</View>
)}
</View>
</View>
)
}
function ReportOptionButton({
title,
description,
}: {
title: string
description: string
}) {
const t = useTheme()
const {hovered, pressed} = useButtonContext()
const interacted = hovered || pressed
return (
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.justify_between,
a.p_md,
a.rounded_md,
{paddingRight: 70},
t.atoms.bg_contrast_25,
interacted && t.atoms.bg_contrast_50,
]}>
<View style={[a.flex_1, a.gap_xs]}>
<Text
style={[a.text_md, a.font_semi_bold, t.atoms.text_contrast_medium]}>
{title}
</Text>
<Text style={[a.leading_tight, {maxWidth: 400}]}>{description}</Text>
</View>
<View
style={[
a.absolute,
a.inset_0,
a.justify_center,
a.pr_md,
{left: 'auto'},
]}>
<ChevronRight size="md" fill={t.atoms.text_contrast_low.color} />
</View>
</View>
)
}
-274
View File
@@ -1,274 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {type AppBskyLabelerDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {type 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, native, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as Toggle from '#/components/forms/Toggle'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {type ReportDialogProps} from './types'
export function SubmitView({
params,
labelers,
selectedLabeler,
selectedReportOption,
goBack,
onSubmitComplete,
}: ReportDialogProps & {
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
selectedLabeler: string
selectedReportOption: ReportOption
goBack: () => void
onSubmitComplete: () => void
}) {
const t = useTheme()
const {_} = useLingui()
const agent = useAgent()
const [details, setDetails] = React.useState<string>('')
const [submitting, setSubmitting] = React.useState<boolean>(false)
const [selectedServices, setSelectedServices] = React.useState<string[]>([
selectedLabeler,
])
const [error, setError] = React.useState('')
const submit = React.useCallback(async () => {
setSubmitting(true)
setError('')
const $type =
params.type === 'account'
? 'com.atproto.admin.defs#repoRef'
: 'com.atproto.repo.strongRef'
const report = {
reasonType: selectedReportOption.reason,
subject: {
$type,
...params,
},
reason: details,
}
const results = await Promise.all(
selectedServices.map(did => {
return agent
.createModerationReport(report, {
encoding: 'application/json',
headers: {
'atproto-proxy': `${did}#atproto_labeler`,
},
})
.then(
_ => true,
_ => false,
)
}),
)
setSubmitting(false)
if (results.includes(true)) {
Toast.show(_(msg`Thank you. Your report has been sent.`))
onSubmitComplete()
} else {
setError(
_(
msg`There was an issue sending your report. Please check your internet connection.`,
),
)
}
}, [
_,
params,
details,
selectedReportOption,
selectedServices,
onSubmitComplete,
setError,
agent,
])
return (
<View style={[a.gap_2xl]}>
<Button
size="small"
variant="solid"
color="secondary"
shape="round"
label={_(msg`Go back to previous step`)}
onPress={goBack}>
<ButtonIcon icon={ChevronLeft} />
</Button>
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.p_md,
a.rounded_md,
a.border,
t.atoms.border_contrast_low,
]}>
<View style={[a.flex_1, a.gap_xs]}>
<Text style={[a.text_md, a.font_semi_bold]}>
{selectedReportOption.title}
</Text>
<Text style={[a.leading_tight, {maxWidth: 400}]}>
{selectedReportOption.description}
</Text>
</View>
<Check size="md" style={[a.pr_sm, t.atoms.text_contrast_low]} />
</View>
<View style={[a.gap_md]}>
<Text style={[t.atoms.text_contrast_medium]}>
<Trans>Select the moderation service(s) to report to</Trans>
</Text>
<Toggle.Group
label="Select mod services"
values={selectedServices}
onChange={setSelectedServices}>
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
{labelers.map(labeler => {
const title = getLabelingServiceTitle({
displayName: labeler.creator.displayName,
handle: labeler.creator.handle,
})
return (
<Toggle.Item
key={labeler.creator.did}
name={labeler.creator.did}
label={title}>
<LabelerToggle title={title} />
</Toggle.Item>
)
})}
</View>
</Toggle.Group>
</View>
<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]}>
{!selectedServices.length ||
(error && (
<Text
style={[
a.flex_1,
a.italic,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
{error ? (
error
) : (
<Trans>You must select at least one labeler for a report</Trans>
)}
</Text>
))}
<Button
testID="sendReportBtn"
size="large"
variant="solid"
color="negative"
label={_(msg`Send report`)}
onPress={submit}
disabled={!selectedServices.length}>
<ButtonText>
<Trans>Send report</Trans>
</ButtonText>
<ButtonIcon icon={submitting ? Loader : SendIcon} />
</Button>
</View>
{/* Maybe fix this later -h */}
{isAndroid ? <View style={{height: 300}} /> : null}
</View>
)
}
function LabelerToggle({title}: {title: string}) {
const t = useTheme()
const ctx = Toggle.useItemContext()
return (
<View
style={[
a.flex_row,
a.align_center,
a.gap_md,
a.p_md,
a.pr_lg,
a.rounded_sm,
a.overflow_hidden,
t.atoms.bg_contrast_25,
ctx.selected && [t.atoms.bg_contrast_50],
]}>
<Toggle.Checkbox />
<View
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.z_10,
]}>
<Text
emoji
style={[
native({marginTop: 2}),
t.atoms.text_contrast_medium,
ctx.selected && t.atoms.text,
]}>
{title}
</Text>
</View>
</View>
)
}
-1
View File
@@ -1 +0,0 @@
export const DMCA_LINK = 'https://bsky.social/about/support/copyright'
-97
View File
@@ -1,97 +0,0 @@
import React from 'react'
import {Pressable, View} from 'react-native'
import {type ScrollView} from 'react-native-gesture-handler'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {type ReportOption} from '#/lib/moderation/useReportOptions'
import {useMyLabelersQuery} from '#/state/queries/preferences'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {type AppBskyLabelerDefs} from '@atproto/api'
import {atoms as a} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {SelectLabelerView} from './SelectLabelerView'
import {SelectReportOptionView} from './SelectReportOptionView'
import {SubmitView} from './SubmitView'
import {type ReportDialogProps} from './types'
export function ReportDialog(props: ReportDialogProps) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<ReportDialogInner {...props} />
</Dialog.Outer>
)
}
function ReportDialogInner(props: ReportDialogProps) {
const {_} = useLingui()
const {
isLoading: isLabelerLoading,
data: labelers,
error,
} = useMyLabelersQuery({excludeNonConfigurableLabelers: true})
const isLoading = useDelayedLoading(500, isLabelerLoading)
const ref = React.useRef<ScrollView>(null)
return (
<Dialog.ScrollableInner label={_(msg`Report dialog`)} ref={ref}>
{isLoading ? (
<View style={[a.align_center, {height: 100}]}>
<Loader size="xl" />
{/* Here to capture focus for a hot sec to prevent flash */}
<Pressable accessible={false} />
</View>
) : error || !labelers ? (
<View>
<Text style={[a.text_md]}>
<Trans>Something went wrong, please try again.</Trans>
</Text>
</View>
) : (
<ReportDialogLoaded labelers={labelers} {...props} />
)}
</Dialog.ScrollableInner>
)
}
function ReportDialogLoaded(
props: ReportDialogProps & {
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
},
) {
const [selectedLabeler, setSelectedLabeler] = React.useState<
string | undefined
>(props.labelers.length === 1 ? props.labelers[0].creator.did : undefined)
const [selectedReportOption, setSelectedReportOption] = React.useState<
ReportOption | undefined
>()
if (selectedReportOption && selectedLabeler) {
return (
<SubmitView
{...props}
selectedLabeler={selectedLabeler}
selectedReportOption={selectedReportOption}
goBack={() => setSelectedReportOption(undefined)}
onSubmitComplete={() => props.control.close()}
/>
)
}
if (selectedLabeler) {
return (
<SelectReportOptionView
{...props}
goBack={() => setSelectedLabeler(undefined)}
onSelectReportOption={setSelectedReportOption}
/>
)
}
return <SelectLabelerView {...props} onSelectLabeler={setSelectedLabeler} />
}
-16
View File
@@ -1,16 +0,0 @@
import type * as Dialog from '#/components/Dialog'
export type ReportDialogProps = {
control: Dialog.DialogOuterProps['control']
params:
| {
type: 'post' | 'list' | 'feedgen' | 'starterpack' | 'other'
uri: string
cid: string
}
| {
type: 'account'
did: string
}
| {type: 'convoMessage'}
}
+219
View File
@@ -0,0 +1,219 @@
import {memo, useState} from 'react'
import {View} from 'react-native'
import {type AppBskyActorDefs, type ChatBskyConvoDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {StackActions, useNavigation} from '@react-navigation/native'
import type React from 'react'
import {type NavigationProp} from '#/lib/routes/types'
import {isNative} from '#/platform/detection'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
import {
useProfileBlockMutationQueue,
useProfileQuery,
} from '#/state/queries/profile'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as Toggle from '#/components/forms/Toggle'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
type ReportDialogParams = {
convoId: string
message: ChatBskyConvoDefs.MessageView
}
/**
* Dialog shown after a report is submitted, allowing the user to block the
* reporter and/or leave the conversation.
*/
export const AfterReportDialog = memo(function BlockOrDeleteDialogInner({
control,
params,
currentScreen,
}: {
control: Dialog.DialogControlProps
params: ReportDialogParams
currentScreen: 'list' | 'conversation'
}): React.ReactNode {
const {_} = useLingui()
return (
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
<Dialog.Handle />
<Dialog.ScrollableInner
label={_(
msg`Would you like to block this account or delete this conversation?`,
)}
style={[web({maxWidth: 400})]}>
<DialogInner params={params} currentScreen={currentScreen} />
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
})
function DialogInner({
params,
currentScreen,
}: {
params: ReportDialogParams
currentScreen: 'list' | 'conversation'
}) {
const t = useTheme()
const {_} = useLingui()
const control = Dialog.useDialogContext()
const {
data: profile,
isLoading,
isError,
} = useProfileQuery({
did: params.message.sender.did,
})
return isLoading ? (
<View style={[a.w_full, a.py_5xl, a.align_center]}>
<Loader size="lg" />
</View>
) : isError || !profile ? (
<View style={[a.w_full, a.gap_lg]}>
<View style={[a.justify_center, a.gap_sm]}>
<Text style={[a.text_2xl, a.font_semi_bold]}>
<Trans>Report submitted</Trans>
</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
<Trans>Our moderation team has received your report.</Trans>
</Text>
</View>
<Button
label={_(msg`Close`)}
onPress={() => control.close()}
size={platform({native: 'small', web: 'large'})}
color="secondary">
<ButtonText>
<Trans>Close</Trans>
</ButtonText>
</Button>
</View>
) : (
<DoneStep
convoId={params.convoId}
currentScreen={currentScreen}
profile={profile}
/>
)
}
function DoneStep({
convoId,
currentScreen,
profile,
}: {
convoId: string
currentScreen: 'list' | 'conversation'
profile: AppBskyActorDefs.ProfileViewDetailed
}) {
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const control = Dialog.useDialogContext()
const {gtMobile} = useBreakpoints()
const t = useTheme()
const [actions, setActions] = useState<string[]>(['block', 'leave'])
const shadow = useProfileShadow(profile)
const [queueBlock] = useProfileBlockMutationQueue(shadow)
const {mutate: leaveConvo} = useLeaveConvo(convoId, {
onMutate: () => {
if (currentScreen === 'conversation') {
navigation.dispatch(
StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}),
)
}
},
onError: () => {
Toast.show(_(msg`Could not leave chat`), 'xmark')
},
})
let btnText = _(msg`Done`)
let toastMsg: string | undefined
if (actions.includes('leave') && actions.includes('block')) {
btnText = _(msg`Block and Delete`)
toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'}))
} else if (actions.includes('leave')) {
btnText = _(msg`Delete Conversation`)
toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'}))
} else if (actions.includes('block')) {
btnText = _(msg`Block User`)
toastMsg = _(msg({message: 'User blocked', context: 'toast'}))
}
const onPressPrimaryAction = () => {
control.close(() => {
if (actions.includes('block')) {
queueBlock()
}
if (actions.includes('leave')) {
leaveConvo()
}
if (toastMsg) {
Toast.show(toastMsg, 'check')
}
})
}
return (
<View style={a.gap_2xl}>
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
<Text style={[a.text_2xl, a.font_semi_bold]}>
<Trans>Report submitted</Trans>
</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
<Trans>Our moderation team has received your report.</Trans>
</Text>
</View>
<Toggle.Group
label={_(msg`Block and/or delete this conversation`)}
values={actions}
onChange={setActions}>
<View style={[a.gap_md]}>
<Toggle.Item name="block" label={_(msg`Block user`)}>
<Toggle.Checkbox />
<Toggle.LabelText style={[a.text_md]}>
<Trans>Block user</Trans>
</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="leave" label={_(msg`Delete conversation`)}>
<Toggle.Checkbox />
<Toggle.LabelText style={[a.text_md]}>
<Trans>Delete conversation</Trans>
</Toggle.LabelText>
</Toggle.Item>
</View>
</Toggle.Group>
<View style={[a.gap_sm]}>
<Button
label={btnText}
onPress={onPressPrimaryAction}
size="large"
color={actions.length > 0 ? 'negative' : 'primary'}>
<ButtonText>{btnText}</ButtonText>
</Button>
<Button
label={_(msg`Close`)}
onPress={() => control.close()}
size="large"
color="secondary">
<ButtonText>
<Trans>Close</Trans>
</ButtonText>
</Button>
</View>
</View>
)
}
+24 -10
View File
@@ -17,10 +17,10 @@ import * as Toast from '#/view/com/util/Toast'
import {type ViewStyleProp} from '#/alf'
import {atoms as a} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {AfterReportDialog} from '#/components/dms/AfterReportDialog'
import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog'
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
import {ReportDialog} from '#/components/dms/ReportDialog'
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft'
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
@@ -33,6 +33,7 @@ import {
} from '#/components/icons/Person'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
import * as Menu from '#/components/Menu'
import {ReportDialog} from '#/components/moderation/ReportDialog'
import * as Prompt from '#/components/Prompt'
import type * as bsky from '#/types/bsky'
@@ -65,6 +66,7 @@ let ConvoMenu = ({
const leaveConvoControl = Prompt.usePromptControl()
const reportControl = Prompt.usePromptControl()
const blockedByListControl = Prompt.usePromptControl()
const blockOrDeleteControl = Prompt.usePromptControl()
const {listBlocks} = blockInfo
@@ -113,15 +115,27 @@ let ConvoMenu = ({
currentScreen={currentScreen}
/>
{latestReportableMessage ? (
<ReportDialog
currentScreen={currentScreen}
params={{
type: 'convoMessage',
convoId: convo.id,
message: latestReportableMessage,
}}
control={reportControl}
/>
<>
<ReportDialog
subject={{
view: 'convo',
convoId: convo.id,
message: latestReportableMessage,
}}
control={reportControl}
onAfterSubmit={() => {
blockOrDeleteControl.open()
}}
/>
<AfterReportDialog
control={blockOrDeleteControl}
currentScreen={currentScreen}
params={{
convoId: convo.id,
message: latestReportableMessage,
}}
/>
</>
) : (
<ReportConversationPrompt control={reportControl} />
)}
+20 -3
View File
@@ -15,11 +15,12 @@ import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import * as ContextMenu from '#/components/ContextMenu'
import {type TriggerProps} from '#/components/ContextMenu/types'
import {ReportDialog} from '#/components/dms/ReportDialog'
import {AfterReportDialog} from '#/components/dms/AfterReportDialog'
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
import {ReportDialog} from '#/components/moderation/ReportDialog'
import * as Prompt from '#/components/Prompt'
import {usePromptControl} from '#/components/Prompt'
import {EmojiReactionPicker} from './EmojiReactionPicker'
@@ -37,6 +38,7 @@ export let MessageContextMenu = ({
const convo = useConvoActive()
const deleteControl = usePromptControl()
const reportControl = usePromptControl()
const blockOrDeleteControl = usePromptControl()
const langPrefs = useLanguagePrefs()
const translate = useTranslate()
@@ -171,9 +173,24 @@ export let MessageContextMenu = ({
</ContextMenu.Root>
<ReportDialog
currentScreen="conversation"
params={{type: 'convoMessage', convoId: convo.convo.id, message}}
// currentScreen="conversation"
control={reportControl}
subject={{
view: 'message',
convoId: convo.convo.id,
message,
}}
onAfterSubmit={() => {
blockOrDeleteControl.open()
}}
/>
<AfterReportDialog
control={blockOrDeleteControl}
currentScreen="conversation"
params={{
convoId: convo.convo.id,
message,
}}
/>
<Prompt.Basic
-444
View File
@@ -1,444 +0,0 @@
import {memo, useMemo, useState} from 'react'
import {View} from 'react-native'
import {
type $Typed,
type AppBskyActorDefs,
type ChatBskyConvoDefs,
type ComAtprotoModerationCreateReport,
RichText as RichTextAPI,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {StackActions, useNavigation} from '@react-navigation/native'
import {useMutation} from '@tanstack/react-query'
import type React from 'react'
import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants'
import {type ReportOption} from '#/lib/moderation/useReportOptions'
import {type NavigationProp} from '#/lib/routes/types'
import {isNative} from '#/platform/detection'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
import {
useProfileBlockMutationQueue,
useProfileQuery,
} from '#/state/queries/profile'
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, platform, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle'
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane'
import {Loader} from '#/components/Loader'
import {SelectReportOptionView} from '#/components/ReportDialog/SelectReportOptionView'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import {MessageItemMetadata} from './MessageItem'
type ReportDialogParams = {
type: 'convoMessage'
convoId: string
message: ChatBskyConvoDefs.MessageView
}
let ReportDialog = ({
control,
params,
currentScreen,
}: {
control: Dialog.DialogControlProps
params: ReportDialogParams
currentScreen: 'list' | 'conversation'
}): React.ReactNode => {
const {_} = useLingui()
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`Report this message`)}>
<DialogInner params={params} currentScreen={currentScreen} />
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
ReportDialog = memo(ReportDialog)
export {ReportDialog}
function DialogInner({
params,
currentScreen,
}: {
params: ReportDialogParams
currentScreen: 'list' | 'conversation'
}) {
const {data: profile, isError} = useProfileQuery({
did: params.message.sender.did,
})
const [reportOption, setReportOption] = useState<ReportOption | null>(null)
const [done, setDone] = useState(false)
const control = Dialog.useDialogContext()
return done ? (
profile ? (
<DoneStep
convoId={params.convoId}
currentScreen={currentScreen}
profile={profile}
/>
) : (
<View style={[a.w_full, a.py_5xl, a.align_center]}>
<Loader />
</View>
)
) : reportOption ? (
<SubmitStep
params={params}
reportOption={reportOption}
goBack={() => setReportOption(null)}
onComplete={() => {
if (isError) {
control.close()
} else {
setDone(true)
}
}}
/>
) : (
<ReasonStep params={params} setReportOption={setReportOption} />
)
}
function ReasonStep({
setReportOption,
}: {
setReportOption: (reportOption: ReportOption) => void
params: ReportDialogParams
}) {
const control = Dialog.useDialogContext()
return (
<SelectReportOptionView
labelers={[]}
goBack={control.close}
params={{
type: 'convoMessage',
}}
onSelectReportOption={setReportOption}
/>
)
}
function SubmitStep({
params,
reportOption,
goBack,
onComplete,
}: {
params: ReportDialogParams
reportOption: ReportOption
goBack: () => void
onComplete: () => void
}) {
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const t = useTheme()
const [details, setDetails] = useState('')
const agent = useAgent()
const {
mutate: submit,
error,
isPending: submitting,
} = useMutation({
mutationFn: async () => {
if (params.type === 'convoMessage') {
const {convoId, message} = params
const subject: $Typed<ChatBskyConvoDefs.MessageRef> = {
$type: 'chat.bsky.convo.defs#messageRef',
messageId: message.id,
convoId,
did: message.sender.did,
}
const report = {
reasonType: reportOption.reason,
subject,
reason: details,
} satisfies ComAtprotoModerationCreateReport.InputSchema
await agent.createModerationReport(report, {
encoding: 'application/json',
headers: BLUESKY_MOD_SERVICE_HEADERS,
})
}
},
onSuccess: onComplete,
})
const copy = useMemo(() => {
return {
convoMessage: {
title: _(msg`Report this message`),
},
}[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_semi_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_semi_bold, a.text_md, t.atoms.text_contrast_medium]}>
<Trans>Reason:</Trans>
</Text>{' '}
<Text style={[a.font_semi_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
defaultValue={details}
onChangeText={setDetails}
label={_(msg`Text field`)}
style={{paddingRight: 60}}
numberOfLines={5}
/>
<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>
<ButtonIcon icon={submitting ? Loader : SendIcon} />
</Button>
</View>
</View>
)
}
function DoneStep({
convoId,
currentScreen,
profile,
}: {
convoId: string
currentScreen: 'list' | 'conversation'
profile: AppBskyActorDefs.ProfileViewDetailed
}) {
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const control = Dialog.useDialogContext()
const {gtMobile} = useBreakpoints()
const t = useTheme()
const [actions, setActions] = useState<string[]>(['block', 'leave'])
const shadow = useProfileShadow(profile)
const [queueBlock] = useProfileBlockMutationQueue(shadow)
const {mutate: leaveConvo} = useLeaveConvo(convoId, {
onMutate: () => {
if (currentScreen === 'conversation') {
navigation.dispatch(
StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}),
)
}
},
onError: () => {
Toast.show(_(msg`Could not leave chat`), 'xmark')
},
})
let btnText = _(msg`Done`)
let toastMsg: string | undefined
if (actions.includes('leave') && actions.includes('block')) {
btnText = _(msg`Block and Delete`)
toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'}))
} else if (actions.includes('leave')) {
btnText = _(msg`Delete Conversation`)
toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'}))
} else if (actions.includes('block')) {
btnText = _(msg`Block User`)
toastMsg = _(msg({message: 'User blocked', context: 'toast'}))
}
const onPressPrimaryAction = () => {
control.close(() => {
if (actions.includes('block')) {
queueBlock()
}
if (actions.includes('leave')) {
leaveConvo()
}
if (toastMsg) {
Toast.show(toastMsg, 'check')
}
})
}
return (
<View style={a.gap_2xl}>
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
<Text style={[a.text_2xl, a.font_semi_bold]}>
<Trans>Report submitted</Trans>
</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
<Trans>Our moderation team has received your report.</Trans>
</Text>
</View>
<Toggle.Group
label={_(msg`Block and/or delete this conversation`)}
values={actions}
onChange={setActions}>
<View style={[a.gap_md]}>
<Toggle.Item name="block" label={_(msg`Block user`)}>
<Toggle.Checkbox />
<Toggle.LabelText style={[a.text_md]}>
<Trans>Block user</Trans>
</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="leave" label={_(msg`Delete conversation`)}>
<Toggle.Checkbox />
<Toggle.LabelText style={[a.text_md]}>
<Trans>Delete conversation</Trans>
</Toggle.LabelText>
</Toggle.Item>
</View>
</Toggle.Group>
<View style={[a.gap_md, web([a.flex_row_reverse])]}>
<Button
label={btnText}
onPress={onPressPrimaryAction}
size="large"
variant="solid"
color={actions.length > 0 ? 'negative' : 'primary'}>
<ButtonText>{btnText}</ButtonText>
</Button>
<Button
label={_(msg`Close`)}
onPress={() => control.close()}
size={platform({native: 'small', web: 'large'})}
variant={platform({
native: 'ghost',
web: 'solid',
})}
color="secondary">
<ButtonText>
<Trans>Close</Trans>
</ButtonText>
</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]}
interactiveStyle={a.underline}
enableTags
/>
</View>
<MessageItemMetadata
item={{
type: 'message',
message,
key: '',
nextMessage: null,
prevMessage: null,
}}
style={[a.text_left, a.mb_0]}
/>
</View>
)
}
@@ -85,7 +85,7 @@ export function useSubmitReportMutation() {
}
break
}
case 'chatMessage': {
case 'convoMessage': {
report = {
reasonType,
reason: state.details,
+14 -4
View File
@@ -38,10 +38,20 @@ export function useCopyForSubject(subject: ParsedReportSubject) {
subtitle: _(msg`Why should this starter pack be reviewed?`),
}
}
case 'chatMessage': {
return {
title: _(msg`Report this message`),
subtitle: _(msg`Why should this message be reviewed?`),
case 'convoMessage': {
switch (subject.view) {
case 'convo': {
return {
title: _(msg`Report this conversation`),
subtitle: _(msg`Why should this conversation be reviewed?`),
}
}
case 'message': {
return {
title: _(msg`Report this message`),
subtitle: _(msg`Why should this message be reviewed?`),
}
}
}
}
}
+119 -80
View File
@@ -113,6 +113,13 @@ function Inner(props: ReportDialogProps) {
const [isPending, setPending] = React.useState(false)
const [isSuccess, setSuccess] = React.useState(false)
// some reasons ONLY go to Bluesky
const isBskyOnlyReason = state?.selectedOption?.reason
? BSKY_LABELER_ONLY_REPORT_REASONS.has(state.selectedOption.reason)
: false
// some subjects (chats) only go to Bluesky
const isBskyOnlySubject = props.subject.type === 'convoMessage'
/**
* Labelers that support this `subject` and its NSID collection
*/
@@ -124,7 +131,7 @@ function Inner(props: ReportDialogProps) {
if (subjectTypes === undefined) return true
if (props.subject.type === 'account') {
return subjectTypes.includes('account')
} else if (props.subject.type === 'chatMessage') {
} else if (props.subject.type === 'convoMessage') {
return subjectTypes.includes('chat')
} else {
return subjectTypes.includes('record')
@@ -134,13 +141,12 @@ function Inner(props: ReportDialogProps) {
const collections: string[] | undefined = l.subjectCollections
if (collections === undefined) return true
// all chat collections accepted, since only Bluesky handles chats
if (props.subject.type === 'chatMessage') return true
if (props.subject.type === 'convoMessage') return true
return collections.includes(props.subject.nsid)
})
.filter(l => {
if (!state.selectedOption) return false
// some reasons ONLY go to Bluesky
if (BSKY_LABELER_ONLY_REPORT_REASONS.has(state.selectedOption.reason)) {
if (isBskyOnlyReason || isBskyOnlySubject) {
return l.creator.did === BSKY_LABELER_DID
}
const supportedReasonTypes: string[] | undefined = l.reasonTypes
@@ -154,10 +160,25 @@ function Inner(props: ReportDialogProps) {
)
)
})
}, [props, allLabelers, state.selectedOption])
}, [
props,
allLabelers,
state.selectedOption,
isBskyOnlyReason,
isBskyOnlySubject,
])
const hasSupportedLabelers = !!supportedLabelers.length
const hasSingleSupportedLabeler = supportedLabelers.length === 1
/**
* We skip the select labeler step if there's only one possible labeler, and
* that labeler is Bluesky (which is the case for chat reports and certain
* reason types). We'll use this below to adjust the indexing and skip the
* step in the UI.
*/
const isAlwaysBskyLabeler =
hasSingleSupportedLabeler && (isBskyOnlyReason || isBskyOnlySubject)
const onSubmit = React.useCallback(async () => {
dispatch({type: 'clearError'})
@@ -185,7 +206,9 @@ function Inner(props: ReportDialogProps) {
)
// give time for user feedback
setTimeout(() => {
props.control.close()
props.control.close(() => {
props.onAfterSubmit?.()
})
}, 1e3)
} catch (e: any) {
logger.metric('reportDialog:failure', {}, {statsig: false})
@@ -369,88 +392,104 @@ function Inner(props: ReportDialogProps) {
) : null}
</StepOuter>
<StepOuter>
<StepTitle
index={3}
title={_(msg`Select moderation service`)}
activeIndex1={state.activeStepIndex1}
{isAlwaysBskyLabeler ? (
<ActionOnce
check={() => !state.selectedLabeler}
callback={() => {
dispatch({
type: 'selectLabeler',
labeler: supportedLabelers[0],
})
}}
/>
{state.activeStepIndex1 >= 3 && (
<>
{state.selectedLabeler ? (
<>
{hasSingleSupportedLabeler ? (
<LabelerCard labeler={state.selectedLabeler} />
) : (
<View style={[a.flex_row, a.align_center, a.gap_md]}>
<View style={[a.flex_1]}>
<LabelerCard labeler={state.selectedLabeler} />
) : (
<StepOuter>
<StepTitle
index={3}
title={_(msg`Select moderation service`)}
activeIndex1={state.activeStepIndex1}
/>
{state.activeStepIndex1 >= 3 && (
<>
{state.selectedLabeler ? (
<>
{hasSingleSupportedLabeler ? (
<LabelerCard labeler={state.selectedLabeler} />
) : (
<View style={[a.flex_row, a.align_center, a.gap_md]}>
<View style={[a.flex_1]}>
<LabelerCard labeler={state.selectedLabeler} />
</View>
<Button
label={_(msg`Change moderation service`)}
size="tiny"
variant="solid"
color="secondary"
shape="round"
onPress={() => {
dispatch({type: 'clearLabeler'})
}}>
<ButtonIcon icon={X} />
</Button>
</View>
<Button
label={_(msg`Change moderation service`)}
size="tiny"
variant="solid"
color="secondary"
shape="round"
onPress={() => {
dispatch({type: 'clearLabeler'})
}}>
<ButtonIcon icon={X} />
</Button>
</View>
)}
</>
) : (
<>
{hasSupportedLabelers ? (
<View style={[a.gap_sm]}>
{hasSingleSupportedLabeler ? (
<>
<LabelerCard labeler={supportedLabelers[0]} />
<ActionOnce
check={() => !state.selectedLabeler}
callback={() => {
dispatch({
type: 'selectLabeler',
labeler: supportedLabelers[0],
})
}}
/>
</>
) : (
<>
{supportedLabelers.map(l => (
<LabelerCard
key={l.creator.did}
labeler={l}
onSelect={() => {
dispatch({type: 'selectLabeler', labeler: l})
)}
</>
) : (
<>
{hasSupportedLabelers ? (
<View style={[a.gap_sm]}>
{hasSingleSupportedLabeler ? (
<>
<LabelerCard labeler={supportedLabelers[0]} />
<ActionOnce
check={() => !state.selectedLabeler}
callback={() => {
dispatch({
type: 'selectLabeler',
labeler: supportedLabelers[0],
})
}}
/>
))}
</>
)}
</View>
) : (
// should never happen in our app
<Admonition.Admonition type="warning">
<Trans>
Unfortunately, none of your subscribed labelers supports
this report type.
</Trans>
</Admonition.Admonition>
)}
</>
)}
</>
)}
</StepOuter>
</>
) : (
<>
{supportedLabelers.map(l => (
<LabelerCard
key={l.creator.did}
labeler={l}
onSelect={() => {
dispatch({type: 'selectLabeler', labeler: l})
}}
/>
))}
</>
)}
</View>
) : (
// should never happen in our app
<Admonition.Admonition type="warning">
<Trans>
Unfortunately, none of your subscribed labelers
supports this report type.
</Trans>
</Admonition.Admonition>
)}
</>
)}
</>
)}
</StepOuter>
)}
<StepOuter>
<StepTitle
index={4}
index={isAlwaysBskyLabeler ? 3 : 4}
title={_(msg`Submit report`)}
activeIndex1={state.activeStepIndex1}
activeIndex1={
isAlwaysBskyLabeler
? state.activeStepIndex1 - 1
: state.activeStepIndex1
}
/>
{state.activeStepIndex1 === 4 && (
<>
@@ -8,6 +8,12 @@ import {
import type * as Dialog from '#/components/Dialog'
export type ReportSubjectConvo = {
view: 'convo' | 'message'
convoId: string
message: ChatBskyConvoDefs.MessageView
}
export type ReportSubject =
| $Typed<AppBskyActorDefs.ProfileViewBasic>
| $Typed<AppBskyActorDefs.ProfileView>
@@ -16,7 +22,7 @@ export type ReportSubject =
| $Typed<AppBskyFeedDefs.GeneratorView>
| $Typed<AppBskyGraphDefs.StarterPackView>
| $Typed<AppBskyFeedDefs.PostView>
| {convoId: string; message: ChatBskyConvoDefs.MessageView}
| ReportSubjectConvo
export type ParsedReportSubject =
| {
@@ -55,13 +61,15 @@ export type ParsedReportSubject =
did: string
nsid: string
}
| {
type: 'chatMessage'
convoId: string
message: ChatBskyConvoDefs.MessageView
}
| ({
type: 'convoMessage'
} & ReportSubjectConvo)
export type ReportDialogProps = {
control: Dialog.DialogOuterProps['control']
subject: ParsedReportSubject
/**
* Called if the report was successfully submitted.
*/
onAfterSubmit?: () => void
}
@@ -18,7 +18,7 @@ export function parseReportSubject(
if ('convoId' in subject) {
return {
type: 'chatMessage',
type: 'convoMessage',
...subject,
}
}
@@ -25,12 +25,13 @@ import {
EmailDialogScreenID,
useEmailDialogControl,
} from '#/components/dialogs/EmailDialog'
import {ReportDialog} from '#/components/dms/ReportDialog'
import {AfterReportDialog} from '#/components/dms/AfterReportDialog'
import {CircleX_Stroke2_Corner0_Rounded} from '#/components/icons/CircleX'
import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag'
import {PersonX_Stroke2_Corner0_Rounded as PersonXIcon} from '#/components/icons/Person'
import {Loader} from '#/components/Loader'
import * as Menu from '#/components/Menu'
import {ReportDialog} from '#/components/moderation/ReportDialog'
export function RejectMenu({
convo,
@@ -100,6 +101,7 @@ export function RejectMenu({
}, [queueBlock, leaveConvo, _])
const reportControl = useDialogControl()
const blockOrDeleteControl = useDialogControl()
const lastMessage = ChatBskyConvoDefs.isMessageView(convo.lastMessage)
? convo.lastMessage
@@ -162,15 +164,27 @@ export function RejectMenu({
</Menu.Outer>
</Menu.Root>
{lastMessage && (
<ReportDialog
currentScreen={currentScreen}
params={{
type: 'convoMessage',
convoId: convo.id,
message: lastMessage,
}}
control={reportControl}
/>
<>
<ReportDialog
subject={{
view: 'convo',
convoId: convo.id,
message: lastMessage,
}}
control={reportControl}
onAfterSubmit={() => {
blockOrDeleteControl.open()
}}
/>
<AfterReportDialog
control={blockOrDeleteControl}
currentScreen={currentScreen}
params={{
convoId: convo.id,
message: lastMessage,
}}
/>
</>
)}
</>
)