541a82c2dd
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
242 lines
5.3 KiB
TypeScript
242 lines
5.3 KiB
TypeScript
import {useMemo} from 'react'
|
|
import {View} from 'react-native'
|
|
import {api} from '@bsky.app/sdk'
|
|
import {type ModerationCause} from '@bsky.app/sdk/moderation'
|
|
import {Trans, useLingui} from '@lingui/react/macro'
|
|
|
|
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
|
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
|
|
import {Button} from '#/components/Button'
|
|
import {RepostStrike_Stroke2_Corner0_Rounded as RepostStrikeIcon} from '#/components/icons/Repost'
|
|
import {
|
|
ModerationDetailsDialog,
|
|
useModerationDetailsDialogControl,
|
|
} from '#/components/moderation/ModerationDetailsDialog'
|
|
import {Text} from '#/components/Typography'
|
|
|
|
export type AppModerationCause =
|
|
| ModerationCause
|
|
| {
|
|
type: 'reply-hidden'
|
|
source: {type: 'user'; did: string}
|
|
priority: 6
|
|
downgraded?: boolean
|
|
}
|
|
|
|
export type CommonProps = {
|
|
size?: 'sm' | 'lg'
|
|
}
|
|
|
|
export function Row({
|
|
children,
|
|
style,
|
|
size = 'sm',
|
|
}: {children: React.ReactNode | React.ReactNode[]} & CommonProps &
|
|
ViewStyleProp) {
|
|
const styles = useMemo(() => {
|
|
switch (size) {
|
|
case 'lg':
|
|
return [{gap: 5}]
|
|
case 'sm':
|
|
default:
|
|
return [{gap: 3}]
|
|
}
|
|
}, [size])
|
|
return (
|
|
<View style={[a.flex_row, a.flex_wrap, a.gap_xs, styles, style]}>
|
|
{children}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
export type LabelProps = {
|
|
cause: AppModerationCause
|
|
disableDetailsDialog?: boolean
|
|
noBg?: boolean
|
|
} & CommonProps
|
|
|
|
export function Label({
|
|
cause,
|
|
size = 'sm',
|
|
disableDetailsDialog,
|
|
noBg,
|
|
}: LabelProps) {
|
|
const t = useTheme()
|
|
const control = useModerationDetailsDialogControl()
|
|
const desc = useModerationCauseDescription(cause)
|
|
const isLabeler = Boolean(desc.sourceType && desc.sourceDid)
|
|
const isBlueskyLabel =
|
|
desc.sourceType === 'labeler' && desc.sourceDid === api.moderation.did
|
|
const avi = size === 'lg' ? 16 : 12
|
|
|
|
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) {
|
|
case 'lg': {
|
|
return {
|
|
outer: [
|
|
t.atoms.bg_contrast_25,
|
|
{
|
|
gap: 5,
|
|
paddingHorizontal: 5,
|
|
paddingVertical: 5,
|
|
},
|
|
],
|
|
text: [a.text_sm],
|
|
}
|
|
}
|
|
case 'sm':
|
|
default: {
|
|
return {
|
|
outer: [
|
|
!noBg && t.atoms.bg_contrast_25,
|
|
{
|
|
gap: 3,
|
|
paddingHorizontal: 3,
|
|
paddingVertical: 3,
|
|
},
|
|
],
|
|
text: [a.text_xs],
|
|
}
|
|
}
|
|
}
|
|
}, [t, size, noBg])
|
|
|
|
return (
|
|
<Button
|
|
disabled={disabled}
|
|
label={label}
|
|
onPress={e => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
onPress()
|
|
}}>
|
|
{({hovered, pressed}) => (
|
|
<View
|
|
style={[
|
|
a.flex_row,
|
|
a.align_center,
|
|
a.rounded_full,
|
|
outer,
|
|
(hovered || pressed) && t.atoms.bg_contrast_50,
|
|
]}>
|
|
{icon}
|
|
|
|
<Text
|
|
emoji
|
|
style={[
|
|
text,
|
|
a.font_semi_bold,
|
|
a.leading_tight,
|
|
a.flex_shrink,
|
|
t.atoms.text_contrast_medium,
|
|
{paddingRight: 3},
|
|
]}>
|
|
{cta}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</Button>
|
|
)
|
|
}
|
|
|
|
export function FollowsYou({size = 'sm'}: CommonProps) {
|
|
const t = useTheme()
|
|
|
|
const variantStyles = useMemo(() => {
|
|
switch (size) {
|
|
case 'sm':
|
|
case 'lg':
|
|
default:
|
|
return [
|
|
{
|
|
paddingHorizontal: 6,
|
|
paddingVertical: 3,
|
|
borderRadius: 4,
|
|
},
|
|
]
|
|
}
|
|
}, [size])
|
|
|
|
return (
|
|
<View style={[variantStyles, a.justify_center, t.atoms.bg_contrast_50]}>
|
|
<Text style={[a.text_xs, a.leading_tight]}>
|
|
<Trans>Follows you</Trans>
|
|
</Text>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
export function MutedOnlyReposts(props: CommonProps) {
|
|
const t = useTheme()
|
|
const {t: l} = useLingui()
|
|
|
|
const size = props.size === 'lg' ? 16 : 12
|
|
|
|
return (
|
|
<LabelBase
|
|
{...props}
|
|
label={l`Reposts Hidden`}
|
|
icon={
|
|
<RepostStrikeIcon
|
|
width={size}
|
|
fill={t.atoms.text_contrast_medium.color}
|
|
/>
|
|
}
|
|
disabled={true}
|
|
onPress={() => {}}
|
|
/>
|
|
)
|
|
}
|