Merge pull request #19479 from ramonlsouza/migrate-presen-actions

refactor: migrate presentation actions
This commit is contained in:
Ramón Souza 2024-02-01 09:33:36 -03:00 committed by GitHub
commit 2c5a7f3f00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 316 additions and 559 deletions

View File

@ -0,0 +1,27 @@
import { RedisMessage } from '../types';
import {throwErrorIfNotPresenter} from "../imports/validation";
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
throwErrorIfNotPresenter(sessionVariables);
const eventName = `MakePresentationDownloadReqMsg`;
const routing = {
meetingId: sessionVariables['x-hasura-meetingid'] as String,
userId: sessionVariables['x-hasura-userid'] as String
};
const header = {
name: eventName,
meetingId: routing.meetingId,
userId: routing.userId
};
const body = {
presId: input.presentationId,
allPages: true,
fileStateType: input.fileStateType,
pages: [],
};
return { eventName, routing, header, body };
}

View File

@ -1,5 +1,4 @@
import { RedisMessage } from '../types';
import { ValidationError } from '../types/ValidationError';
import {throwErrorIfNotPresenter} from "../imports/validation";
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {

View File

@ -280,6 +280,13 @@ type Mutation {
): Boolean
}
type Mutation {
presentationExport(
presentationId: String!
fileStateType: String!
): Boolean
}
type Mutation {
presentationRemove(
presentationId: String!

View File

@ -245,6 +245,13 @@ actions:
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
permissions:
- role: bbb_client
- name: presentationExport
definition:
kind: synchronous
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
permissions:
- role: bbb_client
comment: presentationExport
- name: presentationRemove
definition:
kind: synchronous

View File

@ -1 +0,0 @@
import './methods';

View File

@ -1,12 +0,0 @@
import { Meteor } from 'meteor/meteor';
import clearWhiteboard from './methods/clearWhiteboard';
import sendAnnotations from './methods/sendAnnotations';
import sendBulkAnnotations from './methods/sendBulkAnnotations';
import deleteAnnotations from './methods/deleteAnnotations';
Meteor.methods({
clearWhiteboard,
sendAnnotations,
sendBulkAnnotations,
deleteAnnotations,
});

View File

@ -1,27 +0,0 @@
import RedisPubSub from '/imports/startup/server/redis';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
export default function clearWhiteboard(whiteboardId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ClearWhiteboardPubMsg';
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(whiteboardId, String);
const payload = {
whiteboardId,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (err) {
Logger.error(`Exception while invoking method clearWhiteboard ${err.stack}`);
}
}

View File

@ -1,29 +0,0 @@
import { Meteor } from 'meteor/meteor';
import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
export default function deleteAnnotations(annotations, whiteboardId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'DeleteWhiteboardAnnotationsPubMsg';
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(whiteboardId, String);
check(annotations, Array);
const payload = {
whiteboardId,
annotationsIds: annotations,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (err) {
Logger.error(`Exception while invoking method deleteAnnotation ${err.stack}`);
}
}

View File

@ -1,34 +0,0 @@
import RedisPubSub from '/imports/startup/server/redis';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
export default function sendAnnotationHelper(annotations, meetingId, requesterUserId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'SendWhiteboardAnnotationsPubMsg';
try {
check(annotations, Array);
// TODO see if really necessary, don't know if it's possible
// to have annotations from different pages
// group annotations by same whiteboardId
const groupedAnnotations = annotations.reduce((r, v, i, a, k = v.wbId) => ((r[k] || (r[k] = [])).push(v), r), {}) //groupBy wbId
Object.entries(groupedAnnotations).forEach(([_, whiteboardAnnotations]) => {
const whiteboardId = whiteboardAnnotations[0].wbId;
check(whiteboardId, String);
const payload = {
whiteboardId,
annotations: whiteboardAnnotations,
html5InstanceId: parseInt(process.env.INSTANCE_ID, 10) || 1,
};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
});
} catch (err) {
Logger.error(`Exception while invoking method sendAnnotationHelper ${err.stack}`);
}
}

View File

@ -1,17 +0,0 @@
import { check } from 'meteor/check';
import sendAnnotationHelper from './sendAnnotationHelper';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
export default function sendAnnotations(annotations) {
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
sendAnnotationHelper(annotations, meetingId, requesterUserId);
} catch (err) {
Logger.error(`Exception while invoking method sendAnnotation ${err.stack}`);
}
}

View File

@ -1,22 +0,0 @@
import { extractCredentials } from '/imports/api/common/server/helpers';
import sendAnnotationHelper from './sendAnnotationHelper';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
export default function sendBulkAnnotations(payload) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
try {
check(meetingId, String);
check(requesterUserId, String);
console.log("!!!!!!! sendBulkAnnotations!!!!:", payload)
sendAnnotationHelper(payload, meetingId, requesterUserId);
//payload.forEach((annotation) => sendAnnotationHelper(annotation, meetingId, requesterUserId));
return true;
} catch (err) {
Logger.error(`Exception while invoking method sendBulkAnnotations ${err.stack}`);
return false;
}
}

View File

@ -1 +0,0 @@
import './methods';

View File

@ -1,12 +0,0 @@
import { Meteor } from 'meteor/meteor';
import removePresentation from './methods/removePresentation';
import setPresentation from './methods/setPresentation';
import setPresentationDownloadable from './methods/setPresentationDownloadable';
import exportPresentation from './methods/exportPresentation';
Meteor.methods({
removePresentation,
setPresentation,
setPresentationDownloadable,
exportPresentation,
});

View File

@ -1,29 +0,0 @@
import RedisPubSub from '/imports/startup/server/redis';
import { check } from 'meteor/check';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
export default async function exportPresentation(presentationId, fileStateType) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'MakePresentationDownloadReqMsg';
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(presentationId, String);
const payload = {
presId: presentationId,
allPages: true,
fileStateType,
pages: [],
};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (err) {
Logger.error(`Exception while invoking method exportPresentation ${err.stack}`);
}
}

View File

@ -1,28 +0,0 @@
import RedisPubSub from '/imports/startup/server/redis';
import { check } from 'meteor/check';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
export default function removePresentation(presentationId, podId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'RemovePresentationPubMsg';
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(presentationId, String);
check(podId, String);
const payload = {
presentationId,
podId,
};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (err) {
Logger.error(`Exception while invoking method removePresentation ${err.stack}`);
}
}

View File

@ -1,28 +0,0 @@
import RedisPubSub from '/imports/startup/server/redis';
import { check } from 'meteor/check';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
export default function setPresentation(presentationId, podId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'SetCurrentPresentationPubMsg';
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(presentationId, String);
check(podId, String);
const payload = {
presentationId,
podId,
};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (err) {
Logger.error(`Exception while invoking method setPresentation ${err.stack}`);
}
}

View File

@ -1,31 +0,0 @@
import RedisPubSub from '/imports/startup/server/redis';
import { check } from 'meteor/check';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
export default function setPresentationDownloadable(presentationId, downloadable, fileStateType) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'SetPresentationDownloadablePubMsg';
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(downloadable, Match.Maybe(Boolean));
check(presentationId, String);
check(fileStateType, Match.Maybe(String));
const payload = {
presentationId,
podId: 'DEFAULT_PRESENTATION_POD',
downloadable,
fileStateType,
};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (err) {
Logger.error(`Exception while invoking method setPresentationDownloadable ${err.stack}`);
}
}

View File

@ -1 +0,0 @@
import './methods';

View File

@ -1,6 +0,0 @@
import { Meteor } from 'meteor/meteor';
import switchSlide from './methods/switchSlide';
Meteor.methods({
switchSlide,
});

View File

@ -1,30 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
export default async function switchSlide(slideNumber, podId, presentationId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'SetCurrentPagePubMsg';
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(slideNumber, Number);
check(podId, String);
const payload = {
podId,
presentationId,
pageId: `${presentationId}/${slideNumber}`,
};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (err) {
Logger.error(`Exception while invoking method switchSlide ${err.stack}`);
}
}

View File

@ -1,5 +1,4 @@
import React, { useContext } from 'react';
import PresentationUploaderService from '/imports/ui/components/presentation/presentation-uploader/service';
import ActionsDropdown from './component';
import { layoutSelectInput, layoutDispatch, layoutSelect } from '../../layout/context';
import { SMALL_VIEWPORT_BREAKPOINT, ACTIONS, PANELS } from '../../layout/enums';
@ -12,6 +11,7 @@ import {
import { SET_PRESENTER } from '/imports/ui/core/graphql/mutations/userMutations';
import { TIMER_ACTIVATE, TIMER_DEACTIVATE } from '../../timer/mutations';
import Auth from '/imports/ui/services/auth';
import { PRESENTATION_SET_CURRENT } from '../../presentation/mutations';
const TIMER_CONFIG = Meteor.settings.public.timer;
const MILLI_IN_MINUTE = 60000;
@ -35,11 +35,16 @@ const ActionsDropdownContainer = (props) => {
const [setPresenter] = useMutation(SET_PRESENTER);
const [timerActivate] = useMutation(TIMER_ACTIVATE);
const [timerDeactivate] = useMutation(TIMER_DEACTIVATE);
const [presentationSetCurrent] = useMutation(PRESENTATION_SET_CURRENT);
const handleTakePresenter = () => {
setPresenter({ variables: { userId: Auth.userID } });
};
const setPresentation = (presentationId) => {
presentationSetCurrent({ variables: { presentationId } });
};
const activateTimer = () => {
const stopwatch = true;
const running = false;
@ -71,7 +76,7 @@ const ActionsDropdownContainer = (props) => {
presentations,
isTimerFeatureEnabled: isTimerFeatureEnabled(),
isDropdownOpen: Session.get('dropdownOpen'),
setPresentation: PresentationUploaderService.setPresentation,
setPresentation,
isCameraAsContentEnabled: isCameraAsContentEnabled(),
handleTakePresenter,
activateTimer,

View File

@ -51,6 +51,8 @@ class NotesDropdown extends PureComponent {
intl,
amIPresenter,
presentations,
setPresentation,
removePresentation,
stopExternalVideoShare,
} = this.props;
@ -72,7 +74,7 @@ class NotesDropdown extends PureComponent {
onClick: () => {
this.setConverterButtonDisabled(true);
setTimeout(() => this.setConverterButtonDisabled(false), DEBOUNCE_TIMEOUT);
return Service.convertAndUpload(presentations);
return Service.convertAndUpload(presentations, setPresentation, removePresentation);
},
},
);

View File

@ -6,6 +6,7 @@ import {
PROCESSED_PRESENTATIONS_SUBSCRIPTION,
} from '/imports/ui/components/whiteboard/queries';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import { PRESENTATION_SET_CURRENT, PRESENTATION_REMOVE } from '../../presentation/mutations';
import { EXTERNAL_VIDEO_STOP } from '../../external-video-player/mutations';
const NotesDropdownContainer = ({ ...props }) => {
@ -18,8 +19,18 @@ const NotesDropdownContainer = ({ ...props }) => {
const { data: presentationData } = useSubscription(PROCESSED_PRESENTATIONS_SUBSCRIPTION);
const presentations = presentationData?.pres_presentation || [];
const [presentationSetCurrent] = useMutation(PRESENTATION_SET_CURRENT);
const [presentationRemove] = useMutation(PRESENTATION_REMOVE);
const [stopExternalVideoShare] = useMutation(EXTERNAL_VIDEO_STOP);
const setPresentation = (presentationId) => {
presentationSetCurrent({ variables: { presentationId } });
};
const removePresentation = (presentationId) => {
presentationRemove({ variables: { presentationId } });
};
return (
<NotesDropdown
{
@ -27,6 +38,8 @@ const NotesDropdownContainer = ({ ...props }) => {
amIPresenter,
isRTL,
presentations,
setPresentation,
removePresentation,
stopExternalVideoShare,
...props,
}

View File

@ -7,8 +7,7 @@ import { uniqueId } from '/imports/utils/string-utils';
const PADS_CONFIG = Meteor.settings.public.pads;
async function convertAndUpload(presentations) {
async function convertAndUpload(presentations, setPresentation, removePresentation) {
let filename = 'Shared_Notes';
const duplicates = presentations.filter((pres) => pres.filename?.startsWith(filename) || pres.name?.startsWith(filename)).length;
@ -53,7 +52,9 @@ async function convertAndUpload(presentations) {
onUpload: () => { },
onProgress: () => { },
onDone: () => { },
});
},
setPresentation,
removePresentation);
}
export default {

View File

@ -38,7 +38,7 @@ const ButtonWrapper = styled.div`
background-color: ${colorTransparent};
cursor: pointer;
border: 0;
z-index: 2;
z-index: 999;
margin: 2px;
bottom: 0;

View File

@ -23,7 +23,81 @@ export const PRESENTATION_SET_WRITERS = gql`
}
`;
export const PRESENTATION_SET_PAGE = gql`
mutation PresentationSetPage($presentationId: String!, $pageId: String!) {
presentationSetPage(
presentationId: $presentationId,
pageId: $pageId,
)
}
`;
export const PRESENTATION_SET_DOWNLOADABLE = gql`
mutation PresentationSetDownloadable(
$presentationId: String!,
$downloadable: Boolean!,
$fileStateType: String!,) {
presentationSetDownloadable(
presentationId: $presentationId,
downloadable: $downloadable,
fileStateType: $fileStateType,
)
}
`;
export const PRESENTATION_EXPORT = gql`
mutation PresentationExport(
$presentationId: String!,
$fileStateType: String!,) {
presentationExport(
presentationId: $presentationId,
fileStateType: $fileStateType,
)
}
`;
export const PRESENTATION_SET_CURRENT = gql`
mutation PresentationSetCurrent($presentationId: String!) {
presentationSetCurrent(
presentationId: $presentationId,
)
}
`;
export const PRESENTATION_REMOVE = gql`
mutation PresentationRemove($presentationId: String!) {
presentationRemove(
presentationId: $presentationId,
)
}
`;
export const PRES_ANNOTATION_DELETE = gql`
mutation PresAnnotationDelete($pageId: String!, $annotationsIds: [String]!) {
presAnnotationDelete(
pageId: $pageId,
annotationsIds: $annotationsIds,
)
}
`;
export const PRES_ANNOTATION_SUBMIT = gql`
mutation PresAnnotationSubmit($pageId: String!, $annotations: json!) {
presAnnotationSubmit(
pageId: $pageId,
annotations: $annotations,
)
}
`;
export default {
PRESENTATION_SET_ZOOM,
PRESENTATION_SET_WRITERS,
PRESENTATION_SET_PAGE,
PRESENTATION_SET_DOWNLOADABLE,
PRESENTATION_EXPORT,
PRESENTATION_SET_CURRENT,
PRESENTATION_REMOVE,
PRES_ANNOTATION_DELETE,
PRES_ANNOTATION_SUBMIT,
};

View File

@ -150,12 +150,12 @@ class PresentationToolbar extends PureComponent {
}
handleSkipToSlideChange(event) {
const { skipToSlide, presentationId } = this.props;
const { skipToSlide } = this.props;
const requestedSlideNum = Number.parseInt(event.target.value, 10);
this.handleFTWSlideChange();
if (event) event.currentTarget.blur();
skipToSlide(requestedSlideNum, presentationId);
skipToSlide(requestedSlideNum);
}
handleSwitchWhiteboardMode() {
@ -194,29 +194,21 @@ class PresentationToolbar extends PureComponent {
}
nextSlideHandler(event) {
const {
nextSlide,
currentSlideNum,
numberOfSlides,
endCurrentPoll,
presentationId,
} = this.props;
const { nextSlide, endCurrentPoll } = this.props;
this.handleFTWSlideChange();
if (event) event.currentTarget.blur();
endCurrentPoll();
nextSlide(currentSlideNum, numberOfSlides, presentationId);
nextSlide();
}
previousSlideHandler(event) {
const {
previousSlide, currentSlideNum, endCurrentPoll, presentationId
} = this.props;
const { previousSlide, endCurrentPoll } = this.props;
this.handleFTWSlideChange();
if (event) event.currentTarget.blur();
endCurrentPoll();
previousSlide(currentSlideNum, presentationId);
previousSlide();
}
switchSlide(event) {

View File

@ -2,19 +2,24 @@ import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import PresentationToolbar from './component';
import PresentationToolbarService from './service';
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
import { isPollingEnabled } from '/imports/ui/services/features';
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
import { useSubscription, useMutation } from '@apollo/client';
import POLL_SUBSCRIPTION from '/imports/ui/core/graphql/queries/pollSubscription';
import { POLL_CANCEL, POLL_CREATE } from '/imports/ui/components/poll/mutations';
import { PRESENTATION_SET_PAGE } from '../mutations';
const PresentationToolbarContainer = (props) => {
const pluginsContext = useContext(PluginsContext);
const { pluginsExtensibleAreasAggregatedState } = pluginsContext;
const { userIsPresenter, layoutSwapped } = props;
const {
userIsPresenter,
layoutSwapped,
currentSlideNum,
presentationId,
} = props;
const { data: pollData } = useSubscription(POLL_SUBSCRIPTION);
const hasPoll = pollData?.poll?.length > 0;
@ -23,11 +28,36 @@ const PresentationToolbarContainer = (props) => {
const [stopPoll] = useMutation(POLL_CANCEL);
const [createPoll] = useMutation(POLL_CREATE);
const [presentationSetPage] = useMutation(PRESENTATION_SET_PAGE);
const endCurrentPoll = () => {
if (hasPoll) stopPoll();
};
const setPresentationPage = (pageId) => {
presentationSetPage({
variables: {
presentationId,
pageId,
},
});
};
const skipToSlide = (slideNum) => {
const slideId = `${presentationId}/${slideNum}`;
setPresentationPage(slideId);
};
const previousSlide = () => {
const prevSlideNum = currentSlideNum - 1;
skipToSlide(prevSlideNum);
};
const nextSlide = () => {
const nextSlideNum = currentSlideNum + 1;
skipToSlide(nextSlideNum);
};
const startPoll = (pollType, pollId, answers = [], question, isMultipleResponse = false) => {
Session.set('openPanel', 'poll');
Session.set('forcePollOpen', true);
@ -60,6 +90,9 @@ const PresentationToolbarContainer = (props) => {
pluginProvidedPresentationToolbarItems,
handleToggleFullScreen,
startPoll,
previousSlide,
nextSlide,
skipToSlide,
}}
/>
);
@ -69,9 +102,6 @@ const PresentationToolbarContainer = (props) => {
export default withTracker(() => {
return {
nextSlide: PresentationToolbarService.nextSlide,
previousSlide: PresentationToolbarService.previousSlide,
skipToSlide: PresentationToolbarService.skipToSlide,
isMeteorConnected: Meteor.status().connected,
isPollingEnabled: isPollingEnabled(),
};

View File

@ -1,25 +0,0 @@
import { makeCall } from '/imports/ui/services/api';
const POD_ID = 'DEFAULT_PRESENTATION_POD';
const previousSlide = (currentSlideNum, presentationId) => {
if (currentSlideNum > 1) {
makeCall('switchSlide', currentSlideNum - 1, POD_ID, presentationId);
}
};
const nextSlide = (currentSlideNum, numberOfSlides, presentationId) => {
if (currentSlideNum < numberOfSlides) {
makeCall('switchSlide', currentSlideNum + 1, POD_ID, presentationId);
}
};
const skipToSlide = (requestedSlideNum, presentationId) => {
makeCall('switchSlide', requestedSlideNum, POD_ID, presentationId);
};
export default {
nextSlide,
previousSlide,
skipToSlide,
};

View File

@ -583,6 +583,8 @@ class PresentationUploader extends Component {
selectedToBeNextCurrent,
presentations: propPresentations,
dispatchChangePresentationDownloadable,
setPresentation,
removePresentation,
} = this.props;
const { disableActions, presentations } = this.state;
const presentationsToSave = presentations;
@ -610,7 +612,14 @@ class PresentationUploader extends Component {
if (!disableActions) {
Session.set('showUploadPresentationView', false);
return handleSave(presentationsToSave, true, {}, propPresentations)
return handleSave(
presentationsToSave,
true,
{},
propPresentations,
setPresentation,
removePresentation,
)
.then(() => {
const hasError = presentations.some((p) => !!p.uploadErrorMsgKey);
if (!hasError) {

View File

@ -1,9 +1,9 @@
import React from 'react';
import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';
import { makeCall } from '/imports/ui/services/api';
import ErrorBoundary from '/imports/ui/components/common/error-boundary/component';
import FallbackModal from '/imports/ui/components/common/fallback-errors/fallback-modal/component';
import { useSubscription, useMutation } from '@apollo/client';
import Service from './service';
import PresUploaderToast from '/imports/ui/components/presentation/presentation-toast/presentation-uploader-toast/component';
import PresentationUploader from './component';
@ -13,11 +13,16 @@ import {
isDownloadPresentationConvertedToPdfEnabled,
isPresentationEnabled,
} from '/imports/ui/services/features';
import { useSubscription } from '@apollo/client';
import {
PRESENTATIONS_SUBSCRIPTION,
} from '/imports/ui/components/whiteboard/queries';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import {
PRESENTATION_SET_DOWNLOADABLE,
PRESENTATION_EXPORT,
PRESENTATION_SET_CURRENT,
PRESENTATION_REMOVE,
} from '../mutations';
const PRESENTATION_CONFIG = Meteor.settings.public.presentation;
@ -31,8 +36,36 @@ const PresentationUploaderContainer = (props) => {
const presentations = presentationData?.pres_presentation || [];
const currentPresentation = presentations.find((p) => p.current)?.presentationId || '';
const [presentationSetDownloadable] = useMutation(PRESENTATION_SET_DOWNLOADABLE);
const [presentationExport] = useMutation(PRESENTATION_EXPORT);
const [presentationSetCurrent] = useMutation(PRESENTATION_SET_CURRENT);
const [presentationRemove] = useMutation(PRESENTATION_REMOVE);
const exportPresentation = (presentationId, fileStateType) => {
makeCall('exportPresentation', presentationId, fileStateType);
presentationExport({
variables: {
presentationId,
fileStateType,
},
});
};
const dispatchChangePresentationDownloadable = (presentationId, downloadable, fileStateType) => {
presentationSetDownloadable({
variables: {
presentationId,
downloadable,
fileStateType,
},
});
};
const setPresentation = (presentationId) => {
presentationSetCurrent({ variables: { presentationId } });
};
const removePresentation = (presentationId) => {
presentationRemove({ variables: { presentationId } });
};
return userIsPresenter && (
@ -42,6 +75,9 @@ const PresentationUploaderContainer = (props) => {
presentations={presentations}
currentPresentation={currentPresentation}
exportPresentation={exportPresentation}
dispatchChangePresentationDownloadable={dispatchChangePresentationDownloadable}
setPresentation={setPresentation}
removePresentation={removePresentation}
{...props}
/>
</ErrorBoundary>
@ -52,7 +88,6 @@ export default withTracker(() => {
const {
dispatchDisableDownloadable,
dispatchEnableDownloadable,
dispatchChangePresentationDownloadable,
} = Service;
const isOpen = isPresentationEnabled() && (Session.get('showUploadPresentationView') || false);
@ -70,7 +105,6 @@ export default withTracker(() => {
renderPresentationItemStatus: PresUploaderToast.renderPresentationItemStatus,
dispatchDisableDownloadable,
dispatchEnableDownloadable,
dispatchChangePresentationDownloadable,
isOpen,
selectedToBeNextCurrent: Session.get('selectedToBeNextCurrent') || null,
externalUploadData: Service.getExternalUploadData(),

View File

@ -103,7 +103,7 @@ class PresentationDownloadDropdown extends PureComponent {
const downloadableExtension = downloadFileUri?.split('.').slice(-1)[0];
const originalFileExtension = name?.split('.').slice(-1)[0];
const changeDownloadOriginalOrConvertedPresentation = (enableDownload, fileStateType) => {
handleDownloadableChange(item, fileStateType, enableDownload);
handleDownloadableChange(item?.presentationId, fileStateType, enableDownload);
if (enableDownload) {
handleDownloadingOfPresentation(fileStateType);
}

View File

@ -41,10 +41,6 @@ const futch = (url, opts = {}, onProgress) => new Promise((res, rej) => {
xhr.send(opts.body);
});
const dispatchChangePresentationDownloadable = (presentation, newState, fileStateType) => {
makeCall('setPresentationDownloadable', presentation.presentationId, newState, fileStateType);
};
const requestPresentationUploadToken = (
temporaryPresentationId,
meetingId,
@ -183,19 +179,18 @@ const uploadAndConvertPresentations = (
p.onUpload, p.onProgress, p.onConversion, p.current,
)));
const setPresentation = (presentationId) => {
makeCall('setPresentation', presentationId, POD_ID);
};
const removePresentation = (presentationId) => {
makeCall('removePresentation', presentationId, POD_ID);
};
const removePresentations = (
presentationsToRemove,
) => Promise.all(presentationsToRemove.map((p) => removePresentation(p.presentationId, POD_ID)));
removePresentation,
) => Promise.all(presentationsToRemove.map((p) => removePresentation(p.presentationId)));
const persistPresentationChanges = (oldState, newState, uploadEndpoint) => {
const persistPresentationChanges = (
oldState,
newState,
uploadEndpoint,
setPresentation,
removePresentation,
) => {
const presentationsToUpload = newState.filter((p) => !p.uploadCompleted);
const presentationsToRemove = oldState.filter((p) => !newState.find((u) => { return u.presentationId === p.presentationId }));
@ -214,7 +209,7 @@ const persistPresentationChanges = (oldState, newState, uploadEndpoint) => {
})
.then((presentations) => {
if (currentPresentation === undefined) {
setPresentation('', POD_ID);
setPresentation('');
return Promise.resolve();
}
@ -229,13 +224,18 @@ const persistPresentationChanges = (oldState, newState, uploadEndpoint) => {
return Promise.resolve();
}
return setPresentation(currentPresentation?.presentationId, POD_ID);
return setPresentation(currentPresentation?.presentationId);
})
.then(removePresentations.bind(null, presentationsToRemove, POD_ID));
.then(removePresentations.bind(null, presentationsToRemove, removePresentation));
};
const handleSavePresentation = (
presentations = [], isFromPresentationUploaderInterface = true, newPres = {}, currentPresentations = [],
presentations = [],
isFromPresentationUploaderInterface = true,
newPres = {},
currentPresentations = [],
setPresentation,
removePresentation,
) => {
if (!isPresentationEnabled()) {
return null;
@ -257,7 +257,8 @@ const handleSavePresentation = (
currentPresentations,
presentations,
PRESENTATION_CONFIG.uploadEndpoint,
'DEFAULT_PRESENTATION_POD',
setPresentation,
removePresentation,
);
};
@ -352,8 +353,6 @@ function handleFiledrop(files, files2, that, intl, intlMessages) {
export default {
handleSavePresentation,
persistPresentationChanges,
dispatchChangePresentationDownloadable,
setPresentation,
requestPresentationUploadToken,
getExternalUploadData,
uploadAndConvertPresentation,

View File

@ -20,7 +20,6 @@ import {
findRemoved,
filterInvalidShapes,
mapLanguage,
sendShapeChanges,
usePrevious,
} from "./utils";
// import { throttle } from "/imports/utils/throttle";
@ -85,13 +84,12 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
isPresenter,
removeShapes,
initDefaultPages,
persistShape,
persistShapeWrapper,
shapes,
assets,
currentUser,
whiteboardId,
zoomSlide,
skipToSlide,
curPageId,
zoomChanger,
isMultiUserActive,
@ -675,7 +673,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
console.log("EDITOR : ", editor);
const debouncePersistShape = debounce({ delay: 0 }, persistShape);
const debouncePersistShape = debounce({ delay: 0 }, persistShapeWrapper);
const colorStyles = ['black', 'blue', 'green', 'grey', 'light-blue', 'light-green', 'light-red', 'light-violet', 'orange', 'red', 'violet', 'yellow'];
const dashStyles = ['dashed', 'dotted', 'draw', 'solid'];
@ -712,7 +710,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
createdBy: currentUser?.userId,
},
};
persistShape(updatedRecord, whiteboardId, isModerator);
persistShapeWrapper(updatedRecord, whiteboardId, isModerator);
});
Object.values(updated).forEach(([_, record]) => {
@ -723,11 +721,11 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
createdBy: shapes[record?.id]?.meta?.createdBy,
},
};
persistShape(updatedRecord, whiteboardId, isModerator);
persistShapeWrapper(updatedRecord, whiteboardId, isModerator);
});
Object.values(removed).forEach((record) => {
removeShapes([record.id], whiteboardId);
removeShapes([record.id]);
});
},
{ source: "user", scope: "document" }
@ -897,7 +895,7 @@ Whiteboard.propTypes = {
isIphone: PropTypes.bool.isRequired,
removeShapes: PropTypes.func.isRequired,
initDefaultPages: PropTypes.func.isRequired,
persistShape: PropTypes.func.isRequired,
persistShapeWrapper: PropTypes.func.isRequired,
notifyNotAllowedChange: PropTypes.func.isRequired,
shapes: PropTypes.objectOf(PropTypes.shape).isRequired,
assets: PropTypes.objectOf(PropTypes.shape).isRequired,
@ -906,7 +904,6 @@ Whiteboard.propTypes = {
}).isRequired,
whiteboardId: PropTypes.string,
zoomSlide: PropTypes.func.isRequired,
skipToSlide: PropTypes.func.isRequired,
curPageId: PropTypes.string.isRequired,
presentationWidth: PropTypes.number.isRequired,
presentationHeight: PropTypes.number.isRequired,
@ -940,9 +937,7 @@ Whiteboard.propTypes = {
fullscreenAction: PropTypes.string.isRequired,
fullscreenRef: PropTypes.instanceOf(Element),
handleToggleFullScreen: PropTypes.func.isRequired,
nextSlide: PropTypes.func.isRequired,
numberOfSlides: PropTypes.number.isRequired,
previousSlide: PropTypes.func.isRequired,
sidebarNavigationWidth: PropTypes.number,
presentationId: PropTypes.string,
};

View File

@ -9,15 +9,12 @@ import { CURSOR_SUBSCRIPTION } from './cursors/queries';
import {
initDefaultPages,
persistShape,
removeShapes,
changeCurrentSlide,
notifyNotAllowedChange,
notifyShapeNumberExceeded,
toggleToolsAnimations,
formatAnnotations,
} from './service';
import CursorService from './cursors/service';
import PresentationToolbarService from '../presentation/presentation-toolbar/service';
import SettingsService from '/imports/ui/services/settings';
import Auth from '/imports/ui/services/auth';
import {
@ -34,7 +31,11 @@ import useMeeting from '/imports/ui/core/hooks/useMeeting';
import {
AssetRecordType,
} from "@tldraw/tldraw";
import { PRESENTATION_SET_ZOOM } from '../presentation/mutations';
import {
PRESENTATION_SET_ZOOM,
PRES_ANNOTATION_DELETE,
PRES_ANNOTATION_SUBMIT,
} from '../presentation/mutations';
const WHITEBOARD_CONFIG = Meteor.settings.public.whiteboard;
@ -65,6 +66,17 @@ const WhiteboardContainer = (props) => {
const hasWBAccess = whiteboardWriters?.some((writer) => writer.userId === Auth.userID);
const [presentationSetZoom] = useMutation(PRESENTATION_SET_ZOOM);
const [presentationDeleteAnnotations] = useMutation(PRES_ANNOTATION_DELETE);
const [presentationSubmitAnnotations] = useMutation(PRES_ANNOTATION_SUBMIT);
const removeShapes = (shapeIds) => {
presentationDeleteAnnotations({
variables: {
pageId: currentPresentationPage?.pageId,
annotationsIds: shapeIds,
},
});
};
const zoomSlide = (widthRatio, heightRatio, xOffset, yOffset) => {
const { pageId, num } = currentPresentationPage;
@ -82,6 +94,21 @@ const WhiteboardContainer = (props) => {
});
};
const submitAnnotations = async (newAnnotations) => {
const isAnnotationSent = await presentationSubmitAnnotations({
variables: {
pageId: currentPresentationPage?.pageId,
annotations: newAnnotations,
},
});
return isAnnotationSent?.data?.presAnnotationSubmit;
};
const persistShapeWrapper = (shape, whiteboardId, isModerator) => {
persistShape(shape, whiteboardId, isModerator, submitAnnotations);
};
const isMultiUserActive = whiteboardWriters?.length > 0;
const { data: pollData } = useSubscription(POLL_RESULTS_SUBSCRIPTION);
@ -223,17 +250,13 @@ const WhiteboardContainer = (props) => {
sidebarNavigationWidth,
layoutContextDispatch,
initDefaultPages,
persistShape,
persistShapeWrapper,
isMultiUserActive,
changeCurrentSlide,
shapes,
bgShape,
assets,
removeShapes,
zoomSlide,
skipToSlide: PresentationToolbarService.skipToSlide,
nextSlide: PresentationToolbarService.nextSlide,
previousSlide: PresentationToolbarService.previousSlide,
numberOfSlides: currentPresentationPage?.totalPages,
notifyNotAllowedChange,
notifyShapeNumberExceeded,

View File

@ -1,11 +1,9 @@
import Auth from '/imports/ui/services/auth';
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user';
import { makeCall } from '/imports/ui/services/api';
import PollService from '/imports/ui/components/poll/service';
import { defineMessages } from 'react-intl';
import { notify } from '/imports/ui/services/notification';
import caseInsensitiveReducer from '/imports/utils/caseInsensitiveReducer';
import { getTextSize } from './utils';
const intlMessages = defineMessages({
notifyNotAllowedChange: {
@ -30,7 +28,7 @@ const annotationsRetryDelay = 1000;
let annotationsSenderIsRunning = false;
const proccessAnnotationsQueue = async () => {
const proccessAnnotationsQueue = async (submitAnnotations) => {
annotationsSenderIsRunning = true;
const queueSize = annotationsQueue.length;
@ -41,12 +39,13 @@ const proccessAnnotationsQueue = async () => {
const annotations = annotationsQueue.splice(0, queueSize);
const isAnnotationSent = await makeCall('sendBulkAnnotations', annotations);
try {
const isAnnotationSent = await submitAnnotations(annotations);
if (!isAnnotationSent) {
// undo splice
annotationsQueue.splice(0, 0, ...annotations);
setTimeout(proccessAnnotationsQueue, annotationsRetryDelay);
setTimeout(() => proccessAnnotationsQueue(submitAnnotations), annotationsRetryDelay);
} else {
// ask tiago
const delayPerc = Math.min(
@ -54,11 +53,15 @@ const proccessAnnotationsQueue = async () => {
) / annotationsMaxDelayQueueSize;
const delayDelta = annotationsBufferTimeMax - annotationsBufferTimeMin;
const delayTime = annotationsBufferTimeMin + delayDelta * delayPerc;
setTimeout(proccessAnnotationsQueue, delayTime);
setTimeout(() => proccessAnnotationsQueue(submitAnnotations), delayTime);
}
} catch (error) {
annotationsQueue.splice(0, 0, ...annotations);
setTimeout(() => proccessAnnotationsQueue(submitAnnotations), annotationsRetryDelay);
}
};
const sendAnnotation = (annotation) => {
const sendAnnotation = (annotation, submitAnnotations) => {
// Prevent sending annotations while disconnected
// TODO: Change this to add the annotation, but delay the send until we're
// reconnected. With this it will miss things
@ -70,7 +73,7 @@ const sendAnnotation = (annotation) => {
} else {
annotationsQueue.push(annotation);
}
if (!annotationsSenderIsRunning) setTimeout(proccessAnnotationsQueue, annotationsBufferTimeMin);
if (!annotationsSenderIsRunning) setTimeout(() => proccessAnnotationsQueue(submitAnnotations), annotationsBufferTimeMin);
};
const getMultiUser = (whiteboardId) => {
@ -87,7 +90,7 @@ const getMultiUser = (whiteboardId) => {
return data.multiUser;
};
const persistShape = (shape, whiteboardId, isModerator) => {
const persistShape = async (shape, whiteboardId, isModerator, submitAnnotations) => {
const annotation = {
id: shape.id,
annotationInfo: { ...shape, isModerator },
@ -95,13 +98,7 @@ const persistShape = (shape, whiteboardId, isModerator) => {
userId: Auth.userID,
};
sendAnnotation(annotation);
};
const removeShapes = (shapes, whiteboardId) => makeCall('deleteAnnotations', shapes, whiteboardId);
const changeCurrentSlide = (s) => {
makeCall('changeCurrentSlide', s);
sendAnnotation(annotation, submitAnnotations);
};
const initDefaultPages = (count = 1) => {
@ -283,8 +280,6 @@ export {
sendAnnotation,
getMultiUser,
persistShape,
removeShapes,
changeCurrentSlide,
notifyNotAllowedChange,
notifyShapeNumberExceeded,
toggleToolsAnimations,

View File

@ -1,11 +1,4 @@
import React from 'react';
import { isEqual } from 'radash';
import {
persistShape,
removeShapes,
notifyNotAllowedChange,
notifyShapeNumberExceeded,
} from './service';
const WHITEBOARD_CONFIG = Meteor.settings.public.whiteboard;
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
@ -90,118 +83,6 @@ const isValidShapeType = (shape) => {
return !invalidTypes.includes(shape?.type);
};
const sendShapeChanges = (
app,
changedShapes,
shapes,
prevShapes,
hasShapeAccess,
whiteboardId,
currentUser,
intl,
redo = false,
) => {
let isModerator = currentUser?.role === ROLE_MODERATOR;
const invalidChange = Object.keys(changedShapes)
.find((id) => !hasShapeAccess(id));
const invalidShapeType = Object.keys(changedShapes)
.find((id) => !isValidShapeType(changedShapes[id]));
const currentShapes = app?.document?.pages[app?.currentPageId]?.shapes;
const { maxNumberOfAnnotations } = WHITEBOARD_CONFIG;
// -1 for background shape
const shapeNumberExceeded = Object.keys(currentShapes).length - 1 > maxNumberOfAnnotations;
const isInserting = Object.keys(changedShapes)
.filter(
(shape) => typeof changedShapes[shape] === 'object'
&& changedShapes[shape].type
&& !prevShapes[shape],
).length !== 0;
if (invalidChange || invalidShapeType || (shapeNumberExceeded && isInserting)) {
if (shapeNumberExceeded) {
notifyShapeNumberExceeded(intl, maxNumberOfAnnotations);
} else {
notifyNotAllowedChange(intl);
}
const modApp = app;
// undo last command without persisting to not generate the onUndo/onRedo callback
if (!redo) {
const command = app.stack[app.pointer];
modApp.pointer -= 1;
app.applyPatch(command.before, 'undo');
return;
// eslint-disable-next-line no-else-return
} else {
modApp.pointer += 1;
const command = app.stack[app.pointer];
app.applyPatch(command.after, 'redo');
return;
}
}
const 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).forEach(([, 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, isModerator);
}
}
});
}
let modShape = shape;
if (!shape.id) {
// check it already exists (otherwise we need the full shape)
if (!shapes[id]) {
modShape = app.getShape(id);
}
modShape.id = id;
}
const shapeBounds = app.getShapeBounds(id);
const size = [shapeBounds.width, shapeBounds.height];
if (!shapes[id] || (shapes[id] && !isEqual(shapes[id].size, size))) {
modShape.size = size;
}
if (!shapes[id] || (shapes[id] && !shapes[id].userId)) {
modShape.userId = currentUser?.userId;
}
// do not change moderator status for existing shapes
if (shapes[id]) {
isModerator = shapes[id].isModerator;
}
persistShape(modShape, whiteboardId, isModerator);
}
});
// order the ids of shapes being deleted to prevent crash
// when removing a group shape before its children
const orderedDeletedShapes = [];
deletedShapes.forEach((eid) => {
if (shapes[eid]?.type !== 'group') {
orderedDeletedShapes.unshift(eid);
} else {
orderedDeletedShapes.push(eid);
}
});
if (orderedDeletedShapes.length > 0) {
removeShapes(orderedDeletedShapes, whiteboardId);
}
};
// map different localeCodes from bbb to tldraw
const mapLanguage = (language) => {
// bbb has xx-xx but in tldraw it's only xx
@ -276,10 +157,10 @@ const getTextSize = (text, style, padding) => {
};
const Utils = {
usePrevious, findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, getTextSize,
usePrevious, findRemoved, filterInvalidShapes, mapLanguage, getTextSize,
};
export default Utils;
export {
usePrevious, findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, getTextSize,
usePrevious, findRemoved, filterInvalidShapes, mapLanguage, getTextSize,
};

View File

@ -3,13 +3,10 @@ import '/imports/startup/server';
// 2x
import '/imports/api/meetings/server';
import '/imports/api/users/server';
import '/imports/api/annotations/server';
import '/imports/api/cursor/server';
import '/imports/api/polls/server';
import '/imports/api/captions/server';
import '/imports/api/presentations/server';
import '/imports/api/presentation-upload-token/server';
import '/imports/api/slides/server';
import '/imports/api/breakouts/server';
import '/imports/api/breakouts-history/server';
import '/imports/api/screenshare/server';