diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 64d9c174..250daa31 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2719,7 +2719,7 @@ PODS: - RNWorklets - SocketRocket - Yoga - - RNScreens (4.15.0): + - RNScreens (4.15.2): - boost - DoubleConversion - fast_float @@ -2746,10 +2746,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 4.15.0) + - RNScreens/common (= 4.15.2) - SocketRocket - Yoga - - RNScreens/common (4.15.0): + - RNScreens/common (4.15.2): - boost - DoubleConversion - fast_float @@ -3306,7 +3306,7 @@ SPEC CHECKSUMS: RNGestureHandler: 3a73f098d74712952870e948b3d9cf7b6cae9961 RNReactNativeHapticFeedback: be4f1b4bf0398c30b59b76ed92ecb0a2ff3a69c6 RNReanimated: ee96d03fe3713993a30cc205522792b4cb08e4f9 - RNScreens: 48bbaca97a5f9aedc3e52bd48673efd2b6aac4f6 + RNScreens: 8d88d38778e35ce95abeb228d3b5ea0c6e635cad RNSentry: 95e1ed0ede28a4af58aaafedeac9fcfaba0e89ce RNWorklets: e8335dff9d27004709f58316985769040cd1e8f2 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d diff --git a/package.json b/package.json index fd9d0e8c..b1cecdf2 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "react-native-pager-view": "^7.0.0", "react-native-reanimated": "4.0.2", "react-native-safe-area-context": "^5.6.0", - "react-native-screens": "4.15.0", + "react-native-screens": "4.15.2", "react-native-swipeable-item": "^2.0.9", "react-native-text-ticker": "^1.15.0", "react-native-toast-message": "^2.3.3", diff --git a/src/components/Artist/similar.tsx b/src/components/Artist/similar.tsx index 732c6bdb..0009a9db 100644 --- a/src/components/Artist/similar.tsx +++ b/src/components/Artist/similar.tsx @@ -54,6 +54,7 @@ export default function SimilarArtists(): React.JSX.Element { ) } onScroll={scrollHandler} + removeClippedSubviews /> ) } diff --git a/src/components/Player/components/song-info.tsx b/src/components/Player/components/song-info.tsx index cdeb6488..6f28e627 100644 --- a/src/components/Player/components/song-info.tsx +++ b/src/components/Player/components/song-info.tsx @@ -2,7 +2,6 @@ import TextTicker from 'react-native-text-ticker' import { getToken, XStack, YStack } from 'tamagui' import { TextTickerConfig } from '../component.config' import { Text } from '../../Global/helpers/text' -import { NativeStackNavigationProp } from '@react-navigation/native-stack' import React, { useCallback, useMemo, memo } from 'react' import ItemImage from '../../Global/components/image' import { useQuery } from '@tanstack/react-query' @@ -10,17 +9,13 @@ import { fetchItem } from '../../../api/queries/item' import { useJellifyContext } from '../../../providers' import FavoriteButton from '../../Global/components/favorite-button' import { QueryKeys } from '../../../enums/query-keys' -import { PlayerParamList } from '../../../screens/Player/types' import { useNowPlayingContext } from '../../../providers/Player' import navigationRef from '../../../../navigation' import Icon from '../../Global/components/icon' import { getItemName } from '../../../utils/text' +import { CommonActions } from '@react-navigation/native' -interface SongInfoProps { - navigation: NativeStackNavigationProp -} - -function SongInfo({ navigation }: SongInfoProps): React.JSX.Element { +export default function SongInfo(): React.JSX.Element { const { api } = useJellifyContext() const nowPlaying = useNowPlayingContext() @@ -43,39 +38,25 @@ function SongInfo({ navigation }: SongInfoProps): React.JSX.Element { // Memoize navigation handlers const handleAlbumPress = useCallback(() => { if (album) { - navigation.goBack() // Dismiss player modal - navigationRef.navigate('Tabs', { - screen: 'LibraryTab', - params: { - screen: 'Album', - params: { - album, - }, - }, - }) + navigationRef.goBack() // Dismiss player modal + navigationRef.dispatch(CommonActions.navigate('Album', { album })) } - }, [album, navigation]) + }, [album]) const handleArtistPress = useCallback(() => { if (artistItems) { if (artistItems.length > 1) { - navigation.navigate('MultipleArtistsSheet', { - artists: artistItems, - }) + navigationRef.dispatch( + CommonActions.navigate('MultipleArtistsSheet', { + artists: artistItems, + }), + ) } else { - navigation.goBack() // Dismiss player modal - navigationRef.navigate('Tabs', { - screen: 'LibraryTab', - params: { - screen: 'Artist', - params: { - artist: artistItems[0], - }, - }, - }) + navigationRef.goBack() // Dismiss player modal + navigationRef.dispatch(CommonActions.navigate('Artist', { artist: artistItems[0] })) } } - }, [artistItems, navigation]) + }, [artistItems]) return ( @@ -108,9 +89,3 @@ function SongInfo({ navigation }: SongInfoProps): React.JSX.Element { ) } - -// Memoize the component to prevent unnecessary re-renders -export default memo(SongInfo, (prevProps: SongInfoProps, nextProps: SongInfoProps) => { - // Only re-render if navigation changes (which it shouldn't) - return prevProps.navigation === nextProps.navigation -}) diff --git a/src/components/Player/index.tsx b/src/components/Player/index.tsx index 56248de8..483744e0 100644 --- a/src/components/Player/index.tsx +++ b/src/components/Player/index.tsx @@ -1,16 +1,7 @@ import { useNowPlayingContext } from '../../providers/Player' import React, { useCallback, useMemo, useState } from 'react' -import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' -import { - YStack, - XStack, - getToken, - useTheme, - ZStack, - useWindowDimensions, - View, - getTokenValue, -} from 'tamagui' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { YStack, useTheme, ZStack, useWindowDimensions, View, getTokenValue } from 'tamagui' import Scrubber from './components/scrubber' import Controls from './components/controls' import Toast from 'react-native-toast-message' @@ -21,15 +12,9 @@ import BlurredBackground from './components/blurred-background' import PlayerHeader from './components/header' import SongInfo from './components/song-info' import { usePerformanceMonitor } from '../../hooks/use-performance-monitor' -import { NativeStackNavigationProp } from '@react-navigation/native-stack' -import { PlayerParamList } from '../../screens/Player/types' import { Platform } from 'react-native' -export default function PlayerScreen({ - navigation, -}: { - navigation: NativeStackNavigationProp -}): React.JSX.Element { +export default function PlayerScreen(): React.JSX.Element { const performanceMetrics = usePerformanceMonitor('PlayerScreen', 5) const [showToast, setShowToast] = useState(true) @@ -84,7 +69,7 @@ export default function PlayerScreen({ - + {/* playback progress goes here */} diff --git a/src/providers/Item/index.tsx b/src/providers/Item/index.tsx index 016aae16..f988358d 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, useEffect } from 'react' +import { createContext, ReactNode, useEffect, useRef } from 'react' import { useQuery } from '@tanstack/react-query' import { QueryKeys } from '../../enums/query-keys' import { fetchMediaInfo } from '../../api/queries/media' @@ -11,6 +11,7 @@ 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' +import { usePerformanceMonitor } from '../../hooks/use-performance-monitor' interface ItemContext { item: BaseItemDto @@ -43,6 +44,8 @@ export const ItemProvider: ({ item, children }: ItemProviderProps) => React.JSX. item, children, }) => { + const perfMonitor = usePerformanceMonitor('ItemProvider', 5) + const { api, user } = useJellifyContext() const streamingQuality = useStreamingQualityContext() @@ -51,9 +54,20 @@ export const ItemProvider: ({ item, children }: ItemProviderProps) => React.JSX. const artistIds = ArtistItems?.map(({ Id }) => Id) ?? [] + const prefetchedContext = useRef>({}) + useEffect(() => { // Fail fast if we don't have an Item ID to work with if (!Id) return + + const effectSig = `${Id}-${Type}` + + // If we've already warmed the cache for this item, return + if (prefetchedContext.current[effectSig]) return + prefetchedContext.current[effectSig] = true + + console.debug(`Warming context query cache for item ${Id}`) + /** * Fetch and cache the media sources if this item is a track */ @@ -123,7 +137,7 @@ export const ItemProvider: ({ item, children }: ItemProviderProps) => React.JSX. queryKey: [QueryKeys.UserData, Id], queryFn: () => fetchUserData(api, user, Id), }) - }, [queryClient, api, user, Id, Type, AlbumId, UserData, item, streamingQuality]) + }, [queryClient, api?.basePath, user?.id, Id, streamingQuality]) return ( diff --git a/src/providers/Item/item-artists.tsx b/src/providers/Item/item-artists.tsx index bd95aed8..d494af3f 100644 --- a/src/providers/Item/item-artists.tsx +++ b/src/providers/Item/item-artists.tsx @@ -1,4 +1,4 @@ -import { createContext, useEffect } from 'react' +import { createContext, useEffect, useRef } from 'react' import { useJellifyContext } from '..' import { QueryKeys } from '../../enums/query-keys' import { fetchItem } from '../../api/queries/item' @@ -19,16 +19,28 @@ export const ItemArtistProvider: ({ }) => React.JSX.Element = ({ artistId }) => { const { api } = useJellifyContext() + const prefetchedContext = useRef>({}) + useEffect(() => { + // Fail fast if we don't have an artist ID to work with + if (!artistId) return + + const effectSig = artistId + + // If we've already warmed the cache for this artist, return + if (prefetchedContext.current[effectSig]) return + + prefetchedContext.current[effectSig] = true + + console.debug(`Warming context cache for artist ${artistId}`) /** * Store queryable of artist item */ - if (artistId) - queryClient.ensureQueryData({ - queryKey: [QueryKeys.ArtistById, artistId], - queryFn: () => fetchItem(api, artistId!), - }) - }) + queryClient.ensureQueryData({ + queryKey: [QueryKeys.ArtistById, artistId], + queryFn: () => fetchItem(api, artistId!), + }) + }, [api, artistId]) return } diff --git a/src/providers/Player/queue.tsx b/src/providers/Player/queue.tsx index efaa5749..d1d889a0 100644 --- a/src/providers/Player/queue.tsx +++ b/src/providers/Player/queue.tsx @@ -348,11 +348,6 @@ const QueueContextInitailizer = () => { console.debug( `Queued ${queue.length} tracks, starting at ${finalStartIndex}${shuffleQueue ? ' (shuffled)' : ''}`, ) - - // Set skipping to false after a short delay to prevent flickering - // IDK why this needs to be 1000ms, but there are a lot of events are emitted - // by RNTP at this time so we need to wait for it to settle - setTimeout(() => setSkipping(false), 1000) } /** @@ -547,6 +542,10 @@ const QueueContextInitailizer = () => { trigger('notificationSuccess') console.debug(`Loaded new queue`) + // Set skipping to false after a short delay to prevent flickering + // IDK why this needs to be 500ms, but there are a lot of events are emitted + // by RNTP at this time so we need to wait for it to settle + setTimeout(() => setSkipping(false), 500) if (startPlayback) await TrackPlayer.play() }, onError: async (error: Error) => { diff --git a/src/screens/Tabs/index.tsx b/src/screens/Tabs/index.tsx index dba2aac6..1205a281 100644 --- a/src/screens/Tabs/index.tsx +++ b/src/screens/Tabs/index.tsx @@ -27,6 +27,7 @@ export default function Tabs({ route, navigation }: TabProps): React.JSX.Element animation: 'shift', tabBarActiveTintColor: theme.primary.val, tabBarInactiveTintColor: theme.neutral.val, + lazy: true, }} tabBar={(props) => ( <> @@ -63,7 +64,6 @@ export default function Tabs({ route, navigation }: TabProps): React.JSX.Element ), tabBarButtonTestID: 'library-tab-button', - lazy: false, // Load on mount since we need to be able to navigate here from the player }} /> diff --git a/yarn.lock b/yarn.lock index f3de1968..2e1bdd24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8523,10 +8523,10 @@ react-native-safe-area-context@^5.6.0: resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-5.6.0.tgz#0ab284c291bb57d59330abf7dfe65156d6340e78" integrity sha512-tJas3YOdsuCg3kepCTGF3LWZp9onMbb9Agju2xfs2kRX8d/5TMUPmupBpjerk/B7Tv/zeJnk+qp5neA96Y0otQ== -react-native-screens@4.15.0: - version "4.15.0" - resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.15.0.tgz#8b34056b72a2c92da4de2f77419a116154c88e81" - integrity sha512-LPz+9qWDfwY3pPKojrWPcQnb2sAq9cy/qA8ZAS14ksSdzqFkTTUbs1as2WGBo7xBtdx5Ht78bF9nNh8libX1Xw== +react-native-screens@4.15.2: + version "4.15.2" + resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.15.2.tgz#93ba015f5167a2fb5e2e2b71807f083ec8ed1ef2" + integrity sha512-RA9fUT/5OTPJ2ML3BNUbe8UtfcU7iufE+r9sLr/IesFdBDj38bZiqmH88iorI5Vfgp7O3zf2aK390Tbkfp9Xfw== dependencies: react-freeze "^1.0.0" react-native-is-edge-to-edge "^1.2.1"