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 {type ComAtprotoLabelDefs} from '@atproto/api'
|
||||||
import {useLingui} from '@lingui/react/macro'
|
import {useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
@@ -44,9 +44,11 @@ export function BotBadge({
|
|||||||
export function BotBadgeButton({
|
export function BotBadgeButton({
|
||||||
profile,
|
profile,
|
||||||
width,
|
width,
|
||||||
|
hitSlop,
|
||||||
}: {
|
}: {
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
width: number
|
width: number
|
||||||
|
hitSlop: Insets
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
@@ -61,7 +63,7 @@ export function BotBadgeButton({
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
label={l`Automated account`}
|
label={l`Automated account`}
|
||||||
hitSlop={20}
|
hitSlop={hitSlop}
|
||||||
onPress={evt => {
|
onPress={evt => {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
ax.metric('bot:badge:click', {})
|
ax.metric('bot:badge:click', {})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
import {HITSLOP_20} from '#/lib/constants'
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {atoms as a, useAlf, type ViewStyleProp} from '#/alf'
|
import {atoms as a, useAlf, type ViewStyleProp} from '#/alf'
|
||||||
import {useNativeFontScale} from '#/alf/util/dimensions'
|
import {useNativeFontScale} from '#/alf/util/dimensions'
|
||||||
@@ -8,6 +9,7 @@ import {useSimpleVerificationState} from '#/components/verification'
|
|||||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||||
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
|
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
import {BetaBadge, BetaBadgeButton, useIsBetaBadgeVisible} from './BetaBadge'
|
||||||
|
|
||||||
export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||||
|
|
||||||
@@ -27,6 +29,22 @@ const botIconSizes: Record<Size, number> = {
|
|||||||
xl: 23,
|
xl: 23,
|
||||||
} as const
|
} 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({
|
export function ProfileBadges({
|
||||||
profile,
|
profile,
|
||||||
interactive = false,
|
interactive = false,
|
||||||
@@ -41,13 +59,19 @@ export function ProfileBadges({
|
|||||||
}) {
|
}) {
|
||||||
const shadowed = useProfileShadow(profile)
|
const shadowed = useProfileShadow(profile)
|
||||||
const verification = useSimpleVerificationState({profile})
|
const verification = useSimpleVerificationState({profile})
|
||||||
|
const badgeVisibility = [
|
||||||
|
verification.showBadge,
|
||||||
|
useIsBetaBadgeVisible(profile),
|
||||||
|
isBotAccount(shadowed),
|
||||||
|
]
|
||||||
|
const badgeCount = badgeVisibility.filter(Boolean).length
|
||||||
const nativeScaleMultiplier = useNativeFontScale()
|
const nativeScaleMultiplier = useNativeFontScale()
|
||||||
const {
|
const {
|
||||||
fonts: {scaleMultiplier: alfScaleMultiplier},
|
fonts: {scaleMultiplier: alfScaleMultiplier},
|
||||||
} = useAlf()
|
} = useAlf()
|
||||||
|
|
||||||
// if nothing to show, don't render the container at all
|
// 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'
|
const isOnTheSmallSide = size === 'xs' || size === 'sm'
|
||||||
|
|
||||||
@@ -57,31 +81,57 @@ export function ProfileBadges({
|
|||||||
|
|
||||||
const verificationIconWidth = verificationIconSizes[size] * scaleMultiplier
|
const verificationIconWidth = verificationIconSizes[size] * scaleMultiplier
|
||||||
const botIconWidth = botIconSizes[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 (
|
return (
|
||||||
<View
|
<View style={[a.flex_row, a.align_center, gap, style]}>
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
isOnTheSmallSide ? a.gap_2xs : a.gap_xs,
|
|
||||||
style,
|
|
||||||
]}>
|
|
||||||
{interactive ? (
|
{interactive ? (
|
||||||
<>
|
<>
|
||||||
<VerificationCheckButton
|
<VerificationCheckButton
|
||||||
profile={shadowed}
|
profile={shadowed}
|
||||||
width={verificationIconWidth}
|
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
|
<VerificationCheck
|
||||||
verifier={verification.role === 'verifier'}
|
verifier={verification.role === 'verifier'}
|
||||||
width={verificationIconWidth}
|
width={verificationIconWidth}
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
|
<BetaBadge
|
||||||
|
profile={shadowed}
|
||||||
|
width={betaIconWidth}
|
||||||
|
padding={betaBadgeScaledPadding}
|
||||||
|
/>
|
||||||
<BotBadge profile={shadowed} width={botIconWidth} />
|
<BotBadge profile={shadowed} width={botIconWidth} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {View} from 'react-native'
|
import {type Insets, View} from 'react-native'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {useLingui} from '@lingui/react/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {type Shadow} from '#/state/cache/types'
|
import {type Shadow} from '#/state/cache/types'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
@@ -52,16 +51,25 @@ export function shouldShowVerificationCheckButton(
|
|||||||
export function VerificationCheckButton({
|
export function VerificationCheckButton({
|
||||||
profile,
|
profile,
|
||||||
width,
|
width,
|
||||||
|
hitSlop,
|
||||||
}: {
|
}: {
|
||||||
profile: Shadow<bsky.profile.AnyProfileView>
|
profile: Shadow<bsky.profile.AnyProfileView>
|
||||||
width: number
|
width: number
|
||||||
|
hitSlop: Insets
|
||||||
}) {
|
}) {
|
||||||
const state = useFullVerificationState({
|
const state = useFullVerificationState({
|
||||||
profile,
|
profile,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (shouldShowVerificationCheckButton(state)) {
|
if (shouldShowVerificationCheckButton(state)) {
|
||||||
return <Badge profile={profile} verificationState={state} width={width} />
|
return (
|
||||||
|
<Badge
|
||||||
|
profile={profile}
|
||||||
|
verificationState={state}
|
||||||
|
width={width}
|
||||||
|
hitSlop={hitSlop}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
@@ -71,14 +79,16 @@ function Badge({
|
|||||||
profile,
|
profile,
|
||||||
verificationState: state,
|
verificationState: state,
|
||||||
width,
|
width,
|
||||||
|
hitSlop,
|
||||||
}: {
|
}: {
|
||||||
profile: Shadow<bsky.profile.AnyProfileView>
|
profile: Shadow<bsky.profile.AnyProfileView>
|
||||||
verificationState: FullVerificationState
|
verificationState: FullVerificationState
|
||||||
width: number
|
width: number
|
||||||
|
hitSlop: Insets
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const verificationsDialogControl = useDialogControl()
|
const verificationsDialogControl = useDialogControl()
|
||||||
const verifierDialogControl = useDialogControl()
|
const verifierDialogControl = useDialogControl()
|
||||||
|
|
||||||
@@ -89,10 +99,10 @@ function Badge({
|
|||||||
<Button
|
<Button
|
||||||
label={
|
label={
|
||||||
state.profile.isViewer
|
state.profile.isViewer
|
||||||
? _(msg`View your verifications`)
|
? l`View your verifications`
|
||||||
: _(msg`View this user's verifications`)
|
: l`View this user's verifications`
|
||||||
}
|
}
|
||||||
hitSlop={20}
|
hitSlop={hitSlop}
|
||||||
onPress={evt => {
|
onPress={evt => {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
ax.metric('verification:badge:click', {})
|
ax.metric('verification:badge:click', {})
|
||||||
@@ -132,13 +142,11 @@ function Badge({
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<VerificationsDialog
|
<VerificationsDialog
|
||||||
control={verificationsDialogControl}
|
control={verificationsDialogControl}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
verificationState={state}
|
verificationState={state}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<VerifierDialog
|
<VerifierDialog
|
||||||
control={verifierDialogControl}
|
control={verifierDialogControl}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
|
|||||||
Reference in New Issue
Block a user