Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators

This commit is contained in:
Paul Frazee
2024-02-15 16:20:33 -08:00
9 changed files with 304 additions and 215 deletions
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8 8V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-5v5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h5Zm1 8a1 1 0 0 1-1-1v-5H4v10h10v-4H9Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 286 B

+10 -1
View File
@@ -171,11 +171,18 @@ export type LinkProps = Omit<BaseLinkProps, 'warnOnMismatchingTextChild'> &
* Intended to behave as a web anchor tag. For more complex routing, use a
* `Button`.
*/
export function Link({children, to, action = 'push', ...rest}: LinkProps) {
export function Link({
children,
to,
action = 'push',
onPress: outerOnPress,
...rest
}: LinkProps) {
const {href, isExternal, onPress} = useLink({
to,
displayText: typeof children === 'string' ? children : '',
action,
onPress: outerOnPress,
})
return (
@@ -218,6 +225,7 @@ export function InlineLink({
action = 'push',
warnOnMismatchingTextChild,
style,
onPress: outerOnPress,
...rest
}: InlineLinkProps) {
const t = useTheme()
@@ -227,6 +235,7 @@ export function InlineLink({
displayText: stringChildren ? children : '',
action,
warnOnMismatchingTextChild,
onPress: outerOnPress,
})
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {
+20 -1
View File
@@ -7,6 +7,7 @@ import {Text} from '#/components/Typography'
import {RichText} from '#/components/RichText'
import {RaisingHande4Finger_Stroke2_Corner0_Rounded as RaisingHand} from '#/components/icons/RaisingHand'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {sanitizeHandle} from '#/lib/strings/handles'
export function Outer({
children,
@@ -43,6 +44,22 @@ export function Avatar({avatar}: {avatar?: string}) {
return <UserAvatar type="list" size={40} avatar={avatar} />
}
export function Title({value}: {value: string}) {
return <Text style={[a.text_md, a.font_bold, a.pb_2xs]}>{value}</Text>
}
export function Description({value, handle}: {value?: string; handle: string}) {
return value ? (
<RichText value={value} style={[]} />
) : (
<Text>
<Trans>
Moderation service managed by @{sanitizeHandle(handle, '@')}
</Trans>
</Text>
)
}
export function Content({
title,
description,
@@ -63,7 +80,9 @@ export function Content({
<RichText value={description} style={[]} />
) : (
<Text>
<Trans>Moderation service managed by @{handle}</Trans>
<Trans>
Moderation service managed by @{sanitizeHandle(handle, '@')}
</Trans>
</Text>
)}
</View>
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const SquareBehindSquare4_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M8 8V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-5v5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h5Zm1 8a1 1 0 0 1-1-1v-5H4v10h10v-4H9Z',
})
+76 -39
View File
@@ -13,6 +13,10 @@ import * as ModerationServiceCard from '#/components/ModerationServiceCard'
import {getModerationServiceTitle} from '#/lib/moderation'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences'
import {useModServiceLabelGroupEnableMutation} from '#/state/queries/modservice'
import {Divider} from '#/components/Divider'
import {InlineLink} from '#/components/Link'
import {useDialogStateControlContext} from '#/state/dialogs'
import {logger} from '#/logger'
function LabelerToggle({
labelGroup,
@@ -24,14 +28,24 @@ function LabelerToggle({
preferences: UsePreferencesQueryResponse
}) {
const t = useTheme()
const {mutateAsync, variables} = useModServiceLabelGroupEnableMutation()
const {
mutateAsync: toggleGroupEnabled,
variables: optimisticToggleGroupEnabled,
} = useModServiceLabelGroupEnableMutation()
const {closeAllDialogs} = useDialogStateControlContext()
const modservicePreferences = preferences.moderationOpts.mods.find(
({did}) => did === labeler.creator.did,
const labelerPrefs = React.useMemo(
() =>
preferences.moderationOpts.mods.find(
({did}) => did === labeler.creator.did,
),
[preferences.moderationOpts.mods, labeler.creator.did],
)
const enabled =
variables?.enabled ??
!modservicePreferences?.disabledLabelGroups?.includes(labelGroup)
const isLabelerEnabled = !!labelerPrefs?.enabled
const isEnabled = isLabelerEnabled
? optimisticToggleGroupEnabled?.enabled ??
!labelerPrefs?.disabledLabelGroups?.includes(labelGroup)
: false
const title = getModerationServiceTitle({
displayName: labeler.creator.displayName,
handle: labeler.creator.handle,
@@ -39,42 +53,66 @@ function LabelerToggle({
const onToggleEnabled = React.useCallback(async () => {
try {
await mutateAsync({
// @ts-ignore TODO
did: modservicePreferences?.did,
if (!labelerPrefs) throw new Error(`labelerPrefs not found`)
await toggleGroupEnabled({
did: labelerPrefs.did,
group: labelGroup,
enabled: !enabled,
enabled: !isEnabled,
})
} catch (e: any) {
// TODO
console.error(e)
logger.error(`Failed to toggle label group enabled`, {
message: e.message,
labelGroup,
})
}
}, [mutateAsync, enabled, modservicePreferences, labelGroup])
}, [toggleGroupEnabled, isEnabled, labelerPrefs, labelGroup])
return (
<Toggle.Item
name={labeler.creator.did}
label={title}
key={labeler.creator.did}
value={enabled}
onChange={onToggleEnabled}>
{ctx => (
<ModerationServiceCard.Card.Outer
style={[
...(ctx.focused || ctx.hovered ? [t.atoms.bg_contrast_50] : []),
ctx.selected && {
backgroundColor: t.palette.primary_50,
borderColor: t.palette.primary_100,
},
]}>
<View style={[a.rounded_sm, a.border, t.atoms.border_contrast_low]}>
<View style={[a.flex_row, a.justify_between, a.p_md]}>
<View style={[a.flex_row, a.align_center, a.gap_md]}>
<ModerationServiceCard.Card.Avatar avatar={labeler.creator.avatar} />
<ModerationServiceCard.Card.Content
title={title}
description={labeler.description}
handle={labeler.creator.handle}
/>
</ModerationServiceCard.Card.Outer>
)}
</Toggle.Item>
<View>
<ModerationServiceCard.Card.Title value={title} />
<ModerationServiceCard.Card.Description
value={labeler.description}
handle={labeler.creator.handle}
/>
</View>
</View>
<Toggle.Item
disabled={!isLabelerEnabled}
name={labeler.creator.did}
label={title}
key={labeler.creator.did}
value={isEnabled}
onChange={onToggleEnabled}>
<Toggle.Label>{isEnabled ? 'Enabled' : 'Disabled'}</Toggle.Label>
<Toggle.Switch />
</Toggle.Item>
</View>
<Divider />
<View style={[a.flex_row, a.p_md]}>
<Text style={[a.text_xs]}>
Configure more settings for this labeler{' '}
<InlineLink
to={{
screen: 'ProfileModservice',
params: {
name: labeler.creator.handle,
},
}}
style={[a.text_xs]}
onPress={closeAllDialogs}>
here.
</InlineLink>
</Text>
</View>
</View>
)
}
@@ -115,11 +153,10 @@ export function SettingsDialog({
a.p_md,
a.rounded_md,
a.mb_xl,
a.border,
t.atoms.border_contrast_low,
t.atoms.bg_contrast_25,
]}>
<View style={[a.flex_1, a.gap_xs]}>
<Text style={[a.text_md, a.font_bold]}>{groupInfoStrings.name}</Text>
<Text style={[a.font_bold]}>{groupInfoStrings.name}</Text>
<Text style={[a.leading_tight, {maxWidth: 400}]}>
{groupInfoStrings.description}
</Text>
+82 -70
View File
@@ -26,11 +26,12 @@ import {
} from '#/state/queries/preferences'
import {useModServicesDetailedInfoQuery} from '#/state/queries/modservice'
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {useTheme, atoms as a, useBreakpoints, native} from '#/alf'
import {Divider} from '#/components/Divider'
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
import {SquareBehindSquare4_Stroke2_Corner0_Rounded as SquareBehindSquare} from '#/components/icons/SquareBehindSquare4'
import {Text} from '#/components/Typography'
import * as Toggle from '#/components/forms/Toggle'
import * as ToggleButton from '#/components/forms/ToggleButton'
@@ -38,7 +39,7 @@ import {InlineLink, Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {useLabelGroupStrings} from '#/lib/moderation/useLabelGroupStrings'
import * as Dialog from '#/components/Dialog'
import {Button} from '#/components/Button'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {
getModerationServiceTitle,
useConfigurableLabelGroups,
@@ -365,79 +366,90 @@ function LabelGroup({
</View>
{!!mods.length && (
<View style={[a.flex_row, a.align_start, a.pb_md]}>
<View
style={[
a.flex_row,
a.align_start,
a.justify_between,
a.mb_md,
a.p_sm,
a.rounded_sm,
t.atoms.bg_contrast_25,
]}>
<View style={[a.flex_row, a.align_center, a.flex_1, a.gap_xs]}>
{mods.map(mod => {
const modservicePreferences =
preferences.moderationOpts.mods.find(
({did}) => did === mod.creator.did,
)
const labelGroupEnabled =
!modservicePreferences?.disabledLabelGroups?.includes(
labelGroup,
)
const isLabelerEnabled = modservicePreferences?.enabled
const enabled = labelGroupEnabled && isLabelerEnabled
return (
<View
key={mod.creator.did}
style={[
a.flex_row,
a.align_center,
a.gap_sm,
a.py_sm,
a.px_md,
a.rounded_full,
a.border,
t.atoms.border_contrast_low,
enabled && t.atoms.border_contrast_high,
enabled && t.atoms.bg,
]}>
<View
style={[
a.rounded_full,
t.atoms.bg_contrast_300,
{
height: 6,
width: 6,
},
enabled && {
backgroundColor: t.palette.positive_500,
},
]}
/>
<Text
style={[
a.text_xs,
a.font_bold,
native({
top: 1,
}),
t.atoms.text_contrast_medium,
enabled && t.atoms.text_contrast_high,
]}>
{getModerationServiceTitle({
displayName: mod.creator.displayName,
handle: mod.creator.handle,
})}
</Text>
</View>
)
})}
</View>
<Button
label={_(
msg`Configure moderation services for category: ${name.toLowerCase()}`,
)}
onPress={() =>
size="tiny"
variant="ghost"
color="secondary"
label={_(msg`Configure enabled labelers for this content type`)}
style={[a.mt_xs]}
onPress={() => {
openModSettingsDialog({
labelGroup,
modservices: mods,
})
}
style={[a.flex_1]}>
{ctx => (
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.gap_xs,
a.p_sm,
a.border,
a.rounded_sm,
t.atoms.border_contrast_low,
(ctx.hovered || ctx.focused) && t.atoms.bg_contrast_25,
]}>
{mods.map(mod => {
const modservicePreferences =
preferences.moderationOpts.mods.find(
({did}) => did === mod.creator.did,
)
const enabled =
!modservicePreferences?.disabledLabelGroups?.includes(
labelGroup,
)
return (
<View
key={mod.creator.did}
style={[
a.py_xs,
a.px_sm,
a.rounded_sm,
a.border,
t.atoms.border_contrast_low,
enabled && t.atoms.bg_contrast_800,
]}>
<Text
style={[
a.text_xs,
a.font_bold,
t.atoms.text_contrast_high,
enabled && t.atoms.text_inverted,
]}>
{getModerationServiceTitle({
displayName: mod.creator.displayName,
handle: mod.creator.handle,
})}
</Text>
</View>
)
})}
<Text
style={[
a.text_xs,
a.font_bold,
a.pl_sm,
a.italic,
t.atoms.text_contrast_low,
]}>
Configure
</Text>
</View>
)}
}}>
<ButtonText>Configure</ButtonText>
<ButtonIcon icon={SquareBehindSquare} position="right" />
</Button>
</View>
)}
+25 -66
View File
@@ -19,11 +19,9 @@ import {
} from '#/view/com/util/forms/NativeDropdown'
import {useSession} from '#/state/session'
import {useModalControls} from '#/state/modals'
import {
useModServiceSubscriptionMutation,
useModServiceEnableMutation,
} from '#/state/queries/modservice'
import {useModServiceSubscriptionMutation} from '#/state/queries/modservice'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences'
import {logger} from '#/logger'
import {useTheme, atoms as a, tokens, web, native, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
@@ -54,16 +52,7 @@ export function Header({
preferences.moderationOpts.mods.find(
mod => mod.did === modservice.creator.did,
)
const {mutateAsync: toggleEnabled, variables: enabledVariables} =
useModServiceEnableMutation()
const isEnabled =
enabledVariables?.enabled ??
preferences.moderationOpts.mods.find(
mod => mod.did === modservice.creator.did && mod.enabled,
) ??
variables?.subscribe
const [subscriptionError, setSubscriptionError] = React.useState<string>('')
const [enablementError, setEnablementError] = React.useState<string>('')
const onPressBack = React.useCallback(() => {
if (navigation.canGoBack()) {
@@ -85,20 +74,10 @@ export function Header({
})
} catch (e: any) {
setSubscriptionError(e.message)
logger.error(`Failed to subscribe to labeler`, {message: e.message})
}
}, [toggleSubscription, isSubscribed, modservice.creator.did])
const onPressEnable = React.useCallback(async () => {
try {
await toggleEnabled({
did: modservice.creator.did,
enabled: !isEnabled,
})
} catch (e: any) {
setEnablementError(e.message)
}
}, [toggleEnabled, isEnabled, modservice.creator.did])
return (
<View style={[a.pb_xl, web(a.pt_md), gtMobile && web(a.pt_2xl)]}>
{!gtMobile && (
@@ -179,23 +158,6 @@ export function Header({
{gtMobile && (
<View style={[a.flex_row, a.align_center, a.gap_md]}>
{isSubscribed && (
<Button
size="small"
variant="solid"
color="secondary"
label={
isEnabled
? _(msg`Disable this moderation service`)
: _(msg`Enable this moderation service`)
}
onPress={onPressEnable}>
<ButtonText>
{isEnabled ? <Trans>Disable</Trans> : <Trans>Enable</Trans>}
</ButtonText>
</Button>
)}
<Button
size="small"
variant="solid"
@@ -220,32 +182,29 @@ export function Header({
)}
</View>
{subscriptionError ||
(enablementError && (
<View style={[a.pt_lg]}>
<View
style={[
a.px_lg,
a.py_md,
a.rounded_sm,
a.border,
a.gap_xs,
{
backgroundColor: t.palette.negative_50,
borderColor: t.palette.negative_100,
},
]}>
<Text style={[a.font_bold, {color: t.palette.negative_950}]}>
<Trans>
An error occurred while editing your subscription.
</Trans>
</Text>
<Text style={[{color: t.palette.negative_900}]}>
{subscriptionError || enablementError}
</Text>
</View>
{subscriptionError && (
<View style={[a.pt_lg]}>
<View
style={[
a.px_lg,
a.py_md,
a.rounded_sm,
a.border,
a.gap_xs,
{
backgroundColor: t.palette.negative_50,
borderColor: t.palette.negative_100,
},
]}>
<Text style={[a.font_bold, {color: t.palette.negative_950}]}>
<Trans>An error occurred while editing your subscription.</Trans>
</Text>
<Text style={[{color: t.palette.negative_900}]}>
{subscriptionError || enablementError}
</Text>
</View>
))}
</View>
)}
</View>
)
}
@@ -2,13 +2,13 @@ import React from 'react'
import {View} from 'react-native'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
// import {msg} from '@lingui/macro'
import {LABEL_GROUPS} from '@atproto/api'
// TODO
import {ModPrefItem} from '@atproto/api/dist/client/types/app/bsky/actor/defs'
import {useLabelGroupStrings} from '#/lib/moderation/useLabelGroupStrings'
import {useModServiceLabelGroupEnableMutation} from '#/state/queries/modservice'
import {logger} from '#/logger'
import {useTheme, atoms as a} from '#/alf'
import {Text} from '#/components/Typography'
@@ -16,9 +16,11 @@ import * as Toggle from '#/components/forms/Toggle'
export function PreferenceRow({
labelGroup,
disabled,
modservicePreferences,
}: {
labelGroup: keyof typeof LABEL_GROUPS
disabled?: boolean
modservicePreferences?: ModPrefItem
}) {
const t = useTheme()
@@ -32,15 +34,19 @@ export function PreferenceRow({
const onToggleEnabled = React.useCallback(async () => {
try {
if (!modservicePreferences)
throw new Error(`modservicePreferences not found`)
await mutateAsync({
// @ts-ignore TODO
did: modservicePreferences?.did,
did: modservicePreferences.did,
group: labelGroup,
enabled: !enabled,
})
} catch (e: any) {
// TODO
console.error(e)
logger.error(`Failed to toggle label group enabled`, {
message: e.message,
labelGroup,
})
}
}, [mutateAsync, enabled, modservicePreferences, labelGroup])
@@ -62,8 +68,9 @@ export function PreferenceRow({
<View style={[a.justify_center, {minHeight: 35}]}>
{modservicePreferences && (
<Toggle.Item
disabled={disabled}
name="enable"
value={enabled}
value={disabled ? false : enabled}
onChange={onToggleEnabled}
label={_(msg`Enable`)}>
<Toggle.Label>{enabled ? 'Enabled' : 'Disabled'}</Toggle.Label>
+72 -32
View File
@@ -6,19 +6,18 @@ import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {usePalette} from '#/lib/hooks/usePalette'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {s} from '#/lib/styles'
import {TextLink} from '#/view/com/util/Link'
import * as Toast from '#/view/com/util/Toast'
import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {Haptics} from '#/lib/haptics'
import {useAnalytics} from '#/lib/analytics/analytics'
import {pluralize} from '#/lib/strings/helpers'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {makeProfileLink} from '#/lib/routes/links'
import {logger} from '#/logger'
import {useModServiceInfoQuery} from '#/state/queries/modservice'
import {
useModServiceInfoQuery,
useModServiceEnableMutation,
} from '#/state/queries/modservice'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {
UsePreferencesQueryResponse,
@@ -26,7 +25,6 @@ import {
} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
import {sanitizeHandle} from '#/lib/strings/handles'
import {getLabelGroupsFromLabels} from '#/lib/moderation'
@@ -41,6 +39,7 @@ import {
import {RichText} from '#/components/RichText'
import {InlineLink} from '#/components/Link'
import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle'
import {ErrorState} from '#/screens/ProfileModerationService/ErrorState'
import {Header} from '#/screens/ProfileModerationService/Header'
@@ -116,15 +115,20 @@ export function ProfileModserviceScreenInner({
const t = useTheme()
const {_} = useLingui()
const {height: minHeight} = useSafeAreaFrame()
const pal = usePalette('default')
const {hasSession} = useSession()
const {track} = useAnalytics()
const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation()
const {mutateAsync: unlikeMod, isPending: isUnlikePending} =
useUnlikeMutation()
const {mutateAsync: toggleEnabled, variables: enabledVariables} =
useModServiceEnableMutation()
const [likeUri, setLikeUri] = React.useState<string>(
modservice.viewer?.like || '',
)
// TODO error state
const [_enablementError, setEnablementError] = React.useState<string>('')
const isLiked = !!likeUri
const groups = React.useMemo(() => {
return getLabelGroupsFromLabels(modservice.policies.labelValues).filter(
@@ -136,6 +140,12 @@ export function ProfileModserviceScreenInner({
p => p.did === modservice.creator.did,
)
}, [modservice.creator.did, preferences.moderationOpts.mods])
const isEnabled = Boolean(
enabledVariables?.enabled ??
preferences.moderationOpts.mods.find(
mod => mod.did === modservice.creator.did && mod.enabled,
),
)
useSetTitle(modservice.creator.displayName || modservice.creator.handle)
@@ -152,16 +162,28 @@ export function ProfileModserviceScreenInner({
track('CustomFeed:Like')
setLikeUri(res.uri)
}
} catch (err) {
} catch (e: any) {
Toast.show(
_(
msg`There was an an issue contacting the server, please check your internet connection and try again.`,
),
)
logger.error('Failed up toggle like', {error: err})
logger.error(`Failed to toggle labeler like`, {message: e.message})
}
}, [likeUri, isLiked, modservice, likeMod, unlikeMod, track, _])
const onToggleLabelerEnabled = React.useCallback(async () => {
try {
await toggleEnabled({
did: modservice.creator.did,
enabled: !isEnabled,
})
} catch (e: any) {
setEnablementError(e.message)
logger.error(`Failed to toggle labeler enabled`, {message: e.message})
}
}, [toggleEnabled, isEnabled, modservice.creator.did])
return (
<ScrollView
scrollEventThrottle={1}
@@ -194,25 +216,6 @@ export function ProfileModserviceScreenInner({
</Text>
)}
<Text
style={[
a.text_md,
a.leading_normal,
a.italic,
t.atoms.text_contrast_medium,
]}>
<Trans>
Operated by{' '}
<TextLink
href={makeProfileLink(modservice.creator)}
text={sanitizeHandle(modservice.creator.handle, '@')}
style={pal.link}
/>
. Handles reports of anti-social behavior, illegal content, unwanted
sexual content, and misinformation.
</Trans>
</Text>
<View style={[a.flex_row, a.gap_md, a.align_center]}>
<Button
testID="toggleLikeBtn"
@@ -231,7 +234,9 @@ export function ProfileModserviceScreenInner({
</Button>
{typeof modservice.likeCount === 'number' && (
<InlineLink to={'#todo'} style={[pal.textLight, s.semiBold]}>
<InlineLink
to={'#todo'}
style={[t.atoms.text_contrast_medium, a.font_bold]}>
<Trans>
Liked by {modservice.likeCount}{' '}
{pluralize(modservice.likeCount, 'user')}
@@ -248,18 +253,53 @@ export function ProfileModserviceScreenInner({
Configure moderation service
</Text>
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
This moderation service supports the following types of content.
Configure which types of content you'd like to respect from this
service.
This labeler moderates the following types of content. You can
optionally enable or disable the labeler's recommendations for each
type of content below.
</Text>
</View>
<View style={[a.w_full, a.mt_xl]}>
<Toggle.Item
name="enable"
value={isEnabled}
onChange={onToggleLabelerEnabled}
label={
isEnabled
? _(msg`Disable this moderation service`)
: _(msg`Enable this moderation service`)
}>
<View
style={[
a.w_full,
a.flex_row,
a.justify_between,
a.gap_lg,
a.p_md,
a.rounded_sm,
t.atoms.bg_contrast_25,
]}>
<View style={[a.flex_1]}>
<Text style={[a.font_bold, a.pb_xs]}>
This service is {isEnabled ? 'enabled' : 'disabled'}
</Text>
<Text style={[t.atoms.text_contrast_medium, a.leading_snug]}>
Optionally disable this service without affecting your
subscription.
</Text>
</View>
<Toggle.Switch />
</View>
</Toggle.Item>
</View>
<View style={[a.gap_md, a.mt_xl]}>
{groups.map((def, i) => {
return (
<React.Fragment key={def.id}>
{i !== 0 && <Divider />}
<PreferenceRow
disabled={isEnabled ? undefined : true}
labelGroup={def.id}
modservicePreferences={modservicePreferences}
/>