save room to firebase on unload or portal close (#2207)
* save on unload or portal close * align naming
This commit is contained in:
parent
ae1ab1ab37
commit
d18a72c879
@ -176,7 +176,11 @@ 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";
|
import {
|
||||||
|
loadFromFirebase,
|
||||||
|
saveToFirebase,
|
||||||
|
isSavedToFirebase,
|
||||||
|
} from "../data/firebase";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param func handler taking at most single parameter (event).
|
* @param func handler taking at most single parameter (event).
|
||||||
@ -469,7 +473,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const roomId = roomMatch[1];
|
const roomID = roomMatch[1];
|
||||||
|
|
||||||
let collabForceLoadFlag;
|
let collabForceLoadFlag;
|
||||||
try {
|
try {
|
||||||
@ -488,7 +492,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 === roomId && Date.now() - timestamp < 15000) {
|
if (previousRoom === roomID && Date.now() - timestamp < 15000) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
@ -784,7 +788,17 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
);
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
if (this.state.isCollaborating && this.scene.getElements().length > 0) {
|
const syncableElements = getSyncableElements(
|
||||||
|
this.scene.getElementsIncludingDeleted(),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
this.state.isCollaborating &&
|
||||||
|
!isSavedToFirebase(this.portal, syncableElements)
|
||||||
|
) {
|
||||||
|
// this won't run in time if user decides to leave the site, but
|
||||||
|
// the purpose is to run in immediately after user decides to stay
|
||||||
|
this.saveCollabRoomToFirebase(syncableElements);
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// NOTE: modern browsers no longer allow showing a custom message here
|
// NOTE: modern browsers no longer allow showing a custom message here
|
||||||
event.returnValue = "";
|
event.returnValue = "";
|
||||||
@ -1182,6 +1196,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
closePortal = () => {
|
closePortal = () => {
|
||||||
|
this.saveCollabRoomToFirebase();
|
||||||
window.history.pushState({}, "Excalidraw", window.location.origin);
|
window.history.pushState({}, "Excalidraw", window.location.origin);
|
||||||
this.destroySocketClient();
|
this.destroySocketClient();
|
||||||
};
|
};
|
||||||
@ -1227,8 +1242,8 @@ 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 roomID = roomMatch[1];
|
||||||
const roomSecret = roomMatch[2];
|
const roomKey = roomMatch[2];
|
||||||
|
|
||||||
const initialize = () => {
|
const initialize = () => {
|
||||||
this.portal.socketInitialized = true;
|
this.portal.socketInitialized = true;
|
||||||
@ -1358,7 +1373,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
||||||
);
|
);
|
||||||
|
|
||||||
this.portal.open(socketIOClient(SOCKET_SERVER), roomId, roomSecret);
|
this.portal.open(socketIOClient(SOCKET_SERVER), roomID, roomKey);
|
||||||
|
|
||||||
// All socket listeners are moving to Portal
|
// All socket listeners are moving to Portal
|
||||||
this.portal.socket!.on(
|
this.portal.socket!.on(
|
||||||
@ -1430,7 +1445,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const elements = await loadFromFirebase(roomId, roomSecret);
|
const elements = await loadFromFirebase(roomID, roomKey);
|
||||||
if (elements) {
|
if (elements) {
|
||||||
updateScene(
|
updateScene(
|
||||||
{ type: "SCENE_UPDATE", payload: { elements } },
|
{ type: "SCENE_UPDATE", payload: { elements } },
|
||||||
@ -1484,6 +1499,18 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
saveCollabRoomToFirebase = async (
|
||||||
|
syncableElements: ExcalidrawElement[] = getSyncableElements(
|
||||||
|
this.scene.getElementsIncludingDeleted(),
|
||||||
|
),
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await saveToFirebase(this.portal, syncableElements);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// maybe should move to Portal
|
// maybe should move to Portal
|
||||||
broadcastScene = async (
|
broadcastScene = async (
|
||||||
sceneType: SCENE.INIT | SCENE.UPDATE,
|
sceneType: SCENE.INIT | SCENE.UPDATE,
|
||||||
@ -1530,16 +1557,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
data as SocketUpdateData,
|
data as SocketUpdateData,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (syncAll && this.portal.roomID && this.portal.roomKey) {
|
if (syncAll && this.state.isCollaborating) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
broadcastPromise,
|
broadcastPromise,
|
||||||
saveToFirebase(
|
this.saveCollabRoomToFirebase(syncableElements),
|
||||||
this.portal.roomID,
|
|
||||||
this.portal.roomKey,
|
|
||||||
syncableElements,
|
|
||||||
).catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
await broadcastPromise;
|
await broadcastPromise;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { createIV, getImportedKey } from "./index";
|
import { createIV, getImportedKey } from "./index";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { getSceneVersion } from "../element";
|
import { getSceneVersion } from "../element";
|
||||||
|
import Portal from "../components/Portal";
|
||||||
|
|
||||||
let firebasePromise: Promise<typeof import("firebase/app")> | null = null;
|
let firebasePromise: Promise<typeof import("firebase/app")> | null = null;
|
||||||
|
|
||||||
@ -69,14 +70,40 @@ async function decryptElements(
|
|||||||
return JSON.parse(decodedData);
|
return JSON.parse(decodedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const firebaseSceneVersionCache = new WeakMap<SocketIOClient.Socket, number>();
|
||||||
|
|
||||||
|
export const isSavedToFirebase = (
|
||||||
|
portal: Portal,
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
): boolean => {
|
||||||
|
if (portal.socket && portal.roomID && portal.roomKey) {
|
||||||
|
const sceneVersion = getSceneVersion(elements);
|
||||||
|
return firebaseSceneVersionCache.get(portal.socket) === sceneVersion;
|
||||||
|
}
|
||||||
|
// if no room exists, consider the room saved so that we don't unnecessarily
|
||||||
|
// prevent unload (there's nothing we could do at that point anyway)
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
export async function saveToFirebase(
|
export async function saveToFirebase(
|
||||||
roomId: string,
|
portal: Portal,
|
||||||
roomSecret: string,
|
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
) {
|
) {
|
||||||
|
const { roomID, roomKey, socket } = portal;
|
||||||
|
if (
|
||||||
|
// if no room exists, consider the room saved because there's nothing we can
|
||||||
|
// do at this point
|
||||||
|
!roomID ||
|
||||||
|
!roomKey ||
|
||||||
|
!socket ||
|
||||||
|
isSavedToFirebase(portal, elements)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const firebase = await getFirebase();
|
const firebase = await getFirebase();
|
||||||
const sceneVersion = getSceneVersion(elements);
|
const sceneVersion = getSceneVersion(elements);
|
||||||
const { ciphertext, iv } = await encryptElements(roomSecret, elements);
|
const { ciphertext, iv } = await encryptElements(roomKey, elements);
|
||||||
|
|
||||||
const nextDocData = {
|
const nextDocData = {
|
||||||
sceneVersion,
|
sceneVersion,
|
||||||
@ -87,7 +114,7 @@ export async function saveToFirebase(
|
|||||||
} as FirebaseStoredScene;
|
} as FirebaseStoredScene;
|
||||||
|
|
||||||
const db = firebase.firestore();
|
const db = firebase.firestore();
|
||||||
const docRef = db.collection("scenes").doc(roomId);
|
const docRef = db.collection("scenes").doc(roomID);
|
||||||
const didUpdate = await db.runTransaction(async (transaction) => {
|
const didUpdate = await db.runTransaction(async (transaction) => {
|
||||||
const doc = await transaction.get(docRef);
|
const doc = await transaction.get(docRef);
|
||||||
if (!doc.exists) {
|
if (!doc.exists) {
|
||||||
@ -104,17 +131,21 @@ export async function saveToFirebase(
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (didUpdate) {
|
||||||
|
firebaseSceneVersionCache.set(socket, sceneVersion);
|
||||||
|
}
|
||||||
|
|
||||||
return didUpdate;
|
return didUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadFromFirebase(
|
export async function loadFromFirebase(
|
||||||
roomId: string,
|
roomID: string,
|
||||||
roomSecret: string,
|
roomKey: string,
|
||||||
): Promise<readonly ExcalidrawElement[] | null> {
|
): Promise<readonly ExcalidrawElement[] | null> {
|
||||||
const firebase = await getFirebase();
|
const firebase = await getFirebase();
|
||||||
const db = firebase.firestore();
|
const db = firebase.firestore();
|
||||||
|
|
||||||
const docRef = db.collection("scenes").doc(roomId);
|
const docRef = db.collection("scenes").doc(roomID);
|
||||||
const doc = await docRef.get();
|
const doc = await docRef.get();
|
||||||
if (!doc.exists) {
|
if (!doc.exists) {
|
||||||
return null;
|
return null;
|
||||||
@ -122,6 +153,6 @@ export async function loadFromFirebase(
|
|||||||
const storedScene = doc.data() as FirebaseStoredScene;
|
const storedScene = doc.data() as FirebaseStoredScene;
|
||||||
const ciphertext = storedScene.ciphertext.toUint8Array();
|
const ciphertext = storedScene.ciphertext.toUint8Array();
|
||||||
const iv = storedScene.iv.toUint8Array();
|
const iv = storedScene.iv.toUint8Array();
|
||||||
const plaintext = await decryptElements(roomSecret, iv, ciphertext);
|
const plaintext = await decryptElements(roomKey, iv, ciphertext);
|
||||||
return plaintext;
|
return plaintext;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user