162 lines
4.6 KiB
TypeScript
162 lines
4.6 KiB
TypeScript
import { generateEncryptionKey } from "../../packages/excalidraw/data/encryption";
|
|
import { restore } from "../../packages/excalidraw/data/restore";
|
|
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
|
import type { SceneBounds } from "../../packages/excalidraw/element/bounds";
|
|
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
|
|
import type {
|
|
ExcalidrawElement,
|
|
OrderedExcalidrawElement,
|
|
} from "../../packages/excalidraw/element/types";
|
|
import { t } from "../../packages/excalidraw/i18n";
|
|
import type {
|
|
AppState,
|
|
SocketId,
|
|
UserIdleState,
|
|
} from "../../packages/excalidraw/types";
|
|
import type { MakeBrand } from "../../packages/excalidraw/utility-types";
|
|
import { bytesToHexString } from "../../packages/excalidraw/utils";
|
|
import type { WS_SUBTYPES } from "../app_constants";
|
|
import { DELETED_ELEMENT_TIMEOUT, ROOM_ID_BYTES } from "../app_constants";
|
|
|
|
export type SyncableExcalidrawElement = OrderedExcalidrawElement &
|
|
MakeBrand<"SyncableExcalidrawElement">;
|
|
|
|
export const isSyncableElement = (
|
|
element: OrderedExcalidrawElement,
|
|
): element is SyncableExcalidrawElement => {
|
|
if (element.isDeleted) {
|
|
if (element.updated > Date.now() - DELETED_ELEMENT_TIMEOUT) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
return !isInvisiblySmallElement(element);
|
|
};
|
|
|
|
export const getSyncableElements = (
|
|
elements: readonly OrderedExcalidrawElement[],
|
|
) =>
|
|
elements.filter((element) =>
|
|
isSyncableElement(element),
|
|
) as SyncableExcalidrawElement[];
|
|
|
|
const generateRoomId = async () => {
|
|
const buffer = new Uint8Array(ROOM_ID_BYTES);
|
|
window.crypto.getRandomValues(buffer);
|
|
return bytesToHexString(buffer);
|
|
};
|
|
|
|
export type EncryptedData = {
|
|
data: ArrayBuffer;
|
|
iv: Uint8Array;
|
|
};
|
|
|
|
export type SocketUpdateDataSource = {
|
|
INVALID_RESPONSE: {
|
|
type: WS_SUBTYPES.INVALID_RESPONSE;
|
|
};
|
|
SCENE_INIT: {
|
|
type: WS_SUBTYPES.INIT;
|
|
payload: {
|
|
elements: readonly ExcalidrawElement[];
|
|
};
|
|
};
|
|
SCENE_UPDATE: {
|
|
type: WS_SUBTYPES.UPDATE;
|
|
payload: {
|
|
elements: readonly ExcalidrawElement[];
|
|
};
|
|
};
|
|
MOUSE_LOCATION: {
|
|
type: WS_SUBTYPES.MOUSE_LOCATION;
|
|
payload: {
|
|
socketId: SocketId;
|
|
pointer: { x: number; y: number; tool: "pointer" | "laser" };
|
|
button: "down" | "up";
|
|
selectedElementIds: AppState["selectedElementIds"];
|
|
username: string;
|
|
};
|
|
};
|
|
USER_VISIBLE_SCENE_BOUNDS: {
|
|
type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS;
|
|
payload: {
|
|
socketId: SocketId;
|
|
username: string;
|
|
sceneBounds: SceneBounds;
|
|
};
|
|
};
|
|
IDLE_STATUS: {
|
|
type: WS_SUBTYPES.IDLE_STATUS;
|
|
payload: {
|
|
socketId: SocketId;
|
|
userState: UserIdleState;
|
|
username: string;
|
|
};
|
|
};
|
|
};
|
|
|
|
export type SocketUpdateDataIncoming =
|
|
SocketUpdateDataSource[keyof SocketUpdateDataSource];
|
|
|
|
export type SocketUpdateData =
|
|
SocketUpdateDataSource[keyof SocketUpdateDataSource] & {
|
|
_brand: "socketUpdateData";
|
|
};
|
|
|
|
const RE_COLLAB_LINK = /^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/;
|
|
|
|
export const isCollaborationLink = (link: string) => {
|
|
const hash = new URL(link).hash;
|
|
return RE_COLLAB_LINK.test(hash);
|
|
};
|
|
|
|
export const getCollaborationLinkData = (link: string) => {
|
|
const hash = new URL(link).hash;
|
|
const match = hash.match(RE_COLLAB_LINK);
|
|
if (match && match[2].length !== 22) {
|
|
window.alert(t("alerts.invalidEncryptionKey"));
|
|
return null;
|
|
}
|
|
return match ? { roomId: match[1], roomKey: match[2] } : null;
|
|
};
|
|
|
|
export const generateCollaborationLinkData = async () => {
|
|
const roomId = await generateRoomId();
|
|
const roomKey = await generateEncryptionKey();
|
|
|
|
if (!roomKey) {
|
|
throw new Error("Couldn't generate room key");
|
|
}
|
|
|
|
return { roomId, roomKey };
|
|
};
|
|
|
|
export const getCollaborationLink = (data: {
|
|
roomId: string;
|
|
roomKey: string;
|
|
}) => {
|
|
return `${window.location.origin}${window.location.pathname}#room=${data.roomId},${data.roomKey}`;
|
|
};
|
|
|
|
export const loadScene = async (
|
|
id: string | null,
|
|
privateKey: string | null,
|
|
// Supply local state even if importing from backend to ensure we restore
|
|
// localStorage user settings which we do not persist on server.
|
|
// Non-optional so we don't forget to pass it even if `undefined`.
|
|
localDataState: ImportedDataState | undefined | null,
|
|
) => {
|
|
const data = restore(localDataState || null, null, null, {
|
|
repairBindings: true,
|
|
});
|
|
|
|
return {
|
|
elements: data.elements,
|
|
appState: data.appState,
|
|
// note: this will always be empty because we're not storing files
|
|
// in the scene database/localStorage, and instead fetch them async
|
|
// from a different database
|
|
files: data.files,
|
|
};
|
|
};
|