Display beta badge when isBetaUser is true (#11160)
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import {useState} from 'react'
|
||||
import {type Insets, Pressable, View} from 'react-native'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
|
||||
import * as Tooltip from '#/components/Tooltip'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
/**
|
||||
* Whether to show the beta badge for a given profile. Only shown on the
|
||||
* viewer's own profile, and only when the viewer has opted in to beta features.
|
||||
*/
|
||||
export function useIsBetaBadgeVisible(
|
||||
profile: bsky.profile.AnyProfileView,
|
||||
): boolean {
|
||||
const {currentAccount} = useSession()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const isBetaUser = preferences?.bskyAppState?.isBetaUser ?? false
|
||||
const isSelf = currentAccount?.did === profile.did
|
||||
|
||||
return isSelf && isBetaUser
|
||||
}
|
||||
|
||||
export function BetaBadge({
|
||||
profile,
|
||||
width,
|
||||
padding,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
width: number
|
||||
padding: number
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const isVisible = useIsBetaBadgeVisible(profile)
|
||||
|
||||
if (!isVisible) return null
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
{backgroundColor: t.palette.primary_50, padding},
|
||||
]}>
|
||||
<BeakerIcon width={width} fill={t.palette.primary_500} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function BetaBadgeButton({
|
||||
profile,
|
||||
width,
|
||||
padding,
|
||||
hitSlop,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
width: number
|
||||
padding: number
|
||||
hitSlop: Insets
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const isVisible = useIsBetaBadgeVisible(profile)
|
||||
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false)
|
||||
|
||||
if (!isVisible) return null
|
||||
|
||||
return (
|
||||
<Tooltip.Outer
|
||||
color="primary"
|
||||
visible={tooltipVisible}
|
||||
onVisibleChange={setTooltipVisible}>
|
||||
<Tooltip.Target>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={l`Beta features enabled`}
|
||||
accessibilityHint=""
|
||||
hitSlop={hitSlop}
|
||||
style={({hovered}) => [
|
||||
a.rounded_full,
|
||||
a.transition_transform,
|
||||
{
|
||||
backgroundColor: t.palette.primary_50,
|
||||
padding,
|
||||
transform: [
|
||||
{
|
||||
scale: hovered ? 1.1 : 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
onPress={() => setTooltipVisible(v => !v)}>
|
||||
<BeakerIcon width={width} fill={t.palette.primary_500} />
|
||||
</Pressable>
|
||||
</Tooltip.Target>
|
||||
<Tooltip.BubbleText label={l`Beta features enabled`}>
|
||||
<Trans>Beta features enabled</Trans>
|
||||
</Tooltip.BubbleText>
|
||||
</Tooltip.Outer>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {View} from 'react-native'
|
||||
import {type Insets, View} from 'react-native'
|
||||
import {type ComAtprotoLabelDefs} from '@atproto/api'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
@@ -44,9 +44,11 @@ export function BotBadge({
|
||||
export function BotBadgeButton({
|
||||
profile,
|
||||
width,
|
||||
hitSlop,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
width: number
|
||||
hitSlop: Insets
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const ax = useAnalytics()
|
||||
@@ -61,7 +63,7 @@ export function BotBadgeButton({
|
||||
<>
|
||||
<Button
|
||||
label={l`Automated account`}
|
||||
hitSlop={20}
|
||||
hitSlop={hitSlop}
|
||||
onPress={evt => {
|
||||
evt.preventDefault()
|
||||
ax.metric('bot:badge:click', {})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {atoms as a, useAlf, type ViewStyleProp} from '#/alf'
|
||||
import {useNativeFontScale} from '#/alf/util/dimensions'
|
||||
@@ -8,6 +9,7 @@ import {useSimpleVerificationState} from '#/components/verification'
|
||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {BetaBadge, BetaBadgeButton, useIsBetaBadgeVisible} from './BetaBadge'
|
||||
|
||||
export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
|
||||
@@ -27,6 +29,22 @@ const botIconSizes: Record<Size, number> = {
|
||||
xl: 23,
|
||||
} as const
|
||||
|
||||
const betaIconSizes: Record<Size, number> = {
|
||||
xs: 8,
|
||||
sm: 8,
|
||||
md: 8,
|
||||
lg: 10,
|
||||
xl: 12,
|
||||
} as const
|
||||
|
||||
const betaBadgePadding: Record<Size, number> = {
|
||||
xs: 1,
|
||||
sm: 2,
|
||||
md: 3,
|
||||
lg: 4,
|
||||
xl: 5,
|
||||
} as const
|
||||
|
||||
export function ProfileBadges({
|
||||
profile,
|
||||
interactive = false,
|
||||
@@ -41,13 +59,19 @@ export function ProfileBadges({
|
||||
}) {
|
||||
const shadowed = useProfileShadow(profile)
|
||||
const verification = useSimpleVerificationState({profile})
|
||||
const badgeVisibility = [
|
||||
verification.showBadge,
|
||||
useIsBetaBadgeVisible(profile),
|
||||
isBotAccount(shadowed),
|
||||
]
|
||||
const badgeCount = badgeVisibility.filter(Boolean).length
|
||||
const nativeScaleMultiplier = useNativeFontScale()
|
||||
const {
|
||||
fonts: {scaleMultiplier: alfScaleMultiplier},
|
||||
} = useAlf()
|
||||
|
||||
// if nothing to show, don't render the container at all
|
||||
if (!verification.showBadge && !isBotAccount(shadowed)) return null
|
||||
if (badgeCount < 1) return null
|
||||
|
||||
const isOnTheSmallSide = size === 'xs' || size === 'sm'
|
||||
|
||||
@@ -57,31 +81,57 @@ export function ProfileBadges({
|
||||
|
||||
const verificationIconWidth = verificationIconSizes[size] * scaleMultiplier
|
||||
const botIconWidth = botIconSizes[size] * scaleMultiplier
|
||||
const betaIconWidth = betaIconSizes[size] * scaleMultiplier
|
||||
const betaBadgeScaledPadding = betaBadgePadding[size] * scaleMultiplier
|
||||
|
||||
const gap = isOnTheSmallSide ? a.gap_2xs : a.gap_xs
|
||||
const padding = gap.gap / 2
|
||||
let visibleBadgeIndex = 0
|
||||
const hitSlops = badgeVisibility.map(isVisible => {
|
||||
if (!isVisible) return HITSLOP_20
|
||||
|
||||
const index = visibleBadgeIndex++
|
||||
return {
|
||||
...HITSLOP_20,
|
||||
left: index === 0 ? HITSLOP_20.left : padding,
|
||||
right: index === badgeCount - 1 ? HITSLOP_20.right : padding,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
isOnTheSmallSide ? a.gap_2xs : a.gap_xs,
|
||||
style,
|
||||
]}>
|
||||
<View style={[a.flex_row, a.align_center, gap, style]}>
|
||||
{interactive ? (
|
||||
<>
|
||||
<VerificationCheckButton
|
||||
profile={shadowed}
|
||||
width={verificationIconWidth}
|
||||
hitSlop={hitSlops[0]}
|
||||
/>
|
||||
<BetaBadgeButton
|
||||
profile={shadowed}
|
||||
width={betaIconWidth}
|
||||
padding={betaBadgeScaledPadding}
|
||||
hitSlop={hitSlops[1]}
|
||||
/>
|
||||
<BotBadgeButton
|
||||
profile={shadowed}
|
||||
width={botIconWidth}
|
||||
hitSlop={hitSlops[2]}
|
||||
/>
|
||||
<BotBadgeButton profile={shadowed} width={botIconWidth} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{verification.showBadge && (
|
||||
{verification.showBadge ? (
|
||||
<VerificationCheck
|
||||
verifier={verification.role === 'verifier'}
|
||||
width={verificationIconWidth}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<BetaBadge
|
||||
profile={shadowed}
|
||||
width={betaIconWidth}
|
||||
padding={betaBadgeScaledPadding}
|
||||
/>
|
||||
<BotBadge profile={shadowed} width={botIconWidth} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {View} from 'react-native'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {type Insets, View} from 'react-native'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {type Shadow} from '#/state/cache/types'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
@@ -52,16 +51,25 @@ export function shouldShowVerificationCheckButton(
|
||||
export function VerificationCheckButton({
|
||||
profile,
|
||||
width,
|
||||
hitSlop,
|
||||
}: {
|
||||
profile: Shadow<bsky.profile.AnyProfileView>
|
||||
width: number
|
||||
hitSlop: Insets
|
||||
}) {
|
||||
const state = useFullVerificationState({
|
||||
profile,
|
||||
})
|
||||
|
||||
if (shouldShowVerificationCheckButton(state)) {
|
||||
return <Badge profile={profile} verificationState={state} width={width} />
|
||||
return (
|
||||
<Badge
|
||||
profile={profile}
|
||||
verificationState={state}
|
||||
width={width}
|
||||
hitSlop={hitSlop}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -71,14 +79,16 @@ function Badge({
|
||||
profile,
|
||||
verificationState: state,
|
||||
width,
|
||||
hitSlop,
|
||||
}: {
|
||||
profile: Shadow<bsky.profile.AnyProfileView>
|
||||
verificationState: FullVerificationState
|
||||
width: number
|
||||
hitSlop: Insets
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const ax = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const verificationsDialogControl = useDialogControl()
|
||||
const verifierDialogControl = useDialogControl()
|
||||
|
||||
@@ -89,10 +99,10 @@ function Badge({
|
||||
<Button
|
||||
label={
|
||||
state.profile.isViewer
|
||||
? _(msg`View your verifications`)
|
||||
: _(msg`View this user's verifications`)
|
||||
? l`View your verifications`
|
||||
: l`View this user's verifications`
|
||||
}
|
||||
hitSlop={20}
|
||||
hitSlop={hitSlop}
|
||||
onPress={evt => {
|
||||
evt.preventDefault()
|
||||
ax.metric('verification:badge:click', {})
|
||||
@@ -132,13 +142,11 @@ function Badge({
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<VerificationsDialog
|
||||
control={verificationsDialogControl}
|
||||
profile={profile}
|
||||
verificationState={state}
|
||||
/>
|
||||
|
||||
<VerifierDialog
|
||||
control={verifierDialogControl}
|
||||
profile={profile}
|
||||
|
||||
Reference in New Issue
Block a user