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, }; };