diff --git a/app/components/Toasts/Toasts.js b/app/components/Toasts/Toasts.js index 79259989..c82bedea 100644 --- a/app/components/Toasts/Toasts.js +++ b/app/components/Toasts/Toasts.js @@ -1,30 +1,24 @@ // @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 { - render() { - const { ui } = this.props; +function Toasts() { + const { ui } = useStores(); - return ( - - {ui.orderedToasts.map((toast) => ( - ui.removeToast(toast.id)} - /> - ))} - - ); - } + return ( + + {ui.orderedToasts.map((toast) => ( + ui.removeToast(toast.id)} + /> + ))} + + ); } 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); diff --git a/app/components/Toasts/components/Toast.js b/app/components/Toasts/components/Toast.js index b3d9c98b..da1389f0 100644 --- a/app/components/Toasts/components/Toast.js +++ b/app/components/Toasts/components/Toast.js @@ -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,48 +11,46 @@ type Props = { toast: TToast, }; -class Toast extends React.Component { - 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]); - componentWillUnmount() { - clearTimeout(this.timeout); - } + React.useEffect(() => { + if (reoccurring) { + setPulse(reoccurring); - render() { - const { toast, onRequestClose } = this.props; - const { action } = toast; - const message = - typeof toast.message === "string" - ? toast.message - : toast.message.toString(); + // must match animation time in css below vvv + setTimeout(() => setPulse(false), 250); + } + }, [reoccurring]); - return ( -
  • - - {message} - {action && ( - - {action.text} - - )} - -
  • - ); - } + const message = + typeof toast.message === "string" + ? toast.message + : toast.message.toString(); + + return ( + + + {message} + {action && ( + + {action.text} + + )} + + + ); } 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; diff --git a/app/stores/UiStore.js b/app/stores/UiStore.js index 7f664bce..b5e65d0e 100644 --- a/app/stores/UiStore.js +++ b/app/stores/UiStore.js @@ -25,6 +25,7 @@ class UiStore { @observable mobileSidebarVisible: boolean = false; @observable sidebarCollapsed: boolean = false; @observable toasts: Map = 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; }; diff --git a/app/types/index.js b/app/types/index.js index a30d9260..2c1c0849 100644 --- a/app/types/index.js +++ b/app/types/index.js @@ -17,6 +17,7 @@ export type Toast = { message: string, type: "warning" | "error" | "info" | "success", timeout?: number, + reoccurring?: number, action?: { text: string, onClick: () => void, diff --git a/shared/styles/animations.js b/shared/styles/animations.js index d09a3d2a..00dbb52a 100644 --- a/shared/styles/animations.js +++ b/shared/styles/animations.js @@ -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); } +`;