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';
|
2022-06-01 21:47:28 +08:00
|
|
|
import PollService from '/imports/ui/components/poll/service';
|
2022-10-21 22:05:31 +08:00
|
|
|
import { defineMessages } from 'react-intl';
|
|
|
|
import { notify } from '/imports/ui/services/notification';
|
2023-01-06 00:40:03 +08:00
|
|
|
import caseInsensitiveReducer from '/imports/utils/caseInsensitiveReducer';
|
2018-05-12 00:01:24 +08:00
|
|
|
|
2022-10-21 22:05:31 +08:00
|
|
|
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',
|
|
|
|
},
|
2023-02-03 03:47:16 +08:00
|
|
|
shapeNumberExceeded: {
|
|
|
|
id: 'app.whiteboard.annotations.numberExceeded',
|
|
|
|
description: 'Label shown in toast when the user tries to add more shapes than the limit',
|
|
|
|
},
|
2022-10-21 22:05:31 +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;
|
|
|
|
|
2024-01-24 19:37:51 +08:00
|
|
|
const proccessAnnotationsQueue = async (submitAnnotations) => {
|
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);
|
|
|
|
|
2024-01-24 20:18:42 +08:00
|
|
|
try {
|
|
|
|
const isAnnotationSent = await submitAnnotations(annotations);
|
|
|
|
|
|
|
|
if (!isAnnotationSent) {
|
|
|
|
// undo splice
|
|
|
|
annotationsQueue.splice(0, 0, ...annotations);
|
|
|
|
setTimeout(() => proccessAnnotationsQueue(submitAnnotations), annotationsRetryDelay);
|
|
|
|
} else {
|
|
|
|
// ask tiago
|
|
|
|
const delayPerc = Math.min(
|
|
|
|
annotationsMaxDelayQueueSize, queueSize,
|
|
|
|
) / annotationsMaxDelayQueueSize;
|
|
|
|
const delayDelta = annotationsBufferTimeMax - annotationsBufferTimeMin;
|
|
|
|
const delayTime = annotationsBufferTimeMin + delayDelta * delayPerc;
|
|
|
|
setTimeout(() => proccessAnnotationsQueue(submitAnnotations), delayTime);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
2021-03-25 20:47:57 +08:00
|
|
|
annotationsQueue.splice(0, 0, ...annotations);
|
2024-01-24 20:18:42 +08:00
|
|
|
setTimeout(() => proccessAnnotationsQueue(submitAnnotations), annotationsRetryDelay);
|
2021-03-25 20:47:57 +08:00
|
|
|
}
|
2018-05-12 00:01:24 +08:00
|
|
|
};
|
|
|
|
|
2024-01-24 19:37:51 +08:00
|
|
|
const sendAnnotation = (annotation, submitAnnotations) => {
|
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;
|
|
|
|
|
2023-03-10 19:30:46 +08:00
|
|
|
const index = annotationsQueue.findIndex((ann) => ann.id === annotation.id);
|
2022-09-03 03:26:26 +08:00
|
|
|
if (index !== -1) {
|
|
|
|
annotationsQueue[index] = annotation;
|
|
|
|
} else {
|
|
|
|
annotationsQueue.push(annotation);
|
|
|
|
}
|
2024-01-24 19:37:51 +08:00
|
|
|
if (!annotationsSenderIsRunning) setTimeout(() => proccessAnnotationsQueue(submitAnnotations), annotationsBufferTimeMin);
|
2022-03-09 04:37:37 +08:00
|
|
|
};
|
|
|
|
|
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
|
|
|
},
|
2023-03-10 19:30:46 +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;
|
|
|
|
};
|
|
|
|
|
2024-01-24 20:18:42 +08:00
|
|
|
const persistShape = async (shape, whiteboardId, isModerator, submitAnnotations) => {
|
2022-05-04 20:40:56 +08:00
|
|
|
const annotation = {
|
|
|
|
id: shape.id,
|
2023-06-06 04:02:06 +08:00
|
|
|
annotationInfo: { ...shape, isModerator },
|
2022-05-04 20:40:56 +08:00
|
|
|
wbId: whiteboardId,
|
2022-10-21 22:05:31 +08:00
|
|
|
userId: Auth.userID,
|
2022-05-04 20:40:56 +08:00
|
|
|
};
|
|
|
|
|
2024-01-24 19:37:51 +08:00
|
|
|
sendAnnotation(annotation, submitAnnotations);
|
2022-04-05 22:49:13 +08:00
|
|
|
};
|
|
|
|
|
2022-04-26 21:55:05 +08:00
|
|
|
const initDefaultPages = (count = 1) => {
|
2022-04-05 22:49:13 +08:00
|
|
|
const pages = {};
|
|
|
|
const pageStates = {};
|
2023-02-28 00:05:29 +08:00
|
|
|
let i = 0;
|
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,
|
|
|
|
},
|
|
|
|
};
|
2023-03-10 19:30:46 +08:00
|
|
|
i += 1;
|
2022-04-05 22:49:13 +08:00
|
|
|
}
|
|
|
|
return { pages, pageStates };
|
2021-03-05 06:26:25 +08:00
|
|
|
};
|
|
|
|
|
2022-10-21 22:05:31 +08:00
|
|
|
const notifyNotAllowedChange = (intl) => {
|
|
|
|
if (intl) notify(intl.formatMessage(intlMessages.notifyNotAllowedChange), 'warning', 'whiteboard');
|
|
|
|
};
|
|
|
|
|
2023-02-03 03:47:16 +08:00
|
|
|
const notifyShapeNumberExceeded = (intl, limit) => {
|
|
|
|
if (intl) notify(intl.formatMessage(intlMessages.shapeNumberExceeded, { 0: limit }), 'warning', 'whiteboard');
|
|
|
|
};
|
|
|
|
|
2023-12-06 02:15:25 +08:00
|
|
|
const toggleToolsAnimations = (activeAnim, anim, time, hasWBAccess = false) => {
|
|
|
|
const handleOptionsDropdown = () => {
|
|
|
|
const optionsDropdown = document.getElementById('WhiteboardOptionButton');
|
|
|
|
if (optionsDropdown) {
|
|
|
|
optionsDropdown.classList.remove(activeAnim);
|
|
|
|
optionsDropdown.style.transition = `opacity ${time} ease-in-out`;
|
|
|
|
optionsDropdown.classList.add(anim);
|
|
|
|
}
|
2023-03-27 06:08:09 +08:00
|
|
|
}
|
2023-12-06 02:15:25 +08:00
|
|
|
|
|
|
|
if (hasWBAccess === false) {
|
|
|
|
return handleOptionsDropdown();
|
2023-08-16 22:32:23 +08:00
|
|
|
}
|
2023-03-27 06:08:09 +08:00
|
|
|
|
2023-12-06 02:15:25 +08:00
|
|
|
const checkElementsAndRun = () => {
|
|
|
|
const tlEls = document.querySelectorAll('.tlui-menu-zone, .tlui-toolbar__tools, .tlui-toolbar__extras, .tlui-style-panel__wrapper');
|
|
|
|
if (tlEls.length) {
|
|
|
|
tlEls?.forEach(el => {
|
|
|
|
el.classList.remove(activeAnim);
|
|
|
|
el.style.transition = `opacity ${time} ease-in-out`;
|
|
|
|
el.classList.add(anim);
|
|
|
|
});
|
|
|
|
handleOptionsDropdown();
|
|
|
|
} else {
|
|
|
|
// If the elements are not yet in the DOM, wait for 50ms and try again
|
|
|
|
setTimeout(checkElementsAndRun, 300);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
checkElementsAndRun();
|
|
|
|
};
|
|
|
|
|
|
|
|
const formatAnnotations = (annotations, intl, curPageId, pollResults, currentPresentationPage) => {
|
2023-09-28 04:42:47 +08:00
|
|
|
const result = {};
|
2023-10-28 00:05:43 +08:00
|
|
|
|
|
|
|
if (pollResults) {
|
|
|
|
// check if pollResults is already added to annotations
|
|
|
|
const hasPollResultsAnnotation = annotations.find(
|
|
|
|
(annotation) => annotation.annotationId === pollResults.pollId,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!hasPollResultsAnnotation) {
|
|
|
|
const answers = pollResults.responses.map((response) => ({
|
|
|
|
id: response.optionId,
|
|
|
|
key: response.optionDesc,
|
|
|
|
numVotes: response.optionResponsesCount,
|
|
|
|
}));
|
|
|
|
|
|
|
|
const pollResultsAnnotation = {
|
|
|
|
id: pollResults.pollId,
|
|
|
|
annotationInfo: JSON.stringify({
|
|
|
|
answers,
|
|
|
|
id: pollResults.pollId,
|
|
|
|
whiteboardId: curPageId,
|
|
|
|
questionType: true,
|
|
|
|
questionText: pollResults.questionText,
|
|
|
|
}),
|
|
|
|
wbId: curPageId,
|
|
|
|
userId: Auth.userID,
|
|
|
|
};
|
|
|
|
annotations.push(pollResultsAnnotation);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-28 04:42:47 +08:00
|
|
|
annotations.forEach((annotation) => {
|
2023-10-17 21:49:55 +08:00
|
|
|
if (annotation.annotationInfo === '') return;
|
|
|
|
|
2023-09-28 04:42:47 +08:00
|
|
|
let annotationInfo = JSON.parse(annotation.annotationInfo);
|
|
|
|
|
|
|
|
if (annotationInfo.questionType) {
|
|
|
|
// poll result, convert it to text and create tldraw shape
|
|
|
|
annotationInfo.answers = annotationInfo.answers.reduce(
|
|
|
|
caseInsensitiveReducer, [],
|
|
|
|
);
|
|
|
|
let pollResult = PollService.getPollResultString(annotationInfo, intl)
|
|
|
|
.split('<br/>').join('\n').replace(/(<([^>]+)>)/ig, '');
|
|
|
|
|
|
|
|
const lines = pollResult.split('\n');
|
|
|
|
const longestLine = lines.reduce((a, b) => a.length > b.length ? a : b, '').length;
|
|
|
|
|
|
|
|
// add empty spaces before first | in each of the lines to make them all the same length
|
|
|
|
pollResult = lines.map((line) => {
|
|
|
|
if (!line.includes('|') || line.length === longestLine) return line;
|
|
|
|
|
|
|
|
const splitLine = line.split(' |');
|
|
|
|
const spaces = ' '.repeat(longestLine - line.length);
|
|
|
|
return `${splitLine[0]} ${spaces}|${splitLine[1]}`;
|
|
|
|
}).join('\n');
|
|
|
|
|
2023-12-06 02:15:25 +08:00
|
|
|
// Text measurement estimation
|
|
|
|
const averageCharWidth = 16;
|
|
|
|
const lineHeight = 32;
|
|
|
|
|
|
|
|
const annotationWidth = longestLine * averageCharWidth; // Estimate width
|
|
|
|
const annotationHeight = lines.length * lineHeight; // Estimate height
|
2023-09-28 04:42:47 +08:00
|
|
|
|
2023-12-06 02:15:25 +08:00
|
|
|
const slideWidth = currentPresentationPage?.scaledWidth;
|
|
|
|
const slideHeight = currentPresentationPage?.scaledHeight;
|
|
|
|
const xPosition = slideWidth - annotationWidth;
|
|
|
|
const yPosition = slideHeight - annotationHeight;
|
|
|
|
|
|
|
|
let cpg = parseInt(annotationInfo?.id?.split('/')[1]);
|
|
|
|
if (cpg !== parseInt(curPageId)) return;
|
2023-09-28 04:42:47 +08:00
|
|
|
|
|
|
|
annotationInfo = {
|
2023-12-06 02:15:25 +08:00
|
|
|
"x": xPosition,
|
|
|
|
"isLocked": false,
|
|
|
|
"y": yPosition,
|
|
|
|
"rotation": 0,
|
|
|
|
"typeName": "shape",
|
|
|
|
"opacity": 1,
|
|
|
|
"parentId": `page:${curPageId}`,
|
|
|
|
"index": "a1",
|
|
|
|
"id": `shape:poll-result-${annotationInfo.id}`,
|
|
|
|
"meta": {
|
|
|
|
},
|
|
|
|
"type": "geo",
|
|
|
|
"props": {
|
|
|
|
"url": "",
|
|
|
|
"text": `${pollResult}`,
|
|
|
|
"color": "black",
|
|
|
|
"font": "mono",
|
|
|
|
"fill": "semi",
|
|
|
|
"dash": "draw",
|
|
|
|
"h": annotationHeight,
|
|
|
|
"w": annotationWidth,
|
|
|
|
"size": "m",
|
|
|
|
"growY": 0,
|
|
|
|
"align": "middle",
|
|
|
|
"geo": "rectangle",
|
|
|
|
"verticalAlign": "middle",
|
|
|
|
"labelColor": "black"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-28 04:42:47 +08:00
|
|
|
annotationInfo.questionType = false;
|
|
|
|
}
|
|
|
|
result[annotationInfo.id] = annotationInfo;
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
2023-09-08 01:58:06 +08:00
|
|
|
export {
|
2022-04-05 22:49:13 +08:00
|
|
|
initDefaultPages,
|
2020-04-07 04:34:08 +08:00
|
|
|
sendAnnotation,
|
2021-03-05 06:26:25 +08:00
|
|
|
getMultiUser,
|
2022-04-05 22:49:13 +08:00
|
|
|
persistShape,
|
2022-10-21 22:05:31 +08:00
|
|
|
notifyNotAllowedChange,
|
2023-02-03 03:47:16 +08:00
|
|
|
notifyShapeNumberExceeded,
|
2023-03-27 06:08:09 +08:00
|
|
|
toggleToolsAnimations,
|
2023-09-28 04:42:47 +08:00
|
|
|
formatAnnotations,
|
2023-12-06 02:15:25 +08:00
|
|
|
};
|