Add ability to report a group chat (#10485)
This commit is contained in:
@@ -202,11 +202,6 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/moderation/ReportDialog/index.tsx": {
|
|
||||||
"@typescript-eslint/no-explicit-any": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/features/liveNow/index.tsx": {
|
"src/features/liveNow/index.tsx": {
|
||||||
"@typescript-eslint/no-explicit-any": {
|
"@typescript-eslint/no-explicit-any": {
|
||||||
"count": 3
|
"count": 3
|
||||||
|
|||||||
@@ -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 {memo, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {type AppBskyActorDefs, type ChatBskyConvoDefs} from '@atproto/api'
|
import {type AppBskyActorDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {Trans} from '@lingui/react/macro'
|
|
||||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
@@ -24,7 +22,7 @@ import {IS_NATIVE} from '#/env'
|
|||||||
|
|
||||||
type ReportDialogParams = {
|
type ReportDialogParams = {
|
||||||
convoId: string
|
convoId: string
|
||||||
message: ChatBskyConvoDefs.MessageView
|
did: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,14 +38,12 @@ export const AfterReportDialog = memo(function BlockOrDeleteDialogInner({
|
|||||||
params: ReportDialogParams
|
params: ReportDialogParams
|
||||||
currentScreen: 'list' | 'conversation'
|
currentScreen: 'list' | 'conversation'
|
||||||
}): React.ReactNode {
|
}): React.ReactNode {
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
label={_(
|
label={l`Would you like to block this user and/or delete this conversation?`}
|
||||||
msg`Would you like to block this user and/or delete this conversation?`,
|
|
||||||
)}
|
|
||||||
style={[web({maxWidth: 400})]}>
|
style={[web({maxWidth: 400})]}>
|
||||||
<DialogInner params={params} currentScreen={currentScreen} />
|
<DialogInner params={params} currentScreen={currentScreen} />
|
||||||
<Dialog.Close />
|
<Dialog.Close />
|
||||||
@@ -64,14 +60,14 @@ function DialogInner({
|
|||||||
currentScreen: 'list' | 'conversation'
|
currentScreen: 'list' | 'conversation'
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const control = Dialog.useDialogContext()
|
const control = Dialog.useDialogContext()
|
||||||
const {
|
const {
|
||||||
data: profile,
|
data: profile,
|
||||||
isPending,
|
isPending,
|
||||||
isError,
|
isError,
|
||||||
} = useProfileQuery({
|
} = useProfileQuery({
|
||||||
did: params.message.sender.did,
|
did: params.did,
|
||||||
})
|
})
|
||||||
|
|
||||||
return isPending ? (
|
return isPending ? (
|
||||||
@@ -90,7 +86,7 @@ function DialogInner({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Close`)}
|
label={l`Close`}
|
||||||
onPress={() => control.close()}
|
onPress={() => control.close()}
|
||||||
size={platform({native: 'small', web: 'large'})}
|
size={platform({native: 'small', web: 'large'})}
|
||||||
color="secondary">
|
color="secondary">
|
||||||
@@ -117,7 +113,7 @@ function DoneStep({
|
|||||||
currentScreen: 'list' | 'conversation'
|
currentScreen: 'list' | 'conversation'
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const control = Dialog.useDialogContext()
|
const control = Dialog.useDialogContext()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
@@ -135,29 +131,29 @@ function DoneStep({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
Toast.show(_(msg`Could not leave chat`), {
|
Toast.show(l`Could not leave chat`, {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
let btnText = _(msg`Done`)
|
let btnText = l`Done`
|
||||||
let toastMsg: string | undefined
|
let toastMsg: string | undefined
|
||||||
if (actions.includes('leave') && actions.includes('block')) {
|
if (actions.includes('leave') && actions.includes('block')) {
|
||||||
btnText = _(msg`Block and Delete`)
|
btnText = l`Block and delete`
|
||||||
toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'}))
|
toastMsg = l({message: 'Conversation deleted', context: 'toast'})
|
||||||
} else if (actions.includes('leave')) {
|
} else if (actions.includes('leave')) {
|
||||||
btnText = _(msg`Delete Conversation`)
|
btnText = l`Delete conversation`
|
||||||
toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'}))
|
toastMsg = l({message: 'Conversation deleted', context: 'toast'})
|
||||||
} else if (actions.includes('block')) {
|
} else if (actions.includes('block')) {
|
||||||
btnText = _(msg`Block User`)
|
btnText = l`Block user`
|
||||||
toastMsg = _(msg({message: 'User blocked', context: 'toast'}))
|
toastMsg = l({message: 'User blocked', context: 'toast'})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPressPrimaryAction = () => {
|
const onPressPrimaryAction = () => {
|
||||||
control.close(() => {
|
control.close(() => {
|
||||||
if (actions.includes('block')) {
|
if (actions.includes('block')) {
|
||||||
queueBlock()
|
void queueBlock()
|
||||||
}
|
}
|
||||||
if (actions.includes('leave')) {
|
if (actions.includes('leave')) {
|
||||||
leaveConvo()
|
leaveConvo()
|
||||||
@@ -181,17 +177,17 @@ function DoneStep({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Toggle.Group
|
<Toggle.Group
|
||||||
label={_(msg`Block user and/or delete this conversation`)}
|
label={l`Block user and/or delete this conversation`}
|
||||||
values={actions}
|
values={actions}
|
||||||
onChange={setActions}>
|
onChange={setActions}>
|
||||||
<View style={[a.gap_md]}>
|
<View style={[a.gap_md]}>
|
||||||
<Toggle.Item name="block" label={_(msg`Block user`)}>
|
<Toggle.Item name="block" label={l`Block user`}>
|
||||||
<Toggle.Checkbox />
|
<Toggle.Checkbox />
|
||||||
<Toggle.LabelText style={[a.text_md]}>
|
<Toggle.LabelText style={[a.text_md]}>
|
||||||
<Trans>Block user</Trans>
|
<Trans>Block user</Trans>
|
||||||
</Toggle.LabelText>
|
</Toggle.LabelText>
|
||||||
</Toggle.Item>
|
</Toggle.Item>
|
||||||
<Toggle.Item name="leave" label={_(msg`Delete conversation`)}>
|
<Toggle.Item name="leave" label={l`Delete conversation`}>
|
||||||
<Toggle.Checkbox />
|
<Toggle.Checkbox />
|
||||||
<Toggle.LabelText style={[a.text_md]}>
|
<Toggle.LabelText style={[a.text_md]}>
|
||||||
<Trans>Delete conversation</Trans>
|
<Trans>Delete conversation</Trans>
|
||||||
@@ -199,7 +195,6 @@ function DoneStep({
|
|||||||
</Toggle.Item>
|
</Toggle.Item>
|
||||||
</View>
|
</View>
|
||||||
</Toggle.Group>
|
</Toggle.Group>
|
||||||
|
|
||||||
<View style={[a.gap_sm]}>
|
<View style={[a.gap_sm]}>
|
||||||
<Button
|
<Button
|
||||||
label={btnText}
|
label={btnText}
|
||||||
@@ -209,7 +204,7 @@ function DoneStep({
|
|||||||
<ButtonText>{btnText}</ButtonText>
|
<ButtonText>{btnText}</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Close`)}
|
label={l`Close`}
|
||||||
onPress={() => control.close()}
|
onPress={() => control.close()}
|
||||||
size="large"
|
size="large"
|
||||||
color="secondary">
|
color="secondary">
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {Button, ButtonIcon} from '#/components/Button'
|
|||||||
import {AfterReportDialog} from '#/components/dms/AfterReportDialog'
|
import {AfterReportDialog} from '#/components/dms/AfterReportDialog'
|
||||||
import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog'
|
import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog'
|
||||||
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
|
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 {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeftIcon} from '#/components/icons/ArrowBoxLeft'
|
||||||
import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble'
|
import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble'
|
||||||
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
|
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 Prompt from '#/components/Prompt'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
import {AfterReportConversationDialog} from './AfterReportConversationDialog'
|
||||||
|
|
||||||
let ConvoMenu = ({
|
let ConvoMenu = ({
|
||||||
convo,
|
convo,
|
||||||
@@ -71,6 +72,7 @@ let ConvoMenu = ({
|
|||||||
const reportControl = Prompt.usePromptControl()
|
const reportControl = Prompt.usePromptControl()
|
||||||
const blockedByListControl = Prompt.usePromptControl()
|
const blockedByListControl = Prompt.usePromptControl()
|
||||||
const blockOrDeleteControl = Prompt.usePromptControl()
|
const blockOrDeleteControl = Prompt.usePromptControl()
|
||||||
|
const deleteControl = Prompt.usePromptControl()
|
||||||
|
|
||||||
const {listBlocks} = blockInfo
|
const {listBlocks} = blockInfo
|
||||||
|
|
||||||
@@ -141,12 +143,27 @@ let ConvoMenu = ({
|
|||||||
currentScreen={currentScreen}
|
currentScreen={currentScreen}
|
||||||
params={{
|
params={{
|
||||||
convoId: convo.id,
|
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
|
<BlockedByListDialog
|
||||||
control={blockedByListControl}
|
control={blockedByListControl}
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export let MessageContextMenu = ({
|
|||||||
currentScreen="conversation"
|
currentScreen="conversation"
|
||||||
params={{
|
params={{
|
||||||
convoId: convo.convo.view.id,
|
convoId: convo.convo.view.id,
|
||||||
message,
|
did: message.sender.did,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Prompt.Basic
|
<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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -55,7 +55,9 @@ export function useSubmitReportMutation() {
|
|||||||
let report:
|
let report:
|
||||||
| ComAtprotoModerationCreateReport.InputSchema
|
| ComAtprotoModerationCreateReport.InputSchema
|
||||||
| (Omit<ComAtprotoModerationCreateReport.InputSchema, 'subject'> & {
|
| (Omit<ComAtprotoModerationCreateReport.InputSchema, 'subject'> & {
|
||||||
subject: $Typed<ChatBskyConvoDefs.MessageRef>
|
subject:
|
||||||
|
| $Typed<ChatBskyConvoDefs.MessageRef>
|
||||||
|
| $Typed<ChatBskyConvoDefs.ConvoRef>
|
||||||
})
|
})
|
||||||
|
|
||||||
switch (subject.type) {
|
switch (subject.type) {
|
||||||
@@ -99,6 +101,18 @@ export function useSubmitReportMutation() {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'convo': {
|
||||||
|
report = {
|
||||||
|
reasonType,
|
||||||
|
reason: state.details,
|
||||||
|
subject: {
|
||||||
|
$type: 'chat.bsky.convo.defs#convoRef',
|
||||||
|
convoId: subject.convoId,
|
||||||
|
did: subject.did,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
|
|||||||
@@ -120,4 +120,4 @@ export const BSKY_LABELER_ONLY_REPORT_REASONS: Set<OzoneReportDefs.ReasonType> =
|
|||||||
* moderation service.
|
* moderation service.
|
||||||
*/
|
*/
|
||||||
export const BSKY_LABELER_ONLY_SUBJECT_TYPES: Set<ParsedReportSubject['type']> =
|
export const BSKY_LABELER_ONLY_SUBJECT_TYPES: Set<ParsedReportSubject['type']> =
|
||||||
new Set(['convoMessage', 'status'])
|
new Set(['convoMessage', 'convo', 'status'])
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ export function useCopyForSubject(subject: ParsedReportSubject) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'convo': {
|
||||||
|
return {
|
||||||
|
title: _(msg`Report this conversation`),
|
||||||
|
subtitle: _(msg`Why should this conversation be reviewed?`),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [_, subject])
|
}, [_, subject])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import {Pressable, type ScrollView, View} from 'react-native'
|
import {Pressable, type ScrollView, View} from 'react-native'
|
||||||
import {type AppBskyLabelerDefs, BSKY_LABELER_DID} from '@atproto/api'
|
import {type AppBskyLabelerDefs, BSKY_LABELER_DID} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {Trans} from '@lingui/react/macro'
|
|
||||||
|
|
||||||
import {wait} from '#/lib/async/wait'
|
import {wait} from '#/lib/async/wait'
|
||||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||||
@@ -93,9 +91,9 @@ export function ReportDialog(
|
|||||||
* developer, but nevertheless we should have a graceful fallback.
|
* developer, but nevertheless we should have a graceful fallback.
|
||||||
*/
|
*/
|
||||||
function Invalid() {
|
function Invalid() {
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
return (
|
return (
|
||||||
<Dialog.ScrollableInner label={_(msg`Report dialog`)}>
|
<Dialog.ScrollableInner label={l`Report dialog`}>
|
||||||
<Text style={[a.font_bold, a.text_xl, a.leading_snug, a.pb_xs]}>
|
<Text style={[a.font_bold, a.text_xl, a.leading_snug, a.pb_xs]}>
|
||||||
<Trans>Invalid report subject</Trans>
|
<Trans>Invalid report subject</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
@@ -114,7 +112,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const logger = ax.logger.useChild(ax.logger.Context.ReportDialog)
|
const logger = ax.logger.useChild(ax.logger.Context.ReportDialog)
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const ref = useRef<ScrollView>(null)
|
const ref = useRef<ScrollView>(null)
|
||||||
const {
|
const {
|
||||||
data: allLabelers,
|
data: allLabelers,
|
||||||
@@ -131,8 +129,8 @@ function Inner(props: ReportDialogProps) {
|
|||||||
* Submission handling
|
* Submission handling
|
||||||
*/
|
*/
|
||||||
const {mutateAsync: submitReport} = useSubmitReportMutation()
|
const {mutateAsync: submitReport} = useSubmitReportMutation()
|
||||||
const [isPending, setPending] = useState(false)
|
const [isPending, setIsPending] = useState(false)
|
||||||
const [isSuccess, setSuccess] = useState(false)
|
const [isSuccess, setIsSuccess] = useState(false)
|
||||||
|
|
||||||
// some reasons ONLY go to Bluesky
|
// some reasons ONLY go to Bluesky
|
||||||
const isBskyOnlyReason = state?.selectedOption?.reason
|
const isBskyOnlyReason = state?.selectedOption?.reason
|
||||||
@@ -154,7 +152,10 @@ function Inner(props: ReportDialogProps) {
|
|||||||
if (subjectTypes === undefined) return true
|
if (subjectTypes === undefined) return true
|
||||||
if (props.subject.type === 'account') {
|
if (props.subject.type === 'account') {
|
||||||
return subjectTypes.includes('account')
|
return subjectTypes.includes('account')
|
||||||
} else if (props.subject.type === 'convoMessage') {
|
} else if (
|
||||||
|
props.subject.type === 'convoMessage' ||
|
||||||
|
props.subject.type === 'convo'
|
||||||
|
) {
|
||||||
return subjectTypes.includes('chat')
|
return subjectTypes.includes('chat')
|
||||||
} else {
|
} else {
|
||||||
return subjectTypes.includes('record')
|
return subjectTypes.includes('record')
|
||||||
@@ -164,7 +165,11 @@ function Inner(props: ReportDialogProps) {
|
|||||||
const collections: string[] | undefined = l.subjectCollections
|
const collections: string[] | undefined = l.subjectCollections
|
||||||
if (collections === undefined) return true
|
if (collections === undefined) return true
|
||||||
// all chat collections accepted, since only Bluesky handles chats
|
// all chat collections accepted, since only Bluesky handles chats
|
||||||
if (props.subject.type === 'convoMessage') return true
|
if (
|
||||||
|
props.subject.type === 'convoMessage' ||
|
||||||
|
props.subject.type === 'convo'
|
||||||
|
)
|
||||||
|
return true
|
||||||
return collections.includes(props.subject.nsid)
|
return collections.includes(props.subject.nsid)
|
||||||
})
|
})
|
||||||
.filter(l => {
|
.filter(l => {
|
||||||
@@ -208,7 +213,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
logger.info('submitting')
|
logger.info('submitting')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setPending(true)
|
setIsPending(true)
|
||||||
// wait at least 1s, make it feel substantial
|
// wait at least 1s, make it feel substantial
|
||||||
await wait(
|
await wait(
|
||||||
1e3,
|
1e3,
|
||||||
@@ -217,7 +222,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
state,
|
state,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
setSuccess(true)
|
setIsSuccess(true)
|
||||||
ax.metric('reportDialog:success', {
|
ax.metric('reportDialog:success', {
|
||||||
reason: state.selectedOption?.reason ?? '',
|
reason: state.selectedOption?.reason ?? '',
|
||||||
labeler: state.selectedLabeler?.creator.handle ?? '',
|
labeler: state.selectedLabeler?.creator.handle ?? '',
|
||||||
@@ -229,29 +234,20 @@ function Inner(props: ReportDialogProps) {
|
|||||||
props.onAfterSubmit?.()
|
props.onAfterSubmit?.()
|
||||||
})
|
})
|
||||||
}, 1e3)
|
}, 1e3)
|
||||||
} catch (e: any) {
|
} catch (err) {
|
||||||
|
const e = err as Error
|
||||||
ax.metric('reportDialog:failure', {})
|
ax.metric('reportDialog:failure', {})
|
||||||
logger.error(e, {
|
logger.error(e, {
|
||||||
source: 'ReportDialog',
|
source: 'ReportDialog',
|
||||||
})
|
})
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'setError',
|
type: 'setError',
|
||||||
error: _(msg`Something went wrong. Please try again.`),
|
error: l`Something went wrong. Please try again.`,
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setPending(false)
|
setIsPending(false)
|
||||||
}
|
}
|
||||||
}, [
|
}, [logger, submitReport, props, state, ax, l])
|
||||||
_,
|
|
||||||
submitReport,
|
|
||||||
state,
|
|
||||||
dispatch,
|
|
||||||
props.subject,
|
|
||||||
props.control,
|
|
||||||
props.onAfterSubmit,
|
|
||||||
setPending,
|
|
||||||
setSuccess,
|
|
||||||
])
|
|
||||||
|
|
||||||
useCallOnce(() => {
|
useCallOnce(() => {
|
||||||
ax.metric('reportDialog:open', {
|
ax.metric('reportDialog:open', {
|
||||||
@@ -262,7 +258,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
return (
|
return (
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
testID="report:dialog"
|
testID="report:dialog"
|
||||||
label={_(msg`Report dialog`)}
|
label={l`Report dialog`}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={[a.w_full, {maxWidth: 500}]}>
|
style={[a.w_full, {maxWidth: 500}]}>
|
||||||
<View style={[a.gap_2xl, IS_NATIVE && a.pt_md]}>
|
<View style={[a.gap_2xl, IS_NATIVE && a.pt_md]}>
|
||||||
@@ -293,8 +289,8 @@ function Inner(props: ReportDialogProps) {
|
|||||||
</Admonition.Content>
|
</Admonition.Content>
|
||||||
<Admonition.Button
|
<Admonition.Button
|
||||||
color="negative_subtle"
|
color="negative_subtle"
|
||||||
label={_(msg`Retry loading report options`)}
|
label={l`Retry loading report options`}
|
||||||
onPress={() => refetchLabelers()}>
|
onPress={() => void refetchLabelers()}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Retry</Trans>
|
<Trans>Retry</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
@@ -311,7 +307,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
</View>
|
</View>
|
||||||
<Button
|
<Button
|
||||||
testID="report:clearCategory"
|
testID="report:clearCategory"
|
||||||
label={_(msg`Change report category`)}
|
label={l`Change report category`}
|
||||||
size="tiny"
|
size="tiny"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@@ -341,9 +337,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
{['post', 'account'].includes(props.subject.type) && (
|
{['post', 'account'].includes(props.subject.type) && (
|
||||||
<Link
|
<Link
|
||||||
to={SUPPORT_PAGE}
|
to={SUPPORT_PAGE}
|
||||||
label={_(
|
label={l`Need to report a copyright violation, legal request, or regulatory compliance issue?`}>
|
||||||
msg`Need to report a copyright violation, legal request, or regulatory compliance issue?`,
|
|
||||||
)}>
|
|
||||||
{({hovered, pressed}) => (
|
{({hovered, pressed}) => (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -381,7 +375,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
<StepOuter>
|
<StepOuter>
|
||||||
<StepTitle
|
<StepTitle
|
||||||
index={2}
|
index={2}
|
||||||
title={_(msg`Select a reason`)}
|
title={l`Select a reason`}
|
||||||
activeIndex1={state.activeStepIndex1}
|
activeIndex1={state.activeStepIndex1}
|
||||||
/>
|
/>
|
||||||
{state.selectedOption ? (
|
{state.selectedOption ? (
|
||||||
@@ -391,7 +385,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
</View>
|
</View>
|
||||||
<Button
|
<Button
|
||||||
testID="report:clearReportOption"
|
testID="report:clearReportOption"
|
||||||
label={_(msg`Change report reason`)}
|
label={l`Change report reason`}
|
||||||
size="tiny"
|
size="tiny"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@@ -431,7 +425,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
<StepOuter>
|
<StepOuter>
|
||||||
<StepTitle
|
<StepTitle
|
||||||
index={3}
|
index={3}
|
||||||
title={_(msg`Select moderation service`)}
|
title={l`Select moderation service`}
|
||||||
activeIndex1={state.activeStepIndex1}
|
activeIndex1={state.activeStepIndex1}
|
||||||
/>
|
/>
|
||||||
{state.activeStepIndex1 >= 3 && (
|
{state.activeStepIndex1 >= 3 && (
|
||||||
@@ -446,7 +440,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
<LabelerCard labeler={state.selectedLabeler} />
|
<LabelerCard labeler={state.selectedLabeler} />
|
||||||
</View>
|
</View>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Change moderation service`)}
|
label={l`Change moderation service`}
|
||||||
size="tiny"
|
size="tiny"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@@ -509,7 +503,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
<StepOuter>
|
<StepOuter>
|
||||||
<StepTitle
|
<StepTitle
|
||||||
index={isAlwaysBskyLabeler ? 3 : 4}
|
index={isAlwaysBskyLabeler ? 3 : 4}
|
||||||
title={_(msg`Submit report`)}
|
title={l`Submit report`}
|
||||||
activeIndex1={
|
activeIndex1={
|
||||||
isAlwaysBskyLabeler
|
isAlwaysBskyLabeler
|
||||||
? state.activeStepIndex1 - 1
|
? state.activeStepIndex1 - 1
|
||||||
@@ -529,7 +523,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
</Trans>{' '}
|
</Trans>{' '}
|
||||||
{!state.detailsOpen ? (
|
{!state.detailsOpen ? (
|
||||||
<InlineLinkText
|
<InlineLinkText
|
||||||
label={_(msg`Add more details (optional)`)}
|
label={l`Add more details (optional)`}
|
||||||
{...createStaticClick(() => {
|
{...createStaticClick(() => {
|
||||||
dispatch({type: 'showDetails'})
|
dispatch({type: 'showDetails'})
|
||||||
})}>
|
})}>
|
||||||
@@ -547,7 +541,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
onChangeText={details => {
|
onChangeText={details => {
|
||||||
dispatch({type: 'setDetails', details})
|
dispatch({type: 'setDetails', details})
|
||||||
}}
|
}}
|
||||||
label={_(msg`Additional details (limit 300 characters)`)}
|
label={l`Additional details (limit 300 characters)`}
|
||||||
style={{paddingRight: 60}}
|
style={{paddingRight: 60}}
|
||||||
numberOfLines={4}
|
numberOfLines={4}
|
||||||
/>
|
/>
|
||||||
@@ -570,12 +564,12 @@ function Inner(props: ReportDialogProps) {
|
|||||||
</View>
|
</View>
|
||||||
<Button
|
<Button
|
||||||
testID="report:submit"
|
testID="report:submit"
|
||||||
label={_(msg`Submit report`)}
|
label={l`Submit report`}
|
||||||
size="large"
|
size="large"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={isPending || isSuccess}
|
disabled={isPending || isSuccess}
|
||||||
onPress={onSubmit}>
|
onPress={() => void onSubmit()}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Submit report</Trans>
|
<Trans>Submit report</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
@@ -702,7 +696,7 @@ function CategoryCard({
|
|||||||
onSelect?: (option: ReportCategoryConfig) => void
|
onSelect?: (option: ReportCategoryConfig) => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const gutters = useGutters(['compact'])
|
const gutters = useGutters(['compact'])
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
onSelect?.(option)
|
onSelect?.(option)
|
||||||
@@ -710,7 +704,7 @@ function CategoryCard({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
testID={`report:category:${option.title}`}
|
testID={`report:category:${option.title}`}
|
||||||
label={_(msg`Create report for ${option.title}`)}
|
label={l`Create report for ${option.title}`}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
disabled={!onSelect}>
|
disabled={!onSelect}>
|
||||||
{({hovered, pressed}) => (
|
{({hovered, pressed}) => (
|
||||||
@@ -747,7 +741,7 @@ function OptionCard({
|
|||||||
onSelect?: (option: ReportOption) => void
|
onSelect?: (option: ReportOption) => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const gutters = useGutters(['compact'])
|
const gutters = useGutters(['compact'])
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
onSelect?.(option)
|
onSelect?.(option)
|
||||||
@@ -755,13 +749,11 @@ function OptionCard({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
testID={`report:option:${option.title}`}
|
testID={`report:option:${option.title}`}
|
||||||
label={_(
|
label={l({
|
||||||
msg({
|
message: `Create report for ${option.title}`,
|
||||||
message: `Create report for ${option.title}`,
|
comment:
|
||||||
comment:
|
'Accessibility label for button to create a moderation report for the selected option',
|
||||||
'Accessibility label for button to create a moderation report for the selected option',
|
})}
|
||||||
}),
|
|
||||||
)}
|
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
disabled={!onSelect}>
|
disabled={!onSelect}>
|
||||||
{({hovered, pressed}) => (
|
{({hovered, pressed}) => (
|
||||||
@@ -810,7 +802,7 @@ function LabelerCard({
|
|||||||
onSelect?: (option: AppBskyLabelerDefs.LabelerViewDetailed) => void
|
onSelect?: (option: AppBskyLabelerDefs.LabelerViewDetailed) => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
onSelect?.(labeler)
|
onSelect?.(labeler)
|
||||||
}, [onSelect, labeler])
|
}, [onSelect, labeler])
|
||||||
@@ -821,7 +813,7 @@ function LabelerCard({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
testID={`report:labeler:${labeler.creator.handle}`}
|
testID={`report:labeler:${labeler.creator.handle}`}
|
||||||
label={_(msg`Send report to ${title}`)}
|
label={l`Send report to ${title}`}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
disabled={!onSelect}>
|
disabled={!onSelect}>
|
||||||
{({hovered, pressed}) => (
|
{({hovered, pressed}) => (
|
||||||
|
|||||||
@@ -8,12 +8,17 @@ import {
|
|||||||
|
|
||||||
import type * as Dialog from '#/components/Dialog'
|
import type * as Dialog from '#/components/Dialog'
|
||||||
|
|
||||||
export type ReportSubjectConvo = {
|
export type ReportSubjectConvoMessage = {
|
||||||
view: 'convo' | 'message'
|
view: 'convo' | 'message'
|
||||||
convoId: string
|
convoId: string
|
||||||
message: ChatBskyConvoDefs.MessageView
|
message: ChatBskyConvoDefs.MessageView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ReportSubjectConvo = {
|
||||||
|
convoId: string
|
||||||
|
did: string
|
||||||
|
}
|
||||||
|
|
||||||
export type ReportSubject =
|
export type ReportSubject =
|
||||||
| $Typed<AppBskyActorDefs.ProfileViewBasic>
|
| $Typed<AppBskyActorDefs.ProfileViewBasic>
|
||||||
| $Typed<AppBskyActorDefs.ProfileView>
|
| $Typed<AppBskyActorDefs.ProfileView>
|
||||||
@@ -23,6 +28,7 @@ export type ReportSubject =
|
|||||||
| $Typed<AppBskyFeedDefs.GeneratorView>
|
| $Typed<AppBskyFeedDefs.GeneratorView>
|
||||||
| $Typed<AppBskyGraphDefs.StarterPackView>
|
| $Typed<AppBskyGraphDefs.StarterPackView>
|
||||||
| $Typed<AppBskyFeedDefs.PostView>
|
| $Typed<AppBskyFeedDefs.PostView>
|
||||||
|
| ReportSubjectConvoMessage
|
||||||
| ReportSubjectConvo
|
| ReportSubjectConvo
|
||||||
|
|
||||||
export type ParsedReportSubject =
|
export type ParsedReportSubject =
|
||||||
@@ -70,6 +76,9 @@ export type ParsedReportSubject =
|
|||||||
}
|
}
|
||||||
| ({
|
| ({
|
||||||
type: 'convoMessage'
|
type: 'convoMessage'
|
||||||
|
} & ReportSubjectConvoMessage)
|
||||||
|
| ({
|
||||||
|
type: 'convo'
|
||||||
} & ReportSubjectConvo)
|
} & ReportSubjectConvo)
|
||||||
|
|
||||||
export type ReportDialogProps = {
|
export type ReportDialogProps = {
|
||||||
|
|||||||
@@ -17,9 +17,16 @@ export function parseReportSubject(
|
|||||||
if (!subject) return
|
if (!subject) return
|
||||||
|
|
||||||
if ('convoId' in subject) {
|
if ('convoId' in subject) {
|
||||||
|
if ('message' in subject) {
|
||||||
|
return {
|
||||||
|
type: 'convoMessage',
|
||||||
|
...subject,
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
type: 'convoMessage',
|
type: 'convo',
|
||||||
...subject,
|
convoId: subject.convoId,
|
||||||
|
did: subject.did,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import {atoms as a, useTheme} from '#/alf'
|
|||||||
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
||||||
import {Button, type ButtonColor, ButtonIcon} from '#/components/Button'
|
import {Button, type ButtonColor, ButtonIcon} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {AfterReportConversationDialog} from '#/components/dms/AfterReportConversationDialog'
|
||||||
|
import {ReportConversationDialog} from '#/components/dms/ReportConversationDialog'
|
||||||
import {
|
import {
|
||||||
type ConvoWithDetails,
|
type ConvoWithDetails,
|
||||||
type GroupConvoMember,
|
type GroupConvoMember,
|
||||||
@@ -330,8 +332,7 @@ function SettingsHeader({
|
|||||||
const {joinLink} = convo.details
|
const {joinLink} = convo.details
|
||||||
const isJoinLinkEnabled = isOwner || joinLink?.enabledStatus === 'enabled'
|
const isJoinLinkEnabled = isOwner || joinLink?.enabledStatus === 'enabled'
|
||||||
|
|
||||||
// TODO Enable this once the feature is working end-to-end. -dsb
|
const reportSubjectDid = convo.primaryMember?.did
|
||||||
const isReportLinkEnabled = false
|
|
||||||
|
|
||||||
const {mutate: editGroupName, isPending: isEditingName} =
|
const {mutate: editGroupName, isPending: isEditingName} =
|
||||||
useEditGroupChatName(convo.view.id, {
|
useEditGroupChatName(convo.view.id, {
|
||||||
@@ -402,30 +403,8 @@ function SettingsHeader({
|
|||||||
const editNamePrompt = Prompt.usePromptControl()
|
const editNamePrompt = Prompt.usePromptControl()
|
||||||
const lockChatPrompt = Prompt.usePromptControl()
|
const lockChatPrompt = Prompt.usePromptControl()
|
||||||
const leaveChatPrompt = Prompt.usePromptControl()
|
const leaveChatPrompt = Prompt.usePromptControl()
|
||||||
|
const reportControl = Prompt.usePromptControl()
|
||||||
const handleToggleMute = () => {
|
const deleteControl = Prompt.usePromptControl()
|
||||||
muteConvo({mute: !convo.view.muted})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Need to implement this when the backend is ready. -dsb
|
|
||||||
const handleReportChat = () => {}
|
|
||||||
|
|
||||||
const handlePromptName = () => {
|
|
||||||
setNewGroupName(groupName)
|
|
||||||
editNamePrompt.open()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEditName = () => {
|
|
||||||
editGroupName({name: newGroupName})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleConfirmLock = () => {
|
|
||||||
lockConvo({lock: true})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUnlock = () => {
|
|
||||||
lockConvo({lock: false})
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdAt = new Date(convo.details.createdAt)
|
const createdAt = new Date(convo.details.createdAt)
|
||||||
|
|
||||||
@@ -486,7 +465,7 @@ function SettingsHeader({
|
|||||||
: l`Mute this group chat`
|
: l`Mute this group chat`
|
||||||
}
|
}
|
||||||
text={convo.view.muted ? l`Muted` : l`Mute`}
|
text={convo.view.muted ? l`Muted` : l`Mute`}
|
||||||
onPress={handleToggleMute}
|
onPress={() => muteConvo({mute: !convo.view.muted})}
|
||||||
/>
|
/>
|
||||||
{isOwner ? (
|
{isOwner ? (
|
||||||
<SettingsButton
|
<SettingsButton
|
||||||
@@ -494,7 +473,10 @@ function SettingsHeader({
|
|||||||
icon={EditIcon}
|
icon={EditIcon}
|
||||||
label={l`Edit this group chat’s name`}
|
label={l`Edit this group chat’s name`}
|
||||||
text={l`Edit name`}
|
text={l`Edit name`}
|
||||||
onPress={handlePromptName}
|
onPress={() => {
|
||||||
|
setNewGroupName(groupName)
|
||||||
|
editNamePrompt.open()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{isJoinLinkEnabled ? (
|
{isJoinLinkEnabled ? (
|
||||||
@@ -522,20 +504,22 @@ function SettingsHeader({
|
|||||||
}
|
}
|
||||||
text={lockStatus === 'locked' ? l`Locked` : l`Lock`}
|
text={lockStatus === 'locked' ? l`Locked` : l`Lock`}
|
||||||
onPress={
|
onPress={
|
||||||
lockStatus === 'locked' ? handleUnlock : lockChatPrompt.open
|
lockStatus === 'locked'
|
||||||
|
? () => lockConvo({lock: false})
|
||||||
|
: lockChatPrompt.open
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{!isOwner && isReportLinkEnabled && (
|
{!isOwner && reportSubjectDid ? (
|
||||||
<SettingsButton
|
<SettingsButton
|
||||||
disabled={!isReady}
|
disabled={!isReady}
|
||||||
icon={FlagIcon}
|
icon={FlagIcon}
|
||||||
label={l`Report this group chat`}
|
label={l`Report this group chat`}
|
||||||
text={l`Report`}
|
text={l`Report`}
|
||||||
onPress={handleReportChat}
|
onPress={reportControl.open}
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
{!isOwner && (
|
{!isOwner ? (
|
||||||
<SettingsButton
|
<SettingsButton
|
||||||
disabled={!isReady || isLeaving}
|
disabled={!isReady || isLeaving}
|
||||||
icon={ArrowBoxLeftIcon}
|
icon={ArrowBoxLeftIcon}
|
||||||
@@ -543,14 +527,14 @@ function SettingsHeader({
|
|||||||
text={l`Leave`}
|
text={l`Leave`}
|
||||||
onPress={leaveChatPrompt.open}
|
onPress={leaveChatPrompt.open}
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<EditNamePrompt
|
<EditNamePrompt
|
||||||
control={editNamePrompt}
|
control={editNamePrompt}
|
||||||
value={newGroupName}
|
value={newGroupName}
|
||||||
onChangeText={setNewGroupName}
|
onChangeText={setNewGroupName}
|
||||||
onConfirm={handleEditName}
|
onConfirm={() => editGroupName({name: newGroupName})}
|
||||||
/>
|
/>
|
||||||
{convo.primaryMember && (
|
{convo.primaryMember && (
|
||||||
<InviteLinkDialog
|
<InviteLinkDialog
|
||||||
@@ -561,12 +545,33 @@ function SettingsHeader({
|
|||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<LockChatPrompt control={lockChatPrompt} onConfirm={handleConfirmLock} />
|
<LockChatPrompt
|
||||||
|
control={lockChatPrompt}
|
||||||
|
onConfirm={() => lockConvo({lock: true})}
|
||||||
|
/>
|
||||||
<LeaveChatPrompt
|
<LeaveChatPrompt
|
||||||
control={leaveChatPrompt}
|
control={leaveChatPrompt}
|
||||||
groupName={groupName}
|
groupName={groupName}
|
||||||
onConfirm={leaveConvo}
|
onConfirm={leaveConvo}
|
||||||
/>
|
/>
|
||||||
|
{reportSubjectDid ? (
|
||||||
|
<>
|
||||||
|
<ReportConversationDialog
|
||||||
|
control={reportControl}
|
||||||
|
convoId={convo.view.id}
|
||||||
|
did={reportSubjectDid}
|
||||||
|
onAfterSubmit={deleteControl.open}
|
||||||
|
/>
|
||||||
|
<AfterReportConversationDialog
|
||||||
|
control={deleteControl}
|
||||||
|
currentScreen="conversation"
|
||||||
|
params={{
|
||||||
|
convoId: convo.view.id,
|
||||||
|
did: reportSubjectDid,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ export function RejectMenu({
|
|||||||
currentScreen={currentScreen}
|
currentScreen={currentScreen}
|
||||||
params={{
|
params={{
|
||||||
convoId: convo.id,
|
convoId: convo.id,
|
||||||
message: lastMessage,
|
did: lastMessage.sender.did,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user