Sticker Creator: New toaster implementation, better error handling
This commit is contained in:
parent
4b3d63c82e
commit
f7568810ea
|
@ -2353,12 +2353,27 @@
|
|||
"StickerCreator--Toasts--animated": {
|
||||
"message": "Animated stickers are not currently supported",
|
||||
"description":
|
||||
"Text for the toast when an image that is animated was dropped"
|
||||
"Text for the toast when an image that is animated was dropped on the sticker creator"
|
||||
},
|
||||
"StickerCreator--Toasts--tooLarge": {
|
||||
"message": "Dropped image is too large",
|
||||
"description":
|
||||
"Text for the toast when an image that is too large was dropped"
|
||||
"Text for the toast when an image that is too large was dropped on the sticker creator"
|
||||
},
|
||||
"StickerCreator--Toasts--errorProcessing": {
|
||||
"message": "Error processing image",
|
||||
"description":
|
||||
"Text for the toast when an image cannot be processed was dropped on the sticker creator"
|
||||
},
|
||||
"StickerCreator--Toasts--errorUploading": {
|
||||
"message": "Error uploading stickers: $message$",
|
||||
"description": "Text for the toast when a sticker pack cannot be uploaded",
|
||||
"placeholders": {
|
||||
"message": {
|
||||
"content": "$1",
|
||||
"example": "Not connected"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StickerCreator--Toasts--linkedCopied": {
|
||||
"message": "Link copied",
|
||||
|
|
|
@ -4,6 +4,7 @@ import { history } from '../../util/history';
|
|||
import { Button } from '../../elements/Button';
|
||||
import { useI18n } from '../../util/i18n';
|
||||
import { Text } from '../../elements/Typography';
|
||||
import { Toaster } from '../../components/Toaster';
|
||||
import { stickersDuck } from '../../store';
|
||||
|
||||
export type Props = {
|
||||
|
@ -59,6 +60,8 @@ export const AppStage = (props: Props) => {
|
|||
);
|
||||
|
||||
const addMoreCount = stickersDuck.useAddMoreCount();
|
||||
const toasts = stickersDuck.useToasts();
|
||||
const { dismissToast } = stickersDuck.useStickerActions();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -85,6 +88,14 @@ export const AppStage = (props: Props) => {
|
|||
</Button>
|
||||
) : null}
|
||||
</footer>
|
||||
<Toaster
|
||||
className={styles.toaster}
|
||||
loaf={toasts.map((slice, id) => ({
|
||||
id,
|
||||
text: i18n(slice.key, slice.subs),
|
||||
}))}
|
||||
onDismiss={dismissToast}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,68 +1,17 @@
|
|||
import * as React from 'react';
|
||||
import { AppStage } from './AppStage';
|
||||
import * as styles from './DropStage.scss';
|
||||
import * as appStyles from './AppStage.scss';
|
||||
import { H2, Text } from '../../elements/Typography';
|
||||
import { LabeledCheckbox } from '../../elements/LabeledCheckbox';
|
||||
import { Toast } from '../../elements/Toast';
|
||||
import { StickerGrid } from '../../components/StickerGrid';
|
||||
import { stickersDuck } from '../../store';
|
||||
import { useI18n } from '../../util/i18n';
|
||||
|
||||
const renderToaster = ({
|
||||
hasAnimated,
|
||||
hasTooLarge,
|
||||
numberAdded,
|
||||
resetStatus,
|
||||
i18n,
|
||||
}: {
|
||||
hasAnimated: boolean;
|
||||
hasTooLarge: boolean;
|
||||
numberAdded: number;
|
||||
resetStatus: () => unknown;
|
||||
i18n: ReturnType<typeof useI18n>;
|
||||
}) => {
|
||||
if (hasAnimated) {
|
||||
return (
|
||||
<div className={appStyles.toaster}>
|
||||
<Toast onClick={resetStatus}>
|
||||
{i18n('StickerCreator--Toasts--animated')}
|
||||
</Toast>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasTooLarge) {
|
||||
return (
|
||||
<div className={appStyles.toaster}>
|
||||
<Toast onClick={resetStatus}>
|
||||
{i18n('StickerCreator--Toasts--tooLarge')}
|
||||
</Toast>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (numberAdded > 0) {
|
||||
return (
|
||||
<div className={appStyles.toaster}>
|
||||
<Toast onClick={resetStatus}>
|
||||
{i18n('StickerCreator--Toasts--imagesAdded', [numberAdded])}
|
||||
</Toast>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const DropStage = () => {
|
||||
const i18n = useI18n();
|
||||
const stickerPaths = stickersDuck.useStickerOrder();
|
||||
const stickersReady = stickersDuck.useStickersReady();
|
||||
const haveStickers = stickerPaths.length > 0;
|
||||
const hasAnimated = stickersDuck.useHasAnimated();
|
||||
const hasTooLarge = stickersDuck.useHasTooLarge();
|
||||
const numberAdded = stickersDuck.useImageAddedCount();
|
||||
const [showGuide, setShowGuide] = React.useState<boolean>(true);
|
||||
const { resetStatus } = stickersDuck.useStickerActions();
|
||||
|
||||
|
@ -86,13 +35,6 @@ export const DropStage = () => {
|
|||
<div className={styles.main}>
|
||||
<StickerGrid mode="add" showGuide={showGuide} />
|
||||
</div>
|
||||
{renderToaster({
|
||||
hasAnimated,
|
||||
hasTooLarge,
|
||||
numberAdded,
|
||||
resetStatus,
|
||||
i18n,
|
||||
})}
|
||||
</AppStage>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Button } from '../../elements/Button';
|
|||
import { stickersDuck } from '../../store';
|
||||
import { encryptAndUpload } from '../../util/preload';
|
||||
import { useI18n } from '../../util/i18n';
|
||||
import { Toaster } from '../../components/Toaster';
|
||||
|
||||
const handleCancel = () => history.push('/add-meta');
|
||||
|
||||
|
@ -36,6 +37,9 @@ export const UploadStage = () => {
|
|||
actions.setPackMeta(packMeta);
|
||||
history.push('/share');
|
||||
} catch (e) {
|
||||
actions.addToast('StickerCreator--Toasts--errorUploading', [
|
||||
e.message,
|
||||
]);
|
||||
history.push('/add-meta');
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -51,8 +51,13 @@ const InnerGrid = SortableContainer(
|
|||
actions.initializeStickers(paths);
|
||||
paths.forEach(path => {
|
||||
queue.add(async () => {
|
||||
const webp = await convertToWebp(path);
|
||||
actions.addWebp(webp);
|
||||
try {
|
||||
const webp = await convertToWebp(path);
|
||||
actions.addWebp(webp);
|
||||
} catch (e) {
|
||||
actions.removeSticker(path);
|
||||
actions.addToast('StickerCreator--Toasts--errorProcessing');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import * as React from 'react';
|
||||
import { debounce, dropRight } from 'lodash';
|
||||
import { StoryRow } from '../elements/StoryRow';
|
||||
import { Toaster } from './Toaster';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { text as textKnob } from '@storybook/addon-knobs';
|
||||
|
||||
storiesOf('Sticker Creator/components', module).add('Toaster', () => {
|
||||
const inputText = textKnob('Slices', ['error 1', 'error 2'].join('|'));
|
||||
const initialState = React.useMemo(() => inputText.split('|'), [inputText]);
|
||||
const [state, setState] = React.useState(initialState);
|
||||
|
||||
const handleDismiss = React.useCallback(
|
||||
// Debounce is required here since auto-dismiss is asynchronously called
|
||||
// from multiple rendered instances (multiple themes)
|
||||
debounce(() => {
|
||||
setState(dropRight);
|
||||
}, 10),
|
||||
[setState]
|
||||
);
|
||||
|
||||
return (
|
||||
<StoryRow>
|
||||
<Toaster
|
||||
loaf={state.map((text, id) => ({ id, text }))}
|
||||
onDismiss={handleDismiss}
|
||||
/>
|
||||
</StoryRow>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
import * as React from 'react';
|
||||
import { last, noop } from 'lodash';
|
||||
import { Toast } from '../elements/Toast';
|
||||
|
||||
export type Props = React.HTMLProps<HTMLDivElement> & {
|
||||
loaf: Array<{ id: number; text: string }>;
|
||||
onDismiss: () => unknown;
|
||||
};
|
||||
|
||||
const DEFAULT_DISMISS = 1e4;
|
||||
|
||||
export const Toaster = React.memo(({ loaf, onDismiss, className }: Props) => {
|
||||
const slice = last(loaf);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (!slice) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
onDismiss();
|
||||
}, DEFAULT_DISMISS);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
},
|
||||
[slice, onDismiss]
|
||||
);
|
||||
|
||||
if (!slice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Toast key={slice.id} onClick={onDismiss} tabIndex={0}>
|
||||
{slice.text}
|
||||
</Toast>
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -11,4 +11,5 @@
|
|||
font-size: 14px;
|
||||
color: $color-gray-05;
|
||||
line-height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -4,13 +4,14 @@ import { Toast } from './Toast';
|
|||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { text } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
storiesOf('Sticker Creator/elements', module).add('Toast', () => {
|
||||
const child = text('text', 'foo bar');
|
||||
|
||||
return (
|
||||
<StoryRow>
|
||||
<Toast>{child}</Toast>
|
||||
<Toast onClick={action('click')}>{child}</Toast>
|
||||
</StoryRow>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import * as styles from './Toast.scss';
|
||||
|
||||
export type Props = React.HTMLProps<HTMLButtonElement> & {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const Toast = React.memo(({ children, ...rest }: Props) => (
|
||||
<button className={styles.base} {...rest}>
|
||||
export const Toast = React.memo(({ children, className, ...rest }: Props) => (
|
||||
<button className={classNames(styles.base, className)} {...rest}>
|
||||
{children}
|
||||
</button>
|
||||
));
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from 'redux-ts-utils';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clamp, isNumber, pull, take, uniq } from 'lodash';
|
||||
import { clamp, find, isNumber, pull, remove, take, uniq } from 'lodash';
|
||||
import { SortEnd } from 'react-sortable-hoc';
|
||||
import arrayMove from 'array-move';
|
||||
import { AppState } from '../reducer';
|
||||
|
@ -31,6 +31,13 @@ export const setEmoji = createAction<{ id: string; emoji: EmojiPickDataType }>(
|
|||
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');
|
||||
|
||||
|
@ -45,9 +52,7 @@ export type State = {
|
|||
readonly author: string;
|
||||
readonly packId: string;
|
||||
readonly packKey: string;
|
||||
readonly tooLarge: number;
|
||||
readonly animated: number;
|
||||
readonly imagesAdded: number;
|
||||
readonly toasts: Array<{ key: string; subs?: Array<number | string> }>;
|
||||
readonly data: {
|
||||
readonly [src: string]: {
|
||||
readonly webp?: WebpData;
|
||||
|
@ -63,9 +68,7 @@ const defaultState: State = {
|
|||
author: '',
|
||||
packId: '',
|
||||
packKey: '',
|
||||
tooLarge: 0,
|
||||
animated: 0,
|
||||
imagesAdded: 0,
|
||||
toasts: [],
|
||||
};
|
||||
|
||||
const adjustCover = (state: Draft<State>) => {
|
||||
|
@ -95,23 +98,38 @@ export const reducer = reduceReducers<State>(
|
|||
|
||||
handleAction(addWebp, (state, { payload }) => {
|
||||
if (isNumber(payload.meta.pages)) {
|
||||
state.animated = clamp(state.animated + 1, 0, state.order.length);
|
||||
state.toasts.push({ key: 'StickerCreator--Toasts--animated' });
|
||||
pull(state.order, payload.path);
|
||||
delete state.data[payload.path];
|
||||
} else if (payload.buffer.byteLength > maxByteSize) {
|
||||
state.tooLarge = clamp(state.tooLarge + 1, 0, state.order.length);
|
||||
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 (data) {
|
||||
// If we are adding webp data, proceed to update the state and add/update a toast
|
||||
if (data && !data.webp) {
|
||||
data.webp = payload;
|
||||
state.imagesAdded = clamp(
|
||||
state.imagesAdded + 1,
|
||||
0,
|
||||
state.order.length
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,7 +140,6 @@ export const reducer = reduceReducers<State>(
|
|||
pull(state.order, payload);
|
||||
delete state.data[payload];
|
||||
adjustCover(state);
|
||||
state.imagesAdded = clamp(state.imagesAdded - 1, 0, state.order.length);
|
||||
}),
|
||||
|
||||
handleAction(moveSticker, (state, { payload }) => {
|
||||
|
@ -157,10 +174,17 @@ export const reducer = reduceReducers<State>(
|
|||
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.tooLarge = 0;
|
||||
state.animated = 0;
|
||||
state.imagesAdded = 0;
|
||||
state.toasts = [];
|
||||
}),
|
||||
|
||||
handleAction(reset, () => defaultState),
|
||||
|
@ -212,12 +236,8 @@ const selectUrl = createSelector(
|
|||
);
|
||||
|
||||
export const usePackUrl = () => useSelector(selectUrl);
|
||||
export const useHasTooLarge = () =>
|
||||
useSelector(({ stickers }: AppState) => stickers.tooLarge > 0);
|
||||
export const useHasAnimated = () =>
|
||||
useSelector(({ stickers }: AppState) => stickers.animated > 0);
|
||||
export const useImageAddedCount = () =>
|
||||
useSelector(({ stickers }: AppState) => stickers.imagesAdded);
|
||||
export const useToasts = () =>
|
||||
useSelector(({ stickers }: AppState) => stickers.toasts);
|
||||
export const useAddMoreCount = () =>
|
||||
useSelector(({ stickers }: AppState) =>
|
||||
clamp(minStickers - stickers.order.length, 0, minStickers)
|
||||
|
@ -260,6 +280,9 @@ export const useStickerActions = () => {
|
|||
setTitle: (title: string) => dispatch(setTitle(title)),
|
||||
setAuthor: (author: string) => dispatch(setAuthor(author)),
|
||||
setPackMeta: (e: PackMetaData) => dispatch(setPackMeta(e)),
|
||||
addToast: (key: string, subs?: Array<number>) =>
|
||||
dispatch(addToast({ key, subs })),
|
||||
dismissToast: () => dispatch(dismissToast()),
|
||||
reset: () => dispatch(reset()),
|
||||
resetStatus: () => dispatch(resetStatus()),
|
||||
}),
|
||||
|
|
Loading…
Reference in New Issue