Create mod inbox report details screen (#11641)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {Fragment, useCallback} from 'react'
|
||||
import {Linking, View} from 'react-native'
|
||||
import {LABELS} from '@bsky/sdk/moderation'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {getLabelingServiceTitle, isAppLabeler} from '#/lib/moderation'
|
||||
@@ -171,11 +172,14 @@ function SubItem({
|
||||
]}>
|
||||
{badge >= UNREAD_NOTIFICATION_CAP
|
||||
? l({
|
||||
message: `${i18n.number(UNREAD_NOTIFICATION_CAP - 1)}+`,
|
||||
message: `${i18n.number(UNREAD_NOTIFICATION_CAP - 1)}+ updates`,
|
||||
comment:
|
||||
'Displayed when the number of notifications exceeds the cap – for example, 99+ notifications',
|
||||
})
|
||||
: i18n.number(badge)}
|
||||
: plural(badge, {
|
||||
one: '# update',
|
||||
other: '# updates',
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import {View} from 'react-native'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {NotFoundScreen} from '#/view/screens/NotFound'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {ActionSummaryText} from './components/ActionSummaryText'
|
||||
import {ContentBlock} from './components/ContentBlock'
|
||||
import {SubjectPreview} from './components/SubjectPreview'
|
||||
import {Timeline} from './components/Timeline'
|
||||
|
||||
export function ModerationInboxReportDetailsScreen() {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const ax = useAnalytics()
|
||||
|
||||
const isEnabled = ax.features.enabled(ax.features.ModerationInboxEnable)
|
||||
|
||||
if (!isEnabled) {
|
||||
return <NotFoundScreen />
|
||||
}
|
||||
|
||||
// TODO This is a placeholder value. -dsb
|
||||
const note = 'Lorem ipsum dolor sit amet.'
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="moderationInboxReportDetailsScreen">
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content align="left">
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Your report</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
<Layout.Header.SubtitleText>
|
||||
<Trans>Bluesky Moderation Service</Trans>
|
||||
</Layout.Header.SubtitleText>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
|
||||
<Layout.Content>
|
||||
<View style={[a.p_lg, a.gap_lg]}>
|
||||
<ActionSummaryText header={l`The post was restored`}>
|
||||
<Trans>
|
||||
The post you reported has been restored after another review. If
|
||||
you’d rather not see it, you can mute or block the account. Read
|
||||
our{' '}
|
||||
<InlineLinkText
|
||||
to="https://bsky.social/about/support/community-guidelines"
|
||||
label={l`Read our Community Guidelines`}
|
||||
style={[a.text_md]}>
|
||||
Community Guidelines
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
</ActionSummaryText>
|
||||
|
||||
<ContentBlock header={l`What you reported`}>
|
||||
<SubjectPreview />
|
||||
|
||||
<View style={[a.gap_2xs]}>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Reason:</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_md]}>
|
||||
<Trans>Spam</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={[a.gap_2xs]}>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Your note:</Trans>
|
||||
</Text>
|
||||
<Text emoji style={[a.text_md]}>
|
||||
<Trans>“{note}”</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</ContentBlock>
|
||||
|
||||
<Timeline
|
||||
items={[
|
||||
{
|
||||
title: l`Report submitted`,
|
||||
},
|
||||
{
|
||||
title: l`Post removed`,
|
||||
},
|
||||
{
|
||||
title: l`Post restored`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {H1, Text} from '#/components/Typography'
|
||||
|
||||
export function ActionSummaryText({
|
||||
header,
|
||||
children,
|
||||
}: React.PropsWithChildren<{header: string}>) {
|
||||
const t = useTheme()
|
||||
|
||||
// Cleaner to call this ActionSummaryText and suppress lint in one location vs every call site.
|
||||
// oxlint-disable-next-line bsky-internal/avoid-unwrapped-text
|
||||
return (
|
||||
<View style={[a.gap_xs]}>
|
||||
<H1 accessibilityRole="header" style={[a.text_xl, a.font_semi_bold]}>
|
||||
{header}
|
||||
</H1>
|
||||
<Text style={[a.text_md, t.atoms.text_contrast_high]}>{children}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {H2} from '#/components/Typography'
|
||||
|
||||
export function ContentBlock({
|
||||
header,
|
||||
children,
|
||||
}: React.PropsWithChildren<{header: string}>) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.p_md,
|
||||
a.gap_md,
|
||||
a.rounded_md,
|
||||
a.border,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<H2 accessibilityRole="header" style={[a.text_md, a.font_semi_bold]}>
|
||||
{header}
|
||||
</H2>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -4,26 +4,36 @@ import {isBefore, subYears} from 'date-fns'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
|
||||
import {Link} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function ReportRow({
|
||||
subject,
|
||||
action,
|
||||
date,
|
||||
to,
|
||||
unread,
|
||||
}: {
|
||||
subject: string
|
||||
action: string
|
||||
date: Date
|
||||
to: React.ComponentProps<typeof Link>['to']
|
||||
unread?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {i18n} = useLingui()
|
||||
const {i18n, t: l} = useLingui()
|
||||
|
||||
const shouldShowYear = isBefore(date, subYears(new Date(), 1))
|
||||
|
||||
return (
|
||||
<View
|
||||
<Link
|
||||
label={l`View report details: ${subject}`}
|
||||
to={to}
|
||||
hoverStyle={
|
||||
unread
|
||||
? {backgroundColor: t.palette.primary_50}
|
||||
: t.atoms.bg_contrast_25
|
||||
}
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.w_full,
|
||||
@@ -68,6 +78,6 @@ export function ReportRow({
|
||||
) : undefined}
|
||||
<ChevronRightIcon size="md" style={[t.atoms.text_contrast_medium]} />
|
||||
</View>
|
||||
</View>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import {View} from 'react-native'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
// TODO These are placeholder values. -dsb
|
||||
const POSTED_AT = new Date(2026, 7, 8)
|
||||
|
||||
export function SubjectPreview() {
|
||||
const t = useTheme()
|
||||
const {i18n} = useLingui()
|
||||
|
||||
// TODO These are placeholder values. -dsb
|
||||
const handle = '@deleteme01.bsky.social'
|
||||
const post = '@deleteme01.bsky.social lorem ipsum dolor sit amet…'
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.p_lg,
|
||||
a.gap_sm,
|
||||
a.rounded_md,
|
||||
a.border,
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<Text emoji style={[a.text_md, a.font_semi_bold]}>
|
||||
<Trans>Reply to {handle}</Trans>
|
||||
</Text>
|
||||
<Text emoji style={[a.text_md]}>
|
||||
<Trans>“{post}”</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Posted{' '}
|
||||
{i18n.date(POSTED_AT, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Trans>
|
||||
</Text>
|
||||
<View style={[a.align_start]}>
|
||||
{/* TODO This is a placeholder for a button. -dsb */}
|
||||
<View
|
||||
accessibilityElementsHidden
|
||||
importantForAccessibility="no-hide-descendants"
|
||||
style={[a.py_sm, a.px_md, a.rounded_full, t.atoms.bg_contrast_50]}>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_high]}>
|
||||
<Trans>Open post</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {View} from 'react-native'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {ContentBlock} from './ContentBlock'
|
||||
import {TimelineItem} from './TimelineItem'
|
||||
|
||||
export function Timeline({items}: {items: {title: string; date?: Date}[]}) {
|
||||
const {i18n, t: l} = useLingui()
|
||||
|
||||
return (
|
||||
<ContentBlock header={l`Timeline`}>
|
||||
<View>
|
||||
{items.map(({title, date}, index) => (
|
||||
<TimelineItem
|
||||
key={index}
|
||||
title={title}
|
||||
date={
|
||||
date
|
||||
? i18n.date(date, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
last={index + 1 === items.length}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</ContentBlock>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function TimelineItem({
|
||||
title,
|
||||
date,
|
||||
last,
|
||||
}: {
|
||||
title: string
|
||||
date?: string
|
||||
last?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View style={[a.flex_row, {gap: 10}]}>
|
||||
<View style={[a.align_center]}>
|
||||
{last ? (
|
||||
<View
|
||||
style={[
|
||||
a.mt_xs,
|
||||
a.rounded_full,
|
||||
{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderColor: t.palette.primary_500,
|
||||
borderWidth: 2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={[
|
||||
a.mt_xs,
|
||||
a.rounded_full,
|
||||
{
|
||||
width: 8,
|
||||
height: 8,
|
||||
backgroundColor: t.palette.contrast_300,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{last ? undefined : (
|
||||
<View
|
||||
style={[
|
||||
a.mt_xs,
|
||||
a.flex_1,
|
||||
a.rounded_full,
|
||||
{
|
||||
width: 2,
|
||||
backgroundColor: t.palette.contrast_100,
|
||||
minHeight: 12,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View style={[a.flex_1]}>
|
||||
<Text style={[a.text_sm, a.font_medium]}>{title}</Text>
|
||||
{date ? (
|
||||
<Text
|
||||
style={[!last && a.mb_md, a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
{date}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -115,6 +115,7 @@ function YourReports() {
|
||||
})}
|
||||
action={l`Awaiting review`}
|
||||
date={new Date()}
|
||||
to="/moderation/inbox/report/details"
|
||||
unread
|
||||
/>
|
||||
<ReportRow
|
||||
@@ -124,6 +125,7 @@ function YourReports() {
|
||||
})}
|
||||
action={l`No action taken`}
|
||||
date={new Date()}
|
||||
to="/moderation/inbox/report/details"
|
||||
unread
|
||||
/>
|
||||
<ReportRow
|
||||
@@ -133,6 +135,7 @@ function YourReports() {
|
||||
})}
|
||||
action={l`Message deleted`}
|
||||
date={new Date()}
|
||||
to="/moderation/inbox/report/details"
|
||||
/>
|
||||
</Layout.Center>
|
||||
)
|
||||
@@ -194,6 +197,7 @@ function YourAccount() {
|
||||
})}
|
||||
action={l`Violates community guideline: ${guideline}`}
|
||||
date={new Date()}
|
||||
to="/moderation/inbox/report/details"
|
||||
unread
|
||||
/>
|
||||
<ReportRow
|
||||
@@ -203,6 +207,7 @@ function YourAccount() {
|
||||
})}
|
||||
action={l`“${label}” – shown behind a warning`}
|
||||
date={new Date()}
|
||||
to="/moderation/inbox/report/details"
|
||||
/>
|
||||
<ReportRow
|
||||
subject={l({
|
||||
@@ -211,6 +216,7 @@ function YourAccount() {
|
||||
})}
|
||||
action={l`Ban evasion – ${duration}, now expired`}
|
||||
date={new Date()}
|
||||
to="/moderation/inbox/report/details"
|
||||
/>
|
||||
</Layout.Center>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user