diff --git a/src/components/live/GoLiveDialog.tsx b/src/components/live/GoLiveDialog.tsx
index 44c604cde7..554f562161 100644
--- a/src/components/live/GoLiveDialog.tsx
+++ b/src/components/live/GoLiveDialog.tsx
@@ -12,7 +12,6 @@ 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 {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard'
import * as Select from '#/components/Select'
@@ -140,26 +139,13 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
{(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/queries.ts b/src/components/live/queries.ts
index 19cb54ebf2..3d58cbb3aa 100644
--- a/src/components/live/queries.ts
+++ b/src/components/live/queries.ts
@@ -19,9 +19,23 @@ import {useAgent, useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {useDialogContext} from '#/components/Dialog'
+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 useLiveLinkMetaQuery(url: string | null) {
const liveNowConfig = useLiveNowConfig()
- const {currentAccount} = useSession()
const {_} = useLingui()
const agent = useAgent()
@@ -30,13 +44,18 @@ 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.includes(urlp.hostname)) {
+ const services = Array.from(
+ new Set(
+ liveNowConfig.allowedDomains.map(d => serviceUrlToNameMap[d] || d),
+ ),
+ )
+ throw new Error(
+ _(
+ msg`This service is not supported while the Live feature is in beta. Allowed services: ${services.join(', ')}.`,
+ ),
+ )
}
return await getLinkMeta(agent, url)
diff --git a/src/lib/actor-status.ts b/src/lib/actor-status.ts
index 31e532eaa0..39ab011875 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.includes(url.hostname)
} else {
return false
}
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts
index 357548d3b1..bec44521ae 100644
--- a/src/lib/statsig/gates.ts
+++ b/src/lib/statsig/gates.ts
@@ -7,6 +7,7 @@ export type Gate =
| 'disable_settings_find_contacts'
| 'explore_show_suggested_feeds'
| 'feed_reply_button_open_thread'
+ | 'live_now_beta'
| 'old_postonboarding'
| 'onboarding_add_video_feed'
| 'onboarding_suggested_starterpacks'
diff --git a/src/state/service-config.tsx b/src/state/service-config.tsx
index 242022b60d..4a9367b45d 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,34 @@ export function useTrendingConfig() {
return useContext(TrendingContext)
}
-export function useLiveNowConfig() {
+const DEFAULT_LIVE_ALLOWED_DOMAINS = ['twitch.tv', 'www.twitch.tv']
+export type LiveNowConfig = {
+ allowedDomains: string[]
+}
+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 (!canGoLive) return {allowedDomains: []}
+ if (!currentAccount?.did) return {allowedDomains: []}
+ const vip = ctx.find(live => live.did === currentAccount.did)
+ if (vip) {
+ return {
+ allowedDomains: vip.domains.concat(DEFAULT_LIVE_ALLOWED_DOMAINS),
+ }
+ }
+ return {
+ allowedDomains: DEFAULT_LIVE_ALLOWED_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
+ const isInBeta = IS_DEV ? true : gate('live_now_beta')
+ if (!isInBeta) return false
+ return true
}
export function useCheckEmailConfirmed() {