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