From 5afa8689f2d37d361e652d2c4f970f808b034cf0 Mon Sep 17 00:00:00 2001 From: yyassin Date: Tue, 9 Jan 2024 11:45:09 -0500 Subject: [PATCH 1/5] Images work in live collab. --- client/src/components/lib/Canvas.tsx | 1 + .../components/lib/SaveOpenDropDownMenu.tsx | 4 +- .../StableDiffusion/StableDiffusionSheet.tsx | 12 +- client/src/components/lib/ToolBar.tsx | 7 +- client/src/hooks/useSocket.tsx | 108 ++++++++++----- client/src/lib/image.ts | 124 +++++++++++------- client/src/stores/CanvasElementsStore.ts | 1 + 7 files changed, 160 insertions(+), 97 deletions(-) diff --git a/client/src/components/lib/Canvas.tsx b/client/src/components/lib/Canvas.tsx index 09f7f4b..c53b07f 100644 --- a/client/src/components/lib/Canvas.tsx +++ b/client/src/components/lib/Canvas.tsx @@ -391,6 +391,7 @@ export default function Canvas() { p1: { x: clientX - width / 2, y: clientY - height / 2 }, p2: { x: clientX + width / 2, y: clientY + height / 2 }, }); + setWebsocketAction(pendingImageElementId, 'addCanvasShape'); // Unselect current image and reset cursor setPendingImageElement(''); setCursor(''); diff --git a/client/src/components/lib/SaveOpenDropDownMenu.tsx b/client/src/components/lib/SaveOpenDropDownMenu.tsx index dccef50..d65f8b6 100644 --- a/client/src/components/lib/SaveOpenDropDownMenu.tsx +++ b/client/src/components/lib/SaveOpenDropDownMenu.tsx @@ -140,7 +140,7 @@ export const SaveOpenDropDownMenu = () => { }; return ( - + <> { > Save - + ); }; diff --git a/client/src/components/lib/StableDiffusion/StableDiffusionSheet.tsx b/client/src/components/lib/StableDiffusion/StableDiffusionSheet.tsx index 4293f52..f6c5ec7 100644 --- a/client/src/components/lib/StableDiffusion/StableDiffusionSheet.tsx +++ b/client/src/components/lib/StableDiffusion/StableDiffusionSheet.tsx @@ -36,13 +36,10 @@ const StableDiffusionSheet = () => { 'addCanvasShape', 'editCanvasElement', ]); - const { isUsingStableDiffusion, setIsUsingStableDiffusion, zoom, appHeight } = - useAppStore([ - 'isUsingStableDiffusion', - 'setIsUsingStableDiffusion', - 'zoom', - 'appHeight', - ]); + const { isUsingStableDiffusion, setIsUsingStableDiffusion } = useAppStore([ + 'isUsingStableDiffusion', + 'setIsUsingStableDiffusion', + ]); const { canvasColor, setTool } = useAppStore(['canvasColor', 'setTool']); const { selectedElementIds, @@ -219,7 +216,6 @@ const StableDiffusionSheet = () => { imageFile, addCanvasShape, editCanvasElement, - { zoom, appHeight }, true, ); // And let the user place the image diff --git a/client/src/components/lib/ToolBar.tsx b/client/src/components/lib/ToolBar.tsx index 66b4c7e..6174949 100644 --- a/client/src/components/lib/ToolBar.tsx +++ b/client/src/components/lib/ToolBar.tsx @@ -61,11 +61,7 @@ const ToolButton = ({ active: boolean; children?: React.ReactNode; }) => { - const { setTool, zoom, appHeight } = useAppStore([ - 'setTool', - 'zoom', - 'appHeight', - ]); + const { setTool } = useAppStore(['setTool']); const { removeCanvasElements, setSelectedElements, @@ -119,7 +115,6 @@ const ToolButton = ({ imageFile, addCanvasShape, editCanvasElement, - { zoom, appHeight }, true, ); // And let the user place the image diff --git a/client/src/hooks/useSocket.tsx b/client/src/hooks/useSocket.tsx index c7d44d1..5015caf 100644 --- a/client/src/hooks/useSocket.tsx +++ b/client/src/hooks/useSocket.tsx @@ -7,6 +7,9 @@ import { import { createElement } from '@/lib/canvasElements/canvasElementUtils'; import { useEffect, useRef } from 'react'; import { useAuthStore } from '@/stores/AuthStore'; +import { fileCache } from '@/lib/cache'; +import { dataURLToFile } from '@/lib/bytes'; +import { commitImageToCache, isSupportedImageFile } from '@/lib/image'; /** * Defines a hook that controls all socket related activities @@ -49,6 +52,7 @@ export const useSocket = () => { p1, p2, textStrings, + fileIds, } = useCanvasElementStore([ 'addCanvasShape', 'addCanvasFreehand', @@ -74,6 +78,7 @@ export const useSocket = () => { 'p1', 'p2', 'textStrings', + 'fileIds', ]); const socket = useRef(); @@ -104,8 +109,31 @@ export const useSocket = () => { angle: element.angle, }, ); - addCanvasShape(newElement); - pushCanvasHistory(); + + if (element.type === 'image' && element.imgDataURL) { + // Add the file to the cache, and set the image as placed + newElement.isImagePlaced = true; + const imageFile = dataURLToFile(element.imgDataURL); + if (!isSupportedImageFile(imageFile)) { + throw new Error('Unsupported image type.'); + } + addCanvasShape(newElement); + commitImageToCache( + { + mimeType: imageFile.type, + id: element.id, + dataURL: element.imgDataURL, + created: Date.now(), + lastRetrieved: Date.now(), + }, + newElement, + editCanvasElement, + false, + ).then(pushCanvasHistory); + } else { + addCanvasShape(newElement); + pushCanvasHistory(); + } }, addCanvasFreehand: (element: CanvasElement) => { const newElement = createElement( @@ -132,6 +160,7 @@ export const useSocket = () => { }, true, ); + addCanvasFreehand(newElement); pushCanvasHistory(); }, @@ -202,7 +231,49 @@ export const useSocket = () => { } }, [roomID]); - // Send message once action gets set. Note: will be changed + const processWebsocketAction = async (id: string) => { + //Create element to send to other sockets in room + const element = createElement( + id, + p1[id].x, + p1[id].y, + p2[id].x, + p2[id].y, + types[id], + freehandPoints[id], + { + stroke: strokeColors[id], + fill: fillColors[id], + font: fontFamilies[id], + size: fontSizes[id], + bowing: bowings[id], + roughness: roughnesses[id], + strokeWidth: strokeWidths[id], + fillStyle: fillStyles[id], + strokeLineDash: strokeLineDashes[id], + opacity: opacities[id], + text: textStrings[id], + angle: angles[id], + }, + true, + ); + + if (element.type === 'image') { + const imageFileId = fileIds[id]; + const imageFile = fileCache.cache[imageFileId ?? '']; + if (imageFileId === undefined || imageFile === undefined) { + throw new Error('Image file not found for transmision'); + } + const imgDataURL = imageFile.dataURL; + element.imgDataURL = imgDataURL; + } + + delete element.roughElement; + socket.current?.sendMsgRoom(action, element); + setWebsocketAction('', ''); + }; + + // Send message once action gets set. useEffect(() => { if (actionElementID === '') return; @@ -218,36 +289,7 @@ export const useSocket = () => { setWebsocketAction('', ''); return; } - - //Create element to send to other sockets in room - const element = createElement( - actionElementID, - p1[actionElementID].x, - p1[actionElementID].y, - p2[actionElementID].x, - p2[actionElementID].y, - types[actionElementID], - freehandPoints[actionElementID], - { - stroke: strokeColors[actionElementID], - fill: fillColors[actionElementID], - font: fontFamilies[actionElementID], - size: fontSizes[actionElementID], - bowing: bowings[actionElementID], - roughness: roughnesses[actionElementID], - strokeWidth: strokeWidths[actionElementID], - fillStyle: fillStyles[actionElementID], - strokeLineDash: strokeLineDashes[actionElementID], - opacity: opacities[actionElementID], - text: textStrings[actionElementID], - angle: angles[actionElementID], - }, - true, - ); - - delete element.roughElement; - socket.current?.sendMsgRoom(action, element); - setWebsocketAction('', ''); + processWebsocketAction(actionElementID); }, [ actionElementID, action, diff --git a/client/src/lib/image.ts b/client/src/lib/image.ts index f42ea77..e1dade5 100644 --- a/client/src/lib/image.ts +++ b/client/src/lib/image.ts @@ -252,6 +252,70 @@ export const initializeImageDimensions = ( } }; +/** + * Commits the image file to the cache and updates the image element in state by + * adding the generated file ID, triggering a rerender to show the image. + * @param file The image file to be added to the cache. + * @param imageElement The image element to be updated in state. + * @param editImageInState The callback to update the image element in state. + * @param showCursorImagePreview The flag indicating whether to show a cursor image preview. + * @returns A Promise resolving to the inserted CanvasElement. + */ +export const commitImageToCache = ( + file: BinaryFileData, + imageElement: CanvasElement, + editImageInState: ( + id: string, + partialElement: Partial, + ) => void, + showCursorImagePreview?: boolean, +) => + new Promise(async (resolve, reject) => { + const initImageElement = { ...imageElement, fileId: file.id }; + + try { + // Add image file data to the file cache + fileCache.addFile(file.id, { + ...file, + created: Date.now(), + lastRetrieved: Date.now(), + }); + + // Check if the image data is already in image cache + const cachedImageData = imageCache.cache.get(file.id); + // Update the image cache if not + if (!cachedImageData) { + const fileIds = [initImageElement] + .map((element) => element.fileId ?? '') + .filter((id) => id); + await _updateImageCache({ + imageCache, + fileIds, + files: fileCache.cache, + }); + } + + // If the image is still loading, wait for it to resolve + if (cachedImageData?.image instanceof Promise) { + await cachedImageData.image; + } + + // Update the image element in the application state with the file ID + // once loaded, this will trigger a rerender to show the image, provided + // the element has been placed. + editImageInState(imageElement.id, { fileId: file.id }); + // Resolve the Promise with the inserted image element + resolve(imageElement); + } catch (error: unknown) { + console.error(error); + reject(new Error('Error inserting image')); + } finally { + if (!showCursorImagePreview) { + setCursor(''); + } + } + }); + /** * Inserts an image element into the canvas, handling resizing, caching, and cursor preview. * @@ -259,7 +323,6 @@ export const initializeImageDimensions = ( * @param imageFile Tthe image file to be injected into the element. * @param addImageInState Callback to add the image element to the application state. * @param editImageInState Callback to update the image element in the application state. - * @param appState State slice containing the current zoom level and canvas height. * @param showCursorImagePreview True to show a cursor image preview, false otherwise. * @returns A Promise resolving to the inserted CanvasElement. */ @@ -271,7 +334,6 @@ export const injectImageElement = async ( id: string, partialElement: Partial, ) => void, - appState: { zoom: number; appHeight: number }, showCursorImagePreview?: boolean, ) => { // Add proxy element to state. @@ -329,54 +391,20 @@ export const injectImageElement = async ( // Get the data URL for the image file const dataURL = fileCache.cache[fileId]?.dataURL || (await getDataURL(imageFile)); - const initImageElement = { ...imageElement, fileId }; // Return a promise which asynchronously loads the HTML image into cache. - return new Promise(async (resolve, reject) => { - try { - // Add image file data to the file cache - fileCache.addFile(fileId, { - mimeType, - id: fileId, - dataURL, - created: Date.now(), - lastRetrieved: Date.now(), - }); - - // Check if the image data is already in image cache - const cachedImageData = imageCache.cache.get(fileId); - // Update the image cache if not - if (!cachedImageData) { - const fileIds = [initImageElement] - .map((element) => element.fileId ?? '') - .filter((id) => id); - await _updateImageCache({ - imageCache, - fileIds, - files: fileCache.cache, - }); - } - - // If the image is still loading, wait for it to resolve - if (cachedImageData?.image instanceof Promise) { - await cachedImageData.image; - } - - // Update the image element in the application state with the file ID - // once loaded, this will trigger a rerender to show the image, provided - // the element has been placed. - editImageInState(imageElement.id, { fileId }); - // Resolve the Promise with the inserted image element - resolve(imageElement); - } catch (error: unknown) { - console.error(error); - reject(new Error('Error inserting image')); - } finally { - if (!showCursorImagePreview) { - setCursor(''); - } - } - }); + return commitImageToCache( + { + mimeType, + id: fileId, + dataURL, + created: Date.now(), + lastRetrieved: Date.now(), + }, + imageElement, + editImageInState, + showCursorImagePreview, + ); } catch (error: unknown) { // TODO: Should handle deleting the image from state here console.error('Failed to insert image'); diff --git a/client/src/stores/CanvasElementsStore.ts b/client/src/stores/CanvasElementsStore.ts index 8058b15..f53b356 100644 --- a/client/src/stores/CanvasElementsStore.ts +++ b/client/src/stores/CanvasElementsStore.ts @@ -25,6 +25,7 @@ export interface CanvasElement { roughElement?: Drawable; // The underlying roughjs element, if applicable. freehandPoints?: Vector2[]; // Points for curves fileId?: string; // For image elements; the id of the image in cache. + imgDataURL?: string; // For image elements; the dataURL of the image. Used in exports. isImagePlaced: boolean; // For image elements; true if the element has been placed, false otherwise. text: string; // Container stringW fontFamily: string; // Font Family From 5b2a9e77491d9ce4b536c3abbf3bb67ec6acd010 Mon Sep 17 00:00:00 2001 From: Yousef Yassin Date: Tue, 9 Jan 2024 15:50:46 -0500 Subject: [PATCH 2/5] Images serialization and deserialization. --- .../components/lib/SaveOpenDropDownMenu.tsx | 52 +++++++++++++++++-- client/src/lib/cache.ts | 4 ++ client/src/lib/image.ts | 19 +++---- client/src/stores/CanvasElementsStore.ts | 41 +-------------- 4 files changed, 64 insertions(+), 52 deletions(-) diff --git a/client/src/components/lib/SaveOpenDropDownMenu.tsx b/client/src/components/lib/SaveOpenDropDownMenu.tsx index d65f8b6..a296d68 100644 --- a/client/src/components/lib/SaveOpenDropDownMenu.tsx +++ b/client/src/components/lib/SaveOpenDropDownMenu.tsx @@ -8,6 +8,9 @@ import { } from '@/stores/CanvasElementsStore'; import { createElement } from '@/lib/canvasElements/canvasElementUtils'; import saveAs from 'file-saver'; +import { fileCache, imageCache } from '@/lib/cache'; +import { BinaryFileData } from '@/types'; +import { commitImageToCache } from '@/lib/image'; /** * Component that the save and load button with their functionality in the drop down menu in the canavas @@ -35,6 +38,12 @@ export const SaveOpenDropDownMenu = () => { freehandPoints, p1, p2, + editCanvasElement, + textStrings, + isImagePlaceds, + freehandBounds, + angles, + fileIds, } = useCanvasElementStore([ 'setCanvasElementState', 'allIds', @@ -52,6 +61,12 @@ export const SaveOpenDropDownMenu = () => { 'freehandPoints', 'p1', 'p2', + 'editCanvasElement', + 'textStrings', + 'isImagePlaceds', + 'freehandBounds', + 'angles', + 'fileIds', ]); /** @@ -75,7 +90,10 @@ export const SaveOpenDropDownMenu = () => { reader.readAsText(file[0]); reader.onload = () => { - const state: CanvasElementState = JSON.parse(reader.result as string); + const { fileData, ...state } = JSON.parse( + reader.result as string, + ) as CanvasElementState & { fileData: Record }; + const roughElements: Record = {}; //create the roughElements @@ -107,7 +125,28 @@ export const SaveOpenDropDownMenu = () => { ).roughElement; } state.roughElements = roughElements; + // Reset all fileIds, they will get get set to show the imageswhen they resolve. + const fileIds = state.fileIds; + state.fileIds = {}; setCanvasElementState(state); + + // Populate the cache and images asynchronously + Object.entries(fileIds).forEach(([elemId, fileId]) => { + const binary = fileId && fileData[fileId]; + if (!binary) throw new Error('Failed to resolve saved binary images'); + + const imageElement = { id: elemId }; + commitImageToCache( + { + ...fileData[fileId], + lastRetrieved: Date.now(), + }, + imageElement, + // Will set fileIds, triggering a rerender. A placeholder + // will be shown in the mean time. + editCanvasElement, + ); + }); }; }; @@ -115,7 +154,7 @@ export const SaveOpenDropDownMenu = () => { * Serializes the data and saves it to local disk */ const handleSave = () => { - const serializedState = JSON.stringify({ + const state = { allIds, types, strokeColors, @@ -131,7 +170,14 @@ export const SaveOpenDropDownMenu = () => { freehandPoints, p1, p2, - }); + textStrings, + isImagePlaceds, + freehandBounds, + angles, + fileIds, + fileData: fileCache.cache, + }; + const serializedState = JSON.stringify(state); const blob = new Blob([serializedState], { type: 'text/plain;charset=utf-8', diff --git a/client/src/lib/cache.ts b/client/src/lib/cache.ts index 36b0673..9c78182 100644 --- a/client/src/lib/cache.ts +++ b/client/src/lib/cache.ts @@ -29,6 +29,10 @@ class FileCache { return this.#cache; } + public set cache(newCache: Record) { + this.#cache = newCache; + } + /** * Adds the provided file to the cache, corresponding * to the specified id. diff --git a/client/src/lib/image.ts b/client/src/lib/image.ts index e1dade5..ea7b56e 100644 --- a/client/src/lib/image.ts +++ b/client/src/lib/image.ts @@ -261,18 +261,16 @@ export const initializeImageDimensions = ( * @param showCursorImagePreview The flag indicating whether to show a cursor image preview. * @returns A Promise resolving to the inserted CanvasElement. */ -export const commitImageToCache = ( +export const commitImageToCache = >( file: BinaryFileData, - imageElement: CanvasElement, - editImageInState: ( + imageElement?: T, + editImageInState?: ( id: string, partialElement: Partial, ) => void, showCursorImagePreview?: boolean, ) => - new Promise(async (resolve, reject) => { - const initImageElement = { ...imageElement, fileId: file.id }; - + new Promise(async (resolve, reject) => { try { // Add image file data to the file cache fileCache.addFile(file.id, { @@ -285,15 +283,18 @@ export const commitImageToCache = ( const cachedImageData = imageCache.cache.get(file.id); // Update the image cache if not if (!cachedImageData) { - const fileIds = [initImageElement] - .map((element) => element.fileId ?? '') - .filter((id) => id); + // const fileIds = [initImageElement] + // .map((element) => element.fileId ?? '') + // .filter((id) => id); + const fileIds = [file.id]; await _updateImageCache({ imageCache, fileIds, files: fileCache.cache, }); } + if (!(imageElement !== undefined && editImageInState !== undefined)) + return; // If the image is still loading, wait for it to resolve if (cachedImageData?.image instanceof Promise) { diff --git a/client/src/stores/CanvasElementsStore.ts b/client/src/stores/CanvasElementsStore.ts index f53b356..f24e2fe 100644 --- a/client/src/stores/CanvasElementsStore.ts +++ b/client/src/stores/CanvasElementsStore.ts @@ -618,46 +618,7 @@ const setPendingImageElement = const setCanvasElementState = (set: SetState) => (newCanvasElementState: CanvasElementState) => - set((state) => { - const { - allIds, - types, - strokeColors, - fillColors, - fontFamilies, - fontSizes, - bowings, - roughnesses, - strokeWidths, - strokeLineDashes, - fillStyles, - opacities, - roughElements, - freehandPoints, - p1, - p2, - } = newCanvasElementState; - - return { - ...state, - allIds, - types, - strokeColors, - fillColors, - fontFamilies, - fontSizes, - bowings, - roughnesses, - strokeWidths, - fillStyles, - strokeLineDashes, - opacities, - roughElements, - freehandPoints, - p1, - p2, - }; - }); + set((state) => ({ ...state, ...newCanvasElementState })); /** Store Hook */ const canvasElementStore = create()((set) => ({ From a561ed1eadf5439c019e38b5a9c41fda473c9dcf Mon Sep 17 00:00:00 2001 From: yyassin Date: Tue, 9 Jan 2024 23:07:33 -0500 Subject: [PATCH 3/5] Cleanup and tenant api. --- client/src/components/lib/Canvas.tsx | 14 +++++++---- .../src/components/lib/ToolButtonSelector.tsx | 1 + client/src/hooks/useMultiSelection.tsx | 10 ++++++-- client/src/lib/canvasElements/renderScene.ts | 13 +++++++++-- client/src/lib/canvasElements/resize.ts | 23 ++++++++++++------- node/src/api/tenancy/tenancy.controller.ts | 23 +++++++++++++++++++ node/src/api/tenancy/tenancy.route.ts | 15 ++++++++++++ node/src/app.ts | 2 ++ node/src/constants.ts | 2 ++ node/src/lib/websocket/WebSocketManager.ts | 15 ++++++++++-- 10 files changed, 99 insertions(+), 19 deletions(-) create mode 100644 node/src/api/tenancy/tenancy.controller.ts create mode 100644 node/src/api/tenancy/tenancy.route.ts diff --git a/client/src/components/lib/Canvas.tsx b/client/src/components/lib/Canvas.tsx index c53b07f..ee2d777 100644 --- a/client/src/components/lib/Canvas.tsx +++ b/client/src/components/lib/Canvas.tsx @@ -412,11 +412,15 @@ export default function Canvas() { action === 'drawing' ? currentDrawingElemId.current : selectedElementIds[0]; - const { x1, y1, x2, y2 } = adjustElementCoordinatesById(id, { - p1, - p2, - types, - }); + const { x1, y1, x2, y2 } = adjustElementCoordinatesById( + id, + { + p1, + p2, + types, + }, + types[id] === 'image', + ); updateElement(id, x1, y1, x2, y2, types[id]); } diff --git a/client/src/components/lib/ToolButtonSelector.tsx b/client/src/components/lib/ToolButtonSelector.tsx index 67c99dd..dbc40f3 100644 --- a/client/src/components/lib/ToolButtonSelector.tsx +++ b/client/src/components/lib/ToolButtonSelector.tsx @@ -88,6 +88,7 @@ const ToolButton = ({ if (selectedElementId === undefined) { // If no element is selected, then set the tool options setToolOptions(customizabilityDict); + return; } const { roughElement, fillColor } = createElement( selectedElementId, diff --git a/client/src/hooks/useMultiSelection.tsx b/client/src/hooks/useMultiSelection.tsx index 94b43fb..5dfb898 100644 --- a/client/src/hooks/useMultiSelection.tsx +++ b/client/src/hooks/useMultiSelection.tsx @@ -56,8 +56,14 @@ const getElementsWithinFrame = ( const { elementIds, p1, p2, angles } = appState; // Destructure coordinates of the frame - const { x: frameX1, y: frameY1 } = frame.p1; - const { x: frameX2, y: frameY2 } = frame.p2; + let { x: frameX1, y: frameY1 } = frame.p1; + let { x: frameX2, y: frameY2 } = frame.p2; + + // Adjust the coordinates so that x1 < x2 and y1 < y2 + // this is needed in case we draw the frame from right to left or bottom to top and + // simplifies the logic of checking whether an element is within the frame + [frameX1, frameX2] = [frameX1, frameX2].sort((a, b) => a - b); + [frameY1, frameY2] = [frameY1, frameY2].sort((a, b) => a - b); // Filter element IDs based on whether their positions fall within the frame return elementIds.filter((id) => { diff --git a/client/src/lib/canvasElements/renderScene.ts b/client/src/lib/canvasElements/renderScene.ts index 10a2417..e57cd10 100644 --- a/client/src/lib/canvasElements/renderScene.ts +++ b/client/src/lib/canvasElements/renderScene.ts @@ -122,14 +122,23 @@ export const renderCanvasElements = ( } } else if (type === 'image') { if (isImagePlaceds[id]) { - const [width, height] = [Math.abs(x2 - x1), Math.abs(y2 - y1)]; - + const [width, height] = [x2 - x1, y2 - y1]; const imgFileId = fileIds[id]; const img = imgFileId ? imageCache.cache.get(imgFileId)?.image : undefined; if (img !== undefined && !(img instanceof Promise)) { ctx.drawImage(img, x1, y1, width, height); + // const sX = Math.sign(width); + // const sY = Math.sign(height); + // ctx.scale(sX, sY); + // ctx.drawImage( + // img, + // sX * x1, + // sY * y1, + // Math.abs(width), + // Math.abs(height), + // ); } else { drawImagePlaceholder(width, height, ctx); } diff --git a/client/src/lib/canvasElements/resize.ts b/client/src/lib/canvasElements/resize.ts index 149ff44..5b3c10f 100644 --- a/client/src/lib/canvasElements/resize.ts +++ b/client/src/lib/canvasElements/resize.ts @@ -125,8 +125,8 @@ export const adjustElementCoordinates = ( p2: Vector2, elementType: CanvasElement['type'], ) => { - const { x: x1, y: y1 } = p1; - const { x: x2, y: y2 } = p2; + let { x: x1, y: y1 } = p1; + let { x: x2, y: y2 } = p2; if ( elementType === 'rectangle' || @@ -134,13 +134,11 @@ export const adjustElementCoordinates = ( elementType === 'image' || elementType === 'freehand' ) { - const minX = Math.min(x1, x2); - const maxX = Math.max(x1, x2); - const minY = Math.min(y1, y2); - const maxY = Math.max(y1, y2); + [x1, x2] = [x1, x2].sort((a, b) => a - b); + [y1, y2] = [y1, y2].sort((a, b) => a - b); - // x1, x2 is top left (so min in both) - return { x1: minX, y1: minY, x2: maxX, y2: maxY }; + // x1, y1 is top left (so min in both) + return { x1, y1, x2, y2 }; } else { // This is a line. We make the left most, or // top most point (x1, y1). @@ -171,8 +169,17 @@ export const adjustElementCoordinatesById = ( p2: Record; types: Record; }, + skip = false, ) => { const { p1, p2, types } = appState; + if (skip) { + return { + x1: p1[elementId].x, + y1: p1[elementId].y, + x2: p2[elementId].x, + y2: p2[elementId].y, + }; + } return adjustElementCoordinates( p1[elementId], p2[elementId], diff --git a/node/src/api/tenancy/tenancy.controller.ts b/node/src/api/tenancy/tenancy.controller.ts new file mode 100644 index 0000000..1cba79d --- /dev/null +++ b/node/src/api/tenancy/tenancy.controller.ts @@ -0,0 +1,23 @@ +import { Request, Response } from 'express'; +import { websocketManager } from '../../lib/websocket/WebSocketManager'; +import { HTTP_STATUS } from '../../constants'; + +/** + * Defines the REST controller for the tenancy API. + * @author Yousef Yassin + */ + +/** + * Retrieves the usernames of the tenants in the specified room, + * or empty array if the room does not exist. + */ +export const roomTenancy = async (req: Request, res: Response) => { + const { + body: { roomId }, + } = req; + const tenants = websocketManager.sockets[roomId]; + const tenantIds = tenants ? Object.keys(tenants) : []; + res.status(HTTP_STATUS.SUCCESS).json({ + tenantIds, + }); +}; diff --git a/node/src/api/tenancy/tenancy.route.ts b/node/src/api/tenancy/tenancy.route.ts new file mode 100644 index 0000000..b9ebe3b --- /dev/null +++ b/node/src/api/tenancy/tenancy.route.ts @@ -0,0 +1,15 @@ +import express from 'express'; +import { roomTenancy } from './tenancy.controller'; + +/** + * Defines Tenancy routes. + * @author Yousef Yassin + */ + +// The express router +const router = express.Router(); + +// Endpoint for adding a consumer to the SFUManager. +router.get('/room', roomTenancy); + +export default router; diff --git a/node/src/app.ts b/node/src/app.ts index c45b157..3d31049 100644 --- a/node/src/app.ts +++ b/node/src/app.ts @@ -16,6 +16,7 @@ import commentRoutes from './api/comment/comment.route'; import boardRoutes from './api/board/board.route'; import authRoutes from './api/auth/auth.route'; import sfuRoutes from './api/sfu/sfu.route'; +import tenancyRoutes from './api/tenancy/tenancy.route'; const mainLogger = new Logger('MainModule', LOG_LEVEL); const port = 3005; @@ -38,6 +39,7 @@ app.use('/comment', commentRoutes); app.use('/board', boardRoutes); app.use('/auth', authRoutes); app.use('/sfu', sfuRoutes); +app.use('/tenancy', tenancyRoutes); server.listen(port, () => { mainLogger.info(`Example app listening on port ${port}.`); diff --git a/node/src/constants.ts b/node/src/constants.ts index e691713..07e89da 100644 --- a/node/src/constants.ts +++ b/node/src/constants.ts @@ -16,6 +16,8 @@ export const HTTP_STATUS = { export enum WS_TOPICS { JOIN_ROOM = 'join-room', LEAVE_ROOM = 'leave-room', + NOTIFY_JOIN_ROOM = 'notify-join-room', + NOTIFY_LEAVE_ROOM = 'notify-leave-room', RTC_END_CALL = 'rtc-end-call', RTC_NEW_PRODUCER = 'rtc-new-producer', RTC_DISCONNECT_PRODUCER = 'rtc-disconnect-producer', diff --git a/node/src/lib/websocket/WebSocketManager.ts b/node/src/lib/websocket/WebSocketManager.ts index 85aed0a..9753a42 100644 --- a/node/src/lib/websocket/WebSocketManager.ts +++ b/node/src/lib/websocket/WebSocketManager.ts @@ -56,6 +56,11 @@ export type WSCallback = ({ return sendErrorResponse(socket, 'Socket already in room!'); } this.sockets[room][id] = socket; + this.handleMsg(socket, undefined, { + topic: WS_TOPICS.NOTIFY_JOIN_ROOM, + room, + payload: { id }, + }); return sendSuccessResponse(socket, 'Socket joined room!'); }); // Remove socket from room @@ -66,6 +71,11 @@ export type WSCallback = ({ return sendErrorResponse(socket, 'Socket already is not room!'); } delete this.sockets[room][id]; + this.handleMsg(socket, undefined, { + topic: WS_TOPICS.NOTIFY_LEAVE_ROOM, + room, + payload: { id }, + }); return sendSuccessResponse(socket, 'Socket left room!'); }); this.initWSS(server); @@ -94,9 +104,10 @@ export type WSCallback = ({ * @param socket The WebSocket instance. * @param msg The raw message data. */ - private handleMsg = (socket: WebSocket, msg: RawData) => { + private handleMsg = (socket: WebSocket, msg?: RawData, _msg?: unknown) => { // Parse message to JSON - const jsonMsg = JSON.parse(Buffer.from(msg as ArrayBuffer).toString()); + const jsonMsg = + _msg ?? JSON.parse(Buffer.from(msg as ArrayBuffer).toString()); const { topic, room, payload, id } = jsonMsg; // The only message that can be received without a room is join-room From 5d8585ed1de3ed8b23be1627b381921176519aa1 Mon Sep 17 00:00:00 2001 From: yyassin Date: Tue, 9 Jan 2024 23:17:39 -0500 Subject: [PATCH 4/5] Fix tests/linting. --- .../components/lib/SaveOpenDropDownMenu.tsx | 2 +- node/src/lib/websocket/WebSocketManager.ts | 27 ++++++++++++------- node/test/websockets.spec.ts | 2 ++ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/client/src/components/lib/SaveOpenDropDownMenu.tsx b/client/src/components/lib/SaveOpenDropDownMenu.tsx index a296d68..eb918d2 100644 --- a/client/src/components/lib/SaveOpenDropDownMenu.tsx +++ b/client/src/components/lib/SaveOpenDropDownMenu.tsx @@ -8,7 +8,7 @@ import { } from '@/stores/CanvasElementsStore'; import { createElement } from '@/lib/canvasElements/canvasElementUtils'; import saveAs from 'file-saver'; -import { fileCache, imageCache } from '@/lib/cache'; +import { fileCache } from '@/lib/cache'; import { BinaryFileData } from '@/types'; import { commitImageToCache } from '@/lib/image'; diff --git a/node/src/lib/websocket/WebSocketManager.ts b/node/src/lib/websocket/WebSocketManager.ts index 9753a42..1fc1779 100644 --- a/node/src/lib/websocket/WebSocketManager.ts +++ b/node/src/lib/websocket/WebSocketManager.ts @@ -36,6 +36,7 @@ export type WSCallback = ({ #sockets = {} as Record>; #logger = new Logger(WebSocketManager.name, LOG_LEVEL); callbacks = {} as Record; + #shouldNotify = true; /** Creates a new WebSocket Manager instance */ constructor() { @@ -43,6 +44,10 @@ export type WSCallback = ({ this.#logger.debug('Instantiated.'); } + public set shouldNotify(shouldNotify: boolean) { + this.#shouldNotify = shouldNotify; + } + /** * Initializes the WebSocketManager with the provided HTTP server. * @param server The HTTP server instance. @@ -56,11 +61,12 @@ export type WSCallback = ({ return sendErrorResponse(socket, 'Socket already in room!'); } this.sockets[room][id] = socket; - this.handleMsg(socket, undefined, { - topic: WS_TOPICS.NOTIFY_JOIN_ROOM, - room, - payload: { id }, - }); + this.#shouldNotify && + this.handleMsg(socket, undefined, { + topic: WS_TOPICS.NOTIFY_JOIN_ROOM, + room, + payload: { id }, + }); return sendSuccessResponse(socket, 'Socket joined room!'); }); // Remove socket from room @@ -71,11 +77,12 @@ export type WSCallback = ({ return sendErrorResponse(socket, 'Socket already is not room!'); } delete this.sockets[room][id]; - this.handleMsg(socket, undefined, { - topic: WS_TOPICS.NOTIFY_LEAVE_ROOM, - room, - payload: { id }, - }); + this.#shouldNotify && + this.handleMsg(socket, undefined, { + topic: WS_TOPICS.NOTIFY_LEAVE_ROOM, + room, + payload: { id }, + }); return sendSuccessResponse(socket, 'Socket left room!'); }); this.initWSS(server); diff --git a/node/test/websockets.spec.ts b/node/test/websockets.spec.ts index a45e6fd..678c343 100644 --- a/node/test/websockets.spec.ts +++ b/node/test/websockets.spec.ts @@ -93,6 +93,8 @@ describe('WebSocketManager', () => { beforeEach(() => { webSocketManager = WebSocketManager.Instance; + // Simplifies counting for tests. + webSocketManager.shouldNotify = false; }); afterEach(() => { From a4cd6a6e98d60b08ec31998ccde7321061bf9bdf Mon Sep 17 00:00:00 2001 From: yyassin Date: Tue, 9 Jan 2024 23:19:27 -0500 Subject: [PATCH 5/5] Remove adjust skip. --- client/src/components/lib/Canvas.tsx | 14 +++++--------- client/src/lib/canvasElements/resize.ts | 9 --------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/client/src/components/lib/Canvas.tsx b/client/src/components/lib/Canvas.tsx index ee2d777..c53b07f 100644 --- a/client/src/components/lib/Canvas.tsx +++ b/client/src/components/lib/Canvas.tsx @@ -412,15 +412,11 @@ export default function Canvas() { action === 'drawing' ? currentDrawingElemId.current : selectedElementIds[0]; - const { x1, y1, x2, y2 } = adjustElementCoordinatesById( - id, - { - p1, - p2, - types, - }, - types[id] === 'image', - ); + const { x1, y1, x2, y2 } = adjustElementCoordinatesById(id, { + p1, + p2, + types, + }); updateElement(id, x1, y1, x2, y2, types[id]); } diff --git a/client/src/lib/canvasElements/resize.ts b/client/src/lib/canvasElements/resize.ts index 5b3c10f..878272c 100644 --- a/client/src/lib/canvasElements/resize.ts +++ b/client/src/lib/canvasElements/resize.ts @@ -169,17 +169,8 @@ export const adjustElementCoordinatesById = ( p2: Record; types: Record; }, - skip = false, ) => { const { p1, p2, types } = appState; - if (skip) { - return { - x1: p1[elementId].x, - y1: p1[elementId].y, - x2: p2[elementId].x, - y2: p2[elementId].y, - }; - } return adjustElementCoordinates( p1[elementId], p2[elementId],