Compare commits

..
Author SHA1 Message Date
dwelle 9bf3466cca Merge branch 'master' into zsviczian-fix-exportToSvg
# Conflicts:
#	src/scene/export.ts
2023-07-28 11:26:43 +02:00
zsviczianandGitHub 9a10503c5f Update export.ts 2022-12-16 23:24:08 +01:00
zsviczianandGitHub d87e080e88 Update export.ts 2022-12-16 23:12:03 +01:00
16 changed files with 604 additions and 592 deletions
+1 -1
View File
@@ -54,7 +54,7 @@
"react-dom": "18.2.0", "react-dom": "18.2.0",
"roughjs": "4.5.2", "roughjs": "4.5.2",
"sass": "1.51.0", "sass": "1.51.0",
"socket.io-client": "4.6.1", "socket.io-client": "2.3.1",
"tunnel-rat": "0.1.2" "tunnel-rat": "0.1.2"
}, },
"devDependencies": { "devDependencies": {
-1
View File
@@ -2748,7 +2748,6 @@ class App extends React.Component<AppProps, AppState> {
toast: { toast: {
message: string; message: string;
closable?: boolean; closable?: boolean;
spinner?: boolean;
duration?: number; duration?: number;
} | null, } | null,
) => { ) => {
-11
View File
@@ -25,17 +25,6 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
.Toast__message--spinner {
padding: 0 3rem;
}
.Toast__spinner {
position: absolute;
left: 1.5rem;
top: 50%;
margin-top: -8px;
}
.close { .close {
position: absolute; position: absolute;
top: 0; top: 0;
+1 -16
View File
@@ -1,7 +1,5 @@
import clsx from "clsx";
import { useCallback, useEffect, useRef } from "react"; import { useCallback, useEffect, useRef } from "react";
import { CloseIcon } from "./icons"; import { CloseIcon } from "./icons";
import Spinner from "./Spinner";
import "./Toast.scss"; import "./Toast.scss";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
@@ -11,14 +9,12 @@ export const Toast = ({
message, message,
onClose, onClose,
closable = false, closable = false,
spinner = true,
// To prevent autoclose, pass duration as Infinity // To prevent autoclose, pass duration as Infinity
duration = DEFAULT_TOAST_TIMEOUT, duration = DEFAULT_TOAST_TIMEOUT,
}: { }: {
message: string; message: string;
onClose: () => void; onClose: () => void;
closable?: boolean; closable?: boolean;
spinner?: boolean;
duration?: number; duration?: number;
}) => { }) => {
const timerRef = useRef<number>(0); const timerRef = useRef<number>(0);
@@ -48,18 +44,7 @@ export const Toast = ({
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
> >
{spinner && ( <p className="Toast__message">{message}</p>
<div className="Toast__spinner">
<Spinner />
</div>
)}
<p
className={clsx("Toast__message", {
"Toast__message--spinner": spinner,
})}
>
{message}
</p>
{closable && ( {closable && (
<ToolButton <ToolButton
icon={CloseIcon} icon={CloseIcon}
-2
View File
@@ -7,8 +7,6 @@ export const SYNC_FULL_SCENE_INTERVAL_MS = 20000;
export const SYNC_BROWSER_TABS_TIMEOUT = 50; export const SYNC_BROWSER_TABS_TIMEOUT = 50;
export const CURSOR_SYNC_TIMEOUT = 33; // ~30fps export const CURSOR_SYNC_TIMEOUT = 33; // ~30fps
export const DELETED_ELEMENT_TIMEOUT = 24 * 60 * 60 * 1000; // 1 day export const DELETED_ELEMENT_TIMEOUT = 24 * 60 * 60 * 1000; // 1 day
export const PAUSE_COLLABORATION_TIMEOUT = 2000;
export const RESUME_FALLBACK_TIMEOUT = 5000;
export const FILE_UPLOAD_MAX_BYTES = 3 * 1024 * 1024; // 3 MiB export const FILE_UPLOAD_MAX_BYTES = 3 * 1024 * 1024; // 3 MiB
// 1 year (https://stackoverflow.com/a/25201898/927631) // 1 year (https://stackoverflow.com/a/25201898/927631)
+43 -219
View File
@@ -1,6 +1,6 @@
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
import { PureComponent } from "react"; import { PureComponent } from "react";
import { ExcalidrawImperativeAPI, PauseCollaborationState } from "../../types"; import { ExcalidrawImperativeAPI } from "../../types";
import { ErrorDialog } from "../../components/ErrorDialog"; import { ErrorDialog } from "../../components/ErrorDialog";
import { APP_NAME, ENV, EVENT } from "../../constants"; import { APP_NAME, ENV, EVENT } from "../../constants";
import { ImportedDataState } from "../../data/types"; import { ImportedDataState } from "../../data/types";
@@ -16,7 +16,6 @@ import { Collaborator, Gesture } from "../../types";
import { import {
preventUnload, preventUnload,
resolvablePromise, resolvablePromise,
upsertMap,
withBatchedUpdates, withBatchedUpdates,
} from "../../utils"; } from "../../utils";
import { import {
@@ -25,15 +24,12 @@ import {
FIREBASE_STORAGE_PREFIXES, FIREBASE_STORAGE_PREFIXES,
INITIAL_SCENE_UPDATE_TIMEOUT, INITIAL_SCENE_UPDATE_TIMEOUT,
LOAD_IMAGES_TIMEOUT, LOAD_IMAGES_TIMEOUT,
PAUSE_COLLABORATION_TIMEOUT,
WS_SCENE_EVENT_TYPES, WS_SCENE_EVENT_TYPES,
SYNC_FULL_SCENE_INTERVAL_MS, SYNC_FULL_SCENE_INTERVAL_MS,
RESUME_FALLBACK_TIMEOUT,
} from "../app_constants"; } from "../app_constants";
import { import {
generateCollaborationLinkData, generateCollaborationLinkData,
getCollaborationLink, getCollaborationLink,
getCollaborationLinkData,
getCollabServer, getCollabServer,
getSyncableElements, getSyncableElements,
SocketUpdateDataSource, SocketUpdateDataSource,
@@ -47,8 +43,8 @@ import {
saveToFirebase, saveToFirebase,
} from "../data/firebase"; } from "../data/firebase";
import { import {
importUsernameAndIdFromLocalStorage, importUsernameFromLocalStorage,
saveUsernameAndIdToLocalStorage, saveUsernameToLocalStorage,
} from "../data/localStorage"; } from "../data/localStorage";
import Portal from "./Portal"; import Portal from "./Portal";
import RoomDialog from "./RoomDialog"; import RoomDialog from "./RoomDialog";
@@ -75,19 +71,16 @@ import { resetBrowserStateVersions } from "../data/tabSync";
import { LocalData } from "../data/LocalData"; import { LocalData } from "../data/LocalData";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { appJotaiStore } from "../app-jotai"; import { appJotaiStore } from "../app-jotai";
import { nanoid } from "nanoid";
export const collabAPIAtom = atom<CollabAPI | null>(null); export const collabAPIAtom = atom<CollabAPI | null>(null);
export const collabDialogShownAtom = atom(false); export const collabDialogShownAtom = atom(false);
export const isCollaboratingAtom = atom(false); export const isCollaboratingAtom = atom(false);
export const isOfflineAtom = atom(false); export const isOfflineAtom = atom(false);
export const isCollaborationPausedAtom = atom(false);
interface CollabState { interface CollabState {
errorMessage: string; errorMessage: string;
username: string; username: string;
activeRoomLink: string; activeRoomLink: string;
userId: string;
} }
type CollabInstance = InstanceType<typeof Collab>; type CollabInstance = InstanceType<typeof Collab>;
@@ -101,7 +94,6 @@ export interface CollabAPI {
syncElements: CollabInstance["syncElements"]; syncElements: CollabInstance["syncElements"];
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"]; fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
setUsername: (username: string) => void; setUsername: (username: string) => void;
isPaused: () => boolean;
} }
interface PublicProps { interface PublicProps {
@@ -116,7 +108,6 @@ class Collab extends PureComponent<Props, CollabState> {
excalidrawAPI: Props["excalidrawAPI"]; excalidrawAPI: Props["excalidrawAPI"];
activeIntervalId: number | null; activeIntervalId: number | null;
idleTimeoutId: number | null; idleTimeoutId: number | null;
pauseTimeoutId: number | null;
private socketInitializationTimer?: number; private socketInitializationTimer?: number;
private lastBroadcastedOrReceivedSceneVersion: number = -1; private lastBroadcastedOrReceivedSceneVersion: number = -1;
@@ -124,13 +115,9 @@ class Collab extends PureComponent<Props, CollabState> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
const { username, userId } = importUsernameAndIdFromLocalStorage() || {};
this.state = { this.state = {
errorMessage: "", errorMessage: "",
username: username || "", username: importUsernameFromLocalStorage() || "",
userId: userId || "",
activeRoomLink: "", activeRoomLink: "",
}; };
this.portal = new Portal(this); this.portal = new Portal(this);
@@ -162,7 +149,6 @@ class Collab extends PureComponent<Props, CollabState> {
this.excalidrawAPI = props.excalidrawAPI; this.excalidrawAPI = props.excalidrawAPI;
this.activeIntervalId = null; this.activeIntervalId = null;
this.idleTimeoutId = null; this.idleTimeoutId = null;
this.pauseTimeoutId = null;
} }
componentDidMount() { componentDidMount() {
@@ -181,7 +167,6 @@ class Collab extends PureComponent<Props, CollabState> {
fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase, fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
stopCollaboration: this.stopCollaboration, stopCollaboration: this.stopCollaboration,
setUsername: this.setUsername, setUsername: this.setUsername,
isPaused: this.isPaused,
}; };
appJotaiStore.set(collabAPIAtom, collabAPI); appJotaiStore.set(collabAPIAtom, collabAPI);
@@ -207,13 +192,10 @@ class Collab extends PureComponent<Props, CollabState> {
window.removeEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload); window.removeEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
window.removeEventListener(EVENT.UNLOAD, this.onUnload); window.removeEventListener(EVENT.UNLOAD, this.onUnload);
window.removeEventListener(EVENT.POINTER_MOVE, this.onPointerMove); window.removeEventListener(EVENT.POINTER_MOVE, this.onPointerMove);
// window.removeEventListener( window.removeEventListener(
// EVENT.VISIBILITY_CHANGE, EVENT.VISIBILITY_CHANGE,
// this.onVisibilityChange, this.onVisibilityChange,
// ); );
window.removeEventListener(EVENT.BLUR, this.onVisibilityChange);
window.removeEventListener(EVENT.FOCUS, this.onVisibilityChange);
if (this.activeIntervalId) { if (this.activeIntervalId) {
window.clearInterval(this.activeIntervalId); window.clearInterval(this.activeIntervalId);
this.activeIntervalId = null; this.activeIntervalId = null;
@@ -222,10 +204,6 @@ class Collab extends PureComponent<Props, CollabState> {
window.clearTimeout(this.idleTimeoutId); window.clearTimeout(this.idleTimeoutId);
this.idleTimeoutId = null; this.idleTimeoutId = null;
} }
if (this.pauseTimeoutId) {
window.clearTimeout(this.pauseTimeoutId);
this.pauseTimeoutId = null;
}
} }
isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!; isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
@@ -329,128 +307,6 @@ class Collab extends PureComponent<Props, CollabState> {
} }
}; };
fallbackResumeTimeout: null | ReturnType<typeof setTimeout> = null;
/**
* Handles the pause and resume states of a collaboration session.
* This function gets triggered when a change in the collaboration pause state is detected.
* Based on the state, the function carries out the following actions:
* 1. `PAUSED`: Saves the current scene to Firebase, disconnects the socket, and updates the scene to view mode.
* 2. `RESUMED`: Connects the socket, shows a toast message, sets a fallback to fetch data from Firebase, and resets the pause timeout if any.
* 3. `SYNCED`: Clears the fallback timeout if any, updates the collaboration pause state, and updates the scene to editing mode.
*
* @param state - The new state of the collaboration session. It is one of the values of `PauseCollaborationState` enum, which includes `PAUSED`, `RESUMED`, and `SYNCED`.
*/
onPauseCollaborationChange = (state: PauseCollaborationState) => {
switch (state) {
case PauseCollaborationState.PAUSED: {
if (this.portal.socket) {
// Save current scene to firebase
this.saveCollabRoomToFirebase(
getSyncableElements(
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
),
);
this.portal.socket.disconnect();
this.portal.socketInitialized = false;
this.setIsCollaborationPaused(true);
this.excalidrawAPI.updateScene({
appState: { viewModeEnabled: true },
});
}
break;
}
case PauseCollaborationState.RESUMED: {
if (this.portal.socket && this.isPaused()) {
this.portal.socket.connect();
this.portal.socket.emit(WS_SCENE_EVENT_TYPES.INIT);
console.log("setting toast");
this.excalidrawAPI.setToast({
message: t("toast.reconnectRoomServer"),
duration: Infinity,
spinner: true,
closable: false,
});
// Fallback to fetch data from firebase when reconnecting to scene without collaborators
const fallbackResumeHandler = async () => {
const roomLinkData = getCollaborationLinkData(
this.state.activeRoomLink,
);
if (!roomLinkData) {
return;
}
const elements = await loadFromFirebase(
roomLinkData.roomId,
roomLinkData.roomKey,
this.portal.socket,
);
if (elements) {
this.setLastBroadcastedOrReceivedSceneVersion(
getSceneVersion(elements),
);
this.excalidrawAPI.updateScene({
elements,
});
}
this.onPauseCollaborationChange(PauseCollaborationState.SYNCED);
};
// Set timeout to fallback to fetch data from firebase
this.fallbackResumeTimeout = setTimeout(
fallbackResumeHandler,
RESUME_FALLBACK_TIMEOUT,
);
// When no users are in the room, we fallback to fetch data from firebase immediately and clear fallback timeout
this.portal.socket.on("first-in-room", () => {
if (this.portal.socket) {
this.portal.socket.off("first-in-room");
// Recall init event to initialize collab with other users (fixes https://github.com/excalidraw/excalidraw/pull/6638#issuecomment-1600799080)
this.portal.socket.emit(WS_SCENE_EVENT_TYPES.INIT);
}
fallbackResumeHandler();
});
}
// Clear pause timeout if exists
if (this.pauseTimeoutId) {
clearTimeout(this.pauseTimeoutId);
}
break;
}
case PauseCollaborationState.SYNCED: {
if (this.fallbackResumeTimeout) {
clearTimeout(this.fallbackResumeTimeout);
this.fallbackResumeTimeout = null;
}
if (this.isPaused()) {
this.setIsCollaborationPaused(false);
this.excalidrawAPI.updateScene({
appState: { viewModeEnabled: false },
});
console.log("resetting toast");
this.excalidrawAPI.setToast(null);
this.excalidrawAPI.scrollToContent();
}
}
}
};
isPaused = () => appJotaiStore.get(isCollaborationPausedAtom)!;
setIsCollaborationPaused = (isPaused: boolean) => {
appJotaiStore.set(isCollaborationPausedAtom, isPaused);
};
private destroySocketClient = (opts?: { isUnload: boolean }) => { private destroySocketClient = (opts?: { isUnload: boolean }) => {
this.lastBroadcastedOrReceivedSceneVersion = -1; this.lastBroadcastedOrReceivedSceneVersion = -1;
this.portal.close(); this.portal.close();
@@ -529,11 +385,6 @@ class Collab extends PureComponent<Props, CollabState> {
}); });
} }
if (!this.state.userId) {
const userId = nanoid();
this.onUserIdChange(userId);
}
if (this.portal.socket) { if (this.portal.socket) {
return null; return null;
} }
@@ -648,7 +499,6 @@ class Collab extends PureComponent<Props, CollabState> {
elements: reconciledElements, elements: reconciledElements,
scrollToContent: true, scrollToContent: true,
}); });
this.onPauseCollaborationChange(PauseCollaborationState.SYNCED);
} }
break; break;
} }
@@ -658,45 +508,33 @@ class Collab extends PureComponent<Props, CollabState> {
); );
break; break;
case "MOUSE_LOCATION": { case "MOUSE_LOCATION": {
const { const { pointer, button, username, selectedElementIds } =
pointer, decryptedData.payload;
button, const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
username, decryptedData.payload.socketId ||
selectedElementIds, // @ts-ignore legacy, see #2094 (#2097)
userId, decryptedData.payload.socketID;
socketId,
} = decryptedData.payload; const collaborators = new Map(this.collaborators);
const collaborators = upsertMap( const user = collaborators.get(socketId) || {}!;
userId, user.pointer = pointer;
{ user.button = button;
username, user.selectedElementIds = selectedElementIds;
pointer, user.username = username;
button, collaborators.set(socketId, user);
selectedElementIds,
socketId,
},
this.collaborators,
);
this.excalidrawAPI.updateScene({ this.excalidrawAPI.updateScene({
collaborators: new Map(collaborators), collaborators,
}); });
break; break;
} }
case "IDLE_STATUS": { case "IDLE_STATUS": {
const { userState, username, userId, socketId } = const { userState, socketId, username } = decryptedData.payload;
decryptedData.payload; const collaborators = new Map(this.collaborators);
const collaborators = upsertMap( const user = collaborators.get(socketId) || {}!;
userId, user.userState = userState;
{ user.username = username;
username,
userState,
userId,
socketId,
},
this.collaborators,
);
this.excalidrawAPI.updateScene({ this.excalidrawAPI.updateScene({
collaborators: new Map(collaborators), collaborators,
}); });
break; break;
} }
@@ -705,7 +543,6 @@ class Collab extends PureComponent<Props, CollabState> {
); );
this.portal.socket.on("first-in-room", async () => { this.portal.socket.on("first-in-room", async () => {
console.log("first in room");
if (this.portal.socket) { if (this.portal.socket) {
this.portal.socket.off("first-in-room"); this.portal.socket.off("first-in-room");
} }
@@ -847,9 +684,7 @@ class Collab extends PureComponent<Props, CollabState> {
}; };
private onVisibilityChange = () => { private onVisibilityChange = () => {
// if (document.hidden) { if (document.hidden) {
console.log("VIS CHANGE");
if (!document.hasFocus()) {
if (this.idleTimeoutId) { if (this.idleTimeoutId) {
window.clearTimeout(this.idleTimeoutId); window.clearTimeout(this.idleTimeoutId);
this.idleTimeoutId = null; this.idleTimeoutId = null;
@@ -858,10 +693,6 @@ class Collab extends PureComponent<Props, CollabState> {
window.clearInterval(this.activeIntervalId); window.clearInterval(this.activeIntervalId);
this.activeIntervalId = null; this.activeIntervalId = null;
} }
this.pauseTimeoutId = window.setTimeout(
() => this.onPauseCollaborationChange(PauseCollaborationState.PAUSED),
PAUSE_COLLABORATION_TIMEOUT,
);
this.onIdleStateChange(UserIdleState.AWAY); this.onIdleStateChange(UserIdleState.AWAY);
} else { } else {
this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD); this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD);
@@ -870,11 +701,6 @@ class Collab extends PureComponent<Props, CollabState> {
ACTIVE_THRESHOLD, ACTIVE_THRESHOLD,
); );
this.onIdleStateChange(UserIdleState.ACTIVE); this.onIdleStateChange(UserIdleState.ACTIVE);
if (this.pauseTimeoutId) {
window.clearTimeout(this.pauseTimeoutId);
this.onPauseCollaborationChange(PauseCollaborationState.RESUMED);
this.pauseTimeoutId = null;
}
} }
}; };
@@ -891,19 +717,22 @@ class Collab extends PureComponent<Props, CollabState> {
}; };
private initializeIdleDetector = () => { private initializeIdleDetector = () => {
// document.addEventListener(EVENT.POINTER_MOVE, this.onPointerMove); document.addEventListener(EVENT.POINTER_MOVE, this.onPointerMove);
// document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange); document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange);
window.addEventListener(EVENT.BLUR, this.onVisibilityChange);
window.addEventListener(EVENT.FOCUS, this.onVisibilityChange);
}; };
setCollaborators(sockets: string[]) { setCollaborators(sockets: string[]) {
this.collaborators.forEach((value, key) => { const collaborators: InstanceType<typeof Collab>["collaborators"] =
if (value.socketId && !sockets.includes(value.socketId)) { new Map();
this.collaborators.delete(key); for (const socketId of sockets) {
if (this.collaborators.has(socketId)) {
collaborators.set(socketId, this.collaborators.get(socketId)!);
} else {
collaborators.set(socketId, {});
} }
}); }
this.excalidrawAPI.updateScene({ collaborators: this.collaborators }); this.collaborators = collaborators;
this.excalidrawAPI.updateScene({ collaborators });
} }
public setLastBroadcastedOrReceivedSceneVersion = (version: number) => { public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
@@ -989,12 +818,7 @@ class Collab extends PureComponent<Props, CollabState> {
onUsernameChange = (username: string) => { onUsernameChange = (username: string) => {
this.setUsername(username); this.setUsername(username);
saveUsernameAndIdToLocalStorage(username, this.state.userId); saveUsernameToLocalStorage(username);
};
onUserIdChange = (userId: string) => {
this.setState({ userId });
saveUsernameAndIdToLocalStorage(this.state.username, userId);
}; };
render() { render() {
+19 -32
View File
@@ -34,37 +34,11 @@ class Portal {
open(socket: SocketIOClient.Socket, id: string, key: string) { open(socket: SocketIOClient.Socket, id: string, key: string) {
this.socket = socket; this.socket = socket;
// @ts-ignore
window.socket = socket;
this.roomId = id; this.roomId = id;
this.roomKey = key; this.roomKey = key;
this.initializeSocketListeners();
return socket;
}
close() {
if (!this.socket) {
return;
}
this.queueFileUpload.flush();
this.socket.close();
this.socket = null;
this.roomId = null;
this.roomKey = null;
this.socketInitialized = false;
this.broadcastedElementVersions = new Map();
}
initializeSocketListeners() {
if (!this.socket) {
return;
}
// Initialize socket listeners // Initialize socket listeners
this.socket.on("init-room", () => { this.socket.on("init-room", () => {
console.log("join room");
if (this.socket) { if (this.socket) {
this.socket.emit("join-room", this.roomId); this.socket.emit("join-room", this.roomId);
trackEvent("share", "room joined"); trackEvent("share", "room joined");
@@ -80,6 +54,21 @@ class Portal {
this.socket.on("room-user-change", (clients: string[]) => { this.socket.on("room-user-change", (clients: string[]) => {
this.collab.setCollaborators(clients); this.collab.setCollaborators(clients);
}); });
return socket;
}
close() {
if (!this.socket) {
return;
}
this.queueFileUpload.flush();
this.socket.close();
this.socket = null;
this.roomId = null;
this.roomKey = null;
this.socketInitialized = false;
this.broadcastedElementVersions = new Map();
} }
isOpen() { isOpen() {
@@ -192,14 +181,13 @@ class Portal {
}; };
broadcastIdleChange = (userState: UserIdleState) => { broadcastIdleChange = (userState: UserIdleState) => {
if (this.socket) { if (this.socket?.id) {
const data: SocketUpdateDataSource["IDLE_STATUS"] = { const data: SocketUpdateDataSource["IDLE_STATUS"] = {
type: "IDLE_STATUS", type: "IDLE_STATUS",
payload: { payload: {
socketId: this.socket.id,
userState, userState,
username: this.collab.state.username, username: this.collab.state.username,
userId: this.collab.state.userId,
socketId: this.socket.id,
}, },
}; };
return this._broadcastSocketData( return this._broadcastSocketData(
@@ -213,17 +201,16 @@ class Portal {
pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"]; pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"];
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"]; button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
}) => { }) => {
if (this.socket) { if (this.socket?.id) {
const data: SocketUpdateDataSource["MOUSE_LOCATION"] = { const data: SocketUpdateDataSource["MOUSE_LOCATION"] = {
type: "MOUSE_LOCATION", type: "MOUSE_LOCATION",
payload: { payload: {
socketId: this.socket.id,
pointer: payload.pointer, pointer: payload.pointer,
button: payload.button || "up", button: payload.button || "up",
selectedElementIds: selectedElementIds:
this.collab.excalidrawAPI.getAppState().selectedElementIds, this.collab.excalidrawAPI.getAppState().selectedElementIds,
username: this.collab.state.username, username: this.collab.state.username,
userId: this.collab.state.userId,
socketId: this.socket.id,
}, },
}; };
return this._broadcastSocketData( return this._broadcastSocketData(
+2 -4
View File
@@ -106,21 +106,19 @@ export type SocketUpdateDataSource = {
MOUSE_LOCATION: { MOUSE_LOCATION: {
type: "MOUSE_LOCATION"; type: "MOUSE_LOCATION";
payload: { payload: {
socketId: string;
pointer: { x: number; y: number }; pointer: { x: number; y: number };
button: "down" | "up"; button: "down" | "up";
selectedElementIds: AppState["selectedElementIds"]; selectedElementIds: AppState["selectedElementIds"];
username: string; username: string;
userId: string;
socketId: string;
}; };
}; };
IDLE_STATUS: { IDLE_STATUS: {
type: "IDLE_STATUS"; type: "IDLE_STATUS";
payload: { payload: {
socketId: string;
userState: UserIdleState; userState: UserIdleState;
username: string; username: string;
userId: string;
socketId: string;
}; };
}; };
}; };
+4 -10
View File
@@ -8,14 +8,11 @@ import { clearElementsForLocalStorage } from "../../element";
import { STORAGE_KEYS } from "../app_constants"; import { STORAGE_KEYS } from "../app_constants";
import { ImportedDataState } from "../../data/types"; import { ImportedDataState } from "../../data/types";
export const saveUsernameAndIdToLocalStorage = ( export const saveUsernameToLocalStorage = (username: string) => {
username: string,
userId: string,
) => {
try { try {
localStorage.setItem( localStorage.setItem(
STORAGE_KEYS.LOCAL_STORAGE_COLLAB, STORAGE_KEYS.LOCAL_STORAGE_COLLAB,
JSON.stringify({ username, userId }), JSON.stringify({ username }),
); );
} catch (error: any) { } catch (error: any) {
// Unable to access window.localStorage // Unable to access window.localStorage
@@ -23,14 +20,11 @@ export const saveUsernameAndIdToLocalStorage = (
} }
}; };
export const importUsernameAndIdFromLocalStorage = (): { export const importUsernameFromLocalStorage = (): string | null => {
username: string;
userId: string;
} | null => {
try { try {
const data = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB); const data = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
if (data) { if (data) {
return JSON.parse(data); return JSON.parse(data).username;
} }
} catch (error: any) { } catch (error: any) {
// Unable to access localStorage // Unable to access localStorage
+2 -3
View File
@@ -65,7 +65,7 @@ import {
import { import {
getLibraryItemsFromStorage, getLibraryItemsFromStorage,
importFromLocalStorage, importFromLocalStorage,
importUsernameAndIdFromLocalStorage, importUsernameFromLocalStorage,
} from "./data/localStorage"; } from "./data/localStorage";
import CustomStats from "./CustomStats"; import CustomStats from "./CustomStats";
import { restore, restoreAppState, RestoredDataState } from "../data/restore"; import { restore, restoreAppState, RestoredDataState } from "../data/restore";
@@ -425,8 +425,7 @@ const ExcalidrawWrapper = () => {
// don't sync if local state is newer or identical to browser state // don't sync if local state is newer or identical to browser state
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) { if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
const localDataState = importFromLocalStorage(); const localDataState = importFromLocalStorage();
const username = const username = importUsernameFromLocalStorage();
importUsernameAndIdFromLocalStorage()?.username ?? "";
let langCode = languageDetector.detect() || defaultLang.code; let langCode = languageDetector.detect() || defaultLang.code;
if (Array.isArray(langCode)) { if (Array.isArray(langCode)) {
langCode = langCode[0]; langCode = langCode[0];
+1 -2
View File
@@ -425,8 +425,7 @@
"selection": "selection", "selection": "selection",
"pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor", "pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor",
"unableToEmbed": "Embedding this url is currently not allowed. Raise an issue on GitHub to request the url whitelisted", "unableToEmbed": "Embedding this url is currently not allowed. Raise an issue on GitHub to request the url whitelisted",
"unrecognizedLinkFormat": "The link you embedded does not match the expected format. Please try to paste the 'embed' string provided by the source site", "unrecognizedLinkFormat": "The link you embedded does not match the expected format. Please try to paste the 'embed' string provided by the source site"
"reconnectRoomServer": "Reconnecting to server"
}, },
"colors": { "colors": {
"transparent": "Transparent", "transparent": "Transparent",
+2 -2
View File
@@ -707,8 +707,8 @@ export const _renderScene = ({
if (renderConfig.remoteSelectedElementIds[element.id]) { if (renderConfig.remoteSelectedElementIds[element.id]) {
selectionColors.push( selectionColors.push(
...renderConfig.remoteSelectedElementIds[element.id].map( ...renderConfig.remoteSelectedElementIds[element.id].map(
(userId) => { (socketId) => {
const background = getClientColor(userId); const background = getClientColor(socketId);
return background; return background;
}, },
), ),
+16
View File
@@ -15,6 +15,18 @@ import Scene from "./Scene";
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`; export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
const createScene = (
elements: readonly NonDeletedExcalidrawElement[],
): Scene | null => {
if (!elements || Scene.getScene(elements[0])) {
return null;
}
const scene = new Scene();
scene.replaceAllElements(elements);
elements?.forEach((el) => Scene.mapElementToScene(el, scene));
return scene;
};
export const exportToCanvas = async ( export const exportToCanvas = async (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
@@ -38,6 +50,7 @@ export const exportToCanvas = async (
return { canvas, scale: appState.exportScale }; return { canvas, scale: appState.exportScale };
}, },
) => { ) => {
const scene = createScene(elements);
const [minX, minY, width, height] = getCanvasSize(elements, exportPadding); const [minX, minY, width, height] = getCanvasSize(elements, exportPadding);
const { canvas, scale = 1 } = createCanvas(width, height); const { canvas, scale = 1 } = createCanvas(width, height);
@@ -79,6 +92,7 @@ export const exportToCanvas = async (
}, },
}); });
scene?.destroy();
return canvas; return canvas;
}; };
@@ -105,6 +119,7 @@ export const exportToSvg = async (
exportScale = 1, exportScale = 1,
exportEmbedScene, exportEmbedScene,
} = appState; } = appState;
const scene = createScene(elements);
let metadata = ""; let metadata = "";
if (exportEmbedScene) { if (exportEmbedScene) {
try { try {
@@ -217,6 +232,7 @@ export const exportToSvg = async (
renderEmbeddables: opts?.renderEmbeddables, renderEmbeddables: opts?.renderEmbeddables,
}); });
scene?.destroy();
return svgRoot; return svgRoot;
}; };
-7
View File
@@ -56,7 +56,6 @@ export type Collaborator = {
avatarUrl?: string; avatarUrl?: string;
// user id. If supplied, we'll filter out duplicates when rendering user avatars. // user id. If supplied, we'll filter out duplicates when rendering user avatars.
id?: string; id?: string;
socketId?: string;
}; };
export type DataURL = string & { _brand: "DataURL" }; export type DataURL = string & { _brand: "DataURL" };
@@ -402,12 +401,6 @@ export enum UserIdleState {
IDLE = "idle", IDLE = "idle",
} }
export enum PauseCollaborationState {
PAUSED = "paused",
RESUMED = "resumed",
SYNCED = "synced",
}
export type ExportOpts = { export type ExportOpts = {
saveFileToDisk?: boolean; saveFileToDisk?: boolean;
onExportToBackend?: ( onExportToBackend?: (
-11
View File
@@ -914,14 +914,3 @@ export const isOnlyExportingSingleFrame = (
) )
); );
}; };
export const upsertMap = <T>(key: T, value: object, map: Map<T, object>) => {
if (!map.has(key)) {
map.set(key, value);
} else {
const old = map.get(key);
map.set(key, { ...old, ...value });
}
return map;
};
+513 -271
View File
File diff suppressed because it is too large Load Diff