diff --git a/excalidraw-app/app_constants.ts b/excalidraw-app/app_constants.ts index 1dc6c6f462..c3a0fa9a82 100644 --- a/excalidraw-app/app_constants.ts +++ b/excalidraw-app/app_constants.ts @@ -23,6 +23,7 @@ export enum WS_SUBTYPES { INVALID_RESPONSE = "INVALID_RESPONSE", INIT = "SCENE_INIT", UPDATE = "SCENE_UPDATE", + DELETE = "SCENE_DELETE", MOUSE_LOCATION = "MOUSE_LOCATION", IDLE_STATUS = "IDLE_STATUS", USER_VISIBLE_SCENE_BOUNDS = "USER_VISIBLE_SCENE_BOUNDS", diff --git a/excalidraw-app/collab/Collab.tsx b/excalidraw-app/collab/Collab.tsx index 595295611f..f4fd6a6b68 100644 --- a/excalidraw-app/collab/Collab.tsx +++ b/excalidraw-app/collab/Collab.tsx @@ -72,6 +72,7 @@ import { } from "../data/FileManager"; import { LocalData } from "../data/LocalData"; import { + deleteRoomFromFirebase, isSavedToFirebase, loadFilesFromFirebase, loadFromFirebase, @@ -115,6 +116,7 @@ export interface CollabAPI { onPointerUpdate: CollabInstance["onPointerUpdate"]; startCollaboration: CollabInstance["startCollaboration"]; stopCollaboration: CollabInstance["stopCollaboration"]; + deleteRoom: CollabInstance["deleteRoom"]; syncElements: CollabInstance["syncElements"]; fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"]; setUsername: CollabInstance["setUsername"]; @@ -228,6 +230,7 @@ class Collab extends PureComponent { isCollaborating: this.isCollaborating, onPointerUpdate: this.onPointerUpdate, startCollaboration: this.startCollaboration, + deleteRoom: this.deleteRoom, syncElements: this.syncElements, fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase, stopCollaboration: this.stopCollaboration, @@ -675,6 +678,18 @@ class Collab extends PureComponent { break; } + case WS_SUBTYPES.DELETE: { + const { roomId } = decryptedData.payload; + if (this.portal.roomId === roomId) { + this.destroySocketClient({ isUnload: true }); + this.setIsCollaborating(false); + this.setActiveRoomLink(null); + this.setErrorDialog(t("alerts.collabRoomDeleted")); + window.history.pushState({}, APP_NAME, window.location.origin); + } + break; + } + default: { assertNever(decryptedData, null); } @@ -894,6 +909,42 @@ class Collab extends PureComponent { }); }; + deleteRoom = async (): Promise => { + if (!this.portal.socket || !this.portal.roomId) { + return; + } + + const { roomId, roomKey } = this.portal; + if (!roomId || !roomKey) { + return; + } + + const link = this.getActiveRoomLink(); + if (!link) { + return; + } + + // check if the room belongs to the current user + const isOwner = await roomManager.isRoomOwnedByUser(link); + + if (!isOwner) { + return; + } + + try { + this.portal.broadcastRoomDeletion(); + await deleteRoomFromFirebase(roomId, roomKey); + await roomManager.deleteRoom(roomId); + this.stopCollaboration(false); + this.setActiveRoomLink(null); + window.history.pushState({}, APP_NAME, window.location.origin); + } catch (error) { + console.error("Failed to delete room:", error); + this.setErrorDialog(t("errors.roomDeletionFailed")); + throw error; + } + }; + public setLastBroadcastedOrReceivedSceneVersion = (version: number) => { this.lastBroadcastedOrReceivedSceneVersion = version; }; diff --git a/excalidraw-app/collab/Portal.tsx b/excalidraw-app/collab/Portal.tsx index 2e076a661e..9083246613 100644 --- a/excalidraw-app/collab/Portal.tsx +++ b/excalidraw-app/collab/Portal.tsx @@ -252,6 +252,20 @@ class Portal { this.socket.emit(WS_EVENTS.USER_FOLLOW_CHANGE, payload); } }; + + broadcastRoomDeletion = async () => { + if (this.socket?.id) { + const data: SocketUpdateDataSource["ROOM_DELETED"] = { + type: WS_SUBTYPES.DELETE, + payload: { + socketId: this.socket.id as SocketId, + roomId: this.roomId!, + }, + }; + + this._broadcastSocketData(data as SocketUpdateData); + } + }; } export default Portal; diff --git a/excalidraw-app/data/firebase.ts b/excalidraw-app/data/firebase.ts index 568054f7ef..82b342ef73 100644 --- a/excalidraw-app/data/firebase.ts +++ b/excalidraw-app/data/firebase.ts @@ -315,3 +315,10 @@ export const loadFilesFromFirebase = async ( return { loadedFiles, erroredFiles }; }; + +export const deleteRoomFromFirebase = async ( + roomId: string, + roomKey: string, +): Promise => { + // TODO: delete the room... +}; diff --git a/excalidraw-app/data/index.ts b/excalidraw-app/data/index.ts index 75aa278779..2ec32faf68 100644 --- a/excalidraw-app/data/index.ts +++ b/excalidraw-app/data/index.ts @@ -119,6 +119,13 @@ export type SocketUpdateDataSource = { username: string; }; }; + ROOM_DELETED: { + type: WS_SUBTYPES.DELETE; + payload: { + socketId: SocketId; + roomId: string; + }; + }; }; export type SocketUpdateDataIncoming = @@ -310,7 +317,7 @@ export const exportToBackend = async ( const response = await fetch(BACKEND_V2_POST, { method: "POST", - body: payload.buffer, + body: new Uint8Array(payload.buffer), }); const json = await response.json(); if (json.id) { diff --git a/excalidraw-app/data/roomManager.ts b/excalidraw-app/data/roomManager.ts index b3c2ff60d3..0d7cff11c3 100644 --- a/excalidraw-app/data/roomManager.ts +++ b/excalidraw-app/data/roomManager.ts @@ -186,7 +186,7 @@ class RoomManager { return false; } - const [, roomId] = match; + const roomId = match[1]; return rooms.some((room) => room.roomId === roomId); } catch (error) { console.warn("Failed to check room ownership:", error); diff --git a/excalidraw-app/share/ShareDialog.tsx b/excalidraw-app/share/ShareDialog.tsx index 50b5cabe8e..643ee0a0fe 100644 --- a/excalidraw-app/share/ShareDialog.tsx +++ b/excalidraw-app/share/ShareDialog.tsx @@ -198,10 +198,10 @@ const ActiveRoomDialog = ({ color="danger" onClick={() => { trackEvent("share", "room deleted"); - // TODO: handle deletion - // 1. stop collaboration (for all users?) - // 2. delete room from backend (firebase) - // 3. close + collabAPI.deleteRoom(); + if (!collabAPI.isCollaborating()) { + handleClose(); + } }} /> )} diff --git a/packages/excalidraw/locales/en.json b/packages/excalidraw/locales/en.json index f40cbf73fe..b180e58ba0 100644 --- a/packages/excalidraw/locales/en.json +++ b/packages/excalidraw/locales/en.json @@ -253,7 +253,8 @@ "resetLibrary": "This will clear your library. Are you sure?", "removeItemsFromsLibrary": "Delete {{count}} item(s) from library?", "invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.", - "collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!" + "collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!", + "collabRoomDeleted": "This collab room has been deleted by its owner." }, "errors": { "unsupportedFileType": "Unsupported file type.", @@ -280,7 +281,8 @@ }, "asyncPasteFailedOnRead": "Couldn't paste (couldn't read from system clipboard).", "asyncPasteFailedOnParse": "Couldn't paste.", - "copyToSystemClipboardFailed": "Couldn't copy to clipboard." + "copyToSystemClipboardFailed": "Couldn't copy to clipboard.", + "roomDeletionFailed": "Couldn't delete the collaboration room." }, "toolBar": { "selection": "Selection",