UNPKG

@applicaster/zapp-react-native-utils

Version:

Applicaster Zapp React Native utilities package

492 lines (386 loc) • 13.8 kB
/* eslint-disable @typescript-eslint/no-use-before-define */ /* eslint max-len: off */ import React, { useContext, useEffect, useMemo } from "react"; import { BackHandler, InteractionManager, NativeModules } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useContentTypes, usePickFromState, } from "@applicaster/zapp-react-native-redux/hooks"; import { HooksManager } from "@applicaster/zapp-react-native-utils/appUtils/HooksManager"; import { legacyScreenData } from "@applicaster/quick-brick-core/App/NavigationProvider/utils"; import { LONG_KEY_PRESS_TIMEOUT, QUICK_BRICK_NAVBAR, QUICK_BRICK_CONTENT, } from "@applicaster/quick-brick-core/const"; import { useZappHooksModalState } from "@applicaster/zapp-react-dom-ui-components/Components/ZappHooksModal/hooks/useZappHooksModalState"; import { ZappHookModalContext } from "@applicaster/zapp-react-native-ui-components/Contexts"; import { PathnameContext } from "@applicaster/zapp-react-native-ui-components/Contexts/PathnameContext"; import { HookModalContextT } from "@applicaster/zapp-react-native-ui-components/Contexts/ZappHookModalContext"; import { HOOKS_EVENTS } from "../../appUtils/HooksManager/constants"; import { getNavigationProps, getRiverFromRoute, getTargetRoute, } from "../../navigationUtils"; import { platformSelect } from "../../reactUtils"; import { useConnectionInfo } from "../connection"; import { getTargetScreenData } from "../screen/useTargetScreenData"; import { isTV, isWeb } from "@applicaster/zapp-react-native-utils/reactUtils"; import { useNavbarState } from "../screen"; import { useNavigation } from "./useNavigation"; import { useModalStore } from "../../modalState"; import { ScreenDataContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenDataContext"; export { useNavigation } from "./useNavigation"; /** * This function helps to decide wether the navbar should be presented on the screen * based on route and / or screen Data * * Currently the scenarios include * - players => false * - search screen plugin => false * - hooks with `showNavBar: true` or `presentFullScreen` not set to true, defaults to no navbar for hook screens * - screens with `allow_screen_plugin_presentation` in general property => false * * @param {String} route current route of the screen * @param {Object} screenData payload associated with the currently presented screen * @returns {Boolean} */ export function isNavBarVisible( route: string, screenData: ZappRiver = {} as ZappRiver, showNavBar: boolean, canGoBack: boolean, videoModalState: QuickBrickVideoModalState | null = null ) { /* screenData is not actual navigator.data * or navigator.screenData, * but navigator.data.screen */ if (route.includes("playable")) { return false; } if ( videoModalState?.mode === "FULLSCREEN" && videoModalState?.visible === true ) { return false; } if (screenData?.type === "qb_search_screen" && !isTV()) { return false; } if (screenData?.plugin_type === "login" && isTV()) { return false; } /* Match if screen is Hook */ if (route.startsWith("/hooks/")) { const module = screenData?.module; if (module?.presentFullScreen) { return false; } return module?.showNavBar ?? false; } if (screenData?.hookPlugin) { const hookPlugin = screenData?.hookPlugin?.module; return ( hookPlugin?.showNavBar || hookPlugin?.presentFullScreen !== true || false ); } if ( screenData?.general?.allow_screen_plugin_presentation || screenData?.general?.hide_app_nav_bar ) { return false; } const hideSecondaryLevel = screenData?.navigations?.some( (navigation) => navigation.styles?.hide_secondary_level_menu ); if (canGoBack && hideSecondaryLevel) { return false; } if (typeof showNavBar === "boolean" && !showNavBar) { return false; } return true; } const { ProfilerBridge } = NativeModules; export const usePathname = () => React.useContext(PathnameContext); export const useBackHandler = (cb: () => boolean) => { React.useEffect(() => { BackHandler.addEventListener("hardwareBackPress", cb); return () => { BackHandler.removeEventListener("hardwareBackPress", cb); }; }, [cb]); }; // starts with modal/ const isModalPathname = (pathname: string) => /^modal\//.test(pathname); const isVideoModalPathname = (pathname: string) => /^video-modal\//.test(pathname); const isHookModalPathname = (pathname: string) => /^hooks-modal\//.test(pathname); const isHookPathname = (pathname: string) => /^\/hooks\//.test(pathname); type VariousScreenData = LegacyNavigationScreenData | ZappRiver | ZappEntry; export const useRoute = (): { screenData: VariousScreenData; pathname: string; } => { const pathname = usePathname() || ""; const navigator = useNavigation(); const screenDataContext = legacyScreenData(useContext(ScreenDataContext)); const { plugins, contentTypes, rivers } = usePickFromState([ "plugins", "rivers", "contentTypes", ]); const { modalState } = useModalStore(); const hookModalState = useZappHooksModalState(); const modalScreenData = modalState.screen; const hookModalScreenData = hookModalState.state.screenData?.payload; const videoModalScreenData = navigator.videoModalState?.item && legacyScreenData( { entry: navigator.videoModalState?.item as ZappEntry, screen: getTargetScreenData( navigator.videoModalState?.item as ZappEntry, rivers, contentTypes ), }, plugins ); // There are 4 route scenarios // For regular screens take date from navigator stack. // is path is model grab screenData from modal state // if path is video modal grab screenData from video state // if path is hook modal grab screenData from hook state // if path is hook grab screenData from screenData if (isModalPathname(pathname)) { const screenData = modalScreenData ?? ({} as ZappEntry); return { screenData, pathname }; } if (isVideoModalPathname(pathname)) { const screenData = videoModalScreenData as LegacyNavigationScreenData; return { screenData, pathname }; } if (isHookModalPathname(pathname)) { const screenData = hookModalScreenData; return { screenData, pathname }; } if (isHookPathname(pathname)) { return { screenData: screenDataContext, pathname }; } const screenData = screenDataContext; return { screenData, pathname }; }; type Callbacks = Partial<{ handleHookPresent: ({ route, payload, }: { route: string; payload: HookPluginProps; }) => void; handleHookError: (props: HookPluginProps & { error?: string }) => any; handleHookCancel: (props: Omit<HookPluginProps, "callback">) => any; handleHookComplete: ( props: Omit<HookPluginProps, "callback"> & { route: string } ) => any; }>; export const useZappHooksForEntry = ( entry: ZappEntry, callbacks?: Callbacks ) => { const { setState, resetState, setIsHooksExecutionInProgress, setIsPresentingFullScreen, setIsRunningInBackground, }: HookModalContextT = React.useContext(ZappHookModalContext.ReactContext); const { appData: { layoutVersion }, rivers, plugins, } = usePickFromState(["appData", "rivers", "plugins"]); const contentTypes = useContentTypes(); const isOnline = useConnectionInfo(true); useEffect(() => { resetState(); if (entry) { setIsHooksExecutionInProgress(true); if (!isOnline) { setIsHooksExecutionInProgress(false); // TODO: ideally move this logic to hooks manager // @ts-ignore return callbacks?.handleHookComplete?.({ payload: entry }); } const targetRoute = getTargetRoute(entry, "", { layoutVersion, contentTypes, }); const targetScreen = getRiverFromRoute({ route: targetRoute, rivers }); const hooksOptions = { rivers, plugins, targetScreen, }; const handleHookPresent = ({ route, payload }) => { setIsPresentingFullScreen(); setState({ path: route, screenData: payload, }); callbacks?.handleHookPresent?.({ route, payload }); }; const handleBackgroundHook = (hookProps) => { const { route, payload } = hookProps; setState({ path: route, screenData: payload, }); setIsRunningInBackground(); }; const handleHookError: Callbacks["handleHookError"] = (props) => { setIsHooksExecutionInProgress(false); callbacks?.handleHookError?.(props); }; const handleHookCancel: Callbacks["handleHookCancel"] = (props) => { setIsHooksExecutionInProgress(false); callbacks?.handleHookCancel?.(props); }; const handleHookComplete: Callbacks["handleHookComplete"] = (props) => { setIsHooksExecutionInProgress(false); callbacks?.handleHookComplete?.(props); }; const hookManager = HooksManager(hooksOptions) .on(HOOKS_EVENTS.PRESENT_SCREEN_HOOK, handleHookPresent) .on(HOOKS_EVENTS.ERROR, handleHookError) .on(HOOKS_EVENTS.CANCEL, handleHookCancel) .on(HOOKS_EVENTS.START_BACKGROUND_HOOK, handleBackgroundHook) .on(HOOKS_EVENTS.COMPLETE, handleHookComplete) .handleHooks(entry); return () => { hookManager .removeHandler(HOOKS_EVENTS.PRESENT_SCREEN_HOOK, handleHookPresent) .removeHandler(HOOKS_EVENTS.ERROR, handleHookError) .removeHandler(HOOKS_EVENTS.CANCEL, handleHookCancel) .removeHandler(HOOKS_EVENTS.COMPLETE, handleHookComplete); }; } }, [entry.id]); }; export enum MenuTypes { drawer = "DRAWER", bottomTabBar = "BOTTOM_TAB_BAR", } export const useNavigationPluginData = (): ZappNavigation | undefined => { const { screenData: useRouteScreenData, }: { screenData: QuickBrickNavigationData | null } = useRoute(); const navigations = useRouteScreenData?.targetScreen ? (useRouteScreenData.targetScreen as ZappRiver).navigations : (useRouteScreenData as ZappRiver).navigations; const navigationMenu = navigations?.find((nav) => nav.category === "menu"); return navigationMenu; }; export const useNavigationType = (): MenuTypes => { const navigationMenu = useNavigationPluginData(); return !navigationMenu || navigationMenu.navigation_type === "quick_brick_side_menu" ? MenuTypes.drawer : MenuTypes.bottomTabBar; }; export const useProfilerLogging = () => { const logTimestamp = (data: string) => InteractionManager.runAfterInteractions(() => { if (ProfilerBridge && data) { ProfilerBridge.updateTimer("navigation", data, Date.now()); } }); return logTimestamp; }; export const useRunIfLongPress = ( options = { timeout: LONG_KEY_PRESS_TIMEOUT } ) => { const pressTimeout = React.useRef<Nullable<ReturnType<typeof setTimeout>>>(null); const resetTimeout = () => { clearTimeout(pressTimeout.current as ReturnType<typeof setTimeout>); }; const startTimeout = (cb: () => void) => { pressTimeout.current = setTimeout(cb, options.timeout); }; const onPress = (cb: () => void) => { startTimeout(cb); }; const onPressOut = () => { resetTimeout(); }; return { onPress, onPressOut }; }; /** * @returns boolean - navbar visiblity status */ export const useIsNavBarVisible = (): boolean => { const { visible } = useNavbarState(); return useMemo(() => visible, [visible]); }; const useGetNavBarTopBorderWidth = (): number => { const navigator = useNavigation(); const navProps = getNavigationProps({ navigator, title: "", category: "menu", }); return Number(((navProps?.styles as any)?.top_border_width as number) ?? 0); }; const TAB_BAR_HEIGHT_IOS = 49; const TAB_BAR_HEIGHT_ANDROID = 56; const useGetTabBarHeight = () => platformSelect({ ios: TAB_BAR_HEIGHT_IOS + (useSafeAreaInsets().bottom ?? 0), android: TAB_BAR_HEIGHT_ANDROID, }) as number; export const useGetBottomTabBarHeight = (): number => { const topBorderWidth = useGetNavBarTopBorderWidth(); const tabBarHeight = useGetTabBarHeight(); const isBottomBarNavigation = useNavigationType() === MenuTypes.bottomTabBar; return !isBottomBarNavigation ? 0 : tabBarHeight + topBorderWidth; }; // If current screen is active/focused (visible to the user) export const useIsScreenActive = () => { const pathname = usePathname(); const { currentRoute, videoModalState } = useNavigation(); if ( videoModalState.visible && ["FULLSCREEN", "MAXIMIZED", "PIP"].includes(videoModalState.mode) ) { return false; } return pathname === currentRoute; }; type Return = (prefix: string) => string; export const useUniqueRouteSuffix = (): Return => { const pathname = usePathname(); const { currentRoute } = useNavigation(); const suffix = `currentRoute:${currentRoute}_pathname:${pathname}`; const withUniqueRouteSuffix = React.useCallback((prefix: string) => { if (isWeb()) { // we should use extended route prefix only on Web, where we have stack-navigator return `${prefix}___${suffix}`; } // for other platforms use previous version return prefix; }, []); return withUniqueRouteSuffix; }; export const useNavbarId = () => { const withUniqueRouteSuffix = useUniqueRouteSuffix(); return withUniqueRouteSuffix(QUICK_BRICK_NAVBAR); }; export const useContentId = () => { const withUniqueRouteSuffix = useUniqueRouteSuffix(); return withUniqueRouteSuffix(QUICK_BRICK_CONTENT); };