Persistent rooms via Firebase (#2188)
* Periodically back up collaborative rooms in firebase * Responses to code review * comments from code review, new firebase credentials
This commit is contained in:
parent
f2135ab739
commit
d0985fe67a
1
.env
1
.env
@ -2,3 +2,4 @@ REACT_APP_BACKEND_V1_GET_URL=https://json.excalidraw.com/api/v1/
|
|||||||
REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
||||||
REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
||||||
REACT_APP_SOCKET_SERVER_URL=https://excalidraw-socket.herokuapp.com
|
REACT_APP_SOCKET_SERVER_URL=https://excalidraw-socket.herokuapp.com
|
||||||
|
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
||||||
|
5
firebase-project/.firebaserc
Normal file
5
firebase-project/.firebaserc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"default": "excalidraw-room-persistence"
|
||||||
|
}
|
||||||
|
}
|
66
firebase-project/.gitignore
vendored
Normal file
66
firebase-project/.gitignore
vendored
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
firebase-debug.log*
|
||||||
|
firebase-debug.*.log*
|
||||||
|
|
||||||
|
# Firebase cache
|
||||||
|
.firebase/
|
||||||
|
|
||||||
|
# Firebase config
|
||||||
|
|
||||||
|
# Uncomment this if you'd like others to create their own Firebase project.
|
||||||
|
# For a team working on the same Firebase project(s), it is recommended to leave
|
||||||
|
# it commented so all members can deploy to the same project(s) in .firebaserc.
|
||||||
|
# .firebaserc
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
6
firebase-project/firebase.json
Normal file
6
firebase-project/firebase.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"firestore": {
|
||||||
|
"rules": "firestore.rules",
|
||||||
|
"indexes": "firestore.indexes.json"
|
||||||
|
}
|
||||||
|
}
|
4
firebase-project/firestore.indexes.json
Normal file
4
firebase-project/firestore.indexes.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"indexes": [],
|
||||||
|
"fieldOverrides": []
|
||||||
|
}
|
10
firebase-project/firestore.rules
Normal file
10
firebase-project/firestore.rules
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
rules_version = '2';
|
||||||
|
service cloud.firestore {
|
||||||
|
match /databases/{database}/documents {
|
||||||
|
match /{document=**} {
|
||||||
|
allow get, write: if true;
|
||||||
|
// never set this to true, otherwise anyone can delete anyone else's drawing.
|
||||||
|
allow list: if false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4137
package-lock.json
generated
4137
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -29,6 +29,7 @@
|
|||||||
"@types/react-dom": "16.9.8",
|
"@types/react-dom": "16.9.8",
|
||||||
"@types/socket.io-client": "1.4.33",
|
"@types/socket.io-client": "1.4.33",
|
||||||
"browser-nativefs": "0.10.3",
|
"browser-nativefs": "0.10.3",
|
||||||
|
"firebase": "7.21.1",
|
||||||
"i18next-browser-languagedetector": "6.0.1",
|
"i18next-browser-languagedetector": "6.0.1",
|
||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
"nanoid": "2.1.11",
|
"nanoid": "2.1.11",
|
||||||
@ -49,6 +50,7 @@
|
|||||||
"eslint": "6.8.0",
|
"eslint": "6.8.0",
|
||||||
"eslint-config-prettier": "6.12.0",
|
"eslint-config-prettier": "6.12.0",
|
||||||
"eslint-plugin-prettier": "3.1.4",
|
"eslint-plugin-prettier": "3.1.4",
|
||||||
|
"firebase-tools": "8.11.2",
|
||||||
"husky": "4.3.0",
|
"husky": "4.3.0",
|
||||||
"jest-canvas-mock": "2.2.0",
|
"jest-canvas-mock": "2.2.0",
|
||||||
"lint-staged": "10.4.0",
|
"lint-staged": "10.4.0",
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
getPerfectElementSize,
|
getPerfectElementSize,
|
||||||
getNormalizedDimensions,
|
getNormalizedDimensions,
|
||||||
getElementMap,
|
getElementMap,
|
||||||
getDrawingVersion,
|
getSceneVersion,
|
||||||
getSyncableElements,
|
getSyncableElements,
|
||||||
newLinearElement,
|
newLinearElement,
|
||||||
transformElements,
|
transformElements,
|
||||||
@ -176,6 +176,7 @@ import {
|
|||||||
import { MaybeTransformHandleType } from "../element/transformHandles";
|
import { MaybeTransformHandleType } from "../element/transformHandles";
|
||||||
import { renderSpreadsheet } from "../charts";
|
import { renderSpreadsheet } from "../charts";
|
||||||
import { isValidLibrary } from "../data/json";
|
import { isValidLibrary } from "../data/json";
|
||||||
|
import { loadFromFirebase, saveToFirebase } from "../data/firebase";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param func handler taking at most single parameter (event).
|
* @param func handler taking at most single parameter (event).
|
||||||
@ -468,6 +469,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const roomId = roomMatch[1];
|
||||||
|
|
||||||
let collabForceLoadFlag;
|
let collabForceLoadFlag;
|
||||||
try {
|
try {
|
||||||
collabForceLoadFlag = localStorage?.getItem(
|
collabForceLoadFlag = localStorage?.getItem(
|
||||||
@ -485,7 +488,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
);
|
);
|
||||||
// if loading same room as the one previously unloaded within 15sec
|
// if loading same room as the one previously unloaded within 15sec
|
||||||
// force reload without prompting
|
// force reload without prompting
|
||||||
if (previousRoom === roomMatch[1] && Date.now() - timestamp < 15000) {
|
if (previousRoom === roomId && Date.now() - timestamp < 15000) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
@ -902,7 +905,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
getDrawingVersion(this.scene.getElementsIncludingDeleted()) >
|
getSceneVersion(this.scene.getElementsIncludingDeleted()) >
|
||||||
this.lastBroadcastedOrReceivedSceneVersion
|
this.lastBroadcastedOrReceivedSceneVersion
|
||||||
) {
|
) {
|
||||||
this.broadcastScene(SCENE.UPDATE, /* syncAll */ false);
|
this.broadcastScene(SCENE.UPDATE, /* syncAll */ false);
|
||||||
@ -1210,6 +1213,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
const roomMatch = getCollaborationLinkData(window.location.href);
|
const roomMatch = getCollaborationLinkData(window.location.href);
|
||||||
if (roomMatch) {
|
if (roomMatch) {
|
||||||
|
const roomId = roomMatch[1];
|
||||||
|
const roomSecret = roomMatch[2];
|
||||||
|
|
||||||
const initialize = () => {
|
const initialize = () => {
|
||||||
this.portal.socketInitialized = true;
|
this.portal.socketInitialized = true;
|
||||||
clearTimeout(initializationTimer);
|
clearTimeout(initializationTimer);
|
||||||
@ -1226,12 +1232,18 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
|
|
||||||
const updateScene = (
|
const updateScene = (
|
||||||
decryptedData: SocketUpdateDataSource[SCENE.INIT | SCENE.UPDATE],
|
decryptedData: SocketUpdateDataSource[SCENE.INIT | SCENE.UPDATE],
|
||||||
{ init = false }: { init?: boolean } = {},
|
{
|
||||||
|
init = false,
|
||||||
|
initFromSnapshot = false,
|
||||||
|
}: { init?: boolean; initFromSnapshot?: boolean } = {},
|
||||||
) => {
|
) => {
|
||||||
const { elements: remoteElements } = decryptedData.payload;
|
const { elements: remoteElements } = decryptedData.payload;
|
||||||
|
|
||||||
if (init) {
|
if (init) {
|
||||||
history.resumeRecording();
|
history.resumeRecording();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (init || initFromSnapshot) {
|
||||||
this.setState({
|
this.setState({
|
||||||
...this.state,
|
...this.state,
|
||||||
...calculateScrollCenter(
|
...calculateScrollCenter(
|
||||||
@ -1311,7 +1323,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
// we just received!
|
// we just received!
|
||||||
// Note: this needs to be set before replaceAllElements as it
|
// Note: this needs to be set before replaceAllElements as it
|
||||||
// syncronously calls render.
|
// syncronously calls render.
|
||||||
this.lastBroadcastedOrReceivedSceneVersion = getDrawingVersion(
|
this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(
|
||||||
newElements,
|
newElements,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1323,7 +1335,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
// undo, a user makes a change, and then try to redo, your element(s) will be lost. However,
|
// undo, a user makes a change, and then try to redo, your element(s) will be lost. However,
|
||||||
// right now we think this is the right tradeoff.
|
// right now we think this is the right tradeoff.
|
||||||
history.clear();
|
history.clear();
|
||||||
if (!this.portal.socketInitialized) {
|
if (!this.portal.socketInitialized && !initFromSnapshot) {
|
||||||
initialize();
|
initialize();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1332,11 +1344,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
||||||
);
|
);
|
||||||
|
|
||||||
this.portal.open(
|
this.portal.open(socketIOClient(SOCKET_SERVER), roomId, roomSecret);
|
||||||
socketIOClient(SOCKET_SERVER),
|
|
||||||
roomMatch[1],
|
|
||||||
roomMatch[2],
|
|
||||||
);
|
|
||||||
|
|
||||||
// All socket listeners are moving to Portal
|
// All socket listeners are moving to Portal
|
||||||
this.portal.socket!.on(
|
this.portal.socket!.on(
|
||||||
@ -1406,6 +1414,19 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
isCollaborating: true,
|
isCollaborating: true,
|
||||||
isLoading: opts.showLoadingState ? true : this.state.isLoading,
|
isLoading: opts.showLoadingState ? true : this.state.isLoading,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const elements = await loadFromFirebase(roomId, roomSecret);
|
||||||
|
if (elements) {
|
||||||
|
updateScene(
|
||||||
|
{ type: "SCENE_UPDATE", payload: { elements } },
|
||||||
|
{ initFromSnapshot: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// log the error and move on. other peers will sync us the scene.
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1450,7 +1471,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// maybe should move to Portal
|
// maybe should move to Portal
|
||||||
broadcastScene = (sceneType: SCENE.INIT | SCENE.UPDATE, syncAll: boolean) => {
|
broadcastScene = async (
|
||||||
|
sceneType: SCENE.INIT | SCENE.UPDATE,
|
||||||
|
syncAll: boolean,
|
||||||
|
) => {
|
||||||
if (sceneType === SCENE.INIT && !syncAll) {
|
if (sceneType === SCENE.INIT && !syncAll) {
|
||||||
throw new Error("syncAll must be true when sending SCENE.INIT");
|
throw new Error("syncAll must be true when sending SCENE.INIT");
|
||||||
}
|
}
|
||||||
@ -1479,7 +1503,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
};
|
};
|
||||||
this.lastBroadcastedOrReceivedSceneVersion = Math.max(
|
this.lastBroadcastedOrReceivedSceneVersion = Math.max(
|
||||||
this.lastBroadcastedOrReceivedSceneVersion,
|
this.lastBroadcastedOrReceivedSceneVersion,
|
||||||
getDrawingVersion(this.scene.getElementsIncludingDeleted()),
|
getSceneVersion(this.scene.getElementsIncludingDeleted()),
|
||||||
);
|
);
|
||||||
for (const syncableElement of syncableElements) {
|
for (const syncableElement of syncableElements) {
|
||||||
this.broadcastedElementVersions.set(
|
this.broadcastedElementVersions.set(
|
||||||
@ -1487,7 +1511,25 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
syncableElement.version,
|
syncableElement.version,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.portal._broadcastSocketData(data as SocketUpdateData);
|
|
||||||
|
const broadcastPromise = this.portal._broadcastSocketData(
|
||||||
|
data as SocketUpdateData,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (syncAll && this.portal.roomID && this.portal.roomKey) {
|
||||||
|
await Promise.all([
|
||||||
|
broadcastPromise,
|
||||||
|
saveToFirebase(
|
||||||
|
this.portal.roomID,
|
||||||
|
this.portal.roomKey,
|
||||||
|
syncableElements,
|
||||||
|
).catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
await broadcastPromise;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onSceneUpdated = () => {
|
private onSceneUpdated = () => {
|
||||||
|
127
src/data/firebase.ts
Normal file
127
src/data/firebase.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { createIV, getImportedKey } from "./index";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { getSceneVersion } from "../element";
|
||||||
|
|
||||||
|
let firebasePromise: Promise<typeof import("firebase/app")> | null = null;
|
||||||
|
|
||||||
|
async function loadFirebase() {
|
||||||
|
const firebase = await import("firebase/app");
|
||||||
|
await import("firebase/firestore");
|
||||||
|
|
||||||
|
const firebaseConfig = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
|
||||||
|
firebase.initializeApp(firebaseConfig);
|
||||||
|
|
||||||
|
return firebase;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFirebase(): Promise<typeof import("firebase/app")> {
|
||||||
|
if (!firebasePromise) {
|
||||||
|
firebasePromise = loadFirebase();
|
||||||
|
}
|
||||||
|
const firebase = await firebasePromise!;
|
||||||
|
return firebase;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FirebaseStoredScene {
|
||||||
|
sceneVersion: number;
|
||||||
|
iv: firebase.firestore.Blob;
|
||||||
|
ciphertext: firebase.firestore.Blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function encryptElements(
|
||||||
|
key: string,
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
): Promise<{ ciphertext: ArrayBuffer; iv: Uint8Array }> {
|
||||||
|
const importedKey = await getImportedKey(key, "encrypt");
|
||||||
|
const iv = createIV();
|
||||||
|
const json = JSON.stringify(elements);
|
||||||
|
const encoded = new TextEncoder().encode(json);
|
||||||
|
const ciphertext = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: "AES-GCM",
|
||||||
|
iv,
|
||||||
|
},
|
||||||
|
importedKey,
|
||||||
|
encoded,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ciphertext, iv };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decryptElements(
|
||||||
|
key: string,
|
||||||
|
iv: Uint8Array,
|
||||||
|
ciphertext: ArrayBuffer,
|
||||||
|
): Promise<readonly ExcalidrawElement[]> {
|
||||||
|
const importedKey = await getImportedKey(key, "decrypt");
|
||||||
|
const decrypted = await window.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: "AES-GCM",
|
||||||
|
iv,
|
||||||
|
},
|
||||||
|
importedKey,
|
||||||
|
ciphertext,
|
||||||
|
);
|
||||||
|
|
||||||
|
const decodedData = new TextDecoder("utf-8").decode(
|
||||||
|
new Uint8Array(decrypted) as any,
|
||||||
|
);
|
||||||
|
return JSON.parse(decodedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveToFirebase(
|
||||||
|
roomId: string,
|
||||||
|
roomSecret: string,
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
) {
|
||||||
|
const firebase = await getFirebase();
|
||||||
|
const sceneVersion = getSceneVersion(elements);
|
||||||
|
const { ciphertext, iv } = await encryptElements(roomSecret, elements);
|
||||||
|
|
||||||
|
const nextDocData = {
|
||||||
|
sceneVersion,
|
||||||
|
ciphertext: firebase.firestore.Blob.fromUint8Array(
|
||||||
|
new Uint8Array(ciphertext),
|
||||||
|
),
|
||||||
|
iv: firebase.firestore.Blob.fromUint8Array(iv),
|
||||||
|
} as FirebaseStoredScene;
|
||||||
|
|
||||||
|
const db = firebase.firestore();
|
||||||
|
const docRef = db.collection("scenes").doc(roomId);
|
||||||
|
const didUpdate = await db.runTransaction(async (transaction) => {
|
||||||
|
const doc = await transaction.get(docRef);
|
||||||
|
if (!doc.exists) {
|
||||||
|
transaction.set(docRef, nextDocData);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevDocData = doc.data() as FirebaseStoredScene;
|
||||||
|
if (prevDocData.sceneVersion >= nextDocData.sceneVersion) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.update(docRef, nextDocData);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return didUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadFromFirebase(
|
||||||
|
roomId: string,
|
||||||
|
roomSecret: string,
|
||||||
|
): Promise<readonly ExcalidrawElement[] | null> {
|
||||||
|
const firebase = await getFirebase();
|
||||||
|
const db = firebase.firestore();
|
||||||
|
|
||||||
|
const docRef = db.collection("scenes").doc(roomId);
|
||||||
|
const doc = await docRef.get();
|
||||||
|
if (!doc.exists) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const storedScene = doc.data() as FirebaseStoredScene;
|
||||||
|
const ciphertext = storedScene.ciphertext.toUint8Array();
|
||||||
|
const iv = storedScene.iv.toUint8Array();
|
||||||
|
const plaintext = await decryptElements(roomSecret, iv, ciphertext);
|
||||||
|
return plaintext;
|
||||||
|
}
|
@ -89,7 +89,7 @@ const generateEncryptionKey = async () => {
|
|||||||
return (await window.crypto.subtle.exportKey("jwk", key)).k;
|
return (await window.crypto.subtle.exportKey("jwk", key)).k;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createIV = () => {
|
export const createIV = () => {
|
||||||
const arr = new Uint8Array(12);
|
const arr = new Uint8Array(12);
|
||||||
return window.crypto.getRandomValues(arr);
|
return window.crypto.getRandomValues(arr);
|
||||||
};
|
};
|
||||||
@ -108,7 +108,7 @@ export const generateCollaborationLink = async () => {
|
|||||||
return `${window.location.origin}${window.location.pathname}#room=${id},${key}`;
|
return `${window.location.origin}${window.location.pathname}#room=${id},${key}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getImportedKey = (key: string, usage: KeyUsage) =>
|
export const getImportedKey = (key: string, usage: KeyUsage) =>
|
||||||
window.crypto.subtle.importKey(
|
window.crypto.subtle.importKey(
|
||||||
"jwk",
|
"jwk",
|
||||||
{
|
{
|
||||||
|
@ -31,7 +31,7 @@ const restoreElementWithProperties = <T extends ExcalidrawElement>(
|
|||||||
): T => {
|
): T => {
|
||||||
const base: Pick<T, keyof ExcalidrawElement> = {
|
const base: Pick<T, keyof ExcalidrawElement> = {
|
||||||
type: element.type,
|
type: element.type,
|
||||||
// all elements must have version > 0 so getDrawingVersion() will pick up
|
// all elements must have version > 0 so getSceneVersion() will pick up
|
||||||
// newly added elements
|
// newly added elements
|
||||||
version: element.version || 1,
|
version: element.version || 1,
|
||||||
versionNonce: element.versionNonce ?? 0,
|
versionNonce: element.versionNonce ?? 0,
|
||||||
|
@ -74,7 +74,7 @@ export const getElementMap = (elements: readonly ExcalidrawElement[]) =>
|
|||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getDrawingVersion = (elements: readonly ExcalidrawElement[]) =>
|
export const getSceneVersion = (elements: readonly ExcalidrawElement[]) =>
|
||||||
elements.reduce((acc, el) => acc + el.version, 0);
|
elements.reduce((acc, el) => acc + el.version, 0);
|
||||||
|
|
||||||
export const getNonDeletedElements = (elements: readonly ExcalidrawElement[]) =>
|
export const getNonDeletedElements = (elements: readonly ExcalidrawElement[]) =>
|
||||||
|
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
@ -20,6 +20,7 @@ declare namespace NodeJS {
|
|||||||
readonly REACT_APP_BACKEND_V2_GET_URL: string;
|
readonly REACT_APP_BACKEND_V2_GET_URL: string;
|
||||||
readonly REACT_APP_BACKEND_V2_POST_URL: string;
|
readonly REACT_APP_BACKEND_V2_POST_URL: string;
|
||||||
readonly REACT_APP_SOCKET_SERVER_URL: string;
|
readonly REACT_APP_SOCKET_SERVER_URL: string;
|
||||||
|
readonly REACT_APP_FIREBASE_CONFIG: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user