@applicaster/quick-brick-core
Version:
Core package for Applicaster's Quick Brick App
397 lines (344 loc) • 9.37 kB
text/typescript
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;
}
}