331 lines
9.1 KiB
TypeScript
331 lines
9.1 KiB
TypeScript
// Copyright 2019-2021 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
/* eslint-disable no-param-reassign */
|
|
|
|
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, find, isNumber, pull, remove, take, uniq } from 'lodash';
|
|
import { SortEnd } from 'react-sortable-hoc';
|
|
import { bindActionCreators } from 'redux';
|
|
import arrayMove from 'array-move';
|
|
// eslint-disable-next-line import/no-cycle
|
|
import { AppState } from '../reducer';
|
|
import {
|
|
PackMetaData,
|
|
StickerImageData,
|
|
StickerData,
|
|
} 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 addImageData = createAction<StickerImageData>(
|
|
'stickers/addSticker'
|
|
);
|
|
export const removeSticker = createAction<string>('stickers/removeSticker');
|
|
export const moveSticker = createAction<SortEnd>('stickers/moveSticker');
|
|
export const setCover = createAction<StickerImageData>('stickers/setCover');
|
|
export const resetCover = createAction<StickerImageData>('stickers/resetCover');
|
|
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 addToast = createAction<{
|
|
key: string;
|
|
subs?: Array<number | string>;
|
|
}>('stickers/addToast');
|
|
export const dismissToast = createAction<void>('stickers/dismissToast');
|
|
|
|
export const resetStatus = createAction<void>('stickers/resetStatus');
|
|
export const reset = createAction<void>('stickers/reset');
|
|
|
|
export const minStickers = 1;
|
|
export const maxStickers = 200;
|
|
export const maxByteSize = 300 * 1024;
|
|
|
|
type StateStickerData = {
|
|
readonly imageData?: StickerImageData;
|
|
readonly emoji?: EmojiPickDataType;
|
|
};
|
|
|
|
type StateToastData = {
|
|
key: string;
|
|
subs?: Array<number | string>;
|
|
};
|
|
|
|
export type State = {
|
|
readonly order: Array<string>;
|
|
readonly cover?: StickerImageData;
|
|
readonly title: string;
|
|
readonly author: string;
|
|
readonly packId: string;
|
|
readonly packKey: string;
|
|
readonly toasts: Array<StateToastData>;
|
|
readonly data: {
|
|
readonly [src: string]: StateStickerData;
|
|
};
|
|
};
|
|
|
|
export type Actions = {
|
|
addImageData: typeof addImageData;
|
|
initializeStickers: typeof initializeStickers;
|
|
removeSticker: typeof removeSticker;
|
|
moveSticker: typeof moveSticker;
|
|
setCover: typeof setCover;
|
|
setEmoji: typeof setEmoji;
|
|
setTitle: typeof setTitle;
|
|
setAuthor: typeof setAuthor;
|
|
setPackMeta: typeof setPackMeta;
|
|
addToast: typeof addToast;
|
|
dismissToast: typeof dismissToast;
|
|
reset: typeof reset;
|
|
resetStatus: typeof resetStatus;
|
|
};
|
|
|
|
const defaultState: State = {
|
|
order: [],
|
|
data: {},
|
|
title: '',
|
|
author: '',
|
|
packId: '',
|
|
packKey: '',
|
|
toasts: [],
|
|
};
|
|
|
|
const adjustCover = (state: Draft<State>) => {
|
|
const first = state.order[0];
|
|
|
|
if (first) {
|
|
state.cover = state.data[first].imageData;
|
|
} 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(addImageData, (state, { payload }) => {
|
|
if (isNumber(payload.meta.pages)) {
|
|
state.toasts.push({ key: 'StickerCreator--Toasts--animated' });
|
|
pull(state.order, payload.path);
|
|
delete state.data[payload.path];
|
|
} else if (payload.buffer.byteLength > maxByteSize) {
|
|
state.toasts.push({ key: 'StickerCreator--Toasts--tooLarge' });
|
|
pull(state.order, payload.path);
|
|
delete state.data[payload.path];
|
|
} else {
|
|
const data = state.data[payload.path];
|
|
|
|
// If we are adding image data, proceed to update the state and add/update a toast
|
|
if (data && !data.imageData) {
|
|
data.imageData = payload;
|
|
|
|
const key = 'StickerCreator--Toasts--imagesAdded';
|
|
|
|
const toast = (() => {
|
|
const oldToast = find(state.toasts, { key });
|
|
|
|
if (oldToast) {
|
|
return oldToast;
|
|
}
|
|
|
|
const newToast = { key, subs: [0] };
|
|
state.toasts.push(newToast);
|
|
|
|
return newToast;
|
|
})();
|
|
|
|
if (toast.subs && isNumber(toast.subs[0])) {
|
|
toast.subs[0] = (toast.subs[0] || 0) + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
adjustCover(state);
|
|
}),
|
|
|
|
handleAction(removeSticker, (state, { payload }) => {
|
|
pull(state.order, payload);
|
|
delete state.data[payload];
|
|
adjustCover(state);
|
|
}),
|
|
|
|
handleAction(moveSticker, (state, { payload }) => {
|
|
arrayMove.mutate(state.order, payload.oldIndex, payload.newIndex);
|
|
}),
|
|
|
|
handleAction(setCover, (state, { payload }) => {
|
|
state.cover = payload;
|
|
}),
|
|
|
|
handleAction(resetCover, state => {
|
|
adjustCover(state);
|
|
}),
|
|
|
|
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(addToast, (state, { payload: toast }) => {
|
|
remove(state.toasts, { key: toast.key });
|
|
state.toasts.push(toast);
|
|
}),
|
|
|
|
handleAction(dismissToast, state => {
|
|
state.toasts.pop();
|
|
}),
|
|
|
|
handleAction(resetStatus, state => {
|
|
state.toasts = [];
|
|
}),
|
|
|
|
handleAction(reset, () => defaultState),
|
|
],
|
|
defaultState
|
|
);
|
|
|
|
export const useTitle = (): string =>
|
|
useSelector(({ stickers }: AppState) => stickers.title);
|
|
|
|
export const useAuthor = (): string =>
|
|
useSelector(({ stickers }: AppState) => stickers.author);
|
|
|
|
export const useCover = (): StickerImageData | undefined =>
|
|
useSelector(({ stickers }: AppState) => stickers.cover);
|
|
|
|
export const useStickerOrder = (): Array<string> =>
|
|
useSelector(({ stickers }: AppState) => stickers.order);
|
|
|
|
export const useStickerData = (src: string): StateStickerData =>
|
|
useSelector(({ stickers }: AppState) => stickers.data[src]);
|
|
|
|
export const useStickersReady = (): boolean =>
|
|
useSelector(
|
|
({ stickers }: AppState) =>
|
|
stickers.order.length >= minStickers &&
|
|
stickers.order.length <= maxStickers &&
|
|
Object.values(stickers.data).every(({ imageData }) => Boolean(imageData))
|
|
);
|
|
|
|
export const useEmojisReady = (): boolean =>
|
|
useSelector(({ stickers }: AppState) =>
|
|
Object.values(stickers.data).every(({ emoji }) => !!emoji)
|
|
);
|
|
|
|
export const useAllDataValid = (): boolean => {
|
|
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 = (): string => useSelector(selectUrl);
|
|
|
|
export const useToasts = (): Array<StateToastData> =>
|
|
useSelector(({ stickers }: AppState) => stickers.toasts);
|
|
|
|
export const useAddMoreCount = (): number =>
|
|
useSelector(({ stickers }: AppState) =>
|
|
clamp(minStickers - stickers.order.length, 0, minStickers)
|
|
);
|
|
|
|
const selectOrderedData = createSelector(
|
|
({ stickers }: AppState) => stickers.order,
|
|
({ stickers }) => stickers.data,
|
|
(order, data) =>
|
|
order.map(id => ({
|
|
...data[id],
|
|
emoji: convertShortName(
|
|
(data[id].emoji as EmojiPickDataType).shortName,
|
|
(data[id].emoji as EmojiPickDataType).skinTone
|
|
),
|
|
}))
|
|
);
|
|
|
|
export const useSelectOrderedData = (): Array<StickerData> =>
|
|
useSelector(selectOrderedData);
|
|
|
|
const selectOrderedImagePaths = createSelector(selectOrderedData, data =>
|
|
data.map(({ imageData }) => imageData.src)
|
|
);
|
|
|
|
export const useOrderedImagePaths = (): Array<string> =>
|
|
useSelector(selectOrderedImagePaths);
|
|
|
|
export const useStickerActions = (): Actions => {
|
|
const dispatch = useDispatch();
|
|
|
|
return useMemo(
|
|
() =>
|
|
bindActionCreators(
|
|
{
|
|
addImageData,
|
|
initializeStickers,
|
|
removeSticker,
|
|
moveSticker,
|
|
setCover,
|
|
setEmoji,
|
|
setTitle,
|
|
setAuthor,
|
|
setPackMeta,
|
|
addToast,
|
|
dismissToast,
|
|
reset,
|
|
resetStatus,
|
|
},
|
|
dispatch
|
|
),
|
|
[dispatch]
|
|
);
|
|
};
|