From cc2255cc4946fa95ed49cf011d5f9e1105e4ea63 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 16 Feb 2026 11:44:22 -0600 Subject: [PATCH] [APP-1776] Refactor Live Now config, reorg files (#9871) --- .env.example | 3 + src/App.native.tsx | 71 ++++---- src/App.web.tsx | 57 ++++--- src/components/AccountList.tsx | 2 +- src/components/ProfileCard.tsx | 2 +- src/components/ProfileHoverCard/index.web.tsx | 4 +- src/env/common.ts | 10 ++ .../liveNow/components}/EditLiveDialog.tsx | 7 +- .../liveNow/components}/GoLiveDialog.tsx | 15 +- .../components}/GoLiveDisabledDialog.tsx | 0 .../liveNow/components}/LinkPreview.tsx | 0 .../liveNow/components}/LiveIndicator.tsx | 0 .../liveNow/components}/LiveStatusDialog.tsx | 6 +- .../queries.ts => features/liveNow/index.tsx} | 154 +++++++++++++++++- .../live => features/liveNow}/utils.ts | 48 +++--- src/lib/actor-status.ts | 92 ----------- src/lib/async/retry.ts | 3 +- src/lib/hooks/useDebouncedValue.ts | 16 ++ .../components/ThreadItemAnchor.tsx | 2 +- .../PostThread/components/ThreadItemPost.tsx | 2 +- .../Profile/Header/ProfileHeaderStandard.tsx | 2 +- src/screens/Profile/Header/Shell.tsx | 8 +- src/screens/Settings/Settings.tsx | 2 +- src/state/appConfig.tsx | 87 ++++++++++ src/state/queries/handle-availability.ts | 2 +- src/state/service-config.tsx | 70 +------- src/view/com/posts/PostFeed.tsx | 7 +- src/view/com/posts/PostFeedItem.tsx | 2 +- src/view/com/profile/ProfileMenu.tsx | 11 +- src/view/com/util/PostMeta.tsx | 2 +- src/view/com/util/UserAvatar.tsx | 6 +- src/view/shell/Drawer.tsx | 2 +- src/view/shell/bottom-bar/BottomBar.tsx | 2 +- src/view/shell/desktop/LeftNav.tsx | 2 +- 34 files changed, 414 insertions(+), 285 deletions(-) rename src/{components/live => features/liveNow/components}/EditLiveDialog.tsx (97%) rename src/{components/live => features/liveNow/components}/GoLiveDialog.tsx (97%) rename src/{components/live => features/liveNow/components}/GoLiveDisabledDialog.tsx (100%) rename src/{components/live => features/liveNow/components}/LinkPreview.tsx (100%) rename src/{components/live => features/liveNow/components}/LiveIndicator.tsx (100%) rename src/{components/live => features/liveNow/components}/LiveStatusDialog.tsx (97%) rename src/{components/live/queries.ts => features/liveNow/index.tsx} (55%) rename src/{components/live => features/liveNow}/utils.ts (61%) delete mode 100644 src/lib/actor-status.ts create mode 100644 src/lib/hooks/useDebouncedValue.ts create mode 100644 src/state/appConfig.tsx diff --git a/.env.example b/.env.example index df3456410b..93fa4c3124 100644 --- a/.env.example +++ b/.env.example @@ -46,3 +46,6 @@ GEOLOCATION_DEV_URL= # live-events web worker URL LIVE_EVENTS_DEV_URL= + +# app-config web worker URL +APP_CONFIG_DEV_URL= diff --git a/src/App.native.tsx b/src/App.native.tsx index 5de9f82b7b..7dc16ec058 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -22,6 +22,10 @@ import {ThemeProvider} from '#/lib/ThemeContext' import I18nProvider from '#/locale/i18nProvider' import {logger} from '#/logger' import {Provider as A11yProvider} from '#/state/a11y' +import { + prefetchAppConfig, + Provider as AppConfigProvider, +} from '#/state/appConfig' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as EmailVerificationProvider} from '#/state/email-verification' @@ -103,6 +107,7 @@ if (IS_ANDROID) { Geo.resolve() prefetchAgeAssuranceConfig() prefetchLiveEvents() +prefetchAppConfig() function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -228,38 +233,40 @@ function App() { */ return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/src/App.web.tsx b/src/App.web.tsx index abf188ea28..701167f392 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -13,6 +13,10 @@ import {ThemeProvider} from '#/lib/ThemeContext' import I18nProvider from '#/locale/i18nProvider' import {logger} from '#/logger' import {Provider as A11yProvider} from '#/state/a11y' +import { + prefetchAppConfig, + Provider as AppConfigProvider, +} from '#/state/appConfig' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as EmailVerificationProvider} from '#/state/email-verification' @@ -79,6 +83,7 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom Geo.resolve() prefetchAgeAssuranceConfig() prefetchLiveEvents() +prefetchAppConfig() function InnerApp() { const [isReady, setIsReady] = useState(false) @@ -202,31 +207,33 @@ function App() { */ return ( - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/src/components/AccountList.tsx b/src/components/AccountList.tsx index 70fc29d5b6..2fd56eabb8 100644 --- a/src/components/AccountList.tsx +++ b/src/components/AccountList.tsx @@ -4,7 +4,6 @@ import {type AppBskyActorDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useActorStatus} from '#/lib/actor-status' import {isJwtExpired} from '#/lib/jwt' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' @@ -19,6 +18,7 @@ import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/ import {Text} from '#/components/Typography' import {useSimpleVerificationState} from '#/components/verification' import {VerificationCheck} from '#/components/verification/VerificationCheck' +import {useActorStatus} from '#/features/liveNow' export function AccountList({ onSelectAccount, diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index b932517170..e184ebf514 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -14,7 +14,6 @@ import { import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useActorStatus} from '#/lib/actor-status' import {getModerationCauseKey} from '#/lib/moderation' import {forceLTR} from '#/lib/strings/bidi' import {NON_BREAKING_SPACE} from '#/lib/strings/constants' @@ -47,6 +46,7 @@ import {Text} from '#/components/Typography' import {useSimpleVerificationState} from '#/components/verification' import {VerificationCheck} from '#/components/verification/VerificationCheck' import {type Metrics} from '#/analytics' +import {useActorStatus} from '#/features/liveNow' import type * as bsky from '#/types/bsky' export function Default({ diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx index 12246387fd..bda2380ece 100644 --- a/src/components/ProfileHoverCard/index.web.tsx +++ b/src/components/ProfileHoverCard/index.web.tsx @@ -10,7 +10,6 @@ import {msg, plural} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {useActorStatus} from '#/lib/actor-status' import {getModerationCauseKey} from '#/lib/moderation' import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' @@ -34,7 +33,6 @@ import { shouldShowKnownFollowers, } from '#/components/KnownFollowers' import {InlineLinkText, Link} from '#/components/Link' -import {LiveStatus} from '#/components/live/LiveStatusDialog' import {Loader} from '#/components/Loader' import * as Pills from '#/components/Pills' import {Portal} from '#/components/Portal' @@ -43,6 +41,8 @@ import {Text} from '#/components/Typography' import {useSimpleVerificationState} from '#/components/verification' import {VerificationCheck} from '#/components/verification/VerificationCheck' import {IS_WEB_TOUCH_DEVICE} from '#/env' +import {useActorStatus} from '#/features/liveNow' +import {LiveStatus} from '#/features/liveNow/components/LiveStatusDialog' import {type ProfileHoverCardProps} from './types' const floatingMiddlewares = [ diff --git a/src/env/common.ts b/src/env/common.ts index 5070b35968..5d8d89aace 100644 --- a/src/env/common.ts +++ b/src/env/common.ts @@ -141,3 +141,13 @@ export const LIVE_EVENTS_PROD_URL = `https://live-events.workers.bsky.app` export const LIVE_EVENTS_URL = IS_DEV ? (LIVE_EVENTS_DEV_URL ?? LIVE_EVENTS_PROD_URL) : LIVE_EVENTS_PROD_URL + +/** + * URLs for the app-config web worker. Can be a + * locally running server, see `env.example` for more. + */ +export const APP_CONFIG_DEV_URL = process.env.APP_CONFIG_DEV_URL +export const APP_CONFIG_PROD_URL = `https://app-config.workers.bsky.app` +export const APP_CONFIG_URL = IS_DEV + ? (APP_CONFIG_DEV_URL ?? APP_CONFIG_PROD_URL) + : APP_CONFIG_PROD_URL diff --git a/src/components/live/EditLiveDialog.tsx b/src/features/liveNow/components/EditLiveDialog.tsx similarity index 97% rename from src/components/live/EditLiveDialog.tsx rename to src/features/liveNow/components/EditLiveDialog.tsx index 585e95bba1..edecc13fce 100644 --- a/src/components/live/EditLiveDialog.tsx +++ b/src/features/liveNow/components/EditLiveDialog.tsx @@ -9,6 +9,7 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {differenceInMinutes} from 'date-fns' +import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue' import {cleanError} from '#/lib/strings/errors' import {definitelyUrl} from '#/lib/strings/url-helpers' import {useTickEveryMinute} from '#/state/shell' @@ -20,13 +21,13 @@ import * as TextField from '#/components/forms/TextField' import {Clock_Stroke2_Corner0_Rounded as ClockIcon} from '#/components/icons/Clock' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' -import {LinkPreview} from './LinkPreview' import { + displayDuration, useLiveLinkMetaQuery, useRemoveLiveStatusMutation, useUpsertLiveStatusMutation, -} from './queries' -import {displayDuration, useDebouncedValue} from './utils' +} from '#/features/liveNow' +import {LinkPreview} from '#/features/liveNow/components/LinkPreview' export function EditLiveDialog({ control, diff --git a/src/components/live/GoLiveDialog.tsx b/src/features/liveNow/components/GoLiveDialog.tsx similarity index 97% rename from src/components/live/GoLiveDialog.tsx rename to src/features/liveNow/components/GoLiveDialog.tsx index ac51e87527..3b373c7e00 100644 --- a/src/components/live/GoLiveDialog.tsx +++ b/src/features/liveNow/components/GoLiveDialog.tsx @@ -3,28 +3,29 @@ import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue' 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 { - displayDuration, - getLiveServiceNames, - useDebouncedValue, -} from '#/components/live/utils' import {Loader} from '#/components/Loader' import * as ProfileCard from '#/components/ProfileCard' import * as Select from '#/components/Select' import {Text} from '#/components/Typography' +import { + displayDuration, + getLiveServiceNames, + useLiveLinkMetaQuery, + useLiveNowConfig, + useUpsertLiveStatusMutation, +} from '#/features/liveNow' import type * as bsky from '#/types/bsky' import {LinkPreview} from './LinkPreview' -import {useLiveLinkMetaQuery, useUpsertLiveStatusMutation} from './queries' export function GoLiveDialog({ control, diff --git a/src/components/live/GoLiveDisabledDialog.tsx b/src/features/liveNow/components/GoLiveDisabledDialog.tsx similarity index 100% rename from src/components/live/GoLiveDisabledDialog.tsx rename to src/features/liveNow/components/GoLiveDisabledDialog.tsx diff --git a/src/components/live/LinkPreview.tsx b/src/features/liveNow/components/LinkPreview.tsx similarity index 100% rename from src/components/live/LinkPreview.tsx rename to src/features/liveNow/components/LinkPreview.tsx diff --git a/src/components/live/LiveIndicator.tsx b/src/features/liveNow/components/LiveIndicator.tsx similarity index 100% rename from src/components/live/LiveIndicator.tsx rename to src/features/liveNow/components/LiveIndicator.tsx diff --git a/src/components/live/LiveStatusDialog.tsx b/src/features/liveNow/components/LiveStatusDialog.tsx similarity index 97% rename from src/components/live/LiveStatusDialog.tsx rename to src/features/liveNow/components/LiveStatusDialog.tsx index 122ef5f1b9..a003673a4f 100644 --- a/src/components/live/LiveStatusDialog.tsx +++ b/src/features/liveNow/components/LiveStatusDialog.tsx @@ -17,15 +17,15 @@ import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo' +import {Globe_Stroke2_Corner0_Rounded} from '#/components/icons/Globe' +import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRightIcon} from '#/components/icons/SquareArrowTopRight' import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' import {useGlobalReportDialogControl} from '#/components/moderation/ReportDialog' import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {LiveIndicator} from '#/features/liveNow/components/LiveIndicator' import type * as bsky from '#/types/bsky' -import {Globe_Stroke2_Corner0_Rounded} from '../icons/Globe' -import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRightIcon} from '../icons/SquareArrowTopRight' -import {LiveIndicator} from './LiveIndicator' export function LiveStatusDialog({ control, diff --git a/src/components/live/queries.ts b/src/features/liveNow/index.tsx similarity index 55% rename from src/components/live/queries.ts rename to src/features/liveNow/index.tsx index 3c2534872c..8372a357d7 100644 --- a/src/components/live/queries.ts +++ b/src/features/liveNow/index.tsx @@ -1,24 +1,166 @@ +import {useMemo} from 'react' import { type $Typed, + type AppBskyActorDefs, type AppBskyActorStatus, - type AppBskyEmbedExternal, + AppBskyEmbedExternal, + AtUri, ComAtprotoRepoPutRecord, } from '@atproto/api' import {retry} from '@atproto/common-web' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' +import {isAfter, parseISO} from 'date-fns' import {uploadBlob} from '#/lib/api' import {imageToThumb} from '#/lib/api/resolve' import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta' -import {updateProfileShadow} from '#/state/cache/profile-shadow' -import {useLiveNowConfig} from '#/state/service-config' +import {useAppConfig} from '#/state/appConfig' +import { + updateProfileShadow, + useMaybeProfileShadow, +} from '#/state/cache/profile-shadow' import {useAgent, useSession} from '#/state/session' +import {useTickEveryMinute} from '#/state/shell' import * as Toast from '#/view/com/util/Toast' import {useDialogContext} from '#/components/Dialog' -import {getLiveServiceNames} from '#/components/live/utils' import {useAnalytics} from '#/analytics' +import {getLiveNowHost, getLiveServiceNames} from '#/features/liveNow/utils' +import type * as bsky from '#/types/bsky' + +export * from '#/features/liveNow/utils' + +export const DEFAULT_ALLOWED_DOMAINS = [ + 'twitch.tv', + 'stream.place', + 'bluecast.app', +] + +export type LiveNowConfig = { + canGoLive: boolean + currentAccountAllowedHosts: Set + defaultAllowedHosts: Set + allowedHostsExceptionsByDid: Map> +} + +export function useLiveNowConfig(): LiveNowConfig { + const ax = useAnalytics() + const {liveNow} = useAppConfig() + const {currentAccount} = useSession() + + return useMemo(() => { + const disabled = ax.features.enabled(ax.features.LiveNowBetaDisable) + + const defaultAllowedHosts = new Set( + DEFAULT_ALLOWED_DOMAINS.concat(liveNow.allow), + ) + const allowedHostsExceptionsByDid = new Map>() + for (const ex of liveNow.exceptions) { + allowedHostsExceptionsByDid.set( + ex.did, + new Set(DEFAULT_ALLOWED_DOMAINS.concat(ex.allow)), + ) + } + + if (!currentAccount?.did || disabled) { + return { + canGoLive: false, + currentAccountAllowedHosts: new Set(), + defaultAllowedHosts, + allowedHostsExceptionsByDid, + } + } + + return { + canGoLive: true, + currentAccountAllowedHosts: + allowedHostsExceptionsByDid.get(currentAccount.did) ?? + defaultAllowedHosts, + defaultAllowedHosts, + allowedHostsExceptionsByDid, + } + }, [ax, liveNow, currentAccount]) +} + +export function useActorStatus(actor?: bsky.profile.AnyProfileView) { + const shadowed = useMaybeProfileShadow(actor) + const tick = useTickEveryMinute() + const config = useLiveNowConfig() + + return useMemo(() => { + void tick // revalidate every minute + + if (shadowed && 'status' in shadowed && shadowed.status) { + const isValid = isStatusValidForViewers(shadowed.status, config) + const isDisabled = shadowed.status.isDisabled || false + const isActive = isStatusStillActive(shadowed.status.expiresAt) + if (isValid && !isDisabled && isActive) { + return { + uri: shadowed.status.uri, + cid: shadowed.status.cid, + isDisabled: false, + isActive: true, + status: 'app.bsky.actor.status#live', + embed: shadowed.status.embed as $Typed, // temp_isStatusValid asserts this + expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this + record: shadowed.status.record, + } satisfies AppBskyActorDefs.StatusView + } + return { + uri: shadowed.status.uri, + cid: shadowed.status.cid, + isDisabled, + isActive: false, + status: 'app.bsky.actor.status#live', + embed: shadowed.status.embed as $Typed, // temp_isStatusValid asserts this + expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this + record: shadowed.status.record, + } satisfies AppBskyActorDefs.StatusView + } else { + return { + status: '', + isDisabled: false, + isActive: false, + record: {}, + } satisfies AppBskyActorDefs.StatusView + } + }, [shadowed, config, tick]) +} + +export function isStatusStillActive(timeStr: string | undefined) { + if (!timeStr) return false + const now = new Date() + const expiry = parseISO(timeStr) + + return isAfter(expiry, now) +} + +/** + * Validates whether the live status is valid for display in the app. Does NOT + * validate if the status is valid for the acting user e.g. as they go live. + */ +export function isStatusValidForViewers( + status: AppBskyActorDefs.StatusView, + config: LiveNowConfig, +) { + if (status.status !== 'app.bsky.actor.status#live') return false + if (!status.uri) return false // should not happen, just backwards compat + try { + const {host: liveDid} = new AtUri(status.uri) + if (AppBskyEmbedExternal.isView(status.embed)) { + const host = getLiveNowHost(status.embed.external.uri) + const exception = config.allowedHostsExceptionsByDid.get(liveDid) + const isValidException = exception ? exception.has(host) : false + const isValidForAnyone = config.defaultAllowedHosts.has(host) + return isValidException || isValidForAnyone + } else { + return false + } + } catch { + return false + } +} export function useLiveLinkMetaQuery(url: string | null) { const liveNowConfig = useLiveNowConfig() @@ -30,8 +172,8 @@ export function useLiveLinkMetaQuery(url: string | null) { queryKey: ['link-meta', url], queryFn: async () => { if (!url) return undefined - const urlp = new URL(url) - if (!liveNowConfig.currentAccountAllowedHosts.has(urlp.hostname)) { + const host = getLiveNowHost(url) + if (!liveNowConfig.currentAccountAllowedHosts.has(host)) { const {formatted} = getLiveServiceNames( liveNowConfig.currentAccountAllowedHosts, ) diff --git a/src/components/live/utils.ts b/src/features/liveNow/utils.ts similarity index 61% rename from src/components/live/utils.ts rename to src/features/liveNow/utils.ts index 2172ed3db4..6b2b645eac 100644 --- a/src/components/live/utils.ts +++ b/src/features/liveNow/utils.ts @@ -1,6 +1,6 @@ -import {useEffect, useState} from 'react' import {type I18n} from '@lingui/core' import {plural} from '@lingui/macro' +import psl from 'psl' export function displayDuration(i18n: I18n, durationInMinutes: number) { const roundedDurationInMinutes = Math.round(durationInMinutes) @@ -24,41 +24,49 @@ export function displayDuration(i18n: I18n, durationInMinutes: number) { : minutesString } -// Trailing debounce -export function useDebouncedValue(val: T, delayMs: number): T { - const [prev, setPrev] = useState(val) - - useEffect(() => { - const timeout = setTimeout(() => setPrev(val), delayMs) - return () => clearTimeout(timeout) - }, [val, delayMs]) - - 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', 'bluecast.app': 'Bluecast', - 'www.bluecast.app': 'Bluecast', } export function getLiveServiceNames(domains: Set) { const names = Array.from( - new Set(Array.from(domains.values()).map(d => serviceUrlToNameMap[d] || d)), + new Set( + Array.from(domains.values()) + .map(d => sanitizeLiveNowHost(d)) + .map(d => serviceUrlToNameMap[d] || d), + ), ) return { names, formatted: names.join(', '), } } + +export function sanitizeLiveNowHost(hostname: string) { + // special case this one + if (hostname === 'nba.smart.link') { + return hostname + } + const parsed = psl.parse(hostname) + if (parsed.error || !parsed.listed || !parsed.domain) { + // fall back to dumb version + return hostname.replace(/^www\./, '') + } + return parsed.domain +} + +/** + * Extracts the apex domain from a given URL, for use when matching allowed + * Live Now hosts. + */ +export function getLiveNowHost(url: string) { + const {hostname} = new URL(url) + return sanitizeLiveNowHost(hostname) +} diff --git a/src/lib/actor-status.ts b/src/lib/actor-status.ts deleted file mode 100644 index 89a3a163de..0000000000 --- a/src/lib/actor-status.ts +++ /dev/null @@ -1,92 +0,0 @@ -import {useMemo} from 'react' -import { - type $Typed, - type AppBskyActorDefs, - AppBskyEmbedExternal, - AtUri, -} from '@atproto/api' -import {isAfter, parseISO} from 'date-fns' - -import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' -import {type LiveNowConfig, useLiveNowConfig} from '#/state/service-config' -import {useTickEveryMinute} from '#/state/shell' -import type * as bsky from '#/types/bsky' - -export function useActorStatus(actor?: bsky.profile.AnyProfileView) { - const shadowed = useMaybeProfileShadow(actor) - const tick = useTickEveryMinute() - const config = useLiveNowConfig() - - return useMemo(() => { - void tick // revalidate every minute - - if (shadowed && 'status' in shadowed && shadowed.status) { - const isValid = isStatusValidForViewers(shadowed.status, config) - const isDisabled = shadowed.status.isDisabled || false - const isActive = isStatusStillActive(shadowed.status.expiresAt) - if (isValid && !isDisabled && isActive) { - return { - uri: shadowed.status.uri, - cid: shadowed.status.cid, - isDisabled: false, - isActive: true, - status: 'app.bsky.actor.status#live', - embed: shadowed.status.embed as $Typed, // temp_isStatusValid asserts this - expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this - record: shadowed.status.record, - } satisfies AppBskyActorDefs.StatusView - } - return { - uri: shadowed.status.uri, - cid: shadowed.status.cid, - isDisabled, - isActive: false, - status: 'app.bsky.actor.status#live', - embed: shadowed.status.embed as $Typed, // temp_isStatusValid asserts this - expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this - record: shadowed.status.record, - } satisfies AppBskyActorDefs.StatusView - } else { - return { - status: '', - isDisabled: false, - isActive: false, - record: {}, - } satisfies AppBskyActorDefs.StatusView - } - }, [shadowed, config, tick]) -} - -export function isStatusStillActive(timeStr: string | undefined) { - if (!timeStr) return false - const now = new Date() - const expiry = parseISO(timeStr) - - return isAfter(expiry, now) -} - -/** - * Validates whether the live status is valid for display in the app. Does NOT - * validate if the status is valid for the acting user e.g. as they go live. - */ -export function isStatusValidForViewers( - status: AppBskyActorDefs.StatusView, - config: LiveNowConfig, -) { - if (status.status !== 'app.bsky.actor.status#live') return false - if (!status.uri) return false // should not happen, just backwards compat - try { - const {host: liveDid} = new AtUri(status.uri) - if (AppBskyEmbedExternal.isView(status.embed)) { - const url = new URL(status.embed.external.uri) - const exception = config.allowedHostsExceptionsByDid.get(liveDid) - const isValidException = exception ? exception.has(url.hostname) : false - const isValidForAnyone = config.defaultAllowedHosts.has(url.hostname) - return isValidException || isValidForAnyone - } else { - return false - } - } catch { - return false - } -} diff --git a/src/lib/async/retry.ts b/src/lib/async/retry.ts index 8a1729091d..479a1cdc12 100644 --- a/src/lib/async/retry.ts +++ b/src/lib/async/retry.ts @@ -29,6 +29,7 @@ export async function retry

( export async function networkRetry

( retries: number, fn: () => Promise

, + delay?: number, ): Promise

{ - return retry(retries, isNetworkError, fn) + return retry(retries, isNetworkError, fn, delay) } diff --git a/src/lib/hooks/useDebouncedValue.ts b/src/lib/hooks/useDebouncedValue.ts new file mode 100644 index 0000000000..c8e083b9b3 --- /dev/null +++ b/src/lib/hooks/useDebouncedValue.ts @@ -0,0 +1,16 @@ +import {useEffect, useState} from 'react' + +/** + * Returns a debounced version of the input value that only updates after the + * specified delay has passed without any changes to the input value. + */ +export function useDebouncedValue(val: T, delayMs: number): T { + const [prev, setPrev] = useState(val) + + useEffect(() => { + const timeout = setTimeout(() => setPrev(val), delayMs) + return () => clearTimeout(timeout) + }, [val, delayMs]) + + return prev +} diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index e475b42de1..27fcaf0fa8 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -10,7 +10,6 @@ import { import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useActorStatus} from '#/lib/actor-status' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useTranslate} from '#/lib/hooks/useTranslate' import {makeProfileLink} from '#/lib/routes/links' @@ -60,6 +59,7 @@ import {Text} from '#/components/Typography' import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' import {WhoCanReply} from '#/components/WhoCanReply' import {useAnalytics} from '#/analytics' +import {useActorStatus} from '#/features/liveNow' import * as bsky from '#/types/bsky' export function ThreadItemAnchor({ diff --git a/src/screens/PostThread/components/ThreadItemPost.tsx b/src/screens/PostThread/components/ThreadItemPost.tsx index ea0d5631bb..4fe78785b8 100644 --- a/src/screens/PostThread/components/ThreadItemPost.tsx +++ b/src/screens/PostThread/components/ThreadItemPost.tsx @@ -8,7 +8,6 @@ import { } from '@atproto/api' import {Trans} from '@lingui/macro' -import {useActorStatus} from '#/lib/actor-status' import {MAX_POST_LINES} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {makeProfileLink} from '#/lib/routes/links' @@ -44,6 +43,7 @@ import {RichText} from '#/components/RichText' import * as Skele from '#/components/Skeleton' import {SubtleHover} from '#/components/SubtleHover' import {Text} from '#/components/Typography' +import {useActorStatus} from '#/features/liveNow' export type ThreadItemPostProps = { item: Extract diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index 9c1c8a662a..f2f24edaab 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -10,7 +10,6 @@ import { import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useActorStatus} from '#/lib/actor-status' import {useHaptics} from '#/lib/haptics' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' @@ -39,6 +38,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' import {IS_IOS} from '#/env' +import {useActorStatus} from '#/features/liveNow' import {GermButton} from '../components/GermButton' import {EditProfileDialog} from './EditProfileDialog' import {ProfileHeaderHandle} from './Handle' diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx index 8543d45bd1..ae7c146b8c 100644 --- a/src/screens/Profile/Header/Shell.tsx +++ b/src/screens/Profile/Header/Shell.tsx @@ -14,7 +14,6 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {useActorStatus} from '#/lib/actor-status' import {BACK_HITSLOP} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' import {type NavigationProp} from '#/lib/routes/types' @@ -28,13 +27,14 @@ import {atoms as a, platform, useTheme} from '#/alf' import {Button} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow' -import {EditLiveDialog} from '#/components/live/EditLiveDialog' -import {LiveIndicator} from '#/components/live/LiveIndicator' -import {LiveStatusDialog} from '#/components/live/LiveStatusDialog' import {LabelsOnMe} from '#/components/moderation/LabelsOnMe' import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts' import {useAnalytics} from '#/analytics' import {IS_IOS} from '#/env' +import {useActorStatus} from '#/features/liveNow' +import {EditLiveDialog} from '#/features/liveNow/components/EditLiveDialog' +import {LiveIndicator} from '#/features/liveNow/components/LiveIndicator' +import {LiveStatusDialog} from '#/features/liveNow/components/LiveStatusDialog' import {GrowableAvatar} from './GrowableAvatar' import {GrowableBanner} from './GrowableBanner' import {StatusBarShadow} from './StatusBarShadow' diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index 4f6fbb602b..c79386d05b 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -7,7 +7,6 @@ import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {type NativeStackScreenProps} from '@react-navigation/native-stack' -import {useActorStatus} from '#/lib/actor-status' import {HELP_DESK_URL} from '#/lib/constants' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates' @@ -70,6 +69,7 @@ import { } from '#/components/verification/VerificationCheckButton' import {useAnalytics} from '#/analytics' import {IS_INTERNAL, IS_IOS, IS_NATIVE} from '#/env' +import {useActorStatus} from '#/features/liveNow' import {device, useStorage} from '#/storage' import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged' diff --git a/src/state/appConfig.tsx b/src/state/appConfig.tsx new file mode 100644 index 0000000000..67b0e553ed --- /dev/null +++ b/src/state/appConfig.tsx @@ -0,0 +1,87 @@ +import {createContext, useContext} from 'react' +import {QueryClient, useQuery} from '@tanstack/react-query' + +import {APP_CONFIG_URL} from '#/env' + +const qc = new QueryClient() +const appConfigQueryKey = ['app-config'] + +/** + * Matches the types defined in our `app-config` worker + */ +type AppConfigResponse = { + liveNow: { + allow: string[] + exceptions: { + did: string + allow: string[] + }[] + } +} + +export const DEFAULT_APP_CONFIG_RESPONSE: AppConfigResponse = { + liveNow: { + allow: [], + exceptions: [], + }, +} + +let fetchAppConfigPromise: Promise | undefined + +async function fetchAppConfig(): Promise { + try { + if (!fetchAppConfigPromise) { + fetchAppConfigPromise = (async () => { + const r = await fetch(`${APP_CONFIG_URL}/config`) + if (!r.ok) throw new Error(await r.text()) + const data = await r.json() + return data + })() + } + return await fetchAppConfigPromise + } catch (e) { + fetchAppConfigPromise = undefined + throw e + } +} + +const Context = createContext(DEFAULT_APP_CONFIG_RESPONSE) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const {data} = useQuery( + { + staleTime: Infinity, + queryKey: appConfigQueryKey, + refetchInterval: query => { + // refetch regularly if fetch failed, otherwise never refetch + return query.state.status === 'error' ? 60e3 : Infinity + }, + async queryFn() { + return fetchAppConfig() + }, + }, + qc, + ) + return ( + + {children} + + ) +} + +export async function prefetchAppConfig() { + try { + const data = await fetchAppConfig() + if (data) { + qc.setQueryData(appConfigQueryKey, data) + } + } catch {} +} + +export function useAppConfig() { + const ctx = useContext(Context) + if (!ctx) { + throw new Error('useAppConfig must be used within a Provider') + } + return ctx +} diff --git a/src/state/queries/handle-availability.ts b/src/state/queries/handle-availability.ts index e3f7a6b741..fb25697c76 100644 --- a/src/state/queries/handle-availability.ts +++ b/src/state/queries/handle-availability.ts @@ -6,8 +6,8 @@ import { BSKY_SERVICE_DID, PUBLIC_BSKY_SERVICE, } from '#/lib/constants' +import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue' import {createFullHandle} from '#/lib/strings/handles' -import {useDebouncedValue} from '#/components/live/utils' import {useAnalytics} from '#/analytics' import * as bsky from '#/types/bsky' import {Agent} from '../session/agent' diff --git a/src/state/service-config.tsx b/src/state/service-config.tsx index 29fa6bec90..e102cde2f9 100644 --- a/src/state/service-config.tsx +++ b/src/state/service-config.tsx @@ -2,28 +2,17 @@ import {createContext, useContext, useMemo} from 'react' import {useLanguagePrefs} from '#/state/preferences/languages' import {useServiceConfigQuery} from '#/state/queries/service-config' -import {useSession} from '#/state/session' -import {useAnalytics} from '#/analytics' -import {IS_DEV} from '#/env' import {device} from '#/storage' type TrendingContext = { enabled: boolean } -type LiveNowContext = { - did: string - domains: string[] -}[] - const TrendingContext = createContext({ enabled: false, }) TrendingContext.displayName = 'TrendingContext' -const LiveNowContext = createContext([]) -LiveNowContext.displayName = 'LiveNowContext' - const CheckEmailConfirmedContext = createContext(null) export function Provider({children}: {children: React.ReactNode}) { @@ -60,19 +49,15 @@ export function Provider({children}: {children: React.ReactNode}) { return {enabled} }, [isInitialLoad, config, langPrefs.contentLanguages]) - const liveNow = useMemo(() => config?.liveNow ?? [], [config]) - // probably true, so default to true when loading // if the call fails, the query will set it to false for us const checkEmailConfirmed = config?.checkEmailConfirmed ?? true return ( - - - {children} - - + + {children} + ) } @@ -81,55 +66,6 @@ export function useTrendingConfig() { return useContext(TrendingContext) } -const DEFAULT_LIVE_ALLOWED_DOMAINS = [ - 'twitch.tv', - 'www.twitch.tv', - 'stream.place', - 'bluecast.app', - 'www.bluecast.app', -] -export type LiveNowConfig = { - currentAccountAllowedHosts: Set - defaultAllowedHosts: Set - allowedHostsExceptionsByDid: Map> -} -export function useLiveNowConfig(): LiveNowConfig { - const ctx = useContext(LiveNowContext) - const canGoLive = useCanGoLive() - const {currentAccount} = useSession() - return useMemo(() => { - const defaultAllowedHosts = new Set(DEFAULT_LIVE_ALLOWED_DOMAINS) - const allowedHostsExceptionsByDid = new Map>() - for (const live of ctx) { - allowedHostsExceptionsByDid.set( - live.did, - new Set(DEFAULT_LIVE_ALLOWED_DOMAINS.concat(live.domains)), - ) - } - if (!currentAccount?.did || !canGoLive) - return { - currentAccountAllowedHosts: new Set(), - defaultAllowedHosts, - allowedHostsExceptionsByDid, - } - const vip = ctx.find(live => live.did === currentAccount.did) - return { - currentAccountAllowedHosts: new Set( - DEFAULT_LIVE_ALLOWED_DOMAINS.concat(vip ? vip.domains : []), - ), - defaultAllowedHosts, - allowedHostsExceptionsByDid, - } - }, [ctx, currentAccount, canGoLive]) -} - -export function useCanGoLive() { - const ax = useAnalytics() - const {hasSession} = useSession() - if (!hasSession) return false - return IS_DEV ? true : !ax.features.enabled(ax.features.LiveNowBetaDisable) -} - export function useCheckEmailConfirmed() { const ctx = useContext(CheckEmailConfirmedContext) if (ctx === null) { diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index b79fc1ab32..899907cdef 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -19,7 +19,6 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' -import {isStatusStillActive, isStatusValidForViewers} from '#/lib/actor-status' import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' @@ -40,7 +39,6 @@ import { RQKEY, usePostFeedQuery, } from '#/state/queries/post-feed' -import {useLiveNowConfig} from '#/state/service-config' import {useSession} from '#/state/session' import {useProgressGuide} from '#/state/shell/progress-guide' import {useSelectedFeed} from '#/state/shell/selected-feed' @@ -63,6 +61,11 @@ import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interst import {useAnalytics} from '#/analytics' import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env' import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner' +import { + isStatusStillActive, + isStatusValidForViewers, + useLiveNowConfig, +} from '#/features/liveNow' import {ComposerPrompt} from '../feeds/ComposerPrompt' import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' import {FeedShutdownMsg} from './FeedShutdownMsg' diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index 0961004952..064c94d2c3 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -11,7 +11,6 @@ import { } from '@atproto/api' import {useQueryClient} from '@tanstack/react-query' -import {useActorStatus} from '#/lib/actor-status' import {type ReasonFeedSource} from '#/lib/api/feed/types' import {MAX_POST_LINES} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' @@ -48,6 +47,7 @@ import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug' import {RichText} from '#/components/RichText' import {SubtleHover} from '#/components/SubtleHover' import {useAnalytics} from '#/analytics' +import {useActorStatus} from '#/features/liveNow' import * as bsky from '#/types/bsky' import {PostFeedReason} from './PostFeedReason' diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index 684dcbf0fe..76b58fb718 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -5,7 +5,6 @@ import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' -import {useActorStatus} from '#/lib/actor-status' import {HITSLOP_20} from '#/lib/constants' import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' @@ -20,7 +19,6 @@ import { useProfileFollowMutationQueue, useProfileMuteMutationQueue, } from '#/state/queries/profile' -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' @@ -47,9 +45,6 @@ import { import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' import {StarterPack} from '#/components/icons/StarterPack' -import {EditLiveDialog} from '#/components/live/EditLiveDialog' -import {GoLiveDialog} from '#/components/live/GoLiveDialog' -import {GoLiveDisabledDialog} from '#/components/live/GoLiveDisabledDialog' import * as Menu from '#/components/Menu' import { ReportDialog, @@ -61,6 +56,10 @@ import {VerificationCreatePrompt} from '#/components/verification/VerificationCr import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {useActorStatus, useLiveNowConfig} from '#/features/liveNow' +import {EditLiveDialog} from '#/features/liveNow/components/EditLiveDialog' +import {GoLiveDialog} from '#/features/liveNow/components/GoLiveDialog' +import {GoLiveDisabledDialog} from '#/features/liveNow/components/GoLiveDisabledDialog' import {Dot} from '#/features/nuxs/components/Dot' import {Gradient} from '#/features/nuxs/components/Gradient' import {useDevMode} from '#/storage/hooks/dev-mode' @@ -85,7 +84,7 @@ let ProfileMenu = ({ const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked const [devModeEnabled] = useDevMode() const verification = useFullVerificationState({profile}) - const canGoLive = useCanGoLive() + const {canGoLive} = useLiveNowConfig() const status = useActorStatus(profile) const statusNudge = useNux(Nux.LiveNowBetaNudge) const statusNudgeActive = diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index ca40ef5f42..1a37e55cbd 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -5,7 +5,6 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' -import {useActorStatus} from '#/lib/actor-status' import {makeProfileLink} from '#/lib/routes/links' import {forceLTR} from '#/lib/strings/bidi' import {NON_BREAKING_SPACE} from '#/lib/strings/constants' @@ -21,6 +20,7 @@ import {Text} from '#/components/Typography' import {useSimpleVerificationState} from '#/components/verification' import {VerificationCheck} from '#/components/verification/VerificationCheck' import {IS_ANDROID} from '#/env' +import {useActorStatus} from '#/features/liveNow' import {TimeElapsed} from './TimeElapsed' import {PreviewableUserAvatar} from './UserAvatar' diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 529a6ed52f..6acbc6506e 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -15,7 +15,6 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' -import {useActorStatus} from '#/lib/actor-status' import {useHaptics} from '#/lib/haptics' import { useCameraPermission, @@ -47,13 +46,14 @@ import { import {StreamingLive_Stroke2_Corner0_Rounded as LibraryIcon} from '#/components/icons/StreamingLive' import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' import {Link} from '#/components/Link' -import {LiveIndicator} from '#/components/live/LiveIndicator' -import {LiveStatusDialog} from '#/components/live/LiveStatusDialog' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import * as Menu from '#/components/Menu' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {useAnalytics} from '#/analytics' import {IS_ANDROID, IS_NATIVE, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env' +import {useActorStatus} from '#/features/liveNow' +import {LiveIndicator} from '#/features/liveNow/components/LiveIndicator' +import {LiveStatusDialog} from '#/features/liveNow/components/LiveStatusDialog' import type * as bsky from '#/types/bsky' export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler' diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 42a5fe417c..d46e1e44e5 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -5,7 +5,6 @@ import {msg, Plural, plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {StackActions, useNavigation} from '@react-navigation/native' -import {useActorStatus} from '#/lib/actor-status' import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants' import {type PressableScale} from '#/lib/custom-animations/PressableScale' import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState' @@ -57,6 +56,7 @@ import {Text} from '#/components/Typography' import {useSimpleVerificationState} from '#/components/verification' import {VerificationCheck} from '#/components/verification/VerificationCheck' import {IS_WEB} from '#/env' +import {useActorStatus} from '#/features/liveNow' const iconWidth = 26 diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 7c2a010ab1..ef0ceaba97 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -7,7 +7,6 @@ import {useLingui} from '@lingui/react' import {type BottomTabBarProps} from '@react-navigation/bottom-tabs' import {StackActions} from '@react-navigation/native' -import {useActorStatus} from '#/lib/actor-status' import {PressableScale} from '#/lib/custom-animations/PressableScale' import {BOTTOM_BAR_AVI} from '#/lib/demo' import {useHaptics} from '#/lib/haptics' @@ -49,6 +48,7 @@ import { Message_Stroke2_Corner0_Rounded_Filled as MessageFilled, } from '#/components/icons/Message' import {Text} from '#/components/Typography' +import {useActorStatus} from '#/features/liveNow' import {useDemoMode} from '#/storage/hooks/demo-mode' import {styles} from './BottomBarStyles' diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 3e9ccea647..a9d1942051 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -5,7 +5,6 @@ import {msg, plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation, useNavigationState} from '@react-navigation/native' -import {useActorStatus} from '#/lib/actor-status' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {usePalette} from '#/lib/hooks/usePalette' @@ -74,6 +73,7 @@ import {CENTER_COLUMN_OFFSET} from '#/components/Layout' import * as Menu from '#/components/Menu' import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' +import {useActorStatus} from '#/features/liveNow' import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army' import {router} from '../../../routes'