From d42e27f63605eb3af2966b0a544f118c7b25880f Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 8 Jan 2026 16:57:58 -0600 Subject: [PATCH] Update config and gates --- src/components/live/GoLiveDialog.tsx | 28 +++++--------------- src/components/live/queries.ts | 33 +++++++++++++++++++----- src/lib/actor-status.ts | 13 +++------- src/lib/statsig/gates.ts | 1 + src/state/service-config.tsx | 38 ++++++++++++++++++++-------- 5 files changed, 66 insertions(+), 47 deletions(-) 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() {