UNPKG

@applicaster/zapp-react-native-ui-components

Version:

Applicaster Zapp React Native ui components for the Quick Brick App

370 lines (293 loc) • 9.49 kB
import React, { useEffect, useMemo } from "react"; import * as R from "ramda"; import validateColor from "validate-color"; import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils"; import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions"; import { masterCellLogger } from "../logger"; import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils"; import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer"; import { PushTopicManager } from "@applicaster/zapp-react-native-bridge/PushNotifications/PushTopicManager"; import { StorageMultiSelectProvider } from "@applicaster/zapp-react-native-bridge/ZappStorage/StorageMultiSelectProvider"; import { getCellState } from "../../Cell/utils"; import { getColorFromData } from "@applicaster/zapp-react-native-utils/cellUtils"; const hasElementSpecificViewType = (viewType) => (element) => { if (R.isNil(element)) { return false; } if (element.type === viewType) { return true; } // eslint-disable-next-line @typescript-eslint/no-use-before-define return hasElementsSpecificViewType(viewType)(element.elements); }; const logWarning = R.curry( (colorValueFromCellStyle, style, entry, colorValueFromEntry) => { if (R.isNil(colorValueFromEntry)) { masterCellLogger.warn({ message: `Cannot resolve property ${colorValueFromCellStyle} from the entry.`, data: { configurationValue: colorValueFromCellStyle, entry, }, }); } return style; } ); export const hasElementsSpecificViewType = (viewType) => (elements) => { if (R.isNil(elements) || R.isEmpty(elements)) { return false; } return R.any(hasElementSpecificViewType(viewType))(elements); }; function resolveColorForProp(entry, style, colorProp) { const colorFromProp = style[colorProp]; const nestedEntryValue = R.path(colorFromProp.split("."), entry); const color = colorFromProp.replace(".00", "").replace(".0", ""); // https://github.com/dreamyguy/validate-color/issues/44 if (nestedEntryValue === undefined && !validateColor(color)) { logWarning(colorFromProp, style, entry, nestedEntryValue); } const colorValue = getColorFromData({ data: entry, valueFromLayout: colorFromProp, }); if (!colorValue) { logWarning(colorProp, style, entry, colorValue); return style; } return { ...style, [colorProp]: colorValue }; } export function resolveColor(entry, style) { if (style === null || style === undefined) { return style; } // TODO can be optimized to remove 3 O(n) loops const styleKeys = Object.keys(style); const colorKeys = styleKeys.filter((key) => /color/i.test(key)); return colorKeys.reduce((acc, value) => { return { ...style, ...resolveColorForProp(entry, acc, value) }; }, style); } export function isVideoPreviewEnabled({ enable_video_preview = false, player_screen_id = null, }: { enable_video_preview: boolean; player_screen_id: string | null; }) { return enable_video_preview && !R.isEmpty(player_screen_id); } export const useFilterChildren = < T extends { props: { item: any; pluginIdentifier: string; }; }, >( children: T[] ): T[] => { const actions = useActions(""); const filteredChildren = children.filter((child) => { const item = child.props.item; const actionIdentifier = child.props.pluginIdentifier; const action = actions.actions[actionIdentifier]; // context value of specific plugin if (action?.module && action.module.context) { const currentValue = action.module.context._currentValue; return currentValue?.isActionAvailable ? currentValue.isActionAvailable(item) : true; } masterCellLogger.error({ message: `Action plugin for ${actionIdentifier} could not be found, check the configuration of your action button`, data: { item, action: child.props.pluginIdentifier }, }); return false; }); return filteredChildren; }; export const insertBetween = (separator, items) => { return items.reduce((acc, item, index) => { if (index + 1 >= items.length) { // last element acc.push(item); return acc; } acc.push(item); acc.push(separator(index)); return acc; }, []); }; const recursiveCloneElement = (focused: boolean) => (element) => { const { children, ...otherProps } = element.props; const state = focused ? "focused" : "default"; return React.cloneElement(element, { ...otherProps, state, // eslint-disable-next-line @typescript-eslint/no-use-before-define children: recursiveCloneElementsWithState(focused, children), }); }; export const recursiveCloneElementsWithState = (focused: boolean, children) => { if (R.isNil(children) || R.isEmpty(children)) { return undefined; } return React.Children.map(children, recursiveCloneElement(focused)); }; const next = (currentIndex, items) => items[currentIndex + 1]; const previous = (currentIndex, items) => items[currentIndex - 1]; export const cloneElementsWithIds = (ids, children) => { if (R.isNil(children) || R.isEmpty(children)) { return undefined; } return React.Children.map(children, (element, index) => React.cloneElement(element, { nextFocusLeft: previous(index, ids), nextFocusRight: next(index, ids), }) ); }; export const getFocusedButtonId = (focusable) => { return platformSelect({ tvos: R.path(["itemID"], focusable), default: R.path(["props", "id"], focusable), }); }; export const isSelected = (id: string | number, behavior?: Behavior) => { if (!behavior) { return false; } if (behavior?.selection_source === "now_playing") { const player = playerManager.getActivePlayer(); if (player?.entry?.id === id) { return true; } } if (behavior?.select_mode === "single") { return behavior.current_selection === id; } if (behavior?.select_mode === "multi") { // TODO: Use generic resolver source if (behavior.selection_source === "@{push/topics}") { const tags = PushTopicManager.getInstance().getRegisteredTags(); return tags.includes(String(id)); } if (Array.isArray(behavior.current_selection)) { return behavior.current_selection.includes(id); } const currentSelection = String(behavior.current_selection); if (currentSelection?.startsWith("@{ctx/")) { const keyWithoutCtx = currentSelection.substring( "@{ctx/".length, currentSelection.length - 1 ); const selectedItems = StorageMultiSelectProvider.getProvider( keyWithoutCtx )?.getSelectedItems(); return selectedItems?.includes(String(id)); } } return false; }; export const useBehaviorUpdate = (behavior: Behavior) => { const [lastUpdate, setLastUpdate] = React.useState(null); const player = usePlayer(); const triggerUpdate = () => { setLastUpdate(Date.now()); }; // TODO: Create generic RX to state update useEffect(() => { // TODO: Use generic resolver source if (!behavior) { return; } const currentSelection = String(behavior.current_selection); if (currentSelection?.startsWith("@{ctx/")) { const keyWithoutCtx = currentSelection.substring( "@{ctx/".length, currentSelection.length - 1 ); if (keyWithoutCtx) { const subscription = StorageMultiSelectProvider.getProvider( keyWithoutCtx ) .getObservable() .subscribe(() => { triggerUpdate(); }); return () => { subscription.unsubscribe(); }; } } }, [behavior]); useEffect(() => { if (!behavior) { return; } if (behavior?.selection_source === "@{push/topics}") { const subscription = PushTopicManager.getInstance() .getEntryObservable() .subscribe(() => { triggerUpdate(); }); return () => { subscription.unsubscribe(); }; } }, [behavior]); useEffect(() => { if (!behavior) { return; } if (behavior?.selection_source === "now_playing" && player) { const subscription = player.getEntryObservable().subscribe(() => { triggerUpdate(); }); return () => { subscription.unsubscribe(); }; } }, [behavior, player]); return lastUpdate; }; export const useCellState = ({ id, behavior, focused, }: { id: string | number; behavior: Behavior; focused: boolean; }): CellState => { const lastUpdate = useBehaviorUpdate(behavior); const _isSelected = useMemo( () => isSelected(id, behavior), [behavior, id, lastUpdate] ); return getCellState({ focused, selected: _isSelected }); }; export const hasFocusableInsideBuilder = (elementsBuilder) => (item) => { const elements = elementsBuilder({ entry: item }); return R.anyPass([ hasElementsSpecificViewType("ButtonContainerView"), hasElementsSpecificViewType("FocusableView"), ])(elements); }; export function getEntryState(state, selected) { if (state === "focused_selected") { return state; } if (state === "focused" && selected) { return "focused_selected"; } if (state === "focused" && !selected) { return "focused"; } if (state === "selected") { return "selected"; } return selected ? "selected" : "default"; }