Create mod inbox report details screen (#11641)
This commit is contained in:
+7
-1
@@ -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}
|
||||
|
||||
@@ -25,6 +25,7 @@ export type CommonNavigatorParams = {
|
||||
Moderation: undefined
|
||||
ModerationInbox: undefined
|
||||
ModerationInboxSettings: undefined
|
||||
ModerationInboxReportDetails: undefined
|
||||
ModerationModlists: undefined
|
||||
ModerationMutedAccounts: undefined
|
||||
ModerationBlockedAccounts: undefined
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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