Implement feature gate control dialog in the lab
This commit is contained in:
@@ -1,9 +0,0 @@
|
|||||||
export type Gate =
|
|
||||||
// Keep this alphabetic please.
|
|
||||||
| 'debug_show_feedcontext'
|
|
||||||
| 'debug_subscriptions'
|
|
||||||
| 'old_postonboarding'
|
|
||||||
| 'onboarding_add_video_feed'
|
|
||||||
| 'remove_show_latest_button'
|
|
||||||
| 'test_gate_1'
|
|
||||||
| 'test_gate_2'
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
export type Gate =
|
||||||
|
// Keep this alphabetic please.
|
||||||
|
| 'debug_show_feedcontext'
|
||||||
|
| 'debug_subscriptions'
|
||||||
|
| 'old_postonboarding'
|
||||||
|
| 'onboarding_add_video_feed'
|
||||||
|
| 'remove_show_latest_button'
|
||||||
|
| 'test_gate_1'
|
||||||
|
| 'test_gate_2'
|
||||||
|
|
||||||
|
export interface GateDescription {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
help?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GateDescriptions = Record<Gate, GateDescription | undefined>
|
||||||
|
|
||||||
|
export function useGateDescriptions(): GateDescriptions {
|
||||||
|
const {_} = useLingui()
|
||||||
|
return {
|
||||||
|
debug_show_feedcontext: undefined,
|
||||||
|
debug_subscriptions: undefined,
|
||||||
|
old_postonboarding: undefined,
|
||||||
|
onboarding_add_video_feed: undefined,
|
||||||
|
remove_show_latest_button: undefined,
|
||||||
|
test_gate_1: {
|
||||||
|
title: _(msg`Test Gate 1`),
|
||||||
|
description: _(
|
||||||
|
msg`A test gate which should be removed before we launch this.`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
test_gate_2: {
|
||||||
|
title: _(msg`Test Gate 2`),
|
||||||
|
description: _(
|
||||||
|
msg`A test gate which should be removed before we launch this.`,
|
||||||
|
),
|
||||||
|
help: [
|
||||||
|
_(
|
||||||
|
msg`This is a lengthier description of the feature which instructs the user on how to use it.`,
|
||||||
|
),
|
||||||
|
_(
|
||||||
|
msg`Each line is interpretted as a separate paragraph. We might also need to introduce a way to put images in here, but let's not get ahead of ourselves.`,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import {type Gate} from '#/lib/statsig/gates'
|
||||||
|
|
||||||
export type MetricEvents = {
|
export type MetricEvents = {
|
||||||
// App events
|
// App events
|
||||||
init: {
|
init: {
|
||||||
@@ -329,4 +331,13 @@ export type MetricEvents = {
|
|||||||
details: boolean
|
details: boolean
|
||||||
}
|
}
|
||||||
'reportDialog:failure': {}
|
'reportDialog:failure': {}
|
||||||
|
|
||||||
|
'featureGate:override': {
|
||||||
|
gate: Gate
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
'featureGate:feedback': {
|
||||||
|
gate: Gate
|
||||||
|
feedback: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import {useState} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {type Gate, useGateDescriptions} from '#/lib/statsig/gates'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {Divider} from '#/components/Divider'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {
|
||||||
|
EmojiSad_Stroke2_Corner0_Rounded as EmojiSad,
|
||||||
|
EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile,
|
||||||
|
} from '#/components/icons/Emoji'
|
||||||
|
import {P, Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
enum FeedbackToken {
|
||||||
|
LoveIt = 'love-it',
|
||||||
|
HateIt = 'hate-it',
|
||||||
|
Useful = 'useful',
|
||||||
|
MakesAppLooksBetter = 'makes-app-look-better',
|
||||||
|
MakesThingsEasier = 'makes-things-easier',
|
||||||
|
PromisingButNeedsWork = 'promising-but-needs-work',
|
||||||
|
RemovesImportantFeatures = 'removes-important-features',
|
||||||
|
ConfusedMe = 'confused-me',
|
||||||
|
BrokeMyWillToLive = 'broke-my-will-to-live',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeatureGateDialog({
|
||||||
|
control,
|
||||||
|
gate,
|
||||||
|
}: {
|
||||||
|
control: Dialog.DialogOuterProps['control']
|
||||||
|
gate: Gate
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
const descriptions = useGateDescriptions()
|
||||||
|
const desc = descriptions[gate]
|
||||||
|
|
||||||
|
if (!desc) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Outer control={control}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
<Dialog.ScrollableInner
|
||||||
|
accessibilityDescribedBy="dialog-description"
|
||||||
|
accessibilityLabelledBy="dialog-title">
|
||||||
|
<View style={[a.relative, a.gap_md, a.w_full]}>
|
||||||
|
<Text
|
||||||
|
nativeID="dialog-title"
|
||||||
|
style={[a.text_2xl, a.font_bold, t.atoms.text]}>
|
||||||
|
{desc.title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<P nativeID="dialog-description">{desc.description}</P>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[a.rounded_sm, t.atoms.bg_contrast_25, a.px_md, a.py_sm]}>
|
||||||
|
<Toggle.Item
|
||||||
|
name="quoteposts"
|
||||||
|
type="checkbox"
|
||||||
|
label={_(msg`Tap to toggle this experiment.`)}
|
||||||
|
value={true}
|
||||||
|
onChange={_v => {}}
|
||||||
|
style={[a.justify_between]}>
|
||||||
|
<Text style={[t.atoms.text_contrast_high]}>
|
||||||
|
<Trans>Enable on this device</Trans>
|
||||||
|
</Text>
|
||||||
|
<Toggle.Switch />
|
||||||
|
</Toggle.Item>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{desc.help ? (
|
||||||
|
<>
|
||||||
|
<Divider style={[a.my_lg]} />
|
||||||
|
{desc.help.map((line, i) => (
|
||||||
|
<P key={`help-${i}`}>{line}</P>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
<Divider style={[a.my_lg]} />
|
||||||
|
|
||||||
|
<Text style={[a.text_xl, a.font_bold, t.atoms.text]}>Feedback</Text>
|
||||||
|
|
||||||
|
<View style={[a.flex_row, a.gap_sm]}>
|
||||||
|
<FeedbackButton gate={gate} feedback={FeedbackToken.LoveIt} />
|
||||||
|
<FeedbackButton gate={gate} feedback={FeedbackToken.HateIt} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.flex_col, a.gap_sm]}>
|
||||||
|
<FeedbackButton gate={gate} feedback={FeedbackToken.Useful} />
|
||||||
|
<FeedbackButton
|
||||||
|
gate={gate}
|
||||||
|
feedback={FeedbackToken.MakesAppLooksBetter}
|
||||||
|
/>
|
||||||
|
<FeedbackButton
|
||||||
|
gate={gate}
|
||||||
|
feedback={FeedbackToken.MakesThingsEasier}
|
||||||
|
/>
|
||||||
|
<FeedbackButton
|
||||||
|
gate={gate}
|
||||||
|
feedback={FeedbackToken.PromisingButNeedsWork}
|
||||||
|
/>
|
||||||
|
<FeedbackButton
|
||||||
|
gate={gate}
|
||||||
|
feedback={FeedbackToken.RemovesImportantFeatures}
|
||||||
|
/>
|
||||||
|
<FeedbackButton gate={gate} feedback={FeedbackToken.ConfusedMe} />
|
||||||
|
<FeedbackButton
|
||||||
|
gate={gate}
|
||||||
|
feedback={FeedbackToken.BrokeMyWillToLive}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Dialog.ScrollableInner>
|
||||||
|
</Dialog.Outer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeedbackButton({
|
||||||
|
gate,
|
||||||
|
feedback,
|
||||||
|
}: {
|
||||||
|
gate: Gate
|
||||||
|
feedback: FeedbackToken
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const [pressed, setPressed] = useState(false)
|
||||||
|
const onPress = () => {
|
||||||
|
setPressed(!pressed)
|
||||||
|
logger.metric('featureGate:feedback', {gate, feedback})
|
||||||
|
}
|
||||||
|
const label = {
|
||||||
|
[FeedbackToken.BrokeMyWillToLive]: _(msg`It broke my will to live`),
|
||||||
|
[FeedbackToken.ConfusedMe]: _(msg`It confused me`),
|
||||||
|
[FeedbackToken.HateIt]: _(msg`I hate it`),
|
||||||
|
[FeedbackToken.LoveIt]: _(msg`I love it`),
|
||||||
|
[FeedbackToken.MakesAppLooksBetter]: _(msg`Makes the app look better`),
|
||||||
|
[FeedbackToken.MakesThingsEasier]: _(msg`Makes things easier`),
|
||||||
|
[FeedbackToken.PromisingButNeedsWork]: _(msg`Promising, but needs work`),
|
||||||
|
[FeedbackToken.RemovesImportantFeatures]: _(
|
||||||
|
msg`Removes important features`,
|
||||||
|
),
|
||||||
|
[FeedbackToken.Useful]: _(msg`Useful`),
|
||||||
|
}[feedback]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
color={pressed ? 'primary' : 'secondary'}
|
||||||
|
size="large"
|
||||||
|
onPress={onPress}
|
||||||
|
style={[
|
||||||
|
(feedback === FeedbackToken.LoveIt ||
|
||||||
|
feedback === FeedbackToken.HateIt) &&
|
||||||
|
a.flex_1,
|
||||||
|
]}
|
||||||
|
label={label}>
|
||||||
|
{feedback === FeedbackToken.LoveIt && (
|
||||||
|
<ButtonIcon icon={EmojiSmile} position="left" />
|
||||||
|
)}
|
||||||
|
{feedback === FeedbackToken.HateIt && (
|
||||||
|
<ButtonIcon icon={EmojiSad} position="left" />
|
||||||
|
)}
|
||||||
|
<ButtonText>{label}</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
import {useState} from 'react'
|
import {View} from 'react-native'
|
||||||
import {Alert, View} from 'react-native'
|
import {Trans} from '@lingui/macro'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
|
||||||
|
|
||||||
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
import {type Gate, useGateDescriptions} from '#/lib/statsig/gates'
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
|
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {FeatureGateDialog} from './FeatureGateDialog'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'LabSettings'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'LabSettings'>
|
||||||
export function LabSettingsScreen({}: Props) {
|
export function LabSettingsScreen({}: Props) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const descriptions = useGateDescriptions()
|
||||||
const gate = useGate()
|
|
||||||
|
const gates: Gate[] = Object.entries(descriptions)
|
||||||
|
.filter(([_k, v]) => !!v)
|
||||||
|
.map(([k, _v]) => k as Gate)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen>
|
<Layout.Screen>
|
||||||
@@ -30,18 +33,42 @@ export function LabSettingsScreen({}: Props) {
|
|||||||
<Layout.Header.Slot />
|
<Layout.Header.Slot />
|
||||||
</Layout.Header.Outer>
|
</Layout.Header.Outer>
|
||||||
|
|
||||||
<Layout.Content contentContainerStyle={[a.p_lg]}>
|
<Layout.Content>
|
||||||
<Text
|
<View style={[a.p_lg, a.pb_0]}>
|
||||||
style={[
|
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||||
a.text_md,
|
<Trans>Experimental features on Bluesky.</Trans>
|
||||||
a.mt_xl,
|
</Text>
|
||||||
a.mb_sm,
|
</View>
|
||||||
a.font_bold,
|
<SettingsList.Container>
|
||||||
t.atoms.text_contrast_medium,
|
{gates.map(gate => (
|
||||||
]}>
|
<ExperimentButton key={gate} gate={gate} enabled={true} />
|
||||||
<Trans>The Lab (TODO)</Trans>
|
))}
|
||||||
</Text>
|
</SettingsList.Container>
|
||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ExperimentButton({gate, enabled}: {gate: Gate; enabled: boolean}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const ctrl = useDialogControl()
|
||||||
|
const descriptions = useGateDescriptions()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingsList.Divider />
|
||||||
|
<SettingsList.PressableItem
|
||||||
|
label={descriptions[gate]?.title || ''}
|
||||||
|
onPress={() => ctrl.open()}>
|
||||||
|
<SettingsList.ItemIcon icon={BeakerIcon} />
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
{descriptions[gate]?.title}
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<SettingsList.BadgeText
|
||||||
|
style={[a.flex_1, enabled && {color: t.palette.positive_400}]}>
|
||||||
|
{enabled ? <Trans>Enabled</Trans> : <Trans>Disabled</Trans>}
|
||||||
|
</SettingsList.BadgeText>
|
||||||
|
</SettingsList.PressableItem>
|
||||||
|
<FeatureGateDialog control={ctrl} gate={gate} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,23 +2,23 @@ import {useState} from 'react'
|
|||||||
import {LayoutAnimation, Pressable, View} from 'react-native'
|
import {LayoutAnimation, Pressable, View} from 'react-native'
|
||||||
import {Linking} from 'react-native'
|
import {Linking} from 'react-native'
|
||||||
import {useReducedMotion} from 'react-native-reanimated'
|
import {useReducedMotion} from 'react-native-reanimated'
|
||||||
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
import {type AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
import {IS_INTERNAL} from '#/lib/app-info'
|
import {IS_INTERNAL} from '#/lib/app-info'
|
||||||
import {HELP_DESK_URL} from '#/lib/constants'
|
import {HELP_DESK_URL} from '#/lib/constants'
|
||||||
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
||||||
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
import {type CommonNavigatorParams, type NavigationProp} from '#/lib/routes/types'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {clearStorage} from '#/state/persisted'
|
import {clearStorage} from '#/state/persisted'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration'
|
import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration'
|
||||||
import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile'
|
import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile'
|
||||||
import {SessionAccount, useSession, useSessionApi} from '#/state/session'
|
import {type SessionAccount, useSession, useSessionApi} from '#/state/session'
|
||||||
import {useOnboardingDispatch} from '#/state/shell'
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
import {useCloseAllActiveElements} from '#/state/util'
|
import {useCloseAllActiveElements} from '#/state/util'
|
||||||
@@ -32,6 +32,7 @@ import {AvatarStackWithFetch} from '#/components/AvatarStack'
|
|||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount'
|
import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount'
|
||||||
import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
|
import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
|
||||||
|
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
|
||||||
import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo'
|
import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo'
|
||||||
import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron'
|
import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron'
|
||||||
import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion'
|
import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion'
|
||||||
@@ -203,7 +204,7 @@ export function SettingsScreen({}: Props) {
|
|||||||
</SettingsList.ItemText>
|
</SettingsList.ItemText>
|
||||||
</SettingsList.LinkItem>
|
</SettingsList.LinkItem>
|
||||||
<SettingsList.LinkItem to="/settings/lab" label={_(msg`The Lab`)}>
|
<SettingsList.LinkItem to="/settings/lab" label={_(msg`The Lab`)}>
|
||||||
<SettingsList.ItemIcon icon={EarthIcon} />
|
<SettingsList.ItemIcon icon={BeakerIcon} />
|
||||||
<SettingsList.ItemText>
|
<SettingsList.ItemText>
|
||||||
<Trans>The Lab</Trans>
|
<Trans>The Lab</Trans>
|
||||||
</SettingsList.ItemText>
|
</SettingsList.ItemText>
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import React, {useContext, useMemo} from 'react'
|
import React, {useContext, useMemo} from 'react'
|
||||||
import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'
|
import {
|
||||||
|
type GestureResponderEvent,
|
||||||
|
type StyleProp,
|
||||||
|
type TextStyle,
|
||||||
|
View,
|
||||||
|
type ViewStyle,
|
||||||
|
} from 'react-native'
|
||||||
|
|
||||||
import {HITSLOP_10} from '#/lib/constants'
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
|
||||||
import * as Button from '#/components/Button'
|
import * as Button from '#/components/Button'
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
|
||||||
import {Link, LinkProps} from '#/components/Link'
|
import {Link, type LinkProps} from '#/components/Link'
|
||||||
import {createPortalGroup} from '#/components/Portal'
|
import {createPortalGroup} from '#/components/Portal'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
@@ -268,7 +274,7 @@ export function BadgeText({
|
|||||||
style,
|
style,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<TextStyle>
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user