Implement label handling

This commit is contained in:
Paul Frazee
2024-03-05 16:18:49 -08:00
parent e794e84b48
commit a84e264dae
28 changed files with 677 additions and 664 deletions
@@ -4,7 +4,7 @@ import {InterprettedLabelValueDefinition, LabelPreference} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import {
usePreferencesQuery,
usePreferencesSetContentLabelMutation,
@@ -30,10 +30,10 @@ export function SimpleModerationLabelPref({
const savedPref = preferences?.moderationPrefs.labels[identifier]
const pref = variables?.visibility ?? savedPref ?? 'warn'
const allLabelStrings = useLabelStrings()
const allLabelStrings = useGlobalLabelStrings()
const labelStrings =
labelValueDefinition.identifier in allLabelStrings
? allLabelStrings[labelValueDefinition.identifier].general
? allLabelStrings[labelValueDefinition.identifier]
: {
name: labelValueDefinition.identifier,
description: `Labeled "${labelValueDefinition.identifier}"`,
+3 -3
View File
@@ -4,7 +4,7 @@ import {InterprettedLabelValueDefinition, LabelPreference} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import {
useLabelBehaviorDescription,
useLabelLongBehaviorDescription,
@@ -58,11 +58,11 @@ export function ModerationLabelPref({
'ignore',
)
const allLabelStrings = useLabelStrings()
const allLabelStrings = useGlobalLabelStrings()
const labelStrings = labelValueDefinition.locales[0] // TODO look up locale
? labelValueDefinition.locales[0]
: labelValueDefinition.identifier in allLabelStrings
? allLabelStrings[labelValueDefinition.identifier].general
? allLabelStrings[labelValueDefinition.identifier]
: {
name: labelValueDefinition.identifier,
description: `Labeled "${labelValueDefinition.identifier}"`,
@@ -11,12 +11,12 @@ import {useLabelerInfoQuery} from '#/state/queries/labeler'
export * as Card from '#/components/ModerationServiceCard/Card'
type ModerationServiceProps = {
modservice: AppBskyLabelerDefs.LabelerViewDetailed
labeler: AppBskyLabelerDefs.LabelerViewDetailed
}
export function Link({
children,
modservice,
labeler,
}: ModerationServiceProps & Pick<LinkProps, 'children'>) {
const {_} = useLingui()
@@ -25,11 +25,11 @@ export function Link({
to={{
screen: 'Profile',
params: {
name: modservice.creator.handle,
name: labeler.creator.handle,
},
}}
label={_(
msg`View the moderation service provided by @${modservice.creator.handle}`,
msg`View the moderation service provided by @${labeler.creator.handle}`,
)}>
{children}
</InternalLink>
@@ -54,7 +54,7 @@ export function Loader({
loading?: React.ComponentType<{}>
error?: React.ComponentType<{error: string}>
component: React.ComponentType<{
modservice: AppBskyLabelerDefs.LabelerViewDetailed
labeler: AppBskyLabelerDefs.LabelerViewDetailed
}>
}) {
const {isLoading, data, error} = useLabelerInfoQuery({did})
@@ -68,6 +68,6 @@ export function Loader({
<ErrorComponent error={error?.message || 'Unknown error'} />
) : null
) : (
<Component modservice={data} />
<Component labeler={data} />
)
}
+13 -13
View File
@@ -112,7 +112,7 @@ function LabelGroupButton({
)
}
function ModServiceToggle({title}: {title: string}) {
function LabelerToggle({title}: {title: string}) {
const t = useTheme()
const ctx = Toggle.useItemContext()
@@ -260,7 +260,7 @@ function SubmitView({
key={labeler.creator.did}
name={labeler.creator.did}
label={title}>
<ModServiceToggle title={title} />
<LabelerToggle title={title} />
</Toggle.Item>
)
})}
@@ -477,21 +477,21 @@ function ReportDialogInner(props: ReportDialogProps) {
data: preferences,
} = usePreferencesQuery()
const {
isLoading: isModServicesLoading,
data: modservices,
error: modservicesError,
isLoading: isLabelersLoading,
data: labelers,
error: labelersError,
} = useLabelersDetailedInfoQuery({
dids: preferences ? preferences.moderationOpts.mods.map(m => m.did) : [],
dids: preferences ? preferences.moderationPrefs.mods.map(m => m.did) : [],
})
const isLoading = isPreferencesLoading || isModServicesLoading
const error = preferencesError || modservicesError
const isLoading = isPreferencesLoading || isLabelersLoading
const error = preferencesError || labelersError
const [fakeLoading, setFakeLoading] = React.useState(isLoading)
const labelGroupToLabelerMap = React.useMemo(() => {
if (!modservices) return {}
return getLabelGroupToLabelerMap(modservices)
}, [modservices])
if (!labelers) return {}
return getLabelGroupToLabelerMap(labelers)
}, [labelers])
React.useEffect(() => {
// on initial load, show a loading spinner for a hot sec to prevent flash
@@ -506,10 +506,10 @@ function ReportDialogInner(props: ReportDialogProps) {
{/* Here to capture focus for a hot sec to prevent flash */}
<Pressable accessible={false} />
</View>
) : error || !(preferences && modservices) ? null : ( // TODO
) : error || !(preferences && labelers) ? null : ( // TODO
<ReportDialogLoaded
{...props}
labelers={modservices}
labelers={labelers}
labelGroupToLabelerMap={labelGroupToLabelerMap}
/>
)}
+130
View File
@@ -0,0 +1,130 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {isJustAMute} from '#/lib/moderation'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Text} from '#/components/Typography'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/moderation/ModerationDetailsDialog'
export function ContentHider({
testID,
modui,
ignoreMute,
style,
childContainerStyle,
children,
}: React.PropsWithChildren<{
testID?: string
modui: ModerationUI | undefined
ignoreMute?: boolean
style?: StyleProp<ViewStyle>
childContainerStyle?: StyleProp<ViewStyle>
}>) {
const t = useTheme()
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
const {gtMobile} = useBreakpoints()
const control = useModerationDetailsDialogControl()
const blur = modui?.blurs[0]
const desc = useModerationCauseDescription(blur)
if (!blur || (ignoreMute && isJustAMute(modui))) {
return (
<View testID={testID} style={[styles.outer, style]}>
{children}
</View>
)
}
return (
<View testID={testID} style={[a.overflow_hidden, style]}>
<ModerationDetailsDialog control={control} modcause={blur} />
<Button
variant="solid"
color="secondary"
size="large"
shape="default"
onPress={() => {
if (!modui.noOverride) {
setOverride(v => !v)
} else {
control.open()
}
}}
label={desc.name}
accessibilityHint={
override ? _(msg`Hide the content`) : _(msg`Show the content`)
}
style={
gtMobile ? [a.py_lg, a.px_xl, a.gap_sm] : [a.py_md, a.px_lg, a.gap_sm]
}>
<ButtonIcon icon={desc.icon} position="left" />{' '}
<ButtonText style={[a.flex_1, a.text_left]}>{desc.name}</ButtonText>
{!modui.noOverride && (
<ButtonText>
{override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
</ButtonText>
)}
</Button>
{desc.source && blur.type === 'label' && !override && (
<Button
variant="ghost"
size="tiny"
onPress={() => {
control.open()
}}
label={_(msg`Learn more`)}
style={[]}>
<ButtonText
style={[
a.flex_1,
a.text_sm,
a.font_normal,
t.atoms.text_contrast_medium,
a.text_left,
]}>
<Trans>
{sanitizeDisplayName(desc.source)}.{' '}
<Text style={[{color: t.palette.primary_500}, a.text_sm]}>
Learn more.
</Text>
</Trans>
</ButtonText>
</Button>
)}
{override && <View style={childContainerStyle}>{children}</View>}
</View>
)
}
const styles = StyleSheet.create({
outer: {
overflow: 'hidden',
},
cover: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
borderRadius: 8,
marginTop: 4,
paddingVertical: 14,
paddingLeft: 14,
paddingRight: 18,
},
showBtn: {
marginLeft: 'auto',
alignSelf: 'center',
},
})
+88
View File
@@ -0,0 +1,88 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session'
import {atoms as a} from '#/alf'
import {Button, ButtonText, ButtonIcon, ButtonSize} from '#/components/Button'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {
LabelsOnMeDialog,
useLabelsOnMeDialogControl,
} from '#/components/moderation/LabelsOnMeDialog'
export function LabelsOnMe({
details,
labels,
size,
style,
}: {
details: {did: string} | {uri: string; cid: string}
labels: ComAtprotoLabelDefs.Label[] | undefined
size?: ButtonSize
style?: StyleProp<ViewStyle>
}) {
const {_} = useLingui()
const {currentAccount} = useSession()
const isAccount = 'did' in details
const control = useLabelsOnMeDialogControl()
if (!labels || !currentAccount) {
return null
}
labels = labels.filter(
l => !l.val.startsWith('!') && l.src !== currentAccount.did,
)
if (!labels.length) {
return null
}
return (
<View style={[a.flex_row, style]}>
<LabelsOnMeDialog control={control} subject={details} labels={labels} />
<Button
variant="solid"
color="secondary"
size={size || 'small'}
label={_(msg`View information about these labels`)}
onPress={() => {
console.log('open')
control.open()
}}>
<ButtonIcon position="left" icon={CircleInfo} />
<ButtonText>
{labels.length}{' '}
{labels.length === 1 ? (
<Trans>
label has been placed on this {isAccount ? 'account' : 'content'}
</Trans>
) : (
<Trans>
labels have been placed on this{' '}
{isAccount ? 'account' : 'content'}
</Trans>
)}
</ButtonText>
</Button>
</View>
)
}
export function LabelsOnMyPost({
post,
style,
}: {
post: AppBskyFeedDefs.PostView
style?: StyleProp<ViewStyle>
}) {
const {currentAccount} = useSession()
if (post.author.did !== currentAccount?.did) {
return null
}
return (
<LabelsOnMe details={post} labels={post.labels} size="tiny" style={style} />
)
}
@@ -4,11 +4,15 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ComAtprotoLabelDefs} from '@atproto/api'
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeHandle} from '#/lib/strings/handles'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog'
import {Button} from '#/components/Button'
import {capitalize} from '#/lib/strings/capitalize'
import {Button, ButtonText} from '#/components/Button'
import {InlineLink} from '#/components/Link'
export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog'
@@ -61,12 +65,13 @@ export function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
<Text
nativeID="dialog-title"
style={[a.text_2xl, a.font_bold, a.pb_md, a.leading_tight]}>
<Trans>
The following labels were applied to your{' '}
{isAccount ? 'account' : 'content'}
</Trans>
{isAccount ? (
<Trans>Labels on your account</Trans>
) : (
<Trans>Labels on your content</Trans>
)}
</Text>
<Text nativeID="dialog-description" style={[a.text_md, a.leading_snug]}>
<Text nativeID="dialog-description" style={[a.text_sm, a.leading_snug]}>
<Trans>
You may appeal these labels if you feel they were placed in error.
</Trans>
@@ -74,11 +79,11 @@ export function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
<View style={[a.py_lg, a.gap_md]}>
{labels.map(label => (
<View
key={`${label.src}-${label.val}`}
style={[a.p_md, a.rounded_sm, t.atoms.bg_contrast_25]}>
<Text>{capitalize(label.val)}</Text>
</View>
<Label
key={`${label.val}-${label.src}`}
label={label}
control={props.control}
/>
))}
</View>
@@ -106,3 +111,49 @@ export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) {
</Dialog.Outer>
)
}
function Label({
label,
control,
}: {
label: ComAtprotoLabelDefs.Label
control: Dialog.DialogOuterProps['control']
}) {
const t = useTheme()
const {labeler, strings} = useLabelInfo(label)
return (
<View
key={`${label.src}-${label.val}`}
style={[
a.p_md,
a.rounded_sm,
// t.atoms.bg_contrast_25,
a.border,
t.atoms.border_contrast_low,
a.gap_sm,
a.flex_row,
]}>
<View style={[a.flex_1, a.gap_xs]}>
<Text style={[a.font_bold, a.text_md, t.atoms.text]}>
{strings.name}
</Text>
<Text style={[t.atoms.text]}>{strings.description}</Text>
<InlineLink
to={makeProfileLink(
labeler ? labeler.creator : {did: label.src, handle: ''},
)}
onPress={() => control.close()}
style={[]}>
{labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src}
</InlineLink>
</View>
<View>
<Button variant="solid" color="secondary" size="small">
<ButtonText>
<Trans>Appeal</Trans>
</ButtonText>
</Button>
</View>
</View>
)
}
@@ -4,19 +4,20 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ModerationCause} from '@atproto/api'
import {atoms as a, useBreakpoints} from '#/alf'
import {listUriToHref} from '#/lib/strings/url-helpers'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog'
import {Button} from '#/components/Button'
import {InlineLink} from '#/components/Link'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
import {listUriToHref} from '#/lib/strings/url-helpers'
import {makeProfileLink} from '#/lib/routes/links'
export {useDialogControl as useModerationDetailsDialogControl} from '#/components/Dialog'
export interface ModerationDetailsDialogProps {
control: Dialog.DialogOuterProps['control']
context: 'account' | 'content'
modcause: ModerationCause
}
@@ -24,21 +25,20 @@ export function ModerationDetailsDialog(props: ModerationDetailsDialogProps) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<ModerationDetailsDialogInner {...props} />
</Dialog.Outer>
)
}
function ModerationDetailsDialogInner({
context,
modcause,
control,
}: ModerationDetailsDialogProps & {
control: Dialog.DialogOuterProps['control']
}) {
const t = useTheme()
const {_} = useLingui()
const labelStrings = useLabelStrings()
const control = Dialog.useDialogControl()
const desc = useModerationCauseDescription(modcause)
const {gtMobile} = useBreakpoints()
let name
@@ -95,13 +95,8 @@ function ModerationDetailsDialogInner({
description = _(msg`You have muted this user.`)
}
} else if (modcause.type === 'label') {
if (modcause.labelDef.id in labelStrings) {
name = labelStrings[modcause.labelDef.id][context].name
description = labelStrings[modcause.labelDef.id][context].description
} else {
name = modcause.labelDef.id
description = _(msg`Labeled ${modcause.labelDef.id}`)
}
name = desc.name
description = desc.description
} else {
// should never happen
name = ''
@@ -112,12 +107,38 @@ function ModerationDetailsDialogInner({
<Dialog.ScrollableInner
accessibilityDescribedBy="dialog-description"
accessibilityLabelledBy="dialog-title">
<Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}>
<Text
nativeID="dialog-title"
style={[t.atoms.text, a.text_2xl, a.font_bold, a.mb_md]}>
{name}
</Text>
<Text nativeID="dialog-description" style={[a.text_sm]}>
<Text
nativeID="dialog-description"
style={[t.atoms.text, a.text_md, a.mb_md]}>
{description}
</Text>
{modcause.type === 'label' && (
<View
style={[
t.atoms.bg_contrast_50,
a.mb_md,
a.px_lg,
a.py_lg,
a.rounded_sm,
]}>
<Text style={[t.atoms.text, a.text_sm, a.leading_snug]}>
<Trans>
This label was applied by{' '}
<InlineLink
to={makeProfileLink({did: modcause.label.src, handle: ''})}
onPress={() => control.close()}>
{desc.source}
</InlineLink>
.
</Trans>
</Text>
</View>
)}
<View style={gtMobile && [a.flex_row, a.justify_end]}>
<Button
testID="doneBtn"
+100
View File
@@ -0,0 +1,100 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationUI, ModerationCause} from '@atproto/api'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {getModerationCauseKey} from '#/lib/moderation'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Text} from '#/components/Typography'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/moderation/ModerationDetailsDialog'
export function PostAlerts({
modui,
style,
}: {
modui: ModerationUI
includeMute?: boolean
style?: StyleProp<ViewStyle>
}) {
if (!modui.alert && !modui.inform) {
return null
}
return (
<View style={[a.flex_col, a.gap_xs, a.mb_sm, style]}>
{modui.inform && (
<View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
{modui.informs.map(cause => (
<PostInform key={getModerationCauseKey(cause)} cause={cause} />
))}
</View>
)}
{modui.alerts.map(cause => (
<PostAlert key={getModerationCauseKey(cause)} cause={cause} />
))}
</View>
)
}
function PostInform({cause}: {cause: ModerationCause}) {
const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause)
return (
<>
<Button
label={desc.name}
variant="solid"
color="secondary"
size="tiny"
shape="default"
onPress={() => {
control.open()
}}>
<ButtonIcon icon={desc.icon} position="left" />{' '}
<ButtonText>{desc.name}</ButtonText>
</Button>
<ModerationDetailsDialog control={control} modcause={cause} />
</>
)
}
function PostAlert({cause}: {cause: ModerationCause}) {
const t = useTheme()
const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause)
return (
<>
<Button
label={desc.name}
variant="solid"
color="secondary"
size="small"
shape="default"
onPress={() => {
control.open()
}}>
<ButtonIcon icon={desc.icon} position="left" />
<ButtonText style={[a.flex_1, a.text_left]}>
{desc.name}
{desc.source && (
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{' — '}
{sanitizeDisplayName(desc.source)}
</Text>
)}
</ButtonText>
</Button>
<ModerationDetailsDialog control={control} modcause={cause} />
</>
)
}
+129
View File
@@ -0,0 +1,129 @@
import React, {ComponentProps} from 'react'
import {StyleSheet, Pressable, View, ViewStyle, StyleProp} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {addStyle} from 'lib/styles'
import {useTheme, atoms as a} from '#/alf'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/moderation/ModerationDetailsDialog'
import {Text} from '#/components/Typography'
// import {Link} from '#/components/Link' TODO this imposes some styles that screw things up
import {Link} from '#/view/com/util/Link'
interface Props extends ComponentProps<typeof Link> {
iconSize: number
iconStyles: StyleProp<ViewStyle>
modui: ModerationUI
}
export function PostHider({
testID,
href,
modui,
style,
children,
iconSize,
iconStyles,
...props
}: Props) {
const t = useTheme()
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
const control = useModerationDetailsDialogControl()
const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur)
if (!blur) {
return (
<Link
testID={testID}
style={style}
href={href}
accessible={false}
{...props}>
{children}
</Link>
)
}
return !override ? (
<Pressable
onPress={() => {
if (!modui.noOverride) {
setOverride(v => !v)
}
}}
accessibilityRole="button"
accessibilityHint={
override ? _(msg`Hide the content`) : _(msg`Show the content`)
}
accessibilityLabel=""
style={[
a.flex_row,
a.align_center,
a.gap_sm,
a.py_md,
{
paddingLeft: 6,
paddingRight: 18,
},
override ? {paddingBottom: 0} : undefined,
t.atoms.bg,
]}>
<ModerationDetailsDialog control={control} modcause={blur} />
<Pressable
onPress={() => {
control.open()
}}
accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint="">
<View
style={[
t.atoms.bg_contrast_25,
a.align_center,
a.justify_center,
{
width: iconSize,
height: iconSize,
borderRadius: iconSize,
},
iconStyles,
]}>
<desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} />
</View>
</Pressable>
<Text style={[t.atoms.text_contrast_medium, a.flex_1]} numberOfLines={1}>
{desc.name}
</Text>
{!modui.noOverride && (
<Text style={[{color: t.palette.primary_500}]}>
{override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
</Text>
)}
</Pressable>
) : (
<Link
testID={testID}
style={addStyle(style, styles.child)}
href={href}
accessible={false}
{...props}>
{children}
</Link>
)
}
const styles = StyleSheet.create({
child: {
borderWidth: 0,
borderTopWidth: 0,
borderRadius: 8,
},
})
@@ -0,0 +1,99 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationCause, ModerationDecision} from '@atproto/api'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {getModerationCauseKey} from 'lib/moderation'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Text} from '#/components/Typography'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/moderation/ModerationDetailsDialog'
export function ProfileHeaderAlerts({
moderation,
style,
}: {
moderation: ModerationDecision
style?: StyleProp<ViewStyle>
}) {
const modui = moderation.ui('profileView')
if (!modui.alert && !modui.inform) {
return null
}
return (
<View style={[a.flex_col, a.gap_xs, a.mb_sm, style]}>
{modui.inform && (
<View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
{modui.informs.map(cause => (
<ProfileInform key={getModerationCauseKey(cause)} cause={cause} />
))}
</View>
)}
{modui.alerts.map(cause => (
<ProfileAlert key={getModerationCauseKey(cause)} cause={cause} />
))}
</View>
)
}
function ProfileInform({cause}: {cause: ModerationCause}) {
const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause)
return (
<>
<Button
label={desc.name}
variant="solid"
color="secondary"
size="tiny"
shape="default"
onPress={() => {
control.open()
}}>
<ButtonIcon icon={desc.icon} position="left" />{' '}
<ButtonText>{desc.name}</ButtonText>
</Button>
<ModerationDetailsDialog control={control} modcause={cause} />
</>
)
}
function ProfileAlert({cause}: {cause: ModerationCause}) {
const t = useTheme()
const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause)
return (
<>
<Button
label={desc.name}
variant="solid"
color="secondary"
size="small"
shape="default"
onPress={() => {
control.open()
}}>
<ButtonIcon icon={desc.icon} position="left" />
<ButtonText style={[a.flex_1, a.text_left]}>
{desc.name}
{desc.source && (
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{' — '}
{sanitizeDisplayName(desc.source)}
</Text>
)}
</ButtonText>
</Button>
<ModerationDetailsDialog control={control} modcause={cause} />
</>
)
}
+171
View File
@@ -0,0 +1,171 @@
import React from 'react'
import {
TouchableWithoutFeedback,
StyleProp,
View,
ViewStyle,
} from 'react-native'
import {useNavigation} from '@react-navigation/native'
import {ModerationUI} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {NavigationProp} from 'lib/routes/types'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {useTheme, atoms as a} from '#/alf'
import {CenteredView} from '#/view/com/util/Views'
import {Text} from '#/components/Typography'
import {Button, ButtonText} from '#/components/Button'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/moderation/ModerationDetailsDialog'
export function ScreenHider({
testID,
screenDescription,
modui,
style,
containerStyle,
children,
}: React.PropsWithChildren<{
testID?: string
screenDescription: string
modui: ModerationUI
style?: StyleProp<ViewStyle>
containerStyle?: StyleProp<ViewStyle>
}>) {
const t = useTheme()
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
const navigation = useNavigation<NavigationProp>()
const {isMobile} = useWebMediaQueries()
const control = useModerationDetailsDialogControl()
const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur)
if (!blur || override) {
return (
<View testID={testID} style={style}>
{children}
</View>
)
}
const isNoPwi = !!modui.blurs.find(
cause =>
cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated',
)
return (
<CenteredView
style={[
a.flex_1,
{
paddingTop: 100,
paddingBottom: 150,
},
t.atoms.bg,
containerStyle,
]}
sideBorders>
<View style={[a.align_center, a.mb_md]}>
<View
style={[
t.atoms.bg_contrast_975,
a.align_center,
a.justify_center,
{
borderRadius: 25,
width: 50,
height: 50,
},
]}>
<desc.icon width={24} fill={t.atoms.bg.backgroundColor} />
</View>
</View>
<Text
style={[
a.text_4xl,
a.font_semibold,
a.text_center,
a.mb_md,
t.atoms.text,
]}>
{isNoPwi ? (
<Trans>Sign-in Required</Trans>
) : (
<Trans>Content Warning</Trans>
)}
</Text>
<Text
style={[
a.text_lg,
a.mb_md,
a.px_lg,
a.text_center,
t.atoms.text_contrast_medium,
]}>
{isNoPwi ? (
<Trans>
This account has requested that users sign in to view their profile.
</Trans>
) : (
<>
<Trans>This {screenDescription} has been flagged:</Trans>
<Text style={[a.text_lg, a.font_semibold, t.atoms.text, a.ml_xs]}>
{desc.name}.{' '}
</Text>
<TouchableWithoutFeedback
onPress={() => {
control.open()
}}
accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint="">
<Text style={[a.text_lg, {color: t.palette.primary_500}]}>
<Trans>Learn More</Trans>
</Text>
</TouchableWithoutFeedback>
<ModerationDetailsDialog control={control} modcause={blur} />
</>
)}{' '}
</Text>
{isMobile && <View style={a.flex_1} />}
<View style={[a.flex_row, a.justify_center, a.my_md, a.gap_md]}>
<Button
variant="solid"
color="primary"
size="large"
style={[a.rounded_full]}
label={_(msg`Go back`)}
onPress={() => {
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.navigate('Home')
}
}}>
<ButtonText>
<Trans>Go back</Trans>
</ButtonText>
</Button>
{!modui.noOverride && (
<Button
variant="solid"
color="secondary"
size="large"
style={[a.rounded_full]}
label={_(msg`Show anyway`)}
onPress={() => setOverride(v => !v)}>
<ButtonText>
<Trans>Show anyway</Trans>
</ButtonText>
</Button>
)}
</View>
</CenteredView>
)
}