From fd29569ee7068155945a044e3bc39a0724f3bf49 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 13 Jan 2026 15:34:29 -0600 Subject: [PATCH] Add liveEvents context --- src/App.native.tsx | 9 ++- src/App.web.tsx | 9 ++- src/alf/index.tsx | 7 ++- src/features/liveEvents/context.tsx | 55 +++++++++++++++++ src/features/liveEvents/index.tsx | 94 +++++++++++++++++++++++++++++ src/features/liveEvents/types.ts | 18 ++++++ 6 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 src/features/liveEvents/context.tsx create mode 100644 src/features/liveEvents/index.tsx create mode 100644 src/features/liveEvents/types.ts diff --git a/src/App.native.tsx b/src/App.native.tsx index 34c3cc204c..02f0179906 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -69,6 +69,10 @@ import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbe import {ToastOutlet} from '#/components/Toast' import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance' import {prefetchAgeAssuranceConfig} from '#/ageAssurance' +import { + prefetchLiveEvents, + Provider as LiveEventsProvider, +} from '#/features/liveEvents/context' import * as Geo from '#/geolocation' import {Splash} from '#/Splash' import {BottomSheetProvider} from '../modules/bottom-sheet' @@ -92,6 +96,7 @@ if (isAndroid) { */ Geo.resolve() prefetchAgeAssuranceConfig() +prefetchLiveEvents() function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -230,7 +235,9 @@ function App() { - + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 956c52005c..d31bb56010 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -57,6 +57,10 @@ import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbe import {ToastOutlet} from '#/components/Toast' import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance' import {prefetchAgeAssuranceConfig} from '#/ageAssurance' +import { + prefetchLiveEvents, + Provider as LiveEventsProvider, +} from '#/features/liveEvents/context' import * as Geo from '#/geolocation' import {Splash} from '#/Splash' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' @@ -67,6 +71,7 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom */ Geo.resolve() prefetchAgeAssuranceConfig() +prefetchLiveEvents() function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -198,7 +203,9 @@ function App() { - + + + diff --git a/src/alf/index.tsx b/src/alf/index.tsx index eed3fcbeb2..3aff9ddb57 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -11,7 +11,12 @@ import { import {themes} from '#/alf/themes' import {type Device} from '#/storage' -export {type TextStyleProp, type Theme, type ViewStyleProp} from '@bsky.app/alf' +export { + type TextStyleProp, + type Theme, + utils, + type ViewStyleProp, +} from '@bsky.app/alf' export {atoms} from '#/alf/atoms' export * from '#/alf/breakpoints' export * from '#/alf/fonts' diff --git a/src/features/liveEvents/context.tsx b/src/features/liveEvents/context.tsx new file mode 100644 index 0000000000..4cd8520962 --- /dev/null +++ b/src/features/liveEvents/context.tsx @@ -0,0 +1,55 @@ +import {createContext, useContext} from 'react' +import {QueryClient, useQuery} from '@tanstack/react-query' + +import {IS_DEV, LIVE_EVENTS_URL} from '#/env' +import {type LiveEventsWorkerResponse} from '#/features/liveEvents/types' + +const qc = new QueryClient() +const liveEventsQueryKey = ['live-events'] + +async function fetchLiveEvents(): Promise { + try { + const res = await fetch(`${LIVE_EVENTS_URL}/config`) + if (!res.ok) return null + const data = await res.json() + return data + } catch { + return null + } +} + +const Context = createContext({ + feeds: [], +}) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const {data} = useQuery( + { + staleTime: IS_DEV ? 5e3 : 1000 * 60, + queryKey: liveEventsQueryKey, + async queryFn() { + return fetchLiveEvents() + }, + }, + qc, + ) + + return ( + {children} + ) +} + +export async function prefetchLiveEvents() { + const data = await fetchLiveEvents() + if (data) { + qc.setQueryData(liveEventsQueryKey, data) + } +} + +export function useLiveEvents() { + const ctx = useContext(Context) + if (!ctx) { + throw new Error('useLiveEventsContext must be used within a Provider') + } + return ctx +} diff --git a/src/features/liveEvents/index.tsx b/src/features/liveEvents/index.tsx new file mode 100644 index 0000000000..856e4a4fb7 --- /dev/null +++ b/src/features/liveEvents/index.tsx @@ -0,0 +1,94 @@ +import {createContext, useContext} from 'react' +import {QueryClient, useQuery} from '@tanstack/react-query' + +import {IS_DEV, LIVE_EVENTS_URL} from '#/env' + +type LiveEventFeedImageLayout = 'wide' // maybe more in the future +type LiveEventFeedImage = { + alt: string + overlayColor: string + url: string + blurhash: string +} +type LiveEventFeed = { + title: string + url: string + images: Partial> +} +type LiveEventsConfig = { + feeds: LiveEventFeed[] +} + +const qc = new QueryClient({ + defaultOptions: { + queries: { + /** + * We clear this manually, so disable automatic garbage collection. + * @see https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient#how-it-works + */ + gcTime: Infinity, + }, + }, +}) + +const liveEventsQueryKey = ['live-events'] + +async function getLiveEvents(): Promise { + const res = await fetch(`${LIVE_EVENTS_URL}/config`) + if (!res.ok) return null + return res.json() +} + +export function getLiveEventsCache() { + return qc.getQueryData(liveEventsQueryKey) +} + +export async function prefetchLiveEvents() { + const data = await getLiveEvents() + if (data) { + qc.setQueryData(liveEventsQueryKey, data) + } +} + +export const Context = createContext({ + feeds: [], +}) + +export function useLiveEventsContext() { + const ctx = useContext(Context) + if (!ctx) { + throw new Error('useLiveEventsContext must be used within a Provider') + } + return ctx +} + +export function Provider({children}: React.PropsWithChildren<{}>) { + const {data} = useQuery( + { + /** + * Will re-fetch when stale, at most every minute (or 5s in dev for easier + * testing). + * + * @see https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data#initial-data-from-the-cache-with-initialdataupdatedat + */ + staleTime: IS_DEV ? 5e3 : 1000 * 60, + /** + * N.B. if prefetch failed above, we'll have no `initialData`, and this + * query will run on startup. + */ + initialData: getLiveEventsCache(), + initialDataUpdatedAt: () => + qc.getQueryState(liveEventsQueryKey)?.dataUpdatedAt, + queryKey: liveEventsQueryKey, + async queryFn() { + console.debug(`live-events: fetching config`) + return getLiveEvents() + }, + }, + qc, + ) + + return ( + {children} + ) +} diff --git a/src/features/liveEvents/types.ts b/src/features/liveEvents/types.ts new file mode 100644 index 0000000000..faabcbe9f5 --- /dev/null +++ b/src/features/liveEvents/types.ts @@ -0,0 +1,18 @@ +export type LiveEventFeedImageLayout = 'wide' // maybe more in the future + +export type LiveEventFeedImage = { + alt: string + overlayColor: string + url: string + blurhash: string +} + +export type LiveEventFeed = { + title: string + url: string + images: Record +} + +export type LiveEventsWorkerResponse = { + feeds: LiveEventFeed[] +}