From 676f5fa199863067bd5780959f75ab0102ec29ed Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Wed, 3 Sep 2025 08:47:46 -0500 Subject: [PATCH] Make Album Disc Fetching Lazy, User Data Query Caching Fixes (#504) will use and store data retrieved from the server first, else will rely on a fetch this addresses battery life issues, as these queries were firing network requests far more frequently and for each item, causing unnecessary network usage and overhead update tanstack query to the latest while we're in there --- package.json | 6 +-- src/api/queries/album/utils/album.ts | 2 +- src/api/queries/artist/utils/artist.ts | 2 +- src/api/queries/favorites.ts | 31 ------------ src/api/queries/frequents.ts | 1 + src/api/queries/item.ts | 1 + src/api/queries/media/index.ts | 2 + src/api/queries/recents.ts | 1 + src/api/queries/tracks.ts | 1 + src/api/queries/user-data/index.ts | 17 +++++++ src/api/queries/user-data/keys.ts | 11 +++++ src/api/queries/user-data/utils/index.ts | 35 ++++++++++++++ src/components/Albums/component.tsx | 19 +------- src/components/Artist/albums.tsx | 21 +------- src/components/Artists/component.tsx | 21 +------- .../Global/components/favorite-button.tsx | 36 +++----------- .../components/favorite-context-menu-row.tsx | 14 +----- .../Global/components/favorite-icon.tsx | 14 +----- .../Global/components/horizontal-list.tsx | 20 +------- .../Global/components/item-card.tsx | 4 ++ src/components/Global/components/item-row.tsx | 4 ++ src/components/Global/components/track.tsx | 9 +++- src/components/Library/tab-bar.tsx | 4 +- src/components/Playlists/component.tsx | 19 +------- src/components/Tracks/component.tsx | 28 ++++------- src/hooks/use-item-context.ts | 18 +++---- src/providers/UserData/index.tsx | 7 +-- yarn.lock | 48 +++++++++---------- 28 files changed, 155 insertions(+), 241 deletions(-) create mode 100644 src/api/queries/user-data/index.ts create mode 100644 src/api/queries/user-data/keys.ts create mode 100644 src/api/queries/user-data/utils/index.ts diff --git a/package.json b/package.json index 1f8d1635..732a1512 100644 --- a/package.json +++ b/package.json @@ -48,9 +48,9 @@ "@sentry/react-native": "6.17.0", "@shopify/flash-list": "^2.0.3", "@tamagui/config": "^1.132.23", - "@tanstack/query-async-storage-persister": "^5.85.6", - "@tanstack/react-query": "^5.85.6", - "@tanstack/react-query-persist-client": "^5.85.6", + "@tanstack/query-async-storage-persister": "^5.85.9", + "@tanstack/react-query": "^5.85.9", + "@tanstack/react-query-persist-client": "^5.85.9", "@testing-library/react-native": "^13.2.3", "@typedigital/telemetrydeck-react": "^0.4.1", "axios": "^1.11.0", diff --git a/src/api/queries/album/utils/album.ts b/src/api/queries/album/utils/album.ts index 225f9292..28d82db3 100644 --- a/src/api/queries/album/utils/album.ts +++ b/src/api/queries/album/utils/album.ts @@ -32,7 +32,7 @@ export function fetchAlbums( parentId: library.musicLibraryId, includeItemTypes: [BaseItemKind.MusicAlbum], userId: user.id, - enableUserData: false, // This data is fetched lazily on component render + enableUserData: true, // This will populate the user data query later down the line sortBy, sortOrder, startIndex: page * ApiLimits.Library, diff --git a/src/api/queries/artist/utils/artist.ts b/src/api/queries/artist/utils/artist.ts index dd4b0e58..dfc78acc 100644 --- a/src/api/queries/artist/utils/artist.ts +++ b/src/api/queries/artist/utils/artist.ts @@ -31,7 +31,7 @@ export function fetchArtists( .getAlbumArtists({ parentId: library.musicLibraryId, userId: user.id, - enableUserData: false, // This data is fetched lazily on component render + enableUserData: true, // This will populate the User Data query later down the line sortBy: sortBy, sortOrder: sortOrder, startIndex: page * ApiLimits.Library, diff --git a/src/api/queries/favorites.ts b/src/api/queries/favorites.ts index 276f34e1..2b3d3b98 100644 --- a/src/api/queries/favorites.ts +++ b/src/api/queries/favorites.ts @@ -6,7 +6,6 @@ import { BaseItemKind, ItemSortBy, SortOrder, - UserItemDataDto, } from '@jellyfin/sdk/lib/generated-client/models' import { getItemsApi } from '@jellyfin/sdk/lib/utils/api' import { isUndefined } from 'lodash' @@ -178,33 +177,3 @@ export async function fetchFavoriteTracks( }) }) } - -/** - * Fetches the {@link UserItemDataDto} for a given {@link BaseItemDto} - * @param api The Jellyfin {@link Api} instance - * @param itemId The ID field of the {@link BaseItemDto} to fetch user data for - * @returns The {@link UserItemDataDto} for the given item - */ -export async function fetchUserData( - api: Api | undefined, - user: JellifyUser | undefined, - itemId: string, -): Promise { - return new Promise((resolve, reject) => { - if (isUndefined(api)) return reject('Client instance not set') - if (isUndefined(user)) return reject('User instance not set') - - getItemsApi(api) - .getItemUserData({ - itemId, - userId: user.id, - }) - .then((response) => { - return resolve(response.data) - }) - .catch((error) => { - console.error(error) - return reject(error) - }) - }) -} diff --git a/src/api/queries/frequents.ts b/src/api/queries/frequents.ts index a97ebba8..a9db73cb 100644 --- a/src/api/queries/frequents.ts +++ b/src/api/queries/frequents.ts @@ -35,6 +35,7 @@ export function fetchFrequentlyPlayed( startIndex: page * 100, sortBy: [ItemSortBy.PlayCount], sortOrder: [SortOrder.Descending], + enableUserData: true, }) .then(({ data }) => { if (data.Items) resolve(data.Items) diff --git a/src/api/queries/item.ts b/src/api/queries/item.ts index 6c9c8dd1..cf63fc62 100644 --- a/src/api/queries/item.ts +++ b/src/api/queries/item.ts @@ -29,6 +29,7 @@ export async function fetchItem(api: Api | undefined, itemId: string): Promise { if (response.data.Items && response.data.TotalRecordCount == 1) diff --git a/src/api/queries/media/index.ts b/src/api/queries/media/index.ts index 7af387be..3f05bb72 100644 --- a/src/api/queries/media/index.ts +++ b/src/api/queries/media/index.ts @@ -45,6 +45,7 @@ const useStreamedMediaInfo = (itemId: string | null | undefined) => { return useQuery({ queryKey: mediaInfoQueryKey({ api, user, deviceProfile, itemId }), queryFn: () => fetchMediaInfo(api, user, deviceProfile, itemId), + staleTime: Infinity, // Only refetch when the user's device profile changes }) } @@ -72,5 +73,6 @@ export const useDownloadedMediaInfo = (itemId: string | null | undefined) => { return useQuery({ queryKey: mediaInfoQueryKey({ api, user, deviceProfile, itemId }), queryFn: () => fetchMediaInfo(api, user, deviceProfile, itemId), + staleTime: Infinity, // Only refetch when the user's device profile changes }) } diff --git a/src/api/queries/recents.ts b/src/api/queries/recents.ts index e722bf29..015a7674 100644 --- a/src/api/queries/recents.ts +++ b/src/api/queries/recents.ts @@ -73,6 +73,7 @@ export async function fetchRecentlyPlayed( sortBy: [ItemSortBy.DatePlayed], sortOrder: [SortOrder.Descending], fields: [ItemFields.ParentId], + enableUserData: true, }) .then((response) => { console.debug('Received recently played items response') diff --git a/src/api/queries/tracks.ts b/src/api/queries/tracks.ts index af662906..83a86e67 100644 --- a/src/api/queries/tracks.ts +++ b/src/api/queries/tracks.ts @@ -30,6 +30,7 @@ export function fetchTracks( .getItems({ includeItemTypes: [BaseItemKind.Audio], parentId: library.musicLibraryId, + enableUserData: true, userId: user.id, recursive: true, isFavorite: isFavorite, diff --git a/src/api/queries/user-data/index.ts b/src/api/queries/user-data/index.ts new file mode 100644 index 00000000..ea2ba7b4 --- /dev/null +++ b/src/api/queries/user-data/index.ts @@ -0,0 +1,17 @@ +import { useJellifyContext } from '../../../providers' +import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto' +import { useQuery } from '@tanstack/react-query' +import fetchUserData from './utils' +import UserDataQueryKey from './keys' +import { ONE_MINUTE } from '@/src/constants/query-client' + +export const useIsFavorite = (item: BaseItemDto) => { + const { api, user } = useJellifyContext() + + return useQuery({ + queryKey: UserDataQueryKey(user!, item), + queryFn: () => fetchUserData(api, user, item.Id!), + select: (data) => typeof data === 'object' && data.IsFavorite, + enabled: !!api && !!user && !!item.Id, // Only run if we have the required data + }) +} diff --git a/src/api/queries/user-data/keys.ts b/src/api/queries/user-data/keys.ts new file mode 100644 index 00000000..f848acd6 --- /dev/null +++ b/src/api/queries/user-data/keys.ts @@ -0,0 +1,11 @@ +import { QueryKeys } from '../../../enums/query-keys' +import { JellifyUser } from '../../../types/JellifyUser' +import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client' + +const UserDataQueryKey = (user: JellifyUser, item: BaseItemDto) => [ + QueryKeys.UserData, + user.id, + item.Id, +] + +export default UserDataQueryKey diff --git a/src/api/queries/user-data/utils/index.ts b/src/api/queries/user-data/utils/index.ts new file mode 100644 index 00000000..917e229d --- /dev/null +++ b/src/api/queries/user-data/utils/index.ts @@ -0,0 +1,35 @@ +import { JellifyUser } from '@/src/types/JellifyUser' +import { Api } from '@jellyfin/sdk/lib/api' +import { UserItemDataDto } from '@jellyfin/sdk/lib/generated-client/models/user-item-data-dto' +import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api' +import { isUndefined } from 'lodash' + +/** + * Fetches the {@link UserItemDataDto} for a given {@link BaseItemDto} + * @param api The Jellyfin {@link Api} instance + * @param itemId The ID field of the {@link BaseItemDto} to fetch user data for + * @returns The {@link UserItemDataDto} for the given item + */ +export default async function fetchUserData( + api: Api | undefined, + user: JellifyUser | undefined, + itemId: string, +): Promise { + return new Promise((resolve, reject) => { + if (isUndefined(api)) return reject('Client instance not set') + if (isUndefined(user)) return reject('User instance not set') + + getItemsApi(api) + .getItemUserData({ + itemId, + userId: user.id, + }) + .then((response) => { + return resolve(response.data) + }) + .catch((error) => { + console.error(error) + return reject(error) + }) + }) +} diff --git a/src/components/Albums/component.tsx b/src/components/Albums/component.tsx index 0159543b..4cd24f3f 100644 --- a/src/components/Albums/component.tsx +++ b/src/components/Albums/component.tsx @@ -2,16 +2,13 @@ import { ActivityIndicator, RefreshControl } from 'react-native' import { getToken, Separator, XStack, YStack } from 'tamagui' import React, { RefObject, useEffect, useRef } from 'react' import { Text } from '../Global/helpers/text' -import { FlashList, FlashListRef, ViewToken } from '@shopify/flash-list' +import { FlashList, FlashListRef } from '@shopify/flash-list' import { UseInfiniteQueryResult } from '@tanstack/react-query' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import ItemRow from '../Global/components/item-row' import { useNavigation } from '@react-navigation/native' import LibraryStackParamList from '../../screens/Library/types' import { NativeStackNavigationProp } from '@react-navigation/native-stack' -import { warmItemContext } from '../../hooks/use-item-context' -import { useJellifyContext } from '../../providers' -import useStreamingDeviceProfile from '../../stores/device-profile' import AZScroller, { useAlphabetSelector } from '../Global/components/alphabetical-selector' import { isString } from 'lodash' @@ -28,23 +25,10 @@ export default function Albums({ }: AlbumsProps): React.JSX.Element { const navigation = useNavigation>() - const { api, user } = useJellifyContext() - - const deviceProfile = useStreamingDeviceProfile() - const sectionListRef = useRef>(null) const pendingLetterRef = useRef(null) - const onViewableItemsChangedRef = useRef( - ({ viewableItems }: { viewableItems: ViewToken[] }) => { - viewableItems.forEach(({ isViewable, item }) => { - if (isViewable && typeof item === 'object') - warmItemContext(api, user, item, deviceProfile) - }) - }, - ) - // Memoize expensive stickyHeaderIndices calculation to prevent unnecessary re-computations const stickyHeaderIndices = React.useMemo(() => { if (!showAlphabeticalSelector || !albumsInfiniteQuery.data) return [] @@ -155,7 +139,6 @@ export default function Albums({ } stickyHeaderIndices={stickyHeaderIndices} removeClippedSubviews - onViewableItemsChanged={onViewableItemsChangedRef.current} /> {showAlphabeticalSelector && albumPageParams && ( diff --git a/src/components/Artist/albums.tsx b/src/components/Artist/albums.tsx index 7e8fbfd3..1f4c8018 100644 --- a/src/components/Artist/albums.tsx +++ b/src/components/Artist/albums.tsx @@ -1,26 +1,18 @@ -import React, { useEffect, useRef, useState } from 'react' +import React, { useEffect, useState } from 'react' import { ItemCard } from '../Global/components/item-card' import { ArtistAlbumsProps, ArtistEpsProps, ArtistFeaturedOnProps } from './types' import { Text } from '../Global/helpers/text' import { useArtistContext } from '../../providers/Artist' import { convertRunTimeTicksToSeconds } from '../../utils/runtimeticks' import Animated, { useAnimatedScrollHandler } from 'react-native-reanimated' -import { ActivityIndicator, ViewToken } from 'react-native' +import { ActivityIndicator } from 'react-native' import { useSafeAreaFrame } from 'react-native-safe-area-context' import { getToken } from 'tamagui' import navigationRef from '../../../navigation' -import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto' -import { warmItemContext } from '../../hooks/use-item-context' -import { useJellifyContext } from '../../providers' -import useStreamingDeviceProfile from '../../stores/device-profile' export default function Albums({ route, navigation, }: ArtistAlbumsProps | ArtistEpsProps | ArtistFeaturedOnProps): React.JSX.Element { - const { api, user } = useJellifyContext() - - const deviceProfile = useStreamingDeviceProfile() - const { width } = useSafeAreaFrame() const { albums, fetchingAlbums, featuredOn, scroll } = useArtistContext() const scrollHandler = useAnimatedScrollHandler({ @@ -30,14 +22,6 @@ export default function Albums({ }, }) - const onViewableItemsChangedRef = useRef( - ({ viewableItems }: { viewableItems: ViewToken[] }) => { - viewableItems.forEach(({ isViewable, item }) => { - if (isViewable) warmItemContext(api, user, item, deviceProfile) - }) - }, - ) - const [columns, setColumns] = useState(Math.floor(width / getToken('$20'))) useEffect(() => { @@ -113,7 +97,6 @@ export default function Albums({ ) } removeClippedSubviews - onViewableItemsChanged={onViewableItemsChangedRef.current} /> ) } diff --git a/src/components/Artists/component.tsx b/src/components/Artists/component.tsx index bbef1af8..c8e76117 100644 --- a/src/components/Artists/component.tsx +++ b/src/components/Artists/component.tsx @@ -5,16 +5,13 @@ import { RefreshControl } from 'react-native' import ItemRow from '../Global/components/item-row' import { useLibrarySortAndFilterContext } from '../../providers/Library' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto' -import { FlashList, FlashListRef, ViewToken } from '@shopify/flash-list' +import { FlashList, FlashListRef } from '@shopify/flash-list' import AZScroller, { useAlphabetSelector } from '../Global/components/alphabetical-selector' -import { UseInfiniteQueryResult, useMutation } from '@tanstack/react-query' +import { UseInfiniteQueryResult } from '@tanstack/react-query' import { isString } from 'lodash' import { useNavigation } from '@react-navigation/native' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import LibraryStackParamList from '../../screens/Library/types' -import { warmItemContext } from '../../hooks/use-item-context' -import { useJellifyContext } from '../../providers' -import useStreamingDeviceProfile from '../../stores/device-profile' export interface ArtistsProps { artistsInfiniteQuery: UseInfiniteQueryResult< @@ -39,10 +36,6 @@ export default function Artists({ }: ArtistsProps): React.JSX.Element { const theme = useTheme() - const { api, user } = useJellifyContext() - - const deviceProfile = useStreamingDeviceProfile() - const { isFavorites } = useLibrarySortAndFilterContext() const navigation = useNavigation>() @@ -52,15 +45,6 @@ export default function Artists({ const pendingLetterRef = useRef(null) - const onViewableItemsChangedRef = useRef( - ({ viewableItems }: { viewableItems: ViewToken[] }) => { - viewableItems.forEach(({ isViewable, item }) => { - if (isViewable && typeof item === 'object') - warmItemContext(api, user, item, deviceProfile) - }) - }, - ) - const { mutate: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } = useAlphabetSelector((letter) => (pendingLetterRef.current = letter.toUpperCase())) @@ -177,7 +161,6 @@ export default function Artists({ }} // onEndReachedThreshold default is 0.5 removeClippedSubviews - onViewableItemsChanged={onViewableItemsChangedRef.current} /> {showAlphabeticalSelector && artistPageParams && ( diff --git a/src/components/Global/components/favorite-button.tsx b/src/components/Global/components/favorite-button.tsx index 1001bd2a..a15d4cd8 100644 --- a/src/components/Global/components/favorite-button.tsx +++ b/src/components/Global/components/favorite-button.tsx @@ -1,44 +1,22 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import React from 'react' import Icon from './icon' -import { useQuery } from '@tanstack/react-query' import { isUndefined } from 'lodash' -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' +import { useIsFavorite } from '../../../api/queries/user-data' -interface SetFavoriteMutation { - item: BaseItemDto -} - -export default function FavoriteButton({ - item, - onToggle, -}: { +interface FavoriteButtonProps { item: BaseItemDto onToggle?: () => void -}): React.JSX.Element { - const { api, user } = useJellifyContext() +} + +export default function FavoriteButton({ item, onToggle }: FavoriteButtonProps): React.JSX.Element { const { toggleFavorite } = useJellifyUserDataContext() - const { - data: isFavorite, - isFetching, - refetch, - } = useQuery({ - queryKey: [QueryKeys.UserData, item.Id], - queryFn: () => fetchUserData(api, user, item.Id!), - select: (data) => typeof data === 'object' && data.IsFavorite, - staleTime: ONE_HOUR, - }) + const { data: isFavorite } = useIsFavorite(item) - return isFetching ? ( - - ) : isFavorite ? ( + return isFavorite ? ( fetchUserData(api, user, item.Id!), - select: (data) => typeof data === 'object' && data.IsFavorite, - staleTime: ONE_HOUR, - }) + const { data: isFavorite, refetch } = useIsFavorite(item) return isFavorite ? ( fetchUserData(api, user, item.Id!), - select: (data) => typeof data === 'object' && data.IsFavorite, - enabled: !!api && !!user && !!item.Id, // Only run if we have the required data - }) + const { data: isFavorite } = useIsFavorite(item) return isFavorite ? ( diff --git a/src/components/Global/components/horizontal-list.tsx b/src/components/Global/components/horizontal-list.tsx index d60607bb..ad0cc101 100644 --- a/src/components/Global/components/horizontal-list.tsx +++ b/src/components/Global/components/horizontal-list.tsx @@ -1,9 +1,6 @@ -import useStreamingDeviceProfile from '../../../stores/device-profile' -import { warmItemContext } from '../../../hooks/use-item-context' -import { useJellifyContext } from '../../../providers' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto' -import { FlashList, FlashListProps, ViewToken } from '@shopify/flash-list' -import React, { useRef } from 'react' +import { FlashList, FlashListProps } from '@shopify/flash-list' +import React from 'react' interface HorizontalCardListProps extends FlashListProps {} @@ -16,23 +13,10 @@ interface HorizontalCardListProps extends FlashListProps {} export default function HorizontalCardList({ ...props }: HorizontalCardListProps): React.JSX.Element { - const { api, user } = useJellifyContext() - - const deviceProfile = useStreamingDeviceProfile() - - const onViewableItemsChangedRef = useRef( - ({ viewableItems }: { viewableItems: ViewToken[] }) => { - viewableItems - .filter(({ isViewable }) => isViewable) - .forEach(({ item }) => warmItemContext(api, user, item, deviceProfile)) - }, - ) - return ( { switch (item.Type) { case 'Audio': { @@ -82,6 +85,7 @@ export default function ItemRow({ alignContent='center' minHeight={'$7'} width={'100%'} + onPressIn={warmContext} onLongPress={() => { navigationRef.navigate('Context', { item, diff --git a/src/components/Global/components/track.tsx b/src/components/Global/components/track.tsx index dcd786e5..eca98df2 100644 --- a/src/components/Global/components/track.tsx +++ b/src/components/Global/components/track.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useCallback } from 'react' +import React, { useMemo, useCallback, useEffect } from 'react' import { getToken, Theme, useTheme, XStack, YStack } from 'tamagui' import { Text } from '../helpers/text' import { RunTimeTicks } from '../helpers/time-codes' @@ -69,7 +69,7 @@ export default function Track({ const offlineAudio = useDownloadedTrack(track.Id) - useItemContext(track) + const warmContext = useItemContext(track) // Memoize expensive computations const isPlaying = useMemo( @@ -159,6 +159,10 @@ export default function Track({ [showArtwork, track.Artists], ) + useEffect(() => { + warmContext() + }, [track]) + return ( - + {isFavorites ? 'Favorites' : 'All'} diff --git a/src/components/Playlists/component.tsx b/src/components/Playlists/component.tsx index 753a9f86..8b081427 100644 --- a/src/components/Playlists/component.tsx +++ b/src/components/Playlists/component.tsx @@ -1,16 +1,12 @@ import { RefreshControl } from 'react-native-gesture-handler' import { Separator } from 'tamagui' -import { FlashList, ViewToken } from '@shopify/flash-list' +import { FlashList } from '@shopify/flash-list' import ItemRow from '../Global/components/item-row' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { FetchNextPageOptions } from '@tanstack/react-query' import { useNavigation } from '@react-navigation/native' import { BaseStackParamList } from '@/src/screens/types' import { NativeStackNavigationProp } from '@react-navigation/native-stack' -import { useRef } from 'react' -import { warmItemContext } from '../../hooks/use-item-context' -import { useJellifyContext } from '../../providers' -import useStreamingDeviceProfile from '../../stores/device-profile' export interface PlaylistsProps { canEdit?: boolean | undefined @@ -32,18 +28,6 @@ export default function Playlists({ }: PlaylistsProps): React.JSX.Element { const navigation = useNavigation>() - const { api, user } = useJellifyContext() - - const deviceProfile = useStreamingDeviceProfile() - - const onViewableItemsChangedRef = useRef( - ({ viewableItems }: { viewableItems: ViewToken[] }) => { - viewableItems.forEach(({ isViewable, item }) => { - if (isViewable) warmItemContext(api, user, item, deviceProfile) - }) - }, - ) - return ( ) } diff --git a/src/components/Tracks/component.tsx b/src/components/Tracks/component.tsx index df499cb2..734b2e03 100644 --- a/src/components/Tracks/component.tsx +++ b/src/components/Tracks/component.tsx @@ -1,17 +1,15 @@ -import React, { useRef } from 'react' +import React from 'react' import Track from '../Global/components/track' import { getTokens, Separator } from 'tamagui' import { BaseItemDto, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client/models' import { Queue } from '../../player/types/queue-item' import { queryClient } from '../../constants/query-client' -import { QueryKeys } from '../../enums/query-keys' -import { FlashList, ViewToken } from '@shopify/flash-list' +import { FlashList } from '@shopify/flash-list' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { BaseStackParamList } from '../../screens/types' -import { warmItemContext } from '../../hooks/use-item-context' -import { useJellifyContext } from '../../providers' -import useStreamingDeviceProfile from '../../stores/device-profile' import { useAllDownloadedTracks } from '../../api/queries/download' +import UserDataQueryKey from '../../api/queries/user-data/keys' +import { useJellifyContext } from '../../providers' interface TracksProps { tracks: (string | number | BaseItemDto)[] | undefined @@ -32,9 +30,8 @@ export default function Tracks({ filterDownloaded, filterFavorites, }: TracksProps): React.JSX.Element { - const { api, user } = useJellifyContext() + const { user } = useJellifyContext() - const deviceProfile = useStreamingDeviceProfile() const { data: downloadedTracks } = useAllDownloadedTracks() // Memoize the expensive tracks processing to prevent memory leaks @@ -47,10 +44,9 @@ export default function Tracks({ if (filterFavorites) { return ( ( - queryClient.getQueryData([ - QueryKeys.UserData, - downloadedTrack.Id, - ]) as UserItemDataDto | undefined + queryClient.getQueryData( + UserDataQueryKey(user!, downloadedTrack), + ) as UserItemDataDto | undefined )?.IsFavorite ?? false ) } @@ -79,14 +75,6 @@ export default function Tracks({ [tracksToDisplay, queue], ) - const onViewableItemsChangedRef = useRef( - ({ viewableItems }: { viewableItems: ViewToken[] }) => { - viewableItems.forEach(({ isViewable, item }) => { - if (isViewable) warmItemContext(api, user, item, deviceProfile) - }) - }, - ) - return ( void { const { api, user } = useJellifyContext() const streamingDeviceProfile = useStreamingDeviceProfile() @@ -20,7 +21,7 @@ export default function useItemContext(item: BaseItemDto): void { const prefetchedContext = useRef>(new Set()) - useEffect(() => { + return useCallback(() => { const effectSig = `${item.Id}-${item.Type}` // If we've already warmed the cache for this item, return @@ -33,7 +34,7 @@ export default function useItemContext(item: BaseItemDto): void { }, [api, user, streamingDeviceProfile]) } -export function warmItemContext( +function warmItemContext( api: Api | undefined, user: JellifyUser | undefined, item: BaseItemDto, @@ -72,12 +73,11 @@ export function warmItemContext( }), }) - const userDataQueryKey = [QueryKeys.UserData, Id] - if (queryClient.getQueryState(userDataQueryKey)?.status !== 'success') { - if (UserData) queryClient.setQueryData([QueryKeys.UserData, Id], UserData) + if (queryClient.getQueryState(UserDataQueryKey(user!, item))?.status !== 'success') { + if (UserData) queryClient.setQueryData(UserDataQueryKey(user!, item), UserData) else queryClient.ensureQueryData({ - queryKey: [], + queryKey: UserDataQueryKey(user!, item), queryFn: () => fetchUserData(api, user, Id), }) } diff --git a/src/providers/UserData/index.tsx b/src/providers/UserData/index.tsx index e4f78387..00f0f4a5 100644 --- a/src/providers/UserData/index.tsx +++ b/src/providers/UserData/index.tsx @@ -8,6 +8,7 @@ import { QueryKeys } from '../../enums/query-keys' import Toast from 'react-native-toast-message' import { useJellifyContext } from '..' import useHapticFeedback from '../../hooks/use-haptic-feedback' +import UserDataQueryKey from '../../api/queries/user-data/keys' interface SetFavoriteMutation { item: BaseItemDto @@ -19,7 +20,7 @@ interface JellifyUserDataContext { } const JellifyUserDataContextInitializer = () => { - const { api } = useJellifyContext() + const { api, user } = useJellifyContext() const trigger = useHapticFeedback() @@ -45,7 +46,7 @@ const JellifyUserDataContextInitializer = () => { if (onToggle) onToggle() // Force refresh of track user data - queryClient.invalidateQueries({ queryKey: [QueryKeys.UserData, item.Id] }) + queryClient.invalidateQueries({ queryKey: UserDataQueryKey(user!, item) }) }, }) @@ -70,7 +71,7 @@ const JellifyUserDataContextInitializer = () => { if (onToggle) onToggle() // Force refresh of track user data - queryClient.invalidateQueries({ queryKey: [QueryKeys.UserData, item.Id] }) + queryClient.invalidateQueries({ queryKey: UserDataQueryKey(user!, item) }) }, }) diff --git a/yarn.lock b/yarn.lock index 41e6df85..cb82b54c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3402,38 +3402,38 @@ resolved "https://registry.yarnpkg.com/@tamagui/z-index-stack/-/z-index-stack-1.132.23.tgz#a74f06f3b6a6191951f396105f39a10aec0144aa" integrity sha512-djbRW7FWzuc9bCIVXG00pVa6McM8/H8R4JOL+szxSy1iAo0P2k0OzWfBb+ZbbjTye068fBPGIniq4X7+3huS1Q== -"@tanstack/query-async-storage-persister@^5.85.6": - version "5.85.6" - resolved "https://registry.yarnpkg.com/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.85.6.tgz#a215d1a78fe23efab45b6f31d942cfe4515c5c36" - integrity sha512-f2C0tMVEo6oFdcNE1xOYGJ5KB02cIEDPjbWuqEivJfrlWDqsPlnHnfxTYkuMcddTHTClDf/sqtjuB95zggeEsQ== +"@tanstack/query-async-storage-persister@^5.85.9": + version "5.85.9" + resolved "https://registry.yarnpkg.com/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.85.9.tgz#9f80f4f612a35b67cb4bee4238f1c24adb29515d" + integrity sha512-6+HevrTPQ8TbJ3iMPY9NckQ4eLTIHgoZZflQVIKKcpb7Y08OQ15kw8skm7CCkTZClsTz1+JNr9heuMCBluO+rw== dependencies: - "@tanstack/query-persist-client-core" "5.85.6" + "@tanstack/query-persist-client-core" "5.85.9" -"@tanstack/query-core@5.85.6": - version "5.85.6" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.85.6.tgz#2af90f3c56c38fd2194e0ed1996122373c4bfca5" - integrity sha512-hCj0TktzdCv2bCepIdfwqVwUVWb+GSHm1Jnn8w+40lfhQ3m7lCO7ADRUJy+2unxQ/nzjh2ipC6ye69NDW3l73g== +"@tanstack/query-core@5.85.9": + version "5.85.9" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.85.9.tgz#69e800f4d1c42ca4a110469229d8ecca409d477a" + integrity sha512-5fxb9vwyftYE6KFLhhhDyLr8NO75+Wpu7pmTo+TkwKmMX2oxZDoLwcqGP8ItKSpUMwk3urWgQDZfyWr5Jm9LsQ== -"@tanstack/query-persist-client-core@5.85.6": - version "5.85.6" - resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.85.6.tgz#92f9d3ac83f51b0bd452fe3b65047f4196c3f56a" - integrity sha512-wUdoEurIC0YCNZzR020Xcg3OsJeF4SXmEPqlNwZ6EaGKgWeNjU17hVdK+X4ZeirUm+h0muiEQx+aIQU1lk7roQ== +"@tanstack/query-persist-client-core@5.85.9": + version "5.85.9" + resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.85.9.tgz#2d7e89d37195a307d898d850652c1f4fca77158d" + integrity sha512-er7HfMjn6TQanWG5nudjRNZbo7ahf7IIdWN5kU7L2qRZ2kcw89TQZAZ74GIQsumOXZD7sUcHG2dylveFZNxlZA== dependencies: - "@tanstack/query-core" "5.85.6" + "@tanstack/query-core" "5.85.9" -"@tanstack/react-query-persist-client@^5.85.6": - version "5.85.6" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.85.6.tgz#40c59526f55dc4ee5ac8ddb5bc777e27e2256627" - integrity sha512-zLUfm8JlI6/s0AqvX5l5CcazdHwj5gwcv0mWYOaJJvADyFzl2wwQKqB/H4nYSeygUtrepBgPwVQKNqH9ZwlZpQ== +"@tanstack/react-query-persist-client@^5.85.9": + version "5.85.9" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.85.9.tgz#5e01d5d0744098fa7d0feefc397068092fcdfcb5" + integrity sha512-h75Xyt/XDtk1oRmli5znQRXclT/iZpseTK7ScqEjaiZmPSREgg2mJb1blo2SB1swSidDNsCtnENLNH43PP0/9w== dependencies: - "@tanstack/query-persist-client-core" "5.85.6" + "@tanstack/query-persist-client-core" "5.85.9" -"@tanstack/react-query@^5.85.6": - version "5.85.6" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.85.6.tgz#0885cd9e02f8a5aa228f6b5dc2122d22ba597d68" - integrity sha512-VUAag4ERjh+qlmg0wNivQIVCZUrYndqYu3/wPCVZd4r0E+1IqotbeyGTc+ICroL/PqbpSaGZg02zSWYfcvxbdA== +"@tanstack/react-query@^5.85.9": + version "5.85.9" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.85.9.tgz#d27315b3f327af31237d513e505f3e6a2cad5739" + integrity sha512-2T5zgSpcOZXGkH/UObIbIkGmUPQqZqn7esVQFXLOze622h4spgWf5jmvrqAo9dnI13/hyMcNsF1jsoDcb59nJQ== dependencies: - "@tanstack/query-core" "5.85.6" + "@tanstack/query-core" "5.85.9" "@telemetrydeck/sdk@^2.0.4": version "2.0.4"