mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-04 23:00:34 +02:00
feat: manage rooms locally
This commit is contained in:
@@ -83,6 +83,7 @@ import {
|
||||
saveUsernameToLocalStorage,
|
||||
} from "../data/localStorage";
|
||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||
import { roomManager } from "../data/roomManager";
|
||||
|
||||
import { collabErrorIndicatorAtom } from "./CollabError";
|
||||
import Portal from "./Portal";
|
||||
@@ -547,6 +548,25 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
||||
});
|
||||
|
||||
this.saveCollabRoomToFirebase(getSyncableElements(elements));
|
||||
|
||||
// Save room data to local room manager for new rooms
|
||||
try {
|
||||
await roomManager.addRoom(
|
||||
roomId,
|
||||
roomKey,
|
||||
getCollaborationLink({ roomId, roomKey }),
|
||||
"", // User can edit this later
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn("Failed to save room to local storage:", error);
|
||||
}
|
||||
} else {
|
||||
// Update access time for existing rooms
|
||||
try {
|
||||
await roomManager.updateRoomAccess(existingRoomLinkData.roomId);
|
||||
} catch (error) {
|
||||
console.warn("Failed to update room access time:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// fallback in case you're not alone in the room but still don't receive
|
||||
|
193
excalidraw-app/data/roomManager.ts
Normal file
193
excalidraw-app/data/roomManager.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
generateEncryptionKey,
|
||||
encryptData,
|
||||
decryptData,
|
||||
} from "@excalidraw/excalidraw/data/encryption";
|
||||
|
||||
export interface CollabRoom {
|
||||
id: string;
|
||||
roomId: string;
|
||||
roomKey: string;
|
||||
createdAt: number;
|
||||
lastAccessed: number;
|
||||
url: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface EncryptedRoomData {
|
||||
rooms: CollabRoom[];
|
||||
version: number;
|
||||
}
|
||||
|
||||
const ROOM_STORAGE_KEY = "excalidraw-user-rooms";
|
||||
const ROOM_STORAGE_VERSION = 1;
|
||||
|
||||
class RoomManager {
|
||||
private userKey: string | null = null;
|
||||
|
||||
private async getUserKey(): Promise<string> {
|
||||
if (this.userKey) {
|
||||
return this.userKey;
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(`${ROOM_STORAGE_KEY}-key`);
|
||||
if (stored) {
|
||||
this.userKey = stored;
|
||||
return this.userKey;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to load user key from localStorage:", error);
|
||||
}
|
||||
|
||||
this.userKey = await generateEncryptionKey();
|
||||
|
||||
try {
|
||||
localStorage.setItem(`${ROOM_STORAGE_KEY}-key`, this.userKey);
|
||||
} catch (error) {
|
||||
console.warn("Failed to save user key to localStorage:", error);
|
||||
}
|
||||
|
||||
return this.userKey;
|
||||
}
|
||||
|
||||
private async encryptRoomData(
|
||||
data: EncryptedRoomData,
|
||||
): Promise<{ data: ArrayBuffer; iv: Uint8Array }> {
|
||||
const userKey = await this.getUserKey();
|
||||
const jsonData = JSON.stringify(data);
|
||||
const { encryptedBuffer, iv } = await encryptData(userKey, jsonData);
|
||||
return { data: encryptedBuffer, iv };
|
||||
}
|
||||
|
||||
private async decryptRoomData(
|
||||
encryptedData: ArrayBuffer,
|
||||
iv: Uint8Array,
|
||||
): Promise<EncryptedRoomData | null> {
|
||||
try {
|
||||
const userKey = await this.getUserKey();
|
||||
const decryptedBuffer = await decryptData(iv, encryptedData, userKey);
|
||||
const jsonString = new TextDecoder().decode(decryptedBuffer);
|
||||
const data = JSON.parse(jsonString) as EncryptedRoomData;
|
||||
|
||||
if (data.version === ROOM_STORAGE_VERSION && Array.isArray(data.rooms)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.warn("Failed to decrypt room data:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadRooms(): Promise<CollabRoom[]> {
|
||||
try {
|
||||
const storedData = localStorage.getItem(ROOM_STORAGE_KEY);
|
||||
if (!storedData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { data, iv } = JSON.parse(storedData);
|
||||
const dataBuffer = new Uint8Array(data).buffer;
|
||||
const ivArray = new Uint8Array(iv);
|
||||
|
||||
const decryptedData = await this.decryptRoomData(dataBuffer, ivArray);
|
||||
return decryptedData?.rooms || [];
|
||||
} catch (error) {
|
||||
console.warn("Failed to load rooms:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async saveRooms(rooms: CollabRoom[]): Promise<void> {
|
||||
try {
|
||||
const data: EncryptedRoomData = {
|
||||
rooms,
|
||||
version: ROOM_STORAGE_VERSION,
|
||||
};
|
||||
|
||||
const { data: encryptedData, iv } = await this.encryptRoomData(data);
|
||||
|
||||
const storageData = {
|
||||
data: Array.from(new Uint8Array(encryptedData)),
|
||||
iv: Array.from(iv),
|
||||
};
|
||||
|
||||
localStorage.setItem(ROOM_STORAGE_KEY, JSON.stringify(storageData));
|
||||
} catch (error) {
|
||||
console.warn("Failed to save rooms:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async addRoom(
|
||||
roomId: string,
|
||||
roomKey: string,
|
||||
url: string,
|
||||
name?: string,
|
||||
): Promise<void> {
|
||||
const rooms = await this.loadRooms();
|
||||
|
||||
const filteredRooms = rooms.filter((room) => room.roomId !== roomId);
|
||||
|
||||
const newRoom: CollabRoom = {
|
||||
id: crypto.randomUUID(),
|
||||
roomId,
|
||||
roomKey,
|
||||
createdAt: Date.now(),
|
||||
lastAccessed: Date.now(),
|
||||
url,
|
||||
name,
|
||||
};
|
||||
|
||||
filteredRooms.unshift(newRoom);
|
||||
|
||||
// Limit to the most recent 20 rooms
|
||||
const limitedRooms = filteredRooms.slice(0, 20);
|
||||
|
||||
await this.saveRooms(limitedRooms);
|
||||
}
|
||||
|
||||
async getRooms(): Promise<CollabRoom[]> {
|
||||
const rooms = await this.loadRooms();
|
||||
return rooms.sort((a, b) => b.lastAccessed - a.lastAccessed);
|
||||
}
|
||||
|
||||
async updateRoomAccess(roomId: string): Promise<void> {
|
||||
const rooms = await this.loadRooms();
|
||||
const room = rooms.find((r) => r.roomId === roomId);
|
||||
|
||||
if (room) {
|
||||
room.lastAccessed = Date.now();
|
||||
await this.saveRooms(rooms);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRoom(roomId: string): Promise<void> {
|
||||
const rooms = await this.loadRooms();
|
||||
const filteredRooms = rooms.filter((room) => room.roomId !== roomId);
|
||||
await this.saveRooms(filteredRooms);
|
||||
}
|
||||
|
||||
async updateRoomName(roomId: string, name: string): Promise<void> {
|
||||
const rooms = await this.loadRooms();
|
||||
const room = rooms.find((r) => r.roomId === roomId);
|
||||
|
||||
if (room) {
|
||||
room.name = name;
|
||||
await this.saveRooms(rooms);
|
||||
}
|
||||
}
|
||||
|
||||
async clearAllRooms(): Promise<void> {
|
||||
try {
|
||||
localStorage.removeItem(ROOM_STORAGE_KEY);
|
||||
localStorage.removeItem(`${ROOM_STORAGE_KEY}-key`);
|
||||
this.userKey = null;
|
||||
} catch (error) {
|
||||
console.warn("Failed to clear rooms:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const roomManager = new RoomManager();
|
Reference in New Issue
Block a user