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<IProps> = ({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 <GenericToast
+        description={description}
+        acceptLabel={acceptLabel}
+        onAccept={onAccept}
+        rejectLabel={rejectLabel}
+        onReject={onReject}
+    />;
+};
+
+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<Handler>();
+
+    // 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<Handler>();
+
+    // 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<C extends keyof JSX.IntrinsicElements | JSXElementConstr
     title: string;
     icon?: string;
     component: C;
-    props?: React.ComponentProps<C>;
+    props?: Omit<React.ComponentProps<C>, "toastKey">; // toastKey is injected by ToastContainer
 }
 
 /**