Merge pull request #15636 from germanocaumo/tldraw-shape-updates
This commit is contained in:
commit
861c42cecf
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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] = {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user