Create moderation inbox menu item and feature gate (#11552)
This commit is contained in:
@@ -60,11 +60,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/Navigation.tsx": {
|
||||
"typescript/no-floating-promises": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/Splash.android.tsx": {
|
||||
"typescript/no-floating-promises": {
|
||||
"count": 1
|
||||
@@ -790,20 +785,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/screens/Moderation/index.tsx": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 2
|
||||
},
|
||||
"typescript/no-floating-promises": {
|
||||
"count": 1
|
||||
},
|
||||
"typescript/no-misused-promises": {
|
||||
"count": 1
|
||||
},
|
||||
"typescript/no-unsafe-member-access": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/screens/ModerationInteractionSettings/index.tsx": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 1
|
||||
|
||||
+7
-1
@@ -87,6 +87,7 @@ import {MessagesJoinRequestsScreen} from '#/screens/Messages/JoinRequests'
|
||||
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 {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings'
|
||||
import {NotificationsActivityListScreen} from '#/screens/Notifications/ActivityList'
|
||||
import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
|
||||
@@ -178,6 +179,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
getComponent={() => ModerationScreen}
|
||||
options={{title: title(msg`Moderation`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ModerationInbox"
|
||||
getComponent={() => ModerationInboxScreen}
|
||||
options={{title: title(msg`Moderation inbox`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ModerationModlists"
|
||||
getComponent={() => ModerationModlistsScreen}
|
||||
@@ -919,7 +925,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
// chat-removed-from-group, chat-join-request-rejected: the convo is
|
||||
// no longer accessible to the recipient, so just open the list.
|
||||
// @ts-expect-error nested navigators aren't typed -sfn
|
||||
navigate('MessagesTab', {screen: 'Messages'})
|
||||
void navigate('MessagesTab', {screen: 'Messages'})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -25,6 +25,7 @@ export enum Features {
|
||||
OnboardingInterestsRequiredEnable = 'onboarding:interests:required:enable',
|
||||
CanonicalPostNumberingEnable = 'canonical_post_numbering:enable',
|
||||
ContentVisibilitySettingsEnable = 'content_visibility_settings:enable',
|
||||
ModerationInboxEnable = 'moderation_inbox:enable',
|
||||
|
||||
// values
|
||||
TrendingDiscoverValues = 'trending_discover:values',
|
||||
|
||||
@@ -23,6 +23,7 @@ export type CommonNavigatorParams = {
|
||||
NotFound: undefined
|
||||
Lists: undefined
|
||||
Moderation: undefined
|
||||
ModerationInbox: undefined
|
||||
ModerationModlists: undefined
|
||||
ModerationMutedAccounts: undefined
|
||||
ModerationBlockedAccounts: undefined
|
||||
|
||||
+291
-281
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ export const router = new Router<AllNavigatableRoutes>({
|
||||
Lists: '/lists',
|
||||
// moderation
|
||||
Moderation: '/moderation',
|
||||
ModerationInbox: '/moderation/inbox',
|
||||
ModerationModlists: '/moderation/modlists',
|
||||
ModerationMutedAccounts: '/moderation/muted-accounts',
|
||||
ModerationBlockedAccounts: '/moderation/blocked-accounts',
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {Fragment, useCallback} from 'react'
|
||||
import {Linking, View} from 'react-native'
|
||||
import {LABELS} from '@bsky/sdk/moderation'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {getLabelingServiceTitle, isAppLabeler} from '#/lib/moderation'
|
||||
import {
|
||||
@@ -35,6 +33,7 @@ import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {EditBig_Stroke2_Corner2_Rounded as EditBig} from '#/components/icons/EditBig'
|
||||
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
||||
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
||||
import {Inbox_Stroke2_Corner2_Rounded as Inbox} from '#/components/icons/Inbox'
|
||||
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
||||
import * as LabelingService from '#/components/LabelingServiceCard'
|
||||
import * as Layout from '#/components/Layout'
|
||||
@@ -45,8 +44,11 @@ import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAgeAssurance} from '#/ageAssurance'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_IOS} from '#/env'
|
||||
|
||||
const UNREAD_NOTIFICATION_CAP = 100
|
||||
|
||||
function ErrorState({error}: {error: string}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
@@ -81,7 +83,7 @@ function ErrorState({error}: {error: string}) {
|
||||
export function ModerationScreen(
|
||||
_props: NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>,
|
||||
) {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const {
|
||||
isLoading: isPreferencesLoading,
|
||||
error: preferencesError,
|
||||
@@ -109,7 +111,7 @@ export function ModerationScreen(
|
||||
<ErrorState
|
||||
error={
|
||||
preferencesError?.toString() ||
|
||||
_(msg`Something went wrong, please try again.`)
|
||||
l`Something went wrong, please try again.`
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
@@ -123,12 +125,15 @@ export function ModerationScreen(
|
||||
function SubItem({
|
||||
title,
|
||||
icon: Icon,
|
||||
badge,
|
||||
style,
|
||||
}: ViewStyleProp & {
|
||||
title: string
|
||||
icon: React.ComponentType<SVGIconProps>
|
||||
badge?: number
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l, i18n} = useLingui()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -144,10 +149,38 @@ function SubItem({
|
||||
<Icon size="md" style={[t.atoms.text_contrast_medium]} />
|
||||
<Text style={[a.text_sm, a.font_semi_bold]}>{title}</Text>
|
||||
</View>
|
||||
<ChevronRight
|
||||
size="sm"
|
||||
style={[t.atoms.text_contrast_low, a.self_end, {paddingBottom: 2}]}
|
||||
/>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
||||
{badge ? (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_lg,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.px_sm,
|
||||
{
|
||||
minWidth: 24,
|
||||
height: 24,
|
||||
backgroundColor: t.palette.primary_500,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_xs,
|
||||
a.font_semi_bold,
|
||||
{color: t.palette.white, fontVariant: ['tabular-nums']},
|
||||
]}>
|
||||
{badge >= UNREAD_NOTIFICATION_CAP
|
||||
? l({
|
||||
message: `${i18n.number(UNREAD_NOTIFICATION_CAP - 1)}+`,
|
||||
comment:
|
||||
'Displayed when the number of notifications exceeds the cap – for example, 99+ notifications',
|
||||
})
|
||||
: i18n.number(badge)}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<ChevronRight size="sm" style={[t.atoms.text_contrast_low]} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -157,10 +190,14 @@ export function ModerationScreenInner({
|
||||
}: {
|
||||
preferences: UsePreferencesQueryResponse
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
||||
const ax = useAnalytics()
|
||||
const isModerationInboxEnabled = ax.features.enabled(
|
||||
ax.features.ModerationInboxEnable,
|
||||
)
|
||||
const {
|
||||
isLoading: isLabelersLoading,
|
||||
data: labelers,
|
||||
@@ -184,10 +221,11 @@ export function ModerationScreenInner({
|
||||
const handleCleanup = async () => {
|
||||
try {
|
||||
await removeLabelers({dids: unavailableDids})
|
||||
Toast.show(_(msg`Removed unavailable services`), {
|
||||
Toast.show(l`Removed unavailable services`, {
|
||||
type: 'success',
|
||||
})
|
||||
} catch (e: any) {
|
||||
} catch (error) {
|
||||
const e = error as Error
|
||||
logger.error('Failed to remove unavailable labelers', {
|
||||
safeMessage: e.message,
|
||||
})
|
||||
@@ -214,7 +252,8 @@ export function ModerationScreenInner({
|
||||
await setAdultContentPref({
|
||||
enabled: selected,
|
||||
})
|
||||
} catch (e: any) {
|
||||
} catch (error) {
|
||||
const e = error as Error
|
||||
logger.error(`Failed to set adult content pref`, {
|
||||
message: e.message,
|
||||
})
|
||||
@@ -234,7 +273,7 @@ export function ModerationScreenInner({
|
||||
your{' '}
|
||||
<InlineLinkText
|
||||
to="/settings/account"
|
||||
label={_(msg`Go to account settings`)}>
|
||||
label={l`Go to account settings`}>
|
||||
account settings
|
||||
</InlineLinkText>
|
||||
.
|
||||
@@ -242,7 +281,25 @@ export function ModerationScreenInner({
|
||||
</Admonition.Admonition>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{isModerationInboxEnabled && (
|
||||
<Link
|
||||
label={l`View your moderation inbox`}
|
||||
testID="moderationInboxBtn"
|
||||
to="/moderation/inbox"
|
||||
style={[a.mb_2xl, a.rounded_md, a.overflow_hidden]}>
|
||||
{state => (
|
||||
<SubItem
|
||||
title={l`Moderation inbox`}
|
||||
icon={Inbox}
|
||||
badge={3} // TODO This is a placeholder value. -dsb
|
||||
style={[
|
||||
t.atoms.bg_contrast_25,
|
||||
(state.hovered || state.pressed) && [t.atoms.bg_contrast_50],
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
@@ -252,7 +309,6 @@ export function ModerationScreenInner({
|
||||
]}>
|
||||
<Trans>Moderation tools</Trans>
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
@@ -261,12 +317,12 @@ export function ModerationScreenInner({
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<Link
|
||||
label={_(msg`View your default post interaction settings`)}
|
||||
label={l`View your default post interaction settings`}
|
||||
testID="interactionSettingsBtn"
|
||||
to="/moderation/interaction-settings">
|
||||
{state => (
|
||||
<SubItem
|
||||
title={_(msg`Interaction settings`)}
|
||||
title={l`Interaction settings`}
|
||||
icon={EditBig}
|
||||
style={[
|
||||
(state.hovered || state.pressed) && [t.atoms.bg_contrast_50],
|
||||
@@ -277,11 +333,11 @@ export function ModerationScreenInner({
|
||||
<Divider />
|
||||
<Button
|
||||
testID="mutedWordsBtn"
|
||||
label={_(msg`Open muted words and tags settings`)}
|
||||
label={l`Open muted words and tags settings`}
|
||||
onPress={() => mutedWordsDialogControl.open()}>
|
||||
{state => (
|
||||
<SubItem
|
||||
title={_(msg`Muted words & tags`)}
|
||||
title={l`Muted words & tags`}
|
||||
icon={Filter}
|
||||
style={[
|
||||
(state.hovered || state.pressed) && [t.atoms.bg_contrast_50],
|
||||
@@ -291,12 +347,12 @@ export function ModerationScreenInner({
|
||||
</Button>
|
||||
<Divider />
|
||||
<Link
|
||||
label={_(msg`View your moderation lists`)}
|
||||
label={l`View your moderation lists`}
|
||||
testID="moderationlistsBtn"
|
||||
to="/moderation/modlists">
|
||||
{state => (
|
||||
<SubItem
|
||||
title={_(msg`Moderation lists`)}
|
||||
title={l`Moderation lists`}
|
||||
icon={Group}
|
||||
style={[
|
||||
(state.hovered || state.pressed) && [t.atoms.bg_contrast_50],
|
||||
@@ -306,12 +362,12 @@ export function ModerationScreenInner({
|
||||
</Link>
|
||||
<Divider />
|
||||
<Link
|
||||
label={_(msg`View your muted accounts`)}
|
||||
label={l`View your muted accounts`}
|
||||
testID="mutedAccountsBtn"
|
||||
to="/moderation/muted-accounts">
|
||||
{state => (
|
||||
<SubItem
|
||||
title={_(msg`Muted accounts`)}
|
||||
title={l`Muted accounts`}
|
||||
icon={Person}
|
||||
style={[
|
||||
(state.hovered || state.pressed) && [t.atoms.bg_contrast_50],
|
||||
@@ -321,12 +377,12 @@ export function ModerationScreenInner({
|
||||
</Link>
|
||||
<Divider />
|
||||
<Link
|
||||
label={_(msg`View your blocked accounts`)}
|
||||
label={l`View your blocked accounts`}
|
||||
testID="blockedAccountsBtn"
|
||||
to="/moderation/blocked-accounts">
|
||||
{state => (
|
||||
<SubItem
|
||||
title={_(msg`Blocked accounts`)}
|
||||
title={l`Blocked accounts`}
|
||||
icon={CircleBanSign}
|
||||
style={[
|
||||
(state.hovered || state.pressed) && [t.atoms.bg_contrast_50],
|
||||
@@ -336,12 +392,12 @@ export function ModerationScreenInner({
|
||||
</Link>
|
||||
<Divider />
|
||||
<Link
|
||||
label={_(msg`Manage verification settings`)}
|
||||
label={l`Manage verification settings`}
|
||||
testID="verificationSettingsBtn"
|
||||
to="/moderation/verification-settings">
|
||||
{state => (
|
||||
<SubItem
|
||||
title={_(msg`Verification settings`)}
|
||||
title={l`Verification settings`}
|
||||
icon={CircleCheck}
|
||||
style={[
|
||||
(state.hovered || state.pressed) && [t.atoms.bg_contrast_50],
|
||||
@@ -350,7 +406,6 @@ export function ModerationScreenInner({
|
||||
)}
|
||||
</Link>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
a.pt_2xl,
|
||||
@@ -361,11 +416,9 @@ export function ModerationScreenInner({
|
||||
]}>
|
||||
<Trans>Content filters</Trans>
|
||||
</Text>
|
||||
|
||||
<AgeAssuranceAdmonition style={[a.pb_md]}>
|
||||
{aaCopy.notice}
|
||||
</AgeAssuranceAdmonition>
|
||||
|
||||
<View style={[a.gap_md]}>
|
||||
<View
|
||||
style={[
|
||||
@@ -389,11 +442,13 @@ export function ModerationScreenInner({
|
||||
<Trans>Enable adult content</Trans>
|
||||
</Text>
|
||||
<Toggle.Item
|
||||
label={_(msg`Toggle to enable or disable adult content`)}
|
||||
label={l`Toggle to enable or disable adult content`}
|
||||
disabled={adultContentUIDisabled}
|
||||
name="adultContent"
|
||||
value={adultContentEnabled}
|
||||
onChange={onToggleAdultContentEnabled}>
|
||||
onChange={(selected: boolean) =>
|
||||
void onToggleAdultContentEnabled(selected)
|
||||
}>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Text style={[t.atoms.text_contrast_medium]}>
|
||||
{adultContentEnabled ? (
|
||||
@@ -412,11 +467,11 @@ export function ModerationScreenInner({
|
||||
<Trans>
|
||||
Adult content can only be enabled via the Web at{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`The Bluesky web application`)}
|
||||
label={l`The Bluesky web application`}
|
||||
to=""
|
||||
onPress={evt => {
|
||||
evt.preventDefault()
|
||||
Linking.openURL('https://bsky.app/')
|
||||
void Linking.openURL('https://bsky.app/')
|
||||
return false
|
||||
}}>
|
||||
bsky.app
|
||||
@@ -445,7 +500,6 @@ export function ModerationScreenInner({
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
@@ -456,7 +510,6 @@ export function ModerationScreenInner({
|
||||
]}>
|
||||
<Trans>Advanced</Trans>
|
||||
</Text>
|
||||
|
||||
{unavailableDids.length > 0 && (
|
||||
<Admonition.Outer type="tip" style={[a.mb_md]}>
|
||||
<Admonition.Row>
|
||||
@@ -470,7 +523,7 @@ export function ModerationScreenInner({
|
||||
</Admonition.Content>
|
||||
<Admonition.Button
|
||||
color="primary_subtle"
|
||||
label={_(msg`Remove unavailable moderation services`)}
|
||||
label={l`Remove unavailable moderation services`}
|
||||
onPress={handleCleanup}
|
||||
disabled={isRemovingLabelers}>
|
||||
<ButtonText>
|
||||
@@ -481,7 +534,6 @@ export function ModerationScreenInner({
|
||||
</Admonition.Row>
|
||||
</Admonition.Outer>
|
||||
)}
|
||||
|
||||
{isLabelersLoading ? (
|
||||
<View style={[a.w_full, a.align_center, a.p_lg]}>
|
||||
<Loader size="xl" />
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {NotFoundScreen} from '#/view/screens/NotFound'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
|
||||
export function ModerationInboxScreen() {
|
||||
const ax = useAnalytics()
|
||||
const isEnabled = ax.features.enabled(ax.features.ModerationInboxEnable)
|
||||
|
||||
if (!isEnabled) {
|
||||
return <NotFoundScreen />
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="moderationInboxScreen">
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Moderation inbox</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user