From f7468b6fc2552f3d92c63001eb9a11bad47be344 Mon Sep 17 00:00:00 2001 From: germanocaumo Date: Thu, 19 Sep 2024 12:38:57 -0300 Subject: [PATCH] fix(tldraw): only send diffs updates to server When a shape is changed, the full shape objcect was being transmitted to the server again. Do a diff to only send what changed (similarly as it was in tldraw v1) to save upload bw. TODO: Draw segments diffs (array) is still not working, so all the segments are still being sent every time. --- .../core/apps/WhiteboardModel.scala | 22 ++++--------- .../ui/components/whiteboard/component.jsx | 11 ++++++- .../imports/ui/components/whiteboard/utils.js | 33 +++++++++++++++++-- .../scripts/publish/presentation.rb | 16 +++++++++ 4 files changed, 64 insertions(+), 18 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala index 6a32281844..9cf6fea73f 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala @@ -50,6 +50,7 @@ class WhiteboardModel extends SystemConfiguration { val wb = getWhiteboard(wbId) var annotationsAdded = Array[AnnotationVO]() + var annotationsDiffAdded = Array[AnnotationVO]() var newAnnotationsMap = wb.annotationsMap for (annotation <- annotations) { @@ -57,21 +58,10 @@ class WhiteboardModel extends SystemConfiguration { if (oldAnnotation.isDefined) { val hasPermission = isPresenter || isModerator || oldAnnotation.get.userId == userId if (hasPermission) { - // Determine if the annotation is a line shape - val isLineShape = annotation.annotationInfo.get("type").contains("line") - - // Merge old and new annotation properties with special handling for line shape - val mergedAnnotationInfo = if (isLineShape) { - val newProps = annotation.annotationInfo.get("props").asInstanceOf[Option[Map[String, Any]]].getOrElse(Map.empty) - val oldProps = oldAnnotation.get.annotationInfo.get("props").asInstanceOf[Option[Map[String, Any]]].getOrElse(Map.empty) - val updatedProps = overwriteLineShapeHandles(oldProps, newProps) - annotation.annotationInfo + ("props" -> updatedProps) - } else { - deepMerge(oldAnnotation.get.annotationInfo, annotation.annotationInfo) - } + val mergedAnnotationInfo = deepMerge(oldAnnotation.get.annotationInfo, annotation.annotationInfo) // Apply cleaning if it's an arrow annotation - val finalAnnotationInfo = if (annotation.annotationInfo.get("type").contains("arrow")) { + val finalAnnotationInfo = if (oldAnnotation.get.annotationInfo.get("type").contains("arrow")) { cleanArrowAnnotationProps(mergedAnnotationInfo) } else { mergedAnnotationInfo @@ -80,6 +70,7 @@ class WhiteboardModel extends SystemConfiguration { val newAnnotation = oldAnnotation.get.copy(annotationInfo = finalAnnotationInfo) newAnnotationsMap += (annotation.id -> newAnnotation) annotationsAdded :+= newAnnotation + annotationsDiffAdded :+= annotation println(s"Updated annotation on page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].") } else { println(s"User $userId doesn't have permission to edit annotation ${annotation.id}, ignoring...") @@ -87,6 +78,7 @@ class WhiteboardModel extends SystemConfiguration { } else if (annotation.annotationInfo.contains("type")) { newAnnotationsMap += (annotation.id -> annotation) annotationsAdded :+= annotation + annotationsDiffAdded :+= annotation println(s"Adding annotation to page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].") } else { println(s"New annotation [${annotation.id}] with no type, ignoring...") @@ -97,7 +89,7 @@ class WhiteboardModel extends SystemConfiguration { val newWb = wb.copy(annotationsMap = newAnnotationsMap) saveWhiteboard(newWb) - annotationsAdded + annotationsDiffAdded } private def overwriteLineShapeHandles(oldProps: Map[String, Any], newProps: Map[String, Any]): Map[String, Any] = { @@ -188,4 +180,4 @@ class WhiteboardModel extends SystemConfiguration { } def getChangedModeOn(wbId: String): Long = getWhiteboard(wbId).changedModeOn -} \ No newline at end of file +} diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 45304835be..413bb7325d 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -25,6 +25,7 @@ import { mapLanguage, isValidShapeType, usePrevious, + getDifferences, } from './utils'; import { useMouseEvents, useCursor } from './hooks'; import { notifyShapeNumberExceeded, getCustomEditorAssetUrls, getCustomAssetUrls } from './service'; @@ -507,7 +508,15 @@ const Whiteboard = React.memo((props) => { }, }; - shapeBatchRef.current[updatedRecord.id] = updatedRecord; + const diff = getDifferences(prevShapesRef.current[record?.id], updatedRecord); + + if (diff) { + diff.id = record.id; + + shapeBatchRef.current[updatedRecord.id] = diff; + } else { + shapeBatchRef.current[updatedRecord.id] = updatedRecord; + } }); // Handle removed shapes immediately (not batched) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/utils.js b/bigbluebutton-html5/imports/ui/components/whiteboard/utils.js index d31a28aa42..a7cdea0d1e 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/utils.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/utils.js @@ -30,11 +30,40 @@ const mapLanguage = (language) => { } }; +const getDifferences = (prev, next) => { + // Check if the two values are the same + if (prev === next) return undefined; + + // If both are arrays, find the differences + if (Array.isArray(prev) && Array.isArray(next)) { + const differences = next.filter((item, index) => prev[index] !== item); + return differences.length > 0 ? differences : []; + } + + // If both are objects, recursively check their properties + if (typeof prev === 'object' && typeof next === 'object' && prev !== null && next !== null) { + const differences = {}; + + Object.keys(next).forEach((key) => { + if (Object.prototype.hasOwnProperty.call(next, key)) { + const diff = getDifferences(prev[key], next[key]); + if (diff !== undefined) { + differences[key] = diff; + } + } + }); + return Object.keys(differences).length > 0 ? differences : undefined; + } + + // For other types, return the next value if different + return next; +}; + const Utils = { - usePrevious, mapLanguage, isValidShapeType, + usePrevious, mapLanguage, isValidShapeType, getDifferences, }; export default Utils; export { - usePrevious, mapLanguage, isValidShapeType, + usePrevious, mapLanguage, isValidShapeType, getDifferences, }; diff --git a/record-and-playback/presentation/scripts/publish/presentation.rb b/record-and-playback/presentation/scripts/publish/presentation.rb index 03ba75d967..a2951334a6 100755 --- a/record-and-playback/presentation/scripts/publish/presentation.rb +++ b/record-and-playback/presentation/scripts/publish/presentation.rb @@ -575,6 +575,17 @@ def determine_slide_number(slide, current_slide) slide end +def clean_arrow_end_or_start_props(props) + if props['type'] == 'binding' + # Remove 'x' and 'y' for 'binding' type + props = props.except('x', 'y') + elsif props['type'] == 'point' + # Remove unwanted properties for 'point' type + props = props.except('boundShapeId', 'normalizedAnchor', 'isExact', 'isPrecise') + end + props +end + def events_parse_tldraw_shape(shapes, event, current_presentation, current_slide, timestamp) presentation = event.at_xpath('presentation') slide = event.at_xpath('pageNumber') @@ -615,6 +626,11 @@ def events_parse_tldraw_shape(shapes, event, current_presentation, current_slide prev_shape[:out] = timestamp shape[:shape_unique_id] = prev_shape[:shape_unique_id] shape[:shape_data] = prev_shape[:shape_data].deep_merge(shape[:shape_data]) + # special handling to remove unwanted merged arrow props in tldraw v2 + if shape[:shape_data]['type'] == 'arrow' && shape[:shape_data].key?('props') + shape[:shape_data]['props']['start'] = clean_arrow_end_or_start_props(shape[:shape_data]['props']['start']) + shape[:shape_data]['props']['end'] = clean_arrow_end_or_start_props(shape[:shape_data]['props']['end']) + end else shape[:shape_unique_id] = @svg_shape_unique_id @svg_shape_unique_id += 1