fix: Improve toast messages to not show multiple of the same

This commit is contained in:
Tom Moor
2021-01-02 21:09:43 -08:00
parent 68bbd9fa34
commit bb81aa0065
5 changed files with 79 additions and 61 deletions

View File

@ -1,17 +1,12 @@
// @flow
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import UiStore from "../../stores/UiStore";
import Toast from "./components/Toast";
import useStores from "hooks/useStores";
type Props = {
ui: UiStore,
};
@observer
class Toasts extends React.Component<Props> {
render() {
const { ui } = this.props;
function Toasts() {
const { ui } = useStores();
return (
<List>
@ -24,7 +19,6 @@ class Toasts extends React.Component<Props> {
))}
</List>
);
}
}
const List = styled.ol`
@ -37,4 +31,4 @@ const List = styled.ol`
z-index: ${(props) => props.theme.depths.toasts};
`;
export default inject("ui")(Toasts);
export default observer(Toasts);

View File

@ -1,8 +1,8 @@
// @flow
import { darken } from "polished";
import * as React from "react";
import styled from "styled-components";
import { fadeAndScaleIn } from "shared/styles/animations";
import styled, { css } from "styled-components";
import { fadeAndScaleIn, pulse } from "shared/styles/animations";
import type { Toast as TToast } from "types";
type Props = {
@ -11,34 +11,33 @@ type Props = {
toast: TToast,
};
class Toast extends React.Component<Props> {
timeout: TimeoutID;
function Toast({ closeAfterMs = 3000, onRequestClose, toast }: Props) {
const timeout = React.useRef();
const [pulse, setPulse] = React.useState(false);
const { action, reoccurring } = toast;
static defaultProps = {
closeAfterMs: 3000,
};
React.useEffect(() => {
timeout.current = setTimeout(onRequestClose, toast.timeout || closeAfterMs);
componentDidMount() {
this.timeout = setTimeout(
this.props.onRequestClose,
this.props.toast.timeout || this.props.closeAfterMs
);
return () => clearTimeout(timeout.current);
}, [onRequestClose, toast, closeAfterMs]);
React.useEffect(() => {
if (reoccurring) {
setPulse(reoccurring);
// must match animation time in css below vvv
setTimeout(() => setPulse(false), 250);
}
}, [reoccurring]);
componentWillUnmount() {
clearTimeout(this.timeout);
}
render() {
const { toast, onRequestClose } = this.props;
const { action } = toast;
const message =
typeof toast.message === "string"
? toast.message
: toast.message.toString();
return (
<li>
<ListItem $pulse={pulse}>
<Container
onClick={action ? undefined : onRequestClose}
type={toast.type || "success"}
@ -50,9 +49,8 @@ class Toast extends React.Component<Props> {
</Action>
)}
</Container>
</li>
</ListItem>
);
}
}
const Action = styled.span`
@ -71,6 +69,14 @@ const Action = styled.span`
}
`;
const ListItem = styled.li`
${(props) =>
props.$pulse &&
css`
animation: ${pulse} 250ms;
`}
`;
const Container = styled.div`
display: inline-block;
align-items: center;

View File

@ -25,6 +25,7 @@ class UiStore {
@observable mobileSidebarVisible: boolean = false;
@observable sidebarCollapsed: boolean = false;
@observable toasts: Map<string, Toast> = new Map();
lastToastId: string;
constructor() {
// Rehydrate
@ -178,9 +179,19 @@ class UiStore {
) => {
if (!message) return;
const lastToast = this.toasts.get(this.lastToastId);
if (lastToast && lastToast.message === message) {
this.toasts.set(this.lastToastId, {
...lastToast,
reoccurring: lastToast.reoccurring ? ++lastToast.reoccurring : 1,
});
return;
}
const id = v4();
const createdAt = new Date().toISOString();
this.toasts.set(id, { message, createdAt, id, ...options });
this.lastToastId = id;
return id;
};

View File

@ -17,6 +17,7 @@ export type Toast = {
message: string,
type: "warning" | "error" | "info" | "success",
timeout?: number,
reoccurring?: number,
action?: {
text: string,
onClick: () => void,

View File

@ -80,3 +80,9 @@ export const pulsate = keyframes`
50% { opacity: 0.5; }
100% { opacity: 1; }
`;
export const pulse = keyframes`
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
`;