2.7 KB83 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {ReactNode} from 'react';
9
10import {List} from 'immutable';
11import {atom} from 'jotai';
12import {t} from './i18n';
13import {atomWithOnChange, writeAtom} from './jotaiUtils';
14import platform from './platform';
15
16/**
17 * Push a toast. It will be displayed immediately and auto hides after a timeout.
18 *
19 * If `key` is specified, an existing toast with the same key will be replaced.
20 * This can be useful to ensure there are no 2 "Copied <text>" toasts at the same time,
21 * since the clipboard can only hold a single value.
22 *
23 * Note the internals use O(N) scans in various places.
24 * Do not push too many toasts.
25 */
26export function showToast(message: ReactNode, props?: {durationMs?: number; key?: string}) {
27 const {durationMs = DEFAULT_DURATION_MS, key} = props ?? {};
28 writeAtom(toastQueueAtom, oldValue => {
29 let nextValue = oldValue;
30 const hideAt = new Date(Date.now() + durationMs);
31 if (key != null) {
32 // Remove an existing toast with the same key.
33 nextValue = nextValue.filter(({key: k}) => k !== key);
34 }
35 return nextValue.push({message, disapparAt: hideAt, key: key ?? hideAt.getTime().toString()});
36 });
37}
38
39/** Show "Copied <text>" toast. Existing "copied' toast will be replaced. */
40export function copyAndShowToast(text: string, html?: string) {
41 platform.clipboardCopy(text, html);
42 showToast(t('Copied $text', {replace: {$text: text}}), {key: 'copied'});
43}
44
45/** Hide toasts with the given key. */
46export function hideToast(keys: Iterable<string>) {
47 const keySet = new Set(keys);
48 writeAtom(toastQueueAtom, oldValue => {
49 return oldValue.filter(({key}) => !keySet.has(key));
50 });
51}
52
53// Private states.
54
55type ToastProps = {
56 message: ReactNode;
57 key: string;
58 disapparAt: Date;
59};
60
61const DEFAULT_DURATION_MS = 2000;
62
63type ToastQueue = List<ToastProps>;
64
65const underlyingToastQueueAtom = atom<ToastQueue>(List<ToastProps>());
66export const toastQueueAtom = atomWithOnChange(underlyingToastQueueAtom, newValue => {
67 const firstDisapparAt = newValue.reduce((a, t) => Math.min(a, t.disapparAt.getTime()), Infinity);
68 if (firstDisapparAt === Infinity) {
69 return;
70 }
71 const interval = Math.max(firstDisapparAt - Date.now(), 1);
72 const timeout = setTimeout(() => {
73 writeAtom(underlyingToastQueueAtom, oldValue => removeExpired(oldValue));
74 }, interval);
75 return () => clearTimeout(timeout);
76});
77
78function removeExpired(queue: ToastQueue) {
79 const now = new Date();
80 const newQueue = queue.filter(({disapparAt}) => disapparAt > now);
81 return newQueue.size < queue.size ? newQueue : queue;
82}
83