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