bsky-app/src/view/screens/DebugMod.tsx
dan 49266c355e
Remove special cases for some buttons with text (#3412)
* Toggle.Button -> Toggle.ButtonWithText

* Simplify Prompt.Cancel/Action

* Move lines down for better diff

* Remove ButtonWithText

* Simplify types
2024-04-05 14:57:53 +01:00

923 lines
29 KiB
TypeScript

import React from 'react'
import {View} from 'react-native'
import {
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyFeedPost,
ComAtprotoLabelDefs,
interpretLabelValueDefinition,
LabelPreference,
LABELS,
mock,
moderatePost,
moderateProfile,
ModerationBehavior,
ModerationDecision,
ModerationOpts,
RichText,
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import {FeedNotification} from '#/state/queries/notifications/types'
import {
groupNotifications,
shouldFilterNotif,
} from '#/state/queries/notifications/util'
import {moderationOptsOverrideContext} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {ProfileHeaderStandard} from '#/screens/Profile/Header/ProfileHeaderStandard'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom,
ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
} from '#/components/icons/Chevron'
import {H1, H3, P, Text} from '#/components/Typography'
import {ScreenHider} from '../../components/moderation/ScreenHider'
import {FeedItem as NotifFeedItem} from '../com/notifications/FeedItem'
import {PostThreadItem} from '../com/post-thread/PostThreadItem'
import {FeedItem} from '../com/posts/FeedItem'
import {ProfileCard} from '../com/profile/ProfileCard'
const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
LABELS,
) as (keyof typeof LABELS)[]
export const DebugModScreen = ({}: NativeStackScreenProps<
CommonNavigatorParams,
'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[]>(['warn'])
const [customLabelDef, setCustomLabelDef] =
React.useState<ComAtprotoLabelDefs.LabelValueDefinition>({
identifier: 'custom',
blurs: 'content',
severity: 'alert',
defaultSetting: 'warn',
locales: [
{
lang: 'en',
name: 'Custom label',
description: 'A custom label created in this test environment',
},
],
})
const [view, setView] = React.useState<string[]>(['post'])
const labelStrings = useGlobalLabelStrings()
const {currentAccount} = useSession()
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 isFollowing = scenarioSwitches.includes('following')
const did =
isTargetMe && currentAccount ? currentAccount.did : 'did:web:bob.test'
const profile = React.useMemo(() => {
const mockedProfile = mock.profileViewBasic({
handle: `bob.test`,
displayName: 'Bob Robertson',
description: 'User with this as their bio',
labels:
scenario[0] === 'label' && target[0] === 'account'
? [
mock.label({
src: isSelfLabel ? did : undefined,
val: label[0],
uri: `at://${did}/`,
}),
]
: scenario[0] === 'label' && target[0] === 'profile'
? [
mock.label({
src: isSelfLabel ? did : undefined,
val: label[0],
uri: `at://${did}/app.bsky.actor.profile/self`,
}),
]
: undefined,
viewer: mock.actorViewerState({
following: isFollowing
? `at://${currentAccount?.did || ''}/app.bsky.graph.follow/1234`
: undefined,
muted: scenario[0] === 'mute',
mutedByList: undefined,
blockedBy: undefined,
blocking:
scenario[0] === 'block'
? `at://did:web:alice.test/app.bsky.actor.block/fake`
: undefined,
blockingByList: undefined,
}),
})
mockedProfile.did = did
mockedProfile.avatar = 'https://bsky.social/about/images/favicon-32x32.png'
mockedProfile.banner =
'https://bsky.social/about/images/social-card-default-gradient.png'
return mockedProfile
}, [scenario, target, label, isSelfLabel, did, isFollowing, currentAccount])
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:
scenario[0] === 'label' && target[0] === 'post'
? [
mock.label({
src: isSelfLabel ? did : undefined,
val: label[0],
uri: `at://${did}/app.bsky.feed.post/fake`,
}),
]
: undefined,
embed:
target[0] === 'embed'
? mock.embedRecordView({
record: mock.post({
text: 'Embed',
}),
labels:
scenario[0] === 'label' && target[0] === 'embed'
? [
mock.label({
src: isSelfLabel ? did : undefined,
val: label[0],
uri: `at://${did}/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: '',
},
],
},
})
}, [scenario, label, target, profile, isSelfLabel, did])
const replyNotif = React.useMemo(() => {
const notif = mock.replyNotification({
record: mock.post({
text: "This is the body of the post. It's where the text goes. You get the idea.",
reply: {
parent: {
uri: `at://${did}/app.bsky.feed.post/fake-parent`,
cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
},
root: {
uri: `at://${did}/app.bsky.feed.post/fake-parent`,
cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
},
},
}),
author: profile,
labels:
scenario[0] === 'label' && target[0] === 'post'
? [
mock.label({
src: isSelfLabel ? did : undefined,
val: label[0],
uri: `at://${did}/app.bsky.feed.post/fake`,
}),
]
: undefined,
})
const [item] = groupNotifications([notif])
item.subject = mock.postView({
record: notif.record as AppBskyFeedPost.Record,
author: profile,
labels: notif.labels,
})
return item
}, [scenario, label, target, profile, isSelfLabel, did])
const followNotif = React.useMemo(() => {
const notif = mock.followNotification({
author: profile,
subjectDid: currentAccount?.did || '',
})
const [item] = groupNotifications([notif])
return item
}, [profile, currentAccount])
const modOpts = React.useMemo(() => {
return {
userDid: isLoggedOut ? '' : isTargetMe ? did : 'did:web:alice.test',
prefs: {
adultContentEnabled: !noAdult,
labels: {
[label[0]]: visibility[0] as LabelPreference,
},
labelers: [
{
did: 'did:plc:fake-labeler',
labels: {[label[0]]: visibility[0] as LabelPreference},
},
],
mutedWords: [],
hiddenPosts: [],
},
labelDefs: {
'did:plc:fake-labeler': [
interpretLabelValueDefinition(customLabelDef, 'did:plc:fake-labeler'),
],
},
}
}, [label, visibility, noAdult, isLoggedOut, isTargetMe, did, customLabelDef])
const profileModeration = React.useMemo(() => {
return moderateProfile(profile, modOpts)
}, [profile, modOpts])
const postModeration = React.useMemo(() => {
return moderatePost(post, modOpts)
}, [post, modOpts])
return (
<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="" subtitle="Scenario" />
<ToggleButton.Group
label="Scenario"
values={scenario}
onChange={setScenario}>
<ToggleButton.Button name="label" label="Label">
<ToggleButton.ButtonText>Label</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button name="block" label="Block">
<ToggleButton.ButtonText>Block</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button name="mute" label="Mute">
<ToggleButton.ButtonText>Mute</ToggleButton.ButtonText>
</ToggleButton.Button>
</ToggleButton.Group>
{scenario[0] === 'label' && (
<>
<View
style={[
a.border,
a.rounded_sm,
a.mt_lg,
a.mb_lg,
a.p_lg,
t.atoms.border_contrast_medium,
]}>
<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 =
isSelfLabel &&
LABELS[labelValue].flags.includes('no-self')
return (
<Toggle.Item
key={labelValue}
name={labelValue}
label={labelStrings[labelValue].name}
disabled={disabled}
style={disabled ? {opacity: 0.5} : undefined}>
<Toggle.Radio />
<Toggle.LabelText>{labelValue}</Toggle.LabelText>
</Toggle.Item>
)
})}
<Toggle.Item
name="custom"
label="Custom label"
disabled={isSelfLabel}
style={isSelfLabel ? {opacity: 0.5} : undefined}>
<Toggle.Radio />
<Toggle.LabelText>Custom label</Toggle.LabelText>
</Toggle.Item>
</View>
</Toggle.Group>
{label[0] === 'custom' ? (
<CustomLabelForm
def={customLabelDef}
setDef={setCustomLabelDef}
/>
) : (
<>
<View style={{height: 10}} />
<Divider />
</>
)}
<View style={{height: 10}} />
<SmallToggler label="Advanced">
<Toggle.Group
label="Toggle"
type="checkbox"
values={scenarioSwitches}
onChange={setScenarioSwitches}>
<View style={[a.gap_md, a.flex_row, a.flex_wrap, a.pt_md]}>
<Toggle.Item name="targetMe" label="Target is me">
<Toggle.Checkbox />
<Toggle.LabelText>Target is me</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="following" label="Following target">
<Toggle.Checkbox />
<Toggle.LabelText>Following target</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="selfLabel" label="Self label">
<Toggle.Checkbox />
<Toggle.LabelText>Self label</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="noAdult" label="Adult disabled">
<Toggle.Checkbox />
<Toggle.LabelText>Adult disabled</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="loggedOut" label="Logged out">
<Toggle.Checkbox />
<Toggle.LabelText>Logged out</Toggle.LabelText>
</Toggle.Item>
</View>
</Toggle.Group>
{LABELS[label[0] as keyof typeof LABELS]?.configurable !==
false && (
<View style={[a.mt_md]}>
<Text
style={[a.font_bold, a.text_xs, t.atoms.text, a.pb_sm]}>
Preference
</Text>
<Toggle.Group
label="Preference"
type="radio"
values={visibility}
onChange={setVisiblity}>
<View
style={[
a.flex_row,
a.gap_md,
a.flex_wrap,
a.align_center,
]}>
<Toggle.Item name="hide" label="Hide">
<Toggle.Radio />
<Toggle.LabelText>Hide</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="warn" label="Warn">
<Toggle.Radio />
<Toggle.LabelText>Warn</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="ignore" label="Ignore">
<Toggle.Radio />
<Toggle.LabelText>Ignore</Toggle.LabelText>
</Toggle.Item>
</View>
</Toggle.Group>
</View>
)}
</SmallToggler>
</View>
<View style={[a.flex_row, a.flex_wrap, a.gap_md]}>
<View>
<Text
style={[
a.font_bold,
a.text_xs,
t.atoms.text,
a.pl_md,
a.pb_xs,
]}>
Target
</Text>
<View
style={[
a.border,
a.rounded_full,
a.px_md,
a.py_sm,
t.atoms.border_contrast_medium,
t.atoms.bg,
]}>
<Toggle.Group
label="Target"
type="radio"
values={target}
onChange={setTarget}>
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
<Toggle.Item name="account" label="Account">
<Toggle.Radio />
<Toggle.LabelText>Account</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="profile" label="Profile">
<Toggle.Radio />
<Toggle.LabelText>Profile</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="post" label="Post">
<Toggle.Radio />
<Toggle.LabelText>Post</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="embed" label="Embed">
<Toggle.Radio />
<Toggle.LabelText>Embed</Toggle.LabelText>
</Toggle.Item>
</View>
</Toggle.Group>
</View>
</View>
</View>
</>
)}
<Spacer />
<Heading title="" subtitle="Results" />
<ToggleButton.Group label="Results" values={view} onChange={setView}>
<ToggleButton.Button name="post" label="Post">
<ToggleButton.ButtonText>Post</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button name="notifications" label="Notifications">
<ToggleButton.ButtonText>Notifications</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button name="account" label="Account">
<ToggleButton.ButtonText>Account</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button name="data" label="Data">
<ToggleButton.ButtonText>Data</ToggleButton.ButtonText>
</ToggleButton.Button>
</ToggleButton.Group>
<View
style={[
a.border,
a.rounded_sm,
a.mt_lg,
a.p_md,
t.atoms.border_contrast_medium,
]}>
{view[0] === 'post' && (
<>
<Heading title="Post" subtitle="in feed" />
<MockPostFeedItem post={post} moderation={postModeration} />
<Heading title="Post" subtitle="viewed directly" />
<MockPostThreadItem post={post} moderation={postModeration} />
<Heading title="Post" subtitle="reply in thread" />
<MockPostThreadItem
post={post}
moderation={postModeration}
reply
/>
</>
)}
{view[0] === 'notifications' && (
<>
<Heading title="Notification" subtitle="quote or reply" />
<MockNotifItem notif={replyNotif} moderationOpts={modOpts} />
<View style={{height: 20}} />
<Heading title="Notification" subtitle="follow or like" />
<MockNotifItem notif={followNotif} moderationOpts={modOpts} />
</>
)}
{view[0] === 'account' && (
<>
<Heading title="Account" subtitle="in listing" />
<MockAccountCard
profile={profile}
moderation={profileModeration}
/>
<Heading title="Account" subtitle="viewing directly" />
<MockAccountScreen
profile={profile}
moderation={profileModeration}
moderationOpts={modOpts}
/>
</>
)}
{view[0] === 'data' && (
<>
<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 Moderation Data" data={postModeration} />
</>
)}
</View>
<View style={{height: 400}} />
</CenteredView>
</ScrollView>
</moderationOptsOverrideContext.Provider>
)
}
function Heading({title, subtitle}: {title: string; subtitle?: string}) {
const t = useTheme()
return (
<H3 style={[a.text_3xl, a.font_bold, a.pb_md]}>
{title}{' '}
{!!subtitle && (
<H3 style={[t.atoms.text_contrast_medium, a.text_lg]}>{subtitle}</H3>
)}
</H3>
)
}
function CustomLabelForm({
def,
setDef,
}: {
def: ComAtprotoLabelDefs.LabelValueDefinition
setDef: React.Dispatch<
React.SetStateAction<ComAtprotoLabelDefs.LabelValueDefinition>
>
}) {
const t = useTheme()
return (
<View
style={[
a.flex_row,
a.flex_wrap,
a.gap_md,
t.atoms.bg_contrast_25,
a.rounded_md,
a.p_md,
a.mt_md,
]}>
<View>
<Text style={[a.font_bold, a.text_xs, t.atoms.text, a.pl_md, a.pb_xs]}>
Blurs
</Text>
<View
style={[
a.border,
a.rounded_full,
a.px_md,
a.py_sm,
t.atoms.border_contrast_medium,
t.atoms.bg,
]}>
<Toggle.Group
label="Blurs"
type="radio"
values={[def.blurs]}
onChange={values => setDef(v => ({...v, blurs: values[0]}))}>
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
<Toggle.Item name="content" label="Content">
<Toggle.Radio />
<Toggle.LabelText>Content</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="media" label="Media">
<Toggle.Radio />
<Toggle.LabelText>Media</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="none" label="None">
<Toggle.Radio />
<Toggle.LabelText>None</Toggle.LabelText>
</Toggle.Item>
</View>
</Toggle.Group>
</View>
</View>
<View>
<Text style={[a.font_bold, a.text_xs, t.atoms.text, a.pl_md, a.pb_xs]}>
Severity
</Text>
<View
style={[
a.border,
a.rounded_full,
a.px_md,
a.py_sm,
t.atoms.border_contrast_medium,
t.atoms.bg,
]}>
<Toggle.Group
label="Severity"
type="radio"
values={[def.severity]}
onChange={values => setDef(v => ({...v, severity: values[0]}))}>
<View style={[a.flex_row, a.gap_md, a.flex_wrap, a.align_center]}>
<Toggle.Item name="alert" label="Alert">
<Toggle.Radio />
<Toggle.LabelText>Alert</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="inform" label="Inform">
<Toggle.Radio />
<Toggle.LabelText>Inform</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="none" label="None">
<Toggle.Radio />
<Toggle.LabelText>None</Toggle.LabelText>
</Toggle.Item>
</View>
</Toggle.Group>
</View>
</View>
</View>
)
}
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 SmallToggler({
label,
children,
}: React.PropsWithChildren<{label: string}>) {
const [show, setShow] = React.useState(false)
return (
<View>
<View style={[a.flex_row]}>
<Button
variant="ghost"
color="secondary"
label="Toggle visibility"
size="tiny"
onPress={() => setShow(!show)}>
<ButtonText>{label}</ButtonText>
<ButtonIcon
icon={show ? ChevronTop : ChevronBottom}
position="right"
/>
</Button>
</View>
{show && children}
</View>
)
}
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,
}: {
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,
}: {
post: AppBskyFeedDefs.PostView
moderation: ModerationDecision
}) {
const t = useTheme()
if (moderation.ui('contentList').filter) {
return (
<P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md, a.mb_lg]}>
Filtered from the feed
</P>
)
}
return (
<FeedItem
post={post}
record={post.record as AppBskyFeedPost.Record}
moderation={moderation}
reason={undefined}
/>
)
}
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 MockNotifItem({
notif,
moderationOpts,
}: {
notif: FeedNotification
moderationOpts: ModerationOpts
}) {
const t = useTheme()
if (shouldFilterNotif(notif.notification, moderationOpts)) {
return (
<P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md]}>
Filtered from the feed
</P>
)
}
return <NotifFeedItem item={notif} moderationOpts={moderationOpts} />
}
function MockAccountCard({
profile,
moderation,
}: {
profile: AppBskyActorDefs.ProfileViewBasic
moderation: ModerationDecision
}) {
const t = useTheme()
if (moderation.ui('profileList').filter) {
return (
<P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md, a.mb_lg]}>
Filtered from the listing
</P>
)
}
return <ProfileCard profile={profile} />
}
function MockAccountScreen({
profile,
moderation,
moderationOpts,
}: {
profile: AppBskyActorDefs.ProfileViewBasic
moderation: ModerationDecision
moderationOpts: ModerationOpts
}) {
const t = useTheme()
const {_} = useLingui()
return (
<View style={[t.atoms.border_contrast_medium, a.border, a.mb_md]}>
<ScreenHider
style={{}}
screenDescription={_(msg`profile`)}
modui={moderation.ui('profileView')}>
<ProfileHeaderStandard
// @ts-ignore ProfileViewBasic is close enough -prf
profile={profile}
moderationOpts={moderationOpts}
descriptionRT={new RichText({text: profile.description as string})}
/>
</ScreenHider>
</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: t.palette.contrast_25,
width: 14,
height: 14,
},
]}>
{v && <Check size="xs" fill={t.palette.contrast_900} />}
</View>
<P style={a.text_xs}>{label}</P>
</View>
)
}