Presentation Uploader to 2.0

This commit is contained in:
Oswaldo Acauan 2017-09-07 15:18:14 -03:00
parent a2268494e6
commit c0307e36a6
14 changed files with 216 additions and 131 deletions

View File

@ -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);

View File

@ -7,7 +7,7 @@ import addPresentation from '../modifiers/addPresentation';
const clearCurrentPresentation = (meetingId, presentationId) => {
const selector = {
meetingId,
presentationId: { $ne: presentationId },
id: { $ne: presentationId },
current: true,
};

View File

@ -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 = {

View File

@ -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 = {

View File

@ -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);
}

View File

@ -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,
}));

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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 }),
),
};

View File

@ -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));

View File

@ -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 (
<time dateTime={item.uploadedAt}>
<FormattedDate
value={item.uploadedAt}
day="2-digit"
month="2-digit"
year="numeric"
hour="2-digit"
minute="2-digit"
/>
</time>
);
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 (
<tr
@ -278,24 +321,23 @@ class PresentationUploader extends Component {
{this.renderPresentationItemStatus(item)}
</td>
<td className={styles.tableItemActions}>
{disableActions ? null : (<span>
<Checkbox
disabled={disableCheck}
ariaLabel={'Set as current presentation'}
className={styles.itemAction}
checked={item.isCurrent}
onChange={() => this.handleCurrentChange(item)}
/>
{ hideRemove ? null : (
<ButtonBase
className={cx(styles.itemAction, styles.itemActionRemove)}
label={'Remove presentation'}
onClick={() => this.handleRemove(item)}
>
<Icon iconName={'delete'} />
</ButtonBase>
)}
</span>)}
<Checkbox
disabled={disableActions}
ariaLabel={'Set as current presentation'}
className={styles.itemAction}
checked={item.isCurrent}
onChange={() => this.handleCurrentChange(item)}
/>
{ hideRemove ? null : (
<ButtonBase
disabled={disableActions}
className={cx(styles.itemAction, styles.itemActionRemove)}
label={'Remove presentation'}
onClick={() => this.handleRemove(item)}
>
<Icon iconName={'delete'} />
</ButtonBase>
)}
</td>
</tr>
);
@ -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 (
<Dropzone
multiple={false}
multiple
className={styles.dropzone}
activeClassName={styles.dropzoneActive}
rejectClassName={styles.dropzoneReject}

View File

@ -1,43 +1,60 @@
import Presentations from '/imports/api/presentations';
import Presentations from '/imports/api/2.0/presentations';
import Auth from '/imports/ui/services/auth';
import { call } from '/imports/ui/services/api';
const futch = (url, opts = {}, onProgress) => 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);

View File

@ -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%,

View File

@ -30,3 +30,5 @@ acl:
methods:
- 'assignPresenter'
- 'switchSlide'
- 'removePresentation'
- 'sharePresentation'