From 5f59567d709c9cbb34443acad31c24f43fda9cc8 Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Fri, 5 Dec 2025 04:49:32 -0600 Subject: [PATCH] Discover Refactor (#752) * Discover Refactor tear down discover context provider add animations to rows in discover screen tidy up home screen refresh * handle case where teh discover screen is empty on initial load --- bun.lock | 1 + src/api/mutations/discover/index.ts | 23 +++ src/api/mutations/home/index.ts | 4 +- src/api/queries/playlist/index.ts | 14 ++ src/api/queries/suggestions/index.ts | 42 +++++ src/api/queries/suggestions/keys.ts | 4 + .../{ => suggestions/utils}/suggestions.ts | 2 +- src/components/Discover/component.tsx | 47 +++--- .../Discover/helpers/just-added.tsx | 86 +++++----- .../Discover/helpers/public-playlists.tsx | 112 +++++++------ .../Discover/helpers/suggested-artists.tsx | 86 +++++----- src/components/Search/index.tsx | 8 +- src/providers/Discover/index.tsx | 145 ----------------- src/screens/Discover/index.tsx | 149 +++++++++--------- 14 files changed, 339 insertions(+), 384 deletions(-) create mode 100644 src/api/mutations/discover/index.ts create mode 100644 src/api/queries/suggestions/index.ts create mode 100644 src/api/queries/suggestions/keys.ts rename src/api/queries/{ => suggestions/utils}/suggestions.ts (97%) delete mode 100644 src/providers/Discover/index.tsx diff --git a/bun.lock b/bun.lock index 6886cf7e..40899b94 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "jellify", diff --git a/src/api/mutations/discover/index.ts b/src/api/mutations/discover/index.ts new file mode 100644 index 00000000..94f3e960 --- /dev/null +++ b/src/api/mutations/discover/index.ts @@ -0,0 +1,23 @@ +import { useMutation } from '@tanstack/react-query' +import { useRecentlyAddedAlbums } from '../../queries/album' +import { usePublicPlaylists } from '../../queries/playlist' +import { useDiscoverArtists } from '../../queries/suggestions' + +const useDiscoverQueries = () => { + const { refetch: refetchRecentlyAdded } = useRecentlyAddedAlbums() + + const { refetch: refetchPublicPlaylists } = usePublicPlaylists() + + const { refetch: refetchArtistSuggestions } = useDiscoverArtists() + + return useMutation({ + mutationFn: async () => + await Promise.allSettled([ + refetchRecentlyAdded(), + refetchPublicPlaylists(), + refetchArtistSuggestions(), + ]), + }) +} + +export default useDiscoverQueries diff --git a/src/api/mutations/home/index.ts b/src/api/mutations/home/index.ts index fffce39f..d4cb6263 100644 --- a/src/api/mutations/home/index.ts +++ b/src/api/mutations/home/index.ts @@ -16,12 +16,12 @@ const useHomeQueries = () => { return useMutation({ mutationFn: async () => { - await Promise.all([ + await Promise.allSettled([ refetchRecentlyPlayed(), refetchFrequentlyPlayed(), refetchUserPlaylists(), ]) - await Promise.all([refetchFrequentArtists(), refetchRecentArtists()]) + await Promise.allSettled([refetchFrequentArtists(), refetchRecentArtists()]) return true }, }) diff --git a/src/api/queries/playlist/index.ts b/src/api/queries/playlist/index.ts index 0fc5708e..130a6f16 100644 --- a/src/api/queries/playlist/index.ts +++ b/src/api/queries/playlist/index.ts @@ -39,3 +39,17 @@ export const usePlaylistTracks = (playlist: BaseItemDto) => { }, }) } + +export const usePublicPlaylists = () => { + const api = useApi() + const [library] = useJellifyLibrary() + + return useInfiniteQuery({ + queryKey: [QueryKeys.PublicPlaylists, library?.playlistLibraryId], + queryFn: ({ pageParam }) => fetchPublicPlaylists(api, library, pageParam), + select: (data) => data.pages.flatMap((page) => page), + getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => + lastPage.length > 0 ? lastPageParam + 1 : undefined, + initialPageParam: 0, + }) +} diff --git a/src/api/queries/suggestions/index.ts b/src/api/queries/suggestions/index.ts new file mode 100644 index 00000000..7dec5e74 --- /dev/null +++ b/src/api/queries/suggestions/index.ts @@ -0,0 +1,42 @@ +import { useInfiniteQuery, useQuery } from '@tanstack/react-query' +import { SuggestionQueryKeys } from './keys' +import { fetchArtistSuggestions, fetchSearchSuggestions } from './utils/suggestions' +import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores' +import { isUndefined } from 'lodash' + +export const useSearchSuggestions = () => { + const api = useApi() + + const [library] = useJellifyLibrary() + + const [user] = useJellifyUser() + + return useQuery({ + queryKey: [SuggestionQueryKeys.SearchSuggestions, library?.musicLibraryId], + queryFn: () => fetchSearchSuggestions(api, user, library?.musicLibraryId), + enabled: !isUndefined(library), + }) +} + +export const useDiscoverArtists = () => { + const api = useApi() + + const [library] = useJellifyLibrary() + + const [user] = useJellifyUser() + + return useInfiniteQuery({ + queryKey: [ + SuggestionQueryKeys.InfiniteArtistSuggestions, + user?.id, + library?.musicLibraryId, + ], + queryFn: ({ pageParam }) => + fetchArtistSuggestions(api, user, library?.musicLibraryId, pageParam), + getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => + lastPage.length > 0 ? lastPageParam + 1 : undefined, + select: (data) => data.pages.flatMap((page) => page), + initialPageParam: 0, + maxPages: 2, + }) +} diff --git a/src/api/queries/suggestions/keys.ts b/src/api/queries/suggestions/keys.ts new file mode 100644 index 00000000..e3729a6f --- /dev/null +++ b/src/api/queries/suggestions/keys.ts @@ -0,0 +1,4 @@ +export enum SuggestionQueryKeys { + InfiniteArtistSuggestions, + SearchSuggestions, +} diff --git a/src/api/queries/suggestions.ts b/src/api/queries/suggestions/utils/suggestions.ts similarity index 97% rename from src/api/queries/suggestions.ts rename to src/api/queries/suggestions/utils/suggestions.ts index ace1ce76..b25af480 100644 --- a/src/api/queries/suggestions.ts +++ b/src/api/queries/suggestions/utils/suggestions.ts @@ -2,7 +2,7 @@ import { getArtistsApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api' import { BaseItemDto, BaseItemKind, ItemFields } from '@jellyfin/sdk/lib/generated-client/models' import { Api } from '@jellyfin/sdk' import { isUndefined } from 'lodash' -import { JellifyUser } from '../../types/JellifyUser' +import { JellifyUser } from '../../../../types/JellifyUser' /** * Fetches search suggestions from the Jellyfin server diff --git a/src/components/Discover/component.tsx b/src/components/Discover/component.tsx index 142e8d92..788869bf 100644 --- a/src/components/Discover/component.tsx +++ b/src/components/Discover/component.tsx @@ -1,18 +1,21 @@ import React from 'react' -import { getToken, ScrollView, useTheme, View, YStack } from 'tamagui' +import { getToken, ScrollView, useTheme, YStack } from 'tamagui' import RecentlyAdded from './helpers/just-added' -import { useDiscoverContext } from '../../providers/Discover' import { RefreshControl } from 'react-native' import PublicPlaylists from './helpers/public-playlists' import SuggestedArtists from './helpers/suggested-artists' +import useDiscoverQueries from '../../api/mutations/discover' +import { useIsRestoring } from '@tanstack/react-query' +import { useRecentlyAddedAlbums } from '../../api/queries/album' export default function Index(): React.JSX.Element { const theme = useTheme() - const { refreshing, refresh, publicPlaylists, suggestedArtistsInfiniteQuery } = - useDiscoverContext() + const { mutateAsync: refreshAsync, isPending: refreshing } = useDiscoverQueries() - const publicPlaylistsLength = (publicPlaylists ?? []).length + const isRestoring = useIsRestoring() + + const { isPending: loadingInitialData } = useRecentlyAddedAlbums() return ( } > - - - - - - {publicPlaylistsLength > 0 && ( - - - - )} - - {suggestedArtistsInfiniteQuery.data && ( - - - - )} - + ) } + +function DiscoverContent() { + return ( + + + + + + + + ) +} diff --git a/src/components/Discover/helpers/just-added.tsx b/src/components/Discover/helpers/just-added.tsx index 8d2dd31e..f2397ee8 100644 --- a/src/components/Discover/helpers/just-added.tsx +++ b/src/components/Discover/helpers/just-added.tsx @@ -1,59 +1,65 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' import HorizontalCardList from '../../../components/Global/components/horizontal-list' import { ItemCard } from '../../../components/Global/components/item-card' -import { useDiscoverContext } from '../../../providers/Discover' import { H5, View, XStack } from 'tamagui' -import { H4 } from '../../../components/Global/helpers/text' import Icon from '../../Global/components/icon' import { useNavigation } from '@react-navigation/native' import DiscoverStackParamList from '../../../screens/Discover/types' import navigationRef from '../../../../navigation' import { useRecentlyAddedAlbums } from '../../../api/queries/album' +import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated' -export default function RecentlyAdded(): React.JSX.Element { +export default function RecentlyAdded(): React.JSX.Element | undefined { const recentlyAddedAlbumsInfinityQuery = useRecentlyAddedAlbums() const navigation = useNavigation>() return ( - - { - navigation.navigate('RecentlyAdded', { - albumsInfiniteQuery: recentlyAddedAlbumsInfinityQuery, - }) - }} + recentlyAddedAlbumsInfinityQuery.data && ( + -
Recently Added
- -
+ { + navigation.navigate('RecentlyAdded', { + albumsInfiniteQuery: recentlyAddedAlbumsInfinityQuery, + }) + }} + > +
Recently Added
+ +
- ( - { - navigation.navigate('Album', { - album: item, - }) - }} - onLongPress={() => { - navigationRef.navigate('Context', { - item, - navigation, - }) - }} - gap={'$1'} - captionAlign='left' - /> - )} - /> -
+ ( + { + navigation.navigate('Album', { + album: item, + }) + }} + onLongPress={() => { + navigationRef.navigate('Context', { + item, + navigation, + }) + }} + gap={'$1'} + captionAlign='left' + /> + )} + /> + + ) ) } diff --git a/src/components/Discover/helpers/public-playlists.tsx b/src/components/Discover/helpers/public-playlists.tsx index f3a79d9a..8e0b4b5a 100644 --- a/src/components/Discover/helpers/public-playlists.tsx +++ b/src/components/Discover/helpers/public-playlists.tsx @@ -1,5 +1,4 @@ -import { H5, View, XStack } from 'tamagui' -import { useDiscoverContext } from '../../../providers/Discover' +import { H5, XStack } from 'tamagui' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import Icon from '../../Global/components/icon' import HorizontalCardList from '../../Global/components/horizontal-list' @@ -9,65 +8,74 @@ import { useNavigation } from '@react-navigation/native' import DiscoverStackParamList from '../../../screens/Discover/types' import navigationRef from '../../../../navigation' import { useJellifyServer } from '../../../stores' +import { usePublicPlaylists } from '../../../api/queries/playlist' +import Animated, { FadeIn, LinearTransition } from 'react-native-reanimated' export default function PublicPlaylists() { const { - publicPlaylists, - fetchNextPublicPlaylists, - hasNextPublicPlaylists, - isFetchingNextPublicPlaylists, - isPendingPublicPlaylists, - refetchPublicPlaylists, - } = useDiscoverContext() + data: playlists, + fetchNextPage, + hasNextPage, + isPending, + isFetchingNextPage, + refetch, + } = usePublicPlaylists() const navigation = useNavigation>() const [server] = useJellifyServer() const { width } = useSafeAreaFrame() return ( - - { - navigation.navigate('PublicPlaylists', { - playlists: publicPlaylists, - navigation: navigation, - fetchNextPage: fetchNextPublicPlaylists, - hasNextPage: hasNextPublicPlaylists, - isPending: isPendingPublicPlaylists, - isFetchingNextPage: isFetchingNextPublicPlaylists, - refetch: refetchPublicPlaylists, - }) - }} + playlists && ( + -
- Playlists on {server?.name ?? 'Jellyfin'} -
- -
- ( - { - navigation.navigate('Playlist', { playlist: item, canEdit: false }) - }} - onLongPress={() => - navigationRef.navigate('Context', { - item, - navigation, - }) - } - marginHorizontal={'$1'} - captionAlign='left' - /> - )} - /> -
+ { + navigation.navigate('PublicPlaylists', { + playlists, + navigation: navigation, + fetchNextPage, + hasNextPage, + isPending, + isFetchingNextPage, + refetch, + }) + }} + > +
+ Playlists on {server?.name ?? 'Jellyfin'} +
+ +
+ ( + { + navigation.navigate('Playlist', { playlist: item, canEdit: false }) + }} + onLongPress={() => + navigationRef.navigate('Context', { + item, + navigation, + }) + } + marginHorizontal={'$1'} + captionAlign='left' + /> + )} + /> + + ) ) } diff --git a/src/components/Discover/helpers/suggested-artists.tsx b/src/components/Discover/helpers/suggested-artists.tsx index ef435e1a..3fa9a75f 100644 --- a/src/components/Discover/helpers/suggested-artists.tsx +++ b/src/components/Discover/helpers/suggested-artists.tsx @@ -3,54 +3,62 @@ import Icon from '../../Global/components/icon' import HorizontalCardList from '../../Global/components/horizontal-list' import { ItemCard } from '../../Global/components/item-card' import { NativeStackNavigationProp } from '@react-navigation/native-stack' -import { useDiscoverContext } from '../../../providers/Discover' import { useNavigation } from '@react-navigation/native' import DiscoverStackParamList from '../../../screens/Discover/types' import navigationRef from '../../../../navigation' import { pickFirstGenre } from '../../../utils/genre-formatting' +import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated' +import { useDiscoverArtists } from '../../../api/queries/suggestions' -export default function SuggestedArtists(): React.JSX.Element { - const { suggestedArtistsInfiniteQuery } = useDiscoverContext() +export default function SuggestedArtists(): React.JSX.Element | undefined { + const suggestedArtistsInfiniteQuery = useDiscoverArtists() const navigation = useNavigation>() return ( - - { - navigation.navigate('SuggestedArtists', { - artistsInfiniteQuery: suggestedArtistsInfiniteQuery, - navigation: navigation, - }) - }} - marginLeft={'$2'} + suggestedArtistsInfiniteQuery.data && ( + -
Suggested Artists
- -
- ( - { - navigation.navigate('Artist', { - artist: item, - }) - }} - onLongPress={() => - navigationRef.navigate('Context', { - item, - navigation, - }) - } - /> - )} - /> -
+ { + navigation.navigate('SuggestedArtists', { + artistsInfiniteQuery: suggestedArtistsInfiniteQuery, + navigation: navigation, + }) + }} + marginLeft={'$2'} + > +
Suggested Artists
+ +
+ ( + { + navigation.navigate('Artist', { + artist: item, + }) + }} + onLongPress={() => + navigationRef.navigate('Context', { + item, + navigation, + }) + } + /> + )} + /> + + ) ) } diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 644cccc0..0769f116 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -6,7 +6,7 @@ import { QueryKeys } from '../../enums/query-keys' import { fetchSearchResults } from '../../api/queries/search' import { useQuery } from '@tanstack/react-query' import { FlatList } from 'react-native' -import { fetchSearchSuggestions } from '../../api/queries/suggestions' +import { fetchSearchSuggestions } from '../../api/queries/suggestions/utils/suggestions' import { getToken, H3, Separator, Spinner, YStack } from 'tamagui' import Suggestions from './suggestions' import { isEmpty } from 'lodash' @@ -15,6 +15,7 @@ import { ItemCard } from '../Global/components/item-card' import SearchParamList from '../../screens/Search/types' import { closeAllSwipeableRows } from '../Global/components/swipeable-row-registry' import { useApi, useJellifyLibrary, useJellifyUser } from '../../stores' +import { useSearchSuggestions } from '../../api/queries/suggestions' export default function Search({ navigation, @@ -40,10 +41,7 @@ export default function Search({ data: suggestions, isFetching: fetchingSuggestions, refetch: refetchSuggestions, - } = useQuery({ - queryKey: [QueryKeys.SearchSuggestions, library?.musicLibraryId], - queryFn: () => fetchSearchSuggestions(api, user, library?.musicLibraryId), - }) + } = useSearchSuggestions() const search = () => { let timeout: ReturnType diff --git a/src/providers/Discover/index.tsx b/src/providers/Discover/index.tsx deleted file mode 100644 index fcae88d0..00000000 --- a/src/providers/Discover/index.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { - InfiniteQueryObserverResult, - useInfiniteQuery, - UseInfiniteQueryResult, -} from '@tanstack/react-query' -import { QueryKeys } from '../../enums/query-keys' -import { createContext, ReactNode, useContext, useState } from 'react' -import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' -import { fetchPublicPlaylists } from '../../api/queries/playlist/utils' -import { fetchArtistSuggestions } from '../../api/queries/suggestions' -import { useRefetchRecentlyAdded } from '../../api/queries/album' -import { useApi, useJellifyUser, useJellifyLibrary } from '../../stores' - -interface DiscoverContext { - refreshing: boolean - refresh: () => void - publicPlaylists: BaseItemDto[] | undefined - fetchNextPublicPlaylists: () => void - hasNextPublicPlaylists: boolean - isPendingPublicPlaylists: boolean - isFetchingNextPublicPlaylists: boolean - refetchPublicPlaylists: () => void - suggestedArtistsInfiniteQuery: UseInfiniteQueryResult -} - -const DiscoverContextInitializer = () => { - const api = useApi() - const [user] = useJellifyUser() - const [library] = useJellifyLibrary() - const [refreshing, setRefreshing] = useState(false) - - const refetchRecentlyAdded = useRefetchRecentlyAdded() - - const { - data: publicPlaylists, - refetch: refetchPublicPlaylists, - fetchNextPage: fetchNextPublicPlaylists, - hasNextPage: hasNextPublicPlaylists, - isPending: isPendingPublicPlaylists, - isFetchingNextPage: isFetchingNextPublicPlaylists, - } = useInfiniteQuery({ - queryKey: [QueryKeys.PublicPlaylists, library?.playlistLibraryId], - queryFn: ({ pageParam }) => fetchPublicPlaylists(api, library, pageParam), - select: (data) => data.pages.flatMap((page) => page), - getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => - lastPage.length > 0 ? lastPageParam + 1 : undefined, - initialPageParam: 0, - }) - - const suggestedArtistsInfiniteQuery = useInfiniteQuery({ - queryKey: [QueryKeys.InfiniteSuggestedArtists, user?.id, library?.musicLibraryId], - queryFn: ({ pageParam }) => - fetchArtistSuggestions(api, user, library?.musicLibraryId, pageParam), - getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => - lastPage.length > 0 ? lastPageParam + 1 : undefined, - select: (data) => data.pages.flatMap((page) => page), - initialPageParam: 0, - maxPages: 2, - }) - - const refresh = async () => { - setRefreshing(true) - - await Promise.all([ - refetchRecentlyAdded(), - refetchPublicPlaylists(), - suggestedArtistsInfiniteQuery.refetch(), - ]) - setRefreshing(false) - } - - return { - refreshing, - refresh, - publicPlaylists, - fetchNextPublicPlaylists, - hasNextPublicPlaylists, - isPendingPublicPlaylists, - isFetchingNextPublicPlaylists, - refetchPublicPlaylists, - suggestedArtistsInfiniteQuery, - } -} - -const DiscoverContext = createContext({ - refreshing: false, - refresh: () => {}, - publicPlaylists: undefined, - fetchNextPublicPlaylists: () => {}, - hasNextPublicPlaylists: false, - isPendingPublicPlaylists: false, - isFetchingNextPublicPlaylists: false, - refetchPublicPlaylists: () => {}, - suggestedArtistsInfiniteQuery: { - data: undefined, - error: null, - isEnabled: true, - isStale: false, - isRefetching: false, - isError: false, - isLoading: true, - isPending: true, - isFetching: true, - isSuccess: false, - isFetched: false, - hasPreviousPage: false, - refetch: async () => - Promise.resolve({} as InfiniteQueryObserverResult), - fetchNextPage: async () => - Promise.resolve({} as InfiniteQueryObserverResult), - hasNextPage: false, - isFetchingNextPage: false, - isFetchPreviousPageError: false, - isFetchNextPageError: false, - isFetchingPreviousPage: false, - isLoadingError: false, - isRefetchError: false, - isPlaceholderData: false, - status: 'pending', - fetchStatus: 'idle', - dataUpdatedAt: 0, - errorUpdatedAt: 0, - failureCount: 0, - failureReason: null, - errorUpdateCount: 0, - isFetchedAfterMount: false, - isInitialLoading: false, - isPaused: false, - fetchPreviousPage: async () => - Promise.resolve({} as InfiniteQueryObserverResult), - promise: Promise.resolve([]), - }, -}) - -export const DiscoverProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({ - children, -}: { - children: ReactNode -}) => { - const context = DiscoverContextInitializer() - - return {children} -} - -export const useDiscoverContext = () => useContext(DiscoverContext) diff --git a/src/screens/Discover/index.tsx b/src/screens/Discover/index.tsx index e222f63c..7de45595 100644 --- a/src/screens/Discover/index.tsx +++ b/src/screens/Discover/index.tsx @@ -2,7 +2,6 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack' import Index from '../../components/Discover/component' import AlbumScreen from '../Album' import { ArtistScreen } from '../Artist' -import { DiscoverProvider } from '../../providers/Discover' import { useTheme } from 'tamagui' import RecentlyAdded from './albums' import PublicPlaylists from './playlists' @@ -18,87 +17,85 @@ export function Discover(): React.JSX.Element { const theme = useTheme() return ( - - - + + - ({ - title: route.params.artist.Name ?? 'Unknown Artist', - headerTitleStyle: { - color: theme.background.val, - }, - })} - /> + ({ + title: route.params.artist.Name ?? 'Unknown Artist', + headerTitleStyle: { + color: theme.background.val, + }, + })} + /> - ({ - title: route.params.album.Name ?? 'Untitled Album', - headerTitleStyle: { - color: theme.background.val, - }, - })} - /> + ({ + title: route.params.album.Name ?? 'Untitled Album', + headerTitleStyle: { + color: theme.background.val, + }, + })} + /> - ({ - title: route.params.playlist.Name ?? 'Untitled Playlist', - })} - /> + ({ + title: route.params.playlist.Name ?? 'Untitled Playlist', + })} + /> - + - + - + - ({ - headerTitle: `${getItemName(route.params.item)} Mix`, - })} - /> - - + ({ + headerTitle: `${getItemName(route.params.item)} Mix`, + })} + /> + ) }