Add ability to report a group chat (#10485)
This commit is contained in:
@@ -55,7 +55,9 @@ export function useSubmitReportMutation() {
|
||||
let report:
|
||||
| ComAtprotoModerationCreateReport.InputSchema
|
||||
| (Omit<ComAtprotoModerationCreateReport.InputSchema, 'subject'> & {
|
||||
subject: $Typed<ChatBskyConvoDefs.MessageRef>
|
||||
subject:
|
||||
| $Typed<ChatBskyConvoDefs.MessageRef>
|
||||
| $Typed<ChatBskyConvoDefs.ConvoRef>
|
||||
})
|
||||
|
||||
switch (subject.type) {
|
||||
@@ -99,6 +101,18 @@ export function useSubmitReportMutation() {
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'convo': {
|
||||
report = {
|
||||
reasonType,
|
||||
reason: state.details,
|
||||
subject: {
|
||||
$type: 'chat.bsky.convo.defs#convoRef',
|
||||
convoId: subject.convoId,
|
||||
did: subject.did,
|
||||
},
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
|
||||
@@ -120,4 +120,4 @@ export const BSKY_LABELER_ONLY_REPORT_REASONS: Set<OzoneReportDefs.ReasonType> =
|
||||
* moderation service.
|
||||
*/
|
||||
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])
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@ import {
|
||||
} from 'react'
|
||||
import {Pressable, type ScrollView, View} from 'react-native'
|
||||
import {type AppBskyLabelerDefs, BSKY_LABELER_DID} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {wait} from '#/lib/async/wait'
|
||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||
@@ -93,9 +91,9 @@ export function ReportDialog(
|
||||
* developer, but nevertheless we should have a graceful fallback.
|
||||
*/
|
||||
function Invalid() {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
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]}>
|
||||
<Trans>Invalid report subject</Trans>
|
||||
</Text>
|
||||
@@ -114,7 +112,7 @@ function Inner(props: ReportDialogProps) {
|
||||
const ax = useAnalytics()
|
||||
const logger = ax.logger.useChild(ax.logger.Context.ReportDialog)
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const ref = useRef<ScrollView>(null)
|
||||
const {
|
||||
data: allLabelers,
|
||||
@@ -131,8 +129,8 @@ function Inner(props: ReportDialogProps) {
|
||||
* Submission handling
|
||||
*/
|
||||
const {mutateAsync: submitReport} = useSubmitReportMutation()
|
||||
const [isPending, setPending] = useState(false)
|
||||
const [isSuccess, setSuccess] = useState(false)
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const [isSuccess, setIsSuccess] = useState(false)
|
||||
|
||||
// some reasons ONLY go to Bluesky
|
||||
const isBskyOnlyReason = state?.selectedOption?.reason
|
||||
@@ -154,7 +152,10 @@ function Inner(props: ReportDialogProps) {
|
||||
if (subjectTypes === undefined) return true
|
||||
if (props.subject.type === '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')
|
||||
} else {
|
||||
return subjectTypes.includes('record')
|
||||
@@ -164,7 +165,11 @@ 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 === 'convoMessage') return true
|
||||
if (
|
||||
props.subject.type === 'convoMessage' ||
|
||||
props.subject.type === 'convo'
|
||||
)
|
||||
return true
|
||||
return collections.includes(props.subject.nsid)
|
||||
})
|
||||
.filter(l => {
|
||||
@@ -208,7 +213,7 @@ function Inner(props: ReportDialogProps) {
|
||||
logger.info('submitting')
|
||||
|
||||
try {
|
||||
setPending(true)
|
||||
setIsPending(true)
|
||||
// wait at least 1s, make it feel substantial
|
||||
await wait(
|
||||
1e3,
|
||||
@@ -217,7 +222,7 @@ function Inner(props: ReportDialogProps) {
|
||||
state,
|
||||
}),
|
||||
)
|
||||
setSuccess(true)
|
||||
setIsSuccess(true)
|
||||
ax.metric('reportDialog:success', {
|
||||
reason: state.selectedOption?.reason ?? '',
|
||||
labeler: state.selectedLabeler?.creator.handle ?? '',
|
||||
@@ -229,29 +234,20 @@ function Inner(props: ReportDialogProps) {
|
||||
props.onAfterSubmit?.()
|
||||
})
|
||||
}, 1e3)
|
||||
} catch (e: any) {
|
||||
} catch (err) {
|
||||
const e = err as Error
|
||||
ax.metric('reportDialog:failure', {})
|
||||
logger.error(e, {
|
||||
source: 'ReportDialog',
|
||||
})
|
||||
dispatch({
|
||||
type: 'setError',
|
||||
error: _(msg`Something went wrong. Please try again.`),
|
||||
error: l`Something went wrong. Please try again.`,
|
||||
})
|
||||
} finally {
|
||||
setPending(false)
|
||||
setIsPending(false)
|
||||
}
|
||||
}, [
|
||||
_,
|
||||
submitReport,
|
||||
state,
|
||||
dispatch,
|
||||
props.subject,
|
||||
props.control,
|
||||
props.onAfterSubmit,
|
||||
setPending,
|
||||
setSuccess,
|
||||
])
|
||||
}, [logger, submitReport, props, state, ax, l])
|
||||
|
||||
useCallOnce(() => {
|
||||
ax.metric('reportDialog:open', {
|
||||
@@ -262,7 +258,7 @@ function Inner(props: ReportDialogProps) {
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
testID="report:dialog"
|
||||
label={_(msg`Report dialog`)}
|
||||
label={l`Report dialog`}
|
||||
ref={ref}
|
||||
style={[a.w_full, {maxWidth: 500}]}>
|
||||
<View style={[a.gap_2xl, IS_NATIVE && a.pt_md]}>
|
||||
@@ -293,8 +289,8 @@ function Inner(props: ReportDialogProps) {
|
||||
</Admonition.Content>
|
||||
<Admonition.Button
|
||||
color="negative_subtle"
|
||||
label={_(msg`Retry loading report options`)}
|
||||
onPress={() => refetchLabelers()}>
|
||||
label={l`Retry loading report options`}
|
||||
onPress={() => void refetchLabelers()}>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
@@ -311,7 +307,7 @@ function Inner(props: ReportDialogProps) {
|
||||
</View>
|
||||
<Button
|
||||
testID="report:clearCategory"
|
||||
label={_(msg`Change report category`)}
|
||||
label={l`Change report category`}
|
||||
size="tiny"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
@@ -341,9 +337,7 @@ function Inner(props: ReportDialogProps) {
|
||||
{['post', 'account'].includes(props.subject.type) && (
|
||||
<Link
|
||||
to={SUPPORT_PAGE}
|
||||
label={_(
|
||||
msg`Need to report a copyright violation, legal request, or regulatory compliance issue?`,
|
||||
)}>
|
||||
label={l`Need to report a copyright violation, legal request, or regulatory compliance issue?`}>
|
||||
{({hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
@@ -381,7 +375,7 @@ function Inner(props: ReportDialogProps) {
|
||||
<StepOuter>
|
||||
<StepTitle
|
||||
index={2}
|
||||
title={_(msg`Select a reason`)}
|
||||
title={l`Select a reason`}
|
||||
activeIndex1={state.activeStepIndex1}
|
||||
/>
|
||||
{state.selectedOption ? (
|
||||
@@ -391,7 +385,7 @@ function Inner(props: ReportDialogProps) {
|
||||
</View>
|
||||
<Button
|
||||
testID="report:clearReportOption"
|
||||
label={_(msg`Change report reason`)}
|
||||
label={l`Change report reason`}
|
||||
size="tiny"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
@@ -431,7 +425,7 @@ function Inner(props: ReportDialogProps) {
|
||||
<StepOuter>
|
||||
<StepTitle
|
||||
index={3}
|
||||
title={_(msg`Select moderation service`)}
|
||||
title={l`Select moderation service`}
|
||||
activeIndex1={state.activeStepIndex1}
|
||||
/>
|
||||
{state.activeStepIndex1 >= 3 && (
|
||||
@@ -446,7 +440,7 @@ function Inner(props: ReportDialogProps) {
|
||||
<LabelerCard labeler={state.selectedLabeler} />
|
||||
</View>
|
||||
<Button
|
||||
label={_(msg`Change moderation service`)}
|
||||
label={l`Change moderation service`}
|
||||
size="tiny"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
@@ -509,7 +503,7 @@ function Inner(props: ReportDialogProps) {
|
||||
<StepOuter>
|
||||
<StepTitle
|
||||
index={isAlwaysBskyLabeler ? 3 : 4}
|
||||
title={_(msg`Submit report`)}
|
||||
title={l`Submit report`}
|
||||
activeIndex1={
|
||||
isAlwaysBskyLabeler
|
||||
? state.activeStepIndex1 - 1
|
||||
@@ -529,7 +523,7 @@ function Inner(props: ReportDialogProps) {
|
||||
</Trans>{' '}
|
||||
{!state.detailsOpen ? (
|
||||
<InlineLinkText
|
||||
label={_(msg`Add more details (optional)`)}
|
||||
label={l`Add more details (optional)`}
|
||||
{...createStaticClick(() => {
|
||||
dispatch({type: 'showDetails'})
|
||||
})}>
|
||||
@@ -547,7 +541,7 @@ function Inner(props: ReportDialogProps) {
|
||||
onChangeText={details => {
|
||||
dispatch({type: 'setDetails', details})
|
||||
}}
|
||||
label={_(msg`Additional details (limit 300 characters)`)}
|
||||
label={l`Additional details (limit 300 characters)`}
|
||||
style={{paddingRight: 60}}
|
||||
numberOfLines={4}
|
||||
/>
|
||||
@@ -570,12 +564,12 @@ function Inner(props: ReportDialogProps) {
|
||||
</View>
|
||||
<Button
|
||||
testID="report:submit"
|
||||
label={_(msg`Submit report`)}
|
||||
label={l`Submit report`}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
disabled={isPending || isSuccess}
|
||||
onPress={onSubmit}>
|
||||
onPress={() => void onSubmit()}>
|
||||
<ButtonText>
|
||||
<Trans>Submit report</Trans>
|
||||
</ButtonText>
|
||||
@@ -702,7 +696,7 @@ function CategoryCard({
|
||||
onSelect?: (option: ReportCategoryConfig) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const gutters = useGutters(['compact'])
|
||||
const onPress = useCallback(() => {
|
||||
onSelect?.(option)
|
||||
@@ -710,7 +704,7 @@ function CategoryCard({
|
||||
return (
|
||||
<Button
|
||||
testID={`report:category:${option.title}`}
|
||||
label={_(msg`Create report for ${option.title}`)}
|
||||
label={l`Create report for ${option.title}`}
|
||||
onPress={onPress}
|
||||
disabled={!onSelect}>
|
||||
{({hovered, pressed}) => (
|
||||
@@ -747,7 +741,7 @@ function OptionCard({
|
||||
onSelect?: (option: ReportOption) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const gutters = useGutters(['compact'])
|
||||
const onPress = useCallback(() => {
|
||||
onSelect?.(option)
|
||||
@@ -755,13 +749,11 @@ function OptionCard({
|
||||
return (
|
||||
<Button
|
||||
testID={`report:option:${option.title}`}
|
||||
label={_(
|
||||
msg({
|
||||
message: `Create report for ${option.title}`,
|
||||
comment:
|
||||
'Accessibility label for button to create a moderation report for the selected option',
|
||||
}),
|
||||
)}
|
||||
label={l({
|
||||
message: `Create report for ${option.title}`,
|
||||
comment:
|
||||
'Accessibility label for button to create a moderation report for the selected option',
|
||||
})}
|
||||
onPress={onPress}
|
||||
disabled={!onSelect}>
|
||||
{({hovered, pressed}) => (
|
||||
@@ -810,7 +802,7 @@ function LabelerCard({
|
||||
onSelect?: (option: AppBskyLabelerDefs.LabelerViewDetailed) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const onPress = useCallback(() => {
|
||||
onSelect?.(labeler)
|
||||
}, [onSelect, labeler])
|
||||
@@ -821,7 +813,7 @@ function LabelerCard({
|
||||
return (
|
||||
<Button
|
||||
testID={`report:labeler:${labeler.creator.handle}`}
|
||||
label={_(msg`Send report to ${title}`)}
|
||||
label={l`Send report to ${title}`}
|
||||
onPress={onPress}
|
||||
disabled={!onSelect}>
|
||||
{({hovered, pressed}) => (
|
||||
|
||||
@@ -8,12 +8,17 @@ import {
|
||||
|
||||
import type * as Dialog from '#/components/Dialog'
|
||||
|
||||
export type ReportSubjectConvo = {
|
||||
export type ReportSubjectConvoMessage = {
|
||||
view: 'convo' | 'message'
|
||||
convoId: string
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
}
|
||||
|
||||
export type ReportSubjectConvo = {
|
||||
convoId: string
|
||||
did: string
|
||||
}
|
||||
|
||||
export type ReportSubject =
|
||||
| $Typed<AppBskyActorDefs.ProfileViewBasic>
|
||||
| $Typed<AppBskyActorDefs.ProfileView>
|
||||
@@ -23,6 +28,7 @@ export type ReportSubject =
|
||||
| $Typed<AppBskyFeedDefs.GeneratorView>
|
||||
| $Typed<AppBskyGraphDefs.StarterPackView>
|
||||
| $Typed<AppBskyFeedDefs.PostView>
|
||||
| ReportSubjectConvoMessage
|
||||
| ReportSubjectConvo
|
||||
|
||||
export type ParsedReportSubject =
|
||||
@@ -70,6 +76,9 @@ export type ParsedReportSubject =
|
||||
}
|
||||
| ({
|
||||
type: 'convoMessage'
|
||||
} & ReportSubjectConvoMessage)
|
||||
| ({
|
||||
type: 'convo'
|
||||
} & ReportSubjectConvo)
|
||||
|
||||
export type ReportDialogProps = {
|
||||
|
||||
@@ -17,9 +17,16 @@ export function parseReportSubject(
|
||||
if (!subject) return
|
||||
|
||||
if ('convoId' in subject) {
|
||||
if ('message' in subject) {
|
||||
return {
|
||||
type: 'convoMessage',
|
||||
...subject,
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'convoMessage',
|
||||
...subject,
|
||||
type: 'convo',
|
||||
convoId: subject.convoId,
|
||||
did: subject.did,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user