From 3be51f569869d9310801628bc1f757fa5f0621c3 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 14 Jan 2026 12:48:44 -0600 Subject: [PATCH] Add sidebar cards, update types --- .../components/LiveEventFeedCardCompact.tsx | 133 ++++++++++++++++++ .../components/LiveEventFeedCardWide.tsx | 12 +- .../SidebarLiveEventFeedsBanner.tsx | 13 ++ src/features/liveEvents/index.tsx | 94 ------------- src/features/liveEvents/types.ts | 10 +- src/view/shell/desktop/RightNav.tsx | 7 +- 6 files changed, 162 insertions(+), 107 deletions(-) create mode 100644 src/features/liveEvents/components/LiveEventFeedCardCompact.tsx create mode 100644 src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx delete mode 100644 src/features/liveEvents/index.tsx diff --git a/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx b/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx new file mode 100644 index 0000000000..7135972187 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx @@ -0,0 +1,133 @@ +import {useEffect, useMemo} from 'react' +import {View} from 'react-native' +import {Image} from 'expo-image' +import {LinearGradient} from 'expo-linear-gradient' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isBskyCustomFeedUrl} from '#/lib/strings/url-helpers' +import {logger} from '#/logger' +import {atoms as a, utils} from '#/alf' +import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' +import { + type LiveEventFeed, + type LiveEventFeedMetricContext, +} from '#/features/liveEvents/types' + +const roundedStyles = [a.rounded_md, a.curve_continuous] + +export function LiveEventFeedCardCompact({ + feed, + metricContext, +}: { + feed: LiveEventFeed + metricContext: LiveEventFeedMetricContext +}) { + const {_} = useLingui() + + const layout = feed.layouts.compact + const overlayColor = layout.overlayColor + const textColor = layout.textColor + const url = useMemo(() => { + // Validated in multiple places on the backend + if (isBskyCustomFeedUrl(feed.url)) { + return new URL(feed.url).pathname + } + return '/' + }, [feed.url]) + + useEffect(() => { + logger.metric('liveEvents:feedBanner:seen', { + feed: feed.url, + context: metricContext, + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + { + logger.metric('liveEvents:feedBanner:click', { + feed: feed.url, + context: metricContext, + }) + }}> + {({hovered, pressed}) => ( + + + + + + + + + + + + + {layout.title} + + + + + + )} + + ) +} diff --git a/src/features/liveEvents/components/LiveEventFeedCardWide.tsx b/src/features/liveEvents/components/LiveEventFeedCardWide.tsx index bac88663a0..ea489d951e 100644 --- a/src/features/liveEvents/components/LiveEventFeedCardWide.tsx +++ b/src/features/liveEvents/components/LiveEventFeedCardWide.tsx @@ -27,9 +27,9 @@ export function LiveEventFeedCardWide({ const {_} = useLingui() const {gtPhone} = useBreakpoints() - const image = feed.images.wide - const overlayColor = image.overlayColor - const textColor = image.textColor + const layout = feed.layouts.wide + const overlayColor = layout.overlayColor + const textColor = layout.textColor const url = useMemo(() => { // Validated in multiple places on the backend if (isBskyCustomFeedUrl(feed.url)) { @@ -71,8 +71,8 @@ export function LiveEventFeedCardWide({ - {feed.title} + {layout.title} diff --git a/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx b/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx new file mode 100644 index 0000000000..25a5db8c8c --- /dev/null +++ b/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx @@ -0,0 +1,13 @@ +import {LiveEventFeedCardCompact} from '#/features/liveEvents/components/LiveEventFeedCardCompact' +import {useLiveEvents} from '#/features/liveEvents/context' + +export function SidebarLiveEventFeedsBanner() { + const events = useLiveEvents() + return events.feeds.map(feed => ( + + )) +} diff --git a/src/features/liveEvents/index.tsx b/src/features/liveEvents/index.tsx deleted file mode 100644 index 856e4a4fb7..0000000000 --- a/src/features/liveEvents/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -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 index 4fb449ab39..8bc88c448c 100644 --- a/src/features/liveEvents/types.ts +++ b/src/features/liveEvents/types.ts @@ -1,10 +1,10 @@ -export type LiveEventFeedImageLayout = 'wide' // maybe more in the future +export type LiveEventFeedImageLayout = 'wide' | 'compact' // maybe more in the future -export type LiveEventFeedImage = { - alt: string +export type LiveEventFeedLayout = { + title: string overlayColor: string textColor: string - url: string + image: string blurhash: string } @@ -13,7 +13,7 @@ export type LiveEventFeed = { preview: boolean title: string url: string - images: Record + layouts: Record } export type LiveEventsWorkerResponse = { diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 788df6e64c..04dddaf739 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -22,6 +22,7 @@ import {CENTER_COLUMN_OFFSET} from '#/components/Layout' import {InlineLinkText} from '#/components/Link' import {ProgressGuideList} from '#/components/ProgressGuide/List' import {Text} from '#/components/Typography' +import {SidebarLiveEventFeedsBanner} from '#/features/liveEvents/components/SidebarLiveEventFeedsBanner' function useWebQueryParams() { const navigation = useNavigation() @@ -49,7 +50,8 @@ export function DesktopRightNav({routeName}: {routeName: string}) { const isSearchScreen = routeName === 'Search' const webqueryParams = useWebQueryParams() const searchQuery = webqueryParams?.q - const showTrending = !isSearchScreen || (isSearchScreen && !!searchQuery) + const showExploreScreenDuplicatedContent = + !isSearchScreen || (isSearchScreen && !!searchQuery) const {rightNavVisible, centerColumnOffset, leftNavMinimal} = useLayoutBreakpoints() @@ -90,7 +92,8 @@ export function DesktopRightNav({routeName}: {routeName: string}) { )} - {showTrending && } + {showExploreScreenDuplicatedContent && } + {showExploreScreenDuplicatedContent && } {hasSession && (