Implement label handling
This commit is contained in:
@@ -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}"`,
|
||||
|
||||
@@ -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} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
@@ -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} />
|
||||
)
|
||||
}
|
||||
+63
-12
@@ -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>
|
||||
)
|
||||
}
|
||||
+38
-17
@@ -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"
|
||||
@@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user