Merge pull request #15636 from germanocaumo/tldraw-shape-updates

This commit is contained in:
Gustavo Trott 2022-10-27 08:17:23 -03:00 committed by GitHub
commit 861c42cecf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 434 additions and 377 deletions

View File

@ -1,9 +1,6 @@
package org.bigbluebutton.core.apps package org.bigbluebutton.core.apps
import org.bigbluebutton.core.util.jhotdraw.BezierWrapper
import scala.collection.immutable.List
import scala.collection.immutable.HashMap import scala.collection.immutable.HashMap
import scala.collection.JavaConverters._
import org.bigbluebutton.common2.msgs.AnnotationVO import org.bigbluebutton.common2.msgs.AnnotationVO
import org.bigbluebutton.core.apps.whiteboard.Whiteboard import org.bigbluebutton.core.apps.whiteboard.Whiteboard
import org.bigbluebutton.SystemConfiguration import org.bigbluebutton.SystemConfiguration
@ -24,86 +21,83 @@ class WhiteboardModel extends SystemConfiguration {
} }
private def createWhiteboard(wbId: String): Whiteboard = { private def createWhiteboard(wbId: String): Whiteboard = {
new Whiteboard( Whiteboard(
wbId, wbId,
Array.empty[String], Array.empty[String],
Array.empty[String], Array.empty[String],
System.currentTimeMillis(), System.currentTimeMillis(),
new HashMap[String, Map[String, AnnotationVO]]() new HashMap[String, AnnotationVO]
) )
} }
private def getAnnotationsByUserId(wb: Whiteboard, id: String): Map[String, AnnotationVO] = { private def deepMerge(test: Map[String, _], that: Map[String, _]): Map[String, _] =
wb.annotationsMap.get(id).getOrElse(Map[String, AnnotationVO]()) (for (k <- test.keys ++ that.keys) yield {
} val newValue =
(test.get(k), that.get(k)) match {
case (Some(v), None) => v
case (None, Some(v)) => v
case (Some(v1), Some(v2)) =>
if (v1.isInstanceOf[Map[String, _]] && v2.isInstanceOf[Map[String, _]])
deepMerge(v1.asInstanceOf[Map[String, _]], v2.asInstanceOf[Map[String, _]])
else v2
case (_, _) => ???
}
k -> newValue
}).toMap
def addAnnotations(wbId: String, userId: String, annotations: Array[AnnotationVO]): Array[AnnotationVO] = { def addAnnotations(wbId: String, userId: String, annotations: Array[AnnotationVO], isPresenter: Boolean, isModerator: Boolean): Array[AnnotationVO] = {
var annotationsAdded = Array[AnnotationVO]()
val wb = getWhiteboard(wbId) val wb = getWhiteboard(wbId)
val usersAnnotations = getAnnotationsByUserId(wb, userId) var newAnnotationsMap = wb.annotationsMap
var newUserAnnotations = usersAnnotations
for (annotation <- annotations) { for (annotation <- annotations) {
newUserAnnotations = newUserAnnotations + (annotation.id -> annotation) val oldAnnotation = wb.annotationsMap.get(annotation.id)
println("Adding annotation to page [" + wb.id + "]. After numAnnotations=[" + newUserAnnotations.size + "].") if (!oldAnnotation.isEmpty) {
val hasPermission = isPresenter || isModerator || oldAnnotation.get.userId == userId
if (hasPermission) {
val newAnnotation = oldAnnotation.get.copy(annotationInfo = deepMerge(oldAnnotation.get.annotationInfo, annotation.annotationInfo))
newAnnotationsMap += (annotation.id -> newAnnotation)
annotationsAdded :+= annotation
println(s"Updated annotation onpage [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].")
} else {
println(s"User $userId doesn't have permission to edit annotation ${annotation.id}, ignoring...")
}
} else if (annotation.annotationInfo.contains("type")) {
newAnnotationsMap += (annotation.id -> annotation)
annotationsAdded :+= annotation
println(s"Adding annotation to page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].")
} else {
println(s"New annotation [${annotation.id}] with no type, ignoring (probably received a remove message before and now the shape is incomplete, ignoring...")
}
} }
val newAnnotationsMap = wb.annotationsMap + (userId -> newUserAnnotations)
val newWb = wb.copy(annotationsMap = newAnnotationsMap) val newWb = wb.copy(annotationsMap = newAnnotationsMap)
saveWhiteboard(newWb) saveWhiteboard(newWb)
annotations annotationsAdded
} }
def getHistory(wbId: String): Array[AnnotationVO] = { def getHistory(wbId: String): Array[AnnotationVO] = {
//wb.annotationsMap.values.flatten.toArray.sortBy(_.position);
val wb = getWhiteboard(wbId) val wb = getWhiteboard(wbId)
var annotations = Array[AnnotationVO]() wb.annotationsMap.values.toArray
// TODO: revisit this, probably there is a one-liner simple solution
wb.annotationsMap.values.foreach(
user => user.values.foreach(
annotation => annotations = annotations :+ annotation
)
)
annotations
} }
def clearWhiteboard(wbId: String, userId: String): Option[Boolean] = { def deleteAnnotations(wbId: String, userId: String, annotationsIds: Array[String], isPresenter: Boolean, isModerator: Boolean): Array[String] = {
var cleared: Option[Boolean] = None
if (hasWhiteboard(wbId)) {
val wb = getWhiteboard(wbId)
if (wb.multiUser.contains(userId)) {
if (wb.annotationsMap.contains(userId)) {
val newWb = wb.copy(annotationsMap = wb.annotationsMap - userId)
saveWhiteboard(newWb)
cleared = Some(false)
}
} else {
if (wb.annotationsMap.nonEmpty) {
val newWb = wb.copy(annotationsMap = new HashMap[String, Map[String, AnnotationVO]]())
saveWhiteboard(newWb)
cleared = Some(true)
}
}
}
cleared
}
def deleteAnnotations(wbId: String, userId: String, annotationsIds: Array[String]): Array[String] = {
var annotationsIdsRemoved = Array[String]() var annotationsIdsRemoved = Array[String]()
val wb = getWhiteboard(wbId) val wb = getWhiteboard(wbId)
var newAnnotationsMap = wb.annotationsMap
val usersAnnotations = getAnnotationsByUserId(wb, userId)
var newUserAnnotations = usersAnnotations
for (annotationId <- annotationsIds) { for (annotationId <- annotationsIds) {
val annotation = usersAnnotations.get(annotationId) val annotation = wb.annotationsMap.get(annotationId)
//not empty and annotation exists if (!annotation.isEmpty) {
if (!usersAnnotations.isEmpty && !annotation.isEmpty) { val hasPermission = isPresenter || isModerator || annotation.get.userId == userId
newUserAnnotations = newUserAnnotations - annotationId if (hasPermission) {
println("Removing annotation on page [" + wb.id + "]. After numAnnotations=[" + newUserAnnotations.size + "].") newAnnotationsMap -= annotationId
annotationsIdsRemoved = annotationsIdsRemoved :+ annotationId println("Removing annotation on page [" + wb.id + "]. After numAnnotations=[" + newAnnotationsMap.size + "].")
annotationsIdsRemoved :+= annotationId
} else {
println("User doesn't have permission to remove this annotation, ignoring...")
}
} }
} }
val newAnnotationsMap = wb.annotationsMap + (userId -> newUserAnnotations)
val newWb = wb.copy(annotationsMap = newAnnotationsMap) val newWb = wb.copy(annotationsMap = newAnnotationsMap)
saveWhiteboard(newWb) saveWhiteboard(newWb)
annotationsIdsRemoved annotationsIdsRemoved

View File

@ -55,7 +55,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
outGW.send(notifyEvent) outGW.send(notifyEvent)
LockSettingsUtil.enforceCamLockSettingsForAllUsers(liveMeeting, outGW) LockSettingsUtil.enforceCamLockSettingsForAllUsers(liveMeeting, outGW)
// Dial-in // Dial-in
def buildLockMessage(meetingId: String, userId: String, lockedBy: String, locked: Boolean): BbbCommonEnvCoreMsg = { def buildLockMessage(meetingId: String, userId: String, lockedBy: String, locked: Boolean): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId) val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)

View File

@ -28,11 +28,7 @@ trait ClearWhiteboardPubMsgHdlr extends RightsManagementTrait {
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} }
} else { } else {
for { log.error("Ignoring message ClearWhiteboardPubMsg since this functions is not available in the new Whiteboard")
fullClear <- clearWhiteboard(msg.body.whiteboardId, msg.header.userId, liveMeeting)
} yield {
broadcastEvent(msg, fullClear)
}
} }
} }
} }

View File

@ -21,14 +21,24 @@ trait DeleteWhiteboardAnnotationsPubMsgHdlr extends RightsManagementTrait {
bus.outGW.send(msgEvent) bus.outGW.send(msgEvent)
} }
if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { val isUserAmongPresenters = !permissionFailed(
PermissionCheck.GUEST_LEVEL,
PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId
)
val isUserModerator = !permissionFailed(
PermissionCheck.MOD_LEVEL,
PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId
)
if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && !isUserAmongPresenters) {
if (isNonEjectionGracePeriodOver(msg.body.whiteboardId, msg.header.userId, liveMeeting)) { if (isNonEjectionGracePeriodOver(msg.body.whiteboardId, msg.header.userId, liveMeeting)) {
val meetingId = liveMeeting.props.meetingProp.intId val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to delete an annotation." val reason = "No permission to delete an annotation."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} }
} else { } else {
val deletedAnnotations = deleteWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotationsIds, liveMeeting) val deletedAnnotations = deleteWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotationsIds, liveMeeting, isUserAmongPresenters, isUserModerator)
if (!deletedAnnotations.isEmpty) { if (!deletedAnnotations.isEmpty) {
broadcastEvent(msg, deletedAnnotations) broadcastEvent(msg, deletedAnnotations)
} }

View File

@ -46,13 +46,18 @@ trait SendWhiteboardAnnotationsPubMsgHdlr extends RightsManagementTrait {
PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId
) )
val isUserModerator = !permissionFailed(
PermissionCheck.MOD_LEVEL,
PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId
)
if (isUserOneOfPermited || isUserAmongPresenters) { if (isUserOneOfPermited || isUserAmongPresenters) {
println("============= Printing Sanitized annotations ============") println("============= Printing Sanitized annotations ============")
for (annotation <- msg.body.annotations) { for (annotation <- msg.body.annotations) {
printAnnotationInfo(annotation) printAnnotationInfo(annotation)
} }
println("============= Printed Sanitized annotations ============") println("============= Printed Sanitized annotations ============")
val annotations = sendWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotations, liveMeeting) val annotations = sendWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotations, liveMeeting, isUserAmongPresenters, isUserModerator)
broadcastEvent(msg, msg.body.whiteboardId, annotations, msg.body.html5InstanceId) broadcastEvent(msg, msg.body.whiteboardId, annotations, msg.body.html5InstanceId)
} else { } else {
//val meetingId = liveMeeting.props.meetingProp.intId //val meetingId = liveMeeting.props.meetingProp.intId

View File

@ -11,7 +11,7 @@ case class Whiteboard(
multiUser: Array[String], multiUser: Array[String],
oldMultiUser: Array[String], oldMultiUser: Array[String],
changedModeOn: Long, changedModeOn: Long,
annotationsMap: Map[String, Map[String, AnnotationVO]] annotationsMap: Map[String, AnnotationVO]
) )
class WhiteboardApp2x(implicit val context: ActorContext) class WhiteboardApp2x(implicit val context: ActorContext)
@ -24,9 +24,16 @@ class WhiteboardApp2x(implicit val context: ActorContext)
val log = Logging(context.system, getClass) val log = Logging(context.system, getClass)
def sendWhiteboardAnnotations(whiteboardId: String, requesterId: String, annotations: Array[AnnotationVO], liveMeeting: LiveMeeting): Array[AnnotationVO] = { def sendWhiteboardAnnotations(
whiteboardId: String,
requesterId: String,
annotations: Array[AnnotationVO],
liveMeeting: LiveMeeting,
isPresenter: Boolean,
isModerator: Boolean
): Array[AnnotationVO] = {
// println("Received whiteboard annotation. status=[" + status + "], annotationType=[" + annotationType + "]") // println("Received whiteboard annotation. status=[" + status + "], annotationType=[" + annotationType + "]")
liveMeeting.wbModel.addAnnotations(whiteboardId, requesterId, annotations) liveMeeting.wbModel.addAnnotations(whiteboardId, requesterId, annotations, isPresenter, isModerator)
} }
def getWhiteboardAnnotations(whiteboardId: String, liveMeeting: LiveMeeting): Array[AnnotationVO] = { def getWhiteboardAnnotations(whiteboardId: String, liveMeeting: LiveMeeting): Array[AnnotationVO] = {
@ -34,12 +41,15 @@ class WhiteboardApp2x(implicit val context: ActorContext)
liveMeeting.wbModel.getHistory(whiteboardId) liveMeeting.wbModel.getHistory(whiteboardId)
} }
def clearWhiteboard(whiteboardId: String, requesterId: String, liveMeeting: LiveMeeting): Option[Boolean] = { def deleteWhiteboardAnnotations(
liveMeeting.wbModel.clearWhiteboard(whiteboardId, requesterId) whiteboardId: String,
} requesterId: String,
annotationsIds: Array[String],
def deleteWhiteboardAnnotations(whiteboardId: String, requesterId: String, annotationsIds: Array[String], liveMeeting: LiveMeeting): Array[String] = { liveMeeting: LiveMeeting,
liveMeeting.wbModel.deleteAnnotations(whiteboardId, requesterId, annotationsIds) isPresenter: Boolean,
isModerator: Boolean
): Array[String] = {
liveMeeting.wbModel.deleteAnnotations(whiteboardId, requesterId, annotationsIds, isPresenter, isModerator)
} }
def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Array[String] = { def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Array[String] = {

View File

@ -112,7 +112,7 @@ object Polls {
shape = pollResultToWhiteboardShape(result) shape = pollResultToWhiteboardShape(result)
annot <- send(result, shape) annot <- send(result, shape)
} yield { } yield {
lm.wbModel.addAnnotations(annot.wbId, requesterId, Array[AnnotationVO](annot)) lm.wbModel.addAnnotations(annot.wbId, requesterId, Array[AnnotationVO](annot), false, false)
showPollResult(pollId, lm.polls) showPollResult(pollId, lm.polls)
(result, annot) (result, annot)
} }

View File

@ -1 +1 @@
git clone --branch v5.0.0-alpha.2 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback git clone --branch v5.0.0-alpha.3 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback

View File

@ -1,20 +1,27 @@
import { check } from 'meteor/check'; import { check } from 'meteor/check';
import _ from "lodash";
export default function addAnnotation(meetingId, whiteboardId, userId, annotation) { export default function addAnnotation(meetingId, whiteboardId, userId, annotation, Annotations) {
check(meetingId, String); check(meetingId, String);
check(whiteboardId, String); check(whiteboardId, String);
check(annotation, Object); check(annotation, Object);
const { const {
id, annotationInfo, wbId, id, wbId,
} = annotation; } = annotation;
let { annotationInfo } = annotation;
const selector = { const selector = {
meetingId, meetingId,
id, id,
userId,
}; };
const oldAnnotation = Annotations.findOne(selector);
if (oldAnnotation) {
annotationInfo = _.merge(oldAnnotation.annotationInfo, annotationInfo)
}
const modifier = { const modifier = {
$set: { $set: {
whiteboardId, whiteboardId,

View File

@ -8,7 +8,7 @@ export default function addAnnotation(meetingId, whiteboardId, userId, annotatio
check(whiteboardId, String); check(whiteboardId, String);
check(annotation, Object); check(annotation, Object);
const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation); const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation, Annotations);
try { try {
const { insertedId } = Annotations.upsert(query.selector, query.modifier); const { insertedId } = Annotations.upsert(query.selector, query.modifier);

View File

@ -95,8 +95,6 @@ class Presentation extends PureComponent {
this.setIsPanning = this.setIsPanning.bind(this); this.setIsPanning = this.setIsPanning.bind(this);
this.handlePanShortcut = this.handlePanShortcut.bind(this); this.handlePanShortcut = this.handlePanShortcut.bind(this);
this.renderPresentationMenu = this.renderPresentationMenu.bind(this); this.renderPresentationMenu = this.renderPresentationMenu.bind(this);
this.setIsPanning = this.setIsPanning.bind(this);
this.handlePanShortcut = this.handlePanShortcut.bind(this);
this.onResize = () => setTimeout(this.handleResize.bind(this), 0); this.onResize = () => setTimeout(this.handleResize.bind(this), 0);
this.renderCurrentPresentationToast = this.renderCurrentPresentationToast.bind(this); this.renderCurrentPresentationToast = this.renderCurrentPresentationToast.bind(this);
@ -192,7 +190,7 @@ class Presentation extends PureComponent {
clearFakeAnnotations, clearFakeAnnotations,
} = this.props; } = this.props;
const { presentationWidth, presentationHeight, zoom, isPanning } = this.state; const { presentationWidth, presentationHeight, zoom, isPanning, fitToWidth } = this.state;
const { const {
numCameras: prevNumCameras, numCameras: prevNumCameras,
presentationBounds: prevPresentationBounds, presentationBounds: prevPresentationBounds,
@ -294,7 +292,7 @@ class Presentation extends PureComponent {
}); });
} }
if (zoom <= HUNDRED_PERCENT && isPanning || !userIsPresenter && prevProps.userIsPresenter) { if ((zoom <= HUNDRED_PERCENT && isPanning && !fitToWidth) || !userIsPresenter && prevProps.userIsPresenter) {
this.setIsPanning(); this.setIsPanning();
} }
} }
@ -326,13 +324,6 @@ class Presentation extends PureComponent {
}); });
} }
setIsPanning() {
this.setState({
isPanning: !this.state.isPanning,
});
}
handleResize() { handleResize() {
const presentationSizes = this.getPresentationSizesAvailable(); const presentationSizes = this.getPresentationSizesAvailable();
if (Object.keys(presentationSizes).length > 0) { if (Object.keys(presentationSizes).length > 0) {

View File

@ -106,9 +106,9 @@ class PresentationToolbar extends PureComponent {
document.addEventListener('keydown', this.switchSlide); document.addEventListener('keydown', this.switchSlide);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps, prevState) {
const { zoom, setIsPanning } = this.props; const { zoom, setIsPanning, fitToWidth } = this.props;
if (zoom <= HUNDRED_PERCENT && zoom !== prevProps.zoom) setIsPanning(); if (zoom <= HUNDRED_PERCENT && zoom !== prevProps.zoom && !fitToWidth) setIsPanning();
} }
componentWillUnmount() { componentWillUnmount() {
@ -402,7 +402,7 @@ class PresentationToolbar extends PureComponent {
data-test="panButton" data-test="panButton"
aria-label={intl.formatMessage(intlMessages.pan)} aria-label={intl.formatMessage(intlMessages.pan)}
color="light" color="light"
disabled={(zoom <= HUNDRED_PERCENT)} disabled={(zoom <= HUNDRED_PERCENT && !fitToWidth)}
icon="hand" icon="hand"
size="md" size="md"
circle circle

View File

@ -3,13 +3,8 @@ import _ from "lodash";
import { createGlobalStyle } from "styled-components"; import { createGlobalStyle } from "styled-components";
import Cursors from "./cursors/container"; import Cursors from "./cursors/container";
import { TldrawApp, Tldraw } from "@tldraw/tldraw"; import { TldrawApp, Tldraw } from "@tldraw/tldraw";
import {
ColorStyle,
DashStyle,
SizeStyle,
TDShapeType,
} from "@tldraw/tldraw";
import SlideCalcUtil, {HUNDRED_PERCENT} from '/imports/utils/slideCalcUtils'; import SlideCalcUtil, {HUNDRED_PERCENT} from '/imports/utils/slideCalcUtils';
import { Utils } from "@tldraw/core";
function usePrevious(value) { function usePrevious(value) {
const ref = React.useRef(); const ref = React.useRef();
@ -42,10 +37,13 @@ const TldrawGlobalStyle = createGlobalStyle`
export default function Whiteboard(props) { export default function Whiteboard(props) {
const { const {
isPresenter, isPresenter,
isModerator,
removeShapes, removeShapes,
initDefaultPages, initDefaultPages,
persistShape, persistShape,
notifyNotAllowedChange,
shapes, shapes,
assets,
currentUser, currentUser,
curPres, curPres,
whiteboardId, whiteboardId,
@ -54,7 +52,6 @@ export default function Whiteboard(props) {
skipToSlide, skipToSlide,
slidePosition, slidePosition,
curPageId, curPageId,
svgUri,
presentationWidth, presentationWidth,
presentationHeight, presentationHeight,
isViewersCursorLocked, isViewersCursorLocked,
@ -66,6 +63,7 @@ export default function Whiteboard(props) {
width, width,
height, height,
isPanning, isPanning,
intl,
} = props; } = props;
const { pages, pageStates } = initDefaultPages(curPres?.pages.length || 1); const { pages, pageStates } = initDefaultPages(curPres?.pages.length || 1);
@ -97,66 +95,137 @@ export default function Whiteboard(props) {
return zoom; return zoom;
} }
const hasShapeAccess = (id) => {
const owner = shapes[id]?.userId;
const isBackgroundShape = id?.includes('slide-background');
const hasShapeAccess = !isBackgroundShape && ((owner && owner === currentUser?.userId) || !owner || isPresenter || isModerator);
return hasShapeAccess;
}
const sendShapeChanges= (app, changedShapes, redo = false) => {
const invalidChange = Object.keys(changedShapes)
.find(id => !hasShapeAccess(id));
if (invalidChange) {
notifyNotAllowedChange(intl);
// undo last command without persisting to not generate the onUndo/onRedo callback
if (!redo) {
const command = app.stack[app.pointer];
app.pointer--;
return app.applyPatch(command.before, `undo`);
} else {
app.pointer++
const command = app.stack[app.pointer]
return app.applyPatch(command.after, 'redo');
}
};
let deletedShapes = [];
Object.entries(changedShapes)
.forEach(([id, shape]) => {
if (!shape) deletedShapes.push(id);
else {
//checks to find any bindings assosiated with the changed shapes.
//If any, they may need to be updated as well.
const pageBindings = app.page.bindings;
if (pageBindings) {
Object.entries(pageBindings).map(([k,b]) => {
if (b.toId.includes(id)) {
const boundShape = app.getShape(b.fromId);
if (shapes[b.fromId] && !_.isEqual(boundShape, shapes[b.fromId])) {
const shapeBounds = app.getShapeBounds(b.fromId);
boundShape.size = [shapeBounds.width, shapeBounds.height];
persistShape(boundShape, whiteboardId)
}
}
})
}
if (!shape.id) {
// check it already exists (otherwise we need the full shape)
if (!shapes[id]) {
shape = app.getShape(id);
}
shape.id = id;
}
const shapeBounds = app.getShapeBounds(id);
const size = [shapeBounds.width, shapeBounds.height];
if (!shapes[id] || (shapes[id] && !_.isEqual(shapes[id].size, size))) {
shape.size = size;
}
if (!shapes[id] || (shapes[id] && !shapes[id].userId)) shape.userId = currentUser?.userId;
persistShape(shape, whiteboardId);
}
});
removeShapes(deletedShapes, whiteboardId);
}
const doc = React.useMemo(() => { const doc = React.useMemo(() => {
const currentDoc = rDocument.current; const currentDoc = rDocument.current;
let next = { ...currentDoc }; let next = { ...currentDoc };
let pageBindings = null;
let history = null;
let stack = null;
let changed = false; let changed = false;
if (next.pageStates[curPageId] && !_.isEqual(prevShapes, shapes)) { if (next.pageStates[curPageId] && !_.isEqual(prevShapes, shapes)) {
// mergeDocument loses bindings and history, save it // set shapes as locked for those who aren't allowed to edit it
pageBindings = tldrawAPI?.getPage(curPageId)?.bindings; Object.entries(shapes).forEach(([shapeId, shape]) => {
history = tldrawAPI?.history if (!shape.isLocked && !hasShapeAccess(shapeId)) {
stack = tldrawAPI?.stack shape.isLocked = true;
}
});
const removed = prevShapes && findRemoved(Object.keys(prevShapes),Object.keys((shapes)))
if (removed && removed.length > 0) {
tldrawAPI?.patchState(
{
document: {
pageStates: {
[curPageId]: {
selectedIds: tldrawAPI?.selectedIds?.filter(id => !removed.includes(id)) || [],
},
},
pages: {
[curPageId]: {
shapes: Object.fromEntries(removed.map((id) => [id, undefined])),
},
},
},
},
);
}
next.pages[curPageId].shapes = shapes; next.pages[curPageId].shapes = shapes;
changed = true; changed = true;
} }
if (curPageId && next.pages[curPageId] && !next.pages[curPageId].shapes["slide-background-shape"]) { if (curPageId && !next.assets[`slide-background-asset-${curPageId}`]) {
next.assets[`slide-background-asset-${curPageId}`] = { next.assets[`slide-background-asset-${curPageId}`] = assets[`slide-background-asset-${curPageId}`]
id: `slide-background-asset-${curPageId}`, tldrawAPI?.patchState(
size: [slidePosition?.width || 0, slidePosition?.height || 0], {
src: svgUri, document: {
type: "image", assets: assets
}; },
},
);
}
next.pages[curPageId].shapes["slide-background-shape"] = { if (changed && tldrawAPI) {
assetId: `slide-background-asset-${curPageId}`, // merge patch manually (this improves performance and reduce side effects on fast updates)
childIndex: -1, const patch = {
id: "slide-background-shape", document: {
name: "Image", pages: {
type: TDShapeType.Image, [curPageId]: { shapes: shapes }
parentId: `${curPageId}`, },
point: [0, 0],
isLocked: true,
size: [slidePosition?.width || 0, slidePosition?.height || 0],
style: {
dash: DashStyle.Draw,
size: SizeStyle.Medium,
color: ColorStyle.Blue,
}, },
}; };
const prevState = tldrawAPI._state;
changed = true; const nextState = Utils.deepMerge(tldrawAPI._state, patch);
} const final = tldrawAPI.cleanup(nextState, prevState, patch, '');
tldrawAPI._state = final;
if (changed) { tldrawAPI?.forceUpdate();
if (pageBindings) next.pages[curPageId].bindings = pageBindings;
tldrawAPI?.mergeDocument(next);
if (tldrawAPI && history) tldrawAPI.history = history;
if (tldrawAPI && stack) tldrawAPI.stack = stack;
} }
// move poll result text to bottom right // move poll result text to bottom right
if (next.pages[curPageId]) { if (next.pages[curPageId]) {
const pollResults = Object.entries(next.pages[curPageId].shapes) const pollResults = Object.entries(next.pages[curPageId].shapes)
.filter(([id, shape]) => shape.name.includes("poll-result")) .filter(([id, shape]) => shape.name?.includes("poll-result"))
for (const [id, shape] of pollResults) { for (const [id, shape] of pollResults) {
if (_.isEqual(shape.point, [0, 0])) { if (_.isEqual(shape.point, [0, 0])) {
const shapeBounds = tldrawAPI?.getShapeBounds(id); const shapeBounds = tldrawAPI?.getShapeBounds(id);
@ -209,8 +278,11 @@ export default function Whiteboard(props) {
if (cameraZoom && cameraZoom === 1) { if (cameraZoom && cameraZoom === 1) {
tldrawAPI?.setCamera([slidePosition.x, slidePosition.y], newzoom); tldrawAPI?.setCamera([slidePosition.x, slidePosition.y], newzoom);
} else if (isMounting) { } else if (isMounting) {
if (!fitToWidth) { setIsMounting(false);
setIsMounting(false); const currentAspectRatio = Math.round((presentationWidth / presentationHeight) * 100) / 100;
const previousAspectRatio = Math.round((slidePosition.viewBoxWidth / slidePosition.viewBoxHeight) * 100) / 100;
// case where the presenter had fit-to-width enabled and he reloads the page
if (!fitToWidth && currentAspectRatio !== previousAspectRatio) {
// wee need this to ensure tldraw updates the viewport size after re-mounting // wee need this to ensure tldraw updates the viewport size after re-mounting
setTimeout(() => { setTimeout(() => {
tldrawAPI?.setCamera([slidePosition.x, slidePosition.y], newzoom, 'zoomed'); tldrawAPI?.setCamera([slidePosition.x, slidePosition.y], newzoom, 'zoomed');
@ -252,32 +324,34 @@ export default function Whiteboard(props) {
} }
}, [zoomValue]); }, [zoomValue]);
// update zoom when presenter changes // update zoom when presenter changes if the aspectRatio has changed
React.useEffect(() => { React.useEffect(() => {
if (tldrawAPI && isPresenter && curPageId && slidePosition && !isMounting) { if (tldrawAPI && isPresenter && curPageId && slidePosition && !isMounting) {
const currentAspectRatio = Math.round((presentationWidth / presentationHeight) * 100) / 100; const currentAspectRatio = Math.round((presentationWidth / presentationHeight) * 100) / 100;
const previousAspectRatio = Math.round((slidePosition.viewBoxWidth / slidePosition.viewBoxHeight) * 100) / 100; const previousAspectRatio = Math.round((slidePosition.viewBoxWidth / slidePosition.viewBoxHeight) * 100) / 100;
if (previousAspectRatio !== currentAspectRatio && fitToWidth) { if (previousAspectRatio !== currentAspectRatio) {
const zoom = calculateZoom(slidePosition.width, slidePosition.height) if (fitToWidth) {
tldrawAPI?.setCamera([0, 0], zoom); const zoom = calculateZoom(slidePosition.width, slidePosition.height)
const viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(tldrawAPI?.viewport.width, slidePosition.height); tldrawAPI?.setCamera([0, 0], zoom);
zoomSlide(parseInt(curPageId), podId, HUNDRED_PERCENT, viewedRegionH, 0, 0); const viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(tldrawAPI?.viewport.width, slidePosition.height);
setZoom(HUNDRED_PERCENT); zoomSlide(parseInt(curPageId), podId, HUNDRED_PERCENT, viewedRegionH, 0, 0);
zoomChanger(HUNDRED_PERCENT); setZoom(HUNDRED_PERCENT);
} else { zoomChanger(HUNDRED_PERCENT);
let viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(tldrawAPI?.viewport.height, slidePosition.width); } else if (!isMounting) {
let viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(tldrawAPI?.viewport.width, slidePosition.height); let viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(tldrawAPI?.viewport.height, slidePosition.width);
const camera = tldrawAPI?.getPageState()?.camera; let viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(tldrawAPI?.viewport.width, slidePosition.height);
const zoomFitSlide = calculateZoom(slidePosition.width, slidePosition.height); const camera = tldrawAPI?.getPageState()?.camera;
if (!fitToWidth && camera.zoom === zoomFitSlide) { const zoomFitSlide = calculateZoom(slidePosition.width, slidePosition.height);
viewedRegionW = HUNDRED_PERCENT; if (!fitToWidth && camera.zoom === zoomFitSlide) {
viewedRegionH = HUNDRED_PERCENT; viewedRegionW = HUNDRED_PERCENT;
} viewedRegionH = HUNDRED_PERCENT;
zoomSlide(parseInt(curPageId), podId, viewedRegionW, viewedRegionH, camera.point[0], camera.point[1]); }
const zoomToolbar = Math.round((HUNDRED_PERCENT * camera.zoom) / zoomFitSlide * 100) / 100; zoomSlide(parseInt(curPageId), podId, viewedRegionW, viewedRegionH, camera.point[0], camera.point[1]);
if (zoom !== zoomToolbar) { const zoomToolbar = Math.round((HUNDRED_PERCENT * camera.zoom) / zoomFitSlide * 100) / 100;
setZoom(zoomToolbar); if (zoom !== zoomToolbar) {
zoomChanger(zoomToolbar); setZoom(zoomToolbar);
zoomChanger(zoomToolbar);
}
} }
} }
} }
@ -335,47 +409,53 @@ export default function Whiteboard(props) {
app.onPan = () => {}; app.onPan = () => {};
app.setSelectedIds = () => {}; app.setSelectedIds = () => {};
app.setHoveredId = () => {}; app.setHoveredId = () => {};
} else {
// disable hover highlight for background slide shape
app.setHoveredId = (id) => {
if (id?.includes('slide-background')) return null;
app.patchState(
{
document: {
pageStates: {
[app.getPage()?.id]: {
hoveredId: id || [],
},
},
},
},
`set_hovered_id`
);
};
// disable selecting background slide shape
app.setSelectedIds = (ids) => {
ids = ids.filter(id => !id.includes('slide-background'))
app.patchState(
{
document: {
pageStates: {
[app.getPage()?.id]: {
selectedIds: ids || [],
},
},
},
},
`selected`
);
};
} }
if (curPageId) { if (curPageId) {
app.changePage(curPageId); app.changePage(curPageId);
setIsMounting(true);
} }
}; };
const onPatch = (e, t, reason) => { const onPatch = (e, t, reason) => {
// don't allow select others shapes for editing if don't have permission
if (reason && reason.includes("set_editing_id")) {
if (!hasShapeAccess(e.pageState.editingId)) {
e.pageState.editingId = null;
}
}
// don't allow hover others shapes for editing if don't have permission
if (reason && reason.includes("set_hovered_id")) {
if (!hasShapeAccess(e.pageState.hoveredId)) {
e.pageState.hoveredId = null;
}
}
// don't allow select others shapes if don't have permission
if (reason && reason.includes("selected")) {
const validIds = [];
e.pageState.selectedIds.forEach(id => hasShapeAccess(id) && validIds.push(id));
e.pageState.selectedIds = validIds;
e.patchState(
{
document: {
pageStates: {
[e.getPage()?.id]: {
selectedIds: validIds,
},
},
},
}
);
}
// don't allow selecting others shapes with ctrl (brush)
if (e?.session?.type === "brush" && e?.session?.status === "brushing") {
const validIds = [];
e.pageState.selectedIds.forEach(id => hasShapeAccess(id) && validIds.push(id));
e.pageState.selectedIds = validIds;
if (!validIds.find(id => id === e.pageState.hoveredId)) {
e.pageState.hoveredId = undefined;
}
}
if (reason && isPresenter && (reason.includes("zoomed") || reason.includes("panned"))) { if (reason && isPresenter && (reason.includes("zoomed") || reason.includes("panned"))) {
const camera = tldrawAPI.getPageState()?.camera; const camera = tldrawAPI.getPageState()?.camera;
@ -438,14 +518,84 @@ export default function Whiteboard(props) {
} }
} }
if (reason && reason === 'patched_shapes') { if (reason && reason === 'patched_shapes' && e?.session?.type === "edit" && e?.session?.initialShape?.type === "text") {
const patchedShape = e?.getShape(e?.getPageState()?.editingId); const patchedShape = e?.getShape(e?.getPageState()?.editingId);
if (patchedShape?.type === 'text') { if (!shapes[patchedShape.id]) {
patchedShape.userId = currentUser?.userId;
persistShape(patchedShape, whiteboardId); persistShape(patchedShape, whiteboardId);
} else {
const diff = {
id: patchedShape.id,
point: patchedShape.point,
text: patchedShape.text
}
persistShape(diff, whiteboardId);
} }
} }
}; };
const onUndo = (app) => {
if (app.currentPageId !== curPageId) {
if (isPresenter) {
// change slide for others
skipToSlide(Number.parseInt(app.currentPageId), podId)
} else {
// ignore, stay on same page
app.changePage(curPageId);
}
return;
}
const lastCommand = app.stack[app.pointer+1];
const changedShapes = lastCommand?.before?.document?.pages[app.currentPageId]?.shapes;
if (changedShapes) {
sendShapeChanges(app, changedShapes, true);
}
};
const onRedo = (app) => {
if (app.currentPageId !== curPageId) {
if (isPresenter) {
// change slide for others
skipToSlide(Number.parseInt(app.currentPageId), podId)
} else {
// ignore, stay on same page
app.changePage(curPageId);
}
return;
}
const lastCommand = app.stack[app.pointer];
const changedShapes = lastCommand?.after?.document?.pages[app.currentPageId]?.shapes;
if (changedShapes) {
sendShapeChanges(app, changedShapes);
}
};
const onCommand = (app, command, reason) => {
const changedShapes = command.after?.document?.pages[app.currentPageId]?.shapes;
if (!isMounting && app.currentPageId !== curPageId) {
// can happen then the "move to page action" is called, or using undo after changing a page
const newWhiteboardId = curPres.pages.find(page => page.num === Number.parseInt(app.currentPageId)).id;
//remove from previous page and persist on new
changedShapes && removeShapes(Object.keys(changedShapes), whiteboardId);
changedShapes && Object.entries(changedShapes)
.forEach(([id, shape]) => {
const shapeBounds = app.getShapeBounds(id);
shape.size = [shapeBounds.width, shapeBounds.height];
persistShape(shape, newWhiteboardId);
});
if (isPresenter) {
// change slide for others
skipToSlide(Number.parseInt(app.currentPageId), podId)
} else {
// ignore, stay on same page
app.changePage(curPageId);
}
}
else if (changedShapes) {
sendShapeChanges(app, changedShapes);
}
};
const webcams = document.getElementById('cameraDock'); const webcams = document.getElementById('cameraDock');
const dockPos = webcams?.getAttribute("data-position"); const dockPos = webcams?.getAttribute("data-position");
const editableWB = ( const editableWB = (
@ -466,174 +616,9 @@ export default function Whiteboard(props) {
showMultiplayerMenu={false} showMultiplayerMenu={false}
readOnly={false} readOnly={false}
onPatch={onPatch} onPatch={onPatch}
onUndo={(e, s) => { onUndo={onUndo}
e?.selectedIds?.map(id => { onRedo={onRedo}
const shape = e.getShape(id); onCommand={onCommand}
persistShape(shape, whiteboardId);
const children = shape.children;
children && children.forEach(c => {
const childShape = e.getShape(c);
const shapeBounds = e.getShapeBounds(c);
childShape.size = [shapeBounds.width, shapeBounds.height];
persistShape(childShape, whiteboardId)
});
})
const pageShapes = e.state.document.pages[e.getPage()?.id]?.shapes;
let shapesIdsToRemove = findRemoved(Object.keys(shapes), Object.keys(pageShapes))
if (shapesIdsToRemove.length) {
// add a little delay, wee need to make sure children are updated first
setTimeout(() => removeShapes(shapesIdsToRemove, whiteboardId), 200);
}
}}
onRedo={(e, s) => {
e?.selectedIds?.map(id => {
const shape = e.getShape(id);
persistShape(shape, whiteboardId);
const children = shape.children;
children && children.forEach(c => {
const childShape = e.getShape(c);
const shapeBounds = e.getShapeBounds(c);
childShape.size = [shapeBounds.width, shapeBounds.height];
persistShape(childShape, whiteboardId)
});
});
const pageShapes = e.state.document.pages[e.getPage()?.id]?.shapes;
let shapesIdsToRemove = findRemoved(Object.keys(shapes), Object.keys(pageShapes))
if (shapesIdsToRemove.length) {
// add a little delay, wee need to make sure children are updated first
setTimeout(() => removeShapes(shapesIdsToRemove, whiteboardId), 200);
}
}}
onCommand={(e, s, g) => {
if (s?.id.includes('move_to_page')) {
let groupShapes = [];
let nonGroupShapes = [];
let movedShapes = {};
e.selectedIds.forEach(id => {
const shape = e.getShape(id);
if (shape.type === 'group')
groupShapes.push(id);
else
nonGroupShapes.push(id);
movedShapes[id] = e.getShape(id);
});
//remove shapes on origin page
let idsToRemove = nonGroupShapes.concat(groupShapes);
removeShapes(idsToRemove, whiteboardId);
//persist shapes for destination page
const newWhiteboardId = curPres.pages.find(page => page.num === Number.parseInt(e.getPage()?.id)).id;
let idsToInsert = groupShapes.concat(nonGroupShapes);
idsToInsert.forEach(id => {
persistShape(movedShapes[id], newWhiteboardId);
const children = movedShapes[id].children;
children && children.forEach(c => {
persistShape(e.getShape(c), newWhiteboardId)
});
});
if (isPresenter) {
// change slide for others
skipToSlide(Number.parseInt(e.getPage()?.id), podId)
} else {
// ignore, stay on same page
e.changePage(curPageId);
}
return;
}
if (s?.id.includes('ungroup')) {
e?.selectedIds?.map(id => {
persistShape(e.getShape(id), whiteboardId);
})
// check for deleted shapes
const pageShapes = e.state.document.pages[e.getPage()?.id]?.shapes;
let shapesIdsToRemove = findRemoved(Object.keys(shapes), Object.keys(pageShapes))
if (shapesIdsToRemove.length) {
// add a little delay, wee need to make sure children are updated first
setTimeout(() => removeShapes(shapesIdsToRemove, whiteboardId), 200);
}
return;
}
const conditions = [
"session:complete", "style", "updated_shapes", "duplicate", "stretch",
"align", "move", "delete", "create", "flip", "toggle", "group", "translate",
"transform_single", "arrow", "edit", "erase", "rotate",
]
if (conditions.some(el => s?.id?.startsWith(el))) {
e.selectedIds.forEach(id => {
const shape = e.getShape(id);
const shapeBounds = e.getShapeBounds(id);
shape.size = [shapeBounds.width, shapeBounds.height];
persistShape(shape, whiteboardId);
//checks to find any bindings assosiated with the selected shapes.
//If any, they need to be updated as well.
const pageBindings = e.bindings;
const boundShapes = {};
if (pageBindings) {
Object.entries(pageBindings).map(([k,b]) => {
if (b.toId.includes(id)) {
boundShapes[b.fromId] = e.getShape(b.fromId);
}
})
}
//persist shape(s) that was updated by the client and any shapes bound to it.
Object.entries(boundShapes).map(([k,bs]) => {
const shapeBounds = e.getShapeBounds(k);
bs.size = [shapeBounds.width, shapeBounds.height];
persistShape(bs, whiteboardId)
})
const children = e.getShape(id).children;
//also persist children of the selected shape (grouped shapes)
children && children.forEach(c => {
const shape = e.getShape(c);
const shapeBounds = e.getShapeBounds(c);
shape.size = [shapeBounds.width, shapeBounds.height];
persistShape(shape, whiteboardId)
// also persist shapes that are bound to the children
if (pageBindings) {
Object.entries(pageBindings).map(([k,b]) => {
if (!(b.fromId in boundShapes) && b.toId.includes(c)) {
const shape = e.getShape(b.fromId);
persistShape(shape, whiteboardId)
boundShapes[b.fromId] = shape;
}
})
}
})
});
// draw shapes
Object.entries(e.state.document.pages[e.getPage()?.id]?.shapes)
.filter(([k, s]) => s?.type === 'draw')
.forEach(([k, s]) => {
if (!prevShapes[k] && !k.includes('slide-background')) {
const shapeBounds = e.getShapeBounds(k);
s.size = [shapeBounds.width, shapeBounds.height];
persistShape(s, whiteboardId);
}
});
// check for deleted shapes
const pageShapes = e.state.document.pages[e.getPage()?.id]?.shapes;
let shapesIdsToRemove = findRemoved(Object.keys(shapes), Object.keys(pageShapes))
let groups = [];
let nonGroups = [];
// if we have groups, we need to make sure they are removed lastly
shapesIdsToRemove.forEach(shape => {
if (shapes[shape].type === 'group') {
groups.push(shape);
} else {
nonGroups.push(shape);
}
});
if (shapesIdsToRemove.length) {
shapesIdsToRemove = nonGroups.concat(groups);
removeShapes(shapesIdsToRemove, whiteboardId);
}
}
}}
/> />
); );

View File

@ -6,6 +6,14 @@ import { UsersContext } from "../components-data/users-context/context";
import Auth from "/imports/ui/services/auth"; import Auth from "/imports/ui/services/auth";
import PresentationToolbarService from '../presentation/presentation-toolbar/service'; import PresentationToolbarService from '../presentation/presentation-toolbar/service';
import { layoutSelect } from '../layout/context'; import { layoutSelect } from '../layout/context';
import {
ColorStyle,
DashStyle,
SizeStyle,
TDShapeType,
} from "@tldraw/tldraw";
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const WhiteboardContainer = (props) => { const WhiteboardContainer = (props) => {
const usingUsersContext = useContext(UsersContext); const usingUsersContext = useContext(UsersContext);
@ -15,13 +23,39 @@ const WhiteboardContainer = (props) => {
const { users } = usingUsersContext; const { users } = usingUsersContext;
const currentUser = users[Auth.meetingID][Auth.userID]; const currentUser = users[Auth.meetingID][Auth.userID];
const isPresenter = currentUser.presenter; const isPresenter = currentUser.presenter;
return <Whiteboard {...{ isPresenter, currentUser, isRTL, width, height }} {...props} meetingId={Auth.meetingID} /> const isModerator = currentUser.role === ROLE_MODERATOR;
return <Whiteboard {...{ isPresenter, isModerator, currentUser, isRTL, width, height }} {...props} meetingId={Auth.meetingID} />
}; };
export default withTracker(({ whiteboardId, curPageId, intl, zoomChanger }) => { export default withTracker(({ whiteboardId, curPageId, intl, zoomChanger, slidePosition, svgUri }) => {
const shapes = Service.getShapes(whiteboardId, curPageId, intl); const shapes = Service.getShapes(whiteboardId, curPageId, intl);
const curPres = Service.getCurrentPres(); const curPres = Service.getCurrentPres();
shapes["slide-background-shape"] = {
assetId: `slide-background-asset-${curPageId}`,
childIndex: -1,
id: "slide-background-shape",
name: "Image",
type: TDShapeType.Image,
parentId: `${curPageId}`,
point: [0, 0],
isLocked: true,
size: [slidePosition?.width || 0, slidePosition?.height || 0],
style: {
dash: DashStyle.Draw,
size: SizeStyle.Medium,
color: ColorStyle.Blue,
},
};
const assets = {}
assets[`slide-background-asset-${curPageId}`] = {
id: `slide-background-asset-${curPageId}`,
size: [slidePosition?.width || 0, slidePosition?.height || 0],
src: svgUri,
type: "image",
};
return { return {
initDefaultPages: Service.initDefaultPages, initDefaultPages: Service.initDefaultPages,
persistShape: Service.persistShape, persistShape: Service.persistShape,
@ -29,10 +63,12 @@ export default withTracker(({ whiteboardId, curPageId, intl, zoomChanger }) => {
hasMultiUserAccess: Service.hasMultiUserAccess, hasMultiUserAccess: Service.hasMultiUserAccess,
changeCurrentSlide: Service.changeCurrentSlide, changeCurrentSlide: Service.changeCurrentSlide,
shapes: shapes, shapes: shapes,
assets: assets,
curPres, curPres,
removeShapes: Service.removeShapes, removeShapes: Service.removeShapes,
zoomSlide: PresentationToolbarService.zoomSlide, zoomSlide: PresentationToolbarService.zoomSlide,
skipToSlide: PresentationToolbarService.skipToSlide, skipToSlide: PresentationToolbarService.skipToSlide,
zoomChanger: zoomChanger, zoomChanger: zoomChanger,
notifyNotAllowedChange: Service.notifyNotAllowedChange,
}; };
})(WhiteboardContainer); })(WhiteboardContainer);

View File

@ -305,7 +305,7 @@ export default function Cursors(props) {
{children} {children}
</div> </div>
{otherCursors {otherCursors
.filter((c) => c?.xPercent && c?.yPercent) .filter((c) => c?.xPercent && c.xPercent !== -1.0 && c?.yPercent && c.yPercent !== -1.0)
.filter((c) => { .filter((c) => {
if ((isViewersCursorLocked && c?.role !== "VIEWER") || !isViewersCursorLocked || currentUser?.presenter) { if ((isViewersCursorLocked && c?.role !== "VIEWER") || !isViewersCursorLocked || currentUser?.presenter) {
return c; return c;

View File

@ -7,6 +7,8 @@ import { makeCall } from '/imports/ui/services/api';
import PresentationService from '/imports/ui/components/presentation/service'; import PresentationService from '/imports/ui/components/presentation/service';
import PollService from '/imports/ui/components/poll/service'; import PollService from '/imports/ui/components/poll/service';
import logger from '/imports/startup/client/logger'; import logger from '/imports/startup/client/logger';
import { defineMessages } from 'react-intl';
import { notify } from '/imports/ui/services/notification';
const Annotations = new Mongo.Collection(null); const Annotations = new Mongo.Collection(null);
@ -14,6 +16,13 @@ const UnsentAnnotations = new Mongo.Collection(null);
const ANNOTATION_CONFIG = Meteor.settings.public.whiteboard.annotations; const ANNOTATION_CONFIG = Meteor.settings.public.whiteboard.annotations;
const DRAW_END = ANNOTATION_CONFIG.status.end; const DRAW_END = ANNOTATION_CONFIG.status.end;
const intlMessages = defineMessages({
notifyNotAllowedChange: {
id: 'app.whiteboard.annotations.notAllowed',
description: 'Label shown in toast when the user make a change on a shape he doesnt have permission',
},
});
let annotationsStreamListener = null; let annotationsStreamListener = null;
const clearPreview = (annotation) => { const clearPreview = (annotation) => {
@ -32,7 +41,7 @@ function handleAddedAnnotation({
annotation, annotation,
}) { }) {
const isOwn = Auth.meetingID === meetingId && Auth.userID === userId; const isOwn = Auth.meetingID === meetingId && Auth.userID === userId;
const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation); const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation, Annotations);
Annotations.upsert(query.selector, query.modifier); Annotations.upsert(query.selector, query.modifier);
@ -146,7 +155,12 @@ const sendAnnotation = (annotation) => {
// reconnected. With this it will miss things // reconnected. With this it will miss things
if (!Meteor.status().connected) return; if (!Meteor.status().connected) return;
annotationsQueue.push(annotation); const index = annotationsQueue.findIndex(ann => ann.id === annotation.id);
if (index !== -1) {
annotationsQueue[index] = annotation;
} else {
annotationsQueue.push(annotation);
}
if (!annotationsSenderIsRunning) if (!annotationsSenderIsRunning)
setTimeout(proccessAnnotationsQueue, annotationsBufferTimeMin); setTimeout(proccessAnnotationsQueue, annotationsBufferTimeMin);
}; };
@ -295,7 +309,7 @@ const persistShape = (shape, whiteboardId) => {
id: shape.id, id: shape.id,
annotationInfo: shape, annotationInfo: shape,
wbId: whiteboardId, wbId: whiteboardId,
userId: shape.userId ? shape.userId : Auth.userID, userId: Auth.userID,
}; };
sendAnnotation(annotation); sendAnnotation(annotation);
@ -343,8 +357,8 @@ const getShapes = (whiteboardId, curPageId, intl) => {
dash: "draw" dash: "draw"
}, },
} }
annotation.annotationInfo.questionType = false;
} }
annotation.annotationInfo.userId = annotation.userId;
result[annotation.annotationInfo.id] = annotation.annotationInfo; result[annotation.annotationInfo.id] = annotation.annotationInfo;
}); });
return result; return result;
@ -379,6 +393,10 @@ const initDefaultPages = (count = 1) => {
return { pages, pageStates }; return { pages, pageStates };
}; };
const notifyNotAllowedChange = (intl) => {
if (intl) notify(intl.formatMessage(intlMessages.notifyNotAllowedChange), 'warning', 'whiteboard');
};
export { export {
initDefaultPages, initDefaultPages,
Annotations, Annotations,
@ -402,4 +420,5 @@ export {
removeShapes, removeShapes,
changeCurrentSlide, changeCurrentSlide,
clearFakeAnnotations, clearFakeAnnotations,
notifyNotAllowedChange,
}; };

View File

@ -952,6 +952,7 @@
"app.whiteboard.annotations.poll": "Poll results were published", "app.whiteboard.annotations.poll": "Poll results were published",
"app.whiteboard.annotations.pollResult": "Poll Result", "app.whiteboard.annotations.pollResult": "Poll Result",
"app.whiteboard.annotations.noResponses": "No responses", "app.whiteboard.annotations.noResponses": "No responses",
"app.whiteboard.annotations.notAllowed": "You are not allowed to make this change",
"app.whiteboard.toolbar.tools": "Tools", "app.whiteboard.toolbar.tools": "Tools",
"app.whiteboard.toolbar.tools.hand": "Pan", "app.whiteboard.toolbar.tools.hand": "Pan",
"app.whiteboard.toolbar.tools.pencil": "Pencil", "app.whiteboard.toolbar.tools.pencil": "Pencil",

View File

@ -825,6 +825,7 @@
"app.whiteboard.annotations.poll": "Os resultados da enquete foram publicados", "app.whiteboard.annotations.poll": "Os resultados da enquete foram publicados",
"app.whiteboard.annotations.pollResult": "Resultado da Enquete", "app.whiteboard.annotations.pollResult": "Resultado da Enquete",
"app.whiteboard.annotations.noResponses": "Sem respostas", "app.whiteboard.annotations.noResponses": "Sem respostas",
"app.whiteboard.annotations.notAllowed": "Você não tem permissão para fazer essa alteração",
"app.whiteboard.toolbar.tools": "Ferramentas", "app.whiteboard.toolbar.tools": "Ferramentas",
"app.whiteboard.toolbar.tools.hand": "Mover", "app.whiteboard.toolbar.tools.hand": "Mover",
"app.whiteboard.toolbar.tools.pencil": "Lápis", "app.whiteboard.toolbar.tools.pencil": "Lápis",

View File

@ -31,6 +31,7 @@ require 'yaml'
require 'builder' require 'builder'
require 'fastimage' # require fastimage to get the image size of the slides (gem install fastimage) require 'fastimage' # require fastimage to get the image size of the slides (gem install fastimage)
require 'json' require 'json'
require "active_support"
# This script lives in scripts/archive/steps while properties.yaml lives in scripts/ # This script lives in scripts/archive/steps while properties.yaml lives in scripts/
bbb_props = BigBlueButton.read_props bbb_props = BigBlueButton.read_props
@ -607,12 +608,13 @@ def events_parse_tldraw_shape(shapes, event, current_presentation, current_slide
prev_shape = nil prev_shape = nil
if shape_id if shape_id
# If we have a shape ID, look up the previous shape by ID # If we have a shape ID, look up the previous shape by ID
prev_shape_pos = shapes.rindex { |s| s[:shade_id] == shape_id } prev_shape_pos = shapes.rindex { |s| s[:id] == shape_id }
prev_shape = prev_shape_pos ? shapes[prev_shape_pos] : nil prev_shape = prev_shape_pos ? shapes[prev_shape_pos] : nil
end end
if prev_shape if prev_shape
prev_shape[:out] = timestamp prev_shape[:out] = timestamp
shape[:shape_unique_id] = prev_shape[:shape_unique_id] shape[:shape_unique_id] = prev_shape[:shape_unique_id]
shape[:shape_data] = prev_shape[:shape_data].deep_merge(shape[:shape_data])
else else
shape[:shape_unique_id] = @svg_shape_unique_id shape[:shape_unique_id] = @svg_shape_unique_id
@svg_shape_unique_id += 1 @svg_shape_unique_id += 1