diff --git a/assets/images/live_now_beta.webp b/assets/images/live_now_beta.webp
new file mode 100644
index 0000000000..7b5b074835
Binary files /dev/null and b/assets/images/live_now_beta.webp differ
diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx
index ee2c0f76c7..b6d47fd3b4 100644
--- a/src/components/Dialog/index.tsx
+++ b/src/components/Dialog/index.tsx
@@ -359,7 +359,13 @@ export function FlatListFooter({children}: {children: React.ReactNode}) {
)
}
-export function Handle({difference = false}: {difference?: boolean}) {
+export function Handle({
+ difference = false,
+ fill,
+}: {
+ difference?: boolean
+ fill?: string
+}) {
const t = useTheme()
const {_} = useLingui()
const {screenReaderEnabled} = useA11y()
@@ -390,7 +396,7 @@ export function Handle({difference = false}: {difference?: boolean}) {
opacity: 0.75,
}
: {
- backgroundColor: t.palette.contrast_975,
+ backgroundColor: fill || t.palette.contrast_975,
opacity: 0.5,
},
]}
diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx
index ab911eb05a..5ceaf281c3 100644
--- a/src/components/Menu/index.tsx
+++ b/src/components/Menu/index.tsx
@@ -167,6 +167,7 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
a.gap_sm,
a.px_md,
a.rounded_md,
+ a.overflow_hidden,
a.border,
t.atoms.bg_contrast_25,
t.atoms.border_contrast_low,
@@ -193,7 +194,6 @@ export function ItemText({children, style}: ItemTextProps) {
a.text_md,
a.font_semi_bold,
t.atoms.text_contrast_high,
- {paddingTop: 3},
style,
disabled && t.atoms.text_contrast_low,
]}>
@@ -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' &&