mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-04 12:54:23 +01:00 
			
		
		
		
	feat: improve collab error notification (#7741)
* identify cause * toast after dialog for error messages in collab * remove comment * shake tooltip instead for repeating collab errors * clear collab error * empty commit * simplify & fix reset race condition --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
		@@ -104,6 +104,7 @@ import { openConfirmModal } from "../packages/excalidraw/components/OverwriteCon
 | 
				
			|||||||
import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
 | 
					import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
 | 
				
			||||||
import Trans from "../packages/excalidraw/components/Trans";
 | 
					import Trans from "../packages/excalidraw/components/Trans";
 | 
				
			||||||
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
 | 
					import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
 | 
				
			||||||
 | 
					import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
polyfill();
 | 
					polyfill();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -310,6 +311,7 @@ const ExcalidrawWrapper = () => {
 | 
				
			|||||||
  const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
 | 
					  const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
 | 
				
			||||||
    return isCollaborationLink(window.location.href);
 | 
					    return isCollaborationLink(window.location.href);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					  const collabError = useAtomValue(collabErrorIndicatorAtom);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useHandleLibrary({
 | 
					  useHandleLibrary({
 | 
				
			||||||
    excalidrawAPI,
 | 
					    excalidrawAPI,
 | 
				
			||||||
@@ -748,12 +750,15 @@ const ExcalidrawWrapper = () => {
 | 
				
			|||||||
            return null;
 | 
					            return null;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          return (
 | 
					          return (
 | 
				
			||||||
            <LiveCollaborationTrigger
 | 
					            <div className="top-right-ui">
 | 
				
			||||||
              isCollaborating={isCollaborating}
 | 
					              {collabError.message && <CollabError collabError={collabError} />}
 | 
				
			||||||
              onSelect={() =>
 | 
					              <LiveCollaborationTrigger
 | 
				
			||||||
                setShareDialogState({ isOpen: true, type: "share" })
 | 
					                isCollaborating={isCollaborating}
 | 
				
			||||||
              }
 | 
					                onSelect={() =>
 | 
				
			||||||
            />
 | 
					                  setShareDialogState({ isOpen: true, type: "share" })
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -81,6 +81,7 @@ import { appJotaiStore } from "../app-jotai";
 | 
				
			|||||||
import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
 | 
					import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
 | 
				
			||||||
import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
 | 
					import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
 | 
				
			||||||
import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";
 | 
					import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";
 | 
				
			||||||
 | 
					import { collabErrorIndicatorAtom } from "./CollabError";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
 | 
					export const collabAPIAtom = atom<CollabAPI | null>(null);
 | 
				
			||||||
export const isCollaboratingAtom = atom(false);
 | 
					export const isCollaboratingAtom = atom(false);
 | 
				
			||||||
@@ -88,6 +89,8 @@ export const isOfflineAtom = atom(false);
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
interface CollabState {
 | 
					interface CollabState {
 | 
				
			||||||
  errorMessage: string | null;
 | 
					  errorMessage: string | null;
 | 
				
			||||||
 | 
					  /** errors related to saving */
 | 
				
			||||||
 | 
					  dialogNotifiedErrors: Record<string, boolean>;
 | 
				
			||||||
  username: string;
 | 
					  username: string;
 | 
				
			||||||
  activeRoomLink: string | null;
 | 
					  activeRoomLink: string | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -107,7 +110,7 @@ export interface CollabAPI {
 | 
				
			|||||||
  setUsername: CollabInstance["setUsername"];
 | 
					  setUsername: CollabInstance["setUsername"];
 | 
				
			||||||
  getUsername: CollabInstance["getUsername"];
 | 
					  getUsername: CollabInstance["getUsername"];
 | 
				
			||||||
  getActiveRoomLink: CollabInstance["getActiveRoomLink"];
 | 
					  getActiveRoomLink: CollabInstance["getActiveRoomLink"];
 | 
				
			||||||
  setErrorMessage: CollabInstance["setErrorMessage"];
 | 
					  setCollabError: CollabInstance["setErrorDialog"];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface CollabProps {
 | 
					interface CollabProps {
 | 
				
			||||||
@@ -129,6 +132,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
 | 
				
			|||||||
    super(props);
 | 
					    super(props);
 | 
				
			||||||
    this.state = {
 | 
					    this.state = {
 | 
				
			||||||
      errorMessage: null,
 | 
					      errorMessage: null,
 | 
				
			||||||
 | 
					      dialogNotifiedErrors: {},
 | 
				
			||||||
      username: importUsernameFromLocalStorage() || "",
 | 
					      username: importUsernameFromLocalStorage() || "",
 | 
				
			||||||
      activeRoomLink: null,
 | 
					      activeRoomLink: null,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
@@ -197,7 +201,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
 | 
				
			|||||||
      setUsername: this.setUsername,
 | 
					      setUsername: this.setUsername,
 | 
				
			||||||
      getUsername: this.getUsername,
 | 
					      getUsername: this.getUsername,
 | 
				
			||||||
      getActiveRoomLink: this.getActiveRoomLink,
 | 
					      getActiveRoomLink: this.getActiveRoomLink,
 | 
				
			||||||
      setErrorMessage: this.setErrorMessage,
 | 
					      setCollabError: this.setErrorDialog,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    appJotaiStore.set(collabAPIAtom, collabAPI);
 | 
					    appJotaiStore.set(collabAPIAtom, collabAPI);
 | 
				
			||||||
@@ -276,18 +280,35 @@ class Collab extends PureComponent<CollabProps, CollabState> {
 | 
				
			|||||||
        this.excalidrawAPI.getAppState(),
 | 
					        this.excalidrawAPI.getAppState(),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.resetErrorIndicator();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (this.isCollaborating() && savedData && savedData.reconciledElements) {
 | 
					      if (this.isCollaborating() && savedData && savedData.reconciledElements) {
 | 
				
			||||||
        this.handleRemoteSceneUpdate(
 | 
					        this.handleRemoteSceneUpdate(
 | 
				
			||||||
          this.reconcileElements(savedData.reconciledElements),
 | 
					          this.reconcileElements(savedData.reconciledElements),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (error: any) {
 | 
					    } catch (error: any) {
 | 
				
			||||||
      this.setState({
 | 
					      const errorMessage = /is longer than.*?bytes/.test(error.message)
 | 
				
			||||||
        // firestore doesn't return a specific error code when size exceeded
 | 
					        ? t("errors.collabSaveFailed_sizeExceeded")
 | 
				
			||||||
        errorMessage: /is longer than.*?bytes/.test(error.message)
 | 
					        : t("errors.collabSaveFailed");
 | 
				
			||||||
          ? t("errors.collabSaveFailed_sizeExceeded")
 | 
					
 | 
				
			||||||
          : t("errors.collabSaveFailed"),
 | 
					      if (
 | 
				
			||||||
      });
 | 
					        !this.state.dialogNotifiedErrors[errorMessage] ||
 | 
				
			||||||
 | 
					        !this.isCollaborating()
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        this.setErrorDialog(errorMessage);
 | 
				
			||||||
 | 
					        this.setState({
 | 
				
			||||||
 | 
					          dialogNotifiedErrors: {
 | 
				
			||||||
 | 
					            ...this.state.dialogNotifiedErrors,
 | 
				
			||||||
 | 
					            [errorMessage]: true,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.isCollaborating()) {
 | 
				
			||||||
 | 
					        this.setErrorIndicator(errorMessage);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      console.error(error);
 | 
					      console.error(error);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
@@ -296,6 +317,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
 | 
				
			|||||||
    this.queueBroadcastAllElements.cancel();
 | 
					    this.queueBroadcastAllElements.cancel();
 | 
				
			||||||
    this.queueSaveToFirebase.cancel();
 | 
					    this.queueSaveToFirebase.cancel();
 | 
				
			||||||
    this.loadImageFiles.cancel();
 | 
					    this.loadImageFiles.cancel();
 | 
				
			||||||
 | 
					    this.resetErrorIndicator(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.saveCollabRoomToFirebase(
 | 
					    this.saveCollabRoomToFirebase(
 | 
				
			||||||
      getSyncableElements(
 | 
					      getSyncableElements(
 | 
				
			||||||
@@ -464,7 +486,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
 | 
				
			|||||||
      this.portal.socket.once("connect_error", fallbackInitializationHandler);
 | 
					      this.portal.socket.once("connect_error", fallbackInitializationHandler);
 | 
				
			||||||
    } catch (error: any) {
 | 
					    } catch (error: any) {
 | 
				
			||||||
      console.error(error);
 | 
					      console.error(error);
 | 
				
			||||||
      this.setState({ errorMessage: error.message });
 | 
					      this.setErrorDialog(error.message);
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -923,8 +945,26 @@ class Collab extends PureComponent<CollabProps, CollabState> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  getActiveRoomLink = () => this.state.activeRoomLink;
 | 
					  getActiveRoomLink = () => this.state.activeRoomLink;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setErrorMessage = (errorMessage: string | null) => {
 | 
					  setErrorIndicator = (errorMessage: string | null) => {
 | 
				
			||||||
    this.setState({ errorMessage });
 | 
					    appJotaiStore.set(collabErrorIndicatorAtom, {
 | 
				
			||||||
 | 
					      message: errorMessage,
 | 
				
			||||||
 | 
					      nonce: Date.now(),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  resetErrorIndicator = (resetDialogNotifiedErrors = false) => {
 | 
				
			||||||
 | 
					    appJotaiStore.set(collabErrorIndicatorAtom, { message: null, nonce: 0 });
 | 
				
			||||||
 | 
					    if (resetDialogNotifiedErrors) {
 | 
				
			||||||
 | 
					      this.setState({
 | 
				
			||||||
 | 
					        dialogNotifiedErrors: {},
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setErrorDialog = (errorMessage: string | null) => {
 | 
				
			||||||
 | 
					    this.setState({
 | 
				
			||||||
 | 
					      errorMessage,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render() {
 | 
					  render() {
 | 
				
			||||||
@@ -933,7 +973,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
 | 
				
			|||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <>
 | 
					      <>
 | 
				
			||||||
        {errorMessage != null && (
 | 
					        {errorMessage != null && (
 | 
				
			||||||
          <ErrorDialog onClose={() => this.setState({ errorMessage: null })}>
 | 
					          <ErrorDialog onClose={() => this.setErrorDialog(null)}>
 | 
				
			||||||
            {errorMessage}
 | 
					            {errorMessage}
 | 
				
			||||||
          </ErrorDialog>
 | 
					          </ErrorDialog>
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										35
									
								
								excalidraw-app/collab/CollabError.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								excalidraw-app/collab/CollabError.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					@import "../../packages/excalidraw/css/variables.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.excalidraw {
 | 
				
			||||||
 | 
					  .collab-errors-button {
 | 
				
			||||||
 | 
					    width: 26px;
 | 
				
			||||||
 | 
					    height: 26px;
 | 
				
			||||||
 | 
					    margin-inline-end: 1rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    color: var(--color-danger);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .collab-errors-button-shake {
 | 
				
			||||||
 | 
					    animation: strong-shake 0.15s 6;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @keyframes strong-shake {
 | 
				
			||||||
 | 
					    0% {
 | 
				
			||||||
 | 
					      transform: rotate(0deg);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    25% {
 | 
				
			||||||
 | 
					      transform: rotate(10deg);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    50% {
 | 
				
			||||||
 | 
					      transform: rotate(0eg);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    75% {
 | 
				
			||||||
 | 
					      transform: rotate(-10deg);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    100% {
 | 
				
			||||||
 | 
					      transform: rotate(0deg);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										54
									
								
								excalidraw-app/collab/CollabError.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								excalidraw-app/collab/CollabError.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					import { Tooltip } from "../../packages/excalidraw/components/Tooltip";
 | 
				
			||||||
 | 
					import { warning } from "../../packages/excalidraw/components/icons";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					import { useEffect, useRef, useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "./CollabError.scss";
 | 
				
			||||||
 | 
					import { atom } from "jotai";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ErrorIndicator = {
 | 
				
			||||||
 | 
					  message: string | null;
 | 
				
			||||||
 | 
					  /** used to rerun the useEffect responsible for animation */
 | 
				
			||||||
 | 
					  nonce: number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const collabErrorIndicatorAtom = atom<ErrorIndicator>({
 | 
				
			||||||
 | 
					  message: null,
 | 
				
			||||||
 | 
					  nonce: 0,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const CollabError = ({ collabError }: { collabError: ErrorIndicator }) => {
 | 
				
			||||||
 | 
					  const [isAnimating, setIsAnimating] = useState(false);
 | 
				
			||||||
 | 
					  const clearAnimationRef = useRef<string | number | NodeJS.Timeout>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setIsAnimating(true);
 | 
				
			||||||
 | 
					    clearAnimationRef.current = setTimeout(() => {
 | 
				
			||||||
 | 
					      setIsAnimating(false);
 | 
				
			||||||
 | 
					    }, 1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      clearTimeout(clearAnimationRef.current);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, [collabError.message, collabError.nonce]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!collabError.message) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Tooltip label={collabError.message} long={true}>
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={clsx("collab-errors-button", {
 | 
				
			||||||
 | 
					          "collab-errors-button-shake": isAnimating,
 | 
				
			||||||
 | 
					        })}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {warning}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </Tooltip>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CollabError.displayName = "CollabError";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default CollabError;
 | 
				
			||||||
@@ -4,6 +4,13 @@
 | 
				
			|||||||
  &.theme--dark {
 | 
					  &.theme--dark {
 | 
				
			||||||
    --color-primary-contrast-offset: #726dff; // to offset Chubb illusion
 | 
					    --color-primary-contrast-offset: #726dff; // to offset Chubb illusion
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .top-right-ui {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .footer-center {
 | 
					  .footer-center {
 | 
				
			||||||
    justify-content: flex-end;
 | 
					    justify-content: flex-end;
 | 
				
			||||||
    margin-top: auto;
 | 
					    margin-top: auto;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -70,7 +70,7 @@ const ActiveRoomDialog = ({
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await copyTextToSystemClipboard(activeRoomLink);
 | 
					      await copyTextToSystemClipboard(activeRoomLink);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      collabAPI.setErrorMessage(t("errors.copyToSystemClipboardFailed"));
 | 
					      collabAPI.setCollabError(t("errors.copyToSystemClipboardFailed"));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setJustCopied(true);
 | 
					    setJustCopied(true);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import { useCallback, useEffect, useRef } from "react";
 | 
					import { CSSProperties, useCallback, useEffect, useRef } from "react";
 | 
				
			||||||
import { CloseIcon } from "./icons";
 | 
					import { CloseIcon } from "./icons";
 | 
				
			||||||
import "./Toast.scss";
 | 
					import "./Toast.scss";
 | 
				
			||||||
import { ToolButton } from "./ToolButton";
 | 
					import { ToolButton } from "./ToolButton";
 | 
				
			||||||
@@ -11,11 +11,13 @@ export const Toast = ({
 | 
				
			|||||||
  closable = false,
 | 
					  closable = false,
 | 
				
			||||||
  // To prevent autoclose, pass duration as Infinity
 | 
					  // To prevent autoclose, pass duration as Infinity
 | 
				
			||||||
  duration = DEFAULT_TOAST_TIMEOUT,
 | 
					  duration = DEFAULT_TOAST_TIMEOUT,
 | 
				
			||||||
 | 
					  style,
 | 
				
			||||||
}: {
 | 
					}: {
 | 
				
			||||||
  message: string;
 | 
					  message: string;
 | 
				
			||||||
  onClose: () => void;
 | 
					  onClose: () => void;
 | 
				
			||||||
  closable?: boolean;
 | 
					  closable?: boolean;
 | 
				
			||||||
  duration?: number;
 | 
					  duration?: number;
 | 
				
			||||||
 | 
					  style?: CSSProperties;
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
  const timerRef = useRef<number>(0);
 | 
					  const timerRef = useRef<number>(0);
 | 
				
			||||||
  const shouldAutoClose = duration !== Infinity;
 | 
					  const shouldAutoClose = duration !== Infinity;
 | 
				
			||||||
@@ -43,6 +45,7 @@ export const Toast = ({
 | 
				
			|||||||
      className="Toast"
 | 
					      className="Toast"
 | 
				
			||||||
      onMouseEnter={onMouseEnter}
 | 
					      onMouseEnter={onMouseEnter}
 | 
				
			||||||
      onMouseLeave={onMouseLeave}
 | 
					      onMouseLeave={onMouseLeave}
 | 
				
			||||||
 | 
					      style={style}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <p className="Toast__message">{message}</p>
 | 
					      <p className="Toast__message">{message}</p>
 | 
				
			||||||
      {closable && (
 | 
					      {closable && (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -604,6 +604,10 @@ export const share = createIcon(
 | 
				
			|||||||
  modifiedTablerIconProps,
 | 
					  modifiedTablerIconProps,
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const warning = createIcon(
 | 
				
			||||||
 | 
					  "M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z",
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const shareIOS = createIcon(
 | 
					export const shareIOS = createIcon(
 | 
				
			||||||
  "M16 5l-1.42 1.42-1.59-1.59V16h-1.98V4.83L9.42 6.42 8 5l4-4 4 4zm4 5v11c0 1.1-.9 2-2 2H6c-1.11 0-2-.9-2-2V10c0-1.11.89-2 2-2h3v2H6v11h12V10h-3V8h3c1.1 0 2 .89 2 2z",
 | 
					  "M16 5l-1.42 1.42-1.59-1.59V16h-1.98V4.83L9.42 6.42 8 5l4-4 4 4zm4 5v11c0 1.1-.9 2-2 2H6c-1.11 0-2-.9-2-2V10c0-1.11.89-2 2-2h3v2H6v11h12V10h-3V8h3c1.1 0 2 .89 2 2z",
 | 
				
			||||||
  { width: 24, height: 24 },
 | 
					  { width: 24, height: 24 },
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user