UNPKG

@applicaster/quick-brick-core

Version:

Core package for Applicaster's Quick Brick App

397 lines (344 loc) • 9.37 kB
import * as R from "ramda"; import { v4 } from "uuid"; import { last } from "@applicaster/zapp-react-native-utils/utils"; import { firstStackEntriesSelector, previousStackEntriesSelector, } from "./selectors"; /* initial state */ const MAXIMIZED = "MAXIMIZED"; const MINIMIZED = "MINIMIZED"; const FULLSCREEN = "FULLSCREEN"; const PIP = "PIP"; export const initialState: NavigationReducerState = { stack: { mainStack: [], modal: null, }, options: { videoModal: { visible: false, mode: MAXIMIZED, // VideoModalMode => MAXIMIZED | MINIMIZED | FULLSCREEN previousMode: undefined, item: null, }, }, }; /* Actions */ export enum ACTIONS { PUSH = "PUSH", REPLACE = "REPLACE", BACK = "POP", BACK_PLAYER_NESTED_CONTENT = "BACK_PLAYER_NESTED_CONTENT", REPLACE_TOP = "REPLACE_TOP", SET_NESTED_CONTENT = "SET_NESTED_CONTENT", OPEN_VIDEO_MODAL = "OPEN_VIDEO_MODAL", CLOSE_VIDEO_MODAL = "CLOSE_VIDEO_MODAL", MINIMISE_VIDEO_MODAL = "MINIMISE_VIDEO_MODAL", MAXIMISE_VIDEO_MODAL = "MAXIMISE_VIDEO_MODAL", FULLSCREEN_VIDEO_MODAL = "FULLSCREEN_VIDEO_MODAL", SET_VIDEO_MODAL_ITEM = "SET_VIDEO_MODAL_ITEM", PIP = "PIP", } type ActionType = typeof ACTIONS; type ActionKeys = keyof ActionType; type ActionValues = ActionType[ActionKeys]; /* Navigation Reducer */ const navigationAction = ({ type, route, state = {} }) => ({ type, payload: { route, ...state, key: v4() }, }); export const push = (route, state) => navigationAction({ type: ACTIONS.PUSH, route, state, }); export const replace = (route, state) => navigationAction({ type: ACTIONS.REPLACE, route, state, }); export const back = ({ route = "", state, }: Partial<NavigationScreenState> = {}) => navigationAction({ type: ACTIONS.BACK, route, state, }); export const backAndReplacePlayer = ({ route = "", state, }: Partial<NavigationScreenState> = {}) => navigationAction({ type: ACTIONS.BACK_PLAYER_NESTED_CONTENT, route, state, }); export const setReplaceTop = (route, state) => navigationAction({ type: ACTIONS.REPLACE_TOP, route, state, }); export const setBackPlayerNestedContent = (route, state) => navigationAction({ type: ACTIONS.BACK_PLAYER_NESTED_CONTENT, route, state, }); export const setNestedContent = ( pathname: string, entry: ZappEntry, screen: ZappRiver ) => navigationAction({ type: ACTIONS.SET_NESTED_CONTENT, route: `${pathname}/river/${screen.id}`, state: { entry, screen }, }); const navigationEntry = ( type: ActionValues, { route, screen, entry, key, options }: AnyRecord ) => ({ key, action: type, route, state: { screen, entry, options }, }); function updatePopActions(state) { return R.map( R.when( R.propEq("action", ACTIONS.BACK), R.assoc("action", ACTIONS.PUSH) || R.assoc("action", ACTIONS.REPLACE_TOP) ) )(state); } function removeTop(state, payload) { let playNextState = [...state.mainStack]; const preloadHooks = payload.entry?.targetScreen?.hooks?.preload_plugins || []; const postloadHooks = payload.entry?.targetScreen?.hooks?.postload_plugins || []; const hooksScreenIds = new Set([ ...preloadHooks.map((hook) => hook.screen_id), ...postloadHooks.map((hook) => hook.screen_id), ]); playNextState = playNextState.filter( (item) => !hooksScreenIds.has(item.state.screen.id) ); playNextState.pop(); return playNextState; } function backPlayerNestedContent(state) { const currentState = [...state.mainStack]; const findPlayableAndTruncate = (arr) => { const index = R.findIndex(R.pipe(R.prop("route"), R.includes("playable")))( arr ); return index !== -1 ? R.dropLast(arr.length - index - 1, arr) : [null, arr]; }; return findPlayableAndTruncate(currentState); } function addNestedContent(stack, payload, targetStackEntry) { const targetStackKey = targetStackEntry.key; const stackIndex = R.findIndex(R.propEq("key", targetStackKey))(stack); return R.adjust( stackIndex, R.compose( R.assocPath(["state", "nested"], payload.state), R.assoc("stack", payload) ), stack ); } function navigationStackReducer( state: NavigationStackReducerState = initialState.stack, { type = "", payload }: ReducerAction = { type: "" } ) { switch (type) { case ACTIONS.SET_NESTED_CONTENT: return { ...state, mainStack: addNestedContent( state.mainStack, navigationEntry(ACTIONS.REPLACE, payload as AnyRecord), last(state.mainStack) ), }; case ACTIONS.PUSH: return { ...state, mainStack: [ ...updatePopActions(state.mainStack), navigationEntry(ACTIONS.PUSH, payload as AnyRecord), ], }; case ACTIONS.REPLACE_TOP: return { ...state, mainStack: [ ...removeTop(state, payload), navigationEntry(ACTIONS.PUSH, payload as AnyRecord), ], }; case ACTIONS.REPLACE: return { ...state, mainStack: [navigationEntry(ACTIONS.REPLACE, payload as AnyRecord)], }; case ACTIONS.BACK: if (state.mainStack.length > 1) { return { ...state, mainStack: R.compose( R.adjust(-1, R.assoc("action", ACTIONS.BACK)), (payload as { route }).route ? firstStackEntriesSelector : previousStackEntriesSelector, R.assoc(["stack"], R.__, {}) )(state.mainStack), }; } return state; case ACTIONS.BACK_PLAYER_NESTED_CONTENT: return { ...state, mainStack: backPlayerNestedContent(state), }; case ACTIONS.OPEN_VIDEO_MODAL: return { ...state, modal: navigationEntry(ACTIONS.OPEN_VIDEO_MODAL, payload as AnyRecord), }; case ACTIONS.CLOSE_VIDEO_MODAL: return { ...state, modal: null, }; case ACTIONS.SET_VIDEO_MODAL_ITEM: return { ...state, modal: { ...state.modal, entry: payload as ZappEntry, }, }; default: return state; } } /* Video Modal reducer */ export const setVideoModalOpen = (item, options, screen) => ({ type: ACTIONS.OPEN_VIDEO_MODAL, payload: { item, options, screen, entry: item, route: "videoModal", key: v4(), }, }); export const setVideoModalClose = () => ({ type: ACTIONS.CLOSE_VIDEO_MODAL, }); export const setVideoModalFullscreen = () => ({ type: ACTIONS.FULLSCREEN_VIDEO_MODAL, }); export const setVideoPiP = () => ({ type: ACTIONS.PIP, }); export const setVideoModalMaximized = () => ({ type: ACTIONS.MAXIMISE_VIDEO_MODAL, }); export const setVideoModalMinimized = () => ({ type: ACTIONS.MINIMISE_VIDEO_MODAL, }); export const setVideoModalItem = (payload) => ({ type: ACTIONS.SET_VIDEO_MODAL_ITEM, payload, }); export const videoModalReducer = ( state = initialState.options.videoModal, { type, payload } ) => { switch (type) { case ACTIONS.OPEN_VIDEO_MODAL: { const { item, options } = payload; const mode = options?.mode || MAXIMIZED; return { ...state, visible: true, mode, item: state.item?.id !== item?.id ? item : state.item, }; } case ACTIONS.SET_VIDEO_MODAL_ITEM: return { ...state, item: payload }; case ACTIONS.CLOSE_VIDEO_MODAL: return initialState.options.videoModal; case ACTIONS.FULLSCREEN_VIDEO_MODAL: return { ...state, mode: FULLSCREEN, previousMode: state.mode }; case ACTIONS.MINIMISE_VIDEO_MODAL: return { ...state, mode: MINIMIZED, previousMode: state.mode }; case ACTIONS.MAXIMISE_VIDEO_MODAL: return { ...state, mode: MAXIMIZED, previousMode: state.mode }; case ACTIONS.PIP: return { ...state, mode: PIP, previousMode: state.mode }; } }; const applyReducer = (reducer, action) => (state) => reducer(state, action); export default function navigationReducer( state = initialState, { type, payload }: ReducerAction = { type: "" } ) { switch (type) { case ACTIONS.PUSH: case ACTIONS.REPLACE: case ACTIONS.REPLACE_TOP: case ACTIONS.BACK: case ACTIONS.BACK_PLAYER_NESTED_CONTENT: return { ...state, stack: navigationStackReducer(state.stack, { type, payload }), }; case ACTIONS.SET_NESTED_CONTENT: return { ...state, stack: navigationStackReducer(state.stack, { type, payload, }), }; case ACTIONS.OPEN_VIDEO_MODAL: case ACTIONS.CLOSE_VIDEO_MODAL: case ACTIONS.SET_VIDEO_MODAL_ITEM: return R.evolve( { options: { videoModal: applyReducer(videoModalReducer, { type, payload }), }, stack: applyReducer(navigationStackReducer, { type, payload }), }, state ); case ACTIONS.FULLSCREEN_VIDEO_MODAL: case ACTIONS.MAXIMISE_VIDEO_MODAL: case ACTIONS.MINIMISE_VIDEO_MODAL: case ACTIONS.PIP: return R.evolve( { options: { videoModal: applyReducer(videoModalReducer, { type, payload }), }, }, state ); default: return state; } }