Add ability to report a group chat (#10485)
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
import {memo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyActorDefs} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
|
||||
import {
|
||||
useProfileBlockMutationQueue,
|
||||
useProfileQuery,
|
||||
} from '#/state/queries/profile'
|
||||
import {atoms as a, 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 * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
|
||||
type ReportDialogParams = {
|
||||
convoId: string
|
||||
did: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog shown after a report is submitted, allowing the user to block the
|
||||
* reporter and/or leave the conversation.
|
||||
*/
|
||||
export const AfterReportConversationDialog = memo(
|
||||
function BlockOrLeaveDialogInner({
|
||||
control,
|
||||
params,
|
||||
currentScreen,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
params: ReportDialogParams
|
||||
currentScreen: 'list' | 'conversation'
|
||||
}): React.ReactNode {
|
||||
const {t: l} = useLingui()
|
||||
return (
|
||||
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner
|
||||
label={l`Would you like to block this user and/or leave 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 {t: l} = useLingui()
|
||||
const control = Dialog.useDialogContext()
|
||||
const {
|
||||
data: profile,
|
||||
isPending,
|
||||
isError,
|
||||
} = useProfileQuery({
|
||||
did: params.did,
|
||||
})
|
||||
|
||||
return isPending ? (
|
||||
<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={l`Close`}
|
||||
onPress={() => control.close()}
|
||||
size="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 {t: l} = 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 handleActionsChange = (newActions: string[]) => {
|
||||
const hadBlock = actions.includes('block')
|
||||
const hasBlock = newActions.includes('block')
|
||||
|
||||
// If block was just checked, ensure leave is also checked
|
||||
if (!hadBlock && hasBlock) {
|
||||
if (!newActions.includes('leave')) {
|
||||
setActions([...newActions, 'leave'])
|
||||
} else {
|
||||
setActions(newActions)
|
||||
}
|
||||
}
|
||||
// If block was just unchecked, also uncheck leave
|
||||
else if (hadBlock && !hasBlock) {
|
||||
setActions(newActions.filter(action => action !== 'leave'))
|
||||
}
|
||||
// Otherwise, use the new actions as-is (user can toggle leave independently)
|
||||
else {
|
||||
setActions(newActions)
|
||||
}
|
||||
}
|
||||
|
||||
const {mutate: leaveConvo} = useLeaveConvo(convoId, {
|
||||
onMutate: () => {
|
||||
if (currentScreen === 'conversation') {
|
||||
navigation.dispatch(
|
||||
StackActions.replace('Messages', IS_NATIVE ? {animation: 'pop'} : {}),
|
||||
)
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(l`Could not leave chat`, {
|
||||
type: 'error',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
let btnText = l`Done`
|
||||
let toastMsg: string | undefined
|
||||
if (actions.includes('leave') && actions.includes('block')) {
|
||||
btnText = l({
|
||||
message: 'Block and leave',
|
||||
context: 'button',
|
||||
comment: 'After-report action for a conversation',
|
||||
})
|
||||
toastMsg = l({message: 'Conversation left', context: 'toast'})
|
||||
} else if (actions.includes('leave')) {
|
||||
btnText = l({
|
||||
message: 'Leave conversation',
|
||||
context: 'button',
|
||||
comment: 'After-report action for a conversation',
|
||||
})
|
||||
toastMsg = l({message: 'Conversation left', context: 'toast'})
|
||||
} else if (actions.includes('block')) {
|
||||
// Shouldn't be able to reach this, but here for completeness.
|
||||
btnText = l({
|
||||
message: 'Block user',
|
||||
context: 'button',
|
||||
comment: 'After-report action for a conversation',
|
||||
})
|
||||
toastMsg = l({message: 'User blocked', context: 'toast'})
|
||||
}
|
||||
|
||||
const onPressPrimaryAction = () => {
|
||||
control.close(() => {
|
||||
if (actions.includes('block')) {
|
||||
void queueBlock()
|
||||
}
|
||||
if (actions.includes('leave')) {
|
||||
leaveConvo()
|
||||
}
|
||||
if (toastMsg) {
|
||||
Toast.show(toastMsg, {
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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={l`Block user and/or leave this conversation`}
|
||||
values={actions}
|
||||
onChange={handleActionsChange}>
|
||||
<View style={[a.gap_md]}>
|
||||
<Toggle.Item name="block" label={l`Block user`}>
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.LabelText style={[a.text_md]}>
|
||||
<Trans>Block user</Trans>
|
||||
</Toggle.LabelText>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item
|
||||
name="leave"
|
||||
label={l`Leave conversation`}
|
||||
disabled={actions.includes('block')}>
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.LabelText style={[a.text_md]}>
|
||||
<Trans>Leave 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={l`Close`}
|
||||
onPress={() => control.close()}
|
||||
size="large"
|
||||
color="secondary">
|
||||
<ButtonText>
|
||||
<Trans>Close</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import {memo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyActorDefs, type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {type AppBskyActorDefs} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
@@ -24,7 +22,7 @@ import {IS_NATIVE} from '#/env'
|
||||
|
||||
type ReportDialogParams = {
|
||||
convoId: string
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
did: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,14 +38,12 @@ export const AfterReportDialog = memo(function BlockOrDeleteDialogInner({
|
||||
params: ReportDialogParams
|
||||
currentScreen: 'list' | 'conversation'
|
||||
}): React.ReactNode {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
return (
|
||||
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner
|
||||
label={_(
|
||||
msg`Would you like to block this user and/or delete this conversation?`,
|
||||
)}
|
||||
label={l`Would you like to block this user and/or delete this conversation?`}
|
||||
style={[web({maxWidth: 400})]}>
|
||||
<DialogInner params={params} currentScreen={currentScreen} />
|
||||
<Dialog.Close />
|
||||
@@ -64,14 +60,14 @@ function DialogInner({
|
||||
currentScreen: 'list' | 'conversation'
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const control = Dialog.useDialogContext()
|
||||
const {
|
||||
data: profile,
|
||||
isPending,
|
||||
isError,
|
||||
} = useProfileQuery({
|
||||
did: params.message.sender.did,
|
||||
did: params.did,
|
||||
})
|
||||
|
||||
return isPending ? (
|
||||
@@ -90,7 +86,7 @@ function DialogInner({
|
||||
</View>
|
||||
|
||||
<Button
|
||||
label={_(msg`Close`)}
|
||||
label={l`Close`}
|
||||
onPress={() => control.close()}
|
||||
size={platform({native: 'small', web: 'large'})}
|
||||
color="secondary">
|
||||
@@ -117,7 +113,7 @@ function DoneStep({
|
||||
currentScreen: 'list' | 'conversation'
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const control = Dialog.useDialogContext()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
@@ -135,29 +131,29 @@ function DoneStep({
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Could not leave chat`), {
|
||||
Toast.show(l`Could not leave chat`, {
|
||||
type: 'error',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
let btnText = _(msg`Done`)
|
||||
let btnText = l`Done`
|
||||
let toastMsg: string | undefined
|
||||
if (actions.includes('leave') && actions.includes('block')) {
|
||||
btnText = _(msg`Block and Delete`)
|
||||
toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'}))
|
||||
btnText = l`Block and delete`
|
||||
toastMsg = l({message: 'Conversation deleted', context: 'toast'})
|
||||
} else if (actions.includes('leave')) {
|
||||
btnText = _(msg`Delete Conversation`)
|
||||
toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'}))
|
||||
btnText = l`Delete conversation`
|
||||
toastMsg = l({message: 'Conversation deleted', context: 'toast'})
|
||||
} else if (actions.includes('block')) {
|
||||
btnText = _(msg`Block User`)
|
||||
toastMsg = _(msg({message: 'User blocked', context: 'toast'}))
|
||||
btnText = l`Block user`
|
||||
toastMsg = l({message: 'User blocked', context: 'toast'})
|
||||
}
|
||||
|
||||
const onPressPrimaryAction = () => {
|
||||
control.close(() => {
|
||||
if (actions.includes('block')) {
|
||||
queueBlock()
|
||||
void queueBlock()
|
||||
}
|
||||
if (actions.includes('leave')) {
|
||||
leaveConvo()
|
||||
@@ -181,17 +177,17 @@ function DoneStep({
|
||||
</Text>
|
||||
</View>
|
||||
<Toggle.Group
|
||||
label={_(msg`Block user and/or delete this conversation`)}
|
||||
label={l`Block user and/or delete this conversation`}
|
||||
values={actions}
|
||||
onChange={setActions}>
|
||||
<View style={[a.gap_md]}>
|
||||
<Toggle.Item name="block" label={_(msg`Block user`)}>
|
||||
<Toggle.Item name="block" label={l`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.Item name="leave" label={l`Delete conversation`}>
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.LabelText style={[a.text_md]}>
|
||||
<Trans>Delete conversation</Trans>
|
||||
@@ -199,7 +195,6 @@ function DoneStep({
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
|
||||
<View style={[a.gap_sm]}>
|
||||
<Button
|
||||
label={btnText}
|
||||
@@ -209,7 +204,7 @@ function DoneStep({
|
||||
<ButtonText>{btnText}</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
label={_(msg`Close`)}
|
||||
label={l`Close`}
|
||||
onPress={() => control.close()}
|
||||
size="large"
|
||||
color="secondary">
|
||||
|
||||
@@ -22,7 +22,7 @@ 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 {ReportConversationDialog} from '#/components/dms/ReportConversationDialog'
|
||||
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeftIcon} from '#/components/icons/ArrowBoxLeft'
|
||||
import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble'
|
||||
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
|
||||
@@ -39,6 +39,7 @@ import {ReportDialog} from '#/components/moderation/ReportDialog'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {AfterReportConversationDialog} from './AfterReportConversationDialog'
|
||||
|
||||
let ConvoMenu = ({
|
||||
convo,
|
||||
@@ -71,6 +72,7 @@ let ConvoMenu = ({
|
||||
const reportControl = Prompt.usePromptControl()
|
||||
const blockedByListControl = Prompt.usePromptControl()
|
||||
const blockOrDeleteControl = Prompt.usePromptControl()
|
||||
const deleteControl = Prompt.usePromptControl()
|
||||
|
||||
const {listBlocks} = blockInfo
|
||||
|
||||
@@ -141,12 +143,27 @@ let ConvoMenu = ({
|
||||
currentScreen={currentScreen}
|
||||
params={{
|
||||
convoId: convo.id,
|
||||
message: latestReportableMessage,
|
||||
did: latestReportableMessage.sender.did,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ReportConversationPrompt control={reportControl} />
|
||||
<>
|
||||
<ReportConversationDialog
|
||||
control={reportControl}
|
||||
convoId={convo.id}
|
||||
did={profile.did}
|
||||
onAfterSubmit={deleteControl.open}
|
||||
/>
|
||||
<AfterReportConversationDialog
|
||||
control={deleteControl}
|
||||
currentScreen={currentScreen}
|
||||
params={{
|
||||
convoId: convo.id,
|
||||
did: profile.did,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<BlockedByListDialog
|
||||
control={blockedByListControl}
|
||||
|
||||
@@ -214,7 +214,7 @@ export let MessageContextMenu = ({
|
||||
currentScreen="conversation"
|
||||
params={{
|
||||
convoId: convo.convo.view.id,
|
||||
message,
|
||||
did: message.sender.did,
|
||||
}}
|
||||
/>
|
||||
<Prompt.Basic
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import {type DialogControlProps} from '#/components/Dialog'
|
||||
import {ReportDialog} from '#/components/moderation/ReportDialog'
|
||||
|
||||
export function ReportConversationDialog({
|
||||
control,
|
||||
convoId,
|
||||
did,
|
||||
onAfterSubmit,
|
||||
}: {
|
||||
control: DialogControlProps
|
||||
convoId: string
|
||||
did: string
|
||||
onAfterSubmit?: () => void
|
||||
}) {
|
||||
return (
|
||||
<ReportDialog
|
||||
control={control}
|
||||
subject={{convoId, did}}
|
||||
onAfterSubmit={onAfterSubmit}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {type DialogControlProps} from '#/components/Dialog'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
|
||||
export function ReportConversationPrompt({
|
||||
control,
|
||||
}: {
|
||||
control: DialogControlProps
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<Prompt.Basic
|
||||
control={control}
|
||||
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={() => {}}
|
||||
showCancel={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user