Skip to content
Merged
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
159 changes: 119 additions & 40 deletions packages/mobile/src/screens/feed-screen/FeedScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useMemo } from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'

import {
getFeedQueryKey,
Expand All @@ -13,6 +13,10 @@ import {
FOR_YOU_LOAD_MORE_PAGE_SIZE
} from '@audius/common/api'
import { Name, FeedTab } from '@audius/common/models'
import { StyleSheet, View } from 'react-native'
import PagerView, {
type PagerViewOnPageSelectedEvent
} from 'react-native-pager-view'

import { Screen, ScreenContent } from 'app/components/core'
import { EndOfLineupNotice } from 'app/components/lineup/EndOfLineupNotice'
Expand All @@ -29,6 +33,15 @@ const messages = {
endOfFeed: "Looks like you've reached the end of your feed..."
}

// Page order must match the tab order rendered by FeedTabs. Swiping left
// advances to a higher index (Latest); swiping right returns to For You.
const tabOrder: FeedTab[] = [FeedTab.FOR_YOU, FeedTab.LATEST]

const styles = StyleSheet.create({
pager: { flex: 1 },
page: { flex: 1 }
})

export const FeedScreen = () => {
const [feedTab, setFeedTab] = useFeedTab()
const [feedFilter] = useFeedFilter()
Expand Down Expand Up @@ -59,14 +72,54 @@ export const FeedScreen = () => {
[feedArgs]
)

const handleSelectTab = useCallback(
const pagerRef = useRef<PagerView>(null)
const feedTabIndex = tabOrder.indexOf(feedTab)
// Mirrors the pager's actual on-screen page so we only issue a programmatic
// page change when state and the pager have genuinely diverged (and so a
// swipe doesn't trigger a redundant setPage back to where it already is).
const currentPageRef = useRef(feedTabIndex)

const commitTab = useCallback(
(tab: FeedTab) => {
setFeedTab(tab)
track(make({ eventName: Name.FEED_CHANGE_VIEW, view: tab }))
},
[setFeedTab]
)

// Header tap: update state immediately (instant pill highlight); the effect
// below moves the pager to match.
const handleSelectTab = useCallback(
(tab: FeedTab) => {
commitTab(tab)
},
[commitTab]
)

const handlePageSelected = useCallback(
(e: PagerViewOnPageSelectedEvent) => {
const { position } = e.nativeEvent
currentPageRef.current = position
const tab = tabOrder[position]
// Skip when the pager already matches state. This swallows the initial
// onPageSelected that PagerView emits on mount (Android) — which would
// otherwise fire a spurious analytics event and could clobber the
// persisted tab — and avoids double-committing a header tap.
if (tab === feedTab) return
commitTab(tab)
},
[feedTab, commitTab]
)

// Keep the pager aligned with the persisted tab. Covers the async
// localStorage load (the stored tab resolves after mount) and header taps,
// without disturbing swipe gestures, which update currentPageRef first.
useEffect(() => {
if (currentPageRef.current !== feedTabIndex) {
pagerRef.current?.setPageWithoutAnimation(feedTabIndex)
}
}, [feedTabIndex])

// Memoized so the header isn't a new function reference on every render —
// otherwise Screen's setOptions runs each parent re-render and React
// Navigation rebuilds the header, remounting AccountPictureHeader and
Expand All @@ -80,49 +133,75 @@ export const FeedScreen = () => {
[isForYou]
)

const lineupProps = isForYou
? {
trackIds: forYouFeed.trackIds,
lineupItems: forYouFeed.data,
isPending: forYouFeed.isPending,
isFetching: forYouFeed.isFetching,
hasNextPage: forYouFeed.hasNextPage,
loadNextPage: forYouFeed.loadNextPage,
pageSize: FOR_YOU_LOAD_MORE_PAGE_SIZE,
initialPageSize: FOR_YOU_INITIAL_PAGE_SIZE,
refetch: undefined as undefined | (() => void),
querySource: undefined as { queryKey: unknown[] } | undefined
}
: {
trackIds: followFeed.trackIds,
lineupItems: followFeed.data,
isPending: followFeed.isPending,
isFetching: followFeed.isFetching,
hasNextPage: followFeed.hasNextPage,
loadNextPage: followFeed.loadNextPage,
pageSize: FEED_LOAD_MORE_PAGE_SIZE,
initialPageSize: FEED_INITIAL_PAGE_SIZE,
refetch: () => {
followFeed.refetch()
},
querySource: followQuerySource
}
const forYouLineupProps = {
// For You is intentionally track-focused: render from `trackIds` only and
// omit `lineupItems` so TrackLineup falls back to its pure-track mode,
// dropping playlist/album tiles. The backend `feed/for-you` endpoint has no
// tracks-only param, so this is filtered client-side. Pagination is
// unaffected — useForYouFeed still counts full backend pages (tracks +
// collections) when computing the next offset, so only the rendered set is
// trimmed, not the fetch window.
trackIds: forYouFeed.trackIds,
isPending: forYouFeed.isPending,
isFetching: forYouFeed.isFetching,
hasNextPage: forYouFeed.hasNextPage,
loadNextPage: forYouFeed.loadNextPage,
pageSize: FOR_YOU_LOAD_MORE_PAGE_SIZE,
initialPageSize: FOR_YOU_INITIAL_PAGE_SIZE
}
const followLineupProps = {
trackIds: followFeed.trackIds,
lineupItems: followFeed.data,
isPending: followFeed.isPending,
isFetching: followFeed.isFetching,
hasNextPage: followFeed.hasNextPage,
loadNextPage: followFeed.loadNextPage,
pageSize: FEED_LOAD_MORE_PAGE_SIZE,
initialPageSize: FEED_INITIAL_PAGE_SIZE,
refetch: () => {
followFeed.refetch()
},
querySource: followQuerySource
}

return (
<Screen url='Feed' header={renderHeader}>
<ScreenContent>
<FeedTabs currentTab={feedTab} onSelectTab={handleSelectTab} />
<TrackLineup
key={`feed-${feedTab}`}
source='DISCOVER_FEED'
pullToRefresh={!isForYou}
hideHeaderOnEmpty
LineupEmptyComponent={<SuggestedFollows />}
ListFooterComponent={
<EndOfLineupNotice description={messages.endOfFeed} />
}
{...lineupProps}
/>
{/* Horizontal pager: swipe left/right toggles between For You and
Latest, mirroring the tab headers above. Both lineups stay mounted
so each retains its own scroll position. */}
<PagerView
ref={pagerRef}
style={styles.pager}
initialPage={feedTabIndex}
onPageSelected={handlePageSelected}
>
<View key={FeedTab.FOR_YOU} style={styles.page}>
<TrackLineup
source='DISCOVER_FEED'
pullToRefresh={false}
hideHeaderOnEmpty
LineupEmptyComponent={<SuggestedFollows />}
ListFooterComponent={
<EndOfLineupNotice description={messages.endOfFeed} />
}
{...forYouLineupProps}
/>
</View>
<View key={FeedTab.LATEST} style={styles.page}>
<TrackLineup
source='DISCOVER_FEED'
pullToRefresh
hideHeaderOnEmpty
LineupEmptyComponent={<SuggestedFollows />}
ListFooterComponent={
<EndOfLineupNotice description={messages.endOfFeed} />
}
{...followLineupProps}
/>
</View>
</PagerView>
</ScreenContent>
</Screen>
)
Expand Down
Loading