250 lines
7.3 KiB
TypeScript
250 lines
7.3 KiB
TypeScript
// tslint:disable no-dynamic-delete
|
|
|
|
import { useMemo } from 'react';
|
|
import {
|
|
createAction,
|
|
Draft,
|
|
handleAction,
|
|
reduceReducers,
|
|
} from 'redux-ts-utils';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { createSelector } from 'reselect';
|
|
import { clamp, pull, take, uniq } from 'lodash';
|
|
import { SortEnd } from 'react-sortable-hoc';
|
|
import arrayMove from 'array-move';
|
|
import { AppState } from '../reducer';
|
|
import { PackMetaData, WebpData } from '../../util/preload';
|
|
import { EmojiPickDataType } from '../../../ts/components/emoji/EmojiPicker';
|
|
import { convertShortName } from '../../../ts/components/emoji/lib';
|
|
|
|
export const initializeStickers = createAction<Array<string>>(
|
|
'stickers/initializeStickers'
|
|
);
|
|
export const addWebp = createAction<WebpData>('stickers/addSticker');
|
|
export const removeSticker = createAction<string>('stickers/removeSticker');
|
|
export const moveSticker = createAction<SortEnd>('stickers/moveSticker');
|
|
export const setCover = createAction<WebpData>('stickers/setCover');
|
|
export const setEmoji = createAction<{ id: string; emoji: EmojiPickDataType }>(
|
|
'stickers/setEmoji'
|
|
);
|
|
export const setTitle = createAction<string>('stickers/setTitle');
|
|
export const setAuthor = createAction<string>('stickers/setAuthor');
|
|
export const setPackMeta = createAction<PackMetaData>('stickers/setPackMeta');
|
|
export const resetStatus = createAction<void>('stickers/resetStatus');
|
|
export const reset = createAction<void>('stickers/reset');
|
|
|
|
export const minStickers = 4;
|
|
export const maxStickers = 40;
|
|
export const maxByteSize = 100 * 1024;
|
|
|
|
export type State = {
|
|
readonly order: Array<string>;
|
|
readonly cover?: WebpData;
|
|
readonly title: string;
|
|
readonly author: string;
|
|
readonly packId: string;
|
|
readonly packKey: string;
|
|
readonly tooLarge: number;
|
|
readonly imagesAdded: number;
|
|
readonly data: {
|
|
readonly [src: string]: {
|
|
readonly webp?: WebpData;
|
|
readonly emoji?: EmojiPickDataType;
|
|
};
|
|
};
|
|
};
|
|
|
|
const defaultState: State = {
|
|
order: [],
|
|
data: {},
|
|
title: '',
|
|
author: '',
|
|
packId: '',
|
|
packKey: '',
|
|
tooLarge: 0,
|
|
imagesAdded: 0,
|
|
};
|
|
|
|
const adjustCover = (state: Draft<State>) => {
|
|
const first = state.order[0];
|
|
|
|
if (first) {
|
|
state.cover = state.data[first].webp;
|
|
} else {
|
|
delete state.cover;
|
|
}
|
|
};
|
|
|
|
export const reducer = reduceReducers<State>(
|
|
[
|
|
handleAction(initializeStickers, (state, { payload }) => {
|
|
const truncated = take(
|
|
uniq([...state.order, ...payload]),
|
|
maxStickers - state.order.length
|
|
);
|
|
truncated.forEach(path => {
|
|
if (!state.data[path]) {
|
|
state.data[path] = {};
|
|
state.order.push(path);
|
|
}
|
|
});
|
|
}),
|
|
|
|
handleAction(addWebp, (state, { payload }) => {
|
|
if (payload.buffer.byteLength > maxByteSize) {
|
|
state.tooLarge = clamp(state.tooLarge + 1, 0, state.order.length);
|
|
pull(state.order, payload.path);
|
|
delete state.data[payload.path];
|
|
} else {
|
|
const data = state.data[payload.path];
|
|
|
|
if (data) {
|
|
data.webp = payload;
|
|
state.imagesAdded = clamp(
|
|
state.imagesAdded + 1,
|
|
0,
|
|
state.order.length
|
|
);
|
|
}
|
|
}
|
|
|
|
adjustCover(state);
|
|
}),
|
|
|
|
handleAction(removeSticker, (state, { payload }) => {
|
|
pull(state.order, payload);
|
|
delete state.data[payload];
|
|
adjustCover(state);
|
|
state.imagesAdded = clamp(state.imagesAdded - 1, 0, state.order.length);
|
|
}),
|
|
|
|
handleAction(moveSticker, (state, { payload }) => {
|
|
arrayMove.mutate(state.order, payload.oldIndex, payload.newIndex);
|
|
}),
|
|
|
|
handleAction(setCover, (state, { payload }) => {
|
|
state.cover = payload;
|
|
}),
|
|
|
|
handleAction(setEmoji, (state, { payload }) => {
|
|
const data = state.data[payload.id];
|
|
if (data) {
|
|
data.emoji = payload.emoji;
|
|
}
|
|
}),
|
|
|
|
handleAction(setTitle, (state, { payload }) => {
|
|
state.title = payload;
|
|
}),
|
|
|
|
handleAction(setAuthor, (state, { payload }) => {
|
|
state.author = payload;
|
|
}),
|
|
|
|
handleAction(setPackMeta, (state, { payload: { packId, key } }) => {
|
|
state.packId = packId;
|
|
state.packKey = key;
|
|
}),
|
|
|
|
handleAction(resetStatus, state => {
|
|
state.tooLarge = 0;
|
|
state.imagesAdded = 0;
|
|
}),
|
|
|
|
handleAction(reset, () => defaultState),
|
|
],
|
|
defaultState
|
|
);
|
|
|
|
export const useTitle = () =>
|
|
useSelector(({ stickers }: AppState) => stickers.title);
|
|
export const useAuthor = () =>
|
|
useSelector(({ stickers }: AppState) => stickers.author);
|
|
|
|
export const useCover = () =>
|
|
useSelector(({ stickers }: AppState) => stickers.cover);
|
|
|
|
export const useStickerOrder = () =>
|
|
useSelector(({ stickers }: AppState) => stickers.order);
|
|
|
|
export const useStickerData = (src: string) =>
|
|
useSelector(({ stickers }: AppState) => stickers.data[src]);
|
|
|
|
export const useStickersReady = () =>
|
|
useSelector(
|
|
({ stickers }: AppState) =>
|
|
stickers.order.length >= minStickers &&
|
|
stickers.order.length <= maxStickers &&
|
|
Object.values(stickers.data).every(({ webp }) => !!webp)
|
|
);
|
|
|
|
export const useEmojisReady = () =>
|
|
useSelector(({ stickers }: AppState) =>
|
|
Object.values(stickers.data).every(({ emoji }) => !!emoji)
|
|
);
|
|
|
|
export const useAllDataValid = () => {
|
|
const stickersReady = useStickersReady();
|
|
const emojisReady = useEmojisReady();
|
|
const cover = useCover();
|
|
const title = useTitle();
|
|
const author = useAuthor();
|
|
|
|
return !!(stickersReady && emojisReady && cover && title && author);
|
|
};
|
|
|
|
const selectUrl = createSelector(
|
|
({ stickers }: AppState) => stickers.packId,
|
|
({ stickers }: AppState) => stickers.packKey,
|
|
(id, key) => `https://signal.art/addstickers/#pack_id=${id}&pack_key=${key}`
|
|
);
|
|
|
|
export const usePackUrl = () => useSelector(selectUrl);
|
|
export const useHasTooLarge = () =>
|
|
useSelector(({ stickers }: AppState) => stickers.tooLarge > 0);
|
|
export const useImageAddedCount = () =>
|
|
useSelector(({ stickers }: AppState) => stickers.imagesAdded);
|
|
|
|
const selectOrderedData = createSelector(
|
|
({ stickers }: AppState) => stickers.order,
|
|
({ stickers }) => stickers.data,
|
|
(order, data) =>
|
|
order.map(id => ({
|
|
...data[id],
|
|
emoji: convertShortName(
|
|
data[id].emoji.shortName,
|
|
data[id].emoji.skinTone
|
|
),
|
|
}))
|
|
);
|
|
|
|
export const useSelectOrderedData = () => useSelector(selectOrderedData);
|
|
|
|
const selectOrderedImagePaths = createSelector(selectOrderedData, data =>
|
|
data.map(({ webp }) => webp.src)
|
|
);
|
|
|
|
export const useOrderedImagePaths = () => useSelector(selectOrderedImagePaths);
|
|
|
|
export const useStickerActions = () => {
|
|
const dispatch = useDispatch();
|
|
|
|
return useMemo(
|
|
() => ({
|
|
addWebp: (data: WebpData) => dispatch(addWebp(data)),
|
|
initializeStickers: (paths: Array<string>) =>
|
|
dispatch(initializeStickers(paths)),
|
|
removeSticker: (src: string) => dispatch(removeSticker(src)),
|
|
moveSticker: (sortEnd: SortEnd) => dispatch(moveSticker(sortEnd)),
|
|
setCover: (webp: WebpData) => dispatch(setCover(webp)),
|
|
setEmoji: (p: { id: string; emoji: EmojiPickDataType }) =>
|
|
dispatch(setEmoji(p)),
|
|
setTitle: (title: string) => dispatch(setTitle(title)),
|
|
setAuthor: (author: string) => dispatch(setAuthor(author)),
|
|
setPackMeta: (e: PackMetaData) => dispatch(setPackMeta(e)),
|
|
reset: () => dispatch(reset()),
|
|
resetStatus: () => dispatch(resetStatus()),
|
|
}),
|
|
[dispatch]
|
|
);
|
|
};
|