Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/common/src/api/tan-query/lineups/useTrending.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,11 @@ export const useTrending = (
isPending: query.isPending,
isLoading: query.isLoading,
isFetching: query.isFetching,
isRefetching: query.isRefetching,
isSuccess: query.isSuccess,
isError: query.isError,
isInitialLoading: query.isInitialLoading,
dataUpdatedAt: query.dataUpdatedAt,
hasNextPage: query.hasNextPage,
fetchNextPage: query.fetchNextPage,
loadNextPage,
Expand Down
53 changes: 32 additions & 21 deletions packages/mobile/src/components/lineup/TrackLineup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ const styles = StyleSheet.create({
})

type LoadingItem = { _loading: true }
type TrackEntry = { kind: 'track'; trackId: ID; uid: UID }
type CollectionEntry = { kind: 'collection'; collectionId: ID }
type TrackEntry = { kind: 'track'; trackId: ID; uid: UID; key: string }
type CollectionEntry = { kind: 'collection'; collectionId: ID; key: string }
type Entry = TrackEntry | CollectionEntry
type RenderItem = Entry | LoadingItem

Expand Down Expand Up @@ -310,19 +310,31 @@ export const TrackLineup = ({
]
)

const entries: Entry[] = useMemo(
() =>
visibleItems.map((item) =>
item.type === EntityType.TRACK
? {
kind: 'track' as const,
trackId: item.id,
uid: uidFor(item.id)
}
: { kind: 'collection' as const, collectionId: item.id }
),
[visibleItems, uidFor]
)
// Keys follow the entity, not its position, so a refetch that reorders the
// list moves tiles instead of remounting them. A repeated entity gets a
// suffix to keep keys unique.
const entries: Entry[] = useMemo(() => {
const seen = new Map<string, number>()
const keyFor = (base: string) => {
const count = seen.get(base) ?? 0
seen.set(base, count + 1)
return count === 0 ? base : `${base}-${count}`
}
return visibleItems.map((item) =>
item.type === EntityType.TRACK
? {
kind: 'track' as const,
trackId: item.id,
uid: uidFor(item.id),
key: keyFor(`track-${item.id}`)
}
: {
kind: 'collection' as const,
collectionId: item.id,
key: keyFor(`collection-${item.id}`)
}
)
}, [visibleItems, uidFor])

// Synchronous "load more was triggered" flag — set the moment the scroll
// handler fires so skeletons render on the next frame, without waiting for
Expand All @@ -342,10 +354,12 @@ export const TrackLineup = ({

const sections: Section[] = useMemo(() => {
const getSkeletonCount = () => {
if (entries.length === 0 && isPending) {
if (entries.length === 0 && (isPending || isFetching)) {
return Math.min(maxEntries, initialPageSize ?? pageSize)
}
if (isFetching || isLoadMoreTriggered) {
// Only a load-more gets trailing skeletons. A background refetch
// replaces the list in place.
if (isLoadMoreTriggered) {
return Math.min(maxEntries, pageSize)
}
return 0
Expand Down Expand Up @@ -492,10 +506,7 @@ export const TrackLineup = ({
stickySectionHeadersEnabled={false}
keyExtractor={(item: any, index: number) => {
if ((item as LoadingItem)._loading) return `skeleton-${index}`
const entry = item as Entry
return entry.kind === 'track'
? `track-${entry.uid}-${index}`
: `collection-${entry.collectionId}-${index}`
return (item as Entry).key
}}
renderItem={renderItem}
scrollIndicatorInsets={{ right: Number.MIN_VALUE }}
Expand Down
28 changes: 24 additions & 4 deletions packages/mobile/src/screens/trending-screen/TrendingLineup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ import { TrackLineup } from 'app/components/lineup/TrackLineup'
const { setTrendingTimeRange } = trendingPageActions
const { getTrendingGenre } = trendingPageSelectors

// Matches the query client's default staleTime.
const STALE_TIME_MS = 1000 * 60 * 5

// Trending is persisted to disk so it can paint on a cold start. A snapshot
// that was already stale when this session started gets refetched on mount,
// and painting it first means showing an old order and then reshuffling.
const sessionStartedAt = Date.now()

const sourceFor = (timeRange: TimeRange) => {
if (timeRange === TimeRange.WEEK) return 'DISCOVER_TRENDING_WEEK'
if (timeRange === TimeRange.MONTH) return 'DISCOVER_TRENDING_MONTH'
Expand Down Expand Up @@ -70,8 +78,20 @@ export const TrendingLineup = ({
[timeRange, genre]
)

const { trackIds, isPending, isFetching, hasNextPage, loadNextPage } =
useTrending(queryArgs)
const {
trackIds,
isPending,
isFetching,
isRefetching,
dataUpdatedAt,
hasNextPage,
loadNextPage
} = useTrending(queryArgs)

// Show skeletons until the refetch of a stale restored snapshot lands. If
// it fails, the snapshot is shown.
const isRestoredStale =
isRefetching && dataUpdatedAt < sessionStartedAt - STALE_TIME_MS

const querySource = useMemo(
() => ({ queryKey: [...getTrendingQueryKey(queryArgs)] as unknown[] }),
Expand All @@ -81,10 +101,10 @@ export const TrendingLineup = ({
return (
<TrackLineup
tileVariant={isTrackRowsEnabled ? 'row' : 'tile'}
trackIds={trackIds}
trackIds={isRestoredStale ? [] : trackIds}
source={sourceFor(timeRange)}
querySource={querySource}
isPending={isPending}
isPending={isPending || isRestoredStale}
isFetching={isFetching}
hasNextPage={hasNextPage}
loadNextPage={loadNextPage}
Expand Down
Loading