2018-06-28 22:27:40 +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';
|
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);
|
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;
|
2020-04-07 04:34:08 +08:00
|
|
|
const DRAW_UPDATE = ANNOTATION_CONFIG.status.update;
|
2018-08-02 04:15:52 +08:00
|
|
|
const DRAW_END = ANNOTATION_CONFIG.status.end;
|
2020-04-07 04:34:08 +08:00
|
|
|
|
|
|
|
const ANNOTATION_TYPE_PENCIL = 'pencil';
|
2018-08-23 23:42:34 +08:00
|
|
|
|
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
|
|
|
|
2018-06-28 22:27:40 +08:00
|
|
|
function clearFakeAnnotations() {
|
2020-04-07 04:34:08 +08:00
|
|
|
UnsentAnnotations.remove({});
|
2018-06-28 22:27:40 +08:00
|
|
|
}
|
|
|
|
|
2018-05-12 00:01:24 +08:00
|
|
|
function handleAddedAnnotation({
|
|
|
|
meetingId, whiteboardId, userId, annotation,
|
|
|
|
}) {
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleRemovedAnnotation({
|
|
|
|
meetingId, whiteboardId, userId, shapeId,
|
|
|
|
}) {
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
Annotations.remove(query);
|
|
|
|
}
|
|
|
|
|
2019-10-23 09:26:25 +08:00
|
|
|
export function initAnnotationsStreamListener() {
|
2020-10-16 21:30:50 +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.
|
|
|
|
*/
|
|
|
|
annotationsStreamListener = new Meteor.Streamer(`annotations-${Auth.meetingID}`, { retransmit: false });
|
|
|
|
|
|
|
|
const startStreamHandlersPromise = new Promise((resolve) => {
|
|
|
|
const checkStreamHandlersInterval = setInterval(() => {
|
2020-01-14 00:43:31 +08:00
|
|
|
const streamHandlersSize = Object.values(Meteor.StreamerCentral.instances[`annotations-${Auth.meetingID}`].handlers)
|
2020-04-07 04:34:08 +08:00
|
|
|
.filter(el => el !== undefined)
|
2020-01-14 00:43:31 +08:00
|
|
|
.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(() => {
|
2020-10-16 21:30:50 +08:00
|
|
|
logger.debug({ logCode: 'annotations_stream_handler_attach' }, 'Attaching handlers for annotations stream');
|
|
|
|
|
2019-10-23 09:26:25 +08:00
|
|
|
annotationsStreamListener.on('removed', handleRemovedAnnotation);
|
2018-05-12 00:01:24 +08:00
|
|
|
|
2019-10-23 09:26:25 +08:00
|
|
|
annotationsStreamListener.on('added', ({ annotations }) => {
|
2020-04-07 04:34:08 +08:00
|
|
|
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
|
|
|
|
2018-06-26 21:38:43 +08:00
|
|
|
function increaseBrightness(realHex, percent) {
|
|
|
|
let hex = parseInt(realHex, 10).toString(16).padStart(6, 0);
|
2018-05-12 00:01:24 +08:00
|
|
|
// strip the leading # if it's there
|
|
|
|
hex = hex.replace(/^\s*#|\s*$/g, '');
|
|
|
|
|
|
|
|
// convert 3 char codes --> 6, e.g. `E0F` --> `EE00FF`
|
2018-06-26 21:38:43 +08:00
|
|
|
if (hex.length === 3) {
|
2018-05-12 00:01:24 +08:00
|
|
|
hex = hex.replace(/(.)/g, '$1$1');
|
|
|
|
}
|
|
|
|
|
2018-06-26 21:38:43 +08:00
|
|
|
const r = parseInt(hex.substr(0, 2), 16);
|
|
|
|
const g = parseInt(hex.substr(2, 2), 16);
|
|
|
|
const b = parseInt(hex.substr(4, 2), 16);
|
2018-05-12 00:01:24 +08:00
|
|
|
|
2018-06-26 21:38:43 +08:00
|
|
|
/* eslint-disable no-bitwise, no-mixed-operators */
|
2019-06-29 05:45:50 +08:00
|
|
|
return parseInt(((0 | (1 << 8) + r + ((256 - r) * percent) / 100).toString(16)).substr(1)
|
|
|
|
+ ((0 | (1 << 8) + g + ((256 - g) * percent) / 100).toString(16)).substr(1)
|
|
|
|
+ ((0 | (1 << 8) + b + ((256 - b) * percent) / 100).toString(16)).substr(1), 16);
|
2018-06-26 21:38:43 +08:00
|
|
|
/* eslint-enable no-bitwise, no-mixed-operators */
|
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);
|
|
|
|
|
2021-03-25 20:47:57 +08:00
|
|
|
const isAnnotationSent = await makeCall('sendBulkAnnotations', annotations);
|
|
|
|
|
|
|
|
if (!isAnnotationSent) {
|
|
|
|
// undo splice
|
|
|
|
annotationsQueue.splice(0, 0, ...annotations);
|
|
|
|
setTimeout(proccessAnnotationsQueue, annotationsRetryDelay);
|
|
|
|
} else {
|
|
|
|
// ask tiago
|
|
|
|
const delayPerc = Math.min(annotationsMaxDelayQueueSize, queueSize) / annotationsMaxDelayQueueSize;
|
|
|
|
const delayDelta = annotationsBufferTimeMax - annotationsBufferTimeMin;
|
|
|
|
const delayTime = annotationsBufferTimeMin + (delayDelta * delayPerc);
|
|
|
|
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;
|
|
|
|
|
2020-04-07 04:34:08 +08:00
|
|
|
if (annotation.status === DRAW_END) {
|
|
|
|
annotationsQueue.push(annotation);
|
|
|
|
if (!annotationsSenderIsRunning) setTimeout(proccessAnnotationsQueue, annotationsBufferTimeMin);
|
|
|
|
} else {
|
|
|
|
const { position, ...relevantAnotation } = annotation;
|
|
|
|
const queryFake = addAnnotationQuery(
|
|
|
|
Auth.meetingID, annotation.wbId, Auth.userID,
|
|
|
|
{
|
|
|
|
...relevantAnotation,
|
|
|
|
id: `${annotation.id}`,
|
|
|
|
position: Number.MAX_SAFE_INTEGER,
|
|
|
|
annotationInfo: {
|
|
|
|
...annotation.annotationInfo,
|
|
|
|
color: increaseBrightness(annotation.annotationInfo.color, 40),
|
|
|
|
},
|
2018-05-12 00:01:24 +08:00
|
|
|
},
|
2020-04-07 04:34:08 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
// This is a really hacky solution, but because of the previous code reuse we need to edit
|
|
|
|
// the pencil draw update modifier so that it sets the whole array instead of pushing to
|
|
|
|
// the end
|
|
|
|
const { status, annotationType } = relevantAnotation;
|
|
|
|
if (status === DRAW_UPDATE && annotationType === ANNOTATION_TYPE_PENCIL) {
|
|
|
|
delete queryFake.modifier.$push;
|
|
|
|
queryFake.modifier.$set['annotationInfo.points'] = annotation.annotationInfo.points;
|
|
|
|
}
|
2018-06-26 21:38:43 +08:00
|
|
|
|
2020-04-07 04:34:08 +08:00
|
|
|
UnsentAnnotations.upsert(queryFake.selector, queryFake.modifier);
|
|
|
|
}
|
|
|
|
};
|
2018-05-12 00:01:24 +08:00
|
|
|
|
2018-06-28 22:27:40 +08:00
|
|
|
WhiteboardMultiUser.find({ meetingId: Auth.meetingID }).observeChanges({
|
|
|
|
changed: clearFakeAnnotations,
|
|
|
|
});
|
|
|
|
|
2019-07-25 01:59:04 +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,
|
|
|
|
}, { fields: { multiUser: 1 } },
|
|
|
|
);
|
|
|
|
|
|
|
|
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,
|
|
|
|
userId: { $in: multiUser },
|
|
|
|
presenter: false,
|
|
|
|
}, { fields: { userId: 1 } },
|
|
|
|
).fetch();
|
|
|
|
|
|
|
|
return multiUserSize.length;
|
|
|
|
};
|
|
|
|
|
|
|
|
const getCurrentWhiteboardId = () => {
|
2021-04-12 22:06:43 +08:00
|
|
|
const podId = 'DEFAULT_PRESENTATION_POD';
|
|
|
|
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,
|
|
|
|
}, { fields: { id: 1 } },
|
|
|
|
);
|
|
|
|
|
|
|
|
return currentSlide && currentSlide.id;
|
|
|
|
}
|
|
|
|
|
|
|
|
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) => {
|
|
|
|
makeCall('addGlobalAccess', whiteboardId);
|
|
|
|
};
|
|
|
|
|
|
|
|
const addIndividualAccess = (whiteboardId, userId) => {
|
|
|
|
makeCall('addIndividualAccess', whiteboardId, userId);
|
|
|
|
};
|
|
|
|
|
|
|
|
const removeGlobalAccess = (whiteboardId) => {
|
|
|
|
makeCall('removeGlobalAccess', whiteboardId);
|
|
|
|
};
|
|
|
|
|
|
|
|
const removeIndividualAccess = (whiteboardId, userId) => {
|
|
|
|
makeCall('removeIndividualAccess', whiteboardId, userId);
|
|
|
|
};
|
|
|
|
|
2020-04-07 04:34:08 +08:00
|
|
|
export {
|
|
|
|
Annotations,
|
|
|
|
UnsentAnnotations,
|
|
|
|
sendAnnotation,
|
|
|
|
clearPreview,
|
2021-03-05 06:26:25 +08:00
|
|
|
getMultiUser,
|
|
|
|
getMultiUserSize,
|
|
|
|
getCurrentWhiteboardId,
|
|
|
|
isMultiUserActive,
|
|
|
|
hasMultiUserAccess,
|
|
|
|
changeWhiteboardAccess,
|
|
|
|
addGlobalAccess,
|
|
|
|
addIndividualAccess,
|
|
|
|
removeGlobalAccess,
|
|
|
|
removeIndividualAccess,
|
2020-04-07 04:34:08 +08:00
|
|
|
};
|