diff --git a/bigbluebutton-html5/imports/api/2.0/presentations/server/eventHandlers.js b/bigbluebutton-html5/imports/api/2.0/presentations/server/eventHandlers.js index 397d90af35..91de305505 100644 --- a/bigbluebutton-html5/imports/api/2.0/presentations/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/2.0/presentations/server/eventHandlers.js @@ -2,8 +2,11 @@ import RedisPubSub from '/imports/startup/server/redis2x'; import handlePresentationChange from './handlers/presentationChange'; import handlePresentationInfoReply from './handlers/presentationInfoReply'; import handlePresentationRemove from './handlers/presentationRemove'; +import handlePresentationCurrentChange from './handlers/presentationCurrentChange'; + RedisPubSub.on('SyncGetPresentationInfoRespMsg', handlePresentationInfoReply); RedisPubSub.on('NewPresentationEvtMsg', handlePresentationChange); RedisPubSub.on('PresentationConversionCompletedEvtMsg', handlePresentationChange); RedisPubSub.on('RemovePresentationEvtMsg', handlePresentationRemove); +RedisPubSub.on('SetCurrentPresentationEvtMsg', handlePresentationCurrentChange); diff --git a/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationChange.js b/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationChange.js index 38559bdf91..f8b235abd8 100644 --- a/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationChange.js +++ b/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationChange.js @@ -7,7 +7,7 @@ import addPresentation from '../modifiers/addPresentation'; const clearCurrentPresentation = (meetingId, presentationId) => { const selector = { meetingId, - presentationId: { $ne: presentationId }, + id: { $ne: presentationId }, current: true, }; diff --git a/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationConversionDone.js b/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationConversionDone.js index 39efba82d5..d59686590a 100755 --- a/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationConversionDone.js +++ b/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationConversionDone.js @@ -1,10 +1,9 @@ import { check } from 'meteor/check'; import Presentations from '/imports/api/presentations'; -export default function handlePresentationConversionDone({ payload }) { - const meetingId = payload.meeting_id; - const presentationId = payload.presentation.id; - const status = payload.message_key; +export default function handlePresentationConversionDone({ body }, meetingId) { + const presentationId = body.presentationId; + const status = body.messageKey; check(meetingId, String); check(presentationId, String); @@ -18,7 +17,7 @@ export default function handlePresentationConversionDone({ payload }) { const selector = { meetingId, - 'presentation.id': presentationId, + id: presentationId, }; const modifier = { diff --git a/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationConversionUpdate.js b/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationConversionUpdate.js index ffd0ff8fc2..b155ac8547 100755 --- a/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationConversionUpdate.js +++ b/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationConversionUpdate.js @@ -18,10 +18,9 @@ const GENERATED_SLIDE_KEY = 'GENERATED_SLIDE'; // const GENERATED_SVGIMAGES_KEY = 'GENERATED_SVGIMAGES'; // const CONVERSION_COMPLETED_KEY = 'CONVERSION_COMPLETED'; -export default function handlePresentationConversionUpdate({ payload }) { - const meetingId = payload.meeting_id; - const presentationId = payload.presentation_id; - const status = payload.message_key; +export default function handlePresentationConversionUpdate({ body }, meetingId) { + const presentationId = body.presentationId; + const status = body.messageKey; check(meetingId, String); check(presentationId, String); @@ -36,7 +35,7 @@ export default function handlePresentationConversionUpdate({ payload }) { switch (status) { case SUPPORTED_DOCUMENT_KEY: statusModifier['presentation.id'] = presentationId; - statusModifier['presentation.name'] = payload.presentation_name; + statusModifier['presentation.name'] = body.presentation_name; break; case UNSUPPORTED_DOCUMENT_KEY: @@ -45,13 +44,13 @@ export default function handlePresentationConversionUpdate({ payload }) { case PAGE_COUNT_FAILED_KEY: case PAGE_COUNT_EXCEEDED_KEY: statusModifier['presentation.id'] = presentationId; - statusModifier['presentation.name'] = payload.presentation_name; + statusModifier['presentation.name'] = body.presentation_name; statusModifier['conversion.error'] = true; break; case GENERATED_SLIDE_KEY: - statusModifier['conversion.pages_completed'] = payload.pages_completed; - statusModifier['conversion.num_pages'] = payload.num_pages; + statusModifier['conversion.pagesCompleted'] = body.pagesCompleted; + statusModifier['conversion.numPages'] = body.numPages; break; default: @@ -61,7 +60,7 @@ export default function handlePresentationConversionUpdate({ payload }) { const selector = { meetingId, - 'presentation.id': presentationId, + id: presentationId, }; const modifier = { diff --git a/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationCurrentChange.js b/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationCurrentChange.js new file mode 100644 index 0000000000..145dd38954 --- /dev/null +++ b/bigbluebutton-html5/imports/api/2.0/presentations/server/handlers/presentationCurrentChange.js @@ -0,0 +1,11 @@ +import { check } from 'meteor/check'; +import changeCurrentPresentation from '../modifiers/changeCurrentPresentation'; + +export default function presentationCurrentChange({ body }, meetingId) { + const { presentationId } = body; + + check(meetingId, String); + check(presentationId, String); + + return changeCurrentPresentation(meetingId, presentationId); +} diff --git a/bigbluebutton-html5/imports/api/2.0/presentations/server/methods.js b/bigbluebutton-html5/imports/api/2.0/presentations/server/methods.js index 1ce65c3698..8dd3871c99 100644 --- a/bigbluebutton-html5/imports/api/2.0/presentations/server/methods.js +++ b/bigbluebutton-html5/imports/api/2.0/presentations/server/methods.js @@ -1,4 +1,9 @@ import { Meteor } from 'meteor/meteor'; +import mapToAcl from '/imports/startup/mapToAcl'; +import removePresentation from './methods/removePresentation'; +import sharePresentation from './methods/sharePresentation'; -Meteor.methods({ -}); +Meteor.methods(mapToAcl(['methods.removePresentation', 'methods.sharePresentation'], { + removePresentation, + sharePresentation, +})); diff --git a/bigbluebutton-html5/imports/api/2.0/presentations/server/methods/removePresentation.js b/bigbluebutton-html5/imports/api/2.0/presentations/server/methods/removePresentation.js index bd92af55e3..75e2505719 100755 --- a/bigbluebutton-html5/imports/api/2.0/presentations/server/methods/removePresentation.js +++ b/bigbluebutton-html5/imports/api/2.0/presentations/server/methods/removePresentation.js @@ -1,16 +1,11 @@ -import { isAllowedTo } from '/imports/startup/server/userPermissions'; -import RedisPubSub from '/imports/startup/server/redis'; +import RedisPubSub from '/imports/startup/server/redis2x'; import { check } from 'meteor/check'; -import Presentations from '/imports/api/presentations'; +import Presentations from '/imports/api/2.0/presentations'; export default function removePresentation(credentials, presentationId) { const REDIS_CONFIG = Meteor.settings.redis; - const CHANNEL = REDIS_CONFIG.channels.toBBBApps.presentation; - const EVENT_NAME = 'remove_presentation'; - - if (!isAllowedTo('removePresentation', credentials)) { - throw new Meteor.Error('not-allowed', 'You are not allowed to removePresentation'); - } + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'RemovePresentationPubMsg'; const { meetingId, requesterUserId } = credentials; @@ -18,19 +13,28 @@ export default function removePresentation(credentials, presentationId) { check(requesterUserId, String); check(presentationId, String); - const currentPresentation = Presentations.findOne({ + const presenationToDelete = Presentations.findOne({ meetingId, - 'presentation.id': presentationId, + id: presentationId, }); - if (currentPresentation.name === 'default.pdf') { + if (presenationToDelete.name === 'default.pdf') { throw new Meteor.Error('not-allowed', 'You are not allowed to remove the default slide'); } + if (presenationToDelete.current) { + throw new Meteor.Error('not-allowed', 'You are not allowed to remove the current presentation'); + } + const payload = { - meeting_id: meetingId, - presentation_id: presentationId, + presentationId, }; - return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload); + const header = { + meetingId, + name: EVENT_NAME, + userId: requesterUserId, + }; + + return RedisPubSub.publish(CHANNEL, EVENT_NAME, meetingId, payload, header); } diff --git a/bigbluebutton-html5/imports/api/2.0/presentations/server/methods/sharePresentation.js b/bigbluebutton-html5/imports/api/2.0/presentations/server/methods/sharePresentation.js index af8c507a40..333d8d6901 100755 --- a/bigbluebutton-html5/imports/api/2.0/presentations/server/methods/sharePresentation.js +++ b/bigbluebutton-html5/imports/api/2.0/presentations/server/methods/sharePresentation.js @@ -1,16 +1,11 @@ -import { isAllowedTo } from '/imports/startup/server/userPermissions'; -import RedisPubSub from '/imports/startup/server/redis'; +import RedisPubSub from '/imports/startup/server/redis2x'; import { check } from 'meteor/check'; -import Presentations from '/imports/api/presentations'; +import Presentations from '/imports/api/2.0/presentations'; export default function sharePresentation(credentials, presentationId, shouldShare = true) { const REDIS_CONFIG = Meteor.settings.redis; - const CHANNEL = REDIS_CONFIG.channels.toBBBApps.presentation; - const EVENT_NAME = 'share_presentation'; - - if (!isAllowedTo('sharePresentation', credentials)) { - throw new Meteor.Error('not-allowed', 'You are not allowed to sharePresentation'); - } + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'SetCurrentPresentationPubMsg'; const { meetingId, requesterUserId } = credentials; @@ -21,19 +16,23 @@ export default function sharePresentation(credentials, presentationId, shouldSha const currentPresentation = Presentations.findOne({ meetingId, - 'presentation.id': presentationId, - 'presentation.current': true, + id: presentationId, + current: true, }); - if (currentPresentation && currentPresentation.presentation.id === presentationId) { + if (currentPresentation && currentPresentation.id === presentationId) { return Promise.resolve(); } const payload = { - meeting_id: meetingId, - presentation_id: presentationId, - share: shouldShare, + presentationId, }; - return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload); + const header = { + meetingId, + name: EVENT_NAME, + userId: requesterUserId, + }; + + return RedisPubSub.publish(CHANNEL, EVENT_NAME, meetingId, payload, header); } diff --git a/bigbluebutton-html5/imports/api/2.0/presentations/server/modifiers/addPresentation.js b/bigbluebutton-html5/imports/api/2.0/presentations/server/modifiers/addPresentation.js index 6fed48b922..2c9278c212 100755 --- a/bigbluebutton-html5/imports/api/2.0/presentations/server/modifiers/addPresentation.js +++ b/bigbluebutton-html5/imports/api/2.0/presentations/server/modifiers/addPresentation.js @@ -43,7 +43,11 @@ export default function addPresentation(meetingId, presentation) { const modifier = { $set: Object.assign( - { meetingId }, + { + meetingId, + 'conversion.done': true, + 'conversion.error': false, + }, flat(presentation, { safe: true }), ), }; diff --git a/bigbluebutton-html5/imports/api/2.0/slides/server/eventHandlers.js b/bigbluebutton-html5/imports/api/2.0/slides/server/eventHandlers.js index 65ebaf2fed..b96f4a1a74 100644 --- a/bigbluebutton-html5/imports/api/2.0/slides/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/2.0/slides/server/eventHandlers.js @@ -4,3 +4,5 @@ import handleSlideChange from './handlers/slideChange'; RedisPubSub.on('ResizeAndMovePageEvtMsg', handleSlideResize); RedisPubSub.on('SetCurrentPageEvtMsg', handleSlideChange); +RedisPubSub.on('PresentationConversionUpdateEvtMsg', (...args) => console.error(args)); +RedisPubSub.on('PresentationPageGeneratedEvtMsg', (...args) => console.error(args)); diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx index 8f36b316f0..08d2af1255 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx @@ -1,4 +1,5 @@ import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedDate } from 'react-intl'; import Dropzone from 'react-dropzone'; import update from 'immutability-helper'; @@ -12,6 +13,25 @@ import styles from './styles.scss'; const DEFAULT_FILENAME = 'default.pdf'; +const propTypes = { + defaultFileName: PropTypes.string.isRequired, + fileSizeMin: PropTypes.number.isRequired, + fileSizeMax: PropTypes.number.isRequired, + handleSave: PropTypes.func.isRequired, + fileValidMimeTypes: PropTypes.arrayOf(PropTypes.string).isRequired, + presentations: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + filename: PropTypes.string.isRequired, + isCurrent: PropTypes.bool.isRequired, + conversion: PropTypes.object, + upload: PropTypes.object, + })).isRequired, +}; + +const defaultProps = { + defaultFileName: 'default.pdf', +}; + const intlMessages = defineMessages({ title: { id: 'app.presentationUploder.title', @@ -51,9 +71,13 @@ const intlMessages = defineMessages({ id: 'app.presentationUploder.fileToUpload', defaultMessage: 'To be uploaded...', }, - genericError: { - id: 'app.presentationUploder.conversion.genericError', - defaultMessage: 'Ops, something went wrong.', + uploadProcess: { + id: 'app.presentationUploder.upload.progress', + defaultMessage: 'Uploading ({progress}%)', + }, + 413: { + id: 'app.presentationUploder.upload.413', + defaultMessage: 'File is too large.', }, conversionProcessingSlides: { id: 'app.presentationUploder.conversion.conversionProcessingSlides', @@ -73,7 +97,22 @@ const intlMessages = defineMessages({ }, }); -const isProcessingOrUploading = item => item && (!item.isProcessed || !item.isUploaded); +const isProcessingOrUploading = item => item && (!item.conversion.done || !item.upload.done); + +function updateFileUploadState(id, state) { + this.setState(({ presentations }) => { + const fileIndex = presentations.findIndex(f => f.id === id); + return { + presentations: update(presentations, { + [fileIndex]: { $apply: file => + update(file, { + upload: { $apply: upload => update(upload, { $merge: state }) }, + }), + }, + }), + }; + }); +} class PresentationUploader extends Component { constructor(props) { @@ -131,21 +170,24 @@ class PresentationUploader extends Component { id: file.name, file, filename: file.name, - uploadedAt: new Date(), - isCurrent: true, - isUploaded: false, - isProcessed: false, + isCurrent: false, conversion: { done: false, error: false }, + upload: { done: false, error: false, progress: 0 }, + onProgress: (event) => { + if (!event.lengthComputable) return; + + updateFileUploadState.call(this, file.name, { + progress: (event.loaded / event.total) * 100, + done: event.loaded === event.total, + }); + }, + onError: (error) => { + updateFileUploadState.call(this, file.name, { error }); + }, })); this.setState(({ presentations }) => ({ - presentations: presentations - .map((_) => { - const p = _; - p.isCurrent = false; - return p; - }) - .concat(presentationsToUpload), + presentations: presentations.concat(presentationsToUpload), })); } @@ -198,7 +240,6 @@ class PresentationUploader extends Component { const { presentations } = this.state; const presentationsSorted = presentations - .sort((a, b) => a.uploadedAt.getTime() - b.uploadedAt.getTime()) .sort((a, b) => b.filename === DEFAULT_FILENAME); return ( @@ -215,18 +256,31 @@ class PresentationUploader extends Component { renderPresentationItemStatus(item) { const { intl } = this.props; - if (!item.isUploaded) { return intl.formatMessage(intlMessages.fileToUpload); } + if (!item.upload.done && item.upload.progress === 0) { + return intl.formatMessage(intlMessages.fileToUpload); + } - if (!item.isProcessed && item.conversion.error) { + if (!item.upload.done && !item.upload.error) { + return intl.formatMessage(intlMessages.uploadProcess, { + progress: item.upload.progress, + }); + } + + if (item.upload.done && item.upload.error) { + const errorMessage = intlMessages[item.upload.error.code] || intlMessages.genericError; + return intl.formatMessage(errorMessage); + } + + if (!item.conversion.done && item.conversion.error) { const errorMessage = intlMessages[status] || intlMessages.genericError; return intl.formatMessage(errorMessage); } - if (!item.isProcessed && !item.conversion.error) { - if (item.conversion.pages_completed < item.conversion.num_pages) { + if (!item.conversion.done && !item.conversion.error) { + if (item.conversion.pagesCompleted < item.conversion.numPages) { return intl.formatMessage(intlMessages.conversionProcessingSlides, { - current: item.conversion.pages_completed, - total: item.conversion.num_pages, + current: item.conversion.pagesCompleted, + total: item.conversion.numPages, }); } @@ -235,33 +289,22 @@ class PresentationUploader extends Component { return intl.formatMessage(conversionStatusMessage); } - return ( - - ); + return null; } renderPresentationItem(item) { - const { disableActions, presentations } = this.state; + const { disableActions } = this.state; const itemClassName = {}; itemClassName[styles.tableItemNew] = item.id === item.filename; - itemClassName[styles.tableItemUploading] = false; - itemClassName[styles.tableItemProcessing] = !item.isProcessed && item.isUploaded; - itemClassName[styles.tableItemError] = item.conversion.error || false; + itemClassName[styles.tableItemUploading] = !item.upload.done; + itemClassName[styles.tableItemProcessing] = !item.conversion.done; + itemClassName[styles.tableItemError] = item.conversion.error || item.upload.error; + itemClassName[styles.tableItemAnimated] = + !item.conversion.done && (!item.upload.done && item.upload.progress > 0); - const hideRemove = (item.isCurrent && item.isUploaded) || item.filename === DEFAULT_FILENAME; - const hasSomeFileNotUploaded = presentations.some(_ => !_.isUploaded); - const disableCheck = hasSomeFileNotUploaded; + const hideRemove = (item.isCurrent && item.upload.done) || item.filename === DEFAULT_FILENAME; return (