import * as React from "react"; import * as R from "ramda"; import { View, StyleSheet } from "react-native"; import { first, filter } from "rxjs/operators"; import { compose } from "@applicaster/zapp-react-native-utils/utils"; import { Focusable } from "@applicaster/zapp-react-native-ui-components/Components/Focusable/FocusableTvOS"; import { FocusableCell } from "@applicaster/zapp-react-native-ui-components/Components/FocusableCell"; import { getItemType } from "@applicaster/zapp-react-native-utils/navigationUtils"; import { SCREEN_TYPES } from "@applicaster/zapp-react-native-utils/navigationUtils/itemTypes"; import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager"; import { sendSelectCellEvent } from "@applicaster/zapp-react-native-utils/analyticsUtils"; import { noop } from "@applicaster/zapp-react-native-utils/functionUtils"; import { toBooleanWithDefaultTrue } from "@applicaster/zapp-react-native-utils/booleanUtils"; import { CellWithFocusable } from "./CellWithFocusable"; import { FocusableWrapper } from "./FocusableWrapper"; import { focusableButtonsRegistration$ } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios"; import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils"; type Props = { item: ZappEntry; index: number; shouldScrollHorizontally: ( focusable: any, id: string, title: string ) => boolean | null | undefined; shouldScrollVertically: ( focusable: any, id: string, title: string ) => boolean | null | undefined; riverOffsetUpdater: (arg1: string, arg2: number) => number; offsetUpdater: (arg1: string, arg2: number) => number; componentId: string; component: { id: number | string; component_type: string; styles?: { component_margin_top?: number; component_padding_top?: number; }; }; selected: boolean; CellRenderer: React.FunctionComponent & { hasFocusableInside: (item: any) => boolean; }; preferredFocus: boolean; navigator: { push: (arg1: Record) => void; }; onFocus: (arg1?: any, index?: number) => void | null | undefined; onBlur: (arg1?: any, index?: number) => void | null | undefined; onPress: (arg1?: any, index?: number) => void | null | undefined; willReceiveFocus: (arg1?: any, index?: number) => void | null | undefined; hasReceivedFocus: (arg1?: any, index?: number) => void | null | undefined; hasHeader: boolean; isScreenWrappedInContainer: boolean | null | undefined; sendOnClickEvent: () => void; scrollToIndex: (index: number) => void; appData: { layoutVersion: "v1" | "v2"; }; getHeaderOffset: () => { headerOffset: number; headersAbove: number; }; componentAnchorPointY: number | null | undefined; screenLayout: any; headerOffset: number; headerOffsetOverride: number | null | undefined; focused: boolean; groupId: string; mainOffsetUpdater: (...args: any[]) => void | null | undefined; isFocusable: boolean; shouldUpdate: boolean; behavior: Behavior; componentsMapOffset: number; applyFocusableWrapper: boolean; hasFocusableInside: boolean; }; type State = { focusedId: string | null; }; const styles = StyleSheet.create({ container: { flex: 1 }, }); const baseCellStyles = { height: "100%", display: "flex", flex: 1, } as const; class TvOSCell extends React.Component { cell: any; target: any; layout: any; constructor(props) { super(props); this.onPress = this.onPress.bind(this); this.onFocus = this.onFocus.bind(this); this.onBlur = this.onBlur.bind(this); this.willReceiveFocus = this.willReceiveFocus.bind(this); this.hasReceivedFocus = this.hasReceivedFocus.bind(this); this.scrollTo = this.scrollTo.bind(this); this.state = { focusedId: null }; this.cell = null; this.setInitialFocus = this.setInitialFocus.bind(this); this.setFocusId = this.setFocusId.bind(this); this.unsetFocusId = this.unsetFocusId.bind(this); this.onLayout = this.onLayout.bind(this); } onLayout({ nativeEvent }) { const { target, layout } = nativeEvent; this.target = target; this.layout = layout; } getFocusedId() { return `${this.props?.component?.id}-${this.props?.item?.id}`; } setInitialFocus() { this.setFocusId(this.getFocusedId()); } setFocusId(id) { this.setState((prevState) => { if (prevState.focusedId === id) { return prevState; } return { focusedId: id }; }); } unsetFocusId() { const itemInFocus = focusManager.getCurrentFocus(); itemInFocus.onBlur(); this.setFocusId(null); } setScreenLayout(componentAnchorPointY, screenLayout) { if (componentAnchorPointY && screenLayout) { const updateLayout = componentAnchorPointY !== screenLayout?.componentAnchorPointY; updateLayout && screenLayout?.setScreenLayout({ componentAnchorPointY }); } } onPress() { const { onPress, item, index } = this.props; if (onPress) { onPress(item, index); } else { const { item, navigator, component, appData } = this.props; const itemTargetScreen = R.path(["screen_type"], item); const componentTargetScreen = R.path(["data", "target"], component); const itemType = getItemType(item, appData?.layoutVersion); sendSelectCellEvent(item, component, item.title, index); if ( itemType !== SCREEN_TYPES.PLAYABLE && componentTargetScreen && !itemTargetScreen ) { navigator.push(R.merge(item, { screen_type: componentTargetScreen })); return; } navigator.push(item); } } onFocus(focusable, _idx: number) { const { item: { id, title }, shouldScrollVertically, screenLayout = null, componentAnchorPointY = null, mainOffsetUpdater, getHeaderOffset, groupId, component, index, componentsMapOffset, } = this.props; this.setScreenLayout(componentAnchorPointY, screenLayout); this.setFocusId(id); if ( this.props.shouldUpdate && shouldScrollVertically?.(focusable, String(id), String(title)) ) { const { headerOffset } = getHeaderOffset(); const extraAnchorPointYOffset = toNumberWithDefaultZero( screenLayout?.extraAnchorPointYOffset ); const componentMarginTop = toNumberWithDefaultZero( component?.styles?.component_margin_top ); const componentPaddingTop = toNumberWithDefaultZero( component?.styles?.component_padding_top ); const totalOffset = headerOffset + toNumberWithDefaultZero(componentAnchorPointY) + extraAnchorPointYOffset - toNumberWithDefaultZero(componentsMapOffset) + componentMarginTop + componentPaddingTop; mainOffsetUpdater?.( { tag: this.target }, totalOffset, groupId, id, component.id, index ); } } onBlur() { this.setFocusId(null); } willReceiveFocus() {} hasReceivedFocus() {} scrollTo() {} render() { const { item, index, selected, component, offsetUpdater, preferredFocus, CellRenderer, onFocus, onBlur, willReceiveFocus, hasReceivedFocus, groupId, isFocusable, behavior, applyFocusableWrapper, hasFocusableInside, } = this.props; const { id } = item; const focusedId = this.state.focusedId; const selectedCell = focusedId || selected; const focusableId = R.join("-", [component?.id, id, index]); const handleFocus = (arg1: any, index?: number) => { const focusFn = onFocus || noop; focusFn(arg1, index); this.onFocus(arg1, index); }; if (hasFocusableInside) { return ( 1740} onFocus={handleFocus} onBlur={onBlur || this.onBlur} applyWrapper={applyFocusableWrapper} > {(focused) => ( )} ); } return ( 1740} onPress={this.onPress} willReceiveFocus={willReceiveFocus || this.willReceiveFocus} hasReceivedFocus={hasReceivedFocus || this.hasReceivedFocus} preferredFocus={preferredFocus} offsetUpdater={offsetUpdater} style={baseCellStyles} isFocusable={isFocusable} > {(focused) => ( )} ); } } export function withFocusableWrapperHOC(Component) { return function WrappedComponent(props) { const [focusableViewIsRendered, setFocusableViewIsRendered] = React.useState(false); const { CellRenderer, item, groupId, component } = props; const isFocusable = toBooleanWithDefaultTrue(props?.isFocusable); const focusableGroupId = String(groupId || component?.id); const hasFocusableInside = CellRenderer.hasFocusableInside?.(item); React.useEffect(() => { // start waiting any first registration of FocusableButton inside this focusableGroup // after it we could get rid of applying focusable-wrapper const subscription = focusableButtonsRegistration$(focusableGroupId) .pipe( filter(() => isFocusable), first() ) .subscribe(() => { setFocusableViewIsRendered(true); }); return () => { subscription.unsubscribe(); }; }, [isFocusable, focusableGroupId]); const applyFocusableWrapper = React.useMemo(() => { return isFocusable && hasFocusableInside && !focusableViewIsRendered; }, [isFocusable, hasFocusableInside, focusableViewIsRendered]); return ( ); }; } export const TvOSCellComponent = compose(withFocusableWrapperHOC)(TvOSCell);