Skip to content

Commit 8879241

Browse files
dylanjeffersclaude
andcommitted
Feed: collapse to two tabs (For You / Chronological) with filter pills
Replaces the three-tab UI (FOR_YOU / FOLLOWING / UPLOADS_ONLY) with two top-level tabs and surfaces the existing FeedFilter (All / Original / Reposts) as pills directly under the Chronological tab. Pills are hidden on the For You tab. - FeedTab enum: drop UPLOADS_ONLY, rename FOLLOWING -> CHRONOLOGICAL - New FeedFilters pill components for mobile and web - FeedScreen / FeedPageContent (desktop + mobile) now read feedFilter from state and dispatch setFeedFilter on pill change, instead of deriving filter from tab - Coerce legacy persisted FeedTab values (FOLLOWING / UPLOADS_ONLY) to CHRONOLOGICAL so existing users don't land in an invalid tab Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 519da44 commit 8879241

8 files changed

Lines changed: 221 additions & 71 deletions

File tree

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export enum FeedTab {
22
FOR_YOU = 'FOR_YOU',
3-
FOLLOWING = 'FOLLOWING',
4-
UPLOADS_ONLY = 'UPLOADS_ONLY'
3+
CHRONOLOGICAL = 'CHRONOLOGICAL'
54
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { FeedFilter } from '@audius/common/models'
2+
import { ScrollView, View } from 'react-native'
3+
4+
import { Flex, SelectablePill, useTheme } from '@audius/harmony-native'
5+
6+
const filterLabels: Record<FeedFilter, string> = {
7+
[FeedFilter.ALL]: 'All Posts',
8+
[FeedFilter.ORIGINAL]: 'Original Posts',
9+
[FeedFilter.REPOST]: 'Reposts'
10+
}
11+
12+
const filters: FeedFilter[] = [
13+
FeedFilter.ALL,
14+
FeedFilter.ORIGINAL,
15+
FeedFilter.REPOST
16+
]
17+
18+
type FeedFiltersProps = {
19+
currentFilter: FeedFilter
20+
onSelectFilter: (filter: FeedFilter) => void
21+
}
22+
23+
export const FeedFilters = ({
24+
currentFilter,
25+
onSelectFilter
26+
}: FeedFiltersProps) => {
27+
const { spacing, color } = useTheme()
28+
return (
29+
<View
30+
style={{
31+
backgroundColor: color.background.white,
32+
paddingBottom: spacing.s
33+
}}
34+
>
35+
<ScrollView
36+
horizontal
37+
showsHorizontalScrollIndicator={false}
38+
contentContainerStyle={{ paddingHorizontal: spacing.l }}
39+
>
40+
<Flex direction='row' alignItems='center' gap='s'>
41+
{filters.map((filter) => (
42+
<SelectablePill
43+
key={filter}
44+
type='radio'
45+
size='small'
46+
value={filter}
47+
label={filterLabels[filter]}
48+
isSelected={currentFilter === filter}
49+
onChange={(value, isSelected) => {
50+
if (!isSelected) return
51+
onSelectFilter(value as FeedFilter)
52+
}}
53+
disableUnselectAnimation
54+
/>
55+
))}
56+
</Flex>
57+
</ScrollView>
58+
</View>
59+
)
60+
}

packages/mobile/src/screens/feed-screen/FeedScreen.tsx

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,44 +21,42 @@ import { SuggestedFollows } from 'app/components/suggested-follows'
2121
import { MobileRootHeader } from 'app/screens/app-screen/MobileRootHeader'
2222
import { make, track } from 'app/services/analytics'
2323

24+
import { FeedFilters } from './FeedFilters'
2425
import { FeedTabs } from './FeedTabs'
2526

26-
const { getFeedTab } = feedPageSelectors
27-
const { setFeedTab } = feedPageActions
27+
const { getFeedTab, getFeedFilter } = feedPageSelectors
28+
const { setFeedTab, setFeedFilter } = feedPageActions
2829

2930
const messages = {
3031
header: 'Your Feed',
3132
endOfFeed: "Looks like you've reached the end of your feed..."
3233
}
3334

34-
const tabToFilter: Record<Exclude<FeedTab, FeedTab.FOR_YOU>, FeedFilter> = {
35-
[FeedTab.FOLLOWING]: FeedFilter.ALL,
36-
[FeedTab.UPLOADS_ONLY]: FeedFilter.ORIGINAL
37-
}
38-
3935
// Note: the feed API returns both tracks and collections (playlist reposts).
4036
// The new TrackLineup renders tracks only, so collections are filtered out by
4137
// `trackIds` on the hook side. This is a known limitation introduced by the
4238
// tanquery migration — collection feed rendering will be restored if/when
4339
// TrackLineup learns to render mixed feeds.
4440
export const FeedScreen = () => {
4541
const dispatch = useDispatch()
46-
const feedTab = useSelector(getFeedTab)
42+
const persistedTab = useSelector(getFeedTab)
43+
const feedFilter = useSelector(getFeedFilter)
4744
const { data: currentUserId } = useCurrentUserId()
4845

46+
// Coerce legacy persisted FeedTab values (FOLLOWING / UPLOADS_ONLY) to
47+
// CHRONOLOGICAL after the For You / Chronological refactor.
48+
const feedTab =
49+
persistedTab === FeedTab.FOR_YOU ? FeedTab.FOR_YOU : FeedTab.CHRONOLOGICAL
4950
const isForYou = feedTab === FeedTab.FOR_YOU
50-
const followingFilter = isForYou
51-
? FeedFilter.ALL
52-
: tabToFilter[feedTab as Exclude<FeedTab, FeedTab.FOR_YOU>]
5351

5452
const feedArgs = useMemo(
5553
() => ({
5654
userId: currentUserId,
57-
filter: followingFilter,
55+
filter: feedFilter,
5856
initialPageSize: FEED_INITIAL_PAGE_SIZE,
5957
loadMorePageSize: FEED_LOAD_MORE_PAGE_SIZE
6058
}),
61-
[followingFilter, currentUserId]
59+
[feedFilter, currentUserId]
6260
)
6361
const followFeed = useFeed(feedArgs, { enabled: !isForYou })
6462
const forYouFeed = useForYouFeed(
@@ -82,6 +80,14 @@ export const FeedScreen = () => {
8280
[dispatch]
8381
)
8482

83+
const handleSelectFilter = useCallback(
84+
(filter: FeedFilter) => {
85+
dispatch(setFeedFilter(filter))
86+
track(make({ eventName: Name.FEED_CHANGE_VIEW, view: filter }))
87+
},
88+
[dispatch]
89+
)
90+
8591
const lineupProps = isForYou
8692
? {
8793
trackIds: forYouFeed.trackIds,
@@ -117,6 +123,12 @@ export const FeedScreen = () => {
117123
>
118124
<ScreenContent>
119125
<FeedTabs currentTab={feedTab} onSelectTab={handleSelectTab} />
126+
{isForYou ? null : (
127+
<FeedFilters
128+
currentFilter={feedFilter}
129+
onSelectFilter={handleSelectFilter}
130+
/>
131+
)}
120132
<TrackLineup
121133
key={`feed-${feedTab}`}
122134
source='DISCOVER_FEED'

packages/mobile/src/screens/feed-screen/FeedTabs.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,10 @@ import { Flex, SelectablePill, useTheme } from '@audius/harmony-native'
55

66
const tabLabels: Record<FeedTab, string> = {
77
[FeedTab.FOR_YOU]: 'For You',
8-
[FeedTab.FOLLOWING]: 'Following',
9-
[FeedTab.UPLOADS_ONLY]: 'Uploads Only'
8+
[FeedTab.CHRONOLOGICAL]: 'Chronological'
109
}
1110

12-
const tabs: FeedTab[] = [
13-
FeedTab.FOR_YOU,
14-
FeedTab.FOLLOWING,
15-
FeedTab.UPLOADS_ONLY
16-
]
11+
const tabs: FeedTab[] = [FeedTab.FOR_YOU, FeedTab.CHRONOLOGICAL]
1712

1813
type FeedTabsProps = {
1914
currentTab: FeedTab
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { ChangeEvent, useCallback } from 'react'
2+
3+
import { FeedFilter } from '@audius/common/models'
4+
import { Flex, SelectablePill } from '@audius/harmony'
5+
6+
type FeedFiltersProps = {
7+
currentFilter: FeedFilter
8+
onSelectFilter: (filter: FeedFilter) => void
9+
}
10+
11+
const messages = {
12+
allPosts: 'All Posts',
13+
originalPosts: 'Original Posts',
14+
reposts: 'Reposts'
15+
}
16+
17+
const filterToLabel: Record<FeedFilter, string> = {
18+
[FeedFilter.ALL]: messages.allPosts,
19+
[FeedFilter.ORIGINAL]: messages.originalPosts,
20+
[FeedFilter.REPOST]: messages.reposts
21+
}
22+
23+
const filters: FeedFilter[] = [
24+
FeedFilter.ALL,
25+
FeedFilter.ORIGINAL,
26+
FeedFilter.REPOST
27+
]
28+
29+
export const FeedFilters = ({
30+
currentFilter,
31+
onSelectFilter
32+
}: FeedFiltersProps) => {
33+
const handleChange = useCallback(
34+
(e: ChangeEvent<HTMLInputElement>) => {
35+
onSelectFilter(e.target.value as FeedFilter)
36+
},
37+
[onSelectFilter]
38+
)
39+
40+
return (
41+
<Flex gap='s' role='radiogroup' onChange={handleChange}>
42+
{filters.map((filter) => (
43+
<SelectablePill
44+
name='feed-filter'
45+
key={filter}
46+
type='radio'
47+
value={filter}
48+
label={filterToLabel[filter]}
49+
isSelected={currentFilter === filter}
50+
size='small'
51+
/>
52+
))}
53+
</Flex>
54+
)
55+
}

packages/web/src/pages/feed-page/components/FeedTabs.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,15 @@ type FeedTabsProps = {
1010

1111
const messages = {
1212
forYou: 'For You',
13-
following: 'Following',
14-
uploadsOnly: 'Uploads Only'
13+
chronological: 'Chronological'
1514
}
1615

1716
const tabToLabel: Record<FeedTab, string> = {
1817
[FeedTab.FOR_YOU]: messages.forYou,
19-
[FeedTab.FOLLOWING]: messages.following,
20-
[FeedTab.UPLOADS_ONLY]: messages.uploadsOnly
18+
[FeedTab.CHRONOLOGICAL]: messages.chronological
2119
}
2220

23-
const tabs: FeedTab[] = [
24-
FeedTab.FOR_YOU,
25-
FeedTab.FOLLOWING,
26-
FeedTab.UPLOADS_ONLY
27-
]
21+
const tabs: FeedTab[] = [FeedTab.FOR_YOU, FeedTab.CHRONOLOGICAL]
2822

2923
export const FeedTabs = ({ currentTab, onSelectTab }: FeedTabsProps) => {
3024
const handleChange = useCallback(

packages/web/src/pages/feed-page/components/desktop/FeedPageContent.tsx

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo, useRef } from 'react'
1+
import { useCallback, useMemo, useRef } from 'react'
22

33
import {
44
getFeedQueryKey,
@@ -24,6 +24,7 @@ import { TrackLineup } from 'components/lineup/TrackLineup'
2424
import { LineupVariant } from 'components/lineup/types'
2525
import Page from 'components/page/Page'
2626
import EmptyFeed from 'pages/feed-page/components/EmptyFeed'
27+
import { FeedFilters } from 'pages/feed-page/components/FeedFilters'
2728
import { FeedTabs } from 'pages/feed-page/components/FeedTabs'
2829

2930
const messages = {
@@ -32,17 +33,12 @@ const messages = {
3233
feedDescription: 'Listen to what people you follow are sharing'
3334
}
3435

35-
const { getFeedTab } = feedPageSelectors
36+
const { getFeedTab, getFeedFilter } = feedPageSelectors
3637

3738
type FeedPageContentProps = {
3839
containerRef?: React.RefObject<HTMLDivElement>
3940
}
4041

41-
const tabToFilter: Record<Exclude<FeedTab, FeedTab.FOR_YOU>, FeedFilter> = {
42-
[FeedTab.FOLLOWING]: FeedFilter.ALL,
43-
[FeedTab.UPLOADS_ONLY]: FeedFilter.ORIGINAL
44-
}
45-
4642
// Note: the feed API returns both tracks and collections (playlist reposts).
4743
// The new TrackLineup renders tracks only, so collections are filtered out by
4844
// `trackIds` on the hook side. This is a known limitation introduced by the
@@ -51,28 +47,30 @@ const tabToFilter: Record<Exclude<FeedTab, FeedTab.FOR_YOU>, FeedFilter> = {
5147
const FeedPageContent = ({ containerRef }: FeedPageContentProps) => {
5248
const dispatch = useDispatch()
5349
const titleRowRef = useRef<HTMLDivElement>(null)
54-
const feedTab = useSelector(getFeedTab)
50+
const persistedTab = useSelector(getFeedTab)
51+
const feedFilter = useSelector(getFeedFilter)
5552
const { data: currentUserId } = useCurrentUserId()
5653

5754
// Desktop viewports + fast trackpad / wheel scroll need bigger pages than
5855
// the shared default (mobile-tuned) so successive load-mores keep up with a
5956
// user scrolling deep into the lineup.
6057
const desktopLoadMorePageSize = 10
6158

59+
// Coerce legacy persisted FeedTab values (FOLLOWING / UPLOADS_ONLY) to
60+
// CHRONOLOGICAL after the For You / Chronological refactor.
61+
const feedTab =
62+
persistedTab === FeedTab.FOR_YOU ? FeedTab.FOR_YOU : FeedTab.CHRONOLOGICAL
6263
const isForYou = feedTab === FeedTab.FOR_YOU
63-
const followingFilter = isForYou
64-
? FeedFilter.ALL
65-
: tabToFilter[feedTab as Exclude<FeedTab, FeedTab.FOR_YOU>]
6664

67-
// Following / Uploads-Only lineup. Disabled while For You is active.
65+
// Chronological lineup. Disabled while For You is active.
6866
const feedArgs = useMemo(
6967
() => ({
7068
userId: currentUserId,
71-
filter: followingFilter,
69+
filter: feedFilter,
7270
initialPageSize: FEED_INITIAL_PAGE_SIZE,
7371
loadMorePageSize: desktopLoadMorePageSize
7472
}),
75-
[followingFilter, currentUserId]
73+
[feedFilter, currentUserId]
7674
)
7775
const followFeed = useFeed(feedArgs, { enabled: !isForYou })
7876

@@ -91,13 +89,24 @@ const FeedPageContent = ({ containerRef }: FeedPageContentProps) => {
9189
)
9290

9391
const record = useRecord()
94-
const onSelectTab = (tab: FeedTab) => {
95-
if (containerRef?.current?.scrollTo) {
96-
containerRef.current.scrollTo(0, 0)
97-
}
98-
dispatch(discoverPageAction.setFeedTab(tab))
99-
record(make(Name.FEED_CHANGE_VIEW, { view: tab }))
100-
}
92+
const onSelectTab = useCallback(
93+
(tab: FeedTab) => {
94+
if (containerRef?.current?.scrollTo) {
95+
containerRef.current.scrollTo(0, 0)
96+
}
97+
dispatch(discoverPageAction.setFeedTab(tab))
98+
record(make(Name.FEED_CHANGE_VIEW, { view: tab }))
99+
},
100+
[containerRef, dispatch, record]
101+
)
102+
103+
const onSelectFilter = useCallback(
104+
(filter: FeedFilter) => {
105+
dispatch(discoverPageAction.setFeedFilter(filter))
106+
record(make(Name.FEED_CHANGE_VIEW, { view: filter }))
107+
},
108+
[dispatch, record]
109+
)
101110

102111
const header = (
103112
<Header
@@ -107,6 +116,14 @@ const FeedPageContent = ({ containerRef }: FeedPageContentProps) => {
107116
rightDecorator={
108117
<FeedTabs currentTab={feedTab} onSelectTab={onSelectTab} />
109118
}
119+
bottomBar={
120+
isForYou ? null : (
121+
<FeedFilters
122+
currentFilter={feedFilter}
123+
onSelectFilter={onSelectFilter}
124+
/>
125+
)
126+
}
110127
/>
111128
)
112129

0 commit comments

Comments
 (0)