addons/isl/src/toast.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {ReactNode} from 'react';
b69ab319
b69ab3110import {List} from 'immutable';
b69ab3111import {atom} from 'jotai';
b69ab3112import {t} from './i18n';
b69ab3113import {atomWithOnChange, writeAtom} from './jotaiUtils';
b69ab3114import platform from './platform';
b69ab3115
b69ab3116/**
b69ab3117 * Push a toast. It will be displayed immediately and auto hides after a timeout.
b69ab3118 *
b69ab3119 * If `key` is specified, an existing toast with the same key will be replaced.
b69ab3120 * This can be useful to ensure there are no 2 "Copied <text>" toasts at the same time,
b69ab3121 * since the clipboard can only hold a single value.
b69ab3122 *
b69ab3123 * Note the internals use O(N) scans in various places.
b69ab3124 * Do not push too many toasts.
b69ab3125 */
b69ab3126export function showToast(message: ReactNode, props?: {durationMs?: number; key?: string}) {
b69ab3127 const {durationMs = DEFAULT_DURATION_MS, key} = props ?? {};
b69ab3128 writeAtom(toastQueueAtom, oldValue => {
b69ab3129 let nextValue = oldValue;
b69ab3130 const hideAt = new Date(Date.now() + durationMs);
b69ab3131 if (key != null) {
b69ab3132 // Remove an existing toast with the same key.
b69ab3133 nextValue = nextValue.filter(({key: k}) => k !== key);
b69ab3134 }
b69ab3135 return nextValue.push({message, disapparAt: hideAt, key: key ?? hideAt.getTime().toString()});
b69ab3136 });
b69ab3137}
b69ab3138
b69ab3139/** Show "Copied <text>" toast. Existing "copied' toast will be replaced. */
b69ab3140export function copyAndShowToast(text: string, html?: string) {
b69ab3141 platform.clipboardCopy(text, html);
b69ab3142 showToast(t('Copied $text', {replace: {$text: text}}), {key: 'copied'});
b69ab3143}
b69ab3144
b69ab3145/** Hide toasts with the given key. */
b69ab3146export function hideToast(keys: Iterable<string>) {
b69ab3147 const keySet = new Set(keys);
b69ab3148 writeAtom(toastQueueAtom, oldValue => {
b69ab3149 return oldValue.filter(({key}) => !keySet.has(key));
b69ab3150 });
b69ab3151}
b69ab3152
b69ab3153// Private states.
b69ab3154
b69ab3155type ToastProps = {
b69ab3156 message: ReactNode;
b69ab3157 key: string;
b69ab3158 disapparAt: Date;
b69ab3159};
b69ab3160
b69ab3161const DEFAULT_DURATION_MS = 2000;
b69ab3162
b69ab3163type ToastQueue = List<ToastProps>;
b69ab3164
b69ab3165const underlyingToastQueueAtom = atom<ToastQueue>(List<ToastProps>());
b69ab3166export const toastQueueAtom = atomWithOnChange(underlyingToastQueueAtom, newValue => {
b69ab3167 const firstDisapparAt = newValue.reduce((a, t) => Math.min(a, t.disapparAt.getTime()), Infinity);
b69ab3168 if (firstDisapparAt === Infinity) {
b69ab3169 return;
b69ab3170 }
b69ab3171 const interval = Math.max(firstDisapparAt - Date.now(), 1);
b69ab3172 const timeout = setTimeout(() => {
b69ab3173 writeAtom(underlyingToastQueueAtom, oldValue => removeExpired(oldValue));
b69ab3174 }, interval);
b69ab3175 return () => clearTimeout(timeout);
b69ab3176});
b69ab3177
b69ab3178function removeExpired(queue: ToastQueue) {
b69ab3179 const now = new Date();
b69ab3180 const newQueue = queue.filter(({disapparAt}) => disapparAt > now);
b69ab3181 return newQueue.size < queue.size ? newQueue : queue;
b69ab3182}