diff --git a/src/components/views/toasts/GenericExpiringToast.tsx b/src/components/views/toasts/GenericExpiringToast.tsx new file mode 100644 index 0000000000..83f43208c4 --- /dev/null +++ b/src/components/views/toasts/GenericExpiringToast.tsx @@ -0,0 +1,53 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +import ToastStore from "../../../stores/ToastStore"; +import GenericToast, { IProps as IGenericToastProps } from "./GenericToast"; +import {useExpiringCounter} from "../../../hooks/useTimeout"; + +interface IProps extends IGenericToastProps { + toastKey: string; + numSeconds: number; + dismissLabel: string; + onDismiss?(); +} + +const SECOND = 1000; + +const GenericExpiringToast: React.FC = ({description, acceptLabel, dismissLabel, onAccept, onDismiss, toastKey, numSeconds}) => { + const onReject = () => { + if (onDismiss) onDismiss(); + ToastStore.sharedInstance().dismissToast(toastKey); + }; + const counter = useExpiringCounter(onReject, SECOND, numSeconds); + + let rejectLabel = dismissLabel; + if (counter > 0) { + rejectLabel += ` (${counter})`; + } + + return ; +}; + +export default GenericExpiringToast; diff --git a/src/components/views/toasts/GenericToast.tsx b/src/components/views/toasts/GenericToast.tsx index ea12641948..9f8885ba47 100644 --- a/src/components/views/toasts/GenericToast.tsx +++ b/src/components/views/toasts/GenericToast.tsx @@ -19,7 +19,7 @@ import React, {ReactChild} from "react"; import FormButton from "../elements/FormButton"; import {XOR} from "../../../@types/common"; -interface IProps { +export interface IProps { description: ReactChild; acceptLabel: string; diff --git a/src/hooks/useTimeout.ts b/src/hooks/useTimeout.ts new file mode 100644 index 0000000000..911b7bc75d --- /dev/null +++ b/src/hooks/useTimeout.ts @@ -0,0 +1,67 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {useEffect, useRef, useState} from "react"; + +type Handler = () => void; + +// Hook to simplify timeouts in functional components +export const useTimeout = (handler: Handler, timeoutMs: number) => { + // Create a ref that stores handler + const savedHandler = useRef(); + + // Update ref.current value if handler changes. + useEffect(() => { + savedHandler.current = handler; + }, [handler]); + + // Set up timer + useEffect(() => { + const timeoutID = setTimeout(() => { + savedHandler.current(); + }, timeoutMs); + return () => clearTimeout(timeoutID); + }, [timeoutMs]); +}; + +// Hook to simplify intervals in functional components +export const useInterval = (handler: Handler, intervalMs: number) => { + // Create a ref that stores handler + const savedHandler = useRef(); + + // Update ref.current value if handler changes. + useEffect(() => { + savedHandler.current = handler; + }, [handler]); + + // Set up timer + useEffect(() => { + const intervalID = setInterval(() => { + savedHandler.current(); + }, intervalMs); + return () => clearInterval(intervalID); + }, [intervalMs]); +}; + +// Hook to simplify a variable counting down to 0, handler called when it reached 0 +export const useExpiringCounter = (handler: Handler, intervalMs: number, initialCount: number) => { + const [count, setCount] = useState(initialCount); + useInterval(() => setCount(c => c - 1), intervalMs); + if (count === 0) { + handler(); + } + return count; +}; diff --git a/src/stores/ToastStore.ts b/src/stores/ToastStore.ts index 55c48c3937..7063ba541a 100644 --- a/src/stores/ToastStore.ts +++ b/src/stores/ToastStore.ts @@ -24,7 +24,7 @@ export interface IToast; + props?: Omit, "toastKey">; // toastKey is injected by ToastContainer } /**