save room to firebase on unload or portal close (#2207)

* save on unload or portal close

* align naming
This commit is contained in:
David Luzar 2020-10-06 04:34:40 +02:00 committed by GitHub
parent ae1ab1ab37
commit d18a72c879
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 76 additions and 24 deletions

View File

@ -176,7 +176,11 @@ import {
import { MaybeTransformHandleType } from "../element/transformHandles";
import { renderSpreadsheet } from "../charts";
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).
@ -469,7 +473,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
return false;
}
const roomId = roomMatch[1];
const roomID = roomMatch[1];
let collabForceLoadFlag;
try {
@ -488,7 +492,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
);
// if loading same room as the one previously unloaded within 15sec
// force reload without prompting
if (previousRoom === roomId && Date.now() - timestamp < 15000) {
if (previousRoom === roomID && Date.now() - timestamp < 15000) {
return true;
}
} catch {}
@ -784,7 +788,17 @@ class App extends React.Component<ExcalidrawProps, AppState> {
);
} 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();
// NOTE: modern browsers no longer allow showing a custom message here
event.returnValue = "";
@ -1182,6 +1196,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
};
closePortal = () => {
this.saveCollabRoomToFirebase();
window.history.pushState({}, "Excalidraw", window.location.origin);
this.destroySocketClient();
};
@ -1227,8 +1242,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
const roomMatch = getCollaborationLinkData(window.location.href);
if (roomMatch) {
const roomId = roomMatch[1];
const roomSecret = roomMatch[2];
const roomID = roomMatch[1];
const roomKey = roomMatch[2];
const initialize = () => {
this.portal.socketInitialized = true;
@ -1358,7 +1373,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
/* 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
this.portal.socket!.on(
@ -1430,7 +1445,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
});
try {
const elements = await loadFromFirebase(roomId, roomSecret);
const elements = await loadFromFirebase(roomID, roomKey);
if (elements) {
updateScene(
{ 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
broadcastScene = async (
sceneType: SCENE.INIT | SCENE.UPDATE,
@ -1530,16 +1557,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
data as SocketUpdateData,
);
if (syncAll && this.portal.roomID && this.portal.roomKey) {
if (syncAll && this.state.isCollaborating) {
await Promise.all([
broadcastPromise,
saveToFirebase(
this.portal.roomID,
this.portal.roomKey,
syncableElements,
).catch((e) => {
console.error(e);
}),
this.saveCollabRoomToFirebase(syncableElements),
]);
} else {
await broadcastPromise;

View File

@ -1,6 +1,7 @@
import { createIV, getImportedKey } from "./index";
import { ExcalidrawElement } from "../element/types";
import { getSceneVersion } from "../element";
import Portal from "../components/Portal";
let firebasePromise: Promise<typeof import("firebase/app")> | null = null;
@ -69,14 +70,40 @@ async function decryptElements(
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(
roomId: string,
roomSecret: string,
portal: Portal,
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 sceneVersion = getSceneVersion(elements);
const { ciphertext, iv } = await encryptElements(roomSecret, elements);
const { ciphertext, iv } = await encryptElements(roomKey, elements);
const nextDocData = {
sceneVersion,
@ -87,7 +114,7 @@ export async function saveToFirebase(
} as FirebaseStoredScene;
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 doc = await transaction.get(docRef);
if (!doc.exists) {
@ -104,17 +131,21 @@ export async function saveToFirebase(
return true;
});
if (didUpdate) {
firebaseSceneVersionCache.set(socket, sceneVersion);
}
return didUpdate;
}
export async function loadFromFirebase(
roomId: string,
roomSecret: string,
roomID: string,
roomKey: string,
): Promise<readonly ExcalidrawElement[] | null> {
const firebase = await getFirebase();
const db = firebase.firestore();
const docRef = db.collection("scenes").doc(roomId);
const docRef = db.collection("scenes").doc(roomID);
const doc = await docRef.get();
if (!doc.exists) {
return null;
@ -122,6 +153,6 @@ export async function loadFromFirebase(
const storedScene = doc.data() as FirebaseStoredScene;
const ciphertext = storedScene.ciphertext.toUint8Array();
const iv = storedScene.iv.toUint8Array();
const plaintext = await decryptElements(roomSecret, iv, ciphertext);
const plaintext = await decryptElements(roomKey, iv, ciphertext);
return plaintext;
}