Create mod inbox report details screen (#11641)

This commit is contained in:
DS Boyce
2026-09-03 10:20:46 -07:00
committed by GitHub
parent 3e8a48989a
commit 23d8fcf5ea
12 changed files with 347 additions and 6 deletions
+7 -1
View File
@@ -80,6 +80,7 @@ import {FindContactsFlowScreen} from '#/screens/FindContactsFlowScreen'
import HashtagScreen from '#/screens/Hashtag'
import {LogScreen} from '#/screens/Log'
import {MessagesScreen} from '#/screens/Messages/ChatList'
import {renderMessagesSplitViewLayout} from '#/screens/Messages/components/splitView/MessagesSplitViewLayout'
import {MessagesConversationScreen} from '#/screens/Messages/Conversation'
import {MessagesConversationSettingsScreen} from '#/screens/Messages/ConversationSettings'
import {MessagesInboxScreen} from '#/screens/Messages/Inbox'
@@ -88,6 +89,7 @@ import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
import {ModerationScreen} from '#/screens/Moderation'
import {Screen as ModerationVerificationSettings} from '#/screens/Moderation/VerificationSettings'
import {ModerationInboxScreen} from '#/screens/ModerationInbox'
import {ModerationInboxReportDetailsScreen} from '#/screens/ModerationInbox/Details'
import {ModerationInboxSettingsScreen} from '#/screens/ModerationInbox/Settings'
import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings'
import {NotificationsActivityListScreen} from '#/screens/Notifications/ActivityList'
@@ -141,7 +143,6 @@ import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
import {InviteScannerScreen} from '#/features/inviteFriends'
import {router} from '#/routes'
import {Referrer} from '../modules/expo-bluesky-swiss-army'
import {renderMessagesSplitViewLayout} from './screens/Messages/components/splitView/MessagesSplitViewLayout'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@@ -190,6 +191,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
getComponent={() => ModerationInboxSettingsScreen}
options={{title: title(msg`Mod inbox settings`), requireAuth: true}}
/>
<Stack.Screen
name="ModerationInboxReportDetails"
getComponent={() => ModerationInboxReportDetailsScreen}
options={{title: title(msg`Your report`), requireAuth: true}}
/>
<Stack.Screen
name="ModerationModlists"
getComponent={() => ModerationModlistsScreen}
+1
View File
@@ -25,6 +25,7 @@ export type CommonNavigatorParams = {
Moderation: undefined
ModerationInbox: undefined
ModerationInboxSettings: undefined
ModerationInboxReportDetails: undefined
ModerationModlists: undefined
ModerationMutedAccounts: undefined
ModerationBlockedAccounts: undefined
+1
View File
@@ -19,6 +19,7 @@ export const router = new Router<AllNavigatableRoutes>({
Moderation: '/moderation',
ModerationInbox: '/moderation/inbox',
ModerationInboxSettings: '/moderation/inbox/settings',
ModerationInboxReportDetails: '/moderation/inbox/report/details',
ModerationModlists: '/moderation/modlists',
ModerationMutedAccounts: '/moderation/muted-accounts',
ModerationBlockedAccounts: '/moderation/blocked-accounts',
+6 -2
View File
@@ -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}
+100
View File
@@ -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
youd 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>
)
}
+6
View File
@@ -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>
)