Add sidebar cards, update types
This commit is contained in:
@@ -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 (
|
||||
<Link
|
||||
to={url}
|
||||
label={_(msg`Live event happening now: ${feed.title}`)}
|
||||
style={[a.w_full]}
|
||||
onPress={() => {
|
||||
logger.metric('liveEvents:feedBanner:click', {
|
||||
feed: feed.url,
|
||||
context: metricContext,
|
||||
})
|
||||
}}>
|
||||
{({hovered, pressed}) => (
|
||||
<View style={[roundedStyles, a.shadow_md, a.w_full]}>
|
||||
<View
|
||||
style={[a.w_full, a.align_start, a.overflow_hidden, roundedStyles]}>
|
||||
<Image
|
||||
accessibilityIgnoresInvertColors
|
||||
source={{
|
||||
uri: layout.image,
|
||||
blurhash: layout.blurhash,
|
||||
}}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
contentFit="cover"
|
||||
/>
|
||||
|
||||
<LinearGradient
|
||||
colors={[overlayColor, utils.alpha(overlayColor, 0)]}
|
||||
locations={[0, 1]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 0}}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.transition_opacity,
|
||||
{
|
||||
transitionDuration: '200ms',
|
||||
opacity: hovered || pressed ? 0.6 : 0,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<View style={[a.w_full, a.justify_end]}>
|
||||
<LinearGradient
|
||||
colors={[
|
||||
overlayColor,
|
||||
utils.alpha(overlayColor, 0.7),
|
||||
utils.alpha(overlayColor, 0),
|
||||
]}
|
||||
locations={[0, 0.8, 1]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 0}}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
a.z_10,
|
||||
a.px_lg,
|
||||
a.py_md,
|
||||
]}>
|
||||
<LiveIcon size="md" fill={textColor} />
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.leading_snug,
|
||||
a.font_bold,
|
||||
a.text_lg,
|
||||
a.pr_xl,
|
||||
{color: textColor},
|
||||
]}>
|
||||
{layout.title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -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({
|
||||
<Image
|
||||
accessibilityIgnoresInvertColors
|
||||
source={{
|
||||
uri: image.url,
|
||||
blurhash: image.blurhash,
|
||||
uri: layout.image,
|
||||
blurhash: layout.blurhash,
|
||||
}}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
contentFit="cover"
|
||||
@@ -128,7 +128,7 @@ export function LiveEventFeedCardWide({
|
||||
gtPhone ? a.text_3xl : a.text_lg,
|
||||
{color: textColor},
|
||||
]}>
|
||||
{feed.title}
|
||||
{layout.title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -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 => (
|
||||
<LiveEventFeedCardCompact
|
||||
key={feed.id}
|
||||
feed={feed}
|
||||
metricContext="sidebar"
|
||||
/>
|
||||
))
|
||||
}
|
||||
@@ -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<Record<LiveEventFeedImageLayout, LiveEventFeedImage>>
|
||||
}
|
||||
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<LiveEventsConfig | null> {
|
||||
const res = await fetch(`${LIVE_EVENTS_URL}/config`)
|
||||
if (!res.ok) return null
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export function getLiveEventsCache() {
|
||||
return qc.getQueryData<LiveEventsConfig>(liveEventsQueryKey)
|
||||
}
|
||||
|
||||
export async function prefetchLiveEvents() {
|
||||
const data = await getLiveEvents()
|
||||
if (data) {
|
||||
qc.setQueryData(liveEventsQueryKey, data)
|
||||
}
|
||||
}
|
||||
|
||||
export const Context = createContext<LiveEventsConfig>({
|
||||
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 (
|
||||
<Context.Provider value={data || {feeds: []}}>{children}</Context.Provider>
|
||||
)
|
||||
}
|
||||
@@ -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<LiveEventFeedImageLayout, LiveEventFeedImage>
|
||||
layouts: Record<LiveEventFeedImageLayout, LiveEventFeedLayout>
|
||||
}
|
||||
|
||||
export type LiveEventsWorkerResponse = {
|
||||
|
||||
@@ -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 && <SidebarTrendingTopics />}
|
||||
{showExploreScreenDuplicatedContent && <SidebarLiveEventFeedsBanner />}
|
||||
{showExploreScreenDuplicatedContent && <SidebarTrendingTopics />}
|
||||
|
||||
<Text style={[a.leading_snug, t.atoms.text_contrast_low]}>
|
||||
{hasSession && (
|
||||
|
||||
Reference in New Issue
Block a user