Begin extracting collaboration code to Portal (#1306)

This commit is contained in:
Kent Beck 2020-04-07 15:29:43 -07:00 committed by GitHub
parent ed378170b7
commit 9a1af38c97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -142,13 +142,33 @@ const gesture: Gesture = {
initialScale: null, initialScale: null,
}; };
export class App extends React.Component<any, AppState> { class Portal {
canvas: HTMLCanvasElement | null = null;
rc: RoughCanvas | null = null;
socket: SocketIOClient.Socket | null = null; socket: SocketIOClient.Socket | null = null;
socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initalized socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initalized
roomID: string | null = null; roomID: string | null = null;
roomKey: string | null = null; roomKey: string | null = null;
open(socket: SocketIOClient.Socket, id: string, key: string) {
this.socket = socket;
this.roomID = id;
this.roomKey = key;
}
close() {
if (!this.socket) {
return;
}
this.socket.close();
this.socket = null;
this.roomID = null;
this.roomKey = null;
}
}
export class App extends React.Component<any, AppState> {
canvas: HTMLCanvasElement | null = null;
rc: RoughCanvas | null = null;
room: Portal = new Portal();
lastBroadcastedOrReceivedSceneVersion: number = -1; lastBroadcastedOrReceivedSceneVersion: number = -1;
removeSceneCallback: SceneStateCallbackRemover | null = null; removeSceneCallback: SceneStateCallbackRemover | null = null;
@ -193,8 +213,8 @@ export class App extends React.Component<any, AppState> {
return !element.isDeleted; return !element.isDeleted;
})} })}
setElements={this.setElements} setElements={this.setElements}
onRoomCreate={this.createRoom} onRoomCreate={this.openPortal}
onRoomDestroy={this.destroyRoom} onRoomDestroy={this.closePortal}
onLockToggle={this.toggleLock} onLockToggle={this.toggleLock}
/> />
<main> <main>
@ -428,7 +448,7 @@ export class App extends React.Component<any, AppState> {
}); });
componentDidUpdate() { componentDidUpdate() {
if (this.state.isCollaborating && !this.socket) { if (this.state.isCollaborating && !this.room.socket) {
this.initializeSocketClient({ showLoadingState: true }); this.initializeSocketClient({ showLoadingState: true });
} }
@ -700,7 +720,7 @@ export class App extends React.Component<any, AppState> {
gesture.pointers.delete(event.pointerId); gesture.pointers.delete(event.pointerId);
}; };
createRoom = async () => { openPortal = async () => {
window.history.pushState( window.history.pushState(
{}, {},
"Excalidraw", "Excalidraw",
@ -709,7 +729,7 @@ export class App extends React.Component<any, AppState> {
this.initializeSocketClient({ showLoadingState: false }); this.initializeSocketClient({ showLoadingState: false });
}; };
destroyRoom = () => { closePortal = () => {
window.history.pushState({}, "Excalidraw", window.location.origin); window.history.pushState({}, "Excalidraw", window.location.origin);
this.destroySocketClient(); this.destroySocketClient();
}; };
@ -728,22 +748,17 @@ export class App extends React.Component<any, AppState> {
isCollaborating: false, isCollaborating: false,
collaborators: new Map(), collaborators: new Map(),
}); });
if (this.socket) { this.room.close();
this.socket.close();
this.socket = null;
this.roomID = null;
this.roomKey = null;
}
}; };
private initializeSocketClient = (opts: { showLoadingState: boolean }) => { private initializeSocketClient = (opts: { showLoadingState: boolean }) => {
if (this.socket) { if (this.room.socket) {
return; return;
} }
const roomMatch = getCollaborationLinkData(window.location.href); const roomMatch = getCollaborationLinkData(window.location.href);
if (roomMatch) { if (roomMatch) {
const initialize = () => { const initialize = () => {
this.socketInitialized = true; this.room.socketInitialized = true;
clearTimeout(initializationTimer); clearTimeout(initializationTimer);
if (this.state.isLoading && !this.unmounted) { if (this.state.isLoading && !this.unmounted) {
this.setState({ isLoading: false }); this.setState({ isLoading: false });
@ -849,26 +864,26 @@ export class App extends React.Component<any, 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.socketInitialized === false) { if (this.room.socketInitialized === false) {
initialize(); initialize();
} }
}; };
this.socket = socketIOClient(SOCKET_SERVER); this.room.open(socketIOClient(SOCKET_SERVER), roomMatch[1], roomMatch[2]);
this.roomID = roomMatch[1];
this.roomKey = roomMatch[2]; this.room.socket!.on("init-room", () => {
this.socket.on("init-room", () => { this.room.socket &&
this.socket && this.socket.emit("join-room", this.roomID); this.room.socket.emit("join-room", this.room.roomID);
}); });
this.socket.on( this.room.socket!.on(
"client-broadcast", "client-broadcast",
async (encryptedData: ArrayBuffer, iv: Uint8Array) => { async (encryptedData: ArrayBuffer, iv: Uint8Array) => {
if (!this.roomKey) { if (!this.room.roomKey) {
return; return;
} }
const decryptedData = await decryptAESGEM( const decryptedData = await decryptAESGEM(
encryptedData, encryptedData,
this.roomKey, this.room.roomKey,
iv, iv,
); );
@ -876,7 +891,7 @@ export class App extends React.Component<any, AppState> {
case "INVALID_RESPONSE": case "INVALID_RESPONSE":
return; return;
case "SCENE_INIT": { case "SCENE_INIT": {
if (!this.socketInitialized) { if (!this.room.socketInitialized) {
updateScene(decryptedData, { scrollToContent: true }); updateScene(decryptedData, { scrollToContent: true });
} }
break; break;
@ -909,13 +924,13 @@ export class App extends React.Component<any, AppState> {
} }
}, },
); );
this.socket.on("first-in-room", () => { this.room.socket!.on("first-in-room", () => {
if (this.socket) { if (this.room.socket) {
this.socket.off("first-in-room"); this.room.socket.off("first-in-room");
} }
initialize(); initialize();
}); });
this.socket.on("room-user-change", (clients: string[]) => { this.room.socket!.on("room-user-change", (clients: string[]) => {
this.setState((state) => { this.setState((state) => {
const collaborators: typeof state.collaborators = new Map(); const collaborators: typeof state.collaborators = new Map();
for (const socketID of clients) { for (const socketID of clients) {
@ -931,7 +946,7 @@ export class App extends React.Component<any, AppState> {
}; };
}); });
}); });
this.socket.on("new-user", async (_socketID: string) => { this.room.socket!.on("new-user", async (_socketID: string) => {
this.broadcastScene("SCENE_INIT"); this.broadcastScene("SCENE_INIT");
}); });
@ -946,11 +961,11 @@ export class App extends React.Component<any, AppState> {
pointerCoords: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointerCoords"]; pointerCoords: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointerCoords"];
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"]; button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
}) => { }) => {
if (this.socket?.id) { if (this.room.socket?.id) {
const data: SocketUpdateDataSource["MOUSE_LOCATION"] = { const data: SocketUpdateDataSource["MOUSE_LOCATION"] = {
type: "MOUSE_LOCATION", type: "MOUSE_LOCATION",
payload: { payload: {
socketID: this.socket.id, socketID: this.room.socket.id,
pointerCoords: payload.pointerCoords, pointerCoords: payload.pointerCoords,
button: payload.button || "up", button: payload.button || "up",
selectedElementIds: this.state.selectedElementIds, selectedElementIds: this.state.selectedElementIds,
@ -985,13 +1000,18 @@ export class App extends React.Component<any, AppState> {
_brand: "socketUpdateData"; _brand: "socketUpdateData";
}, },
) { ) {
if (this.socketInitialized && this.socket && this.roomID && this.roomKey) { if (
this.room.socketInitialized &&
this.room.socket &&
this.room.roomID &&
this.room.roomKey
) {
const json = JSON.stringify(data); const json = JSON.stringify(data);
const encoded = new TextEncoder().encode(json); const encoded = new TextEncoder().encode(json);
const encrypted = await encryptAESGEM(encoded, this.roomKey); const encrypted = await encryptAESGEM(encoded, this.room.roomKey);
this.socket.emit( this.room.socket.emit(
"server-broadcast", "server-broadcast",
this.roomID, this.room.roomID,
encrypted.data, encrypted.data,
encrypted.iv, encrypted.iv,
); );
@ -2490,7 +2510,7 @@ export class App extends React.Component<any, AppState> {
// sometimes the pointer goes off screen // sometimes the pointer goes off screen
return; return;
} }
this.socket && this.room.socket &&
this.broadcastMouseLocation({ this.broadcastMouseLocation({
pointerCoords, pointerCoords,
button, button,