import React, { useEffect } from "react"; import * as R from "ramda"; import { isGroup } from "@applicaster/zapp-react-native-utils/componentsUtils"; import { applyDecorators } from "../../Decorators"; import { useCellResolver, useComponentResolver, } from "@applicaster/zapp-react-native-utils/reactHooks/resolvers"; import { riverLogger } from "./logger"; import { tvPluginsWithCellRenderer } from "../../const"; import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils"; import type { BehaviorSubject } from "rxjs"; import { useCallbackActions } from "@applicaster/zapp-react-native-utils/zappFrameworkUtils/HookCallback/useCallbackActions"; import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers"; export type RiverItemType = { item: ZappUIComponent; index: number; isScreenWrappedInContainer: boolean; groupId: string | number; feed: ZappFeed; onLoadFinished: (index: number) => void; onLoadFailed: ({ error, index }: { error: Error; index: number }) => void; riverId: string; getStaticComponentFeed: GeneralContentScreenProps["getStaticComponentFeed"]; readyToBeDisplayed?: boolean; isLast: boolean; loadingState: BehaviorSubject<{ index: number }>; }; function getFeedUrl(feed: ZappFeed, index: number) { try { const feedUrl = R.path(["entry", index, "content", "src"], feed); return (R.contains || R.includes)("fetchData?", feedUrl) ? feedUrl : null; } catch (error) { return null; } } const isNextIndex = (index, readyIndex) => { return readyIndex + 1 >= index; }; /** * useLoadingState for RiverItemComponent * takes currentIndex and loadingState as arguments **/ const useLoadingState = ( currentIndex: number, loadingState: RiverItemType["loadingState"] ) => { const [readyToBeDisplayed, setReadyToBeDisplayed] = React.useState( isNextIndex(currentIndex, loadingState.getValue().index) ); useEffect(() => { const subscription = loadingState.subscribe(({ index }) => { if (isNextIndex(currentIndex, index)) { setReadyToBeDisplayed(true); } }); return () => { subscription.unsubscribe(); }; }, [currentIndex]); return readyToBeDisplayed; }; function RiverItemComponent(props: RiverItemType) { const { item, riverId, index, groupId, feed, isScreenWrappedInContainer, onLoadFinished, onLoadFailed, getStaticComponentFeed, isLast, loadingState, } = props; const callbackAction = useCallbackActions(item); const readyToBeDisplayed = useLoadingState(index, loadingState); const feedUrl = getFeedUrl(feed, index); const Component = useComponentResolver( { componentType: item.component_type, decorators: applyDecorators, }, [] ); let CellRenderer = useCellResolver({ component: item }); if (tvPluginsWithCellRenderer.includes(item.component_type) && isTV()) { riverLogger.warning({ message: "Cell Renderer is not created for this component type by RiverItem", data: { component_type: item.component_type }, jsOnly: true, }); CellRenderer = undefined; } const isComponentMissing = isNilOrEmpty(Component); /** * TODO: Move this plugin existence check further up the stack (before ComponentsMap). * Filtering items at the list-rendering or data-processing level would prevent * mounting RiverItem entirely for missing components. */ React.useEffect(() => { if (isComponentMissing) { riverLogger.warning({ message: `Component ${item.component_type} is null - skipping rendering`, }); onLoadFinished(index); } else { riverLogger.log({ message: "mounting component", data: { item, feedUrl, Component, CellRenderer }, jsOnly: true, }); if (!CellRenderer && !isGroup(item)) { riverLogger.warning({ message: "Cell Renderer is null - will fallback to default cell", data: { item, CellRenderer }, jsOnly: true, }); } } }, []); if (!readyToBeDisplayed || isComponentMissing) { return null; } return ( onLoadFinished(index)} groupId={groupId} feedUrl={feedUrl} isLast={isLast} callback={callbackAction} /> ); } export const RiverItem = React.memo( RiverItemComponent, R.equals );