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
This commit is contained in:
Violet Caulfield
2025-09-03 08:47:46 -05:00
committed by GitHub
parent 1d380e63cc
commit 676f5fa199
28 changed files with 155 additions and 241 deletions

View File

@@ -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",

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<UserItemDataDto | void> {
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)
})
})
}

View File

@@ -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)

View File

@@ -29,6 +29,7 @@ export async function fetchItem(api: Api | undefined, itemId: string): Promise<B
.getItems({
ids: [itemId],
fields: [ItemFields.Tags],
enableUserData: true,
})
.then((response) => {
if (response.data.Items && response.data.TotalRecordCount == 1)

View File

@@ -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
})
}

View File

@@ -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')

View File

@@ -30,6 +30,7 @@ export function fetchTracks(
.getItems({
includeItemTypes: [BaseItemKind.Audio],
parentId: library.musicLibraryId,
enableUserData: true,
userId: user.id,
recursive: true,
isFavorite: isFavorite,

View File

@@ -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
})
}

View File

@@ -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

View File

@@ -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<UserItemDataDto | void> {
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)
})
})
}

View File

@@ -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<NativeStackNavigationProp<LibraryStackParamList>>()
const { api, user } = useJellifyContext()
const deviceProfile = useStreamingDeviceProfile()
const sectionListRef = useRef<FlashListRef<string | number | BaseItemDto>>(null)
const pendingLetterRef = useRef<string | null>(null)
const onViewableItemsChangedRef = useRef(
({ viewableItems }: { viewableItems: ViewToken<string | number | BaseItemDto>[] }) => {
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 && (

View File

@@ -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<BaseItemDto>[] }) => {
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}
/>
)
}

View File

@@ -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<NativeStackNavigationProp<LibraryStackParamList>>()
@@ -52,15 +45,6 @@ export default function Artists({
const pendingLetterRef = useRef<string | null>(null)
const onViewableItemsChangedRef = useRef(
({ viewableItems }: { viewableItems: ViewToken<string | number | BaseItemDto>[] }) => {
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 && (

View File

@@ -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 ? (
<Spinner alignSelf='center' />
) : isFavorite ? (
return isFavorite ? (
<Animated.View entering={FadeIn} exiting={FadeOut}>
<Icon
name={'heart'}

View File

@@ -1,25 +1,15 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../../enums/query-keys'
import { fetchUserData } from '../../../api/queries/favorites'
import { useJellifyContext } from '../../../providers'
import { ListItem, XStack } from 'tamagui'
import Icon from './icon'
import { useJellifyUserDataContext } from '../../../providers/UserData'
import { Text } from '../helpers/text'
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
import { ONE_HOUR } from '../../../constants/query-client'
import { useIsFavorite } from '../../../api/queries/user-data'
export default function FavoriteContextMenuRow({ item }: { item: BaseItemDto }): React.JSX.Element {
const { api, user } = useJellifyContext()
const { toggleFavorite } = useJellifyUserDataContext()
const { data: isFavorite, 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, refetch } = useIsFavorite(item)
return isFavorite ? (
<ListItem

View File

@@ -1,12 +1,9 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { Spacer } from 'tamagui'
import Icon from './icon'
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../../enums/query-keys'
import { fetchUserData } from '../../../api/queries/favorites'
import { memo } from 'react'
import { useJellifyContext } from '../../../providers'
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
import { useIsFavorite } from '../../../api/queries/user-data'
/**
* This component is used to display a favorite icon for a given item.
@@ -16,14 +13,7 @@ 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 { api, user } = useJellifyContext()
const { data: isFavorite } = useQuery({
queryKey: [QueryKeys.UserData, item.Id],
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
})
const { data: isFavorite } = useIsFavorite(item)
return isFavorite ? (
<Animated.View entering={FadeIn} exiting={FadeOut}>

View File

@@ -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<BaseItemDto> {}
@@ -16,23 +13,10 @@ interface HorizontalCardListProps extends FlashListProps<BaseItemDto> {}
export default function HorizontalCardList({
...props
}: HorizontalCardListProps): React.JSX.Element {
const { api, user } = useJellifyContext()
const deviceProfile = useStreamingDeviceProfile()
const onViewableItemsChangedRef = useRef(
({ viewableItems }: { viewableItems: ViewToken<BaseItemDto>[] }) => {
viewableItems
.filter(({ isViewable }) => isViewable)
.forEach(({ item }) => warmItemContext(api, user, item, deviceProfile))
},
)
return (
<FlashList
horizontal
data={props.data}
onViewableItemsChanged={onViewableItemsChangedRef.current}
renderItem={props.renderItem}
removeClippedSubviews
style={{

View File

@@ -4,6 +4,7 @@ import { getToken, Card as TamaguiCard, View, YStack } from 'tamagui'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { Text } from '../helpers/text'
import ItemImage from './image'
import useItemContext from '../../../hooks/use-item-context'
interface CardProps extends TamaguiCardProps {
caption?: string | null | undefined
@@ -21,6 +22,8 @@ interface CardProps extends TamaguiCardProps {
* @param props
*/
export function ItemCard(props: CardProps) {
const warmContext = useItemContext(props.item)
return (
<View alignItems='center' margin={'$1.5'}>
<TamaguiCard
@@ -32,6 +35,7 @@ export function ItemCard(props: CardProps) {
circular={!props.squared}
borderRadius={props.squared ? '$5' : 'unset'}
animation='bouncy'
onPressIn={warmContext}
hoverStyle={props.onPress ? { scale: 0.925 } : {}}
pressStyle={props.onPress ? { scale: 0.875 } : {}}
{...props}

View File

@@ -15,6 +15,7 @@ import { useLoadNewQueue } from '../../../providers/Player/hooks/mutations'
import { useJellifyContext } from '../../../providers'
import { useNetworkStatus } from '../../../stores/network'
import useStreamingDeviceProfile from '../../../stores/device-profile'
import useItemContext from '../../../hooks/use-item-context'
interface ItemRowProps {
item: BaseItemDto
@@ -49,6 +50,8 @@ export default function ItemRow({
const { mutate: loadNewQueue } = useLoadNewQueue()
const warmContext = useItemContext(item)
const gestureCallback = () => {
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,

View File

@@ -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 (
<Theme name={invertedColors ? 'inverted_purple' : undefined}>
<XStack
@@ -167,6 +171,7 @@ export default function Track({
height={showArtwork ? '$6' : '$5'}
flex={1}
testID={testID ?? undefined}
onPressIn={warmContext}
onPress={handlePress}
onLongPress={handleLongPress}
paddingVertical={'$2'}

View File

@@ -58,10 +58,10 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
>
<Icon
name={isFavorites ? 'heart' : 'heart-outline'}
color={isFavorites ? '$secondary' : '$borderColor'}
color={isFavorites ? '$primary' : '$borderColor'}
/>
<Text color={isFavorites ? '$secondary' : '$borderColor'}>
<Text color={isFavorites ? '$primary' : '$borderColor'}>
{isFavorites ? 'Favorites' : 'All'}
</Text>
</XStack>

View File

@@ -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<NativeStackNavigationProp<BaseStackParamList>>()
const { api, user } = useJellifyContext()
const deviceProfile = useStreamingDeviceProfile()
const onViewableItemsChangedRef = useRef(
({ viewableItems }: { viewableItems: ViewToken<BaseItemDto>[] }) => {
viewableItems.forEach(({ isViewable, item }) => {
if (isViewable) warmItemContext(api, user, item, deviceProfile)
})
},
)
return (
<FlashList
contentInsetAdjustmentBehavior='automatic'
@@ -68,7 +52,6 @@ export default function Playlists({
}
}}
removeClippedSubviews
onViewableItemsChanged={onViewableItemsChangedRef.current}
/>
)
}

View File

@@ -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<BaseItemDto>[] }) => {
viewableItems.forEach(({ isViewable, item }) => {
if (isViewable) warmItemContext(api, user, item, deviceProfile)
})
},
)
return (
<FlashList
contentInsetAdjustmentBehavior='automatic'

View File

@@ -6,12 +6,13 @@ import { QueryKeys } from '../enums/query-keys'
import { fetchMediaInfo } from '../api/queries/media/utils'
import { fetchAlbumDiscs, fetchItem } from '../api/queries/item'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { fetchUserData } from '../api/queries/favorites'
import fetchUserData from '../api/queries/user-data/utils'
import { useJellifyContext } from '../providers'
import { useEffect, useRef } from 'react'
import { useCallback, useRef } from 'react'
import useStreamingDeviceProfile, { useDownloadingDeviceProfile } from '../stores/device-profile'
import UserDataQueryKey from '../api/queries/user-data/keys'
export default function useItemContext(item: BaseItemDto): void {
export default function useItemContext(item: BaseItemDto): () => void {
const { api, user } = useJellifyContext()
const streamingDeviceProfile = useStreamingDeviceProfile()
@@ -20,7 +21,7 @@ export default function useItemContext(item: BaseItemDto): void {
const prefetchedContext = useRef<Set<string>>(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),
})
}

View File

@@ -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) })
},
})

View File

@@ -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"