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
|
||||
|
||||
import org.bigbluebutton.core.util.jhotdraw.BezierWrapper
|
||||
import scala.collection.immutable.List
|
||||
import scala.collection.immutable.HashMap
|
||||
import scala.collection.JavaConverters._
|
||||
import org.bigbluebutton.common2.msgs.AnnotationVO
|
||||
import org.bigbluebutton.core.apps.whiteboard.Whiteboard
|
||||
import org.bigbluebutton.SystemConfiguration
|
||||
@ -24,86 +21,83 @@ class WhiteboardModel extends SystemConfiguration {
|
||||
}
|
||||
|
||||
private def createWhiteboard(wbId: String): Whiteboard = {
|
||||
new Whiteboard(
|
||||
Whiteboard(
|
||||
wbId,
|
||||
Array.empty[String],
|
||||
Array.empty[String],
|
||||
System.currentTimeMillis(),
|
||||
new HashMap[String, Map[String, AnnotationVO]]()
|
||||
new HashMap[String, AnnotationVO]
|
||||
)
|
||||
}
|
||||
|
||||
private def getAnnotationsByUserId(wb: Whiteboard, id: String): Map[String, AnnotationVO] = {
|
||||
wb.annotationsMap.get(id).getOrElse(Map[String, AnnotationVO]())
|
||||
}
|
||||
private def deepMerge(test: Map[String, _], that: Map[String, _]): Map[String, _] =
|
||||
(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 usersAnnotations = getAnnotationsByUserId(wb, userId)
|
||||
var newUserAnnotations = usersAnnotations
|
||||
var newAnnotationsMap = wb.annotationsMap
|
||||
for (annotation <- annotations) {
|
||||
newUserAnnotations = newUserAnnotations + (annotation.id -> annotation)
|
||||
println("Adding annotation to page [" + wb.id + "]. After numAnnotations=[" + newUserAnnotations.size + "].")
|
||||
val oldAnnotation = wb.annotationsMap.get(annotation.id)
|
||||
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)
|
||||
saveWhiteboard(newWb)
|
||||
annotations
|
||||
annotationsAdded
|
||||
}
|
||||
|
||||
def getHistory(wbId: String): Array[AnnotationVO] = {
|
||||
//wb.annotationsMap.values.flatten.toArray.sortBy(_.position);
|
||||
val wb = getWhiteboard(wbId)
|
||||
var annotations = Array[AnnotationVO]()
|
||||
// TODO: revisit this, probably there is a one-liner simple solution
|
||||
wb.annotationsMap.values.foreach(
|
||||
user => user.values.foreach(
|
||||
annotation => annotations = annotations :+ annotation
|
||||
)
|
||||
)
|
||||
annotations
|
||||
wb.annotationsMap.values.toArray
|
||||
}
|
||||
|
||||
def clearWhiteboard(wbId: String, userId: String): Option[Boolean] = {
|
||||
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] = {
|
||||
def deleteAnnotations(wbId: String, userId: String, annotationsIds: Array[String], isPresenter: Boolean, isModerator: Boolean): Array[String] = {
|
||||
var annotationsIdsRemoved = Array[String]()
|
||||
val wb = getWhiteboard(wbId)
|
||||
var newAnnotationsMap = wb.annotationsMap
|
||||
|
||||
val usersAnnotations = getAnnotationsByUserId(wb, userId)
|
||||
var newUserAnnotations = usersAnnotations
|
||||
for (annotationId <- annotationsIds) {
|
||||
val annotation = usersAnnotations.get(annotationId)
|
||||
val annotation = wb.annotationsMap.get(annotationId)
|
||||
|
||||
//not empty and annotation exists
|
||||
if (!usersAnnotations.isEmpty && !annotation.isEmpty) {
|
||||
newUserAnnotations = newUserAnnotations - annotationId
|
||||
println("Removing annotation on page [" + wb.id + "]. After numAnnotations=[" + newUserAnnotations.size + "].")
|
||||
annotationsIdsRemoved = annotationsIdsRemoved :+ annotationId
|
||||
if (!annotation.isEmpty) {
|
||||
val hasPermission = isPresenter || isModerator || annotation.get.userId == userId
|
||||
if (hasPermission) {
|
||||
newAnnotationsMap -= 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)
|
||||
saveWhiteboard(newWb)
|
||||
annotationsIdsRemoved
|
||||
|
@ -55,7 +55,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
outGW.send(notifyEvent)
|
||||
|
||||
LockSettingsUtil.enforceCamLockSettingsForAllUsers(liveMeeting, outGW)
|
||||
|
||||
|
||||
// Dial-in
|
||||
def buildLockMessage(meetingId: String, userId: String, lockedBy: String, locked: Boolean): BbbCommonEnvCoreMsg = {
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
fullClear <- clearWhiteboard(msg.body.whiteboardId, msg.header.userId, liveMeeting)
|
||||
} yield {
|
||||
broadcastEvent(msg, fullClear)
|
||||
}
|
||||
log.error("Ignoring message ClearWhiteboardPubMsg since this functions is not available in the new Whiteboard")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,14 +21,24 @@ trait DeleteWhiteboardAnnotationsPubMsgHdlr extends RightsManagementTrait {
|
||||
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)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "No permission to delete an annotation."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
}
|
||||
} 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) {
|
||||
broadcastEvent(msg, deletedAnnotations)
|
||||
}
|
||||
|
@ -46,13 +46,18 @@ trait SendWhiteboardAnnotationsPubMsgHdlr extends RightsManagementTrait {
|
||||
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) {
|
||||
println("============= Printing Sanitized annotations ============")
|
||||
for (annotation <- msg.body.annotations) {
|
||||
printAnnotationInfo(annotation)
|
||||
}
|
||||
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)
|
||||
} else {
|
||||
//val meetingId = liveMeeting.props.meetingProp.intId
|
||||
|
@ -11,7 +11,7 @@ case class Whiteboard(
|
||||
multiUser: Array[String],
|
||||
oldMultiUser: Array[String],
|
||||
changedModeOn: Long,
|
||||
annotationsMap: Map[String, Map[String, AnnotationVO]]
|
||||
annotationsMap: Map[String, AnnotationVO]
|
||||
)
|
||||
|
||||
class WhiteboardApp2x(implicit val context: ActorContext)
|
||||
@ -24,9 +24,16 @@ class WhiteboardApp2x(implicit val context: ActorContext)
|
||||
|
||||
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 + "]")
|
||||
liveMeeting.wbModel.addAnnotations(whiteboardId, requesterId, annotations)
|
||||
liveMeeting.wbModel.addAnnotations(whiteboardId, requesterId, annotations, isPresenter, isModerator)
|
||||
}
|
||||
|
||||
def getWhiteboardAnnotations(whiteboardId: String, liveMeeting: LiveMeeting): Array[AnnotationVO] = {
|
||||
@ -34,12 +41,15 @@ class WhiteboardApp2x(implicit val context: ActorContext)
|
||||
liveMeeting.wbModel.getHistory(whiteboardId)
|
||||
}
|
||||
|
||||
def clearWhiteboard(whiteboardId: String, requesterId: String, liveMeeting: LiveMeeting): Option[Boolean] = {
|
||||
liveMeeting.wbModel.clearWhiteboard(whiteboardId, requesterId)
|
||||
}
|
||||
|
||||
def deleteWhiteboardAnnotations(whiteboardId: String, requesterId: String, annotationsIds: Array[String], liveMeeting: LiveMeeting): Array[String] = {
|
||||
liveMeeting.wbModel.deleteAnnotations(whiteboardId, requesterId, annotationsIds)
|
||||
def deleteWhiteboardAnnotations(
|
||||
whiteboardId: String,
|
||||
requesterId: String,
|
||||
annotationsIds: Array[String],
|
||||
liveMeeting: LiveMeeting,
|
||||
isPresenter: Boolean,
|
||||
isModerator: Boolean
|
||||
): Array[String] = {
|
||||
liveMeeting.wbModel.deleteAnnotations(whiteboardId, requesterId, annotationsIds, isPresenter, isModerator)
|
||||
}
|
||||
|
||||
def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Array[String] = {
|
||||
|
@ -112,7 +112,7 @@ object Polls {
|
||||
shape = pollResultToWhiteboardShape(result)
|
||||
annot <- send(result, shape)
|
||||
} 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)
|
||||
(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 _ from "lodash";
|
||||
|
||||
export default function addAnnotation(meetingId, whiteboardId, userId, annotation) {
|
||||
export default function addAnnotation(meetingId, whiteboardId, userId, annotation, Annotations) {
|
||||
check(meetingId, String);
|
||||
check(whiteboardId, String);
|
||||
check(annotation, Object);
|
||||
|
||||
const {
|
||||
id, annotationInfo, wbId,
|
||||
id, wbId,
|
||||
} = annotation;
|
||||
|
||||
let { annotationInfo } = annotation;
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
id,
|
||||
userId,
|
||||
};
|
||||
|
||||
const oldAnnotation = Annotations.findOne(selector);
|
||||
if (oldAnnotation) {
|
||||
annotationInfo = _.merge(oldAnnotation.annotationInfo, annotationInfo)
|
||||
}
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
whiteboardId,
|
||||
|
@ -8,7 +8,7 @@ export default function addAnnotation(meetingId, whiteboardId, userId, annotatio
|
||||
check(whiteboardId, String);
|
||||
check(annotation, Object);
|
||||
|
||||
const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation);
|
||||
const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation, Annotations);
|
||||
|
||||
try {
|
||||
const { insertedId } = Annotations.upsert(query.selector, query.modifier);
|
||||
|
@ -95,8 +95,6 @@ class Presentation extends PureComponent {
|
||||
this.setIsPanning = this.setIsPanning.bind(this);
|
||||
this.handlePanShortcut = this.handlePanShortcut.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.renderCurrentPresentationToast = this.renderCurrentPresentationToast.bind(this);
|
||||
@ -192,7 +190,7 @@ class Presentation extends PureComponent {
|
||||
clearFakeAnnotations,
|
||||
} = this.props;
|
||||
|
||||
const { presentationWidth, presentationHeight, zoom, isPanning } = this.state;
|
||||
const { presentationWidth, presentationHeight, zoom, isPanning, fitToWidth } = this.state;
|
||||
const {
|
||||
numCameras: prevNumCameras,
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -326,13 +324,6 @@ class Presentation extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setIsPanning() {
|
||||
this.setState({
|
||||
isPanning: !this.state.isPanning,
|
||||
});
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
const presentationSizes = this.getPresentationSizesAvailable();
|
||||
if (Object.keys(presentationSizes).length > 0) {
|
||||
|
@ -106,9 +106,9 @@ class PresentationToolbar extends PureComponent {
|
||||
document.addEventListener('keydown', this.switchSlide);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { zoom, setIsPanning } = this.props;
|
||||
if (zoom <= HUNDRED_PERCENT && zoom !== prevProps.zoom) setIsPanning();
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { zoom, setIsPanning, fitToWidth } = this.props;
|
||||
if (zoom <= HUNDRED_PERCENT && zoom !== prevProps.zoom && !fitToWidth) setIsPanning();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -402,7 +402,7 @@ class PresentationToolbar extends PureComponent {
|
||||
data-test="panButton"
|
||||
aria-label={intl.formatMessage(intlMessages.pan)}
|
||||
color="light"
|
||||
disabled={(zoom <= HUNDRED_PERCENT)}
|
||||
disabled={(zoom <= HUNDRED_PERCENT && !fitToWidth)}
|
||||
icon="hand"
|
||||
size="md"
|
||||
circle
|
||||
|
@ -3,13 +3,8 @@ import _ from "lodash";
|
||||
import { createGlobalStyle } from "styled-components";
|
||||
import Cursors from "./cursors/container";
|
||||
import { TldrawApp, Tldraw } from "@tldraw/tldraw";
|
||||
import {
|
||||
ColorStyle,
|
||||
DashStyle,
|
||||
SizeStyle,
|
||||
TDShapeType,
|
||||
} from "@tldraw/tldraw";
|
||||
import SlideCalcUtil, {HUNDRED_PERCENT} from '/imports/utils/slideCalcUtils';
|
||||
import { Utils } from "@tldraw/core";
|
||||
|
||||
function usePrevious(value) {
|
||||
const ref = React.useRef();
|
||||
@ -42,10 +37,13 @@ const TldrawGlobalStyle = createGlobalStyle`
|
||||
export default function Whiteboard(props) {
|
||||
const {
|
||||
isPresenter,
|
||||
isModerator,
|
||||
removeShapes,
|
||||
initDefaultPages,
|
||||
persistShape,
|
||||
notifyNotAllowedChange,
|
||||
shapes,
|
||||
assets,
|
||||
currentUser,
|
||||
curPres,
|
||||
whiteboardId,
|
||||
@ -54,7 +52,6 @@ export default function Whiteboard(props) {
|
||||
skipToSlide,
|
||||
slidePosition,
|
||||
curPageId,
|
||||
svgUri,
|
||||
presentationWidth,
|
||||
presentationHeight,
|
||||
isViewersCursorLocked,
|
||||
@ -66,6 +63,7 @@ export default function Whiteboard(props) {
|
||||
width,
|
||||
height,
|
||||
isPanning,
|
||||
intl,
|
||||
} = props;
|
||||
|
||||
const { pages, pageStates } = initDefaultPages(curPres?.pages.length || 1);
|
||||
@ -97,66 +95,137 @@ export default function Whiteboard(props) {
|
||||
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 currentDoc = rDocument.current;
|
||||
|
||||
let next = { ...currentDoc };
|
||||
|
||||
let pageBindings = null;
|
||||
let history = null;
|
||||
let stack = null;
|
||||
let changed = false;
|
||||
|
||||
if (next.pageStates[curPageId] && !_.isEqual(prevShapes, shapes)) {
|
||||
// mergeDocument loses bindings and history, save it
|
||||
pageBindings = tldrawAPI?.getPage(curPageId)?.bindings;
|
||||
history = tldrawAPI?.history
|
||||
stack = tldrawAPI?.stack
|
||||
// set shapes as locked for those who aren't allowed to edit it
|
||||
Object.entries(shapes).forEach(([shapeId, shape]) => {
|
||||
if (!shape.isLocked && !hasShapeAccess(shapeId)) {
|
||||
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;
|
||||
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (curPageId && next.pages[curPageId] && !next.pages[curPageId].shapes["slide-background-shape"]) {
|
||||
next.assets[`slide-background-asset-${curPageId}`] = {
|
||||
id: `slide-background-asset-${curPageId}`,
|
||||
size: [slidePosition?.width || 0, slidePosition?.height || 0],
|
||||
src: svgUri,
|
||||
type: "image",
|
||||
};
|
||||
if (curPageId && !next.assets[`slide-background-asset-${curPageId}`]) {
|
||||
next.assets[`slide-background-asset-${curPageId}`] = assets[`slide-background-asset-${curPageId}`]
|
||||
tldrawAPI?.patchState(
|
||||
{
|
||||
document: {
|
||||
assets: assets
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
next.pages[curPageId].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,
|
||||
if (changed && tldrawAPI) {
|
||||
// merge patch manually (this improves performance and reduce side effects on fast updates)
|
||||
const patch = {
|
||||
document: {
|
||||
pages: {
|
||||
[curPageId]: { shapes: shapes }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
if (pageBindings) next.pages[curPageId].bindings = pageBindings;
|
||||
tldrawAPI?.mergeDocument(next);
|
||||
if (tldrawAPI && history) tldrawAPI.history = history;
|
||||
if (tldrawAPI && stack) tldrawAPI.stack = stack;
|
||||
const prevState = tldrawAPI._state;
|
||||
const nextState = Utils.deepMerge(tldrawAPI._state, patch);
|
||||
const final = tldrawAPI.cleanup(nextState, prevState, patch, '');
|
||||
tldrawAPI._state = final;
|
||||
tldrawAPI?.forceUpdate();
|
||||
}
|
||||
|
||||
// move poll result text to bottom right
|
||||
if (next.pages[curPageId]) {
|
||||
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) {
|
||||
if (_.isEqual(shape.point, [0, 0])) {
|
||||
const shapeBounds = tldrawAPI?.getShapeBounds(id);
|
||||
@ -209,8 +278,11 @@ export default function Whiteboard(props) {
|
||||
if (cameraZoom && cameraZoom === 1) {
|
||||
tldrawAPI?.setCamera([slidePosition.x, slidePosition.y], newzoom);
|
||||
} 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
|
||||
setTimeout(() => {
|
||||
tldrawAPI?.setCamera([slidePosition.x, slidePosition.y], newzoom, 'zoomed');
|
||||
@ -252,32 +324,34 @@ export default function Whiteboard(props) {
|
||||
}
|
||||
}, [zoomValue]);
|
||||
|
||||
// update zoom when presenter changes
|
||||
// update zoom when presenter changes if the aspectRatio has changed
|
||||
React.useEffect(() => {
|
||||
if (tldrawAPI && isPresenter && curPageId && slidePosition && !isMounting) {
|
||||
const currentAspectRatio = Math.round((presentationWidth / presentationHeight) * 100) / 100;
|
||||
const previousAspectRatio = Math.round((slidePosition.viewBoxWidth / slidePosition.viewBoxHeight) * 100) / 100;
|
||||
if (previousAspectRatio !== currentAspectRatio && fitToWidth) {
|
||||
const zoom = calculateZoom(slidePosition.width, slidePosition.height)
|
||||
tldrawAPI?.setCamera([0, 0], zoom);
|
||||
const viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(tldrawAPI?.viewport.width, slidePosition.height);
|
||||
zoomSlide(parseInt(curPageId), podId, HUNDRED_PERCENT, viewedRegionH, 0, 0);
|
||||
setZoom(HUNDRED_PERCENT);
|
||||
zoomChanger(HUNDRED_PERCENT);
|
||||
} else {
|
||||
let viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(tldrawAPI?.viewport.height, slidePosition.width);
|
||||
let viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(tldrawAPI?.viewport.width, slidePosition.height);
|
||||
const camera = tldrawAPI?.getPageState()?.camera;
|
||||
const zoomFitSlide = calculateZoom(slidePosition.width, slidePosition.height);
|
||||
if (!fitToWidth && camera.zoom === zoomFitSlide) {
|
||||
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;
|
||||
if (zoom !== zoomToolbar) {
|
||||
setZoom(zoomToolbar);
|
||||
zoomChanger(zoomToolbar);
|
||||
if (previousAspectRatio !== currentAspectRatio) {
|
||||
if (fitToWidth) {
|
||||
const zoom = calculateZoom(slidePosition.width, slidePosition.height)
|
||||
tldrawAPI?.setCamera([0, 0], zoom);
|
||||
const viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(tldrawAPI?.viewport.width, slidePosition.height);
|
||||
zoomSlide(parseInt(curPageId), podId, HUNDRED_PERCENT, viewedRegionH, 0, 0);
|
||||
setZoom(HUNDRED_PERCENT);
|
||||
zoomChanger(HUNDRED_PERCENT);
|
||||
} else if (!isMounting) {
|
||||
let viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(tldrawAPI?.viewport.height, slidePosition.width);
|
||||
let viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(tldrawAPI?.viewport.width, slidePosition.height);
|
||||
const camera = tldrawAPI?.getPageState()?.camera;
|
||||
const zoomFitSlide = calculateZoom(slidePosition.width, slidePosition.height);
|
||||
if (!fitToWidth && camera.zoom === zoomFitSlide) {
|
||||
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;
|
||||
if (zoom !== zoomToolbar) {
|
||||
setZoom(zoomToolbar);
|
||||
zoomChanger(zoomToolbar);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -335,47 +409,53 @@ export default function Whiteboard(props) {
|
||||
app.onPan = () => {};
|
||||
app.setSelectedIds = () => {};
|
||||
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) {
|
||||
app.changePage(curPageId);
|
||||
setIsMounting(true);
|
||||
}
|
||||
};
|
||||
|
||||
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"))) {
|
||||
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);
|
||||
if (patchedShape?.type === 'text') {
|
||||
if (!shapes[patchedShape.id]) {
|
||||
patchedShape.userId = currentUser?.userId;
|
||||
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 dockPos = webcams?.getAttribute("data-position");
|
||||
const editableWB = (
|
||||
@ -466,174 +616,9 @@ export default function Whiteboard(props) {
|
||||
showMultiplayerMenu={false}
|
||||
readOnly={false}
|
||||
onPatch={onPatch}
|
||||
onUndo={(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);
|
||||
}
|
||||
}}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onUndo={onUndo}
|
||||
onRedo={onRedo}
|
||||
onCommand={onCommand}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -6,6 +6,14 @@ import { UsersContext } from "../components-data/users-context/context";
|
||||
import Auth from "/imports/ui/services/auth";
|
||||
import PresentationToolbarService from '../presentation/presentation-toolbar/service';
|
||||
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 usingUsersContext = useContext(UsersContext);
|
||||
@ -15,13 +23,39 @@ const WhiteboardContainer = (props) => {
|
||||
const { users } = usingUsersContext;
|
||||
const currentUser = users[Auth.meetingID][Auth.userID];
|
||||
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 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 {
|
||||
initDefaultPages: Service.initDefaultPages,
|
||||
persistShape: Service.persistShape,
|
||||
@ -29,10 +63,12 @@ export default withTracker(({ whiteboardId, curPageId, intl, zoomChanger }) => {
|
||||
hasMultiUserAccess: Service.hasMultiUserAccess,
|
||||
changeCurrentSlide: Service.changeCurrentSlide,
|
||||
shapes: shapes,
|
||||
assets: assets,
|
||||
curPres,
|
||||
removeShapes: Service.removeShapes,
|
||||
zoomSlide: PresentationToolbarService.zoomSlide,
|
||||
skipToSlide: PresentationToolbarService.skipToSlide,
|
||||
zoomChanger: zoomChanger,
|
||||
notifyNotAllowedChange: Service.notifyNotAllowedChange,
|
||||
};
|
||||
})(WhiteboardContainer);
|
||||
|
@ -305,7 +305,7 @@ export default function Cursors(props) {
|
||||
{children}
|
||||
</div>
|
||||
{otherCursors
|
||||
.filter((c) => c?.xPercent && c?.yPercent)
|
||||
.filter((c) => c?.xPercent && c.xPercent !== -1.0 && c?.yPercent && c.yPercent !== -1.0)
|
||||
.filter((c) => {
|
||||
if ((isViewersCursorLocked && c?.role !== "VIEWER") || !isViewersCursorLocked || currentUser?.presenter) {
|
||||
return c;
|
||||
|
@ -7,6 +7,8 @@ import { makeCall } from '/imports/ui/services/api';
|
||||
import PresentationService from '/imports/ui/components/presentation/service';
|
||||
import PollService from '/imports/ui/components/poll/service';
|
||||
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);
|
||||
|
||||
@ -14,6 +16,13 @@ const UnsentAnnotations = new Mongo.Collection(null);
|
||||
const ANNOTATION_CONFIG = Meteor.settings.public.whiteboard.annotations;
|
||||
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;
|
||||
|
||||
const clearPreview = (annotation) => {
|
||||
@ -32,7 +41,7 @@ function handleAddedAnnotation({
|
||||
annotation,
|
||||
}) {
|
||||
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);
|
||||
|
||||
@ -146,7 +155,12 @@ const sendAnnotation = (annotation) => {
|
||||
// reconnected. With this it will miss things
|
||||
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)
|
||||
setTimeout(proccessAnnotationsQueue, annotationsBufferTimeMin);
|
||||
};
|
||||
@ -295,7 +309,7 @@ const persistShape = (shape, whiteboardId) => {
|
||||
id: shape.id,
|
||||
annotationInfo: shape,
|
||||
wbId: whiteboardId,
|
||||
userId: shape.userId ? shape.userId : Auth.userID,
|
||||
userId: Auth.userID,
|
||||
};
|
||||
|
||||
sendAnnotation(annotation);
|
||||
@ -343,8 +357,8 @@ const getShapes = (whiteboardId, curPageId, intl) => {
|
||||
dash: "draw"
|
||||
},
|
||||
}
|
||||
annotation.annotationInfo.questionType = false;
|
||||
}
|
||||
annotation.annotationInfo.userId = annotation.userId;
|
||||
result[annotation.annotationInfo.id] = annotation.annotationInfo;
|
||||
});
|
||||
return result;
|
||||
@ -379,6 +393,10 @@ const initDefaultPages = (count = 1) => {
|
||||
return { pages, pageStates };
|
||||
};
|
||||
|
||||
const notifyNotAllowedChange = (intl) => {
|
||||
if (intl) notify(intl.formatMessage(intlMessages.notifyNotAllowedChange), 'warning', 'whiteboard');
|
||||
};
|
||||
|
||||
export {
|
||||
initDefaultPages,
|
||||
Annotations,
|
||||
@ -402,4 +420,5 @@ export {
|
||||
removeShapes,
|
||||
changeCurrentSlide,
|
||||
clearFakeAnnotations,
|
||||
notifyNotAllowedChange,
|
||||
};
|
||||
|
@ -952,6 +952,7 @@
|
||||
"app.whiteboard.annotations.poll": "Poll results were published",
|
||||
"app.whiteboard.annotations.pollResult": "Poll Result",
|
||||
"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.hand": "Pan",
|
||||
"app.whiteboard.toolbar.tools.pencil": "Pencil",
|
||||
|
@ -825,6 +825,7 @@
|
||||
"app.whiteboard.annotations.poll": "Os resultados da enquete foram publicados",
|
||||
"app.whiteboard.annotations.pollResult": "Resultado da Enquete",
|
||||
"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.hand": "Mover",
|
||||
"app.whiteboard.toolbar.tools.pencil": "Lápis",
|
||||
|
@ -31,6 +31,7 @@ require 'yaml'
|
||||
require 'builder'
|
||||
require 'fastimage' # require fastimage to get the image size of the slides (gem install fastimage)
|
||||
require 'json'
|
||||
require "active_support"
|
||||
|
||||
# This script lives in scripts/archive/steps while properties.yaml lives in scripts/
|
||||
bbb_props = BigBlueButton.read_props
|
||||
@ -607,12 +608,13 @@ def events_parse_tldraw_shape(shapes, event, current_presentation, current_slide
|
||||
prev_shape = nil
|
||||
if shape_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
|
||||
end
|
||||
if prev_shape
|
||||
prev_shape[:out] = timestamp
|
||||
shape[:shape_unique_id] = prev_shape[:shape_unique_id]
|
||||
shape[:shape_data] = prev_shape[:shape_data].deep_merge(shape[:shape_data])
|
||||
else
|
||||
shape[:shape_unique_id] = @svg_shape_unique_id
|
||||
@svg_shape_unique_id += 1
|
||||
|
Loading…
Reference in New Issue
Block a user