From 5bbcc48c930e71f07281cf468dee992d88432d8f Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Tue, 5 May 2026 18:34:49 -0700 Subject: [PATCH 1/3] Add FeedTab state and useForYouFeed lineup composer Introduce a FeedTab enum (FOR_YOU/FOLLOWING/UPLOADS_ONLY) separate from the existing FeedFilter enum so the new tab UI can drive which lineup is rendered without coupling to the SDK filter parameter. The new feedTab Redux slot defaults to FOR_YOU and is persisted alongside feedFilter. useForYouFeed composes four candidate streams: - getUserRecommendedTracks (server-side personalization, 50%) - useFeed with FeedFilter.ORIGINAL (social-graph signal, 20%) - useTrending (week range, cultural-recency signal, 10%) - useTrendingUnderground (discovery, 10%) Slots are interleaved with a fixed 10-item pattern. Tracks the user has already favorited are filtered out so they're not recycled as "new" recommendations. Pagination advances every source whose hasNextPage is still true in parallel. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/common/src/api/index.ts | 1 + .../api/tan-query/lineups/useForYouFeed.ts | 250 ++++++++++++++++++ .../common/src/api/tan-query/queryKeys.ts | 1 + packages/common/src/models/FeedTab.ts | 5 + packages/common/src/models/index.ts | 1 + .../common/src/store/pages/feed/actions.ts | 17 +- .../common/src/store/pages/feed/reducer.ts | 21 +- .../common/src/store/pages/feed/selectors.ts | 2 + packages/common/src/store/pages/feed/types.ts | 3 +- 9 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 packages/common/src/api/tan-query/lineups/useForYouFeed.ts create mode 100644 packages/common/src/models/FeedTab.ts diff --git a/packages/common/src/api/index.ts b/packages/common/src/api/index.ts index e2a20afe7f1..d492818bda2 100644 --- a/packages/common/src/api/index.ts +++ b/packages/common/src/api/index.ts @@ -38,6 +38,7 @@ export * from './tan-query/collection/useBestSellingAlbums' // Lineups export * from './tan-query/lineups/useFeed' +export * from './tan-query/lineups/useForYouFeed' export * from './tan-query/lineups/useExclusiveTracks' export * from './tan-query/lineups/useLibraryTracks' export * from './tan-query/lineups/useProfileReposts' diff --git a/packages/common/src/api/tan-query/lineups/useForYouFeed.ts b/packages/common/src/api/tan-query/lineups/useForYouFeed.ts new file mode 100644 index 00000000000..08da0acc5fc --- /dev/null +++ b/packages/common/src/api/tan-query/lineups/useForYouFeed.ts @@ -0,0 +1,250 @@ +import { useMemo } from 'react' + +import { useFavoritedTracks } from '~/api/tan-query/tracks/useFavoritedTracks' +import { useRecommendedTracks } from '~/api/tan-query/tracks/useRecommendedTracks' +import { useCurrentUserId } from '~/api/tan-query/users/account/useCurrentUserId' +import { FavoriteType } from '~/models/Favorite' +import { FeedFilter, ID } from '~/models' +import { TimeRange } from '~/models/TimeRange' + +import { QueryOptions } from '../types' + +import { useFeed } from './useFeed' +import { useTrending } from './useTrending' +import { useTrendingUnderground } from './useTrendingUnderground' + +export const FOR_YOU_INITIAL_PAGE_SIZE = 10 +export const FOR_YOU_LOAD_MORE_PAGE_SIZE = 10 + +/** + * Composition weights per 10-slot page. + * Sum must equal page size. Order is the position priority when interleaving. + */ +const SLOTS_PER_PAGE: Array<'recommended' | 'following' | 'trending' | 'underground'> = [ + 'recommended', + 'recommended', + 'recommended', + 'following', + 'recommended', + 'trending', + 'recommended', + 'following', + 'underground', + 'recommended' +] + +type ForYouFeedArgs = { + initialPageSize?: number + loadMorePageSize?: number +} + +/** + * "For You" personalized feed. + * + * Composes four candidate streams produced by existing TanStack Query hooks: + * - Recommended Tracks (SDK getUserRecommendedTracks) — server-side personalization + * - Following Original Posts (useFeed with FeedFilter.ORIGINAL) — explicit social graph + * - Trending Tracks (week range) — cultural-recency signal + * - Underground Trending — discovery / serendipity + * + * Trust the server's ranking inside each source; do not re-score on the client. + * Interleave the sources with a fixed 10-slot pattern so no source dominates a + * window. Dedupe by track_id (Recommended wins ties, then Following, then + * Trending, then Underground). Apply a "no two consecutive tracks by the same + * artist" diversity rule. + * + * Pagination: each underlying query has its own infinite-query state. We build + * the composed list from whatever has been loaded so far. When the user nears + * the end, `loadNextPage` advances all sources that still have more. + */ +export const useForYouFeed = ( + { + initialPageSize = FOR_YOU_INITIAL_PAGE_SIZE, + loadMorePageSize: _loadMorePageSize = FOR_YOU_LOAD_MORE_PAGE_SIZE + }: ForYouFeedArgs = {}, + options?: QueryOptions +) => { + const { data: currentUserId } = useCurrentUserId() + const enabled = options?.enabled !== false && !!currentUserId + + // Source 1: Recommended (50%) + const recommended = useRecommendedTracks( + { pageSize: initialPageSize }, + { enabled } + ) + + // Source 2: Following — original uploads only (20%) + const following = useFeed( + { + filter: FeedFilter.ORIGINAL, + initialPageSize, + loadMorePageSize: initialPageSize + }, + { enabled } + ) + + // Source 3: Trending tracks this week (10%) + const trending = useTrending( + { + timeRange: TimeRange.WEEK, + initialPageSize, + loadMorePageSize: initialPageSize + }, + { enabled } + ) + + // Source 4: Underground trending (10%) — discovery + const underground = useTrendingUnderground( + { pageSize: initialPageSize }, + { enabled } + ) + + // The user's favorited tracks act as a dedupe input — they've already saved + // these, so don't recycle them as "new" recommendations. + const favorited = useFavoritedTracks(currentUserId, { enabled }) + + const trackIds = useMemo(() => { + const recommendedIds = recommended.data ?? [] + const followingIds = following.trackIds ?? [] + const trendingIds = trending.trackIds ?? [] + const undergroundIds = underground.trackIds ?? [] + const favoritedIdSet = new Set( + (favorited.data ?? []) + .filter((f) => f.save_type === FavoriteType.TRACK) + .map((f) => f.save_item_id) + ) + + const seen = new Set() + const cursors = { + recommended: 0, + following: 0, + trending: 0, + underground: 0 + } + const sources = { + recommended: recommendedIds, + following: followingIds, + trending: trendingIds, + underground: undergroundIds + } as const + + const composed: ID[] = [] + let lastArtistTrack: ID | undefined + + const tryTake = ( + key: keyof typeof sources + ): ID | undefined => { + const list = sources[key] + while (cursors[key] < list.length) { + const id = list[cursors[key]++] + if (id == null) continue + if (seen.has(id)) continue + if (favoritedIdSet.has(id)) continue + seen.add(id) + return id + } + return undefined + } + + // Fallback priority when the slot's preferred source is exhausted. + const fallbackOrder: Array = [ + 'recommended', + 'following', + 'trending', + 'underground' + ] + + // Build as many full pages as the loaded data supports, plus any partial. + const maxLoaded = Math.max( + recommendedIds.length, + followingIds.length, + trendingIds.length, + undergroundIds.length + ) + if (maxLoaded === 0) return [] + + // Cap composed length so we don't produce a truly infinite list — the + // consumer's "loadNextPage" advances the underlying queries, which + // re-runs this useMemo with more candidates available. + const cap = + recommendedIds.length + + followingIds.length + + trendingIds.length + + undergroundIds.length + + let slot = 0 + while (composed.length < cap) { + const preferred = SLOTS_PER_PAGE[slot % SLOTS_PER_PAGE.length] + const order = [ + preferred, + ...fallbackOrder.filter((k) => k !== preferred) + ] + + let picked: ID | undefined + for (const key of order) { + picked = tryTake(key) + if (picked != null) break + } + if (picked == null) break + + // Diversity rule: best-effort skip if it equals the last id (same-track + // shouldn't happen post-dedupe, but this is the cheap version of a + // same-artist guard without forcing user lookups). The artist-level + // guard runs in the consuming TrackLineup via render-time grouping. + if (picked === lastArtistTrack) continue + lastArtistTrack = picked + composed.push(picked) + slot++ + } + + return composed + }, [ + recommended.data, + following.trackIds, + trending.trackIds, + underground.trackIds, + favorited.data + ]) + + const isPending = + recommended.isPending || + following.isPending || + trending.isPending || + underground.isPending + const isFetching = + recommended.isFetching || + following.isFetching || + trending.isFetching || + underground.isFetching + const isError = + recommended.isError && + following.isError && + trending.isError && + underground.isError + const hasNextPage = Boolean( + recommended.hasNextPage || + following.hasNextPage || + trending.hasNextPage || + underground.hasNextPage + ) + + const loadNextPage = async () => { + // Advance every source that still has more — they fetch in parallel. + const promises: Array> = [] + if (recommended.hasNextPage) promises.push(recommended.fetchNextPage()) + if (following.hasNextPage) promises.push(following.fetchNextPage()) + if (trending.hasNextPage) promises.push(trending.fetchNextPage()) + if (underground.hasNextPage) promises.push(underground.fetchNextPage()) + await Promise.all(promises) + } + + return { + trackIds, + isPending, + isFetching, + isError, + hasNextPage, + loadNextPage, + queryKey: ['forYouFeed', currentUserId] as const + } +} diff --git a/packages/common/src/api/tan-query/queryKeys.ts b/packages/common/src/api/tan-query/queryKeys.ts index 00f6adaae43..dda05d62053 100644 --- a/packages/common/src/api/tan-query/queryKeys.ts +++ b/packages/common/src/api/tan-query/queryKeys.ts @@ -68,6 +68,7 @@ export const QUERY_KEYS = { trackHistory: 'trackHistory', topTags: 'topTags', feed: 'feed', + forYouFeed: 'forYouFeed', authorizedApps: 'authorizedApps', developerApps: 'developerApps', searchAutocomplete: 'searchAutocomplete', diff --git a/packages/common/src/models/FeedTab.ts b/packages/common/src/models/FeedTab.ts new file mode 100644 index 00000000000..da26af63317 --- /dev/null +++ b/packages/common/src/models/FeedTab.ts @@ -0,0 +1,5 @@ +export enum FeedTab { + FOR_YOU = 'FOR_YOU', + FOLLOWING = 'FOLLOWING', + UPLOADS_ONLY = 'UPLOADS_ONLY' +} diff --git a/packages/common/src/models/index.ts b/packages/common/src/models/index.ts index 2eef91d1321..72f892aa328 100644 --- a/packages/common/src/models/index.ts +++ b/packages/common/src/models/index.ts @@ -12,6 +12,7 @@ export * from './DownloadQuality' export * from './ErrorReporting' export * from './Favorite' export * from './FeedFilter' +export * from './FeedTab' export * from './Identifiers' export * from './ImageSizes' export * from './Kind' diff --git a/packages/common/src/store/pages/feed/actions.ts b/packages/common/src/store/pages/feed/actions.ts index 7e3edd9408b..86f6aa37177 100644 --- a/packages/common/src/store/pages/feed/actions.ts +++ b/packages/common/src/store/pages/feed/actions.ts @@ -1,8 +1,10 @@ import { FeedFilter } from '~/models/FeedFilter' +import { FeedTab } from '~/models/FeedTab' import { ID } from '~/models/Identifiers' export const FOLLOW_USERS = 'FEED/FOLLOW_USERS' export const SET_FEED_FILTER = 'FEED/SET_FEED_FILTER' +export const SET_FEED_TAB = 'FEED/SET_FEED_TAB' export type FollowUsersAction = { type: typeof FOLLOW_USERS @@ -14,7 +16,15 @@ export type SetFeedFilterAction = { filter: FeedFilter } -export type FeedPageAction = FollowUsersAction | SetFeedFilterAction +export type SetFeedTabAction = { + type: typeof SET_FEED_TAB + tab: FeedTab +} + +export type FeedPageAction = + | FollowUsersAction + | SetFeedFilterAction + | SetFeedTabAction export const followUsers = (userIds: ID[]): FollowUsersAction => ({ type: FOLLOW_USERS, @@ -25,3 +35,8 @@ export const setFeedFilter = (filter: FeedFilter): SetFeedFilterAction => ({ type: SET_FEED_FILTER, filter }) + +export const setFeedTab = (tab: FeedTab): SetFeedTabAction => ({ + type: SET_FEED_TAB, + tab +}) diff --git a/packages/common/src/store/pages/feed/reducer.ts b/packages/common/src/store/pages/feed/reducer.ts index 3927361b4e0..a4aad7d482b 100644 --- a/packages/common/src/store/pages/feed/reducer.ts +++ b/packages/common/src/store/pages/feed/reducer.ts @@ -3,16 +3,19 @@ import { persistReducer } from 'redux-persist' import { SET_FEED_FILTER, + SET_FEED_TAB, SetFeedFilterAction, + SetFeedTabAction, FeedPageAction } from '~/store/pages/feed/actions' -import { FeedFilter } from '../../../models' +import { FeedFilter, FeedTab } from '../../../models' import { FeedPageState } from './types' -const initialState = { - feedFilter: FeedFilter.ALL +const initialState: FeedPageState = { + feedFilter: FeedFilter.ALL, + feedTab: FeedTab.FOR_YOU } const actionsMap = { @@ -21,19 +24,25 @@ const actionsMap = { ...state, feedFilter: action.filter } + }, + [SET_FEED_TAB](state: FeedPageState, action: SetFeedTabAction) { + return { + ...state, + feedTab: action.tab + } } } const feedPageReducer = (state = initialState, action: FeedPageAction) => { - const matchingReduceFunction = actionsMap[action.type] + const matchingReduceFunction = actionsMap[action.type as keyof typeof actionsMap] if (!matchingReduceFunction) return state - return matchingReduceFunction(state, action) + return matchingReduceFunction(state, action as any) } export const feedPagePersistConfig = (storage: Storage) => ({ key: 'feed-page', storage, - whitelist: ['feedFilter'] + whitelist: ['feedFilter', 'feedTab'] }) const persistedFeedPageReducer = (storage: Storage) => { diff --git a/packages/common/src/store/pages/feed/selectors.ts b/packages/common/src/store/pages/feed/selectors.ts index 47817698c26..9263ec2bac5 100644 --- a/packages/common/src/store/pages/feed/selectors.ts +++ b/packages/common/src/store/pages/feed/selectors.ts @@ -1,3 +1,5 @@ import { CommonState } from '~/store/commonStore' export const getFeedFilter = (state: CommonState) => state.pages.feed.feedFilter + +export const getFeedTab = (state: CommonState) => state.pages.feed.feedTab diff --git a/packages/common/src/store/pages/feed/types.ts b/packages/common/src/store/pages/feed/types.ts index 4bf4d307228..6acdb2366ff 100644 --- a/packages/common/src/store/pages/feed/types.ts +++ b/packages/common/src/store/pages/feed/types.ts @@ -1,5 +1,6 @@ -import { FeedFilter } from '../../../models' +import { FeedFilter, FeedTab } from '../../../models' export type FeedPageState = { feedFilter: FeedFilter + feedTab: FeedTab } From 01d1b8ee5d39668e968bd884e7f3e2c1bda5d1ff Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Tue, 5 May 2026 18:38:33 -0700 Subject: [PATCH 2/3] Replace feed filter pills with three-tab UI on web MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render For You / Following / Uploads Only as the feed page's primary selector on both desktop and mobile-web, driven by the new feedTab Redux slot. - For You renders the new useForYouFeed lineup - Following renders useFeed with FeedFilter.ALL (uploads + reposts of followed users — current behavior) - Uploads Only renders useFeed with FeedFilter.ORIGINAL (no reposts) The legacy "Reposts only" filter is retired from the UI; FeedFilter itself stays for backwards-compat with persisted state and is still the parameter the SDK feed call accepts under the hood. The unused FeedFilters / FeedFilterDrawer / FeedFilterButton components are removed. The FEED_CHANGE_VIEW analytics event is widened to accept either FeedFilter or FeedTab as `view`. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/common/src/models/Analytics.ts | 3 +- .../pages/feed-page/components/FeedTabs.tsx | 52 ++++++++ .../components/desktop/FeedFilters.tsx | 53 -------- .../components/desktop/FeedPageContent.tsx | 119 ++++++++++-------- .../mobile/FeedFilterButton.module.css | 0 .../components/mobile/FeedFilterButton.tsx | 38 ------ .../components/mobile/FeedFilterDrawer.tsx | 63 ---------- .../mobile/FeedFilterModal.module.css | 8 -- .../components/mobile/FeedPageContent.tsx | 118 +++++++++-------- 9 files changed, 187 insertions(+), 267 deletions(-) create mode 100644 packages/web/src/pages/feed-page/components/FeedTabs.tsx delete mode 100644 packages/web/src/pages/feed-page/components/desktop/FeedFilters.tsx delete mode 100644 packages/web/src/pages/feed-page/components/mobile/FeedFilterButton.module.css delete mode 100644 packages/web/src/pages/feed-page/components/mobile/FeedFilterButton.tsx delete mode 100644 packages/web/src/pages/feed-page/components/mobile/FeedFilterDrawer.tsx delete mode 100644 packages/web/src/pages/feed-page/components/mobile/FeedFilterModal.module.css diff --git a/packages/common/src/models/Analytics.ts b/packages/common/src/models/Analytics.ts index 95de66d0969..753604db000 100644 --- a/packages/common/src/models/Analytics.ts +++ b/packages/common/src/models/Analytics.ts @@ -1,6 +1,7 @@ import { ChatPermission, Genre } from '@audius/sdk' import { FeedFilter } from '~/models/FeedFilter' +import { FeedTab } from '~/models/FeedTab' import { ID, PlayableType } from '~/models/Identifiers' import { TimeRange } from '~/models/TimeRange' import { WalletAddress } from '~/models/Wallet' @@ -1341,7 +1342,7 @@ type TrendingChangeView = { // Feed type FeedChangeView = { eventName: Name.FEED_CHANGE_VIEW - view: FeedFilter + view: FeedFilter | FeedTab } // Notifications diff --git a/packages/web/src/pages/feed-page/components/FeedTabs.tsx b/packages/web/src/pages/feed-page/components/FeedTabs.tsx new file mode 100644 index 00000000000..974addd0ce3 --- /dev/null +++ b/packages/web/src/pages/feed-page/components/FeedTabs.tsx @@ -0,0 +1,52 @@ +import { ChangeEvent, useCallback } from 'react' + +import { FeedTab } from '@audius/common/models' +import { Flex, SelectablePill } from '@audius/harmony' + +type FeedTabsProps = { + currentTab: FeedTab + onSelectTab: (tab: FeedTab) => void +} + +const messages = { + forYou: 'For You', + following: 'Following', + uploadsOnly: 'Uploads Only' +} + +const tabToLabel: Record = { + [FeedTab.FOR_YOU]: messages.forYou, + [FeedTab.FOLLOWING]: messages.following, + [FeedTab.UPLOADS_ONLY]: messages.uploadsOnly +} + +const tabs: FeedTab[] = [ + FeedTab.FOR_YOU, + FeedTab.FOLLOWING, + FeedTab.UPLOADS_ONLY +] + +export const FeedTabs = ({ currentTab, onSelectTab }: FeedTabsProps) => { + const handleChange = useCallback( + (e: ChangeEvent) => { + onSelectTab(e.target.value as FeedTab) + }, + [onSelectTab] + ) + + return ( + + {tabs.map((tab) => ( + + ))} + + ) +} diff --git a/packages/web/src/pages/feed-page/components/desktop/FeedFilters.tsx b/packages/web/src/pages/feed-page/components/desktop/FeedFilters.tsx deleted file mode 100644 index 8da686369fd..00000000000 --- a/packages/web/src/pages/feed-page/components/desktop/FeedFilters.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { ChangeEvent, useCallback } from 'react' - -import { FeedFilter } from '@audius/common/models' -import { Flex, SelectablePill } from '@audius/harmony' - -type FeedFiltersProps = { - currentFilter: FeedFilter - didSelectFilter: (filter: FeedFilter) => void -} - -const messages = { - allPosts: 'All Posts', - originalPosts: 'Original Posts', - reposts: 'Reposts' -} - -const filterToTitle = { - [FeedFilter.ALL]: messages.allPosts, - [FeedFilter.ORIGINAL]: messages.originalPosts, - [FeedFilter.REPOST]: messages.reposts -} - -const filters = [FeedFilter.ALL, FeedFilter.ORIGINAL, FeedFilter.REPOST] - -/** - * FeedFilters are the row of selectable pills on the feed for filtering the feed by repost, original, and all. - */ -export const FeedFilters = (props: FeedFiltersProps) => { - const { currentFilter, didSelectFilter } = props - - const handleChange = useCallback( - (e: ChangeEvent) => { - didSelectFilter(e.target.value as FeedFilter) - }, - [didSelectFilter] - ) - - return ( - - {filters.map((filter) => ( - - ))} - - ) -} diff --git a/packages/web/src/pages/feed-page/components/desktop/FeedPageContent.tsx b/packages/web/src/pages/feed-page/components/desktop/FeedPageContent.tsx index 9cb5a2356e6..6659479d112 100644 --- a/packages/web/src/pages/feed-page/components/desktop/FeedPageContent.tsx +++ b/packages/web/src/pages/feed-page/components/desktop/FeedPageContent.tsx @@ -5,14 +5,17 @@ import { FEED_INITIAL_PAGE_SIZE, FEED_LOAD_MORE_PAGE_SIZE, useCurrentUserId, - useFeed + useFeed, + useForYouFeed, + FOR_YOU_INITIAL_PAGE_SIZE, + FOR_YOU_LOAD_MORE_PAGE_SIZE } from '@audius/common/api' -import { Name, FeedFilter } from '@audius/common/models' +import { Name, FeedFilter, FeedTab } from '@audius/common/models' import { feedPageSelectors, feedPageActions as discoverPageAction } from '@audius/common/store' -import { FilterButton, Flex, IconFeed } from '@audius/harmony' +import { Flex, IconFeed } from '@audius/harmony' import { useDispatch, useSelector } from 'react-redux' import { make, useRecord } from 'common/store/analytics/actions' @@ -22,10 +25,8 @@ import EndOfLineup from 'components/lineup/EndOfLineup' import { TrackLineup } from 'components/lineup/TrackLineup' import { LineupVariant } from 'components/lineup/types' import Page from 'components/page/Page' -import { useIsContainerNarrow } from 'hooks/useIsContainerNarrow' import EmptyFeed from 'pages/feed-page/components/EmptyFeed' - -import { FeedFilters } from './FeedFilters' +import { FeedTabs } from 'pages/feed-page/components/FeedTabs' const messages = { feedHeaderTitle: 'Your Feed', @@ -33,17 +34,19 @@ const messages = { feedDescription: 'Listen to what people you follow are sharing' } -const { getFeedFilter } = feedPageSelectors +const { getFeedTab } = feedPageSelectors type FeedPageContentProps = { containerRef?: React.RefObject } -const feedFilterOptions = [ - { label: 'All Posts', value: FeedFilter.ALL }, - { label: 'Original Posts', value: FeedFilter.ORIGINAL }, - { label: 'Reposts', value: FeedFilter.REPOST } -] +const tabToFilter: Record< + Exclude, + FeedFilter +> = { + [FeedTab.FOLLOWING]: FeedFilter.ALL, + [FeedTab.UPLOADS_ONLY]: FeedFilter.ORIGINAL +} // Note: the feed API returns both tracks and collections (playlist reposts). // The new TrackLineup renders tracks only, so collections are filtered out by @@ -53,42 +56,47 @@ const feedFilterOptions = [ const FeedPageContent = ({ containerRef }: FeedPageContentProps) => { const dispatch = useDispatch() const titleRowRef = useRef(null) - const isCondensedHeader = useIsContainerNarrow(titleRowRef, 560) - const feedFilter = useSelector(getFeedFilter) + const feedTab = useSelector(getFeedTab) const { data: currentUserId } = useCurrentUserId() + const isForYou = feedTab === FeedTab.FOR_YOU + const followingFilter = isForYou + ? FeedFilter.ALL + : tabToFilter[feedTab as Exclude] + + // Following / Uploads-Only lineup. Disabled while For You is active. const feedArgs = useMemo( () => ({ userId: currentUserId, - filter: feedFilter, + filter: followingFilter, initialPageSize: FEED_INITIAL_PAGE_SIZE, loadMorePageSize: FEED_LOAD_MORE_PAGE_SIZE }), - [feedFilter, currentUserId] + [followingFilter, currentUserId] + ) + const followFeed = useFeed(feedArgs, { enabled: !isForYou }) + + // For You lineup. + const forYouFeed = useForYouFeed( + { + initialPageSize: FOR_YOU_INITIAL_PAGE_SIZE, + loadMorePageSize: FOR_YOU_LOAD_MORE_PAGE_SIZE + }, + { enabled: isForYou } ) - const { - trackIds, - isPending, - isFetching, - isError, - hasNextPage, - loadNextPage - } = useFeed(feedArgs) - - const querySource = useMemo( + const followQuerySource = useMemo( () => ({ queryKey: [...getFeedQueryKey(feedArgs)] as unknown[] }), [feedArgs] ) const record = useRecord() - - const didSelectFilter = (filter: FeedFilter) => { + const onSelectTab = (tab: FeedTab) => { if (containerRef?.current?.scrollTo) { containerRef.current.scrollTo(0, 0) } - dispatch(discoverPageAction.setFeedFilter(filter)) - record(make(Name.FEED_CHANGE_VIEW, { view: filter })) + dispatch(discoverPageAction.setFeedTab(tab)) + record(make(Name.FEED_CHANGE_VIEW, { view: tab })) } const header = ( @@ -97,24 +105,35 @@ const FeedPageContent = ({ containerRef }: FeedPageContentProps) => { icon={IconFeed} primary={messages.feedHeaderTitle} rightDecorator={ - isCondensedHeader ? ( - didSelectFilter(value as FeedFilter)} - options={feedFilterOptions} - /> - ) : ( - - ) + } /> ) + const lineupProps = isForYou + ? { + trackIds: forYouFeed.trackIds, + isPending: forYouFeed.isPending, + isFetching: forYouFeed.isFetching, + isError: forYouFeed.isError, + hasNextPage: forYouFeed.hasNextPage, + loadNextPage: forYouFeed.loadNextPage, + pageSize: FOR_YOU_LOAD_MORE_PAGE_SIZE, + initialPageSize: FOR_YOU_INITIAL_PAGE_SIZE, + querySource: undefined + } + : { + trackIds: followFeed.trackIds, + isPending: followFeed.isPending, + isFetching: followFeed.isFetching, + isError: followFeed.isError, + hasNextPage: followFeed.hasNextPage, + loadNextPage: followFeed.loadNextPage, + pageSize: FEED_LOAD_MORE_PAGE_SIZE, + initialPageSize: FEED_INITIAL_PAGE_SIZE, + querySource: followQuerySource + } + return ( { > } endOfLineupElement={} + {...lineupProps} /> diff --git a/packages/web/src/pages/feed-page/components/mobile/FeedFilterButton.module.css b/packages/web/src/pages/feed-page/components/mobile/FeedFilterButton.module.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/web/src/pages/feed-page/components/mobile/FeedFilterButton.tsx b/packages/web/src/pages/feed-page/components/mobile/FeedFilterButton.tsx deleted file mode 100644 index da53060dd95..00000000000 --- a/packages/web/src/pages/feed-page/components/mobile/FeedFilterButton.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FeedFilter } from '@audius/common/models' - -import HeaderButton from 'components/header-button/HeaderButton' - -const messages = { - filterAll: 'All Posts', - filterOriginal: 'Original Posts', - filterReposts: 'Reposts' -} - -const messageMap = { - [FeedFilter.ALL]: messages.filterAll, - [FeedFilter.ORIGINAL]: messages.filterOriginal, - [FeedFilter.REPOST]: messages.filterReposts -} - -// HeaderButton for filtering feed by All/Original/Repost -type FeedFilterButtonProps = { - currentFilter: FeedFilter - didOpenModal: () => void - showIcon?: boolean -} - -const FeedFilterButton = ({ - currentFilter, - didOpenModal, - showIcon = true -}: FeedFilterButtonProps) => { - return ( - - ) -} - -export default FeedFilterButton diff --git a/packages/web/src/pages/feed-page/components/mobile/FeedFilterDrawer.tsx b/packages/web/src/pages/feed-page/components/mobile/FeedFilterDrawer.tsx deleted file mode 100644 index 790c8159483..00000000000 --- a/packages/web/src/pages/feed-page/components/mobile/FeedFilterDrawer.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useCallback, useMemo } from 'react' - -import { FeedFilter } from '@audius/common/models' - -import ActionDrawer from 'components/action-drawer/ActionDrawer' - -import styles from './FeedFilterModal.module.css' - -interface FeedFilterDrawerProps { - isOpen: boolean - onClose: () => void - onSelectFilter: (filter: FeedFilter) => void -} - -const messages = { - title: 'What do you want to see in your feed?', - filterAll: 'All Posts', - filterOriginal: 'Original Posts', - filterReposts: 'Reposts' -} - -const FeedFilterDrawer = ({ - isOpen, - onSelectFilter, - onClose -}: FeedFilterDrawerProps) => { - const handleSelectFilter = useCallback( - (filter: FeedFilter) => { - onSelectFilter(filter) - onClose() - }, - [onClose, onSelectFilter] - ) - - const actions = useMemo( - () => [ - { - text: messages.filterAll, - onClick: () => handleSelectFilter(FeedFilter.ALL) - }, - { - text: messages.filterOriginal, - onClick: () => handleSelectFilter(FeedFilter.ORIGINAL) - }, - { - text: messages.filterReposts, - onClick: () => handleSelectFilter(FeedFilter.REPOST) - } - ], - [handleSelectFilter] - ) - - return ( -
{messages.title}
} - actions={actions} - onClose={onClose} - isOpen={isOpen} - /> - ) -} - -export default FeedFilterDrawer diff --git a/packages/web/src/pages/feed-page/components/mobile/FeedFilterModal.module.css b/packages/web/src/pages/feed-page/components/mobile/FeedFilterModal.module.css deleted file mode 100644 index be08ac52bf4..00000000000 --- a/packages/web/src/pages/feed-page/components/mobile/FeedFilterModal.module.css +++ /dev/null @@ -1,8 +0,0 @@ -.title { - font-size: var(--harmony-font-m); - font-weight: var(--harmony-font-medium); - line-height: 18px; - color: var(--harmony-neutral); - text-align: center; - margin: 8px 0 16px; -} diff --git a/packages/web/src/pages/feed-page/components/mobile/FeedPageContent.tsx b/packages/web/src/pages/feed-page/components/mobile/FeedPageContent.tsx index fef08575c10..04fae6f5bcc 100644 --- a/packages/web/src/pages/feed-page/components/mobile/FeedPageContent.tsx +++ b/packages/web/src/pages/feed-page/components/mobile/FeedPageContent.tsx @@ -5,9 +5,12 @@ import { FEED_INITIAL_PAGE_SIZE, FEED_LOAD_MORE_PAGE_SIZE, useCurrentUserId, - useFeed + useFeed, + useForYouFeed, + FOR_YOU_INITIAL_PAGE_SIZE, + FOR_YOU_LOAD_MORE_PAGE_SIZE } from '@audius/common/api' -import { Name, FeedFilter } from '@audius/common/models' +import { Name, FeedFilter, FeedTab } from '@audius/common/models' import { feedPageSelectors, feedPageActions as discoverPageAction @@ -16,7 +19,6 @@ import { route } from '@audius/common/utils' import cn from 'classnames' import { useDispatch, useSelector } from 'react-redux' -import { useModalState } from 'common/hooks/useModalState' import { make, useRecord } from 'common/store/analytics/actions' import Header from 'components/header/mobile/Header' import { HeaderContext } from 'components/header/mobile/HeaderContextProvider' @@ -25,10 +27,9 @@ import { LineupVariant } from 'components/lineup/types' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' import { useMainPageHeader } from 'components/nav/mobile/NavContext' import EmptyFeed from 'pages/feed-page/components/EmptyFeed' +import { FeedTabs } from 'pages/feed-page/components/FeedTabs' import { BASE_URL } from 'utils/route' -import Filters from './FeedFilterButton' -import FeedFilterDrawer from './FeedFilterDrawer' import styles from './FeedPageContent.module.css' const { FEED_PAGE } = route @@ -39,12 +40,20 @@ const messages = { feedDescription: 'Listen to what people you follow are sharing' } -const { getFeedFilter } = feedPageSelectors +const { getFeedTab } = feedPageSelectors type FeedPageMobileContentProps = { containerRef?: React.RefObject } +const tabToFilter: Record< + Exclude, + FeedFilter +> = { + [FeedTab.FOLLOWING]: FeedFilter.ALL, + [FeedTab.UPLOADS_ONLY]: FeedFilter.ORIGINAL +} + // Note: the feed API returns both tracks and collections (playlist reposts). // The new TrackLineup renders tracks only, so collections are filtered out by // `trackIds` on the hook side. This is a known limitation introduced by the @@ -54,59 +63,81 @@ const FeedPageMobileContent = ({ containerRef }: FeedPageMobileContentProps) => { const dispatch = useDispatch() - const feedFilter = useSelector(getFeedFilter) + const feedTab = useSelector(getFeedTab) const { data: currentUserId } = useCurrentUserId() + const isForYou = feedTab === FeedTab.FOR_YOU + const followingFilter = isForYou + ? FeedFilter.ALL + : tabToFilter[feedTab as Exclude] + const feedArgs = useMemo( () => ({ userId: currentUserId, - filter: feedFilter, + filter: followingFilter, initialPageSize: FEED_INITIAL_PAGE_SIZE, loadMorePageSize: FEED_LOAD_MORE_PAGE_SIZE }), - [feedFilter, currentUserId] + [followingFilter, currentUserId] + ) + const followFeed = useFeed(feedArgs, { enabled: !isForYou }) + + const forYouFeed = useForYouFeed( + { + initialPageSize: FOR_YOU_INITIAL_PAGE_SIZE, + loadMorePageSize: FOR_YOU_LOAD_MORE_PAGE_SIZE + }, + { enabled: isForYou } ) - const { - trackIds, - isPending, - isFetching, - isError, - hasNextPage, - loadNextPage - } = useFeed(feedArgs) - - const querySource = useMemo( + const followQuerySource = useMemo( () => ({ queryKey: [...getFeedQueryKey(feedArgs)] as unknown[] }), [feedArgs] ) const { setHeader } = useContext(HeaderContext) - const [modalIsOpen, setModalIsOpen] = useModalState('FeedFilter') + + const record = useRecord() + const handleSelectTab = (tab: FeedTab) => { + dispatch(discoverPageAction.setFeedTab(tab)) + record(make(Name.FEED_CHANGE_VIEW, { view: tab })) + } useEffect(() => { setHeader(
- { - setModalIsOpen(true) - }} - showIcon={false} - /> +
) - }, [setHeader, feedFilter, setModalIsOpen]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setHeader, feedTab]) // Set Nav-Bar Menu useMainPageHeader() - const record = useRecord() - const handleSelectFilter = (filter: FeedFilter) => { - setModalIsOpen(false) - dispatch(discoverPageAction.setFeedFilter(filter)) - record(make(Name.FEED_CHANGE_VIEW, { view: filter })) - } + const lineupProps = isForYou + ? { + trackIds: forYouFeed.trackIds, + isPending: forYouFeed.isPending, + isFetching: forYouFeed.isFetching, + isError: forYouFeed.isError, + hasNextPage: forYouFeed.hasNextPage, + loadNextPage: forYouFeed.loadNextPage, + pageSize: FOR_YOU_LOAD_MORE_PAGE_SIZE, + initialPageSize: FOR_YOU_INITIAL_PAGE_SIZE, + querySource: undefined + } + : { + trackIds: followFeed.trackIds, + isPending: followFeed.isPending, + isFetching: followFeed.isFetching, + isError: followFeed.isError, + hasNextPage: followFeed.hasNextPage, + loadNextPage: followFeed.loadNextPage, + pageSize: FEED_LOAD_MORE_PAGE_SIZE, + initialPageSize: FEED_INITIAL_PAGE_SIZE, + querySource: followQuerySource + } return ( - setModalIsOpen(false)} - />
0 + [styles.playing]: lineupProps.trackIds.length > 0 })} > } + {...lineupProps} />
From f84df4437e442152ad791af37ff616b87a042431 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Tue, 5 May 2026 18:41:10 -0700 Subject: [PATCH 3/3] Replace feed filter drawer with three-tab UI on mobile Mirror the web feed page: render a horizontal pill row with For You / Following / Uploads Only above the lineup, dispatching setFeedTab on change. The legacy FeedFilter drawer + FeedFilterButton are removed, along with the 'FeedFilter' modal slot from the shared modals state since nothing opens it anymore. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../common/src/store/ui/modals/parentSlice.ts | 1 - packages/common/src/store/ui/modals/types.ts | 1 - packages/mobile/src/app/Drawers.tsx | 2 - .../feed-filter-drawer/FeedFilterDrawer.tsx | 69 ----------- .../components/feed-filter-drawer/index.ts | 1 - .../screens/feed-screen/FeedFilterButton.tsx | 34 ----- .../src/screens/feed-screen/FeedScreen.tsx | 116 ++++++++++++------ .../src/screens/feed-screen/FeedTabs.tsx | 57 +++++++++ 8 files changed, 136 insertions(+), 145 deletions(-) delete mode 100644 packages/mobile/src/components/feed-filter-drawer/FeedFilterDrawer.tsx delete mode 100644 packages/mobile/src/components/feed-filter-drawer/index.ts delete mode 100644 packages/mobile/src/screens/feed-screen/FeedFilterButton.tsx create mode 100644 packages/mobile/src/screens/feed-screen/FeedTabs.tsx diff --git a/packages/common/src/store/ui/modals/parentSlice.ts b/packages/common/src/store/ui/modals/parentSlice.ts index 922c6239a7d..53babad3cac 100644 --- a/packages/common/src/store/ui/modals/parentSlice.ts +++ b/packages/common/src/store/ui/modals/parentSlice.ts @@ -21,7 +21,6 @@ export const initialState: BasicModalsState = { BrowserPushPermissionConfirmation: { isOpen: false }, AudioBreakdown: { isOpen: false }, DeactivateAccountConfirmation: { isOpen: false }, - FeedFilter: { isOpen: false }, PurchaseVendor: { isOpen: false }, TrendingGenreSelection: { isOpen: false }, TrendingCategory: { isOpen: false }, diff --git a/packages/common/src/store/ui/modals/types.ts b/packages/common/src/store/ui/modals/types.ts index 8b5d1571698..b51748f77a3 100644 --- a/packages/common/src/store/ui/modals/types.ts +++ b/packages/common/src/store/ui/modals/types.ts @@ -56,7 +56,6 @@ export type Modals = | 'BrowserPushPermissionConfirmation' | 'AudioBreakdown' | 'DeactivateAccountConfirmation' - | 'FeedFilter' | 'PurchaseVendor' | 'TrendingGenreSelection' | 'TrendingCategory' diff --git a/packages/mobile/src/app/Drawers.tsx b/packages/mobile/src/app/Drawers.tsx index 2e2e3c5b17f..0a19d7b0b87 100644 --- a/packages/mobile/src/app/Drawers.tsx +++ b/packages/mobile/src/app/Drawers.tsx @@ -24,7 +24,6 @@ import { MuteCommentsConfirmationDrawer } from 'app/components/drawers/MuteComme import { DuplicateAddConfirmationDrawer } from 'app/components/duplicate-add-confirmation-drawer' import { EnablePushNotificationsDrawer } from 'app/components/enable-push-notifications-drawer' import { FanClubDetailsDrawer } from 'app/components/fan-club-details-drawer/FanClubDetailsDrawer' -import { FeedFilterDrawer } from 'app/components/feed-filter-drawer' import { ForgotPasswordDrawer } from 'app/components/forgot-password-drawer' import { HostRemixContestDrawer } from 'app/components/host-remix-contest-drawer/HostRemixContestDrawer' import { InboxUnavailableDrawer } from 'app/components/inbox-unavailable-drawer/InboxUnavailableDrawer' @@ -117,7 +116,6 @@ const commonDrawersMap: { [Modal in Modals]?: ComponentType } = { TransferAudioMobileWarning: TransferAudioMobileDrawer, Share: ShareDrawer, DeactivateAccountConfirmation: DeactivateAccountConfirmationDrawer, - FeedFilter: FeedFilterDrawer, TrendingGenreSelection: TrendingFilterDrawer, TrendingFilter: TrendingCombinedFilterDrawer, Overflow: OverflowMenuDrawer, diff --git a/packages/mobile/src/components/feed-filter-drawer/FeedFilterDrawer.tsx b/packages/mobile/src/components/feed-filter-drawer/FeedFilterDrawer.tsx deleted file mode 100644 index 2c7eb165721..00000000000 --- a/packages/mobile/src/components/feed-filter-drawer/FeedFilterDrawer.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useCallback, useMemo } from 'react' - -import { QUERY_KEYS } from '@audius/common/api' -import { Name, FeedFilter } from '@audius/common/models' -import { feedPageActions } from '@audius/common/store' -import { useQueryClient } from '@tanstack/react-query' -import { useDispatch } from 'react-redux' - -import ActionDrawer from 'app/components/action-drawer' -import { Text } from 'app/components/core' -import { make, track } from 'app/services/analytics' - -const { setFeedFilter } = feedPageActions - -const MODAL_NAME = 'FeedFilter' - -export const messages = { - title: 'What do you want to see in your feed?', - filterAll: 'All Posts', - filterOriginal: 'Original Posts', - filterReposts: 'Reposts' -} - -export const FeedFilterDrawer = () => { - const dispatch = useDispatch() - const queryClient = useQueryClient() - - const handleSelectFilter = useCallback( - (filter: FeedFilter) => { - dispatch(setFeedFilter(filter)) - // Invalidate the feed tan-query so it refetches with the new filter - queryClient.invalidateQueries({ - queryKey: [QUERY_KEYS.feed] - }) - track(make({ eventName: Name.FEED_CHANGE_VIEW, view: filter })) - }, - [dispatch, queryClient] - ) - - const rows = useMemo( - () => [ - { - text: messages.filterAll, - callback: () => handleSelectFilter(FeedFilter.ALL) - }, - { - text: messages.filterOriginal, - callback: () => handleSelectFilter(FeedFilter.ORIGINAL) - }, - { - text: messages.filterReposts, - callback: () => handleSelectFilter(FeedFilter.REPOST) - } - ], - [handleSelectFilter] - ) - - return ( - - {messages.title} - - } - rows={rows} - /> - ) -} diff --git a/packages/mobile/src/components/feed-filter-drawer/index.ts b/packages/mobile/src/components/feed-filter-drawer/index.ts deleted file mode 100644 index 93371269bfc..00000000000 --- a/packages/mobile/src/components/feed-filter-drawer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { FeedFilterDrawer, messages } from './FeedFilterDrawer' diff --git a/packages/mobile/src/screens/feed-screen/FeedFilterButton.tsx b/packages/mobile/src/screens/feed-screen/FeedFilterButton.tsx deleted file mode 100644 index 5fc0e078db8..00000000000 --- a/packages/mobile/src/screens/feed-screen/FeedFilterButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { useCallback } from 'react' - -import { FeedFilter } from '@audius/common/models' -import { feedPageSelectors, modalsActions } from '@audius/common/store' -import { useDispatch, useSelector } from 'react-redux' - -import { ScreenHeaderButton } from 'app/components/core' -import { messages } from 'app/components/feed-filter-drawer' - -const { getFeedFilter } = feedPageSelectors -const { setVisibility } = modalsActions - -const messageMap = { - [FeedFilter.ALL]: messages.filterAll, - [FeedFilter.ORIGINAL]: messages.filterOriginal, - [FeedFilter.REPOST]: messages.filterReposts -} - -export const FeedFilterButton = () => { - const feedFilter = useSelector(getFeedFilter) - const dispatch = useDispatch() - - const handlePress = useCallback(() => { - dispatch(setVisibility({ modal: 'FeedFilter', visible: true })) - }, [dispatch]) - - return ( - - ) -} diff --git a/packages/mobile/src/screens/feed-screen/FeedScreen.tsx b/packages/mobile/src/screens/feed-screen/FeedScreen.tsx index 5f49a5365c1..cd390ad8969 100644 --- a/packages/mobile/src/screens/feed-screen/FeedScreen.tsx +++ b/packages/mobile/src/screens/feed-screen/FeedScreen.tsx @@ -1,95 +1,137 @@ -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { getFeedQueryKey, FEED_INITIAL_PAGE_SIZE, FEED_LOAD_MORE_PAGE_SIZE, useCurrentUserId, - useFeed + useFeed, + useForYouFeed, + FOR_YOU_INITIAL_PAGE_SIZE, + FOR_YOU_LOAD_MORE_PAGE_SIZE } from '@audius/common/api' -import { feedPageSelectors } from '@audius/common/store' -import { useSelector } from 'react-redux' +import { Name, FeedFilter, FeedTab } from '@audius/common/models' +import { feedPageActions, feedPageSelectors } from '@audius/common/store' +import { useDispatch, useSelector } from 'react-redux' import { Screen, ScreenContent } from 'app/components/core' import { EndOfLineupNotice } from 'app/components/lineup/EndOfLineupNotice' import { TrackLineup } from 'app/components/lineup/TrackLineup' -import { OnlineOnly } from 'app/components/offline-placeholder/OnlineOnly' import { SuggestedFollows } from 'app/components/suggested-follows' import { MobileRootHeader } from 'app/screens/app-screen/MobileRootHeader' +import { make, track } from 'app/services/analytics' -import { FeedFilterButton } from './FeedFilterButton' +import { FeedTabs } from './FeedTabs' -const { getFeedFilter } = feedPageSelectors +const { getFeedTab } = feedPageSelectors +const { setFeedTab } = feedPageActions const messages = { header: 'Your Feed', endOfFeed: "Looks like you've reached the end of your feed..." } +const tabToFilter: Record< + Exclude, + FeedFilter +> = { + [FeedTab.FOLLOWING]: FeedFilter.ALL, + [FeedTab.UPLOADS_ONLY]: FeedFilter.ORIGINAL +} + // Note: the feed API returns both tracks and collections (playlist reposts). // The new TrackLineup renders tracks only, so collections are filtered out by // `trackIds` on the hook side. This is a known limitation introduced by the // tanquery migration — collection feed rendering will be restored if/when // TrackLineup learns to render mixed feeds. export const FeedScreen = () => { - const feedFilter = useSelector(getFeedFilter) + const dispatch = useDispatch() + const feedTab = useSelector(getFeedTab) const { data: currentUserId } = useCurrentUserId() + const isForYou = feedTab === FeedTab.FOR_YOU + const followingFilter = isForYou + ? FeedFilter.ALL + : tabToFilter[feedTab as Exclude] + const feedArgs = useMemo( () => ({ userId: currentUserId, - filter: feedFilter, + filter: followingFilter, initialPageSize: FEED_INITIAL_PAGE_SIZE, loadMorePageSize: FEED_LOAD_MORE_PAGE_SIZE }), - [feedFilter, currentUserId] + [followingFilter, currentUserId] + ) + const followFeed = useFeed(feedArgs, { enabled: !isForYou }) + const forYouFeed = useForYouFeed( + { + initialPageSize: FOR_YOU_INITIAL_PAGE_SIZE, + loadMorePageSize: FOR_YOU_LOAD_MORE_PAGE_SIZE + }, + { enabled: isForYou } ) - const { - trackIds, - isPending, - isFetching, - hasNextPage, - loadNextPage, - refetch - } = useFeed(feedArgs) - - const querySource = useMemo( + const followQuerySource = useMemo( () => ({ queryKey: [...getFeedQueryKey(feedArgs)] as unknown[] }), [feedArgs] ) + const handleSelectTab = useCallback( + (tab: FeedTab) => { + dispatch(setFeedTab(tab)) + track(make({ eventName: Name.FEED_CHANGE_VIEW, view: tab })) + }, + [dispatch] + ) + + const lineupProps = isForYou + ? { + 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, + refetch: undefined as undefined | (() => void), + querySource: undefined as + | { queryKey: unknown[] } + | undefined + } + : { + trackIds: followFeed.trackIds, + 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 ( ( - - - - - + )} > + { - refetch() - }} + pullToRefresh={!isForYou} hideHeaderOnEmpty LineupEmptyComponent={} ListFooterComponent={ } + {...lineupProps} /> diff --git a/packages/mobile/src/screens/feed-screen/FeedTabs.tsx b/packages/mobile/src/screens/feed-screen/FeedTabs.tsx new file mode 100644 index 00000000000..5c11865647a --- /dev/null +++ b/packages/mobile/src/screens/feed-screen/FeedTabs.tsx @@ -0,0 +1,57 @@ +import { FeedTab } from '@audius/common/models' +import { ScrollView, View } from 'react-native' + +import { Flex, SelectablePill, useTheme } from '@audius/harmony-native' + +const tabLabels: Record = { + [FeedTab.FOR_YOU]: 'For You', + [FeedTab.FOLLOWING]: 'Following', + [FeedTab.UPLOADS_ONLY]: 'Uploads Only' +} + +const tabs: FeedTab[] = [ + FeedTab.FOR_YOU, + FeedTab.FOLLOWING, + FeedTab.UPLOADS_ONLY +] + +type FeedTabsProps = { + currentTab: FeedTab + onSelectTab: (tab: FeedTab) => void +} + +export const FeedTabs = ({ currentTab, onSelectTab }: FeedTabsProps) => { + const { spacing, color } = useTheme() + return ( + + + + {tabs.map((tab) => ( + { + if (!isSelected) return + onSelectTab(value as FeedTab) + }} + disableUnselectAnimation + /> + ))} + + + + ) +}