Refactor/rewrite the entire moderation-application system

This commit is contained in:
Paul Frazee
2024-02-14 21:48:27 -08:00
parent cc635ae949
commit b009a34a7a
43 changed files with 1094 additions and 1062 deletions
+366 -338
View File
@@ -7,14 +7,16 @@ import {
moderatePost,
moderateProfile,
ModerationOpts,
PostModeration,
ProfileModeration,
ModerationUI,
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyFeedPost,
LabelTarget,
LabelPreference,
ModerationDecision,
ModerationBehavior,
RichText,
} from '@atproto/api'
import {moderationOptsOverrideContext} from '#/state/queries/preferences'
import {atoms as a, useTheme} from '#/alf'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
@@ -22,23 +24,24 @@ import {H1, H3, P, Text} 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 {Button, ButtonIcon, ButtonText} from '#/components/Button'
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 {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom,
ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
} from '#/components/icons/Chevron'
import {ScreenHider} from '../com/util/moderation/ScreenHider'
import {ProfileHeader} from '../com/profile/ProfileHeader'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {ProfileCardPills} from '../com/profile/ProfileCard'
import {ProfileCard} from '../com/profile/ProfileCard'
import {FeedItem} from '../com/posts/FeedItem'
import {PostThreadItem} from '../com/post-thread/PostThreadItem'
const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
LABELS,
) as (keyof typeof LABELS)[]
const MOCK_MOD_OPTS = {
userDid: 'at://did:web:alice',
userDid: 'did:web:alice.test',
adultContentEnabled: true,
labelGroups: {},
mods: [
@@ -54,36 +57,53 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
'DebugMod'
>) => {
const t = useTheme()
const [scenario, setScenario] = React.useState<string[]>(['label'])
const [scenarioSwitches, setScenarioSwitches] = React.useState<string[]>([])
const [label, setLabel] = React.useState<string[]>([LABEL_VALUES[0]])
const [target, setTarget] = React.useState<string[]>(['account'])
const [visibility, setVisiblity] = React.useState<string[]>(['hide'])
const labelStrings = useLabelStrings()
const isTargetMe =
scenario[0] === 'label' && scenarioSwitches.includes('targetMe')
const isSelfLabel =
scenario[0] === 'label' && scenarioSwitches.includes('selfLabel')
const noAdult =
scenario[0] === 'label' && scenarioSwitches.includes('noAdult')
const isLoggedOut =
scenario[0] === 'label' && scenarioSwitches.includes('loggedOut')
const profile = React.useMemo(() => {
const mockedProfile = mock.profileViewBasic({
handle: `bob.test`,
displayName: 'Bob Robertson',
description: 'User with this as their bio',
labels:
target[0] === 'account'
scenario[0] === 'label' && target[0] === 'account'
? [
mock.label({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
val: label[0],
uri: `at://did:web:bob/`,
uri: `at://did:web:bob.test/`,
}),
]
: target[0] === 'profile'
: scenario[0] === 'label' && target[0] === 'profile'
? [
mock.label({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
val: label[0],
uri: `at://did:web:bob/app.bsky.actor.profile/self`,
uri: `at://did:web:bob.test/app.bsky.actor.profile/self`,
}),
]
: undefined,
viewer: mock.actorViewerState({
muted: false,
muted: scenario[0] === 'mute',
mutedByList: undefined,
blockedBy: undefined,
blocking: undefined,
blocking:
scenario[0] === 'block'
? 'did://did:web:alice/app.bsky.actor.block/fake'
: undefined,
blockingByList: undefined,
}),
})
@@ -91,7 +111,7 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
mockedProfile.banner =
'https://bsky.social/about/images/social-card-default-gradient.png'
return mockedProfile
}, [target, label])
}, [scenario, target, label, isSelfLabel])
const post = React.useMemo(() => {
return mock.postView({
@@ -100,41 +120,63 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
}),
author: profile,
labels:
target[0] === 'post'
scenario[0] === 'label' && target[0] === 'post'
? [
mock.label({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
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,
}),
embed:
target[0] === 'embed'
? mock.embedRecordView({
record: mock.post({
text: 'Embed',
}),
labels:
scenario[0] === 'label' && target[0] === 'embed'
? [
mock.label({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
val: label[0],
uri: `at://bob.test/app.bsky.feed.post/fake`,
}),
]
: undefined,
author: profile,
})
: {
$type: 'app.bsky.embed.images#view',
images: [
{
thumb:
'https://bsky.social/about/images/social-card-default-gradient.png',
fullsize:
'https://bsky.social/about/images/social-card-default-gradient.png',
alt: '',
},
],
},
})
}, [label, target, profile])
}, [scenario, label, target, profile, isSelfLabel])
const modOpts = React.useMemo(() => {
return {
...MOCK_MOD_OPTS,
userDid: isLoggedOut
? ''
: isTargetMe
? 'did:web:bob.test'
: 'did:web:alice.test',
adultContentEnabled: !noAdult,
labelGroups: {
[LABELS[label[0] as keyof typeof LABELS].groupId]:
visibility[0] as LabelPreference,
},
}
}, [label, visibility])
}, [label, visibility, noAdult, isLoggedOut, isTargetMe])
const profileModeration = React.useMemo(() => {
return moderateProfile(profile, modOpts)
@@ -143,188 +185,201 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
return moderatePost(post, modOpts)
}, [post, modOpts])
console.log(post, profile)
console.log(profileModeration, postModeration)
console.log(post)
return (
<ScrollView>
<CenteredView style={[t.atoms.bg, a.px_lg, a.py_lg]}>
<H1 style={[a.text_5xl, a.font_bold, a.pb_lg]}>Moderation states</H1>
<moderationOptsOverrideContext.Provider value={modOpts}>
<ScrollView>
<CenteredView style={[t.atoms.bg, a.px_lg, a.py_lg]}>
<H1 style={[a.text_5xl, a.font_bold, a.pb_lg]}>Moderation states</H1>
<Heading title="Config" />
<Heading title="" subtitle="Target" />
<ToggleButton.Group label="Target" 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>
<Heading title="Config" />
<View style={{height: 10}} />
<Heading title="" subtitle="Scenario" />
<ToggleButton.Group
label="Scenario"
values={scenario}
onChange={setScenario}>
<ToggleButton.Button name="label" label="Label">
Label
</ToggleButton.Button>
<ToggleButton.Button name="block" label="Block">
Block
</ToggleButton.Button>
<ToggleButton.Button name="mute" label="Mute">
Mute
</ToggleButton.Button>
</ToggleButton.Group>
<Heading title="" subtitle="Preference" />
<ToggleButton.Group
label="Visiblity"
values={visibility}
onChange={setVisiblity}>
<ToggleButton.Button name="hide" label="Hide">
Hide
</ToggleButton.Button>
<ToggleButton.Button name="warn" label="Warn">
Warn
</ToggleButton.Button>
<ToggleButton.Button name="ignore" label="Ignore">
Ignore
</ToggleButton.Button>
</ToggleButton.Group>
{scenario[0] === 'label' && (
<>
<Toggle.Group
label="Toggle"
type="checkbox"
values={scenarioSwitches}
onChange={setScenarioSwitches}>
<View style={[a.gap_md, a.flex_row, a.pt_md]}>
<Toggle.Item name="targetMe" label="Target is me">
<Toggle.Checkbox />
<Toggle.Label>Target is me</Toggle.Label>
</Toggle.Item>
<Toggle.Item name="selfLabel" label="Self label">
<Toggle.Checkbox />
<Toggle.Label>Self label</Toggle.Label>
</Toggle.Item>
<Toggle.Item name="noAdult" label="Adult disabled">
<Toggle.Checkbox />
<Toggle.Label>Adult disabled</Toggle.Label>
</Toggle.Item>
<Toggle.Item name="loggedOut" label="Logged out">
<Toggle.Checkbox />
<Toggle.Label>Logged out</Toggle.Label>
</Toggle.Item>
</View>
</Toggle.Group>
<View style={{height: 10}} />
<View style={{height: 10}} />
<Heading title="" subtitle="Target" />
<ToggleButton.Group
label="Target"
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>
<Heading title="" subtitle="Label" />
<Toggle.Group
label="Toggle"
type="radio"
values={label}
onChange={setLabel}>
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
{LABEL_VALUES.map(labelValue => {
let targetFixed = target[0]
if (targetFixed !== 'account' && targetFixed !== 'profile') {
targetFixed = 'content'
}
const disabled = !LABELS[labelValue].targets.includes(
targetFixed as LabelTarget,
)
return (
<Toggle.Item
key={labelValue}
name={labelValue}
label={labelStrings[labelValue].general.name}
disabled={disabled}
style={disabled ? {opacity: 0.5} : undefined}>
<Toggle.Radio />
<Toggle.Label>{labelValue}</Toggle.Label>
</Toggle.Item>
)
})}
</View>
</Toggle.Group>
<View style={{height: 10}} />
<Spacer />
<Heading title="" subtitle="Preference" />
<ToggleButton.Group
label="Visiblity"
values={visibility}
onChange={setVisiblity}>
<ToggleButton.Button name="hide" label="Hide">
Hide
</ToggleButton.Button>
<ToggleButton.Button name="warn" label="Warn">
Warn
</ToggleButton.Button>
<ToggleButton.Button name="ignore" label="Ignore">
Ignore
</ToggleButton.Button>
</ToggleButton.Group>
<Heading title={label[0]} />
<Text style={{fontFamily: 'monospace'}}>
{JSON.stringify(LABELS[label[0]], null, 2)}
</Text>
<View style={{height: 10}} />
<Spacer />
<Heading title="" subtitle="Label" />
<Toggle.Group
label="Toggle"
type="radio"
values={label}
onChange={setLabel}>
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
{LABEL_VALUES.map(labelValue => {
let targetFixed = target[0]
if (
targetFixed !== 'account' &&
targetFixed !== 'profile'
) {
targetFixed = 'content'
}
const disabled =
!LABELS[labelValue].targets.includes(
targetFixed as LabelTarget,
) ||
(isSelfLabel &&
LABELS[labelValue].flags.includes('no-self'))
return (
<Toggle.Item
key={labelValue}
name={labelValue}
label={labelStrings[labelValue].general.name}
disabled={disabled}
style={disabled ? {opacity: 0.5} : undefined}>
<Toggle.Radio />
<Toggle.Label>{labelValue}</Toggle.Label>
</Toggle.Item>
)
})}
</View>
</Toggle.Group>
</>
)}
<Heading title="Output" />
<P style={[a.font_bold]}>Post moderation</P>
<View
style={[
t.atoms.border_contrast_low,
a.border,
a.px_md,
a.py_sm,
a.rounded_sm,
a.flex_col,
a.gap_xs,
]}>
<ModerationUIView mod={postModeration.avatar} label="Avatar" />
<ModerationUIView mod={postModeration.content} label="Content" />
<ModerationUIView mod={postModeration.embed} label="Embed" />
</View>
<P style={[a.font_bold, a.mt_md]}>Profile Moderation</P>
<View
style={[
t.atoms.border_contrast_low,
a.border,
a.px_md,
a.py_sm,
a.rounded_sm,
a.flex_col,
a.gap_xs,
]}>
<ModerationUIView mod={profileModeration.avatar} label="Avatar" />
<ModerationUIView mod={profileModeration.account} label="Account" />
<ModerationUIView mod={profileModeration.profile} label="Profile" />
</View>
<Spacer />
<Spacer />
<ModerationUIView
label="Profile Moderation UI"
mod={profileModeration}
/>
<ModerationUIView label="Post Moderation UI" mod={postModeration} />
<DataView
label={label[0]}
data={LABELS[label[0] as keyof typeof LABELS]}
/>
<DataView label="Profile Moderation Data" data={profileModeration} />
<DataView label="Post Data" data={postModeration} />
<Heading title="Post" subtitle="in feed" />
<MockPost
label={label[0]}
context="feed"
post={post}
moderation={postModeration}
/>
<Spacer />
<Spacer />
<Heading title="Post" subtitle="in feed" />
<MockPostFeedItem post={post} moderation={postModeration} />
<Heading title="Post" subtitle="viewed directly" />
<MockPost
label={label[0]}
context="view"
post={post}
moderation={postModeration}
/>
<Spacer />
<Spacer />
<Heading title="Post" subtitle="viewed directly" />
<MockPostThreadItem post={post} moderation={postModeration} />
<Heading title="Post" subtitle="reply in thread" />
<MockPost
label={label[0]}
context="reply"
post={post}
moderation={postModeration}
/>
<Spacer />
<Spacer />
<Heading title="Post" subtitle="reply in thread" />
<MockPostThreadItem post={post} moderation={postModeration} reply />
<Heading title="Notification" subtitle="quote or reply" />
<P>TODO</P>
<Spacer />
<Spacer />
<Heading title="Notification" subtitle="quote or reply" />
<P>TODO</P>
{(target[0] === 'account' || target[0] === 'profile') && (
<>
<Heading title="Notification" subtitle="follow or like" />
<P>TODO</P>
<Spacer />
<Spacer />
{(target[0] === 'account' || target[0] === 'profile') && (
<>
<Heading title="Notification" subtitle="follow or like" />
<P>TODO</P>
<Heading title="Account" subtitle="in listing" />
<MockAccountCard
label={label[0]}
profile={profile}
moderation={profileModeration}
/>
<Spacer />
<Spacer />
<Heading title="Account" subtitle="in listing" />
<MockAccountCard
profile={profile}
moderation={profileModeration}
/>
<Heading title="Account" subtitle="viewing directly" />
<MockAccountScreen
label={label[0]}
profile={profile}
moderation={profileModeration}
moderationOpts={modOpts}
/>
</>
)}
<Spacer />
<View style={{height: 400}} />
</CenteredView>
</ScrollView>
<Heading title="Account" subtitle="viewing directly" />
<MockAccountScreen
profile={profile}
moderation={profileModeration}
moderationOpts={modOpts}
/>
</>
)}
<View style={{height: 400}} />
</CenteredView>
</ScrollView>
</moderationOptsOverrideContext.Provider>
)
}
@@ -340,105 +395,146 @@ function Heading({title, subtitle}: {title: string; subtitle?: string}) {
)
}
function Spacer() {
return <View style={{height: 40}} />
function Toggler({label, children}: React.PropsWithChildren<{label: string}>) {
const t = useTheme()
const [show, setShow] = React.useState(false)
return (
<View style={a.mb_md}>
<View
style={[
t.atoms.border_contrast_medium,
a.border,
a.rounded_sm,
a.p_xs,
]}>
<Button
variant="solid"
color="secondary"
label="Toggle visibility"
size="small"
onPress={() => setShow(!show)}>
<ButtonText>{label}</ButtonText>
<ButtonIcon
icon={show ? ChevronTop : ChevronBottom}
position="right"
/>
</Button>
{show && children}
</View>
</View>
)
}
function MockPost({
function DataView({label, data}: {label: string; data: any}) {
return (
<Toggler label={label}>
<Text style={[{fontFamily: 'monospace'}, a.p_md]}>
{JSON.stringify(data, null, 2)}
</Text>
</Toggler>
)
}
function ModerationUIView({
mod,
label,
context,
}: {
mod: ModerationDecision
label: string
}) {
return (
<Toggler label={label}>
<View style={a.p_lg}>
{[
'profileList',
'profileView',
'avatar',
'banner',
'displayName',
'contentList',
'contentView',
'contentMedia',
].map(key => {
const ui = mod.ui(key as keyof ModerationBehavior)
return (
<View key={key} style={[a.flex_row, a.gap_md]}>
<Text style={[a.font_bold, {width: 100}]}>{key}</Text>
<Flag v={ui.filter} label="Filter" />
<Flag v={ui.blur} label="Blur" />
<Flag v={ui.alert} label="Alert" />
<Flag v={ui.inform} label="Inform" />
<Flag v={ui.noOverride} label="No-override" />
</View>
)
})}
</View>
</Toggler>
)
}
function Spacer() {
return <View style={{height: 30}} />
}
function MockPostFeedItem({
post,
moderation,
}: {
label: string
context: 'feed' | 'view' | 'reply'
post: AppBskyFeedDefs.PostView
moderation: PostModeration
moderation: ModerationDecision
}) {
const t = useTheme()
if (context === 'feed' && moderation.content.filter) {
if (moderation.ui('contentList').filter) {
return (
<P style={[t.atoms.bg_contrast_50, a.px_lg, a.py_md]}>
Filtered from the feed
</P>
)
}
return (
<View
style={[t.atoms.border_contrast_medium, a.border, a.rounded_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_medium}>
@bob.bsky.social &middot; 5m
</P>
</View>
<FeedItem
post={post}
record={post.record as AppBskyFeedPost.Record}
moderation={moderation}
reason={undefined}
/>
)
}
<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_medium}>
@bob.bsky.social &middot; 5m
</P>
</View>
<PostAlerts moderation={moderation.embed} />
<P>Embedded content</P>
</View>
</ContentHider>
</View>
</View>
</PostHider>
</View>
function MockPostThreadItem({
post,
reply,
}: {
post: AppBskyFeedDefs.PostView
moderation: ModerationDecision
reply?: boolean
}) {
return (
<PostThreadItem
// @ts-ignore
post={post}
record={post.record as AppBskyFeedPost.Record}
depth={reply ? 1 : 0}
isHighlightedPost={!reply}
treeView={false}
prevPost={undefined}
nextPost={undefined}
hasPrecedingItem={false}
onPostReply={() => {}}
/>
)
}
function MockAccountCard({
label,
profile,
moderation,
}: {
label: string
profile: AppBskyActorDefs.ProfileViewBasic
moderation: ProfileModeration
moderation: ModerationDecision
}) {
const t = useTheme()
if (moderation.account.filter || moderation.profile.filter) {
if (moderation.ui('profileList').filter) {
return (
<P style={[t.atoms.bg_contrast_50, a.px_lg, a.py_md]}>
Filtered from the listing
@@ -446,104 +542,36 @@ function MockAccountCard({
)
}
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_medium}>@bob.bsky.social</P>
<P>Thought leader or something.</P>
<ProfileCardPills followedBy={false} moderation={moderation} />
</View>
</View>
)
return <ProfileCard profile={profile} />
}
function MockAccountScreen({
label,
profile,
moderation,
moderationOpts,
}: {
label: string
profile: AppBskyActorDefs.ProfileViewBasic
moderation: ProfileModeration
moderation: ModerationDecision
moderationOpts: ModerationOpts
}) {
const t = useTheme()
return (
<View
key={label}
style={[t.atoms.border_contrast_medium, a.border, a.mb_md]}>
<View style={[t.atoms.border_contrast_medium, a.border, a.mb_md]}>
<ScreenHider
style={{}}
screenDescription="profile"
moderation={moderation.account}>
modui={moderation.ui('profileView')}>
<ProfileHeader
// @ts-ignore ProfileViewBasic is close enough -prf
profile={profile}
moderationOpts={moderationOpts}
descriptionRT={null /*TODO*/}
descriptionRT={new RichText({text: profile.description as string})}
/>
{/*
<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_medium}>@bob.bsky.social</P>
<P>Thought leader or something.</P>
</View>
*/}
</ScreenHider>
</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 (
@@ -556,12 +584,12 @@ function Flag({v, label}: {v: boolean | undefined; label: string}) {
a.border,
t.atoms.border_contrast_medium,
{
backgroundColor: v ? t.palette.black : t.palette.white,
backgroundColor: t.palette.contrast_25,
width: 14,
height: 14,
},
]}>
{v && <Check size="xs" fill={t.palette.white} />}
{v && <Check size="xs" fill={t.palette.contrast_900} />}
</View>
<P style={a.text_xs}>{label}</P>
</View>
+1 -1
View File
@@ -298,7 +298,7 @@ function ProfileScreenLoaded({
testID="profileView"
style={styles.container}
screenDescription="profile"
moderation={moderation.account}>
modui={moderation.ui('profileView')}>
<PagerWithHeader
testID="profilePager"
isHeaderReady={!showPlaceholder}
+41
View File
@@ -129,6 +129,15 @@ export function Buttons() {
<ButtonIcon icon={Globe} position="left" />
<ButtonText>Link out</ButtonText>
</Button>
<Button
variant="gradient"
color="gradient_sky"
size="tiny"
label="Link out">
<ButtonIcon icon={Globe} position="left" />
<ButtonText>Link out</ButtonText>
</Button>
</View>
<View style={[a.flex_row, a.gap_md, a.align_start]}>
@@ -148,6 +157,14 @@ export function Buttons() {
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="gradient"
color="gradient_sunset"
size="tiny"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="outline"
color="primary"
@@ -164,6 +181,14 @@ export function Buttons() {
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="ghost"
color="primary"
size="tiny"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
</View>
<View style={[a.flex_row, a.gap_md, a.align_start]}>
@@ -183,6 +208,14 @@ export function Buttons() {
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="gradient"
color="gradient_sunset"
size="tiny"
shape="square"
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="outline"
color="primary"
@@ -199,6 +232,14 @@ export function Buttons() {
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="ghost"
color="primary"
size="tiny"
shape="square"
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
</View>
</View>
)