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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 UlXSte|Afc?{~jGU@C4uh0AJQb @@ -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 242449bf49..339b26350c 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 e9ee082f4c..a5fce102ba 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 0000000000..b081322c44 --- /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 2ab319fafd..eadadeb8eb 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 3304bceb61..ba5e2b5b7a 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 44c604cde7..9d866fd6a5 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 19cb54ebf2..7db944d06d 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 6b4267cb01..10b0ba8f90 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 0000000000..7eb82bf60f --- /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 0000000000..5da8b4aca3 --- /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 31e532eaa0..7c18b5dc66 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 dcb330764c..700ca12f83 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 6c03c936a6..e23050c465 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 242022b60d..a04cbb51ee 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 9e6fcdcccb..97e5e9731e 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 8cf6d2c968..f9c1bac007 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' &&