From 2223babc2b2b6207a4c5404231dbdefe7a1ca1b0 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 14 Jan 2026 16:57:21 -0600 Subject: [PATCH] [APP-1759] Live Now Open Beta (#9699) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add report dialog to LiveStatus dialog * Mr Worldwide™ * Bug fix bug fix * Copy update * Simplify parsing * Bump api sdk * update dev-env * Update yarn.lock * Bump api sdk * Refactor useActorStatus, add disablement and appeal dialog * Fix types * Guard against chat profile views * Go live (disabled) * Fix types * Update config and gates * Add allowed services * Merge conflicts * Fix gradient handling for nudge * Only show nudge on your own profile * Fix live avi overflow breakage * Add TODO * Feedback * Update nux image --------- Co-authored-by: Samuel Newman --- assets/images/live_now_beta.webp | Bin 0 -> 14882 bytes src/components/Dialog/index.tsx | 10 +- src/components/Menu/index.tsx | 12 +- src/components/Menu/index.web.tsx | 11 +- src/components/Menu/types.ts | 1 + .../dialogs/nuxs/LiveNowBetaDialog.tsx | 209 ++++++++++++++++++ src/components/dialogs/nuxs/index.tsx | 14 +- src/components/live/EditLiveDialog.tsx | 28 +-- src/components/live/GoLiveDialog.tsx | 43 ++-- src/components/live/queries.ts | 15 +- src/components/live/utils.ts | 25 +++ src/features/nuxs/components/Dot.tsx | 31 +++ src/features/nuxs/components/Gradient.tsx | 24 ++ src/lib/actor-status.ts | 13 +- src/lib/statsig/gates.ts | 1 + src/state/queries/nuxs/definitions.ts | 12 + src/state/service-config.tsx | 32 ++- src/view/com/posts/PostFeed.tsx | 2 +- src/view/com/profile/ProfileMenu.tsx | 80 +++++-- 19 files changed, 456 insertions(+), 107 deletions(-) create mode 100644 assets/images/live_now_beta.webp create mode 100644 src/components/dialogs/nuxs/LiveNowBetaDialog.tsx create mode 100644 src/features/nuxs/components/Dot.tsx create mode 100644 src/features/nuxs/components/Gradient.tsx diff --git a/assets/images/live_now_beta.webp b/assets/images/live_now_beta.webp new file mode 100644 index 0000000000000000000000000000000000000000..7b5b074835217dd4276b5638a88745604e5d8645 GIT binary patch literal 14882 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 z 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 d^mR$R&Pk5RB?mYEZ#n+|znrYa2bKp63; @@ -202,16 +202,18 @@ export function ItemText({children, style}: ItemTextProps) { ) } -export function ItemIcon({icon: Comp}: ItemIconProps) { +export function ItemIcon({icon: Comp, fill}: ItemIconProps) { const t = useTheme() const {disabled} = useMenuItemContext() return ( ) diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx index 242449bf4..339b26350 100644 --- a/src/components/Menu/index.web.tsx +++ b/src/components/Menu/index.web.tsx @@ -262,6 +262,7 @@ export function Item({children, label, onPress, style, ...rest}: ItemProps) { a.gap_lg, a.py_sm, a.rounded_xs, + a.overflow_hidden, {minHeight: 32, paddingHorizontal: 10}, web({outline: 0}), (hovered || focused) && @@ -302,7 +303,7 @@ export function ItemText({children, style}: ItemTextProps) { ) } -export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) { +export function ItemIcon({icon: Comp, position = 'left', fill}: ItemIconProps) { const t = useTheme() const {disabled} = useMenuItemContext() return ( @@ -319,9 +320,11 @@ export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) { diff --git a/src/components/Menu/types.ts b/src/components/Menu/types.ts index e9ee082f4..a5fce102b 100644 --- a/src/components/Menu/types.ts +++ b/src/components/Menu/types.ts @@ -107,6 +107,7 @@ export type ItemTextProps = React.PropsWithChildren export type ItemIconProps = React.PropsWithChildren<{ icon: React.ComponentType position?: 'left' | 'right' + fill?: (props: {disabled: boolean}) => string }> export type GroupProps = React.PropsWithChildren diff --git a/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx b/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx new file mode 100644 index 000000000..b081322c4 --- /dev/null +++ b/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx @@ -0,0 +1,209 @@ +import {useCallback, useMemo} from 'react' +import {View} from 'react-native' +import {Image} from 'expo-image' +import {LinearGradient} from 'expo-linear-gradient' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isWeb} from '#/platform/detection' +import {atoms as a, select, useTheme, utils, web} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {useNuxDialogContext} from '#/components/dialogs/nuxs' +import { + createIsEnabledCheck, + isExistingUserAsOf, +} from '#/components/dialogs/nuxs/utils' +import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' +import {Text} from '#/components/Typography' +import {IS_E2E} from '#/env' + +export const enabled = createIsEnabledCheck(props => { + return ( + !IS_E2E && + isExistingUserAsOf( + '2026-01-16T00:00:00.000Z', + props.currentProfile.createdAt, + ) && + props.gate('live_now_beta') + ) +}) + +export function LiveNowBetaDialog() { + const t = useTheme() + const {_} = useLingui() + const nuxDialogs = useNuxDialogContext() + const control = Dialog.useDialogControl() + + Dialog.useAutoOpen(control) + + const onClose = useCallback(() => { + nuxDialogs.dismissActiveNux() + }, [nuxDialogs]) + + const shadowColor = useMemo(() => { + return select(t.name, { + light: utils.alpha(t.palette.primary_900, 0.4), + dark: utils.alpha(t.palette.primary_25, 0.4), + dim: utils.alpha(t.palette.primary_25, 0.4), + }) + }, [t]) + + return ( + + + + + + + + + + Beta Feature + + + + + + {_( + + + + + + + Show when you’re live + + + + Streaming on Twitch? Set your live status on Bluesky to add a + badge to your avatar. Tapping it takes people straight to your + stream. + + + + + {!isWeb && ( + + )} + + + + + + ) +} diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx index 2ab319faf..eadadeb8e 100644 --- a/src/components/dialogs/nuxs/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -20,9 +20,9 @@ import {useProfileQuery} from '#/state/queries/profile' import {type SessionAccount, useSession} from '#/state/session' import {useOnboardingState} from '#/state/shell' import { - enabled as isFindContactsAnnouncementEnabled, - FindContactsAnnouncement, -} from '#/components/dialogs/nuxs/FindContactsAnnouncement' + enabled as isLiveNowBetaDialogEnabled, + LiveNowBetaDialog, +} from '#/components/dialogs/nuxs/LiveNowBetaDialog' import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing' import {type EnabledCheckProps} from '#/components/dialogs/nuxs/utils' import {useGeolocation} from '#/geolocation' @@ -37,8 +37,8 @@ const queuedNuxs: { enabled?: (props: EnabledCheckProps) => boolean }[] = [ { - id: Nux.FindContactsAnnouncement, - enabled: isFindContactsAnnouncementEnabled, + id: Nux.LiveNowBetaDialog, + enabled: isLiveNowBetaDialogEnabled, }, ] @@ -186,9 +186,7 @@ function Inner({ return ( {/*For example, activeNux === Nux.NeueTypography && */} - {activeNux === Nux.FindContactsAnnouncement && ( - - )} + {activeNux === Nux.LiveNowBetaDialog && } ) } diff --git a/src/components/live/EditLiveDialog.tsx b/src/components/live/EditLiveDialog.tsx index 3304bceb6..ba5e2b5b7 100644 --- a/src/components/live/EditLiveDialog.tsx +++ b/src/components/live/EditLiveDialog.tsx @@ -18,7 +18,6 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import {Clock_Stroke2_Corner0_Rounded as ClockIcon} from '#/components/icons/Clock' -import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {LinkPreview} from './LinkPreview' @@ -172,26 +171,13 @@ function DialogInner({ {(liveLinkError || linkMetaError) && ( - - - - {liveLinkError ? ( - This is not a valid link - ) : ( - cleanError(linkMetaError) - )} - - + + {liveLinkError ? ( + This is not a valid link + ) : ( + cleanError(linkMetaError) + )} + )} diff --git a/src/components/live/GoLiveDialog.tsx b/src/components/live/GoLiveDialog.tsx index 44c604cde..9d866fd6a 100644 --- a/src/components/live/GoLiveDialog.tsx +++ b/src/components/live/GoLiveDialog.tsx @@ -6,13 +6,14 @@ import {useLingui} from '@lingui/react' import {cleanError} from '#/lib/strings/errors' import {definitelyUrl} from '#/lib/strings/url-helpers' import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useLiveNowConfig} from '#/state/service-config' import {useTickEveryMinute} from '#/state/shell' import {atoms as a, ios, native, platform, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' -import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' +import {getLiveServiceNames} from '#/components/live/utils' import {Loader} from '#/components/Loader' import * as ProfileCard from '#/components/ProfileCard' import * as Select from '#/components/Select' @@ -49,6 +50,10 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) { const [duration, setDuration] = useState(60) const moderationOpts = useModerationOpts() const tick = useTickEveryMinute() + const liveNowConfig = useLiveNowConfig() + const {formatted: allowedServices} = getLiveServiceNames( + liveNowConfig.allowedDomains, + ) const time = useCallback( (offset: number) => { @@ -139,27 +144,21 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) { /> - {(liveLinkError || linkMetaError) && ( - - - - {liveLinkError ? ( - This is not a valid link - ) : ( - cleanError(linkMetaError) - )} - - + {liveLinkError || linkMetaError ? ( + + {liveLinkError ? ( + This is not a valid link + ) : ( + cleanError(linkMetaError) + )} + + ) : ( + + + The following services are enabled for your account:{' '} + {allowedServices} + + )} diff --git a/src/components/live/queries.ts b/src/components/live/queries.ts index 19cb54ebf..7db944d06 100644 --- a/src/components/live/queries.ts +++ b/src/components/live/queries.ts @@ -18,10 +18,10 @@ import {useLiveNowConfig} from '#/state/service-config' import {useAgent, useSession} from '#/state/session' import * as Toast from '#/view/com/util/Toast' import {useDialogContext} from '#/components/Dialog' +import {getLiveServiceNames} from '#/components/live/utils' export function useLiveLinkMetaQuery(url: string | null) { const liveNowConfig = useLiveNowConfig() - const {currentAccount} = useSession() const {_} = useLingui() const agent = useAgent() @@ -30,13 +30,14 @@ export function useLiveLinkMetaQuery(url: string | null) { queryKey: ['link-meta', url], queryFn: async () => { if (!url) return undefined - const config = liveNowConfig.find(cfg => cfg.did === currentAccount?.did) - - if (!config) throw new Error(_(msg`You are not allowed to go live`)) - const urlp = new URL(url) - if (!config.domains.includes(urlp.hostname)) { - throw new Error(_(msg`${urlp.hostname} is not a valid URL`)) + if (!liveNowConfig.allowedDomains.has(urlp.hostname)) { + const {formatted} = getLiveServiceNames(liveNowConfig.allowedDomains) + throw new Error( + _( + msg`This service is not supported while the Live feature is in beta. Allowed services: ${formatted}.`, + ), + ) } return await getLinkMeta(agent, url) diff --git a/src/components/live/utils.ts b/src/components/live/utils.ts index 6b4267cb0..10b0ba8f9 100644 --- a/src/components/live/utils.ts +++ b/src/components/live/utils.ts @@ -35,3 +35,28 @@ export function useDebouncedValue(val: T, delayMs: number): T { return prev } + +const serviceUrlToNameMap: Record = { + 'twitch.tv': 'Twitch', + 'www.twitch.tv': 'Twitch', + 'youtube.com': 'YouTube', + 'www.youtube.com': 'YouTube', + 'youtu.be': 'YouTube', + 'nba.com': 'NBA', + 'www.nba.com': 'NBA', + 'nba.smart.link': 'nba.smart.link', + 'espn.com': 'ESPN', + 'www.espn.com': 'ESPN', + 'stream.place': 'Streamplace', + 'skylight.social': 'Skylight', +} + +export function getLiveServiceNames(domains: Set) { + const names = Array.from( + new Set(Array.from(domains.values()).map(d => serviceUrlToNameMap[d] || d)), + ) + return { + names, + formatted: names.join(', '), + } +} diff --git a/src/features/nuxs/components/Dot.tsx b/src/features/nuxs/components/Dot.tsx new file mode 100644 index 000000000..7eb82bf60 --- /dev/null +++ b/src/features/nuxs/components/Dot.tsx @@ -0,0 +1,31 @@ +import {View, type ViewStyle} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' + +/** + * The little blue dot used to nudge a user towards a certain feature. The dot + * is absolutely positioned, and is intended to be configured by passing in + * positional styles via `top`, `bottom`, `left`, and `right` props. + */ +export function Dot({ + top, + bottom, + left, + right, +}: Pick) { + const t = useTheme() + return ( + + + + ) +} diff --git a/src/features/nuxs/components/Gradient.tsx b/src/features/nuxs/components/Gradient.tsx new file mode 100644 index 000000000..5da8b4aca --- /dev/null +++ b/src/features/nuxs/components/Gradient.tsx @@ -0,0 +1,24 @@ +import {LinearGradient} from 'expo-linear-gradient' + +import {atoms as a, useTheme, utils, type ViewStyleProp} from '#/alf' + +/** + * A gradient overlay using the primary color at low opacity. This component is + * absolutely positioned and intended to be composed within other components, + * with optional styling allowed, such as adjusting border radius. + */ +export function Gradient({style}: ViewStyleProp) { + const t = useTheme() + return ( + + ) +} diff --git a/src/lib/actor-status.ts b/src/lib/actor-status.ts index 31e532eaa..7c18b5dc6 100644 --- a/src/lib/actor-status.ts +++ b/src/lib/actor-status.ts @@ -7,7 +7,7 @@ import { import {isAfter, parseISO} from 'date-fns' import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' -import {useLiveNowConfig} from '#/state/service-config' +import {type LiveNowConfig, useLiveNowConfig} from '#/state/service-config' import {useTickEveryMinute} from '#/state/shell' import type * as bsky from '#/types/bsky' @@ -20,7 +20,7 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) { tick! // revalidate every minute if (shadowed && 'status' in shadowed && shadowed.status) { - const isValid = validateStatus(shadowed.did, shadowed.status, config) + const isValid = validateStatus(shadowed.status, config) const isDisabled = shadowed.status.isDisabled || false const isActive = isStatusStillActive(shadowed.status.expiresAt) if (isValid && !isDisabled && isActive) { @@ -65,19 +65,14 @@ export function isStatusStillActive(timeStr: string | undefined) { } export function validateStatus( - did: string, status: AppBskyActorDefs.StatusView, - config: {did: string; domains: string[]}[], + config: LiveNowConfig, ) { if (status.status !== 'app.bsky.actor.status#live') return false - const sources = config.find(cfg => cfg.did === did) - if (!sources) { - return false - } try { if (AppBskyEmbedExternal.isView(status.embed)) { const url = new URL(status.embed.external.uri) - return sources.domains.includes(url.hostname) + return config.allowedDomains.has(url.hostname) } else { return false } diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index dcb330764..700ca12f8 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -8,6 +8,7 @@ export type Gate = | 'explore_show_suggested_feeds' | 'feed_reply_button_open_thread' | 'is_bsky_team_member' // special, do not remove + | 'live_now_beta' | 'old_postonboarding' | 'onboarding_add_video_feed' | 'onboarding_suggested_starterpacks' diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts index 6c03c936a..e23050c46 100644 --- a/src/state/queries/nuxs/definitions.ts +++ b/src/state/queries/nuxs/definitions.ts @@ -12,6 +12,8 @@ export enum Nux { BookmarksAnnouncement = 'BookmarksAnnouncement', FindContactsAnnouncement = 'FindContactsAnnouncement', FindContactsDismissibleBanner = 'FindContactsDismissibleBanner', + LiveNowBetaDialog = 'LiveNowBetaDialog', + LiveNowBetaNudge = 'LiveNowBetaNudge', /* * Blocking announcements. New IDs are required for each new announcement. @@ -62,6 +64,14 @@ export type AppNux = BaseNux< id: Nux.FindContactsDismissibleBanner data: undefined } + | { + id: Nux.LiveNowBetaDialog + data: undefined + } + | { + id: Nux.LiveNowBetaNudge + data: undefined + } > export const NuxSchemas: Record | undefined> = { @@ -75,4 +85,6 @@ export const NuxSchemas: Record | undefined> = { [Nux.BookmarksAnnouncement]: undefined, [Nux.FindContactsAnnouncement]: undefined, [Nux.FindContactsDismissibleBanner]: undefined, + [Nux.LiveNowBetaDialog]: undefined, + [Nux.LiveNowBetaNudge]: undefined, } diff --git a/src/state/service-config.tsx b/src/state/service-config.tsx index 242022b60..a04cbb51e 100644 --- a/src/state/service-config.tsx +++ b/src/state/service-config.tsx @@ -1,7 +1,10 @@ import {createContext, useContext, useMemo} from 'react' +import {useGate} from '#/lib/statsig/statsig' import {useLanguagePrefs} from '#/state/preferences/languages' import {useServiceConfigQuery} from '#/state/queries/service-config' +import {useSession} from '#/state/session' +import {IS_DEV} from '#/env' import {device} from '#/storage' type TrendingContext = { @@ -18,7 +21,7 @@ const TrendingContext = createContext({ }) TrendingContext.displayName = 'TrendingContext' -const LiveNowContext = createContext(null) +const LiveNowContext = createContext([]) LiveNowContext.displayName = 'LiveNowContext' const CheckEmailConfirmedContext = createContext(null) @@ -82,19 +85,28 @@ export function useTrendingConfig() { return useContext(TrendingContext) } -export function useLiveNowConfig() { +const DEFAULT_LIVE_ALLOWED_DOMAINS = ['twitch.tv', 'www.twitch.tv'] +export type LiveNowConfig = { + allowedDomains: Set +} +export function useLiveNowConfig(): LiveNowConfig { const ctx = useContext(LiveNowContext) - if (!ctx) { - throw new Error( - 'useLiveNowConfig must be used within a ServiceConfigManager', - ) + const canGoLive = useCanGoLive() + const {currentAccount} = useSession() + if (!currentAccount?.did || !canGoLive) return {allowedDomains: new Set()} + const vip = ctx.find(live => live.did === currentAccount.did) + return { + allowedDomains: new Set( + DEFAULT_LIVE_ALLOWED_DOMAINS.concat(vip ? vip.domains : []), + ), } - return ctx } -export function useCanGoLive(did?: string) { - const config = useLiveNowConfig() - return !!config.find(cfg => cfg.did === did) +export function useCanGoLive() { + const gate = useGate() + const {hasSession} = useSession() + if (!hasSession) return false + return IS_DEV ? true : gate('live_now_beta') } export function useCheckEmailConfirmed() { diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 9e6fcdccc..97e5e9731 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -954,7 +954,7 @@ let PostFeed = ({ const actor = post.author if ( actor.status && - validateStatus(actor.did, actor.status, liveNowConfig) && + validateStatus(actor.status, liveNowConfig) && isStatusStillActive(actor.status.expiresAt) ) { if (!seenActorWithStatusRef.current.has(actor.did)) { diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index 8cf6d2c96..f9c1bac00 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -15,6 +15,7 @@ import {logger} from '#/logger' import {isWeb} from '#/platform/detection' import {type Shadow} from '#/state/cache/types' import {useModalControls} from '#/state/modals' +import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' import { RQKEY as profileQueryKey, useProfileBlockMutationQueue, @@ -25,6 +26,7 @@ import {useCanGoLive} from '#/state/service-config' import {useSession} from '#/state/session' import {EventStopper} from '#/view/com/util/EventStopper' import * as Toast from '#/view/com/util/Toast' +import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog' @@ -59,6 +61,8 @@ import * as Prompt from '#/components/Prompt' import {useFullVerificationState} from '#/components/verification' import {VerificationCreatePrompt} from '#/components/verification/VerificationCreatePrompt' import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt' +import {Dot} from '#/features/nuxs/components/Dot' +import {Gradient} from '#/features/nuxs/components/Gradient' import {useDevMode} from '#/storage/hooks/dev-mode' let ProfileMenu = ({ @@ -66,6 +70,7 @@ let ProfileMenu = ({ }: { profile: Shadow }): React.ReactNode => { + const t = useTheme() const {_} = useLingui() const {currentAccount, hasSession} = useSession() const {openModal} = useModalControls() @@ -79,8 +84,15 @@ let ProfileMenu = ({ const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked const [devModeEnabled] = useDevMode() const verification = useFullVerificationState({profile}) - const canGoLive = useCanGoLive(currentAccount?.did) + const canGoLive = useCanGoLive() const status = useActorStatus(profile) + const statusNudge = useNux(Nux.LiveNowBetaNudge) + const statusNudgeActive = + isSelf && + canGoLive && + statusNudge.status === 'ready' && + !statusNudge.nux?.completed + const {mutate: saveNux} = useSaveNux() const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) @@ -229,17 +241,22 @@ let ProfileMenu = ({ {({props}) => { return ( - + <> + + + {statusNudgeActive && } + ) }} @@ -337,11 +354,19 @@ let ProfileMenu = ({ ? _(msg`Edit live status`) : _(msg`Go live`) } - onPress={ - status.isDisabled - ? goLiveDisabledDialogControl.open - : goLiveDialogControl.open - }> + onPress={() => { + if (status.isDisabled) { + goLiveDisabledDialogControl.open() + } else { + goLiveDialogControl.open() + } + saveNux({ + id: Nux.LiveNowBetaNudge, + data: undefined, + completed: true, + }) + }}> + {statusNudgeActive && } {status.isDisabled ? ( Go live (disabled) @@ -351,7 +376,26 @@ let ProfileMenu = ({ Go live )} - + {statusNudgeActive && ( + + New + + )} + t.palette.primary_500 + : undefined + } + /> )} {verification.viewer.role === 'verifier' && -- 2.51.2