|
@@ -37,7 +37,7 @@ import {
|
|
|
loadScene,
|
|
|
loadFromBlob,
|
|
|
SOCKET_SERVER,
|
|
|
- SocketUpdateData,
|
|
|
+ SocketUpdateDataSource,
|
|
|
} from "../data";
|
|
|
import { restore } from "../data/restore";
|
|
|
|
|
@@ -270,19 +270,18 @@ export class App extends React.Component<any, AppState> {
|
|
|
iv,
|
|
|
);
|
|
|
|
|
|
+ let deletedIds = this.state.deletedIds;
|
|
|
switch (decryptedData.type) {
|
|
|
case "INVALID_RESPONSE":
|
|
|
return;
|
|
|
case "SCENE_UPDATE":
|
|
|
const {
|
|
|
- elements: sceneElements,
|
|
|
- appState: sceneAppState,
|
|
|
+ elements: remoteElements,
|
|
|
+ appState: remoteAppState,
|
|
|
} = decryptedData.payload;
|
|
|
- const restoredState = restore(
|
|
|
- sceneElements || [],
|
|
|
- sceneAppState || getDefaultAppState(),
|
|
|
- { scrollToContent: true },
|
|
|
- );
|
|
|
+ const restoredState = restore(remoteElements || [], null, {
|
|
|
+ scrollToContent: true,
|
|
|
+ });
|
|
|
// Perform reconciliation - in collaboration, if we encounter
|
|
|
// elements with more staler versions than ours, ignore them
|
|
|
// and keep ours.
|
|
@@ -301,6 +300,23 @@ export class App extends React.Component<any, AppState> {
|
|
|
},
|
|
|
{},
|
|
|
);
|
|
|
+
|
|
|
+ deletedIds = { ...deletedIds };
|
|
|
+
|
|
|
+ for (const [id, remoteDeletedEl] of Object.entries(
|
|
|
+ remoteAppState.deletedIds,
|
|
|
+ )) {
|
|
|
+ if (
|
|
|
+ !localElementMap[id] ||
|
|
|
+ // don't remove local element if it's newer than the one
|
|
|
+ // deleted on remote
|
|
|
+ remoteDeletedEl.version >= localElementMap[id].version
|
|
|
+ ) {
|
|
|
+ deletedIds[id] = remoteDeletedEl;
|
|
|
+ delete localElementMap[id];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// Reconcile
|
|
|
elements = restoredState.elements
|
|
|
.reduce((elements, element) => {
|
|
@@ -320,26 +336,28 @@ export class App extends React.Component<any, AppState> {
|
|
|
localElementMap[element.id].version > element.version
|
|
|
) {
|
|
|
elements.push(localElementMap[element.id]);
|
|
|
+ delete localElementMap[element.id];
|
|
|
} else {
|
|
|
- elements.push(element);
|
|
|
+ if (deletedIds.hasOwnProperty(element.id)) {
|
|
|
+ if (element.version > deletedIds[element.id].version) {
|
|
|
+ elements.push(element);
|
|
|
+ delete deletedIds[element.id];
|
|
|
+ delete localElementMap[element.id];
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ elements.push(element);
|
|
|
+ delete localElementMap[element.id];
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
return elements;
|
|
|
}, [] as any)
|
|
|
- // add local elements that are currently being edited
|
|
|
- // (can't be done in the step above because the elements may
|
|
|
- // not exist on remote at all)
|
|
|
- .concat(
|
|
|
- elements.filter(element => {
|
|
|
- return (
|
|
|
- element.id === this.state.editingElement?.id ||
|
|
|
- element.id === this.state.resizingElement?.id ||
|
|
|
- element.id === this.state.draggingElement?.id
|
|
|
- );
|
|
|
- }),
|
|
|
- );
|
|
|
+ // add local elements that weren't deleted or on remote
|
|
|
+ .concat(...Object.values(localElementMap));
|
|
|
}
|
|
|
- this.setState({});
|
|
|
+ this.setState({
|
|
|
+ deletedIds,
|
|
|
+ });
|
|
|
if (this.socketInitialized === false) {
|
|
|
this.socketInitialized = true;
|
|
|
}
|
|
@@ -382,20 +400,58 @@ export class App extends React.Component<any, AppState> {
|
|
|
});
|
|
|
});
|
|
|
this.socket.on("new-user", async (socketID: string) => {
|
|
|
- this.broadcastSocketData({
|
|
|
- type: "SCENE_UPDATE",
|
|
|
- payload: {
|
|
|
- elements: elements.filter(element => {
|
|
|
- return element.id !== this.state.editingElement?.id;
|
|
|
- }),
|
|
|
- appState: this.state,
|
|
|
- },
|
|
|
- });
|
|
|
+ this.broadcastSceneUpdate();
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- private broadcastSocketData = async (data: SocketUpdateData) => {
|
|
|
+ private broadcastMouseLocation = (payload: {
|
|
|
+ pointerCoords: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointerCoords"];
|
|
|
+ }) => {
|
|
|
+ if (this.socket?.id) {
|
|
|
+ const data: SocketUpdateDataSource["MOUSE_LOCATION"] = {
|
|
|
+ type: "MOUSE_LOCATION",
|
|
|
+ payload: {
|
|
|
+ socketID: this.socket.id,
|
|
|
+ pointerCoords: payload.pointerCoords,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ return this._broadcastSocketData(
|
|
|
+ data as typeof data & { _brand: "socketUpdateData" },
|
|
|
+ );
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ private broadcastSceneUpdate = () => {
|
|
|
+ const deletedIds = { ...this.state.deletedIds };
|
|
|
+ const _elements = elements.filter(element => {
|
|
|
+ if (element.id in deletedIds) {
|
|
|
+ delete deletedIds[element.id];
|
|
|
+ }
|
|
|
+ return element.id !== this.state.editingElement?.id;
|
|
|
+ });
|
|
|
+ const data: SocketUpdateDataSource["SCENE_UPDATE"] = {
|
|
|
+ type: "SCENE_UPDATE",
|
|
|
+ payload: {
|
|
|
+ elements: _elements,
|
|
|
+ appState: {
|
|
|
+ viewBackgroundColor: this.state.viewBackgroundColor,
|
|
|
+ name: this.state.name,
|
|
|
+ deletedIds,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ };
|
|
|
+ return this._broadcastSocketData(
|
|
|
+ data as typeof data & { _brand: "socketUpdateData" },
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ // Low-level. Use type-specific broadcast* method.
|
|
|
+ private async _broadcastSocketData(
|
|
|
+ data: SocketUpdateDataSource[keyof SocketUpdateDataSource] & {
|
|
|
+ _brand: "socketUpdateData";
|
|
|
+ },
|
|
|
+ ) {
|
|
|
if (this.socketInitialized && this.socket && this.roomID && this.roomKey) {
|
|
|
const json = JSON.stringify(data);
|
|
|
const encoded = new TextEncoder().encode(json);
|
|
@@ -407,7 +463,7 @@ export class App extends React.Component<any, AppState> {
|
|
|
encrypted.iv,
|
|
|
);
|
|
|
}
|
|
|
- };
|
|
|
+ }
|
|
|
|
|
|
private unmounted = false;
|
|
|
public async componentDidMount() {
|
|
@@ -2128,14 +2184,7 @@ export class App extends React.Component<any, AppState> {
|
|
|
// sometimes the pointer goes off screen
|
|
|
return;
|
|
|
}
|
|
|
- this.socket &&
|
|
|
- this.broadcastSocketData({
|
|
|
- type: "MOUSE_LOCATION",
|
|
|
- payload: {
|
|
|
- socketID: this.socket.id,
|
|
|
- pointerCoords,
|
|
|
- },
|
|
|
- });
|
|
|
+ this.socket && this.broadcastMouseLocation({ pointerCoords });
|
|
|
};
|
|
|
|
|
|
private saveDebounced = debounce(() => {
|
|
@@ -2188,15 +2237,7 @@ export class App extends React.Component<any, AppState> {
|
|
|
}
|
|
|
this.saveDebounced();
|
|
|
if (history.isRecording()) {
|
|
|
- this.broadcastSocketData({
|
|
|
- type: "SCENE_UPDATE",
|
|
|
- payload: {
|
|
|
- elements: elements.filter(element => {
|
|
|
- return element.id !== this.state.editingElement?.id;
|
|
|
- }),
|
|
|
- appState: this.state,
|
|
|
- },
|
|
|
- });
|
|
|
+ this.broadcastSceneUpdate();
|
|
|
history.pushEntry(this.state, elements);
|
|
|
history.skipRecording();
|
|
|
}
|