UNPKG

@applicaster/quick-brick-core

Version:

Core package for Applicaster's Quick Brick App

468 lines (407 loc) • 11.3 kB
import * as R from "ramda"; import { v4 } from "uuid"; 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, }, location: { key: "", pathname: "/", state: 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", SET_LOCATION = "SET_LOCATION", 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, locationState: LocationReducerState) { const locationKey = locationState.key; const stackIndex = R.findIndex(R.propEq("key", locationKey))(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, meta }: ReducerAction = { type: "" } ) { switch (type) { case ACTIONS.SET_NESTED_CONTENT: return { ...state, mainStack: addNestedContent( state.mainStack, navigationEntry(ACTIONS.REPLACE, payload as AnyRecord), meta as LocationReducerState ), }; 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; } } function locationReducer( state = initialState.location, { type, payload, meta = {} } ) { const { route, key, screen, entry, options } = payload; switch (type) { case ACTIONS.BACK: // eslint-disable-next-line no-case-declarations const previousEntry = R.compose( R.last, payload.route ? firstStackEntriesSelector : previousStackEntriesSelector )(meta); if (previousEntry) { const { state, key, action } = previousEntry; return { pathname: previousEntry.route, state, key, action }; } return state; case ACTIONS.PUSH: case ACTIONS.REPLACE: case ACTIONS.REPLACE_TOP: case ACTIONS.BACK_PLAYER_NESTED_CONTENT: return { pathname: route, state: { screen, entry, options }, key }; case ACTIONS.SET_NESTED_CONTENT: // eslint-disable-next-line no-case-declarations const nestedEntry = navigationEntry( ACTIONS.REPLACE, payload as AnyRecord ); return R.assocPath(["state", "nested"], nestedEntry.state, state); 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 = R.pathOr(MAXIMIZED, ["mode"], options); return R.compose( R.set(R.lensProp("mode"), mode), R.set(R.lensProp("visible"), true), R.over( R.lensProp("item"), R.unless(R.tryCatch(R.propEq("id", item?.id), R.F), () => item) ) )(state); } case ACTIONS.SET_VIDEO_MODAL_ITEM: return R.set(R.lensProp("item"), payload)(state); case ACTIONS.CLOSE_VIDEO_MODAL: return initialState.options.videoModal; case ACTIONS.FULLSCREEN_VIDEO_MODAL: return R.compose( R.set(R.lensProp("mode"), FULLSCREEN), R.set(R.lensProp("previousMode"), state.mode) )(state); case ACTIONS.MINIMISE_VIDEO_MODAL: return R.compose( R.set(R.lensProp("mode"), MINIMIZED), R.set(R.lensProp("previousMode"), state.mode) )(state); case ACTIONS.MAXIMISE_VIDEO_MODAL: return R.compose( R.set(R.lensProp("mode"), MAXIMIZED), R.set(R.lensProp("previousMode"), state.mode) )(state); case ACTIONS.PIP: return R.compose( R.set(R.lensProp("mode"), PIP), R.set(R.lensProp("previousMode"), state.mode) )(state); } }; 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 R.evolve( { stack: applyReducer(navigationStackReducer, { type, payload }), location: applyReducer(locationReducer, { type, payload, meta: state, }), }, state ); case ACTIONS.SET_NESTED_CONTENT: return R.evolve( { stack: applyReducer(navigationStackReducer, { type, payload, meta: state.location, }), location: applyReducer(locationReducer, { type, payload, meta: state.stack, }), }, state ); 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; } }