From 1d0641f2ae0ad1b1f5fe16d11778762f6652268c Mon Sep 17 00:00:00 2001 From: Violet Caulfield Date: Wed, 20 Aug 2025 21:33:04 -0500 Subject: [PATCH] lots of context prefetching and favorites prefetching improvements --- App.tsx | 7 +- src/api/queries/query.config.ts | 5 - src/components/Artists/component.tsx | 1 + src/components/Context/index.tsx | 39 ++--- .../Global/components/favorite-button.tsx | 32 ++-- .../components/favorite-context-menu-row.tsx | 27 +--- .../Global/components/favorite-icon.tsx | 16 +- .../Global/components/instant-mix-button.tsx | 1 - src/components/Global/components/track.tsx | 36 +---- src/constants/query-client.ts | 12 +- src/providers/Home/index.tsx | 17 +- src/providers/Item/index.tsx | 152 +++++++++--------- src/providers/Item/item-artists.tsx | 20 +-- src/providers/Library/index.tsx | 8 - src/providers/Network/index.tsx | 1 - src/providers/Player/index.tsx | 21 --- src/providers/Player/queue.tsx | 28 ++-- src/providers/Playlist/index.tsx | 1 - src/providers/UserData/index.tsx | 7 +- 19 files changed, 165 insertions(+), 266 deletions(-) diff --git a/App.tsx b/App.tsx index 6a2d1745..3c970101 100644 --- a/App.tsx +++ b/App.tsx @@ -7,7 +7,7 @@ import { TamaguiProvider } from 'tamagui' import { Platform, useColorScheme } from 'react-native' import jellifyConfig from './tamagui.config' import { clientPersister } from './src/constants/storage' -import { queryClient } from './src/constants/query-client' +import { ONE_DAY, queryClient } from './src/constants/query-client' import { GestureHandlerRootView } from 'react-native-gesture-handler' import TrackPlayer, { AndroidAudioContentType, @@ -110,10 +110,9 @@ function Container({ playerIsReady }: { playerIsReady: boolean }): React.JSX.Ele persister: clientPersister, /** - * Infinity, since data can remain the - * same forever on the server + * Maximum query data age of one day */ - maxAge: Infinity, + maxAge: ONE_DAY, }} > diff --git a/src/api/queries/query.config.ts b/src/api/queries/query.config.ts index 50150d73..46aff7df 100644 --- a/src/api/queries/query.config.ts +++ b/src/api/queries/query.config.ts @@ -50,11 +50,6 @@ const QueryConfig = { width: 1000, format: ImageFormat.Jpg, }, - staleTime: { - oneDay: 1000 * 60 * 60 * 24, // 1 Day - oneWeek: 1000 * 60 * 60 * 24 * 7, // 7 Days - oneFortnight: 1000 * 60 * 60 * 24 * 7 * 14, // 14 Days - }, } export default QueryConfig diff --git a/src/components/Artists/component.tsx b/src/components/Artists/component.tsx index ea1ca6ca..87a1a9e0 100644 --- a/src/components/Artists/component.tsx +++ b/src/components/Artists/component.tsx @@ -168,6 +168,7 @@ export default function Artists({ if (artistsInfiniteQuery.hasNextPage && !artistsInfiniteQuery.isFetching) artistsInfiniteQuery.fetchNextPage() }} + removeClippedSubviews /> {showAlphabeticalSelector && artistPageParams && ( diff --git a/src/components/Context/index.tsx b/src/components/Context/index.tsx index 302de509..4d75a66c 100644 --- a/src/components/Context/index.tsx +++ b/src/components/Context/index.tsx @@ -1,24 +1,22 @@ import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models' -import { getToken, getTokenValue, ListItem, ScrollView, View, YGroup, ZStack } from 'tamagui' +import { getToken, ListItem, ScrollView, View, YGroup } from 'tamagui' import { BaseStackParamList, RootStackParamList } from '../../screens/types' import { Text } from '../Global/helpers/text' import FavoriteContextMenuRow from '../Global/components/favorite-context-menu-row' -import { Blurhash } from 'react-native-blurhash' -import { getPrimaryBlurhashFromDto } from '../../utils/blurhash' -import { Platform, useColorScheme } from 'react-native' +import { useColorScheme } from 'react-native' import { useThemeSettingContext } from '../../providers/Settings' import LinearGradient from 'react-native-linear-gradient' import Icon from '../Global/components/icon' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { useQuery } from '@tanstack/react-query' import { QueryKeys } from '../../enums/query-keys' -import { fetchAlbumDiscs, fetchItem, fetchItems } from '../../api/queries/item' +import { fetchAlbumDiscs, fetchItem } from '../../api/queries/item' import { useJellifyContext } from '../../providers' import { getItemsApi } from '@jellyfin/sdk/lib/utils/api' import { useAddToQueueContext } from '../../providers/Player/queue' import { AddToQueueMutation } from '../../providers/Player/interfaces' import { QueuingType } from '../../enums/queuing-type' -import { useCallback, useMemo } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import navigationRef from '../../../navigation' import { goToAlbumFromContextSheet, goToArtistFromContextSheet } from './utils/navigation' import { getItemName } from '../../utils/text' @@ -27,6 +25,7 @@ import { StackActions } from '@react-navigation/native' import TextTicker from 'react-native-text-ticker' import { TextTickerConfig } from '../Player/component.config' import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { trigger } from 'react-native-haptic-feedback' type StackNavigation = Pick, 'navigate' | 'dispatch'> @@ -47,8 +46,6 @@ export default function ItemContext({ item, stackNavigation }: ContextProps): Re const isTrack = item.Type === BaseItemKind.Audio const isPlaylist = item.Type === BaseItemKind.Playlist - const itemArtists = item.ArtistItems ?? [] - const { data: album } = useQuery({ queryKey: [QueryKeys.Album, item.AlbumId], queryFn: () => fetchItem(api, item.AlbumId!), @@ -77,7 +74,7 @@ export default function ItemContext({ item, stackNavigation }: ContextProps): Re const renderAddToPlaylistRow = isTrack - const renderViewAlbumRow = useMemo(() => isAlbum || (isTrack && album), [album, item]) + const renderViewAlbumRow = isAlbum || (isTrack && album) const artistIds = !isPlaylist ? isArtist @@ -87,24 +84,22 @@ export default function ItemContext({ item, stackNavigation }: ContextProps): Re : [] : [] + const itemTracks = isTrack + ? [item] + : isAlbum && discs + ? discs.flatMap((data) => data.data) + : isPlaylist && tracks + ? tracks + : [] + + useEffect(() => trigger('impactLight'), [item?.Id]) + return ( - {renderAddToQueueRow && ( - data.data) - : isPlaylist && tracks - ? tracks - : [] - } - /> - )} + {renderAddToQueueRow && } {renderAddToPlaylistRow && } diff --git a/src/components/Global/components/favorite-button.tsx b/src/components/Global/components/favorite-button.tsx index 3ae5eaae..1001bd2a 100644 --- a/src/components/Global/components/favorite-button.tsx +++ b/src/components/Global/components/favorite-button.tsx @@ -1,14 +1,15 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' -import React, { useEffect, useState } from 'react' +import React from 'react' import Icon from './icon' import { useQuery } from '@tanstack/react-query' import { isUndefined } from 'lodash' -import { getTokens, Spinner } from 'tamagui' +import { Spinner } from 'tamagui' import { QueryKeys } from '../../../enums/query-keys' import { fetchUserData } from '../../../api/queries/favorites' import { useJellifyUserDataContext } from '../../../providers/UserData' import { useJellifyContext } from '../../../providers' import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' +import { ONE_HOUR } from '../../../constants/query-client' interface SetFavoriteMutation { item: BaseItemDto @@ -21,26 +22,21 @@ export default function FavoriteButton({ item: BaseItemDto onToggle?: () => void }): React.JSX.Element { - const [isFavorite, setFavorite] = useState(isFavoriteItem(item)) - const { api, user } = useJellifyContext() const { toggleFavorite } = useJellifyUserDataContext() - const { data, isFetching, refetch } = useQuery({ - queryKey: [QueryKeys.UserData, item.Id!], + const { + data: isFavorite, + isFetching, + refetch, + } = useQuery({ + queryKey: [QueryKeys.UserData, item.Id], queryFn: () => fetchUserData(api, user, item.Id!), - staleTime: 1000 * 60 * 60 * 1, // 1 hour, + select: (data) => typeof data === 'object' && data.IsFavorite, + staleTime: ONE_HOUR, }) - useEffect(() => { - refetch() - }, [item]) - - useEffect(() => { - if (data) setFavorite(data.IsFavorite ?? false) - }, [data]) - - return isFetching && isUndefined(item.UserData) ? ( + return isFetching ? ( ) : isFavorite ? ( @@ -50,7 +46,6 @@ export default function FavoriteButton({ onPress={() => toggleFavorite(isFavorite, { item, - setFavorite, onToggle, }) } @@ -62,9 +57,8 @@ export default function FavoriteButton({ name={'heart-outline'} color={'$primary'} onPress={() => - toggleFavorite(isFavorite, { + toggleFavorite(!!isFavorite, { item, - setFavorite, onToggle, }) } diff --git a/src/components/Global/components/favorite-context-menu-row.tsx b/src/components/Global/components/favorite-context-menu-row.tsx index 11b8a184..2a7f9e9d 100644 --- a/src/components/Global/components/favorite-context-menu-row.tsx +++ b/src/components/Global/components/favorite-context-menu-row.tsx @@ -3,31 +3,24 @@ import { useQuery } from '@tanstack/react-query' import { QueryKeys } from '../../../enums/query-keys' import { fetchUserData } from '../../../api/queries/favorites' import { useJellifyContext } from '../../../providers' -import { getToken, ListItem, XStack } from 'tamagui' +import { ListItem, XStack } from 'tamagui' import Icon from './icon' import { useJellifyUserDataContext } from '../../../providers/UserData' -import { useEffect, useState } from 'react' import { Text } from '../helpers/text' import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' +import { ONE_HOUR } from '../../../constants/query-client' export default function FavoriteContextMenuRow({ item }: { item: BaseItemDto }): React.JSX.Element { const { api, user } = useJellifyContext() const { toggleFavorite } = useJellifyUserDataContext() - const { data: userData, refetch } = useQuery({ + const { data: isFavorite, refetch } = useQuery({ queryKey: [QueryKeys.UserData, item.Id], queryFn: () => fetchUserData(api, user, item.Id!), - staleTime: 1000 * 60 * 60 * 1, // 1 hour, + select: (data) => typeof data === 'object' && data.IsFavorite, + staleTime: ONE_HOUR, }) - const [isFavorite, setIsFavorite] = useState( - userData?.IsFavorite ?? item.UserData?.IsFavorite ?? false, - ) - - useEffect(() => { - setIsFavorite(userData?.IsFavorite ?? false) - }, [userData]) - return isFavorite ? ( { toggleFavorite(isFavorite, { item, - setFavorite: setIsFavorite, onToggle: () => refetch(), }) }} @@ -47,12 +39,10 @@ export default function FavoriteContextMenuRow({ item }: { item: BaseItemDto }): exiting={FadeOut} key={`${item.Id}-remove-favorite-row`} > - + - - Remove from favorites - + Remove from favorites @@ -63,9 +53,8 @@ export default function FavoriteContextMenuRow({ item }: { item: BaseItemDto }): justifyContent='flex-start' gap={'$2'} onPress={() => { - toggleFavorite(isFavorite, { + toggleFavorite(!!isFavorite, { item, - setFavorite: setIsFavorite, onToggle: () => refetch(), }) }} diff --git a/src/components/Global/components/favorite-icon.tsx b/src/components/Global/components/favorite-icon.tsx index 011bc199..a24ac224 100644 --- a/src/components/Global/components/favorite-icon.tsx +++ b/src/components/Global/components/favorite-icon.tsx @@ -4,7 +4,7 @@ import Icon from './icon' import { useQuery } from '@tanstack/react-query' import { QueryKeys } from '../../../enums/query-keys' import { fetchUserData } from '../../../api/queries/favorites' -import { useEffect, useState, memo } from 'react' +import { memo } from 'react' import { useJellifyContext } from '../../../providers' import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' @@ -16,23 +16,15 @@ import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' * @returns A React component that displays a favorite icon for a given item. */ function FavoriteIcon({ item }: { item: BaseItemDto }): React.JSX.Element { - const [isFavorite, setIsFavorite] = useState(item.UserData?.IsFavorite ?? false) - const { api, user } = useJellifyContext() - const { data: userData, isPending } = useQuery({ - queryKey: [QueryKeys.UserData, item.Id!], + const { data: isFavorite } = useQuery({ + queryKey: [QueryKeys.UserData, item.Id], queryFn: () => fetchUserData(api, user, item.Id!), - staleTime: 1000 * 60 * 5, // 5 minutes, + select: (data) => typeof data === 'object' && data.IsFavorite, enabled: !!api && !!user && !!item.Id, // Only run if we have the required data }) - useEffect(() => { - if (!isPending && userData !== undefined) { - setIsFavorite(userData?.IsFavorite ?? false) - } - }, [userData, isPending]) - return isFavorite ? ( diff --git a/src/components/Global/components/instant-mix-button.tsx b/src/components/Global/components/instant-mix-button.tsx index 26dd6eda..71e5611c 100644 --- a/src/components/Global/components/instant-mix-button.tsx +++ b/src/components/Global/components/instant-mix-button.tsx @@ -20,7 +20,6 @@ export default function InstantMixButton({ const { data, isFetching, refetch } = useQuery({ queryKey: [QueryKeys.InstantMix, item.Id!], queryFn: () => fetchInstantMixFromItem(api, user, item), - staleTime: 1000 * 60 * 60 * 24, // 24 hours }) return data ? ( diff --git a/src/components/Global/components/track.tsx b/src/components/Global/components/track.tsx index 428e7644..1af66f64 100644 --- a/src/components/Global/components/track.tsx +++ b/src/components/Global/components/track.tsx @@ -2,28 +2,19 @@ import React, { useMemo, useCallback } from 'react' import { getToken, Theme, useTheme, XStack, YStack } from 'tamagui' import { Text } from '../helpers/text' import { RunTimeTicks } from '../helpers/time-codes' -import { BaseItemDto, BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client/models' +import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import Icon from './icon' import { QueuingType } from '../../../enums/queuing-type' import { Queue } from '../../../player/types/queue-item' import FavoriteIcon from './favorite-icon' -import FastImage from 'react-native-fast-image' -import { getImageApi } from '@jellyfin/sdk/lib/utils/api' import { networkStatusTypes } from '../../../components/Network/internetConnectionWatcher' import { useNetworkContext } from '../../../providers/Network' import { useLoadQueueContext, usePlayQueueContext } from '../../../providers/Player/queue' -import { useJellifyContext } from '../../../providers' import DownloadedIcon from './downloaded-icon' -import { useQuery } from '@tanstack/react-query' -import { QueryKeys } from '../../../enums/query-keys' -import { fetchMediaInfo } from '../../../api/queries/media' -import { useStreamingQualityContext } from '../../../providers/Settings' -import { getQualityParams } from '../../../utils/mappings' import { useNowPlayingContext } from '../../../providers/Player' import navigationRef from '../../../../navigation' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { BaseStackParamList } from '../../../screens/types' -import { fetchItem } from '../../../api/queries/item' import ItemImage from './image' import { ItemProvider } from '../../../providers/Item' @@ -61,12 +52,10 @@ export default function Track({ }: TrackProps): React.JSX.Element { const theme = useTheme() - const { api, user } = useJellifyContext() const nowPlaying = useNowPlayingContext() const playQueue = usePlayQueueContext() const useLoadNewQueue = useLoadQueueContext() const { downloadedTracks, networkStatus } = useNetworkContext() - const streamingQuality = useStreamingQualityContext() // Memoize expensive computations const isPlaying = useMemo( @@ -129,29 +118,6 @@ export default function Track({ } }, [showRemove, onRemove, track, isNested]) - // Only fetch media info if needed (for streaming) - useQuery({ - queryKey: [QueryKeys.MediaSources, streamingQuality, track.Id], - queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), track.Id!), - staleTime: Infinity, // Don't refetch media info unless the user changes the quality - enabled: !isDownloaded, // Only fetch if not downloaded - }) - - // Fire query for fetching the track's media sources - useQuery({ - queryKey: [QueryKeys.MediaSources, streamingQuality, track.Id], - queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), track.Id!), - staleTime: Infinity, // Don't refetch media info unless the user changes the quality - enabled: track.Type === 'Audio', - }) - - // Fire query for fetching the track's album - useQuery({ - queryKey: [QueryKeys.Album, track.AlbumId], - queryFn: () => fetchItem(api, track.AlbumId!), - enabled: track.Type === BaseItemKind.Audio && !!track.AlbumId, - }) - // Memoize text color to prevent recalculation const textColor = useMemo(() => { if (isPlaying) return theme.primary.val diff --git a/src/constants/query-client.ts b/src/constants/query-client.ts index 8d2c7103..2a1ff00d 100644 --- a/src/constants/query-client.ts +++ b/src/constants/query-client.ts @@ -1,5 +1,9 @@ import { QueryClient } from '@tanstack/react-query' +export const ONE_MINUTE = 1000 * 60 +export const ONE_HOUR = ONE_MINUTE * 60 +export const ONE_DAY = ONE_HOUR * 24 + /** * A global instance of the Tanstack React Query client * @@ -16,18 +20,18 @@ export const queryClient = new QueryClient({ /** * This needs to be set equal to or higher than the `maxAge` set in `../App.tsx` * - * Because data can remain on the server forever, the `maxAge` is set to `Infinity` + * Because we want to preserve hybrid network functionality, the `maxAge` is set to {@link ONE_DAY} * - * Therefore, this also needs to be set to `Infinity`, disabling garbage collection + * Therefore, this also needs to be set to {@link ONE_DAY} * * @see https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient#how-it-works */ - gcTime: Infinity, + gcTime: ONE_DAY, /** * 1 hour as a default - reduced from 2 hours for better battery usage */ - staleTime: 1000 * 60 * 60, // 1 hour + staleTime: ONE_HOUR, // 1 hour retry(failureCount: number, error: Error) { if (failureCount > 2) return false diff --git a/src/providers/Home/index.tsx b/src/providers/Home/index.tsx index 5ebb80f5..7db4ecf5 100644 --- a/src/providers/Home/index.tsx +++ b/src/providers/Home/index.tsx @@ -1,14 +1,6 @@ -import React, { - createContext, - ReactNode, - useCallback, - useContext, - useEffect, - useState, -} from 'react' +import React, { createContext, ReactNode, useCallback, useContext, useState } from 'react' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { - InfiniteData, InfiniteQueryObserverResult, useInfiniteQuery, UseInfiniteQueryResult, @@ -19,7 +11,6 @@ import { queryClient } from '../../constants/query-client' import QueryConfig from '../../api/queries/query.config' import { fetchFrequentlyPlayed, fetchFrequentlyPlayedArtists } from '../../api/queries/frequents' import { useJellifyContext } from '..' -import { useIsFocused } from '@react-navigation/native' interface HomeContext { refreshing: boolean onRefresh: () => void @@ -44,12 +35,6 @@ const HomeContextInitializer = () => { const { api, library, user } = useJellifyContext() const [refreshing, setRefreshing] = useState(false) - const isFocused = useIsFocused() - - useEffect(() => { - console.debug(`Home focused: ${isFocused}`) - }, [isFocused]) - const { data: recentTracks, isFetching: isFetchingRecentTracks, diff --git a/src/providers/Item/index.tsx b/src/providers/Item/index.tsx index 8a6cdf78..016aae16 100644 --- a/src/providers/Item/index.tsx +++ b/src/providers/Item/index.tsx @@ -1,5 +1,5 @@ import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models' -import { createContext, ReactNode, useMemo } from 'react' +import { createContext, ReactNode, useEffect } from 'react' import { useQuery } from '@tanstack/react-query' import { QueryKeys } from '../../enums/query-keys' import { fetchMediaInfo } from '../../api/queries/media' @@ -9,6 +9,8 @@ import { getQualityParams } from '../../utils/mappings' import { fetchAlbumDiscs, fetchItem } from '../../api/queries/item' import { getItemsApi } from '@jellyfin/sdk/lib/utils/api' import { ItemArtistProvider } from './item-artists' +import { queryClient } from '../../constants/query-client' +import { fetchUserData } from '../../api/queries/favorites' interface ItemContext { item: BaseItemDto @@ -45,88 +47,88 @@ export const ItemProvider: ({ item, children }: ItemProviderProps) => React.JSX. const streamingQuality = useStreamingQualityContext() - const { Id, Type, AlbumId, ArtistItems } = item + const { Id, Type, AlbumId, ArtistItems, UserData } = item const artistIds = ArtistItems?.map(({ Id }) => Id) ?? [] - /** - * Fetch and cache the media sources if this item is a track - */ - useQuery({ - queryKey: [QueryKeys.MediaSources, streamingQuality, Id], - queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), Id!), - staleTime: Infinity, // Don't refetch media info unless the user changes the quality - enabled: !!Id && Type === BaseItemKind.Audio, - }) + useEffect(() => { + // Fail fast if we don't have an Item ID to work with + if (!Id) return + /** + * Fetch and cache the media sources if this item is a track + */ + if (Type === BaseItemKind.Audio) + queryClient.ensureQueryData({ + queryKey: [QueryKeys.MediaSources, streamingQuality, Id], + queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), Id!), + }) - /** - * ...or store it as a queryable if the item is an artist - * - * Referenced later in the context sheet - */ - useQuery({ - queryKey: [QueryKeys.ArtistById, Id], - queryFn: () => item, - enabled: !!Id && Type === BaseItemKind.MusicArtist, - }) + /** + * ...or store it as a queryable if the item is an artist + * + * Referenced later in the context sheet + */ + if (Type === BaseItemKind.MusicArtist) + queryClient.setQueryData([QueryKeys.ArtistById, Id], item) - /** - * Fire query for a track's album... - * - * Referenced later in the context sheet - */ - useQuery({ - queryKey: [QueryKeys.Album, AlbumId], - queryFn: () => fetchItem(api, item.AlbumId!), - enabled: !!AlbumId && Type === BaseItemKind.Audio, - }) + /** + * Fire query for a track's album... + * + * Referenced later in the context sheet + */ + if (!!AlbumId && Type === BaseItemKind.Audio) + queryClient.ensureQueryData({ + queryKey: [QueryKeys.Album, AlbumId], + queryFn: () => fetchItem(api, item.AlbumId!), + }) - /** - * ...or store it if it is an album - * - * Referenced later in the context sheet - */ - useQuery({ - queryKey: [QueryKeys.Album, Id], - queryFn: () => item, - enabled: !!Id && Type === BaseItemKind.MusicAlbum, - }) + /** + * ...or store it if it is an album + * + * Referenced later in the context sheet + */ + if (Type === BaseItemKind.MusicAlbum) queryClient.setQueryData([QueryKeys.Album, Id], item) - /** - * Prefetch for an album's tracks - * - * Referenced later in the context sheet - */ - useQuery({ - queryKey: [QueryKeys.ItemTracks, Id], - queryFn: () => fetchAlbumDiscs(api, item), - enabled: !!Id && item.Type === BaseItemKind.MusicAlbum, - }) + /** + * Prefetch for an album's tracks + * + * Referenced later in the context sheet + */ + if (Type === BaseItemKind.MusicAlbum) + queryClient.ensureQueryData({ + queryKey: [QueryKeys.ItemTracks, Id], + queryFn: () => fetchAlbumDiscs(api, item), + }) - /** - * Fire query for a playlist's tracks - * - * Referenced later in the context sheet - */ - useQuery({ - queryKey: [QueryKeys.ItemTracks, Id], - queryFn: () => - getItemsApi(api!) - .getItems({ parentId: Id! }) - .then(({ data }) => { - if (data.Items) return data.Items - else return [] - }), - enabled: !!Id && Type === BaseItemKind.Playlist, - }) + /** + * Prefetch query for a playlist's tracks + * + * Referenced later in the context sheet + */ + if (Type === BaseItemKind.Playlist) + queryClient.ensureQueryData({ + queryKey: [QueryKeys.ItemTracks, Id], + queryFn: () => + getItemsApi(api!) + .getItems({ parentId: Id! }) + .then(({ data }) => { + if (data.Items) return data.Items + else return [] + }), + }) - return useMemo( - () => ( - - {artistIds.map((Id) => Id && )} - {children} - - ), - [item], + if (UserData) queryClient.setQueryData([QueryKeys.UserData, Id], UserData) + else + queryClient.ensureQueryData({ + queryKey: [QueryKeys.UserData, Id], + queryFn: () => fetchUserData(api, user, Id), + }) + }, [queryClient, api, user, Id, Type, AlbumId, UserData, item, streamingQuality]) + + return ( + + {artistIds.map((Id) => Id && )} + {children} + ) } diff --git a/src/providers/Item/item-artists.tsx b/src/providers/Item/item-artists.tsx index 70618597..bd95aed8 100644 --- a/src/providers/Item/item-artists.tsx +++ b/src/providers/Item/item-artists.tsx @@ -1,8 +1,8 @@ -import { createContext } from 'react' +import { createContext, useEffect } from 'react' import { useJellifyContext } from '..' -import { useQuery } from '@tanstack/react-query' import { QueryKeys } from '../../enums/query-keys' import { fetchItem } from '../../api/queries/item' +import { queryClient } from '../../constants/query-client' interface ItemArtistContext { artistId: string | undefined @@ -19,13 +19,15 @@ export const ItemArtistProvider: ({ }) => React.JSX.Element = ({ artistId }) => { const { api } = useJellifyContext() - /** - * Store queryable of artist item - */ - useQuery({ - queryKey: [QueryKeys.ArtistById, artistId], - queryFn: () => fetchItem(api, artistId!), - enabled: !!artistId, + useEffect(() => { + /** + * Store queryable of artist item + */ + if (artistId) + queryClient.ensureQueryData({ + queryKey: [QueryKeys.ArtistById, artistId], + queryFn: () => fetchItem(api, artistId!), + }) }) return diff --git a/src/providers/Library/index.tsx b/src/providers/Library/index.tsx index cbf8b799..56b1feb8 100644 --- a/src/providers/Library/index.tsx +++ b/src/providers/Library/index.tsx @@ -100,8 +100,6 @@ const LibraryContextInitializer = () => { ), select: selectArtists, initialPageParam: 0, - staleTime: QueryConfig.staleTime.oneDay, // Cache for 1 day to reduce network requests - gcTime: QueryConfig.staleTime.oneWeek, // Keep in memory for 1 week getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => { return lastPage.length === QueryConfig.limits.library ? lastPageParam + 1 : undefined }, @@ -123,8 +121,6 @@ const LibraryContextInitializer = () => { sortDescending ? SortOrder.Descending : SortOrder.Ascending, ), initialPageParam: 0, - staleTime: QueryConfig.staleTime.oneDay, // Cache for 1 day to reduce network requests - gcTime: QueryConfig.staleTime.oneWeek, // Keep in memory for 1 week getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => { console.debug(`Tracks last page length: ${lastPage.length}`) return lastPage.length === QueryConfig.limits.library * 2 @@ -149,8 +145,6 @@ const LibraryContextInitializer = () => { initialPageParam: alphabet[0], select: (data) => data.pages.flatMap((page) => [page.title, ...page.data]), maxPages: alphabet.length, - staleTime: QueryConfig.staleTime.oneDay, // Cache for 1 day to reduce network requests - gcTime: QueryConfig.staleTime.oneWeek, // Keep in memory for 1 week getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => { console.debug(`Albums last page length: ${lastPage.data.length}`) if (lastPageParam !== alphabet[alphabet.length - 1]) { @@ -180,8 +174,6 @@ const LibraryContextInitializer = () => { queryFn: () => fetchUserPlaylists(api, user, library), select: (data) => data.pages.flatMap((page) => page), initialPageParam: 0, - staleTime: QueryConfig.staleTime.oneDay, // Cache for 1 day to reduce network requests - gcTime: QueryConfig.staleTime.oneWeek, // Keep in memory for 1 week getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => { return lastPage.length === QueryConfig.limits.library ? lastPageParam + 1 : undefined }, diff --git a/src/providers/Network/index.tsx b/src/providers/Network/index.tsx index 57a9e86c..ee544814 100644 --- a/src/providers/Network/index.tsx +++ b/src/providers/Network/index.tsx @@ -115,7 +115,6 @@ const NetworkContextInitializer = () => { const { data: storageUsage } = useQuery({ queryKey: [QueryKeys.StorageInUse], queryFn: () => fetchStorageInUse(), - staleTime: 1000 * 60 * 60 * 1, // 1 hour }) const { mutate: clearDownloads } = useMutation({ diff --git a/src/providers/Player/index.tsx b/src/providers/Player/index.tsx index 5ac5398c..d25f3018 100644 --- a/src/providers/Player/index.tsx +++ b/src/providers/Player/index.tsx @@ -90,7 +90,6 @@ const PlayerContextInitializer = () => { nowPlayingJson ? JSON.parse(nowPlayingJson) : undefined, ) - const [initialized, setInitialized] = useState(false) const [repeatMode, setRepeatMode] = useState( repeatModeJson ? JSON.parse(repeatModeJson) : RepeatMode.Off, ) @@ -579,26 +578,6 @@ const PlayerContextInitializer = () => { } }, [currentIndex, playQueue]) - /** - * Initialize the player. This is used to load the queue from the {@link QueueProvider} - * and set it to the player if we have already completed the onboarding process - * and the user has a valid queue in storage - */ - useEffect(() => { - console.debug('Initialized', initialized) - console.debug('Play queue length', playQueue.length) - console.debug('Current index', currentIndex) - if (playQueue.length > 0 && currentIndex > -1 && !initialized) { - TrackPlayer.setQueue(playQueue) - TrackPlayer.skip(currentIndex) - console.debug('Loaded queue from storage') - setInitialized(true) - } else if (queueRef === 'Recently Played' && currentIndex === -1) { - console.debug('Not loading queue as it is empty') - setInitialized(true) - } - }, []) - /** * Clean up prefetched track IDs when the current index changes significantly */ diff --git a/src/providers/Player/queue.tsx b/src/providers/Player/queue.tsx index 27ee6678..0550ca8c 100644 --- a/src/providers/Player/queue.tsx +++ b/src/providers/Player/queue.tsx @@ -161,10 +161,10 @@ const QueueContextInitailizer = () => { * {@link Event.PlaybackActiveTrackChanged} events until that mutation has settled */ const [skipping, setSkipping] = useState(false) - const [initialized, setInitialized] = useState(false) const [shuffled, setShuffled] = useState(shuffledInit ?? false) + const [initialized, setInitialized] = useState(false) //#endregion State //#region Context @@ -178,13 +178,10 @@ const QueueContextInitailizer = () => { useTrackPlayerEvents( [Event.PlaybackActiveTrackChanged], async ({ index, track }: { index?: number | undefined; track?: Track | undefined }) => { - console.debug(`Active Track Changed to: ${index}. Skipping: ${skipping}`) - if (skipping) return - - // We get an event emitted when the queue is loaded from storage for the first time - // This is the most convenient place I could find to flip this boolean and start - // listening to emitted updates - if (!initialized) return setInitialized(true) + console.debug( + `Active Track Changed to: ${index}. Skipping: ${skipping}, Initialized: ${initialized}`, + ) + if (skipping || !initialized) return let newIndex = -1 @@ -547,7 +544,6 @@ const QueueContextInitailizer = () => { setSkipping(true) }, onSuccess: async (data: void, { startPlayback }: QueueMutation) => { - setInitialized(true) trigger('notificationSuccess') console.debug(`Loaded new queue`) @@ -717,6 +713,20 @@ const QueueContextInitailizer = () => { //#region useEffect(s) + /** + * Initialization + */ + useEffect(() => { + if (playQueue.length > 0 && currentIndex > -1 && !initialized) { + TrackPlayer.setQueue(playQueue) + TrackPlayer.skip(currentIndex) + setInitialized(true) + } else { + console.debug(`No queue to initialize from`) + setInitialized(true) + } + }, [initialized]) + /** * Store play queue in storage when it changes */ diff --git a/src/providers/Playlist/index.tsx b/src/providers/Playlist/index.tsx index 98e2e624..e2e41643 100644 --- a/src/providers/Playlist/index.tsx +++ b/src/providers/Playlist/index.tsx @@ -56,7 +56,6 @@ const PlaylistContextInitializer = (playlist: BaseItemDto) => { return response.data.Items ? response.data.Items! : [] }) }, - staleTime: 1000 * 60 * 60 * 2, // 2 hours, since these are mutable }) const useUpdatePlaylist = useMutation({ diff --git a/src/providers/UserData/index.tsx b/src/providers/UserData/index.tsx index c9738896..73b8d41c 100644 --- a/src/providers/UserData/index.tsx +++ b/src/providers/UserData/index.tsx @@ -11,7 +11,6 @@ import { useJellifyContext } from '..' interface SetFavoriteMutation { item: BaseItemDto - setFavorite: React.Dispatch> onToggle?: () => void } @@ -27,7 +26,7 @@ const JellifyUserDataContextInitializer = () => { itemId: mutation.item.Id!, }) }, - onSuccess: ({ data }, { item, setFavorite, onToggle }) => { + onSuccess: ({ data }, { item, onToggle }) => { // Burnt.alert({ // title: `Added favorite`, // duration: 1, @@ -40,7 +39,6 @@ const JellifyUserDataContextInitializer = () => { trigger('notificationSuccess') - setFavorite(true) if (onToggle) onToggle() // Force refresh of track user data @@ -54,7 +52,7 @@ const JellifyUserDataContextInitializer = () => { itemId: mutation.item.Id!, }) }, - onSuccess: ({ data }, { item, setFavorite, onToggle }) => { + onSuccess: ({ data }, { item, onToggle }) => { // Burnt.alert({ // title: `Removed favorite`, // duration: 1, @@ -65,7 +63,6 @@ const JellifyUserDataContextInitializer = () => { type: 'error', }) trigger('notificationSuccess') - setFavorite(false) if (onToggle) onToggle()