2021-10-20 04:35:39 +08:00
|
|
|
import Users from '/imports/api/users';
|
2018-05-12 00:01:24 +08:00
|
|
|
import Auth from '/imports/ui/services/auth';
|
2021-03-05 06:26:25 +08:00
|
|
|
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user';
|
2018-05-12 00:01:24 +08:00
|
|
|
import addAnnotationQuery from '/imports/api/annotations/addAnnotation';
|
2021-03-05 06:26:25 +08:00
|
|
|
import { Slides } from '/imports/api/slides';
|
2019-02-14 00:15:09 +08:00
|
|
|
import { makeCall } from '/imports/ui/services/api';
|
2021-04-12 22:06:43 +08:00
|
|
|
import PresentationService from '/imports/ui/components/presentation/service';
|
2022-06-01 21:47:28 +08:00
|
|
|
import PollService from '/imports/ui/components/poll/service';
|
2020-10-21 22:50:06 +08:00
|
|
|
import logger from '/imports/startup/client/logger';
|
2018-05-12 00:01:24 +08:00
|
|
|
|
|
|
|
const Annotations = new Mongo.Collection(null);
|
2022-04-26 21:55:05 +08:00
|
|
|
|
2020-04-07 04:34:08 +08:00
|
|
|
const UnsentAnnotations = new Mongo.Collection(null);
|
2018-08-02 04:15:52 +08:00
|
|
|
const ANNOTATION_CONFIG = Meteor.settings.public.whiteboard.annotations;
|
|
|
|
const DRAW_END = ANNOTATION_CONFIG.status.end;
|
2019-10-23 09:26:25 +08:00
|
|
|
|
|
|
|
let annotationsStreamListener = null;
|
|
|
|
|
2020-04-07 04:34:08 +08:00
|
|
|
const clearPreview = (annotation) => {
|
|
|
|
UnsentAnnotations.remove({ id: annotation });
|
|
|
|
};
|
2018-05-12 00:01:24 +08:00
|
|
|
|
2022-05-06 04:44:23 +08:00
|
|
|
const clearFakeAnnotations = () => {
|
2020-04-07 04:34:08 +08:00
|
|
|
UnsentAnnotations.remove({});
|
2022-08-09 04:07:41 +08:00
|
|
|
Annotations.remove({ id: /-fake/g, annotationType: { $ne: 'text' } });
|
2022-03-09 04:37:37 +08:00
|
|
|
}
|
|
|
|
|
2018-05-12 00:01:24 +08:00
|
|
|
function handleAddedAnnotation({
|
2022-04-05 22:49:13 +08:00
|
|
|
meetingId,
|
|
|
|
whiteboardId,
|
|
|
|
userId,
|
|
|
|
annotation,
|
2018-05-12 00:01:24 +08:00
|
|
|
}) {
|
|
|
|
const isOwn = Auth.meetingID === meetingId && Auth.userID === userId;
|
|
|
|
const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation);
|
|
|
|
|
2020-04-07 04:34:08 +08:00
|
|
|
Annotations.upsert(query.selector, query.modifier);
|
2018-09-25 06:43:54 +08:00
|
|
|
|
2020-04-07 04:34:08 +08:00
|
|
|
if (isOwn) {
|
|
|
|
UnsentAnnotations.remove({ id: `${annotation.id}` });
|
2018-05-12 00:01:24 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-05 22:49:13 +08:00
|
|
|
function handleRemovedAnnotation({ meetingId, whiteboardId, userId, shapeId }) {
|
2018-05-12 00:01:24 +08:00
|
|
|
const query = { meetingId, whiteboardId };
|
|
|
|
|
|
|
|
if (userId) {
|
|
|
|
query.userId = userId;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (shapeId) {
|
2020-04-07 04:34:08 +08:00
|
|
|
query.id = shapeId;
|
2018-05-12 00:01:24 +08:00
|
|
|
}
|
2022-03-09 04:37:37 +08:00
|
|
|
const annotationIsFake = Annotations.remove(query) === 0;
|
|
|
|
if (annotationIsFake) {
|
|
|
|
query.id = { $in: [shapeId, `${shapeId}-fake`] };
|
|
|
|
Annotations.remove(query);
|
|
|
|
}
|
2018-05-12 00:01:24 +08:00
|
|
|
}
|
|
|
|
|
2019-10-23 09:26:25 +08:00
|
|
|
export function initAnnotationsStreamListener() {
|
2022-04-05 22:49:13 +08:00
|
|
|
logger.info(
|
|
|
|
{ logCode: "init_annotations_stream_listener" },
|
|
|
|
"initAnnotationsStreamListener called"
|
|
|
|
);
|
2020-01-13 20:34:54 +08:00
|
|
|
/**
|
|
|
|
* We create a promise to add the handlers after a ddp subscription stop.
|
|
|
|
* The problem was caused because we add handlers to stream before the onStop event happens,
|
|
|
|
* which set the handlers to undefined.
|
|
|
|
*/
|
2022-04-05 22:49:13 +08:00
|
|
|
annotationsStreamListener = new Meteor.Streamer(
|
|
|
|
`annotations-${Auth.meetingID}`,
|
|
|
|
{ retransmit: false }
|
|
|
|
);
|
2020-01-13 20:34:54 +08:00
|
|
|
|
|
|
|
const startStreamHandlersPromise = new Promise((resolve) => {
|
|
|
|
const checkStreamHandlersInterval = setInterval(() => {
|
2022-04-05 22:49:13 +08:00
|
|
|
const streamHandlersSize = Object.values(
|
|
|
|
Meteor.StreamerCentral.instances[`annotations-${Auth.meetingID}`]
|
|
|
|
.handlers
|
|
|
|
).filter((el) => el !== undefined).length;
|
2020-01-13 20:34:54 +08:00
|
|
|
|
2020-01-14 00:43:31 +08:00
|
|
|
if (!streamHandlersSize) {
|
2020-01-13 20:34:54 +08:00
|
|
|
resolve(clearInterval(checkStreamHandlersInterval));
|
|
|
|
}
|
|
|
|
}, 250);
|
|
|
|
});
|
2019-10-23 09:26:25 +08:00
|
|
|
|
2020-01-13 20:34:54 +08:00
|
|
|
startStreamHandlersPromise.then(() => {
|
2022-04-05 22:49:13 +08:00
|
|
|
logger.debug(
|
|
|
|
{ logCode: "annotations_stream_handler_attach" },
|
|
|
|
"Attaching handlers for annotations stream"
|
|
|
|
);
|
|
|
|
|
|
|
|
annotationsStreamListener.on("removed", handleRemovedAnnotation);
|
|
|
|
|
|
|
|
annotationsStreamListener.on("added", ({ annotations }) => {
|
|
|
|
annotations.forEach((annotation) => handleAddedAnnotation(annotation));
|
2019-10-23 09:26:25 +08:00
|
|
|
});
|
2020-01-13 20:34:54 +08:00
|
|
|
});
|
2019-10-23 09:26:25 +08:00
|
|
|
}
|
2018-05-12 00:01:24 +08:00
|
|
|
|
2019-02-14 00:15:09 +08:00
|
|
|
const annotationsQueue = [];
|
2018-05-12 00:01:24 +08:00
|
|
|
// How many packets we need to have to use annotationsBufferTimeMax
|
|
|
|
const annotationsMaxDelayQueueSize = 60;
|
|
|
|
// Minimum bufferTime
|
|
|
|
const annotationsBufferTimeMin = 30;
|
|
|
|
// Maximum bufferTime
|
|
|
|
const annotationsBufferTimeMax = 200;
|
2021-03-25 20:47:57 +08:00
|
|
|
// Time before running 'sendBulkAnnotations' again if user is offline
|
|
|
|
const annotationsRetryDelay = 1000;
|
|
|
|
|
2018-05-12 00:01:24 +08:00
|
|
|
let annotationsSenderIsRunning = false;
|
|
|
|
|
2019-02-14 00:15:09 +08:00
|
|
|
const proccessAnnotationsQueue = async () => {
|
2018-05-12 00:01:24 +08:00
|
|
|
annotationsSenderIsRunning = true;
|
|
|
|
const queueSize = annotationsQueue.length;
|
|
|
|
|
|
|
|
if (!queueSize) {
|
|
|
|
annotationsSenderIsRunning = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-02-14 00:15:09 +08:00
|
|
|
const annotations = annotationsQueue.splice(0, queueSize);
|
|
|
|
|
2022-04-05 22:49:13 +08:00
|
|
|
const isAnnotationSent = await makeCall("sendBulkAnnotations", annotations);
|
2021-03-25 20:47:57 +08:00
|
|
|
|
|
|
|
if (!isAnnotationSent) {
|
|
|
|
// undo splice
|
|
|
|
annotationsQueue.splice(0, 0, ...annotations);
|
|
|
|
setTimeout(proccessAnnotationsQueue, annotationsRetryDelay);
|
|
|
|
} else {
|
|
|
|
// ask tiago
|
2022-04-05 22:49:13 +08:00
|
|
|
const delayPerc =
|
|
|
|
Math.min(annotationsMaxDelayQueueSize, queueSize) /
|
|
|
|
annotationsMaxDelayQueueSize;
|
2021-03-25 20:47:57 +08:00
|
|
|
const delayDelta = annotationsBufferTimeMax - annotationsBufferTimeMin;
|
2022-04-05 22:49:13 +08:00
|
|
|
const delayTime = annotationsBufferTimeMin + delayDelta * delayPerc;
|
2021-03-25 20:47:57 +08:00
|
|
|
setTimeout(proccessAnnotationsQueue, delayTime);
|
|
|
|
}
|
2018-05-12 00:01:24 +08:00
|
|
|
};
|
|
|
|
|
2020-04-07 04:34:08 +08:00
|
|
|
const sendAnnotation = (annotation) => {
|
2018-06-26 21:28:58 +08:00
|
|
|
// Prevent sending annotations while disconnected
|
2020-04-07 04:34:08 +08:00
|
|
|
// TODO: Change this to add the annotation, but delay the send until we're
|
|
|
|
// reconnected. With this it will miss things
|
2018-06-26 21:28:58 +08:00
|
|
|
if (!Meteor.status().connected) return;
|
|
|
|
|
2022-05-04 20:40:56 +08:00
|
|
|
annotationsQueue.push(annotation);
|
|
|
|
if (!annotationsSenderIsRunning)
|
|
|
|
setTimeout(proccessAnnotationsQueue, annotationsBufferTimeMin);
|
2020-04-07 04:34:08 +08:00
|
|
|
};
|
2018-05-12 00:01:24 +08:00
|
|
|
|
2022-03-09 04:37:37 +08:00
|
|
|
const sendLiveSyncPreviewAnnotation = (annotation) => {
|
|
|
|
// Prevent sending annotations while disconnected
|
|
|
|
if (!Meteor.status().connected) return;
|
|
|
|
|
|
|
|
annotationsQueue.push(annotation);
|
|
|
|
if (!annotationsSenderIsRunning) setTimeout(proccessAnnotationsQueue, annotationsBufferTimeMin);
|
|
|
|
|
|
|
|
// skip optimistic for draw end since the smoothing is done in akka
|
|
|
|
if (annotation.status === DRAW_END) return;
|
|
|
|
|
|
|
|
const { position, ...relevantAnotation } = annotation;
|
|
|
|
const queryFake = addAnnotationQuery(
|
|
|
|
Auth.meetingID, annotation.wbId, Auth.userID,
|
|
|
|
{
|
|
|
|
...relevantAnotation,
|
|
|
|
id: `${annotation.id}-fake`,
|
|
|
|
position: Number.MAX_SAFE_INTEGER,
|
|
|
|
annotationInfo: {
|
|
|
|
...annotation.annotationInfo,
|
|
|
|
color: increaseBrightness(annotation.annotationInfo.color, 40),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
Annotations.upsert(queryFake.selector, queryFake.modifier);
|
|
|
|
};
|
|
|
|
|
2018-06-28 22:27:40 +08:00
|
|
|
WhiteboardMultiUser.find({ meetingId: Auth.meetingID }).observeChanges({
|
|
|
|
changed: clearFakeAnnotations,
|
|
|
|
});
|
|
|
|
|
2022-04-05 22:49:13 +08:00
|
|
|
Users.find(
|
|
|
|
{ userId: Auth.userID },
|
|
|
|
{ fields: { presenter: 1 } }
|
|
|
|
).observeChanges({
|
2018-06-28 22:27:40 +08:00
|
|
|
changed(id, { presenter }) {
|
|
|
|
if (presenter === false) clearFakeAnnotations();
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2021-03-05 06:26:25 +08:00
|
|
|
const getMultiUser = (whiteboardId) => {
|
|
|
|
const data = WhiteboardMultiUser.findOne(
|
|
|
|
{
|
|
|
|
meetingId: Auth.meetingID,
|
|
|
|
whiteboardId,
|
2022-04-05 22:49:13 +08:00
|
|
|
},
|
|
|
|
{ fields: { multiUser: 1 } }
|
2021-03-05 06:26:25 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
if (!data || !data.multiUser || !Array.isArray(data.multiUser)) return [];
|
|
|
|
|
|
|
|
return data.multiUser;
|
|
|
|
};
|
|
|
|
|
|
|
|
const getMultiUserSize = (whiteboardId) => {
|
|
|
|
const multiUser = getMultiUser(whiteboardId);
|
|
|
|
|
|
|
|
if (multiUser.length === 0) return 0;
|
|
|
|
|
|
|
|
// Individual whiteboard access is controlled by an array of userIds.
|
|
|
|
// When an user leaves the meeting or the presenter role moves from an
|
|
|
|
// user to another we applying a filter at the whiteboard collection.
|
|
|
|
// Ideally this should change to something more cohese but this would
|
|
|
|
// require extra changes at multiple backend modules.
|
|
|
|
const multiUserSize = Users.find(
|
|
|
|
{
|
|
|
|
meetingId: Auth.meetingID,
|
2022-08-12 02:44:16 +08:00
|
|
|
$or: [
|
|
|
|
{
|
|
|
|
userId: { $in: multiUser },
|
|
|
|
presenter: false,
|
|
|
|
},
|
|
|
|
{ presenter: true },
|
|
|
|
],
|
2022-04-05 22:49:13 +08:00
|
|
|
},
|
|
|
|
{ fields: { userId: 1 } }
|
2021-03-05 06:26:25 +08:00
|
|
|
).fetch();
|
|
|
|
|
|
|
|
return multiUserSize.length;
|
|
|
|
};
|
|
|
|
|
|
|
|
const getCurrentWhiteboardId = () => {
|
2022-04-05 22:49:13 +08:00
|
|
|
const podId = "DEFAULT_PRESENTATION_POD";
|
2021-04-12 22:06:43 +08:00
|
|
|
const currentPresentation = PresentationService.getCurrentPresentation(podId);
|
|
|
|
|
|
|
|
if (!currentPresentation) return null;
|
|
|
|
|
|
|
|
const currentSlide = Slides.findOne(
|
|
|
|
{
|
|
|
|
podId,
|
|
|
|
presentationId: currentPresentation.id,
|
2021-03-05 06:26:25 +08:00
|
|
|
current: true,
|
2022-04-05 22:49:13 +08:00
|
|
|
},
|
|
|
|
{ fields: { id: 1 } }
|
2021-03-05 06:26:25 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
return currentSlide && currentSlide.id;
|
2022-04-05 22:49:13 +08:00
|
|
|
};
|
2021-03-05 06:26:25 +08:00
|
|
|
|
|
|
|
const isMultiUserActive = (whiteboardId) => {
|
|
|
|
const multiUser = getMultiUser(whiteboardId);
|
|
|
|
|
|
|
|
return multiUser.length !== 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
const hasMultiUserAccess = (whiteboardId, userId) => {
|
|
|
|
const multiUser = getMultiUser(whiteboardId);
|
|
|
|
|
|
|
|
return multiUser.includes(userId);
|
|
|
|
};
|
|
|
|
|
|
|
|
const changeWhiteboardAccess = (userId, access) => {
|
|
|
|
const whiteboardId = getCurrentWhiteboardId();
|
|
|
|
|
|
|
|
if (!whiteboardId) return;
|
|
|
|
|
|
|
|
if (access) {
|
|
|
|
addIndividualAccess(whiteboardId, userId);
|
|
|
|
} else {
|
|
|
|
removeIndividualAccess(whiteboardId, userId);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const addGlobalAccess = (whiteboardId) => {
|
2022-04-05 22:49:13 +08:00
|
|
|
makeCall("addGlobalAccess", whiteboardId);
|
2021-03-05 06:26:25 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
const addIndividualAccess = (whiteboardId, userId) => {
|
2022-04-05 22:49:13 +08:00
|
|
|
makeCall("addIndividualAccess", whiteboardId, userId);
|
2021-03-05 06:26:25 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
const removeGlobalAccess = (whiteboardId) => {
|
2022-04-05 22:49:13 +08:00
|
|
|
makeCall("removeGlobalAccess", whiteboardId);
|
2021-03-05 06:26:25 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
const removeIndividualAccess = (whiteboardId, userId) => {
|
2022-04-05 22:49:13 +08:00
|
|
|
makeCall("removeIndividualAccess", whiteboardId, userId);
|
|
|
|
};
|
|
|
|
|
2022-05-07 00:37:43 +08:00
|
|
|
const persistShape = (shape, whiteboardId) => {
|
2022-05-04 20:40:56 +08:00
|
|
|
const annotation = {
|
|
|
|
id: shape.id,
|
|
|
|
annotationInfo: shape,
|
|
|
|
wbId: whiteboardId,
|
2022-05-21 01:14:32 +08:00
|
|
|
userId: shape.userId ? shape.userId : Auth.userID,
|
2022-05-04 20:40:56 +08:00
|
|
|
};
|
|
|
|
|
2022-05-13 22:51:41 +08:00
|
|
|
sendAnnotation(annotation);
|
2022-04-05 22:49:13 +08:00
|
|
|
};
|
|
|
|
|
2022-05-07 00:37:43 +08:00
|
|
|
const removeShapes = (shapes, whiteboardId) => makeCall("deleteAnnotations", shapes, whiteboardId);
|
2022-04-05 22:49:13 +08:00
|
|
|
|
2022-04-26 21:55:05 +08:00
|
|
|
const changeCurrentSlide = (s) => {
|
|
|
|
makeCall("changeCurrentSlide", s);
|
|
|
|
}
|
2022-05-04 20:40:56 +08:00
|
|
|
|
2022-06-01 03:40:31 +08:00
|
|
|
const getShapes = (whiteboardId, curPageId, intl) => {
|
2022-05-13 23:41:10 +08:00
|
|
|
const annotations = Annotations.find(
|
2022-05-04 20:40:56 +08:00
|
|
|
{
|
|
|
|
whiteboardId,
|
|
|
|
},
|
|
|
|
{
|
2022-05-21 01:14:32 +08:00
|
|
|
fields: { annotationInfo: 1, userId: 1, },
|
2022-05-04 20:40:56 +08:00
|
|
|
},
|
|
|
|
).fetch();
|
|
|
|
|
2022-05-19 03:35:42 +08:00
|
|
|
let result = {};
|
|
|
|
|
|
|
|
annotations.forEach((annotation) => {
|
2022-06-01 03:40:31 +08:00
|
|
|
if (annotation.annotationInfo.questionType) {
|
|
|
|
// poll result, convert it to text and create tldraw shape
|
|
|
|
const pollResult = PollService.getPollResultString(annotation.annotationInfo, intl)
|
|
|
|
.split('<br/>').join('\n').replace( /(<([^>]+)>)/ig, '');
|
|
|
|
annotation.annotationInfo = {
|
|
|
|
childIndex: 2,
|
|
|
|
id: annotation.annotationInfo.id,
|
|
|
|
name: `poll-result-${annotation.annotationInfo.id}`,
|
|
|
|
type: "text",
|
|
|
|
text: pollResult,
|
|
|
|
parentId: `${curPageId}`,
|
|
|
|
point: [0, 0],
|
|
|
|
rotation: 0,
|
|
|
|
style: {
|
|
|
|
isFilled: false,
|
|
|
|
size: "medium",
|
|
|
|
scale: 1,
|
|
|
|
color: "black",
|
|
|
|
textAlign: "start",
|
|
|
|
font: "script",
|
|
|
|
dash: "draw"
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2022-05-21 01:14:32 +08:00
|
|
|
annotation.annotationInfo.userId = annotation.userId;
|
2022-05-19 03:35:42 +08:00
|
|
|
result[annotation.annotationInfo.id] = annotation.annotationInfo;
|
|
|
|
});
|
2022-05-04 20:40:56 +08:00
|
|
|
return result;
|
2022-04-05 22:49:13 +08:00
|
|
|
};
|
|
|
|
|
2022-04-26 21:55:05 +08:00
|
|
|
const getCurrentPres = () => {
|
|
|
|
const podId = "DEFAULT_PRESENTATION_POD";
|
|
|
|
return PresentationService.getCurrentPresentation(podId);
|
|
|
|
}
|
|
|
|
|
|
|
|
const initDefaultPages = (count = 1) => {
|
2022-04-05 22:49:13 +08:00
|
|
|
const pages = {};
|
|
|
|
const pageStates = {};
|
|
|
|
let i = 1;
|
2022-04-26 21:55:05 +08:00
|
|
|
while (i < count + 1) {
|
2022-04-05 22:49:13 +08:00
|
|
|
pages[`${i}`] = {
|
|
|
|
id: `${i}`,
|
|
|
|
name: `Slide ${i}`,
|
|
|
|
shapes: {},
|
|
|
|
bindings: {},
|
|
|
|
};
|
|
|
|
pageStates[`${i}`] = {
|
|
|
|
id: `${i}`,
|
|
|
|
selectedIds: [],
|
|
|
|
camera: {
|
|
|
|
point: [0, 0],
|
|
|
|
zoom: 1,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
return { pages, pageStates };
|
2021-03-05 06:26:25 +08:00
|
|
|
};
|
|
|
|
|
2020-04-07 04:34:08 +08:00
|
|
|
export {
|
2022-04-05 22:49:13 +08:00
|
|
|
initDefaultPages,
|
2020-04-07 04:34:08 +08:00
|
|
|
Annotations,
|
|
|
|
UnsentAnnotations,
|
|
|
|
sendAnnotation,
|
2022-03-09 04:37:37 +08:00
|
|
|
sendLiveSyncPreviewAnnotation,
|
2020-04-07 04:34:08 +08:00
|
|
|
clearPreview,
|
2021-03-05 06:26:25 +08:00
|
|
|
getMultiUser,
|
|
|
|
getMultiUserSize,
|
|
|
|
getCurrentWhiteboardId,
|
|
|
|
isMultiUserActive,
|
|
|
|
hasMultiUserAccess,
|
|
|
|
changeWhiteboardAccess,
|
|
|
|
addGlobalAccess,
|
|
|
|
addIndividualAccess,
|
|
|
|
removeGlobalAccess,
|
|
|
|
removeIndividualAccess,
|
2022-04-05 22:49:13 +08:00
|
|
|
persistShape,
|
|
|
|
getShapes,
|
2022-04-26 21:55:05 +08:00
|
|
|
getCurrentPres,
|
2022-05-04 20:40:56 +08:00
|
|
|
removeShapes,
|
2022-04-26 21:55:05 +08:00
|
|
|
changeCurrentSlide,
|
2022-05-06 04:44:23 +08:00
|
|
|
clearFakeAnnotations,
|
2020-04-07 04:34:08 +08:00
|
|
|
};
|