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 && (