diff --git a/src/components/ReportDialog/SelectLabelerView.tsx b/src/components/ReportDialog/SelectLabelerView.tsx
deleted file mode 100644
index 02b3244428..0000000000
--- a/src/components/ReportDialog/SelectLabelerView.tsx
+++ /dev/null
@@ -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 (
-
-
-
- Select moderator
-
-
- To whom would you like to send this report?
-
-
-
-
-
-
- {props.labelers.map(labeler => {
- return (
-
- )
- })}
-
-
- )
-}
-
-function LabelerButton({
- labeler,
-}: {
- labeler: AppBskyLabelerDefs.LabelerViewDetailed
-}) {
- const t = useTheme()
- const {hovered, pressed} = useButtonContext()
- const interacted = hovered || pressed
-
- return (
-
-
-
-
-
- @{labeler.creator.handle}
-
-
-
- )
-}
diff --git a/src/components/ReportDialog/SelectReportOptionView.tsx b/src/components/ReportDialog/SelectReportOptionView.tsx
deleted file mode 100644
index 0fd321cdf8..0000000000
--- a/src/components/ReportDialog/SelectReportOptionView.tsx
+++ /dev/null
@@ -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 (
-
- {props.labelers?.length > 1 ? (
-
- ) : null}
-
-
- {i18n.title}
-
- {i18n.description}
-
-
-
-
-
-
- {reportOptions.map(reportOption => {
- return (
-
- )
- })}
-
- {(props.params.type === 'post' || props.params.type === 'account') && (
-
-
- Need to report a copyright violation?
-
-
-
- View details
-
-
-
-
- )}
-
-
- )
-}
-
-function ReportOptionButton({
- title,
- description,
-}: {
- title: string
- description: string
-}) {
- const t = useTheme()
- const {hovered, pressed} = useButtonContext()
- const interacted = hovered || pressed
-
- return (
-
-
-
- {title}
-
- {description}
-
-
-
-
-
-
- )
-}
diff --git a/src/components/ReportDialog/SubmitView.tsx b/src/components/ReportDialog/SubmitView.tsx
deleted file mode 100644
index 3dbb1389d2..0000000000
--- a/src/components/ReportDialog/SubmitView.tsx
+++ /dev/null
@@ -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('')
- const [submitting, setSubmitting] = React.useState(false)
- const [selectedServices, setSelectedServices] = React.useState([
- 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 (
-
-
-
-
-
-
- {selectedReportOption.title}
-
-
- {selectedReportOption.description}
-
-
-
-
-
-
-
-
- Select the moderation service(s) to report to
-
-
-
-
- {labelers.map(labeler => {
- const title = getLabelingServiceTitle({
- displayName: labeler.creator.displayName,
- handle: labeler.creator.handle,
- })
- return (
-
-
-
- )
- })}
-
-
-
-
-
- Optionally provide additional information below:
-
-
-
-
-
-
-
-
-
-
-
-
- {!selectedServices.length ||
- (error && (
-
- {error ? (
- error
- ) : (
- You must select at least one labeler for a report
- )}
-
- ))}
-
-
-
- {/* Maybe fix this later -h */}
- {isAndroid ? : null}
-
- )
-}
-
-function LabelerToggle({title}: {title: string}) {
- const t = useTheme()
- const ctx = Toggle.useItemContext()
-
- return (
-
-
-
-
- {title}
-
-
-
- )
-}
diff --git a/src/components/ReportDialog/const.ts b/src/components/ReportDialog/const.ts
deleted file mode 100644
index 30c9aff88d..0000000000
--- a/src/components/ReportDialog/const.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const DMCA_LINK = 'https://bsky.social/about/support/copyright'
diff --git a/src/components/ReportDialog/index.tsx b/src/components/ReportDialog/index.tsx
deleted file mode 100644
index 2401ab89f5..0000000000
--- a/src/components/ReportDialog/index.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
- )
-}
-
-function ReportDialogInner(props: ReportDialogProps) {
- const {_} = useLingui()
- const {
- isLoading: isLabelerLoading,
- data: labelers,
- error,
- } = useMyLabelersQuery({excludeNonConfigurableLabelers: true})
- const isLoading = useDelayedLoading(500, isLabelerLoading)
-
- const ref = React.useRef(null)
-
- return (
-
- {isLoading ? (
-
-
- {/* Here to capture focus for a hot sec to prevent flash */}
-
-
- ) : error || !labelers ? (
-
-
- Something went wrong, please try again.
-
-
- ) : (
-
- )}
-
- )
-}
-
-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 (
- setSelectedReportOption(undefined)}
- onSubmitComplete={() => props.control.close()}
- />
- )
- }
- if (selectedLabeler) {
- return (
- setSelectedLabeler(undefined)}
- onSelectReportOption={setSelectedReportOption}
- />
- )
- }
- return
-}
diff --git a/src/components/ReportDialog/types.ts b/src/components/ReportDialog/types.ts
deleted file mode 100644
index ac9a18d6dd..0000000000
--- a/src/components/ReportDialog/types.ts
+++ /dev/null
@@ -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'}
-}
diff --git a/src/components/dms/AfterReportDialog.tsx b/src/components/dms/AfterReportDialog.tsx
new file mode 100644
index 0000000000..a87e5bdc25
--- /dev/null
+++ b/src/components/dms/AfterReportDialog.tsx
@@ -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 (
+
+
+
+
+
+
+
+ )
+})
+
+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 ? (
+
+
+
+ ) : isError || !profile ? (
+
+
+
+ Report submitted
+
+
+ Our moderation team has received your report.
+
+
+
+
+
+ ) : (
+
+ )
+}
+
+function DoneStep({
+ convoId,
+ currentScreen,
+ profile,
+}: {
+ convoId: string
+ currentScreen: 'list' | 'conversation'
+ profile: AppBskyActorDefs.ProfileViewDetailed
+}) {
+ const {_} = useLingui()
+ const navigation = useNavigation()
+ const control = Dialog.useDialogContext()
+ const {gtMobile} = useBreakpoints()
+ const t = useTheme()
+ const [actions, setActions] = useState(['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 (
+
+
+
+ Report submitted
+
+
+ Our moderation team has received your report.
+
+
+
+
+
+
+
+ Block user
+
+
+
+
+
+ Delete conversation
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx
index 1b1ebbcd59..fa50ec1e99 100644
--- a/src/components/dms/ConvoMenu.tsx
+++ b/src/components/dms/ConvoMenu.tsx
@@ -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 ? (
-
+ <>
+ {
+ blockOrDeleteControl.open()
+ }}
+ />
+
+ >
) : (
)}
diff --git a/src/components/dms/MessageContextMenu.tsx b/src/components/dms/MessageContextMenu.tsx
index 670e677db2..b00b138ad7 100644
--- a/src/components/dms/MessageContextMenu.tsx
+++ b/src/components/dms/MessageContextMenu.tsx
@@ -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 = ({
{
+ blockOrDeleteControl.open()
+ }}
+ />
+
{
- const {_} = useLingui()
- return (
-
-
-
-
-
-
-
- )
-}
-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(null)
- const [done, setDone] = useState(false)
- const control = Dialog.useDialogContext()
-
- return done ? (
- profile ? (
-
- ) : (
-
-
-
- )
- ) : reportOption ? (
- setReportOption(null)}
- onComplete={() => {
- if (isError) {
- control.close()
- } else {
- setDone(true)
- }
- }}
- />
- ) : (
-
- )
-}
-
-function ReasonStep({
- setReportOption,
-}: {
- setReportOption: (reportOption: ReportOption) => void
- params: ReportDialogParams
-}) {
- const control = Dialog.useDialogContext()
-
- return (
-
- )
-}
-
-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 = {
- $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 (
-
-
-
-
- {copy.title}
-
-
- Your report will be sent to the Bluesky Moderation Service
-
-
-
-
- {params.type === 'convoMessage' && (
-
- )}
-
-
-
- Reason:
- {' '}
- {reportOption.title}
-
-
-
-
-
-
- Optionally provide additional information below:
-
-
-
-
-
-
-
-
-
-
-
- {error && (
-
-
- There was an issue sending your report. Please check your internet
- connection.
-
-
- )}
-
-
-
-
- )
-}
-
-function DoneStep({
- convoId,
- currentScreen,
- profile,
-}: {
- convoId: string
- currentScreen: 'list' | 'conversation'
- profile: AppBskyActorDefs.ProfileViewDetailed
-}) {
- const {_} = useLingui()
- const navigation = useNavigation()
- const control = Dialog.useDialogContext()
- const {gtMobile} = useBreakpoints()
- const t = useTheme()
- const [actions, setActions] = useState(['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 (
-
-
-
- Report submitted
-
-
- Our moderation team has received your report.
-
-
-
-
-
-
-
- Block user
-
-
-
-
-
- Delete conversation
-
-
-
-
-
-
-
-
-
-
- )
-}
-
-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/components/moderation/ReportDialog/action.ts b/src/components/moderation/ReportDialog/action.ts
index 4c12b2c2bc..1c35bd0494 100644
--- a/src/components/moderation/ReportDialog/action.ts
+++ b/src/components/moderation/ReportDialog/action.ts
@@ -85,7 +85,7 @@ export function useSubmitReportMutation() {
}
break
}
- case 'chatMessage': {
+ case 'convoMessage': {
report = {
reasonType,
reason: state.details,
diff --git a/src/components/moderation/ReportDialog/copy.ts b/src/components/moderation/ReportDialog/copy.ts
index 11536c00c2..1027673b12 100644
--- a/src/components/moderation/ReportDialog/copy.ts
+++ b/src/components/moderation/ReportDialog/copy.ts
@@ -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?`),
+ }
+ }
}
}
}
diff --git a/src/components/moderation/ReportDialog/index.tsx b/src/components/moderation/ReportDialog/index.tsx
index 06920619f5..a264d12bc2 100644
--- a/src/components/moderation/ReportDialog/index.tsx
+++ b/src/components/moderation/ReportDialog/index.tsx
@@ -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}
-
- !state.selectedLabeler}
+ callback={() => {
+ dispatch({
+ type: 'selectLabeler',
+ labeler: supportedLabelers[0],
+ })
+ }}
/>
- {state.activeStepIndex1 >= 3 && (
- <>
- {state.selectedLabeler ? (
- <>
- {hasSingleSupportedLabeler ? (
-
- ) : (
-
-
-
+ ) : (
+
+
+ {state.activeStepIndex1 >= 3 && (
+ <>
+ {state.selectedLabeler ? (
+ <>
+ {hasSingleSupportedLabeler ? (
+
+ ) : (
+
+
+
+
+
-
-
- )}
- >
- ) : (
- <>
- {hasSupportedLabelers ? (
-
- {hasSingleSupportedLabeler ? (
- <>
-
- !state.selectedLabeler}
- callback={() => {
- dispatch({
- type: 'selectLabeler',
- labeler: supportedLabelers[0],
- })
- }}
- />
- >
- ) : (
- <>
- {supportedLabelers.map(l => (
- {
- dispatch({type: 'selectLabeler', labeler: l})
+ )}
+ >
+ ) : (
+ <>
+ {hasSupportedLabelers ? (
+
+ {hasSingleSupportedLabeler ? (
+ <>
+
+ !state.selectedLabeler}
+ callback={() => {
+ dispatch({
+ type: 'selectLabeler',
+ labeler: supportedLabelers[0],
+ })
}}
/>
- ))}
- >
- )}
-
- ) : (
- // should never happen in our app
-
-
- Unfortunately, none of your subscribed labelers supports
- this report type.
-
-
- )}
- >
- )}
- >
- )}
-
+ >
+ ) : (
+ <>
+ {supportedLabelers.map(l => (
+ {
+ dispatch({type: 'selectLabeler', labeler: l})
+ }}
+ />
+ ))}
+ >
+ )}
+
+ ) : (
+ // should never happen in our app
+
+
+ Unfortunately, none of your subscribed labelers
+ supports this report type.
+
+
+ )}
+ >
+ )}
+ >
+ )}
+
+ )}
{state.activeStepIndex1 === 4 && (
<>
diff --git a/src/components/moderation/ReportDialog/types.ts b/src/components/moderation/ReportDialog/types.ts
index e84369905c..6df3a82ff6 100644
--- a/src/components/moderation/ReportDialog/types.ts
+++ b/src/components/moderation/ReportDialog/types.ts
@@ -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
| $Typed
@@ -16,7 +22,7 @@ export type ReportSubject =
| $Typed
| $Typed
| $Typed
- | {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
}
diff --git a/src/components/moderation/ReportDialog/utils/parseReportSubject.ts b/src/components/moderation/ReportDialog/utils/parseReportSubject.ts
index baab1934fd..0abfe1d9fd 100644
--- a/src/components/moderation/ReportDialog/utils/parseReportSubject.ts
+++ b/src/components/moderation/ReportDialog/utils/parseReportSubject.ts
@@ -18,7 +18,7 @@ export function parseReportSubject(
if ('convoId' in subject) {
return {
- type: 'chatMessage',
+ type: 'convoMessage',
...subject,
}
}
diff --git a/src/screens/Messages/components/RequestButtons.tsx b/src/screens/Messages/components/RequestButtons.tsx
index 4437a9a43f..4b53b40420 100644
--- a/src/screens/Messages/components/RequestButtons.tsx
+++ b/src/screens/Messages/components/RequestButtons.tsx
@@ -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({
{lastMessage && (
-
+ <>
+ {
+ blockOrDeleteControl.open()
+ }}
+ />
+
+ >
)}
>
)