From 97418c9d08a953b5ba3ac03512ae03f750e2808d Mon Sep 17 00:00:00 2001 From: Dharit Tantiviramanond Date: Mon, 28 Oct 2024 16:35:09 -0400 Subject: [PATCH 01/14] [C-5317] ScreenReady and useIsScreenReady --- .../src/components/core/ScreenReady.tsx | 24 ++++++ packages/mobile/src/hooks/useIsScreenReady.ts | 34 ++++++++ .../src/screens/track-screen/TrackScreen.tsx | 33 ++++---- .../track-screen/TrackScreenDetailsTile.tsx | 81 ++++++++++--------- 4 files changed, 118 insertions(+), 54 deletions(-) create mode 100644 packages/mobile/src/components/core/ScreenReady.tsx create mode 100644 packages/mobile/src/hooks/useIsScreenReady.ts diff --git a/packages/mobile/src/components/core/ScreenReady.tsx b/packages/mobile/src/components/core/ScreenReady.tsx new file mode 100644 index 00000000000..36876903dc2 --- /dev/null +++ b/packages/mobile/src/components/core/ScreenReady.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react' + +import Animated, { FadeIn } from 'react-native-reanimated' +import { usePrevious } from 'react-use' + +import { useIsScreenReady } from 'app/hooks/useIsScreenReady' + +export const ScreenReady = ({ + loadingComponent = null, + children +}: { + loadingComponent?: ReactNode + children: ReactNode +}) => { + const isReady = useIsScreenReady() + const wasReady = usePrevious(isReady) + return isReady ? ( + + {children} + + ) : ( + <>{loadingComponent} + ) +} diff --git a/packages/mobile/src/hooks/useIsScreenReady.ts b/packages/mobile/src/hooks/useIsScreenReady.ts new file mode 100644 index 00000000000..92919a92e6b --- /dev/null +++ b/packages/mobile/src/hooks/useIsScreenReady.ts @@ -0,0 +1,34 @@ +import { useCallback, useState } from 'react' + +import { useFocusEffect } from '@react-navigation/native' +import { InteractionManager } from 'react-native' + +import { useNavigation } from './useNavigation' + +export const useIsScreenReady = () => { + const delay = 1 + const [isReady, setIsReady] = useState(false) + const navigation = useNavigation() + + useFocusEffect( + useCallback(() => { + let timer + const unsubscribe = navigation.addListener('transitionEnd', (e) => { + timer = setTimeout(() => { + InteractionManager.runAfterInteractions(() => { + setIsReady(true) + }) + }, delay) + }) + + // Reset the state when screen loses focus + return () => { + clearTimeout(timer) + unsubscribe() + setIsReady(false) + } + }, [navigation]) + ) + + return isReady +} diff --git a/packages/mobile/src/screens/track-screen/TrackScreen.tsx b/packages/mobile/src/screens/track-screen/TrackScreen.tsx index 43b68d7135f..fa1f9cb93db 100644 --- a/packages/mobile/src/screens/track-screen/TrackScreen.tsx +++ b/packages/mobile/src/screens/track-screen/TrackScreen.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useEffect } from 'react' import { useFeatureFlag, useProxySelector } from '@audius/common/hooks' import { trackPageMessages } from '@audius/common/messages' @@ -10,7 +10,6 @@ import { trackPageSelectors, reachabilitySelectors } from '@audius/common/store' -import { useFocusEffect } from '@react-navigation/native' import { useDispatch, useSelector } from 'react-redux' import { IconArrowRight, Button, Text, Flex } from '@audius/harmony-native' @@ -21,6 +20,7 @@ import { VirtualizedScrollView } from 'app/components/core' import { Lineup } from 'app/components/lineup' +import { useIsScreenReady } from 'app/hooks/useIsScreenReady' import { useNavigation } from 'app/hooks/useNavigation' import { useRoute } from 'app/hooks/useRoute' @@ -65,20 +65,21 @@ export const TrackScreen = () => { const { isEnabled: isCommentingEnabled } = useFeatureFlag( FeatureFlags.COMMENTS_ENABLED ) - - const handleFetchTrack = useCallback(() => { - dispatch(tracksActions.reset()) - dispatch( - fetchTrack( - id ?? null, - decodeURIComponent(slug ?? ''), - handle ?? user?.handle, - canBeUnlisted + const isReady = useIsScreenReady() + + useEffect(() => { + if (isReady) { + dispatch(tracksActions.reset()) + dispatch( + fetchTrack( + id ?? null, + decodeURIComponent(slug ?? ''), + handle ?? user?.handle, + canBeUnlisted + ) ) - ) - }, [dispatch, canBeUnlisted, id, slug, handle, user?.handle]) - - useFocusEffect(handleFetchTrack) + } + }, [dispatch, canBeUnlisted, id, slug, handle, user?.handle, isReady]) if (!track || !user) { return @@ -141,7 +142,7 @@ export const TrackScreen = () => { isLineupLoading={!lineup?.entries?.[0]} /> - {isReachable ? ( + {isReachable && isReady ? ( <> {/* Comments */} {isCommentingEnabled && !comments_disabled ? ( diff --git a/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx b/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx index d0eb310b81a..67c22dca4ca 100644 --- a/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx +++ b/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx @@ -70,6 +70,7 @@ import { import CoSign, { Size } from 'app/components/co-sign' import { useCommentDrawer } from 'app/components/comments/CommentDrawerContext' import { DogEar, Tag, UserGeneratedText } from 'app/components/core' +import { ScreenReady } from 'app/components/core/ScreenReady' import { DeletedTile } from 'app/components/details-tile/DeletedTile' import { DetailsProgressInfo } from 'app/components/details-tile/DetailsProgressInfo' import { DetailsTileActionButtons } from 'app/components/details-tile/DetailsTileActionButtons' @@ -80,7 +81,9 @@ import { DetailsTileStats } from 'app/components/details-tile/DetailsTileStats' import { TrackMetadataList } from 'app/components/details-tile/TrackMetadataList' import { TrackImage } from 'app/components/image/TrackImage' import { OfflineStatusRow } from 'app/components/offline-downloads' +import { Skeleton } from 'app/components/skeleton' import UserBadges from 'app/components/user-badges' +import { useIsScreenReady } from 'app/hooks/useIsScreenReady' import { useNavigation } from 'app/hooks/useNavigation' import { useFeatureFlag } from 'app/hooks/useRemoteConfig' import { make, track as trackEvent } from 'app/services/analytics' @@ -623,46 +626,48 @@ export const TrackScreenDetailsTile = ({ borderBottomLeftRadius='m' borderBottomRightRadius='m' > - {!hasStreamAccess && !isOwner && streamConditions && trackId ? ( - - ) : null} - {(hasStreamAccess || isOwner) && streamConditions ? ( - }> + {!hasStreamAccess && !isOwner && streamConditions && trackId ? ( + + ) : null} + {(hasStreamAccess || isOwner) && streamConditions ? ( + + ) : null} + - ) : null} - - {description ? ( - - - {description} - - - ) : null} - - {renderTags()} - + {description ? ( + + + {description} + + + ) : null} + + {renderTags()} + + - {renderBottomContent()} + {renderBottomContent()} ) } From 96c7b244fdebb3134bd853244bdd226ccf35c591 Mon Sep 17 00:00:00 2001 From: Dharit Tantiviramanond Date: Mon, 28 Oct 2024 16:53:44 -0400 Subject: [PATCH 02/14] lint --- .../mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx b/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx index 67c22dca4ca..66ef356beca 100644 --- a/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx +++ b/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx @@ -83,7 +83,6 @@ import { TrackImage } from 'app/components/image/TrackImage' import { OfflineStatusRow } from 'app/components/offline-downloads' import { Skeleton } from 'app/components/skeleton' import UserBadges from 'app/components/user-badges' -import { useIsScreenReady } from 'app/hooks/useIsScreenReady' import { useNavigation } from 'app/hooks/useNavigation' import { useFeatureFlag } from 'app/hooks/useRemoteConfig' import { make, track as trackEvent } from 'app/services/analytics' From 6309742bbad735f7adc1656a17c272e2967a4153 Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Mon, 28 Oct 2024 15:01:30 -0700 Subject: [PATCH 03/14] remove duplicate screen options --- .../src/screens/app-screen/AppTabScreen.tsx | 88 ++++--------------- 1 file changed, 15 insertions(+), 73 deletions(-) diff --git a/packages/mobile/src/screens/app-screen/AppTabScreen.tsx b/packages/mobile/src/screens/app-screen/AppTabScreen.tsx index 1ab469dcb24..61830e9b8d2 100644 --- a/packages/mobile/src/screens/app-screen/AppTabScreen.tsx +++ b/packages/mobile/src/screens/app-screen/AppTabScreen.tsx @@ -210,26 +210,10 @@ export const AppTabScreen = ({ baseScreen, Stack }: AppTabScreenProps) => { }} > {baseScreen(Stack)} - - - - + + + + {isSearchV2Enabled ? ( { cardStyleInterpolator: forFade })} /> - - + + )} - - - + + + - - + + - - - + + + @@ -349,7 +291,7 @@ export const AppTabScreen = ({ baseScreen, Stack }: AppTabScreenProps) => { From 37ce12f7908f6b631e9411f7e841976d1f1aa263 Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Mon, 28 Oct 2024 15:09:25 -0700 Subject: [PATCH 04/14] put back options for custom screen options --- packages/mobile/src/screens/app-screen/AppTabScreen.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mobile/src/screens/app-screen/AppTabScreen.tsx b/packages/mobile/src/screens/app-screen/AppTabScreen.tsx index 61830e9b8d2..ebaa5eda1dc 100644 --- a/packages/mobile/src/screens/app-screen/AppTabScreen.tsx +++ b/packages/mobile/src/screens/app-screen/AppTabScreen.tsx @@ -291,7 +291,7 @@ export const AppTabScreen = ({ baseScreen, Stack }: AppTabScreenProps) => { @@ -303,7 +303,7 @@ export const AppTabScreen = ({ baseScreen, Stack }: AppTabScreenProps) => { // @ts-ignore hard to correctly type navigation params (PAY-1141) params?.chatId } - options={{ fullScreenGestureEnabled: false }} + options={{ ...screenOptions, fullScreenGestureEnabled: false }} /> From cfbdc3b3b10ed722628fe1412b8a670df913077a Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Tue, 29 Oct 2024 13:09:33 -0700 Subject: [PATCH 05/14] Add ScreenContext and cascading content wrappers --- .../src/components/core/Screen/Screen.tsx | 3 ++- .../core/Screen/ScreenContextProvider.tsx | 27 +++++++++++++++++++ .../core/Screen/ScreenPrimaryContent.tsx | 13 +++++++++ .../core/Screen/ScreenSecondaryContent.tsx | 13 +++++++++ .../core/Screen}/hooks/useIsScreenReady.ts | 2 +- .../core/Screen/hooks/useScreenContext.ts | 7 +++++ .../src/components/core/Screen/types.ts | 5 ++++ .../src/components/core/ScreenReady.tsx | 8 +++--- .../src/screens/track-screen/TrackScreen.tsx | 11 ++++---- 9 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 packages/mobile/src/components/core/Screen/ScreenContextProvider.tsx create mode 100644 packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx create mode 100644 packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx rename packages/mobile/src/{ => components/core/Screen}/hooks/useIsScreenReady.ts (92%) create mode 100644 packages/mobile/src/components/core/Screen/hooks/useScreenContext.ts create mode 100644 packages/mobile/src/components/core/Screen/types.ts diff --git a/packages/mobile/src/components/core/Screen/Screen.tsx b/packages/mobile/src/components/core/Screen/Screen.tsx index accafdc689c..e73c4edc89c 100644 --- a/packages/mobile/src/components/core/Screen/Screen.tsx +++ b/packages/mobile/src/components/core/Screen/Screen.tsx @@ -13,6 +13,7 @@ import { makeStyles } from 'app/styles' import type { ThemeColors } from 'app/utils/theme' import { useThemePalette } from 'app/utils/theme' +import { ScreenContextProvider } from './ScreenContextProvider' import { SecondaryScreenTitle } from './SecondaryScreenTitle' type ScreenVariant = 'primary' | 'secondary' | 'secondaryAlt' | 'white' @@ -125,7 +126,7 @@ export const Screen = (props: ScreenProps) => { return ( - {children} + {children} ) } diff --git a/packages/mobile/src/components/core/Screen/ScreenContextProvider.tsx b/packages/mobile/src/components/core/Screen/ScreenContextProvider.tsx new file mode 100644 index 00000000000..6721b3f9322 --- /dev/null +++ b/packages/mobile/src/components/core/Screen/ScreenContextProvider.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from 'react' +import { createContext, useState } from 'react' + +import { useIsScreenReady } from './hooks/useIsScreenReady' +import type { ScreenContextType } from './types' + +export const ScreenContext = createContext({ + isScreenReady: false, + isPrimaryContentReady: false, + setIsPrimaryContentReady: () => {} +}) + +export const ScreenContextProvider = ({ + children +}: { + children: ReactNode +}) => { + const [isPrimaryContentReady, setIsPrimaryContentReady] = useState(false) + const isScreenReady = useIsScreenReady() + return ( + + {children} + + ) +} diff --git a/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx b/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx new file mode 100644 index 00000000000..9584e2b7fb6 --- /dev/null +++ b/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx @@ -0,0 +1,13 @@ +import { useEffectOnce } from 'react-use' + +import { useScreenContext } from './hooks/useScreenContext' + +export const ScreenPrimaryContent = ({ children }) => { + const { isPrimaryContentReady, setIsPrimaryContentReady } = useScreenContext() + useEffectOnce(() => { + requestAnimationFrame(() => { + setIsPrimaryContentReady(true) + }) + }) + return isPrimaryContentReady ? children : null +} diff --git a/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx b/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx new file mode 100644 index 00000000000..871870301fe --- /dev/null +++ b/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from 'react' + +import { useScreenContext } from './hooks/useScreenContext' + +export const ScreenSecondaryContent = ({ + children +}: { + children: ReactNode +}) => { + const { isPrimaryContentReady } = useScreenContext() + if (!isPrimaryContentReady) return null + return children +} diff --git a/packages/mobile/src/hooks/useIsScreenReady.ts b/packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts similarity index 92% rename from packages/mobile/src/hooks/useIsScreenReady.ts rename to packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts index 92919a92e6b..985754dbbf9 100644 --- a/packages/mobile/src/hooks/useIsScreenReady.ts +++ b/packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts @@ -3,7 +3,7 @@ import { useCallback, useState } from 'react' import { useFocusEffect } from '@react-navigation/native' import { InteractionManager } from 'react-native' -import { useNavigation } from './useNavigation' +import { useNavigation } from '../../../../hooks/useNavigation' export const useIsScreenReady = () => { const delay = 1 diff --git a/packages/mobile/src/components/core/Screen/hooks/useScreenContext.ts b/packages/mobile/src/components/core/Screen/hooks/useScreenContext.ts new file mode 100644 index 00000000000..4979e8d2a59 --- /dev/null +++ b/packages/mobile/src/components/core/Screen/hooks/useScreenContext.ts @@ -0,0 +1,7 @@ +import { useContext } from 'react' + +import { ScreenContext } from '../ScreenContextProvider' + +export const useScreenContext = () => { + return useContext(ScreenContext) +} diff --git a/packages/mobile/src/components/core/Screen/types.ts b/packages/mobile/src/components/core/Screen/types.ts new file mode 100644 index 00000000000..69d277cafa7 --- /dev/null +++ b/packages/mobile/src/components/core/Screen/types.ts @@ -0,0 +1,5 @@ +export type ScreenContextType = { + isScreenReady: boolean + isPrimaryContentReady: boolean + setIsPrimaryContentReady: (ready: boolean) => void +} diff --git a/packages/mobile/src/components/core/ScreenReady.tsx b/packages/mobile/src/components/core/ScreenReady.tsx index 36876903dc2..40b7da29c4c 100644 --- a/packages/mobile/src/components/core/ScreenReady.tsx +++ b/packages/mobile/src/components/core/ScreenReady.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from 'react' import Animated, { FadeIn } from 'react-native-reanimated' import { usePrevious } from 'react-use' -import { useIsScreenReady } from 'app/hooks/useIsScreenReady' +import { useScreenContext } from './Screen/hooks/useScreenContext' export const ScreenReady = ({ loadingComponent = null, @@ -12,9 +12,9 @@ export const ScreenReady = ({ loadingComponent?: ReactNode children: ReactNode }) => { - const isReady = useIsScreenReady() - const wasReady = usePrevious(isReady) - return isReady ? ( + const { isScreenReady } = useScreenContext() + const wasReady = usePrevious(isScreenReady) + return isScreenReady ? ( {children} diff --git a/packages/mobile/src/screens/track-screen/TrackScreen.tsx b/packages/mobile/src/screens/track-screen/TrackScreen.tsx index fa1f9cb93db..323667cb44b 100644 --- a/packages/mobile/src/screens/track-screen/TrackScreen.tsx +++ b/packages/mobile/src/screens/track-screen/TrackScreen.tsx @@ -19,8 +19,8 @@ import { ScreenContent, VirtualizedScrollView } from 'app/components/core' +import { useScreenContext } from 'app/components/core/Screen/hooks/useScreenContext' import { Lineup } from 'app/components/lineup' -import { useIsScreenReady } from 'app/hooks/useIsScreenReady' import { useNavigation } from 'app/hooks/useNavigation' import { useRoute } from 'app/hooks/useRoute' @@ -65,10 +65,10 @@ export const TrackScreen = () => { const { isEnabled: isCommentingEnabled } = useFeatureFlag( FeatureFlags.COMMENTS_ENABLED ) - const isReady = useIsScreenReady() + const { isScreenReady } = useScreenContext() useEffect(() => { - if (isReady) { + if (isScreenReady) { dispatch(tracksActions.reset()) dispatch( fetchTrack( @@ -79,7 +79,7 @@ export const TrackScreen = () => { ) ) } - }, [dispatch, canBeUnlisted, id, slug, handle, user?.handle, isReady]) + }, [dispatch, canBeUnlisted, id, slug, handle, user?.handle, isScreenReady]) if (!track || !user) { return @@ -132,6 +132,7 @@ export const TrackScreen = () => { return ( + {isScreenReady ? 'ready' : 'not ready'} {/* Track Details */} @@ -142,7 +143,7 @@ export const TrackScreen = () => { isLineupLoading={!lineup?.entries?.[0]} /> - {isReachable && isReady ? ( + {isReachable && isScreenReady ? ( <> {/* Comments */} {isCommentingEnabled && !comments_disabled ? ( From ae9d509bc64d45599b699239d04e9abb33befaa4 Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Tue, 29 Oct 2024 14:25:24 -0700 Subject: [PATCH 06/14] fast track screen --- .../core/Screen/ScreenContextProvider.tsx | 23 +- .../core/Screen/ScreenPrimaryContent.tsx | 23 +- .../core/Screen/ScreenSecondaryContent.tsx | 11 +- .../core/Screen/hooks/useIsScreenReady.ts | 24 +- .../core/Screen/hooks/useScreenContext.ts | 7 - .../src/components/core/ScreenReady.tsx | 3 +- .../src/screens/track-screen/TrackScreen.tsx | 12 +- .../track-screen/TrackScreenDetailsTile.tsx | 149 ++-- .../TrackScreenDetailsTileSkeleton.tsx | 672 ++++++++++++++++++ 9 files changed, 802 insertions(+), 122 deletions(-) delete mode 100644 packages/mobile/src/components/core/Screen/hooks/useScreenContext.ts create mode 100644 packages/mobile/src/screens/track-screen/TrackScreenDetailsTileSkeleton.tsx diff --git a/packages/mobile/src/components/core/Screen/ScreenContextProvider.tsx b/packages/mobile/src/components/core/Screen/ScreenContextProvider.tsx index 6721b3f9322..23f3950a63a 100644 --- a/packages/mobile/src/components/core/Screen/ScreenContextProvider.tsx +++ b/packages/mobile/src/components/core/Screen/ScreenContextProvider.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react' -import { createContext, useState } from 'react' +import { createContext, useContext, useMemo, useState } from 'react' import { useIsScreenReady } from './hooks/useIsScreenReady' import type { ScreenContextType } from './types' @@ -17,11 +17,22 @@ export const ScreenContextProvider = ({ }) => { const [isPrimaryContentReady, setIsPrimaryContentReady] = useState(false) const isScreenReady = useIsScreenReady() + + const context = useMemo(() => { + return { isScreenReady, isPrimaryContentReady, setIsPrimaryContentReady } + }, [isScreenReady, isPrimaryContentReady]) + return ( - - {children} - + {children} ) } + +export const useScreenContext = () => { + const context = useContext(ScreenContext) + if (!context) { + throw new Error( + 'useScreenContext must be used within a ScreenContextProvider' + ) + } + return context +} diff --git a/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx b/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx index 9584e2b7fb6..3a5df010bff 100644 --- a/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx +++ b/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx @@ -1,13 +1,20 @@ -import { useEffectOnce } from 'react-use' +import { useEffect, type ReactNode } from 'react' -import { useScreenContext } from './hooks/useScreenContext' - -export const ScreenPrimaryContent = ({ children }) => { - const { isPrimaryContentReady, setIsPrimaryContentReady } = useScreenContext() - useEffectOnce(() => { +import { useScreenContext } from './ScreenContextProvider' +export const ScreenPrimaryContent = ({ + children, + skeleton +}: { + children: ReactNode + skeleton?: ReactNode +}) => { + const { isScreenReady, setIsPrimaryContentReady } = useScreenContext() + useEffect(() => { + if (!isScreenReady) return requestAnimationFrame(() => { setIsPrimaryContentReady(true) }) - }) - return isPrimaryContentReady ? children : null + }, [isScreenReady, setIsPrimaryContentReady]) + + return isScreenReady ? <>{children} : <>{skeleton ?? null} } diff --git a/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx b/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx index 871870301fe..4abb0d055bb 100644 --- a/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx +++ b/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx @@ -1,13 +1,14 @@ import type { ReactNode } from 'react' -import { useScreenContext } from './hooks/useScreenContext' - +import { useScreenContext } from './ScreenContextProvider' export const ScreenSecondaryContent = ({ - children + children, + skeleton }: { children: ReactNode + skeleton?: ReactNode }) => { const { isPrimaryContentReady } = useScreenContext() - if (!isPrimaryContentReady) return null - return children + if (!isPrimaryContentReady) return <>{skeleton ?? null} + return <>{children} } diff --git a/packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts b/packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts index 985754dbbf9..eeaa7799e75 100644 --- a/packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts +++ b/packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts @@ -3,31 +3,29 @@ import { useCallback, useState } from 'react' import { useFocusEffect } from '@react-navigation/native' import { InteractionManager } from 'react-native' -import { useNavigation } from '../../../../hooks/useNavigation' - export const useIsScreenReady = () => { const delay = 1 const [isReady, setIsReady] = useState(false) - const navigation = useNavigation() useFocusEffect( useCallback(() => { - let timer - const unsubscribe = navigation.addListener('transitionEnd', (e) => { - timer = setTimeout(() => { - InteractionManager.runAfterInteractions(() => { - setIsReady(true) - }) - }, delay) - }) + const timer = setTimeout(() => { + InteractionManager.runAfterInteractions(() => { + setIsReady(true) + }) + }, delay) + + setTimeout(() => { + clearTimeout(timer) + setIsReady(true) + }, 200) // Reset the state when screen loses focus return () => { clearTimeout(timer) - unsubscribe() setIsReady(false) } - }, [navigation]) + }, []) ) return isReady diff --git a/packages/mobile/src/components/core/Screen/hooks/useScreenContext.ts b/packages/mobile/src/components/core/Screen/hooks/useScreenContext.ts deleted file mode 100644 index 4979e8d2a59..00000000000 --- a/packages/mobile/src/components/core/Screen/hooks/useScreenContext.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useContext } from 'react' - -import { ScreenContext } from '../ScreenContextProvider' - -export const useScreenContext = () => { - return useContext(ScreenContext) -} diff --git a/packages/mobile/src/components/core/ScreenReady.tsx b/packages/mobile/src/components/core/ScreenReady.tsx index 40b7da29c4c..1beeff0d062 100644 --- a/packages/mobile/src/components/core/ScreenReady.tsx +++ b/packages/mobile/src/components/core/ScreenReady.tsx @@ -3,8 +3,7 @@ import type { ReactNode } from 'react' import Animated, { FadeIn } from 'react-native-reanimated' import { usePrevious } from 'react-use' -import { useScreenContext } from './Screen/hooks/useScreenContext' - +import { useScreenContext } from './Screen/ScreenContextProvider' export const ScreenReady = ({ loadingComponent = null, children diff --git a/packages/mobile/src/screens/track-screen/TrackScreen.tsx b/packages/mobile/src/screens/track-screen/TrackScreen.tsx index 323667cb44b..6bded5d9393 100644 --- a/packages/mobile/src/screens/track-screen/TrackScreen.tsx +++ b/packages/mobile/src/screens/track-screen/TrackScreen.tsx @@ -19,7 +19,8 @@ import { ScreenContent, VirtualizedScrollView } from 'app/components/core' -import { useScreenContext } from 'app/components/core/Screen/hooks/useScreenContext' +import { ScreenSecondaryContent } from 'app/components/core/Screen/ScreenSecondaryContent' +import { useIsScreenReady } from 'app/components/core/Screen/hooks/useIsScreenReady' import { Lineup } from 'app/components/lineup' import { useNavigation } from 'app/hooks/useNavigation' import { useRoute } from 'app/hooks/useRoute' @@ -65,8 +66,8 @@ export const TrackScreen = () => { const { isEnabled: isCommentingEnabled } = useFeatureFlag( FeatureFlags.COMMENTS_ENABLED ) - const { isScreenReady } = useScreenContext() + const isScreenReady = useIsScreenReady() useEffect(() => { if (isScreenReady) { dispatch(tracksActions.reset()) @@ -132,7 +133,6 @@ export const TrackScreen = () => { return ( - {isScreenReady ? 'ready' : 'not ready'} {/* Track Details */} @@ -143,8 +143,8 @@ export const TrackScreen = () => { isLineupLoading={!lineup?.entries?.[0]} /> - {isReachable && isScreenReady ? ( - <> + {isReachable ? ( + {/* Comments */} {isCommentingEnabled && !comments_disabled ? ( @@ -188,7 +188,7 @@ export const TrackScreen = () => { } /> - + ) : null} diff --git a/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx b/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx index 66ef356beca..ea3ab11f900 100644 --- a/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx +++ b/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx @@ -70,7 +70,7 @@ import { import CoSign, { Size } from 'app/components/co-sign' import { useCommentDrawer } from 'app/components/comments/CommentDrawerContext' import { DogEar, Tag, UserGeneratedText } from 'app/components/core' -import { ScreenReady } from 'app/components/core/ScreenReady' +import { ScreenPrimaryContent } from 'app/components/core/Screen/ScreenPrimaryContent' import { DeletedTile } from 'app/components/details-tile/DeletedTile' import { DetailsProgressInfo } from 'app/components/details-tile/DetailsProgressInfo' import { DetailsTileActionButtons } from 'app/components/details-tile/DetailsTileActionButtons' @@ -81,7 +81,6 @@ import { DetailsTileStats } from 'app/components/details-tile/DetailsTileStats' import { TrackMetadataList } from 'app/components/details-tile/TrackMetadataList' import { TrackImage } from 'app/components/image/TrackImage' import { OfflineStatusRow } from 'app/components/offline-downloads' -import { Skeleton } from 'app/components/skeleton' import UserBadges from 'app/components/user-badges' import { useNavigation } from 'app/hooks/useNavigation' import { useFeatureFlag } from 'app/hooks/useRemoteConfig' @@ -552,80 +551,80 @@ export const TrackScreenDetailsTile = ({ return ( - {renderDogEar()} - - - {headerText} - + + {renderDogEar()} + + + {headerText} + - {badges.length > 0 ? ( - - {badges.map((badge) => badge)} + {badges.length > 0 ? ( + + {badges.map((badge) => badge)} + + ) : null} + {imageElement} + + + {title} + + {user ? ( + + + + {user.name} + + + + + ) : null} - ) : null} - {imageElement} - - - {title} - - {user ? ( - - - - {user.name} - - - - + {isLongFormContent && track ? ( + + ) : null} + {hasStreamAccess ? ( + ) : null} + {shouldShowPreview ? : null} + - {isLongFormContent && track ? ( - - ) : null} - {hasStreamAccess ? ( - - ) : null} - {shouldShowPreview ? : null} - - - - }> + {!hasStreamAccess && !isOwner && streamConditions && trackId ? ( {renderTags()} - - - {renderBottomContent()} + + {renderBottomContent()} + ) } diff --git a/packages/mobile/src/screens/track-screen/TrackScreenDetailsTileSkeleton.tsx b/packages/mobile/src/screens/track-screen/TrackScreenDetailsTileSkeleton.tsx new file mode 100644 index 00000000000..06c202155ad --- /dev/null +++ b/packages/mobile/src/screens/track-screen/TrackScreenDetailsTileSkeleton.tsx @@ -0,0 +1,672 @@ +import React, { useCallback } from 'react' + +import { useGatedContentAccess } from '@audius/common/hooks' +import { + Name, + ShareSource, + RepostSource, + FavoriteSource, + PlaybackSource, + FavoriteType, + SquareSizes, + isContentUSDCPurchaseGated, + isContentCollectibleGated +} from '@audius/common/models' +import type { + UID, + SearchUser, + SearchTrack, + Track, + User +} from '@audius/common/models' +import { FeatureFlags, trpc } from '@audius/common/services' +import type { CommonState } from '@audius/common/store' +import { + accountSelectors, + trackPageLineupActions, + queueSelectors, + reachabilitySelectors, + tracksSocialActions, + mobileOverflowMenuUIActions, + shareModalUIActions, + OverflowAction, + OverflowSource, + repostsUserListActions, + favoritesUserListActions, + trackPageActions, + RepostType, + playerSelectors, + playbackPositionSelectors, + PurchaseableContentType, + usePublishConfirmationModal, + useEarlyReleaseConfirmationModal +} from '@audius/common/store' +import { + formatReleaseDate, + Genre, + getDogEarType, + removeNullable +} from '@audius/common/utils' +import dayjs from 'dayjs' +import { TouchableOpacity } from 'react-native' +import { useDispatch, useSelector } from 'react-redux' + +import { + Box, + Button, + Divider, + Flex, + IconCalendarMonth, + IconPause, + IconPlay, + IconRepeatOff, + IconVisibilityHidden, + MusicBadge, + Paper, + Text, + spacing, + type ImageProps +} from '@audius/harmony-native' +import CoSign, { Size } from 'app/components/co-sign' +import { useCommentDrawer } from 'app/components/comments/CommentDrawerContext' +import { DogEar, Tag, UserGeneratedText } from 'app/components/core' +import { ScreenPrimaryContent } from 'app/components/core/Screen/ScreenPrimaryContent' +import { DeletedTile } from 'app/components/details-tile/DeletedTile' +import { DetailsProgressInfo } from 'app/components/details-tile/DetailsProgressInfo' +import { DetailsTileActionButtons } from 'app/components/details-tile/DetailsTileActionButtons' +import { DetailsTileAiAttribution } from 'app/components/details-tile/DetailsTileAiAttribution' +import { DetailsTileHasAccess } from 'app/components/details-tile/DetailsTileHasAccess' +import { DetailsTileNoAccess } from 'app/components/details-tile/DetailsTileNoAccess' +import { DetailsTileStats } from 'app/components/details-tile/DetailsTileStats' +import { TrackMetadataList } from 'app/components/details-tile/TrackMetadataList' +import { TrackImage } from 'app/components/image/TrackImage' +import { OfflineStatusRow } from 'app/components/offline-downloads' +import Skeleton from 'app/components/skeleton' +import UserBadges from 'app/components/user-badges' +import { useNavigation } from 'app/hooks/useNavigation' +import { useFeatureFlag } from 'app/hooks/useRemoteConfig' +import { make, track as trackEvent } from 'app/services/analytics' +import { makeStyles } from 'app/styles' + +import { DownloadSection } from './DownloadSection' +const { getPlaying, getTrackId, getPreviewing } = playerSelectors +const { setFavorite } = favoritesUserListActions +const { setRepost } = repostsUserListActions +const { requestOpen: requestOpenShareModal } = shareModalUIActions +const { open: openOverflowMenu } = mobileOverflowMenuUIActions +const { repostTrack, saveTrack, undoRepostTrack, unsaveTrack } = + tracksSocialActions +const { tracksActions } = trackPageLineupActions +const { getUserId } = accountSelectors +const { getIsReachable } = reachabilitySelectors +const { getTrackPosition } = playbackPositionSelectors +const { makeGetCurrent } = queueSelectors +const getCurrentQueueItem = makeGetCurrent() + +const messages = { + track: 'track', + podcast: 'podcast', + remix: 'remix', + collectibleGated: 'collectible gated', + specialAccess: 'special access', + premiumTrack: 'premium track', + generatedWithAi: 'generated with ai', + trackDeleted: 'track [deleted by artist]', + play: 'Play', + pause: 'Pause', + resume: 'Resume', + replay: 'Replay', + preview: 'Preview', + hidden: 'Hidden', + releases: (releaseDate: string) => + `Releases ${formatReleaseDate({ date: releaseDate, withHour: true })}` +} + +const useStyles = makeStyles(({ palette, spacing }) => ({ + coverArt: { + borderWidth: 1, + borderColor: palette.neutralLight8, + borderRadius: spacing(2), + height: 224, + width: 224, + alignSelf: 'center' + } +})) + +type TrackScreenDetailsTileProps = { + track: Track | SearchTrack + user: User | SearchUser + uid: UID + isLineupLoading: boolean +} + +const recordPlay = (id, play = true, isPreview = false) => { + trackEvent( + make({ + eventName: play ? Name.PLAYBACK_PLAY : Name.PLAYBACK_PAUSE, + id: String(id), + source: PlaybackSource.TRACK_PAGE, + isPreview + }) + ) +} + +export const TrackScreenDetailsTile = ({ + track, + user, + uid, + isLineupLoading +}: TrackScreenDetailsTileProps) => { + const styles = useStyles() + const { hasStreamAccess } = useGatedContentAccess(track as Track) // track is of type Track | SearchTrack but we only care about some of their common fields, maybe worth refactoring later + const navigation = useNavigation() + + const isReachable = useSelector(getIsReachable) + const currentUserId = useSelector(getUserId) + const dispatch = useDispatch() + const playingId = useSelector(getTrackId) + const isPlaybackActive = useSelector(getPlaying) + const isPreviewing = useSelector(getPreviewing) + const isPlayingId = playingId === track.track_id + const isPlaying = isPlaybackActive && isPlayingId + const playbackPositionInfo = useSelector((state) => + getTrackPosition(state, { trackId, userId: currentUserId }) + ) + const isCurrentTrack = useSelector((state: CommonState) => { + return track && track.track_id === getTrackId(state) + }) + const { onOpen: openPublishConfirmation } = usePublishConfirmationModal() + const { onOpen: openEarlyReleaseConfirmation } = + useEarlyReleaseConfirmationModal() + const { isEnabled: isSearchV2Enabled } = useFeatureFlag( + FeatureFlags.SEARCH_V2 + ) + + const { + _co_sign: coSign, + description, + genre, + has_current_user_reposted: hasReposted, + has_current_user_saved: hasSaved, + is_unlisted: isUnlisted, + is_stream_gated: isStreamGated, + owner_id: ownerId, + play_count: playCount, + remix_of: remixOf, + repost_count: repostCount, + save_count: saveCount, + comment_count: commentCount, + comments_disabled: commentsDisabled, + title, + track_id: trackId, + stream_conditions: streamConditions, + ddex_app: ddexApp, + is_delete: isDeleted, + release_date: releaseDate, + is_scheduled_release: isScheduledRelease, + _is_publishing, + preview_cid + } = track as Track + + const isOwner = ownerId === currentUserId + const hideFavorite = isUnlisted || !hasStreamAccess + const hideRepost = isUnlisted || !isReachable || !hasStreamAccess + const hideOverflow = !isReachable || (isUnlisted && !isOwner) + const hideShare = isUnlisted && !isOwner + + const remixParentTrackId = remixOf?.tracks?.[0]?.parent_track_id + const isRemix = !!remixParentTrackId + const hasDownloadableAssets = + (track as Track)?.is_downloadable || + ((track as Track)?._stems?.length ?? 0) > 0 + + const { data: albumInfo } = trpc.tracks.getAlbumBacklink.useQuery( + { trackId }, + { enabled: !!trackId } + ) + const { open: openCommentDrawer } = useCommentDrawer() + + const isLongFormContent = + track?.genre === Genre.PODCASTS || track?.genre === Genre.AUDIOBOOKS + const aiAttributionUserId = track?.ai_attribution_user_id + const isUSDCPurchaseGated = isContentUSDCPurchaseGated(streamConditions) + const { isEnabled: isCommentsEnabled } = useFeatureFlag( + FeatureFlags.COMMENTS_ENABLED + ) + + const isPlayingPreview = isPreviewing && isPlaying + const isPlayingFullAccess = isPlaying && !isPreviewing + const shouldShowScheduledRelease = + isScheduledRelease && + isUnlisted && + releaseDate && + dayjs(releaseDate).isAfter(dayjs()) + const shouldShowPreview = + isUSDCPurchaseGated && (isOwner || !hasStreamAccess) && preview_cid + const shouldHideFavoriteCount = + isUnlisted || (!isOwner && (saveCount ?? 0) <= 0) + const shouldHideRepostCount = + isUnlisted || (!isOwner && (repostCount ?? 0) <= 0) + const shouldHideCommentCount = + !isCommentsEnabled || + isUnlisted || + commentsDisabled || + (!isOwner && (commentCount ?? 0) <= 0) + const shouldHidePlayCount = + (!isOwner && isUnlisted) || + isStreamGated || + (!isOwner && (playCount ?? 0) <= 0) + + let headerText + if (isRemix) { + headerText = messages.remix + } else if (isStreamGated) { + if (isContentCollectibleGated(streamConditions)) { + headerText = messages.collectibleGated + } else if (isContentUSDCPurchaseGated(streamConditions)) { + headerText = messages.premiumTrack + } else { + headerText = messages.specialAccess + } + } else { + headerText = messages.track + } + + const PlayIcon = + playbackPositionInfo?.status === 'COMPLETED' && !isCurrentTrack + ? IconRepeatOff + : IconPlay + + const badges = [ + aiAttributionUserId ? ( + + ) : null, + shouldShowScheduledRelease ? ( + + {messages.releases(releaseDate)} + + ) : isUnlisted ? ( + {messages.hidden} + ) : null + ].filter((badge) => badge !== null) + + const playText = playbackPositionInfo?.status + ? playbackPositionInfo?.status === 'IN_PROGRESS' || isCurrentTrack + ? messages.resume + : messages.replay + : messages.play + + const renderImage = useCallback( + (props: ImageProps) => ( + + ), + [track] + ) + const innerImageElement = renderImage({ + style: styles.coverArt + }) + + const imageElement = coSign ? ( + {innerImageElement} + ) : ( + innerImageElement + ) + + const currentQueueItem = useSelector(getCurrentQueueItem) + const play = useCallback( + ({ isPreview = false } = {}) => { + if (isLineupLoading) return + + if (isPlaying && isPlayingId && isPreviewing === isPreview) { + dispatch(tracksActions.pause()) + recordPlay(trackId, false, true) + } else if ( + currentQueueItem.uid !== uid && + currentQueueItem.track && + currentQueueItem.track.track_id === trackId + ) { + dispatch(tracksActions.play()) + recordPlay(trackId) + } else { + dispatch(tracksActions.play(uid, { isPreview })) + recordPlay(trackId, true, true) + } + }, + [ + trackId, + currentQueueItem, + uid, + dispatch, + isPlaying, + isPlayingId, + isPreviewing, + isLineupLoading + ] + ) + + const handlePressPlay = useCallback(() => play(), [play]) + const handlePressPreview = useCallback( + () => play({ isPreview: true }), + [play] + ) + + const handlePressFavorites = useCallback(() => { + dispatch(setFavorite(trackId, FavoriteType.TRACK)) + navigation.push('Favorited', { + id: trackId, + favoriteType: FavoriteType.TRACK + }) + }, [dispatch, trackId, navigation]) + + const handlePressReposts = useCallback(() => { + dispatch(setRepost(trackId, RepostType.TRACK)) + navigation.push('Reposts', { id: trackId, repostType: RepostType.TRACK }) + }, [dispatch, trackId, navigation]) + + const handlePressComments = useCallback(() => { + openCommentDrawer({ entityId: trackId, navigation }) + trackEvent( + make({ + eventName: Name.COMMENTS_CLICK_COMMENT_STAT, + trackId, + source: 'track_page' + }) + ) + }, [openCommentDrawer, trackId, navigation]) + + const handlePressSave = () => { + if (!isOwner) { + if (hasSaved) { + dispatch(unsaveTrack(trackId, FavoriteSource.TRACK_PAGE)) + } else { + dispatch(saveTrack(trackId, FavoriteSource.TRACK_PAGE)) + } + } + } + + const handlePressRepost = () => { + if (!isOwner) { + if (hasReposted) { + dispatch(undoRepostTrack(trackId, RepostSource.TRACK_PAGE)) + } else { + dispatch(repostTrack(trackId, RepostSource.TRACK_PAGE)) + } + } + } + + const handlePressShare = () => { + dispatch( + requestOpenShareModal({ + type: 'track', + trackId, + source: ShareSource.PAGE + }) + ) + } + + const handlePressArtistName = useCallback(() => { + if (!user) { + return + } + navigation.push('Profile', { handle: user.handle }) + }, [navigation, user]) + + const handlePressTag = useCallback( + (tag: string) => { + if (isSearchV2Enabled) { + navigation.push('Search', { query: `#${tag}` }) + } else { + navigation.push('TagSearch', { query: tag }) + } + }, + [isSearchV2Enabled, navigation] + ) + + const handlePressEdit = useCallback(() => { + navigation?.push('EditTrack', { id: trackId }) + }, [navigation, trackId]) + + const handlePressOverflow = () => { + const isLongFormContent = + genre === Genre.PODCASTS || genre === Genre.AUDIOBOOKS + const addToAlbumAction = + isOwner && !ddexApp ? OverflowAction.ADD_TO_ALBUM : null + const overflowActions = [ + addToAlbumAction, + !isUnlisted || isOwner ? OverflowAction.ADD_TO_PLAYLIST : null, + isOwner + ? null + : user.does_current_user_follow + ? OverflowAction.UNFOLLOW_ARTIST + : OverflowAction.FOLLOW_ARTIST, + isLongFormContent + ? playbackPositionInfo?.status === 'COMPLETED' + ? OverflowAction.MARK_AS_UNPLAYED + : OverflowAction.MARK_AS_PLAYED + : null, + albumInfo ? OverflowAction.VIEW_ALBUM_PAGE : null, + OverflowAction.VIEW_ARTIST_PAGE, + isOwner && !ddexApp ? OverflowAction.EDIT_TRACK : null, + isOwner && isScheduledRelease && isUnlisted + ? OverflowAction.RELEASE_NOW + : null, + isOwner && !ddexApp ? OverflowAction.DELETE_TRACK : null + ].filter(removeNullable) + + dispatch( + openOverflowMenu({ + source: OverflowSource.TRACKS, + id: trackId, + overflowActions + }) + ) + } + + const publish = useCallback(() => { + dispatch(trackPageActions.makeTrackPublic(trackId)) + }, [dispatch, trackId]) + + const handlePressPublish = useCallback(() => { + if (isScheduledRelease) { + openEarlyReleaseConfirmation({ + confirmCallback: publish, + contentType: 'track' + }) + } else { + openPublishConfirmation({ + confirmCallback: publish, + contentType: 'track' + }) + } + }, [ + openPublishConfirmation, + openEarlyReleaseConfirmation, + isScheduledRelease, + publish + ]) + + const renderBottomContent = () => { + return hasDownloadableAssets ? ( + <> + + + + ) : null + } + + const renderDogEar = () => { + const dogEarType = getDogEarType({ + isOwner, + streamConditions + }) + return dogEarType ? : null + } + + const PreviewButton = () => ( + + ) + + const renderTags = () => { + if (!track || (isUnlisted && !track.field_visibility?.tags)) { + return null + } + + const filteredTags = (track.tags || '').split(',').filter(Boolean) + return filteredTags.length > 0 ? ( + + {filteredTags.map((tag) => ( + handlePressTag(tag)}> + {tag} + + ))} + + ) : null + } + + if (!trackId) return null + + if (isDeleted) { + return ( + + ) + } + + return ( + + }> + {renderDogEar()} + + + {headerText} + + + {badges.length > 0 ? ( + + {badges.map((badge) => badge)} + + ) : null} + {imageElement} + + + {title} + + {user ? ( + + + + {user.name} + + + + + ) : null} + + {isLongFormContent && track ? ( + + ) : null} + {hasStreamAccess ? ( + + ) : null} + {shouldShowPreview ? : null} + + + + {!hasStreamAccess && !isOwner && streamConditions && trackId ? ( + + ) : null} + {(hasStreamAccess || isOwner) && streamConditions ? ( + + ) : null} + + {description ? ( + + + {description} + + + ) : null} + + {renderTags()} + + + {renderBottomContent()} + + + ) +} From acd01ae22fed69b30d7d8ef4128a6261e8ab790b Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Tue, 29 Oct 2024 14:39:26 -0700 Subject: [PATCH 07/14] remove extra skeleton file --- .../TrackScreenDetailsTileSkeleton.tsx | 672 ------------------ 1 file changed, 672 deletions(-) delete mode 100644 packages/mobile/src/screens/track-screen/TrackScreenDetailsTileSkeleton.tsx diff --git a/packages/mobile/src/screens/track-screen/TrackScreenDetailsTileSkeleton.tsx b/packages/mobile/src/screens/track-screen/TrackScreenDetailsTileSkeleton.tsx deleted file mode 100644 index 06c202155ad..00000000000 --- a/packages/mobile/src/screens/track-screen/TrackScreenDetailsTileSkeleton.tsx +++ /dev/null @@ -1,672 +0,0 @@ -import React, { useCallback } from 'react' - -import { useGatedContentAccess } from '@audius/common/hooks' -import { - Name, - ShareSource, - RepostSource, - FavoriteSource, - PlaybackSource, - FavoriteType, - SquareSizes, - isContentUSDCPurchaseGated, - isContentCollectibleGated -} from '@audius/common/models' -import type { - UID, - SearchUser, - SearchTrack, - Track, - User -} from '@audius/common/models' -import { FeatureFlags, trpc } from '@audius/common/services' -import type { CommonState } from '@audius/common/store' -import { - accountSelectors, - trackPageLineupActions, - queueSelectors, - reachabilitySelectors, - tracksSocialActions, - mobileOverflowMenuUIActions, - shareModalUIActions, - OverflowAction, - OverflowSource, - repostsUserListActions, - favoritesUserListActions, - trackPageActions, - RepostType, - playerSelectors, - playbackPositionSelectors, - PurchaseableContentType, - usePublishConfirmationModal, - useEarlyReleaseConfirmationModal -} from '@audius/common/store' -import { - formatReleaseDate, - Genre, - getDogEarType, - removeNullable -} from '@audius/common/utils' -import dayjs from 'dayjs' -import { TouchableOpacity } from 'react-native' -import { useDispatch, useSelector } from 'react-redux' - -import { - Box, - Button, - Divider, - Flex, - IconCalendarMonth, - IconPause, - IconPlay, - IconRepeatOff, - IconVisibilityHidden, - MusicBadge, - Paper, - Text, - spacing, - type ImageProps -} from '@audius/harmony-native' -import CoSign, { Size } from 'app/components/co-sign' -import { useCommentDrawer } from 'app/components/comments/CommentDrawerContext' -import { DogEar, Tag, UserGeneratedText } from 'app/components/core' -import { ScreenPrimaryContent } from 'app/components/core/Screen/ScreenPrimaryContent' -import { DeletedTile } from 'app/components/details-tile/DeletedTile' -import { DetailsProgressInfo } from 'app/components/details-tile/DetailsProgressInfo' -import { DetailsTileActionButtons } from 'app/components/details-tile/DetailsTileActionButtons' -import { DetailsTileAiAttribution } from 'app/components/details-tile/DetailsTileAiAttribution' -import { DetailsTileHasAccess } from 'app/components/details-tile/DetailsTileHasAccess' -import { DetailsTileNoAccess } from 'app/components/details-tile/DetailsTileNoAccess' -import { DetailsTileStats } from 'app/components/details-tile/DetailsTileStats' -import { TrackMetadataList } from 'app/components/details-tile/TrackMetadataList' -import { TrackImage } from 'app/components/image/TrackImage' -import { OfflineStatusRow } from 'app/components/offline-downloads' -import Skeleton from 'app/components/skeleton' -import UserBadges from 'app/components/user-badges' -import { useNavigation } from 'app/hooks/useNavigation' -import { useFeatureFlag } from 'app/hooks/useRemoteConfig' -import { make, track as trackEvent } from 'app/services/analytics' -import { makeStyles } from 'app/styles' - -import { DownloadSection } from './DownloadSection' -const { getPlaying, getTrackId, getPreviewing } = playerSelectors -const { setFavorite } = favoritesUserListActions -const { setRepost } = repostsUserListActions -const { requestOpen: requestOpenShareModal } = shareModalUIActions -const { open: openOverflowMenu } = mobileOverflowMenuUIActions -const { repostTrack, saveTrack, undoRepostTrack, unsaveTrack } = - tracksSocialActions -const { tracksActions } = trackPageLineupActions -const { getUserId } = accountSelectors -const { getIsReachable } = reachabilitySelectors -const { getTrackPosition } = playbackPositionSelectors -const { makeGetCurrent } = queueSelectors -const getCurrentQueueItem = makeGetCurrent() - -const messages = { - track: 'track', - podcast: 'podcast', - remix: 'remix', - collectibleGated: 'collectible gated', - specialAccess: 'special access', - premiumTrack: 'premium track', - generatedWithAi: 'generated with ai', - trackDeleted: 'track [deleted by artist]', - play: 'Play', - pause: 'Pause', - resume: 'Resume', - replay: 'Replay', - preview: 'Preview', - hidden: 'Hidden', - releases: (releaseDate: string) => - `Releases ${formatReleaseDate({ date: releaseDate, withHour: true })}` -} - -const useStyles = makeStyles(({ palette, spacing }) => ({ - coverArt: { - borderWidth: 1, - borderColor: palette.neutralLight8, - borderRadius: spacing(2), - height: 224, - width: 224, - alignSelf: 'center' - } -})) - -type TrackScreenDetailsTileProps = { - track: Track | SearchTrack - user: User | SearchUser - uid: UID - isLineupLoading: boolean -} - -const recordPlay = (id, play = true, isPreview = false) => { - trackEvent( - make({ - eventName: play ? Name.PLAYBACK_PLAY : Name.PLAYBACK_PAUSE, - id: String(id), - source: PlaybackSource.TRACK_PAGE, - isPreview - }) - ) -} - -export const TrackScreenDetailsTile = ({ - track, - user, - uid, - isLineupLoading -}: TrackScreenDetailsTileProps) => { - const styles = useStyles() - const { hasStreamAccess } = useGatedContentAccess(track as Track) // track is of type Track | SearchTrack but we only care about some of their common fields, maybe worth refactoring later - const navigation = useNavigation() - - const isReachable = useSelector(getIsReachable) - const currentUserId = useSelector(getUserId) - const dispatch = useDispatch() - const playingId = useSelector(getTrackId) - const isPlaybackActive = useSelector(getPlaying) - const isPreviewing = useSelector(getPreviewing) - const isPlayingId = playingId === track.track_id - const isPlaying = isPlaybackActive && isPlayingId - const playbackPositionInfo = useSelector((state) => - getTrackPosition(state, { trackId, userId: currentUserId }) - ) - const isCurrentTrack = useSelector((state: CommonState) => { - return track && track.track_id === getTrackId(state) - }) - const { onOpen: openPublishConfirmation } = usePublishConfirmationModal() - const { onOpen: openEarlyReleaseConfirmation } = - useEarlyReleaseConfirmationModal() - const { isEnabled: isSearchV2Enabled } = useFeatureFlag( - FeatureFlags.SEARCH_V2 - ) - - const { - _co_sign: coSign, - description, - genre, - has_current_user_reposted: hasReposted, - has_current_user_saved: hasSaved, - is_unlisted: isUnlisted, - is_stream_gated: isStreamGated, - owner_id: ownerId, - play_count: playCount, - remix_of: remixOf, - repost_count: repostCount, - save_count: saveCount, - comment_count: commentCount, - comments_disabled: commentsDisabled, - title, - track_id: trackId, - stream_conditions: streamConditions, - ddex_app: ddexApp, - is_delete: isDeleted, - release_date: releaseDate, - is_scheduled_release: isScheduledRelease, - _is_publishing, - preview_cid - } = track as Track - - const isOwner = ownerId === currentUserId - const hideFavorite = isUnlisted || !hasStreamAccess - const hideRepost = isUnlisted || !isReachable || !hasStreamAccess - const hideOverflow = !isReachable || (isUnlisted && !isOwner) - const hideShare = isUnlisted && !isOwner - - const remixParentTrackId = remixOf?.tracks?.[0]?.parent_track_id - const isRemix = !!remixParentTrackId - const hasDownloadableAssets = - (track as Track)?.is_downloadable || - ((track as Track)?._stems?.length ?? 0) > 0 - - const { data: albumInfo } = trpc.tracks.getAlbumBacklink.useQuery( - { trackId }, - { enabled: !!trackId } - ) - const { open: openCommentDrawer } = useCommentDrawer() - - const isLongFormContent = - track?.genre === Genre.PODCASTS || track?.genre === Genre.AUDIOBOOKS - const aiAttributionUserId = track?.ai_attribution_user_id - const isUSDCPurchaseGated = isContentUSDCPurchaseGated(streamConditions) - const { isEnabled: isCommentsEnabled } = useFeatureFlag( - FeatureFlags.COMMENTS_ENABLED - ) - - const isPlayingPreview = isPreviewing && isPlaying - const isPlayingFullAccess = isPlaying && !isPreviewing - const shouldShowScheduledRelease = - isScheduledRelease && - isUnlisted && - releaseDate && - dayjs(releaseDate).isAfter(dayjs()) - const shouldShowPreview = - isUSDCPurchaseGated && (isOwner || !hasStreamAccess) && preview_cid - const shouldHideFavoriteCount = - isUnlisted || (!isOwner && (saveCount ?? 0) <= 0) - const shouldHideRepostCount = - isUnlisted || (!isOwner && (repostCount ?? 0) <= 0) - const shouldHideCommentCount = - !isCommentsEnabled || - isUnlisted || - commentsDisabled || - (!isOwner && (commentCount ?? 0) <= 0) - const shouldHidePlayCount = - (!isOwner && isUnlisted) || - isStreamGated || - (!isOwner && (playCount ?? 0) <= 0) - - let headerText - if (isRemix) { - headerText = messages.remix - } else if (isStreamGated) { - if (isContentCollectibleGated(streamConditions)) { - headerText = messages.collectibleGated - } else if (isContentUSDCPurchaseGated(streamConditions)) { - headerText = messages.premiumTrack - } else { - headerText = messages.specialAccess - } - } else { - headerText = messages.track - } - - const PlayIcon = - playbackPositionInfo?.status === 'COMPLETED' && !isCurrentTrack - ? IconRepeatOff - : IconPlay - - const badges = [ - aiAttributionUserId ? ( - - ) : null, - shouldShowScheduledRelease ? ( - - {messages.releases(releaseDate)} - - ) : isUnlisted ? ( - {messages.hidden} - ) : null - ].filter((badge) => badge !== null) - - const playText = playbackPositionInfo?.status - ? playbackPositionInfo?.status === 'IN_PROGRESS' || isCurrentTrack - ? messages.resume - : messages.replay - : messages.play - - const renderImage = useCallback( - (props: ImageProps) => ( - - ), - [track] - ) - const innerImageElement = renderImage({ - style: styles.coverArt - }) - - const imageElement = coSign ? ( - {innerImageElement} - ) : ( - innerImageElement - ) - - const currentQueueItem = useSelector(getCurrentQueueItem) - const play = useCallback( - ({ isPreview = false } = {}) => { - if (isLineupLoading) return - - if (isPlaying && isPlayingId && isPreviewing === isPreview) { - dispatch(tracksActions.pause()) - recordPlay(trackId, false, true) - } else if ( - currentQueueItem.uid !== uid && - currentQueueItem.track && - currentQueueItem.track.track_id === trackId - ) { - dispatch(tracksActions.play()) - recordPlay(trackId) - } else { - dispatch(tracksActions.play(uid, { isPreview })) - recordPlay(trackId, true, true) - } - }, - [ - trackId, - currentQueueItem, - uid, - dispatch, - isPlaying, - isPlayingId, - isPreviewing, - isLineupLoading - ] - ) - - const handlePressPlay = useCallback(() => play(), [play]) - const handlePressPreview = useCallback( - () => play({ isPreview: true }), - [play] - ) - - const handlePressFavorites = useCallback(() => { - dispatch(setFavorite(trackId, FavoriteType.TRACK)) - navigation.push('Favorited', { - id: trackId, - favoriteType: FavoriteType.TRACK - }) - }, [dispatch, trackId, navigation]) - - const handlePressReposts = useCallback(() => { - dispatch(setRepost(trackId, RepostType.TRACK)) - navigation.push('Reposts', { id: trackId, repostType: RepostType.TRACK }) - }, [dispatch, trackId, navigation]) - - const handlePressComments = useCallback(() => { - openCommentDrawer({ entityId: trackId, navigation }) - trackEvent( - make({ - eventName: Name.COMMENTS_CLICK_COMMENT_STAT, - trackId, - source: 'track_page' - }) - ) - }, [openCommentDrawer, trackId, navigation]) - - const handlePressSave = () => { - if (!isOwner) { - if (hasSaved) { - dispatch(unsaveTrack(trackId, FavoriteSource.TRACK_PAGE)) - } else { - dispatch(saveTrack(trackId, FavoriteSource.TRACK_PAGE)) - } - } - } - - const handlePressRepost = () => { - if (!isOwner) { - if (hasReposted) { - dispatch(undoRepostTrack(trackId, RepostSource.TRACK_PAGE)) - } else { - dispatch(repostTrack(trackId, RepostSource.TRACK_PAGE)) - } - } - } - - const handlePressShare = () => { - dispatch( - requestOpenShareModal({ - type: 'track', - trackId, - source: ShareSource.PAGE - }) - ) - } - - const handlePressArtistName = useCallback(() => { - if (!user) { - return - } - navigation.push('Profile', { handle: user.handle }) - }, [navigation, user]) - - const handlePressTag = useCallback( - (tag: string) => { - if (isSearchV2Enabled) { - navigation.push('Search', { query: `#${tag}` }) - } else { - navigation.push('TagSearch', { query: tag }) - } - }, - [isSearchV2Enabled, navigation] - ) - - const handlePressEdit = useCallback(() => { - navigation?.push('EditTrack', { id: trackId }) - }, [navigation, trackId]) - - const handlePressOverflow = () => { - const isLongFormContent = - genre === Genre.PODCASTS || genre === Genre.AUDIOBOOKS - const addToAlbumAction = - isOwner && !ddexApp ? OverflowAction.ADD_TO_ALBUM : null - const overflowActions = [ - addToAlbumAction, - !isUnlisted || isOwner ? OverflowAction.ADD_TO_PLAYLIST : null, - isOwner - ? null - : user.does_current_user_follow - ? OverflowAction.UNFOLLOW_ARTIST - : OverflowAction.FOLLOW_ARTIST, - isLongFormContent - ? playbackPositionInfo?.status === 'COMPLETED' - ? OverflowAction.MARK_AS_UNPLAYED - : OverflowAction.MARK_AS_PLAYED - : null, - albumInfo ? OverflowAction.VIEW_ALBUM_PAGE : null, - OverflowAction.VIEW_ARTIST_PAGE, - isOwner && !ddexApp ? OverflowAction.EDIT_TRACK : null, - isOwner && isScheduledRelease && isUnlisted - ? OverflowAction.RELEASE_NOW - : null, - isOwner && !ddexApp ? OverflowAction.DELETE_TRACK : null - ].filter(removeNullable) - - dispatch( - openOverflowMenu({ - source: OverflowSource.TRACKS, - id: trackId, - overflowActions - }) - ) - } - - const publish = useCallback(() => { - dispatch(trackPageActions.makeTrackPublic(trackId)) - }, [dispatch, trackId]) - - const handlePressPublish = useCallback(() => { - if (isScheduledRelease) { - openEarlyReleaseConfirmation({ - confirmCallback: publish, - contentType: 'track' - }) - } else { - openPublishConfirmation({ - confirmCallback: publish, - contentType: 'track' - }) - } - }, [ - openPublishConfirmation, - openEarlyReleaseConfirmation, - isScheduledRelease, - publish - ]) - - const renderBottomContent = () => { - return hasDownloadableAssets ? ( - <> - - - - ) : null - } - - const renderDogEar = () => { - const dogEarType = getDogEarType({ - isOwner, - streamConditions - }) - return dogEarType ? : null - } - - const PreviewButton = () => ( - - ) - - const renderTags = () => { - if (!track || (isUnlisted && !track.field_visibility?.tags)) { - return null - } - - const filteredTags = (track.tags || '').split(',').filter(Boolean) - return filteredTags.length > 0 ? ( - - {filteredTags.map((tag) => ( - handlePressTag(tag)}> - {tag} - - ))} - - ) : null - } - - if (!trackId) return null - - if (isDeleted) { - return ( - - ) - } - - return ( - - }> - {renderDogEar()} - - - {headerText} - - - {badges.length > 0 ? ( - - {badges.map((badge) => badge)} - - ) : null} - {imageElement} - - - {title} - - {user ? ( - - - - {user.name} - - - - - ) : null} - - {isLongFormContent && track ? ( - - ) : null} - {hasStreamAccess ? ( - - ) : null} - {shouldShowPreview ? : null} - - - - {!hasStreamAccess && !isOwner && streamConditions && trackId ? ( - - ) : null} - {(hasStreamAccess || isOwner) && streamConditions ? ( - - ) : null} - - {description ? ( - - - {description} - - - ) : null} - - {renderTags()} - - - {renderBottomContent()} - - - ) -} From c4ef007a576db694898eaace036acbe52276c155 Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Tue, 29 Oct 2024 15:53:53 -0700 Subject: [PATCH 08/14] PR feedback --- .../core/Screen/ScreenContextProvider.tsx | 7 ++++- .../core/Screen/ScreenPrimaryContent.tsx | 12 +++++---- .../core/Screen/ScreenSecondaryContent.tsx | 15 ++++++----- .../core/Screen/hooks/useIsScreenReady.ts | 27 ++++++++++++------- .../src/components/core/Screen/types.ts | 5 ---- 5 files changed, 39 insertions(+), 27 deletions(-) delete mode 100644 packages/mobile/src/components/core/Screen/types.ts diff --git a/packages/mobile/src/components/core/Screen/ScreenContextProvider.tsx b/packages/mobile/src/components/core/Screen/ScreenContextProvider.tsx index 23f3950a63a..0368341e33c 100644 --- a/packages/mobile/src/components/core/Screen/ScreenContextProvider.tsx +++ b/packages/mobile/src/components/core/Screen/ScreenContextProvider.tsx @@ -2,7 +2,12 @@ import type { ReactNode } from 'react' import { createContext, useContext, useMemo, useState } from 'react' import { useIsScreenReady } from './hooks/useIsScreenReady' -import type { ScreenContextType } from './types' + +export type ScreenContextType = { + isScreenReady: boolean + isPrimaryContentReady: boolean + setIsPrimaryContentReady: (ready: boolean) => void +} export const ScreenContext = createContext({ isScreenReady: false, diff --git a/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx b/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx index 3a5df010bff..d83ed646e70 100644 --- a/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx +++ b/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx @@ -1,14 +1,16 @@ import { useEffect, type ReactNode } from 'react' import { useScreenContext } from './ScreenContextProvider' -export const ScreenPrimaryContent = ({ - children, - skeleton -}: { + +type ScreenPrimaryContentProps = { children: ReactNode skeleton?: ReactNode -}) => { +} + +export const ScreenPrimaryContent = (props: ScreenPrimaryContentProps) => { + const { children, skeleton } = props const { isScreenReady, setIsPrimaryContentReady } = useScreenContext() + useEffect(() => { if (!isScreenReady) return requestAnimationFrame(() => { diff --git a/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx b/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx index 4abb0d055bb..2c3e6b50188 100644 --- a/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx +++ b/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx @@ -1,14 +1,15 @@ import type { ReactNode } from 'react' import { useScreenContext } from './ScreenContextProvider' -export const ScreenSecondaryContent = ({ - children, - skeleton -}: { + +type ScreenSecondaryContentProps = { children: ReactNode skeleton?: ReactNode -}) => { +} + +export const ScreenSecondaryContent = (props: ScreenSecondaryContentProps) => { + const { children, skeleton } = props const { isPrimaryContentReady } = useScreenContext() - if (!isPrimaryContentReady) return <>{skeleton ?? null} - return <>{children} + + return <>{isPrimaryContentReady ? children : skeleton ?? null} } diff --git a/packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts b/packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts index eeaa7799e75..5d13eeafb38 100644 --- a/packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts +++ b/packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts @@ -3,27 +3,36 @@ import { useCallback, useState } from 'react' import { useFocusEffect } from '@react-navigation/native' import { InteractionManager } from 'react-native' +const SCHEDULING_DELAY = 1 + +// Maximum delay before allowing the screen to render +const RESET_DELAY = 200 + export const useIsScreenReady = () => { - const delay = 1 const [isReady, setIsReady] = useState(false) + // Runs each time the screen is focused useFocusEffect( useCallback(() => { - const timer = setTimeout(() => { + // Scheduled with minimal delay to keep the callback from interrupting interactions + const interactionTimer = setTimeout(() => { InteractionManager.runAfterInteractions(() => { setIsReady(true) }) - }, delay) + }, SCHEDULING_DELAY) - setTimeout(() => { - clearTimeout(timer) + // Failsafe to ensure the screen is never left in a loading state + // This puts a cap on the performance impact of this hook + const resetTimer = setTimeout(() => { + clearTimeout(interactionTimer) setIsReady(true) - }, 200) + }, RESET_DELAY) - // Reset the state when screen loses focus + // Stop tracking if the screen loses focus return () => { - clearTimeout(timer) - setIsReady(false) + clearTimeout(interactionTimer) + clearTimeout(resetTimer) + setIsReady(true) } }, []) ) diff --git a/packages/mobile/src/components/core/Screen/types.ts b/packages/mobile/src/components/core/Screen/types.ts deleted file mode 100644 index 69d277cafa7..00000000000 --- a/packages/mobile/src/components/core/Screen/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type ScreenContextType = { - isScreenReady: boolean - isPrimaryContentReady: boolean - setIsPrimaryContentReady: (ready: boolean) => void -} From dc962c3737120025ea99f4d1fa64d1bb3f0632a3 Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Tue, 29 Oct 2024 16:02:07 -0700 Subject: [PATCH 09/14] documentation --- .../src/components/core/Screen/ScreenPrimaryContent.tsx | 7 +++++++ .../src/components/core/Screen/ScreenSecondaryContent.tsx | 7 +++++++ .../src/components/core/Screen/hooks/useIsScreenReady.ts | 4 ++++ 3 files changed, 18 insertions(+) diff --git a/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx b/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx index d83ed646e70..83c8dbc6655 100644 --- a/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx +++ b/packages/mobile/src/components/core/Screen/ScreenPrimaryContent.tsx @@ -7,6 +7,13 @@ type ScreenPrimaryContentProps = { skeleton?: ReactNode } +/** + * ScreenPrimaryContent is a wrapper that ensures the primary content is only rendered + * after the screen is ready and blocks any ScreenSecondaryContent from rendering until + * the PrimaryContent is ready + * + * _Note: ScreenPrimaryContent should not be used outside of a Screen component_ + */ export const ScreenPrimaryContent = (props: ScreenPrimaryContentProps) => { const { children, skeleton } = props const { isScreenReady, setIsPrimaryContentReady } = useScreenContext() diff --git a/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx b/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx index 2c3e6b50188..0c6f49d1732 100644 --- a/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx +++ b/packages/mobile/src/components/core/Screen/ScreenSecondaryContent.tsx @@ -7,6 +7,13 @@ type ScreenSecondaryContentProps = { skeleton?: ReactNode } +/** + * ScreenSecondaryContent is a wrapper that ensures the secondary content is only + * rendered after any ScreenPrimaryContent within the same Screen component is ready + * + * _Note: ScreenSecondaryContent should not be used outside of a Screen component + * or in a Screen without a ScreenPrimaryContent_ + */ export const ScreenSecondaryContent = (props: ScreenSecondaryContentProps) => { const { children, skeleton } = props const { isPrimaryContentReady } = useScreenContext() diff --git a/packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts b/packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts index 5d13eeafb38..76ead6cf253 100644 --- a/packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts +++ b/packages/mobile/src/components/core/Screen/hooks/useIsScreenReady.ts @@ -8,6 +8,10 @@ const SCHEDULING_DELAY = 1 // Maximum delay before allowing the screen to render const RESET_DELAY = 200 +/** + * useIsScreenReady returns a boolean indicating whether the screen animations + * have completed and the screen content can be safely rendered + */ export const useIsScreenReady = () => { const [isReady, setIsReady] = useState(false) From f1c2d6fb3ed1e63853c45604caf4ba48536e8453 Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Tue, 29 Oct 2024 16:21:57 -0700 Subject: [PATCH 10/14] wip --- .../collection-screen/CollectionScreen.tsx | 5 +- .../CollectionScreenDetailsTile.tsx | 238 +++++++++--------- .../useFetchCollectionLineup.ts | 1 + 3 files changed, 126 insertions(+), 118 deletions(-) diff --git a/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx b/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx index 61d64eadaa7..5b2df890db6 100644 --- a/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx +++ b/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx @@ -41,6 +41,7 @@ import { VirtualizedScrollView, Divider } from 'app/components/core' +import { ScreenSecondaryContent } from 'app/components/core/Screen/ScreenSecondaryContent' import { CollectionImage } from 'app/components/image/CollectionImage' import { SuggestedTracks } from 'app/components/suggested-tracks' import { useNavigation } from 'app/hooks/useNavigation' @@ -332,10 +333,10 @@ const CollectionScreenComponent = (props: CollectionScreenComponentProps) => { updatedAt={updated_at} /> {isOwner && !is_album && !ddex_app ? ( - <> + - + ) : null} diff --git a/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx b/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx index 7fc2cf6ff23..6c4feda53af 100644 --- a/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx +++ b/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx @@ -48,6 +48,8 @@ import { spacing } from '@audius/harmony-native' import { DogEar, UserGeneratedText } from 'app/components/core' +import { ScreenPrimaryContent } from 'app/components/core/Screen/ScreenPrimaryContent' +import { ScreenSecondaryContent } from 'app/components/core/Screen/ScreenSecondaryContent' import { CollectionMetadataList } from 'app/components/details-tile/CollectionMetadataList' import { DetailsTileActionButtons } from 'app/components/details-tile/DetailsTileActionButtons' import { DetailsTileHasAccess } from 'app/components/details-tile/DetailsTileHasAccess' @@ -409,127 +411,131 @@ export const CollectionScreenDetailsTile = ({ return ( - {renderDogEar()} - - - {messages.collectionType} - + + {renderDogEar()} + + + {messages.collectionType} + - {badges.length > 0 ? ( - - {badges.map((badge) => badge)} + {badges.length > 0 ? ( + + {badges.map((badge) => badge)} + + ) : null} + {imageElement} + + + {title} + + {user ? ( + + + + {user.name} + + + + + ) : null} - ) : null} - {imageElement} - - - {title} - - {user ? ( - - - - {user.name} - - - - + {shouldShowPlay ? ( + ) : null} - - {shouldShowPlay ? ( - - ) : null} - {shouldShowPreview ? : null} - - - - {!hasStreamAccess && - !isOwner && - streamConditions && - numericCollectionId ? ( - - ) : null} - {(hasStreamAccess || isOwner) && streamConditions ? ( - : null} + - ) : null} - {isPublished && numericCollectionId ? ( - - ) : null} - {description ? ( - - - {description} - - - ) : null} - {numericCollectionId ? ( - - ) : null} - - - - {renderTrackList()} + + + {!hasStreamAccess && + !isOwner && + streamConditions && + numericCollectionId ? ( + + ) : null} + {(hasStreamAccess || isOwner) && streamConditions ? ( + + ) : null} + {isPublished && numericCollectionId ? ( + + ) : null} + {description ? ( + + + {description} + + + ) : null} + {numericCollectionId ? ( + + ) : null} + + + + + + {renderTrackList()} + ) } diff --git a/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts b/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts index e1a09e9c995..ac88ad814a2 100644 --- a/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts +++ b/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts @@ -13,6 +13,7 @@ import { areSetsEqual, Uid, makeUid } from '@audius/common/utils' import moment from 'moment' import { useDispatch, useSelector } from 'react-redux' +import { useIsScreenReady } from 'app/components/core/Screen/hooks/useIsScreenReady' import { useReachabilityEffect } from 'app/hooks/useReachabilityEffect' import { getOfflineTrackIds } from 'app/store/offline-downloads/selectors' From f81aef9f083d985cf332bf28b84635d61eaab7d0 Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Tue, 29 Oct 2024 17:20:03 -0700 Subject: [PATCH 11/14] Make collection screen load faster --- .../collection-screen/CollectionScreen.tsx | 21 +-- .../CollectionScreenDetailsTile.tsx | 134 +++++++++++------- .../useFetchCollectionLineup.ts | 36 ++++- 3 files changed, 114 insertions(+), 77 deletions(-) diff --git a/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx b/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx index 5b2df890db6..c125338286d 100644 --- a/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx +++ b/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx @@ -17,7 +17,6 @@ import type { import { accountSelectors, collectionPageSelectors, - collectionPageActions, collectionsSocialActions, mobileOverflowMenuUIActions, shareModalUIActions, @@ -53,7 +52,6 @@ import { useThemePalette } from 'app/utils/theme' import { CollectionScreenDetailsTile } from './CollectionScreenDetailsTile' import { CollectionScreenSkeleton } from './CollectionScreenSkeleton' -import { useFetchCollectionLineup } from './useFetchCollectionLineup' const { setFavorite } = favoritesUserListActions const { setRepost } = repostsUserListActions @@ -65,7 +63,6 @@ const { undoRepostCollection, unsaveCollection } = collectionsSocialActions -const { resetCollection, fetchCollection } = collectionPageActions const { getCollection, getUser } = collectionPageSelectors const getUserId = accountSelectors.getUserId @@ -84,25 +81,9 @@ const useStyles = makeStyles(({ spacing }) => ({ */ export const CollectionScreen = () => { const { params } = useRoute<'Collection'>() - const dispatch = useDispatch() // params is incorrectly typed and can sometimes be undefined - const { - id = null, - searchCollection, - slug, - collectionType, - handle - } = params ?? {} - - const permalink = slug ? `/${handle}/${collectionType}/${slug}` : undefined - - const handleFetchCollection = useCallback(() => { - dispatch(resetCollection()) - dispatch(fetchCollection(id, permalink, true)) - }, [dispatch, id, permalink]) - - useFetchCollectionLineup(id, handleFetchCollection) + const { id = null, searchCollection, collectionType } = params ?? {} const cachedCollection = useSelector((state) => getCollection(state, { id }) diff --git a/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx b/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx index 6c4feda53af..be5d8722194 100644 --- a/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx +++ b/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx @@ -22,7 +22,8 @@ import { playerSelectors, cacheTracksSelectors, PurchaseableContentType, - queueActions + queueActions, + collectionPageActions } from '@audius/common/store' import { formatReleaseDate, getDogEarType } from '@audius/common/utils' import type { Maybe, Nullable } from '@audius/common/utils' @@ -60,15 +61,19 @@ import { OfflineStatusRow } from 'app/components/offline-downloads' import { TrackList } from 'app/components/track-list' import UserBadges from 'app/components/user-badges' import { useNavigation } from 'app/hooks/useNavigation' +import { useRoute } from 'app/hooks/useRoute' import { make, track } from 'app/services/analytics' import type { AppState } from 'app/store' import { makeStyles } from 'app/styles' +import { useFetchCollectionLineup } from './useFetchCollectionLineup' + const { getPlaying, getPreviewing, getUid, getCurrentTrack } = playerSelectors const { getIsReachable } = reachabilitySelectors const { getCollectionTracksLineup } = collectionPageSelectors const { getCollection, getCollectionTracks } = cacheCollectionsSelectors const { getTracks } = cacheTracksSelectors +const { resetCollection, fetchCollection } = collectionPageActions const selectTrackUids = createSelector( (state: AppState) => getCollectionTracksLineup(state).entries, @@ -218,6 +223,18 @@ export const CollectionScreenDetailsTile = ({ const isReachable = useSelector(getIsReachable) + const { params } = useRoute<'Collection'>() + + // params is incorrectly typed and can sometimes be undefined + const { slug, collectionType, handle } = params ?? {} + const permalink = slug ? `/${handle}/${collectionType}/${slug}` : undefined + + const handleFetchCollection = useCallback(() => { + dispatch(resetCollection()) + dispatch(fetchCollection(collectionId as number, permalink, true)) + }, [dispatch, collectionId, permalink]) + + useFetchCollectionLineup(collectionId, handleFetchCollection) const { data: currentUserId } = useGetCurrentUserId({}) // Since we're supporting SmartCollections, need to explicitly check that // collectionId is a number before fetching the playlist. -1 is a placeholder, @@ -248,7 +265,6 @@ export const CollectionScreenDetailsTile = ({ const collectionTrackCount = useSelector(selectTrackCount) const trackCount = trackCountProp ?? collectionTrackCount const isLineupLoading = useSelector(selectIsLineupLoading) - const playingUid = useSelector(getUid) const isQueued = useSelector(selectIsQueued) const isPlaybackActive = useSelector(getPlaying) const isPlaying = isPlaybackActive && isQueued @@ -335,53 +351,6 @@ export const CollectionScreenDetailsTile = ({ [play] ) - const handlePressTrackListItemPlay = useCallback( - (uid: UID, id: ID) => { - if (isPlaying && playingUid === uid) { - dispatch(tracksActions.pause()) - recordPlay(id, false) - } else if (playingUid !== uid) { - dispatch(tracksActions.play(uid)) - recordPlay(id) - } else { - dispatch(tracksActions.play()) - recordPlay(id) - } - }, - [dispatch, isPlaying, playingUid] - ) - - const renderTrackList = useCallback(() => { - return ( - - - {isOwner ? messages.empty : messages.emptyPublic} - - - ) - } - /> - ) - }, [ - numericCollectionId, - isAlbum, - handlePressTrackListItemPlay, - isLineupLoading, - styles, - uids, - isOwner, - messages - ]) - const renderDogEar = () => { const dogEarType = getDogEarType({ isOwner, @@ -534,8 +503,73 @@ export const CollectionScreenDetailsTile = ({ - {renderTrackList()} + ) } + +type CollectionTrackListProps = { + isAlbum?: boolean + isOwner: boolean + isPlaying: boolean + numericCollectionId?: number + isLineupLoading: boolean + uids: UID[] +} + +const CollectionTrackList = ({ + isAlbum, + isOwner, + numericCollectionId, + isLineupLoading, + isPlaying, + uids +}: CollectionTrackListProps) => { + const styles = useStyles() + const dispatch = useDispatch() + const playingUid = useSelector(getUid) + const messages = getMessages(isAlbum ? 'album' : 'playlist') + + const handlePressTrackListItemPlay = useCallback( + (uid: UID, id: ID) => { + if (isPlaying && playingUid === uid) { + dispatch(tracksActions.pause()) + recordPlay(id, false) + } else if (playingUid !== uid) { + dispatch(tracksActions.play(uid)) + recordPlay(id) + } else { + dispatch(tracksActions.play()) + recordPlay(id) + } + }, + [dispatch, isPlaying, playingUid] + ) + return ( + + + {isOwner ? messages.empty : messages.emptyPublic} + + + ) + } + /> + ) +} diff --git a/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts b/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts index ac88ad814a2..1e3b93f3201 100644 --- a/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts +++ b/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useEffect } from 'react' import type { SmartCollectionVariant } from '@audius/common/models' import { Kind } from '@audius/common/models' @@ -7,13 +7,15 @@ import { cacheActions, collectionPageLineupActions, collectionPageSelectors, - queueSelectors + queueSelectors, + reachabilitySelectors } from '@audius/common/store' import { areSetsEqual, Uid, makeUid } from '@audius/common/utils' import moment from 'moment' import { useDispatch, useSelector } from 'react-redux' +import { usePrevious } from 'react-use' -import { useIsScreenReady } from 'app/components/core/Screen/hooks/useIsScreenReady' +import { useScreenContext } from 'app/components/core/Screen/ScreenContextProvider' import { useReachabilityEffect } from 'app/hooks/useReachabilityEffect' import { getOfflineTrackIds } from 'app/store/offline-downloads/selectors' @@ -22,7 +24,7 @@ import { useHasCollectionChanged } from './useHasCollectionChanged' const { getCollection } = cacheCollectionsSelectors const { getCollectionTracksLineup } = collectionPageSelectors const { getPositions } = queueSelectors - +const { getIsReachable } = reachabilitySelectors /** * Returns a collection lineup, supports boths online and offline * @param collectionId the numeric collection id @@ -32,6 +34,7 @@ export const useFetchCollectionLineup = ( fetchLineup: () => void ) => { const dispatch = useDispatch() + const { isScreenReady } = useScreenContext() const offlineTrackIds = useSelector( (state) => new Set(getOfflineTrackIds(state) || []), areSetsEqual @@ -73,7 +76,7 @@ export const useFetchCollectionLineup = ( ) as Record const fetchLineupOffline = useCallback(() => { - if (collectionId && collection) { + if (collectionId && collection && isScreenReady) { const trackIdEncounters = {} as Record const sortedTracks = collection.playlist_contents.track_ids .filter(({ track: trackId }) => offlineTrackIds.has(trackId.toString())) @@ -122,6 +125,7 @@ export const useFetchCollectionLineup = ( }, [ collectionId, collection, + isScreenReady, dispatch, offlineTrackIds, queueUidsByTrackId, @@ -129,7 +133,25 @@ export const useFetchCollectionLineup = ( collectionUidSource ]) + const isReachable = useSelector(getIsReachable) + const fetchLineupWrapped = useCallback(() => { + if (isScreenReady) { + if (isReachable) { + fetchLineup() + } else { + fetchLineupOffline() + } + } + }, [isScreenReady, isReachable, fetchLineup, fetchLineupOffline]) + + const wasScreenReady = usePrevious(isScreenReady) + useEffect(() => { + if (isScreenReady && !wasScreenReady) { + fetchLineupWrapped() + } + }, [isScreenReady, fetchLineupWrapped, wasScreenReady]) + // Fetch the lineup based on reachability - useReachabilityEffect(fetchLineup, fetchLineupOffline) - useHasCollectionChanged(collectionId as number, fetchLineup) + useReachabilityEffect(fetchLineupWrapped, fetchLineupWrapped) + useHasCollectionChanged(collectionId as number, fetchLineupWrapped) } From 84d8ea400ea3411e6b0d20f72c4673b3c10510d3 Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Tue, 29 Oct 2024 17:53:33 -0700 Subject: [PATCH 12/14] wip --- .../useFetchCollectionLineup.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts b/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts index 1e3b93f3201..54d9d174156 100644 --- a/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts +++ b/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts @@ -31,10 +31,15 @@ const { getIsReachable } = reachabilitySelectors */ export const useFetchCollectionLineup = ( collectionId: number | SmartCollectionVariant | null, - fetchLineup: () => void + fetchLineupProp: () => void ) => { + const fetchLineup = useCallback(() => { + console.log('fetchLineup') + return fetchLineupProp() + }, [fetchLineupProp]) + const dispatch = useDispatch() - const { isScreenReady } = useScreenContext() + const { isPrimaryContentReady } = useScreenContext() const offlineTrackIds = useSelector( (state) => new Set(getOfflineTrackIds(state) || []), areSetsEqual @@ -76,7 +81,8 @@ export const useFetchCollectionLineup = ( ) as Record const fetchLineupOffline = useCallback(() => { - if (collectionId && collection && isScreenReady) { + if (collectionId && collection) { + console.log('fetchLineupOffline') const trackIdEncounters = {} as Record const sortedTracks = collection.playlist_contents.track_ids .filter(({ track: trackId }) => offlineTrackIds.has(trackId.toString())) @@ -125,7 +131,6 @@ export const useFetchCollectionLineup = ( }, [ collectionId, collection, - isScreenReady, dispatch, offlineTrackIds, queueUidsByTrackId, @@ -135,23 +140,25 @@ export const useFetchCollectionLineup = ( const isReachable = useSelector(getIsReachable) const fetchLineupWrapped = useCallback(() => { - if (isScreenReady) { + if (isPrimaryContentReady) { if (isReachable) { fetchLineup() } else { fetchLineupOffline() } } - }, [isScreenReady, isReachable, fetchLineup, fetchLineupOffline]) + }, [isPrimaryContentReady, isReachable, fetchLineup, fetchLineupOffline]) - const wasScreenReady = usePrevious(isScreenReady) + // TODO: this gets called again when revisiting the same collection + // Need to figure out a better way to handle this + const wasScreenReady = usePrevious(isPrimaryContentReady) useEffect(() => { - if (isScreenReady && !wasScreenReady) { + if (isPrimaryContentReady && !wasScreenReady) { fetchLineupWrapped() } - }, [isScreenReady, fetchLineupWrapped, wasScreenReady]) + }, [isPrimaryContentReady, fetchLineupWrapped, wasScreenReady]) // Fetch the lineup based on reachability - useReachabilityEffect(fetchLineupWrapped, fetchLineupWrapped) + useReachabilityEffect(fetchLineup, fetchLineupOffline, false) useHasCollectionChanged(collectionId as number, fetchLineupWrapped) } From d533bdd2dd7d3930f2cd750bb6647700922cf7df Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Wed, 30 Oct 2024 15:30:03 -0700 Subject: [PATCH 13/14] lineup timing refactor --- .../CollectionScreenDetailsTile.tsx | 43 +++++++++++-------- .../useFetchCollectionLineup.ts | 42 +++--------------- 2 files changed, 30 insertions(+), 55 deletions(-) diff --git a/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx b/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx index be5d8722194..aa13cd84c52 100644 --- a/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx +++ b/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx @@ -223,18 +223,6 @@ export const CollectionScreenDetailsTile = ({ const isReachable = useSelector(getIsReachable) - const { params } = useRoute<'Collection'>() - - // params is incorrectly typed and can sometimes be undefined - const { slug, collectionType, handle } = params ?? {} - const permalink = slug ? `/${handle}/${collectionType}/${slug}` : undefined - - const handleFetchCollection = useCallback(() => { - dispatch(resetCollection()) - dispatch(fetchCollection(collectionId as number, permalink, true)) - }, [dispatch, collectionId, permalink]) - - useFetchCollectionLineup(collectionId, handleFetchCollection) const { data: currentUserId } = useGetCurrentUserId({}) // Since we're supporting SmartCollections, need to explicitly check that // collectionId is a number before fetching the playlist. -1 is a placeholder, @@ -304,6 +292,10 @@ export const CollectionScreenDetailsTile = ({ const shouldShowPreview = isUSDCPurchaseGated && !hasStreamAccess && !shouldShowPlay + useEffect(() => { + dispatch(resetCollection()) + }, [dispatch]) + useRefetchLineupOnTrackAdd(collectionId) const badges = [ @@ -507,7 +499,7 @@ export const CollectionScreenDetailsTile = ({ isAlbum={isAlbum} isOwner={isOwner} isPlaying={isPlaying} - numericCollectionId={numericCollectionId} + collectionId={collectionId} isLineupLoading={isLineupLoading} uids={uids} /> @@ -516,11 +508,10 @@ export const CollectionScreenDetailsTile = ({ ) } -type CollectionTrackListProps = { - isAlbum?: boolean - isOwner: boolean - isPlaying: boolean - numericCollectionId?: number +type CollectionTrackListProps = Pick< + CollectionScreenDetailsTileProps, + 'isAlbum' | 'isOwner' | 'isPlaying' | 'collectionId' +> & { isLineupLoading: boolean uids: UID[] } @@ -528,7 +519,7 @@ type CollectionTrackListProps = { const CollectionTrackList = ({ isAlbum, isOwner, - numericCollectionId, + collectionId, isLineupLoading, isPlaying, uids @@ -538,6 +529,20 @@ const CollectionTrackList = ({ const playingUid = useSelector(getUid) const messages = getMessages(isAlbum ? 'album' : 'playlist') + const numericCollectionId = + typeof collectionId === 'number' ? collectionId : undefined + + const { params } = useRoute<'Collection'>() + const { slug, collectionType, handle } = params ?? {} + const permalink = slug ? `/${handle}/${collectionType}/${slug}` : undefined + + const handleFetchCollection = useCallback(() => { + dispatch(resetCollection()) + dispatch(fetchCollection(collectionId as number, permalink, true)) + }, [dispatch, collectionId, permalink]) + + useFetchCollectionLineup(collectionId, handleFetchCollection) + const handlePressTrackListItemPlay = useCallback( (uid: UID, id: ID) => { if (isPlaying && playingUid === uid) { diff --git a/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts b/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts index 54d9d174156..e1a09e9c995 100644 --- a/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts +++ b/packages/mobile/src/screens/collection-screen/useFetchCollectionLineup.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react' +import { useCallback } from 'react' import type { SmartCollectionVariant } from '@audius/common/models' import { Kind } from '@audius/common/models' @@ -7,15 +7,12 @@ import { cacheActions, collectionPageLineupActions, collectionPageSelectors, - queueSelectors, - reachabilitySelectors + queueSelectors } from '@audius/common/store' import { areSetsEqual, Uid, makeUid } from '@audius/common/utils' import moment from 'moment' import { useDispatch, useSelector } from 'react-redux' -import { usePrevious } from 'react-use' -import { useScreenContext } from 'app/components/core/Screen/ScreenContextProvider' import { useReachabilityEffect } from 'app/hooks/useReachabilityEffect' import { getOfflineTrackIds } from 'app/store/offline-downloads/selectors' @@ -24,22 +21,16 @@ import { useHasCollectionChanged } from './useHasCollectionChanged' const { getCollection } = cacheCollectionsSelectors const { getCollectionTracksLineup } = collectionPageSelectors const { getPositions } = queueSelectors -const { getIsReachable } = reachabilitySelectors + /** * Returns a collection lineup, supports boths online and offline * @param collectionId the numeric collection id */ export const useFetchCollectionLineup = ( collectionId: number | SmartCollectionVariant | null, - fetchLineupProp: () => void + fetchLineup: () => void ) => { - const fetchLineup = useCallback(() => { - console.log('fetchLineup') - return fetchLineupProp() - }, [fetchLineupProp]) - const dispatch = useDispatch() - const { isPrimaryContentReady } = useScreenContext() const offlineTrackIds = useSelector( (state) => new Set(getOfflineTrackIds(state) || []), areSetsEqual @@ -82,7 +73,6 @@ export const useFetchCollectionLineup = ( const fetchLineupOffline = useCallback(() => { if (collectionId && collection) { - console.log('fetchLineupOffline') const trackIdEncounters = {} as Record const sortedTracks = collection.playlist_contents.track_ids .filter(({ track: trackId }) => offlineTrackIds.has(trackId.toString())) @@ -138,27 +128,7 @@ export const useFetchCollectionLineup = ( collectionUidSource ]) - const isReachable = useSelector(getIsReachable) - const fetchLineupWrapped = useCallback(() => { - if (isPrimaryContentReady) { - if (isReachable) { - fetchLineup() - } else { - fetchLineupOffline() - } - } - }, [isPrimaryContentReady, isReachable, fetchLineup, fetchLineupOffline]) - - // TODO: this gets called again when revisiting the same collection - // Need to figure out a better way to handle this - const wasScreenReady = usePrevious(isPrimaryContentReady) - useEffect(() => { - if (isPrimaryContentReady && !wasScreenReady) { - fetchLineupWrapped() - } - }, [isPrimaryContentReady, fetchLineupWrapped, wasScreenReady]) - // Fetch the lineup based on reachability - useReachabilityEffect(fetchLineup, fetchLineupOffline, false) - useHasCollectionChanged(collectionId as number, fetchLineupWrapped) + useReachabilityEffect(fetchLineup, fetchLineupOffline) + useHasCollectionChanged(collectionId as number, fetchLineup) } From 9093785a696d73ce75e09d409e35fd78cf9a0ca5 Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Wed, 30 Oct 2024 16:07:28 -0700 Subject: [PATCH 14/14] wrap a couple more things --- .../CollectionScreenDetailsTile.tsx | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx b/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx index aa13cd84c52..0ea2a9b0018 100644 --- a/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx +++ b/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx @@ -446,24 +446,26 @@ export const CollectionScreenDetailsTile = ({ borderBottomLeftRadius='m' borderBottomRightRadius='m' > - {!hasStreamAccess && - !isOwner && - streamConditions && - numericCollectionId ? ( - - ) : null} - {(hasStreamAccess || isOwner) && streamConditions ? ( - - ) : null} + + {!hasStreamAccess && + !isOwner && + streamConditions && + numericCollectionId ? ( + + ) : null} + {(hasStreamAccess || isOwner) && streamConditions ? ( + + ) : null} + {isPublished && numericCollectionId ? ( ) : null} - + + +