450 lines
12 KiB
TypeScript
450 lines
12 KiB
TypeScript
import React from 'react'
|
|
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
|
import {View} from 'react-native'
|
|
import {
|
|
LABELS,
|
|
mock,
|
|
moderatePost,
|
|
moderateProfile,
|
|
PostModeration,
|
|
ProfileModeration,
|
|
ModerationUI,
|
|
AppBskyActorDefs,
|
|
AppBskyFeedDefs,
|
|
} from '@atproto/api'
|
|
|
|
import {atoms as a, useTheme} from '#/alf'
|
|
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
|
import {H1, H3, P} from '#/components/Typography'
|
|
import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
|
|
import * as Toggle from '#/components/forms/Toggle'
|
|
import * as ToggleButton from '#/components/forms/ToggleButton'
|
|
import {UserAvatar} from '../com/util/UserAvatar'
|
|
|
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
|
import {PostHider} from '../com/util/moderation/PostHider'
|
|
import {PostAlerts} from '../com/util/moderation/PostAlerts'
|
|
import {ContentHider} from '../com/util/moderation/ContentHider'
|
|
import {ProfileHeader} from '../com/profile/ProfileHeader'
|
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
|
import {ProfileCardPills} from '../com/profile/ProfileCard'
|
|
|
|
const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
|
|
LABELS,
|
|
) as (keyof typeof LABELS)[]
|
|
|
|
const MOCK_MOD_OPTS = {
|
|
userDid: 'at://did:web:alice',
|
|
adultContentEnabled: true,
|
|
labelGroups: {},
|
|
mods: [
|
|
{
|
|
did: 'did:plc:fake-labeler',
|
|
enabled: true,
|
|
},
|
|
],
|
|
}
|
|
|
|
export const DebugModScreen = ({}: NativeStackScreenProps<
|
|
CommonNavigatorParams,
|
|
'DebugMod'
|
|
>) => {
|
|
const t = useTheme()
|
|
const [label, setLabel] = React.useState<string[]>([LABEL_VALUES[0]])
|
|
const [target, setTarget] = React.useState<string[]>(['account'])
|
|
const labelStrings = useLabelStrings()
|
|
|
|
const profile = React.useMemo(() => {
|
|
const mockedProfile = mock.profileViewBasic({
|
|
handle: `bob.test`,
|
|
displayName: 'Bob Robertson',
|
|
labels:
|
|
target[0] === 'account'
|
|
? [
|
|
mock.label({
|
|
val: label[0],
|
|
uri: `at://did:web:bob/`,
|
|
}),
|
|
]
|
|
: target[0] === 'profile'
|
|
? [
|
|
mock.label({
|
|
val: label[0],
|
|
uri: `at://did:web:bob/app.bsky.actor.profile/self`,
|
|
}),
|
|
]
|
|
: undefined,
|
|
viewer: mock.actorViewerState({
|
|
muted: false,
|
|
mutedByList: undefined,
|
|
blockedBy: undefined,
|
|
blocking: undefined,
|
|
blockingByList: undefined,
|
|
}),
|
|
})
|
|
mockedProfile.avatar = 'https://bsky.social/about/images/favicon-32x32.png'
|
|
mockedProfile.banner =
|
|
'https://bsky.social/about/images/social-card-default-gradient.png'
|
|
return mockedProfile
|
|
}, [target, label])
|
|
|
|
const post = React.useMemo(() => {
|
|
return mock.postView({
|
|
record: mock.post({
|
|
text: "This is the body of the post. It's where the text goes. You get the idea.",
|
|
}),
|
|
author: profile,
|
|
labels:
|
|
target[0] === 'post'
|
|
? [
|
|
mock.label({
|
|
val: label[0],
|
|
uri: `at://bob.test/app.bsky.feed.post/fake`,
|
|
}),
|
|
]
|
|
: undefined,
|
|
embed: mock.embedRecordView({
|
|
record: mock.post({
|
|
text: 'Embed',
|
|
}),
|
|
labels:
|
|
target[0] === 'embed'
|
|
? [
|
|
mock.label({
|
|
val: label[0],
|
|
uri: `at://bob.test/app.bsky.feed.post/fake`,
|
|
}),
|
|
]
|
|
: undefined,
|
|
author: profile,
|
|
}),
|
|
})
|
|
}, [label, target, profile])
|
|
|
|
const profileModeration = React.useMemo(
|
|
() => moderateProfile(profile, MOCK_MOD_OPTS),
|
|
[profile],
|
|
)
|
|
const postModeration = React.useMemo(
|
|
() => moderatePost(post, MOCK_MOD_OPTS),
|
|
[post],
|
|
)
|
|
|
|
console.log(post, profile)
|
|
console.log(profileModeration, postModeration)
|
|
|
|
return (
|
|
<ScrollView>
|
|
<CenteredView style={[t.atoms.bg, a.px_lg, a.py_lg]}>
|
|
<H1 style={a.pb_lg}>Moderation states</H1>
|
|
|
|
<H3 style={a.pb_md}>Target</H3>
|
|
<View style={a.pb_2xl}>
|
|
<ToggleButton.Group
|
|
label="Preferences"
|
|
values={target}
|
|
onChange={setTarget}>
|
|
<ToggleButton.Button name="account" label="Account">
|
|
Account
|
|
</ToggleButton.Button>
|
|
<ToggleButton.Button name="profile" label="Profile">
|
|
Profile
|
|
</ToggleButton.Button>
|
|
<ToggleButton.Button name="post" label="Post">
|
|
Post
|
|
</ToggleButton.Button>
|
|
<ToggleButton.Button name="embed" label="Embed">
|
|
Embed
|
|
</ToggleButton.Button>
|
|
</ToggleButton.Group>
|
|
</View>
|
|
|
|
<H3 style={a.pb_md}>Label</H3>
|
|
<Toggle.Group
|
|
label="Toggle"
|
|
type="radio"
|
|
values={label}
|
|
onChange={setLabel}>
|
|
<View style={[a.flex_row, a.gap_md, a.flex_wrap, a.pb_2xl]}>
|
|
{LABEL_VALUES.map(labelValue => (
|
|
<Toggle.Item
|
|
key={labelValue}
|
|
name={labelValue}
|
|
label={labelStrings[labelValue].general.name}>
|
|
<Toggle.Radio />
|
|
<Toggle.Label>{labelValue}</Toggle.Label>
|
|
</Toggle.Item>
|
|
))}
|
|
</View>
|
|
</Toggle.Group>
|
|
|
|
<H3 style={a.pb_md}>Post</H3>
|
|
<MockPost label={label[0]} post={post} moderation={postModeration} />
|
|
<View
|
|
style={[
|
|
t.atoms.bg_contrast_50,
|
|
a.px_md,
|
|
a.py_sm,
|
|
a.rounded_sm,
|
|
a.flex_col,
|
|
a.gap_xs,
|
|
a.mb_2xl,
|
|
]}>
|
|
<ModerationUIView mod={postModeration.avatar} label="Avatar" />
|
|
<ModerationUIView mod={postModeration.content} label="Content" />
|
|
<ModerationUIView mod={postModeration.embed} label="Embed" />
|
|
</View>
|
|
|
|
<H3 style={a.pb_md}>
|
|
Account{' '}
|
|
<H3 style={[t.atoms.text_contrast_low, a.text_lg]}>in listing</H3>
|
|
</H3>
|
|
<MockAccountCard
|
|
label={label[0]}
|
|
profile={profile}
|
|
moderation={profileModeration}
|
|
/>
|
|
<View
|
|
style={[
|
|
t.atoms.bg_contrast_50,
|
|
a.px_md,
|
|
a.py_sm,
|
|
a.rounded_sm,
|
|
a.flex_col,
|
|
a.gap_xs,
|
|
a.mb_2xl,
|
|
]}>
|
|
<ModerationUIView mod={profileModeration.avatar} label="Avatar" />
|
|
<ModerationUIView mod={profileModeration.account} label="Account" />
|
|
<ModerationUIView mod={profileModeration.profile} label="Profile" />
|
|
</View>
|
|
|
|
<H3 style={a.pb_md}>
|
|
Account{' '}
|
|
<H3 style={[t.atoms.text_contrast_low, a.text_lg]}>
|
|
viewing directly
|
|
</H3>
|
|
</H3>
|
|
<MockAccountScreen label={label[0]} profile={profile} />
|
|
<View
|
|
style={[
|
|
t.atoms.bg_contrast_50,
|
|
a.px_md,
|
|
a.py_sm,
|
|
a.rounded_sm,
|
|
a.flex_col,
|
|
a.gap_xs,
|
|
a.mb_2xl,
|
|
]}>
|
|
<ModerationUIView mod={profileModeration.avatar} label="Avatar" />
|
|
<ModerationUIView mod={profileModeration.account} label="Account" />
|
|
<ModerationUIView mod={profileModeration.profile} label="Profile" />
|
|
</View>
|
|
|
|
<View style={{height: 400}} />
|
|
</CenteredView>
|
|
</ScrollView>
|
|
)
|
|
}
|
|
|
|
function MockPost({
|
|
label,
|
|
post,
|
|
moderation,
|
|
}: {
|
|
label: string
|
|
post: AppBskyFeedDefs.PostView
|
|
moderation: PostModeration
|
|
}) {
|
|
const t = useTheme()
|
|
return (
|
|
<View
|
|
style={[
|
|
t.atoms.border_contrast_medium,
|
|
a.border,
|
|
a.rounded_md,
|
|
a.mb_md,
|
|
a.p_2xs,
|
|
]}>
|
|
{' '}
|
|
<PostHider
|
|
key={label}
|
|
href="#"
|
|
moderation={moderation.content}
|
|
iconSize={38}
|
|
iconStyles={{marginLeft: 2, marginRight: 2}}
|
|
style={[a.px_lg, a.py_lg, a.rounded_md]}>
|
|
<View style={[a.flex_row, a.gap_md]}>
|
|
<UserAvatar
|
|
size={64}
|
|
avatar={post.author.avatar}
|
|
moderation={moderation.avatar}
|
|
/>
|
|
<View style={[a.flex_1]}>
|
|
<View style={[a.flex_row]}>
|
|
<P style={[a.font_bold]}>Bob Robertson </P>
|
|
<P style={t.atoms.text_contrast_low}>
|
|
@bob.bsky.social · 5m
|
|
</P>
|
|
</View>
|
|
|
|
<PostAlerts moderation={moderation.content} />
|
|
|
|
<P style={a.mb_lg}>
|
|
This is the body of the post. It's where the text goes. You get
|
|
the idea.
|
|
</P>
|
|
|
|
<ContentHider
|
|
moderation={moderation.embed}
|
|
moderationDecisions={moderation.decisions}>
|
|
<View
|
|
style={[
|
|
t.atoms.border_contrast_medium,
|
|
a.border,
|
|
a.rounded_md,
|
|
a.flex_col,
|
|
a.gap_xs,
|
|
a.px_xl,
|
|
a.py_xl,
|
|
]}>
|
|
<View style={[a.flex_row]}>
|
|
<P style={[a.font_bold]}>Bob Robertson </P>
|
|
<P style={t.atoms.text_contrast_low}>
|
|
@bob.bsky.social · 5m
|
|
</P>
|
|
</View>
|
|
<PostAlerts moderation={moderation.embed} />
|
|
<P>Embedded content</P>
|
|
</View>
|
|
</ContentHider>
|
|
</View>
|
|
</View>
|
|
</PostHider>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
function MockAccountCard({
|
|
label,
|
|
profile,
|
|
moderation,
|
|
}: {
|
|
label: string
|
|
profile: AppBskyActorDefs.ProfileViewBasic
|
|
moderation: ProfileModeration
|
|
}) {
|
|
const t = useTheme()
|
|
return (
|
|
<View
|
|
key={label}
|
|
style={[
|
|
t.atoms.border_contrast_medium,
|
|
a.border,
|
|
a.rounded_md,
|
|
a.flex_row,
|
|
a.gap_md,
|
|
a.px_lg,
|
|
a.py_md,
|
|
a.mb_md,
|
|
]}>
|
|
<UserAvatar
|
|
size={64}
|
|
avatar={profile.avatar}
|
|
moderation={moderation.avatar}
|
|
/>
|
|
<View style={[a.flex_1]}>
|
|
<P style={[a.font_bold]}>
|
|
{sanitizeDisplayName('Bob Robertson', moderation.profile)}{' '}
|
|
</P>
|
|
<P style={t.atoms.text_contrast_low}>@bob.bsky.social</P>
|
|
<P>Thought leader or something.</P>
|
|
<ProfileCardPills followedBy={false} moderation={moderation} />
|
|
</View>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
function MockAccountScreen({
|
|
label,
|
|
profile,
|
|
}: {
|
|
label: string
|
|
profile: AppBskyActorDefs.ProfileViewBasic
|
|
}) {
|
|
const t = useTheme()
|
|
return (
|
|
<View
|
|
key={label}
|
|
style={[t.atoms.border_contrast_medium, a.border, a.mb_md]}>
|
|
<ProfileHeader
|
|
// @ts-ignore ProfileViewBasic is close enough -prf
|
|
profile={profile}
|
|
moderationOpts={MOCK_MOD_OPTS}
|
|
descriptionRT={null /*TODO*/}
|
|
/>
|
|
|
|
{/*
|
|
<UserBanner />
|
|
<View
|
|
style={[
|
|
a.absolute,
|
|
t.atoms.bg,
|
|
a.rounded_full,
|
|
{top: 100, left: 10, width: 100, height: 100, padding: 2},
|
|
]}>
|
|
<UserAvatar size={96} moderation={moderation.avatar} />
|
|
</View>
|
|
<View
|
|
style={[
|
|
a.pb_2xl,
|
|
a.px_xl,
|
|
t.atoms.border_contrast_medium,
|
|
a.border_b,
|
|
{paddingTop: 60},
|
|
]}>
|
|
<H3>Bob Robertson</H3>
|
|
<P style={t.atoms.text_contrast_low}>@bob.bsky.social</P>
|
|
<P>Thought leader or something.</P>
|
|
</View>
|
|
*/}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
function ModerationUIView({mod, label}: {mod: ModerationUI; label: string}) {
|
|
return (
|
|
<View style={[a.flex_row, a.gap_md]}>
|
|
<P style={[a.font_bold, a.text_xs, {width: 60}]}>{label}:</P>
|
|
<Flag v={mod.filter} label="Filter" />
|
|
<Flag v={mod.blur} label="Blur" />
|
|
<Flag v={mod.alert} label="Alert" />
|
|
<Flag v={mod.noOverride} label="No-override" />
|
|
</View>
|
|
)
|
|
}
|
|
|
|
function Flag({v, label}: {v: boolean | undefined; label: string}) {
|
|
const t = useTheme()
|
|
return (
|
|
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
|
<View
|
|
style={[
|
|
a.justify_center,
|
|
a.align_center,
|
|
a.rounded_xs,
|
|
a.border,
|
|
t.atoms.border_contrast_medium,
|
|
{
|
|
backgroundColor: v ? t.palette.black : t.palette.white,
|
|
width: 14,
|
|
height: 14,
|
|
},
|
|
]}>
|
|
{v && <Check size="xs" fill={t.palette.white} />}
|
|
</View>
|
|
<P style={a.text_xs}>{label}</P>
|
|
</View>
|
|
)
|
|
}
|