Collapse label appeal flow into ModerationDetailsDialog (#11091)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
+83
-46
@@ -66,8 +66,60 @@ export function Label({
|
|||||||
const isLabeler = Boolean(desc.sourceType && desc.sourceDid)
|
const isLabeler = Boolean(desc.sourceType && desc.sourceDid)
|
||||||
const isBlueskyLabel =
|
const isBlueskyLabel =
|
||||||
desc.sourceType === 'labeler' && desc.sourceDid === BSKY_LABELER_DID
|
desc.sourceType === 'labeler' && desc.sourceDid === BSKY_LABELER_DID
|
||||||
|
const avi = size === 'lg' ? 16 : 12
|
||||||
|
|
||||||
const {outer, avi, text} = useMemo(() => {
|
return (
|
||||||
|
<>
|
||||||
|
<LabelBase
|
||||||
|
label={desc.name}
|
||||||
|
size={size}
|
||||||
|
noBg={noBg}
|
||||||
|
disabled={disableDetailsDialog}
|
||||||
|
onPress={() => control.open()}
|
||||||
|
icon={
|
||||||
|
isBlueskyLabel || !isLabeler ? (
|
||||||
|
<desc.icon width={avi} fill={t.atoms.text_contrast_medium.color} />
|
||||||
|
) : (
|
||||||
|
<UserAvatar avatar={desc.sourceAvi} type="user" size={avi} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!disableDetailsDialog && (
|
||||||
|
<ModerationDetailsDialog control={control} modcause={cause} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LabelBaseProps = {
|
||||||
|
/**
|
||||||
|
* The accessibility label for the pill.
|
||||||
|
*/
|
||||||
|
label: string
|
||||||
|
/**
|
||||||
|
* The visible pill text. Defaults to `label`. Use this when the visible
|
||||||
|
* text is too terse to serve as the accessibility label, e.g. "+2".
|
||||||
|
*/
|
||||||
|
cta?: string
|
||||||
|
onPress: () => void
|
||||||
|
disabled?: boolean
|
||||||
|
noBg?: boolean
|
||||||
|
icon?: React.ReactNode
|
||||||
|
} & CommonProps
|
||||||
|
|
||||||
|
export function LabelBase({
|
||||||
|
label,
|
||||||
|
cta = label,
|
||||||
|
onPress,
|
||||||
|
disabled,
|
||||||
|
size = 'sm',
|
||||||
|
noBg,
|
||||||
|
icon,
|
||||||
|
}: LabelBaseProps) {
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
const {outer, text} = useMemo(() => {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 'lg': {
|
case 'lg': {
|
||||||
return {
|
return {
|
||||||
@@ -79,7 +131,6 @@ export function Label({
|
|||||||
paddingVertical: 5,
|
paddingVertical: 5,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
avi: 16,
|
|
||||||
text: [a.text_sm],
|
text: [a.text_sm],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +145,6 @@ export function Label({
|
|||||||
paddingVertical: 3,
|
paddingVertical: 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
avi: 12,
|
|
||||||
text: [a.text_xs],
|
text: [a.text_xs],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,52 +152,39 @@ export function Label({
|
|||||||
}, [t, size, noBg])
|
}, [t, size, noBg])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Button
|
||||||
<Button
|
disabled={disabled}
|
||||||
disabled={disableDetailsDialog}
|
label={label}
|
||||||
label={desc.name}
|
onPress={e => {
|
||||||
onPress={e => {
|
e.preventDefault()
|
||||||
e.preventDefault()
|
e.stopPropagation()
|
||||||
e.stopPropagation()
|
onPress()
|
||||||
control.open()
|
}}>
|
||||||
}}>
|
{({hovered, pressed}) => (
|
||||||
{({hovered, pressed}) => (
|
<View
|
||||||
<View
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.rounded_full,
|
||||||
|
outer,
|
||||||
|
(hovered || pressed) && t.atoms.bg_contrast_50,
|
||||||
|
]}>
|
||||||
|
{icon}
|
||||||
|
|
||||||
|
<Text
|
||||||
|
emoji
|
||||||
style={[
|
style={[
|
||||||
a.flex_row,
|
text,
|
||||||
a.align_center,
|
a.font_semi_bold,
|
||||||
a.rounded_full,
|
a.leading_tight,
|
||||||
outer,
|
t.atoms.text_contrast_medium,
|
||||||
(hovered || pressed) && t.atoms.bg_contrast_50,
|
{paddingRight: 3},
|
||||||
]}>
|
]}>
|
||||||
{isBlueskyLabel || !isLabeler ? (
|
{cta}
|
||||||
<desc.icon
|
</Text>
|
||||||
width={avi}
|
</View>
|
||||||
fill={t.atoms.text_contrast_medium.color}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<UserAvatar avatar={desc.sourceAvi} type="user" size={avi} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Text
|
|
||||||
emoji
|
|
||||||
style={[
|
|
||||||
text,
|
|
||||||
a.font_semi_bold,
|
|
||||||
a.leading_tight,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
{paddingRight: 3},
|
|
||||||
]}>
|
|
||||||
{desc.name}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{!disableDetailsDialog && (
|
|
||||||
<ModerationDetailsDialog control={control} modcause={cause} />
|
|
||||||
)}
|
)}
|
||||||
</>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -321,7 +321,11 @@ export function QuoteEmbed({
|
|||||||
linkDisabled
|
linkDisabled
|
||||||
/>
|
/>
|
||||||
{moderation ? (
|
{moderation ? (
|
||||||
<PostAlerts modui={moderation.ui('contentView')} style={[a.py_xs]} />
|
<PostAlerts
|
||||||
|
post={quote}
|
||||||
|
modui={moderation.ui('contentView')}
|
||||||
|
style={[a.py_xs]}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{richText ? (
|
{richText ? (
|
||||||
<RichText
|
<RichText
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import {useState} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {type ComAtprotoLabelDefs, ToolsOzoneReportDefs} from '@atproto/api'
|
||||||
|
import {XRPCError} from '@atproto/api'
|
||||||
|
import {msg} from '@lingui/core/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {Trans} from '@lingui/react/macro'
|
||||||
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useLabelSubject} from '#/lib/moderation'
|
||||||
|
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
|
||||||
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {useAgent} from '#/state/session'
|
||||||
|
import {atoms as a, useBreakpoints} from '#/alf'
|
||||||
|
import {Admonition} from '#/components/Admonition'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {InlineLinkText} from '#/components/Link'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import * as Toast from '#/components/Toast'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {IS_ANDROID} from '#/env'
|
||||||
|
|
||||||
|
export function AppealForm({
|
||||||
|
label,
|
||||||
|
control,
|
||||||
|
onPressBack,
|
||||||
|
}: {
|
||||||
|
label: ComAtprotoLabelDefs.Label
|
||||||
|
control: Dialog.DialogOuterProps['control']
|
||||||
|
onPressBack: () => void
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {labeler, strings} = useLabelInfo(label)
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const [details, setDetails] = useState('')
|
||||||
|
const {subject} = useLabelSubject({label})
|
||||||
|
const isAccountReport = 'did' in subject
|
||||||
|
const agent = useAgent()
|
||||||
|
const sourceName = labeler
|
||||||
|
? sanitizeHandle(labeler.creator.handle, '@')
|
||||||
|
: label.src
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const {mutate, isPending} = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const $type = !isAccountReport
|
||||||
|
? 'com.atproto.repo.strongRef'
|
||||||
|
: 'com.atproto.admin.defs#repoRef'
|
||||||
|
await agent.createModerationReport(
|
||||||
|
{
|
||||||
|
reasonType: ToolsOzoneReportDefs.REASONAPPEAL,
|
||||||
|
subject: {
|
||||||
|
$type,
|
||||||
|
...subject,
|
||||||
|
},
|
||||||
|
reason: details,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
encoding: 'application/json',
|
||||||
|
headers: {
|
||||||
|
'atproto-proxy': `${label.src}#atproto_labeler`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
if (err instanceof XRPCError && err.error === 'AlreadyAppealed') {
|
||||||
|
setError(
|
||||||
|
_(
|
||||||
|
msg`You've already appealed this label and it's being reviewed by our moderation team.`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setError(_(msg`Failed to submit appeal, please try again.`))
|
||||||
|
}
|
||||||
|
logger.error('Failed to submit label appeal', {message: err})
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
control.close()
|
||||||
|
Toast.show(_(msg({message: 'Appeal submitted', context: 'toast'})))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = () => mutate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View>
|
||||||
|
<Text style={[a.text_2xl, a.font_semi_bold, a.pb_xs, a.leading_tight]}>
|
||||||
|
<Trans>Appeal "{strings.name}" label</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.text_md, a.leading_snug]}>
|
||||||
|
<Trans>
|
||||||
|
This appeal will be sent to{' '}
|
||||||
|
<InlineLinkText
|
||||||
|
label={sourceName}
|
||||||
|
to={makeProfileLink(
|
||||||
|
labeler ? labeler.creator : {did: label.src, handle: ''},
|
||||||
|
)}
|
||||||
|
onPress={() => control.close()}
|
||||||
|
style={[a.text_md, a.leading_snug]}>
|
||||||
|
{sourceName}
|
||||||
|
</InlineLinkText>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{error && (
|
||||||
|
<Admonition type="error" style={[a.mt_sm]}>
|
||||||
|
{error}
|
||||||
|
</Admonition>
|
||||||
|
)}
|
||||||
|
<View style={[a.my_md]}>
|
||||||
|
<Dialog.Input
|
||||||
|
label={_(msg`Text input field`)}
|
||||||
|
placeholder={_(
|
||||||
|
msg`Please explain why you think this label was incorrectly applied by ${
|
||||||
|
labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src
|
||||||
|
}`,
|
||||||
|
)}
|
||||||
|
value={details}
|
||||||
|
onChangeText={setDetails}
|
||||||
|
autoFocus={true}
|
||||||
|
numberOfLines={3}
|
||||||
|
multiline
|
||||||
|
maxLength={300}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
gtMobile
|
||||||
|
? [a.flex_row, a.justify_between]
|
||||||
|
: [{flexDirection: 'column-reverse'}, a.gap_sm]
|
||||||
|
}>
|
||||||
|
<Button
|
||||||
|
testID="backBtn"
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
onPress={onPressBack}
|
||||||
|
label={_(msg`Back`)}>
|
||||||
|
<ButtonText>{_(msg`Back`)}</ButtonText>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
testID="submitBtn"
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
onPress={onSubmit}
|
||||||
|
label={_(msg`Submit`)}>
|
||||||
|
<ButtonText>{_(msg`Submit`)}</ButtonText>
|
||||||
|
{isPending && <ButtonIcon icon={Loader} />}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
{IS_ANDROID && <View style={{height: 300}} />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
import {type AppBskyFeedDefs, type ComAtprotoLabelDefs} from '@atproto/api'
|
import {type ComAtprotoLabelDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {Plural} from '@lingui/react/macro'
|
import {Plural} from '@lingui/react/macro'
|
||||||
|
|
||||||
|
import {filterUserFacingLabels} from '#/lib/moderation'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {
|
import {
|
||||||
@@ -19,12 +20,10 @@ import {
|
|||||||
} from '#/components/moderation/LabelsOnMeDialog'
|
} from '#/components/moderation/LabelsOnMeDialog'
|
||||||
|
|
||||||
export function LabelsOnMe({
|
export function LabelsOnMe({
|
||||||
type,
|
|
||||||
labels,
|
labels,
|
||||||
size,
|
size,
|
||||||
style,
|
style,
|
||||||
}: {
|
}: {
|
||||||
type: 'account' | 'content'
|
|
||||||
labels: ComAtprotoLabelDefs.Label[] | undefined
|
labels: ComAtprotoLabelDefs.Label[] | undefined
|
||||||
size?: ButtonSize
|
size?: ButtonSize
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
@@ -36,18 +35,14 @@ export function LabelsOnMe({
|
|||||||
if (!labels || !currentAccount) {
|
if (!labels || !currentAccount) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
labels = labels.filter(
|
labels = filterUserFacingLabels(labels, currentAccount.did)
|
||||||
l =>
|
|
||||||
!l.val.startsWith('!') &&
|
|
||||||
!(l.val === 'bot' && l.src === currentAccount.did),
|
|
||||||
)
|
|
||||||
if (!labels.length) {
|
if (!labels.length) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_row, style]}>
|
<View style={[a.flex_row, style]}>
|
||||||
<LabelsOnMeDialog control={control} labels={labels} type={type} />
|
<LabelsOnMeDialog control={control} labels={labels} type="account" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@@ -59,37 +54,13 @@ export function LabelsOnMe({
|
|||||||
}}>
|
}}>
|
||||||
<ButtonIcon position="left" icon={CircleInfo} />
|
<ButtonIcon position="left" icon={CircleInfo} />
|
||||||
<ButtonText style={[a.leading_snug]}>
|
<ButtonText style={[a.leading_snug]}>
|
||||||
{type === 'account' ? (
|
<Plural
|
||||||
<Plural
|
value={labels.length}
|
||||||
value={labels.length}
|
one="# account label"
|
||||||
one="# account label"
|
other="# account labels"
|
||||||
other="# account labels"
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Plural
|
|
||||||
value={labels.length}
|
|
||||||
one="# content label"
|
|
||||||
other="# content labels"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</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 type="content" labels={post.labels} size="tiny" style={style} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,35 +1,34 @@
|
|||||||
import {useCallback, useMemo, useState} from 'react'
|
import {useMemo, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {type ComAtprotoLabelDefs, ToolsOzoneReportDefs} from '@atproto/api'
|
import {type ComAtprotoLabelDefs} from '@atproto/api'
|
||||||
import {XRPCError} from '@atproto/api'
|
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {Trans} from '@lingui/react/macro'
|
import {Trans} from '@lingui/react/macro'
|
||||||
import {useMutation} from '@tanstack/react-query'
|
|
||||||
|
|
||||||
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||||
import {useLabelSubject} from '#/lib/moderation'
|
|
||||||
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
|
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {logger} from '#/logger'
|
import {useSession} from '#/state/session'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {Admonition} from '#/components/Admonition'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import * as Toast from '#/components/Toast'
|
import {AppealForm} from '#/components/moderation/AppealForm'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {IS_ANDROID} from '#/env'
|
|
||||||
import {Admonition} from '../Admonition'
|
|
||||||
import {Divider} from '../Divider'
|
import {Divider} from '../Divider'
|
||||||
import {Loader} from '../Loader'
|
|
||||||
|
|
||||||
export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog'
|
export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog'
|
||||||
|
|
||||||
export interface LabelsOnMeDialogProps {
|
export interface LabelsOnMeDialogProps {
|
||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
labels: ComAtprotoLabelDefs.Label[]
|
labels: ComAtprotoLabelDefs.Label[]
|
||||||
|
/**
|
||||||
|
* Whether the labels are being shown on the user's account or on a post.
|
||||||
|
* With `content`, the list may include account-level labels, which get a
|
||||||
|
* per-label callout.
|
||||||
|
*/
|
||||||
type: 'account' | 'content'
|
type: 'account' | 'content'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +61,7 @@ function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
|
|||||||
label={
|
label={
|
||||||
isAccount
|
isAccount
|
||||||
? _(msg`The following labels were applied to your account.`)
|
? _(msg`The following labels were applied to your account.`)
|
||||||
: _(msg`The following labels were applied to your content.`)
|
: _(msg`The following labels were applied to this post.`)
|
||||||
}>
|
}>
|
||||||
{appealingLabel ? (
|
{appealingLabel ? (
|
||||||
<AppealForm
|
<AppealForm
|
||||||
@@ -76,7 +75,7 @@ function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
|
|||||||
{isAccount ? (
|
{isAccount ? (
|
||||||
<Trans>Labels on your account</Trans>
|
<Trans>Labels on your account</Trans>
|
||||||
) : (
|
) : (
|
||||||
<Trans>Labels on your content</Trans>
|
<Trans>Labels applied to this post</Trans>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[a.text_md, a.leading_snug]}>
|
<Text style={[a.text_md, a.leading_snug]}>
|
||||||
@@ -96,9 +95,10 @@ function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
|
|||||||
<View style={[a.py_lg, a.gap_md]}>
|
<View style={[a.py_lg, a.gap_md]}>
|
||||||
{labels.map(label => (
|
{labels.map(label => (
|
||||||
<Label
|
<Label
|
||||||
key={`${label.val}-${label.src}`}
|
key={`${label.val}-${label.src}-${label.uri}`}
|
||||||
label={label}
|
label={label}
|
||||||
isSelfLabel={label.src === currentAccount?.did}
|
isSelfLabel={label.src === currentAccount?.did}
|
||||||
|
showAccountCallout={!isAccount && label.uri.startsWith('did:')}
|
||||||
control={props.control}
|
control={props.control}
|
||||||
onPressAppeal={setAppealingLabel}
|
onPressAppeal={setAppealingLabel}
|
||||||
/>
|
/>
|
||||||
@@ -114,11 +114,17 @@ function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
|
|||||||
function Label({
|
function Label({
|
||||||
label,
|
label,
|
||||||
isSelfLabel,
|
isSelfLabel,
|
||||||
|
showAccountCallout,
|
||||||
control,
|
control,
|
||||||
onPressAppeal,
|
onPressAppeal,
|
||||||
}: {
|
}: {
|
||||||
label: ComAtprotoLabelDefs.Label
|
label: ComAtprotoLabelDefs.Label
|
||||||
isSelfLabel: boolean
|
isSelfLabel: boolean
|
||||||
|
/**
|
||||||
|
* Call out that the label applies to the whole account, for contexts that
|
||||||
|
* mix account and content labels.
|
||||||
|
*/
|
||||||
|
showAccountCallout?: boolean
|
||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
onPressAppeal: (label: ComAtprotoLabelDefs.Label) => void
|
onPressAppeal: (label: ComAtprotoLabelDefs.Label) => void
|
||||||
}) {
|
}) {
|
||||||
@@ -145,12 +151,20 @@ function Label({
|
|||||||
<Text emoji style={[t.atoms.text_contrast_medium, a.leading_snug]}>
|
<Text emoji style={[t.atoms.text_contrast_medium, a.leading_snug]}>
|
||||||
{strings.description}
|
{strings.description}
|
||||||
</Text>
|
</Text>
|
||||||
|
{showAccountCallout && (
|
||||||
|
<Admonition type="info" style={[a.mt_sm]}>
|
||||||
|
<Trans>
|
||||||
|
This label was applied to the entire user account and will
|
||||||
|
appear on all posts.
|
||||||
|
</Trans>
|
||||||
|
</Admonition>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
{!isSelfLabel && (
|
{!isSelfLabel && (
|
||||||
<View>
|
<View>
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary"
|
color="primary_subtle"
|
||||||
size="small"
|
size="small"
|
||||||
label={_(msg`Appeal`)}
|
label={_(msg`Appeal`)}
|
||||||
onPress={() => onPressAppeal(label)}>
|
onPress={() => onPressAppeal(label)}>
|
||||||
@@ -211,141 +225,3 @@ function Label({
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppealForm({
|
|
||||||
label,
|
|
||||||
control,
|
|
||||||
onPressBack,
|
|
||||||
}: {
|
|
||||||
label: ComAtprotoLabelDefs.Label
|
|
||||||
control: Dialog.DialogOuterProps['control']
|
|
||||||
onPressBack: () => void
|
|
||||||
}) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {labeler, strings} = useLabelInfo(label)
|
|
||||||
const {gtMobile} = useBreakpoints()
|
|
||||||
const [details, setDetails] = useState('')
|
|
||||||
const {subject} = useLabelSubject({label})
|
|
||||||
const isAccountReport = 'did' in subject
|
|
||||||
const agent = useAgent()
|
|
||||||
const sourceName = labeler
|
|
||||||
? sanitizeHandle(labeler.creator.handle, '@')
|
|
||||||
: label.src
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const {mutate, isPending} = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
const $type = !isAccountReport
|
|
||||||
? 'com.atproto.repo.strongRef'
|
|
||||||
: 'com.atproto.admin.defs#repoRef'
|
|
||||||
await agent.createModerationReport(
|
|
||||||
{
|
|
||||||
reasonType: ToolsOzoneReportDefs.REASONAPPEAL,
|
|
||||||
subject: {
|
|
||||||
$type,
|
|
||||||
...subject,
|
|
||||||
},
|
|
||||||
reason: details,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
encoding: 'application/json',
|
|
||||||
headers: {
|
|
||||||
'atproto-proxy': `${label.src}#atproto_labeler`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onError: err => {
|
|
||||||
if (err instanceof XRPCError && err.error === 'AlreadyAppealed') {
|
|
||||||
setError(
|
|
||||||
_(
|
|
||||||
msg`You've already appealed this label and it's being reviewed by our moderation team.`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setError(_(msg`Failed to submit appeal, please try again.`))
|
|
||||||
}
|
|
||||||
logger.error('Failed to submit label appeal', {message: err})
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
control.close()
|
|
||||||
Toast.show(_(msg({message: 'Appeal submitted', context: 'toast'})))
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSubmit = useCallback(() => mutate(), [mutate])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View>
|
|
||||||
<Text style={[a.text_2xl, a.font_semi_bold, a.pb_xs, a.leading_tight]}>
|
|
||||||
<Trans>Appeal "{strings.name}" label</Trans>
|
|
||||||
</Text>
|
|
||||||
<Text style={[a.text_md, a.leading_snug]}>
|
|
||||||
<Trans>
|
|
||||||
This appeal will be sent to{' '}
|
|
||||||
<InlineLinkText
|
|
||||||
label={sourceName}
|
|
||||||
to={makeProfileLink(
|
|
||||||
labeler ? labeler.creator : {did: label.src, handle: ''},
|
|
||||||
)}
|
|
||||||
onPress={() => control.close()}
|
|
||||||
style={[a.text_md, a.leading_snug]}>
|
|
||||||
{sourceName}
|
|
||||||
</InlineLinkText>
|
|
||||||
.
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
{error && (
|
|
||||||
<Admonition type="error" style={[a.mt_sm]}>
|
|
||||||
{error}
|
|
||||||
</Admonition>
|
|
||||||
)}
|
|
||||||
<View style={[a.my_md]}>
|
|
||||||
<Dialog.Input
|
|
||||||
label={_(msg`Text input field`)}
|
|
||||||
placeholder={_(
|
|
||||||
msg`Please explain why you think this label was incorrectly applied by ${
|
|
||||||
labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src
|
|
||||||
}`,
|
|
||||||
)}
|
|
||||||
value={details}
|
|
||||||
onChangeText={setDetails}
|
|
||||||
autoFocus={true}
|
|
||||||
numberOfLines={3}
|
|
||||||
multiline
|
|
||||||
maxLength={300}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={
|
|
||||||
gtMobile
|
|
||||||
? [a.flex_row, a.justify_between]
|
|
||||||
: [{flexDirection: 'column-reverse'}, a.gap_sm]
|
|
||||||
}>
|
|
||||||
<Button
|
|
||||||
testID="backBtn"
|
|
||||||
variant="solid"
|
|
||||||
color="secondary"
|
|
||||||
size="large"
|
|
||||||
onPress={onPressBack}
|
|
||||||
label={_(msg`Back`)}>
|
|
||||||
<ButtonText>{_(msg`Back`)}</ButtonText>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
testID="submitBtn"
|
|
||||||
variant="solid"
|
|
||||||
color="primary"
|
|
||||||
size="large"
|
|
||||||
onPress={onSubmit}
|
|
||||||
label={_(msg`Submit`)}>
|
|
||||||
<ButtonText>{_(msg`Submit`)}</ButtonText>
|
|
||||||
{isPending && <ButtonIcon icon={Loader} />}
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
{IS_ANDROID && <View style={{height: 300}} />}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {type ModerationCause} from '@atproto/api'
|
import {type ModerationCause} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
@@ -9,10 +10,12 @@ import {useModerationCauseDescription} from '#/lib/moderation/useModerationCause
|
|||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {listUriToHref} from '#/lib/strings/url-helpers'
|
import {listUriToHref} from '#/lib/strings/url-helpers'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {atoms as a, useGutters, useTheme, web} from '#/alf'
|
import {atoms as a, useBreakpoints, useGutters, useTheme, web} from '#/alf'
|
||||||
import {Admonition} from '#/components/Admonition'
|
import {Admonition} from '#/components/Admonition'
|
||||||
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
|
import {AppealForm} from '#/components/moderation/AppealForm'
|
||||||
import {type AppModerationCause} from '#/components/Pills'
|
import {type AppModerationCause} from '#/components/Pills'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
@@ -47,6 +50,19 @@ function ModerationDetailsDialogInner({
|
|||||||
const desc = useModerationCauseDescription(modcause)
|
const desc = useModerationCauseDescription(modcause)
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const timeDiff = useGetTimeAgo({future: true})
|
const timeDiff = useGetTimeAgo({future: true})
|
||||||
|
const [isAppealing, setIsAppealing] = useState(false)
|
||||||
|
const {gtPhone} = useBreakpoints()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Appeal eligibility: only for label causes on content belonging to the
|
||||||
|
* current user, where the label was not self-applied.
|
||||||
|
*/
|
||||||
|
const canAppeal =
|
||||||
|
modcause?.type === 'label' &&
|
||||||
|
!!currentAccount &&
|
||||||
|
modcause.label.src !== currentAccount.did &&
|
||||||
|
(modcause.label.uri === currentAccount.did ||
|
||||||
|
modcause.label.uri.startsWith(`at://${currentAccount.did}/`))
|
||||||
|
|
||||||
let name
|
let name
|
||||||
let description
|
let description
|
||||||
@@ -137,6 +153,23 @@ function ModerationDetailsDialogInner({
|
|||||||
const sourceName =
|
const sourceName =
|
||||||
desc.source || desc.sourceDisplayName || _(msg`an unknown labeler`)
|
desc.source || desc.sourceDisplayName || _(msg`an unknown labeler`)
|
||||||
|
|
||||||
|
if (isAppealing && modcause?.type === 'label') {
|
||||||
|
return (
|
||||||
|
<Dialog.ScrollableInner
|
||||||
|
label={_(msg`Appeal label`)}
|
||||||
|
style={web({
|
||||||
|
maxWidth: 460,
|
||||||
|
})}>
|
||||||
|
<AppealForm
|
||||||
|
label={modcause.label}
|
||||||
|
control={control}
|
||||||
|
onPressBack={() => setIsAppealing(false)}
|
||||||
|
/>
|
||||||
|
<Dialog.Close />
|
||||||
|
</Dialog.ScrollableInner>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
label={_(msg`Moderation details`)}
|
label={_(msg`Moderation details`)}
|
||||||
@@ -156,11 +189,48 @@ function ModerationDetailsDialogInner({
|
|||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{canAppeal && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.flex_wrap,
|
||||||
|
a.gap_sm,
|
||||||
|
a.pt_md,
|
||||||
|
a.pb_xs,
|
||||||
|
a.mt_md,
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_sm,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
gtPhone ? a.flex_1 : a.w_full,
|
||||||
|
]}>
|
||||||
|
<Trans>
|
||||||
|
You may appeal these labels if you feel they were placed in
|
||||||
|
error.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
color="primary_subtle"
|
||||||
|
size="small"
|
||||||
|
label={_(msg`Appeal this label`)}
|
||||||
|
style={[gtPhone ? undefined : a.w_full]}
|
||||||
|
onPress={() => setIsAppealing(true)}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Appeal</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{desc.isSubjectAccount && (
|
{desc.isSubjectAccount && (
|
||||||
<Admonition type="info" style={[a.mt_md]}>
|
<Admonition type="info" style={[a.mt_md]}>
|
||||||
<Trans>
|
<Trans>
|
||||||
This moderation was applied to the entire user account and will
|
This label was applied to the entire user account and will appear
|
||||||
appear on all posts.
|
on all posts.
|
||||||
</Trans>
|
</Trans>
|
||||||
</Admonition>
|
</Admonition>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,28 +1,92 @@
|
|||||||
import {type StyleProp, type ViewStyle} from 'react-native'
|
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
import {type ModerationCause, type ModerationUI} from '@atproto/api'
|
import {
|
||||||
|
type AppBskyFeedDefs,
|
||||||
|
type ComAtprotoLabelDefs,
|
||||||
|
type ModerationCause,
|
||||||
|
type ModerationUI,
|
||||||
|
} from '@atproto/api'
|
||||||
|
import {plural} from '@lingui/core/macro'
|
||||||
|
import {useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {getModerationCauseKey, unique} from '#/lib/moderation'
|
import {
|
||||||
|
filterUserFacingLabels,
|
||||||
|
getModerationCauseKey,
|
||||||
|
unique,
|
||||||
|
} from '#/lib/moderation'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {
|
||||||
|
LabelsOnMeDialog,
|
||||||
|
useLabelsOnMeDialogControl,
|
||||||
|
} from '#/components/moderation/LabelsOnMeDialog'
|
||||||
import * as Pills from '#/components/Pills'
|
import * as Pills from '#/components/Pills'
|
||||||
|
|
||||||
export function PostAlerts({
|
export function PostAlerts({
|
||||||
|
post,
|
||||||
modui,
|
modui,
|
||||||
size = 'sm',
|
view = 'compact',
|
||||||
style,
|
style,
|
||||||
additionalCauses,
|
additionalCauses,
|
||||||
}: {
|
}: {
|
||||||
|
post?: AppBskyFeedDefs.PostView
|
||||||
modui: ModerationUI
|
modui: ModerationUI
|
||||||
size?: Pills.CommonProps['size']
|
/**
|
||||||
|
* Expanded views (e.g. the thread anchor post) render larger pills and
|
||||||
|
* surface the "+n" additional labels pill. Compact views (feeds, replies)
|
||||||
|
* keep the alerts minimal.
|
||||||
|
*/
|
||||||
|
view?: 'expanded' | 'compact'
|
||||||
includeMute?: boolean
|
includeMute?: boolean
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
additionalCauses?: ModerationCause[] | Pills.AppModerationCause[]
|
additionalCauses?: ModerationCause[] | Pills.AppModerationCause[]
|
||||||
}) {
|
}) {
|
||||||
if (!modui.alert && !modui.inform && !additionalCauses?.length) {
|
const {currentAccount} = useSession()
|
||||||
|
const size: Pills.CommonProps['size'] = view === 'expanded' ? 'lg' : 'sm'
|
||||||
|
|
||||||
|
const alerts = modui.alerts.filter(unique)
|
||||||
|
const informs = modui.informs.filter(unique)
|
||||||
|
/*
|
||||||
|
* The "+n" pill surfaces labels for the author to review and appeal, so it
|
||||||
|
* only applies when the viewer is the author, and only in expanded views.
|
||||||
|
* It renders even when no other moderation is visible, since it may be the
|
||||||
|
* author's only entry point to appeal labels on their content.
|
||||||
|
*/
|
||||||
|
const isOwnPost = !!post && post.author.did === currentAccount?.did
|
||||||
|
const allLabels: ComAtprotoLabelDefs.Label[] =
|
||||||
|
isOwnPost && view === 'expanded'
|
||||||
|
? [...(post.labels ?? []), ...(post.author.labels ?? [])]
|
||||||
|
: []
|
||||||
|
/*
|
||||||
|
* Labels that the moderation system already surfaces in this context -
|
||||||
|
* whether as an alert, an inform, or a blur handled by ContentHider - should
|
||||||
|
* not be repeated in the "+n" pill.
|
||||||
|
*/
|
||||||
|
const shownCauses = [...alerts, ...informs, ...modui.blurs]
|
||||||
|
const additionalLabels = filterUserFacingLabels(
|
||||||
|
allLabels,
|
||||||
|
currentAccount?.did,
|
||||||
|
).filter(label =>
|
||||||
|
shownCauses.every(
|
||||||
|
cause =>
|
||||||
|
cause.type !== 'label' ||
|
||||||
|
cause.label.val !== label.val ||
|
||||||
|
cause.label.src !== label.src ||
|
||||||
|
cause.label.uri !== label.uri,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!modui.alert &&
|
||||||
|
!modui.inform &&
|
||||||
|
!additionalCauses?.length &&
|
||||||
|
!additionalLabels.length
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pills.Row size={size} style={[size === 'sm' && {marginLeft: -3}, style]}>
|
<Pills.Row size={size} style={[size === 'sm' && {marginLeft: -3}, style]}>
|
||||||
{modui.alerts.filter(unique).map(cause => (
|
{alerts.map(cause => (
|
||||||
<Pills.Label
|
<Pills.Label
|
||||||
key={getModerationCauseKey(cause)}
|
key={getModerationCauseKey(cause)}
|
||||||
cause={cause}
|
cause={cause}
|
||||||
@@ -30,7 +94,7 @@ export function PostAlerts({
|
|||||||
noBg={size === 'sm'}
|
noBg={size === 'sm'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{modui.informs.filter(unique).map(cause => (
|
{informs.map(cause => (
|
||||||
<Pills.Label
|
<Pills.Label
|
||||||
key={getModerationCauseKey(cause)}
|
key={getModerationCauseKey(cause)}
|
||||||
cause={cause}
|
cause={cause}
|
||||||
@@ -46,6 +110,60 @@ export function PostAlerts({
|
|||||||
noBg={size === 'sm'}
|
noBg={size === 'sm'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{additionalLabels.length ? (
|
||||||
|
<AdditionalLabels
|
||||||
|
labels={additionalLabels}
|
||||||
|
size={size}
|
||||||
|
hasPrecedingPills={
|
||||||
|
alerts.length > 0 ||
|
||||||
|
informs.length > 0 ||
|
||||||
|
(additionalCauses?.length ?? 0) > 0
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</Pills.Row>
|
</Pills.Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AdditionalLabels({
|
||||||
|
labels,
|
||||||
|
size,
|
||||||
|
hasPrecedingPills,
|
||||||
|
}: {
|
||||||
|
labels: ComAtprotoLabelDefs.Label[]
|
||||||
|
size?: Pills.CommonProps['size']
|
||||||
|
/**
|
||||||
|
* The compact "+n" syntax only makes sense as a continuation of other
|
||||||
|
* pills. When this pill stands alone, spell it out.
|
||||||
|
*/
|
||||||
|
hasPrecedingPills: boolean
|
||||||
|
}) {
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
const control = useLabelsOnMeDialogControl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.flex_row]}>
|
||||||
|
<LabelsOnMeDialog control={control} labels={labels} type="content" />
|
||||||
|
|
||||||
|
<Pills.LabelBase
|
||||||
|
label={l`${plural(labels.length, {
|
||||||
|
one: '# label applied to your post',
|
||||||
|
other: '# labels applied to your post',
|
||||||
|
})}`}
|
||||||
|
cta={
|
||||||
|
hasPrecedingPills
|
||||||
|
? l`+${labels.length}`
|
||||||
|
: l`${plural(labels.length, {
|
||||||
|
one: '# label applied',
|
||||||
|
other: '# labels applied',
|
||||||
|
})}`
|
||||||
|
}
|
||||||
|
size={size}
|
||||||
|
noBg={size === 'sm'}
|
||||||
|
onPress={() => {
|
||||||
|
control.open()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,6 +58,21 @@ export function labelIsHideableOffense(
|
|||||||
return ['!hide', '!takedown'].includes(label.val)
|
return ['!hide', '!takedown'].includes(label.val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters out labels that are not user-facing: system labels (val prefixed
|
||||||
|
* with `!`) and the user's own "bot" self-label.
|
||||||
|
*/
|
||||||
|
export function filterUserFacingLabels(
|
||||||
|
labels: ComAtprotoLabelDefs.Label[],
|
||||||
|
currentAccountDid: string | undefined,
|
||||||
|
): ComAtprotoLabelDefs.Label[] {
|
||||||
|
return labels.filter(
|
||||||
|
label =>
|
||||||
|
!label.val.startsWith('!') &&
|
||||||
|
!(label.val === 'bot' && label.src === currentAccountDid),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function getLabelingServiceTitle({
|
export function getLabelingServiceTitle({
|
||||||
displayName,
|
displayName,
|
||||||
handle,
|
handle,
|
||||||
|
|||||||
@@ -164,7 +164,6 @@ function DirectChatItem({
|
|||||||
isWithinLeftPanel ? null : (
|
isWithinLeftPanel ? null : (
|
||||||
<PostAlerts
|
<PostAlerts
|
||||||
modui={moderation.ui('contentList')}
|
modui={moderation.ui('contentList')}
|
||||||
size="sm"
|
|
||||||
style={[a.pb_2xs, a.max_w_full, a.overflow_hidden]}
|
style={[a.pb_2xs, a.max_w_full, a.overflow_hidden]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -240,9 +240,9 @@ function MessageInputPostEmbed({
|
|||||||
</View>
|
</View>
|
||||||
<ContentHider modui={moderation.ui('contentView')}>
|
<ContentHider modui={moderation.ui('contentView')}>
|
||||||
<PostAlerts
|
<PostAlerts
|
||||||
|
post={post}
|
||||||
modui={moderation.ui('contentView')}
|
modui={moderation.ui('contentView')}
|
||||||
style={a.py_xs}
|
style={a.py_xs}
|
||||||
size="sm"
|
|
||||||
/>
|
/>
|
||||||
{rt.text && (
|
{rt.text && (
|
||||||
<RichText
|
<RichText
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Tra
|
|||||||
import {GalleryBleed} from '#/components/images/Gallery'
|
import {GalleryBleed} from '#/components/images/Gallery'
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
|
||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
import {type AppModerationCause} from '#/components/Pills'
|
import {type AppModerationCause} from '#/components/Pills'
|
||||||
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||||
@@ -384,14 +383,14 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={[a.pb_sm]}>
|
<View style={[a.pb_sm]}>
|
||||||
<LabelsOnMyPost post={post} style={[a.pb_sm]} />
|
|
||||||
<ContentHider
|
<ContentHider
|
||||||
modui={moderation.ui('contentView')}
|
modui={moderation.ui('contentView')}
|
||||||
ignoreMute
|
ignoreMute
|
||||||
childContainerStyle={[a.pt_sm]}>
|
childContainerStyle={[a.pt_sm]}>
|
||||||
<PostAlerts
|
<PostAlerts
|
||||||
|
post={post}
|
||||||
modui={moderation.ui('contentView')}
|
modui={moderation.ui('contentView')}
|
||||||
size="lg"
|
view="expanded"
|
||||||
includeMute
|
includeMute
|
||||||
style={[a.pb_sm]}
|
style={[a.pb_sm]}
|
||||||
additionalCauses={additionalPostAlerts}
|
additionalCauses={additionalPostAlerts}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import {
|
|||||||
GalleryBleed,
|
GalleryBleed,
|
||||||
maybeApplyGalleryOffsetStyles,
|
maybeApplyGalleryOffsetStyles,
|
||||||
} from '#/components/images/Gallery'
|
} from '#/components/images/Gallery'
|
||||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
|
||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
import {PostHider} from '#/components/moderation/PostHider'
|
import {PostHider} from '#/components/moderation/PostHider'
|
||||||
import {type AppModerationCause} from '#/components/Pills'
|
import {type AppModerationCause} from '#/components/Pills'
|
||||||
@@ -310,8 +309,8 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
|
|||||||
}),
|
}),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<LabelsOnMyPost post={post} style={[a.pb_xs]} />
|
|
||||||
<PostAlerts
|
<PostAlerts
|
||||||
|
post={post}
|
||||||
modui={moderation.ui('contentList')}
|
modui={moderation.ui('contentList')}
|
||||||
style={[a.pb_2xs]}
|
style={[a.pb_2xs]}
|
||||||
additionalCauses={additionalPostAlerts}
|
additionalCauses={additionalPostAlerts}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
|
|||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
||||||
import {GalleryBleed} from '#/components/images/Gallery'
|
import {GalleryBleed} from '#/components/images/Gallery'
|
||||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
|
||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
import {PostHider} from '#/components/moderation/PostHider'
|
import {PostHider} from '#/components/moderation/PostHider'
|
||||||
import {type AppModerationCause} from '#/components/Pills'
|
import {type AppModerationCause} from '#/components/Pills'
|
||||||
@@ -340,8 +339,8 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
|
|||||||
<View style={[a.flex_row]}>
|
<View style={[a.flex_row]}>
|
||||||
<ThreadItemTreeReplyChildReplyLine item={item} />
|
<ThreadItemTreeReplyChildReplyLine item={item} />
|
||||||
<View style={[a.flex_1, a.pl_2xs]}>
|
<View style={[a.flex_1, a.pl_2xs]}>
|
||||||
<LabelsOnMyPost post={post} style={[a.pb_2xs]} />
|
|
||||||
<PostAlerts
|
<PostAlerts
|
||||||
|
post={post}
|
||||||
modui={moderation.ui('contentList')}
|
modui={moderation.ui('contentList')}
|
||||||
style={[a.pb_2xs]}
|
style={[a.pb_2xs]}
|
||||||
additionalCauses={additionalPostAlerts}
|
additionalCauses={additionalPostAlerts}
|
||||||
|
|||||||
@@ -228,7 +228,6 @@ let ProfileHeaderShell = ({
|
|||||||
{!isPlaceholderProfile &&
|
{!isPlaceholderProfile &&
|
||||||
(isMe ? (
|
(isMe ? (
|
||||||
<LabelsOnMe
|
<LabelsOnMe
|
||||||
type="account"
|
|
||||||
labels={profile.labels}
|
labels={profile.labels}
|
||||||
style={[
|
style={[
|
||||||
a.px_lg,
|
a.px_lg,
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import {
|
|||||||
maybeApplyGalleryOffsetStyles,
|
maybeApplyGalleryOffsetStyles,
|
||||||
} from '#/components/images/Gallery'
|
} from '#/components/images/Gallery'
|
||||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
|
||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||||
import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
|
import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
|
||||||
@@ -215,12 +214,12 @@ function PostInner({
|
|||||||
{replyAuthorDid !== '' && (
|
{replyAuthorDid !== '' && (
|
||||||
<PostRepliedTo parentAuthor={replyAuthorDid} />
|
<PostRepliedTo parentAuthor={replyAuthorDid} />
|
||||||
)}
|
)}
|
||||||
<LabelsOnMyPost post={post} />
|
|
||||||
<ContentHider
|
<ContentHider
|
||||||
modui={moderation.ui('contentView')}
|
modui={moderation.ui('contentView')}
|
||||||
style={styles.contentHider}
|
style={styles.contentHider}
|
||||||
childContainerStyle={styles.contentHiderChild}>
|
childContainerStyle={styles.contentHiderChild}>
|
||||||
<PostAlerts
|
<PostAlerts
|
||||||
|
post={post}
|
||||||
modui={moderation.ui('contentView')}
|
modui={moderation.ui('contentView')}
|
||||||
style={[a.pb_xs]}
|
style={[a.pb_xs]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import {
|
|||||||
maybeApplyGalleryOffsetStyles,
|
maybeApplyGalleryOffsetStyles,
|
||||||
} from '#/components/images/Gallery'
|
} from '#/components/images/Gallery'
|
||||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
|
||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
import {type AppModerationCause} from '#/components/Pills'
|
import {type AppModerationCause} from '#/components/Pills'
|
||||||
import {Embed} from '#/components/Post/Embed'
|
import {Embed} from '#/components/Post/Embed'
|
||||||
@@ -420,7 +419,6 @@ let FeedItemInner = ({
|
|||||||
isParentNotFound={isParentNotFound}
|
isParentNotFound={isParentNotFound}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<LabelsOnMyPost post={post} />
|
|
||||||
<PostContent
|
<PostContent
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
richText={richText}
|
richText={richText}
|
||||||
@@ -495,6 +493,7 @@ let PostContent = ({
|
|||||||
ignoreMute
|
ignoreMute
|
||||||
childContainerStyle={styles.contentHiderChild}>
|
childContainerStyle={styles.contentHiderChild}>
|
||||||
<PostAlerts
|
<PostAlerts
|
||||||
|
post={post}
|
||||||
modui={moderation.ui('contentList')}
|
modui={moderation.ui('contentList')}
|
||||||
style={[a.pb_xs]}
|
style={[a.pb_xs]}
|
||||||
additionalCauses={additionalPostAlerts}
|
additionalCauses={additionalPostAlerts}
|
||||||
|
|||||||
Reference in New Issue
Block a user