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 ( - {disableActions ? null : ( - this.handleCurrentChange(item)} - /> - { hideRemove ? null : ( - this.handleRemove(item)} - > - - - )} - )} + this.handleCurrentChange(item)} + /> + { hideRemove ? null : ( + this.handleRemove(item)} + > + + + )} ); @@ -311,14 +353,13 @@ class PresentationUploader extends Component { const { disableActions } = this.state; - // TODO: Change the multiple prop when the endpoint supports multiple files - const hasSomeFileNotUploaded = this.state.presentations.some(_ => !_.isUploaded); + const hasSomeFileNotUploaded = this.state.presentations.some(_ => !_.upload.done); if (hasSomeFileNotUploaded || disableActions) return null; return ( new Promise((res, rej) => { + const xhr = new XMLHttpRequest(); + + xhr.open(opts.method || 'get', url); + + Object.keys(opts.headers || {}) + .forEach(k => xhr.setRequestHeader(k, opts.headers[k])); + + xhr.onload = (e) => { + if (e.target.status !== 200) { + return rej({ code: e.target.status, message: e.target.statusText }); + } + + return res(e.target.responseText); + }; + xhr.onerror = rej; + if (xhr.upload && onProgress) { + xhr.upload.addEventListener('progress', onProgress, false); + } + xhr.send(opts.body); +}); + const getPresentations = () => Presentations .find() .fetch() - .map(p => ({ - _id: p._id, - id: p.presentation.id, - filename: p.presentation.name, - uploadedAt: p.upload ? p.upload.date : new Date(), - isCurrent: p.presentation.current, - isUploaded: true, - isProcessed: p.conversion.done, - conversion: p.conversion, + .map(presentation => ({ + id: presentation.id, + filename: presentation.name, + isCurrent: presentation.current, + upload: { done: true, error: false }, + conversion: presentation.conversion || { done: true, error: false }, })); -const uploadPresentation = (file, meetingID, endpoint) => { +const uploadPresentation = (file, meetingID, endpoint, onError, onProgress) => { const data = new FormData(); - data.append('Filename', file.filename); data.append('presentation_name', file.filename); data.append('fileUpload', file); data.append('conference', meetingID); data.append('room', meetingID); - /* TODO: Should we do the request on the html5 server instead of the client? */ - /* TODO: Upload progress */ - return fetch(endpoint, { + const opts = { method: 'POST', body: data, - }); + }; + + return futch(endpoint, opts, onProgress).catch(onError); }; const uploadPresentations = (presentationsToUpload, meetingID, uploadEndpoint) => Promise.all( presentationsToUpload - .map(p => uploadPresentation(p.file, meetingID, uploadEndpoint)), + .map(p => uploadPresentation(p.file, meetingID, uploadEndpoint, p.onError, p.onProgress)), ); const removePresentation = presentationID => call('removePresentation', presentationID); diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.scss index c2e5f9b799..65740dc93b 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.scss @@ -108,8 +108,7 @@ $item-height: 1rem; background-color: transparentize($color-danger, .75); } -.tableItemUploading, -.tableItemProcessing { +.tableItemAnimated { background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, diff --git a/bigbluebutton-html5/private/config/public/acl.yaml b/bigbluebutton-html5/private/config/public/acl.yaml index 8534af3b41..88f121f7ca 100644 --- a/bigbluebutton-html5/private/config/public/acl.yaml +++ b/bigbluebutton-html5/private/config/public/acl.yaml @@ -30,3 +30,5 @@ acl: methods: - 'assignPresenter' - 'switchSlide' + - 'removePresentation' + - 'sharePresentation'