bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx

413 lines
12 KiB
React
Raw Normal View History

2017-04-29 02:42:32 +08:00
import React, { Component } from 'react';
2017-09-08 02:18:14 +08:00
import PropTypes from 'prop-types';
2017-04-29 02:42:32 +08:00
import { defineMessages, injectIntl, FormattedDate } from 'react-intl';
import Dropzone from 'react-dropzone';
2017-05-04 00:36:16 +08:00
import update from 'immutability-helper';
import cx from 'classnames';
2017-05-04 04:51:17 +08:00
import ModalFullscreen from '/imports/ui/components/modal/fullscreen/component';
2017-04-29 02:42:32 +08:00
import Icon from '/imports/ui/components/icon/component';
import ButtonBase from '/imports/ui/components/button/base/component';
2017-05-04 00:36:16 +08:00
import Checkbox from '/imports/ui/components/checkbox/component';
2017-04-29 02:42:32 +08:00
import styles from './styles.scss';
2017-05-06 04:17:38 +08:00
const DEFAULT_FILENAME = 'default.pdf';
2017-09-08 02:18:14 +08:00
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',
};
2017-04-29 02:42:32 +08:00
const intlMessages = defineMessages({
title: {
id: 'app.presentationUploder.title',
defaultMessage: 'Presentation',
},
message: {
id: 'app.presentationUploder.message',
defaultMessage: `As a presenter in BigBlueButton, you have the ability of
uploading any office document or PDF file. We recommend for the best results,
to please upload a PDF file.`,
},
confirmLabel: {
id: 'app.presentationUploder.confirmLabel',
defaultMessage: 'Start',
},
confirmDesc: {
id: 'app.presentationUploder.confirmDesc',
defaultMessage: 'Save your changes and start the presentation',
},
dismissLabel: {
id: 'app.presentationUploder.dismissLabel',
defaultMessage: 'Cancel',
},
dismissDesc: {
id: 'app.presentationUploder.dismissDesc',
defaultMessage: 'Closes and discarts your changes',
},
dropzoneLabel: {
id: 'app.presentationUploder.dropzoneLabel',
defaultMessage: 'Drag files here to upload',
},
browseFilesLabel: {
id: 'app.presentationUploder.browseFilesLabel',
defaultMessage: 'or browse for files',
},
2017-05-06 04:17:38 +08:00
fileToUpload: {
id: 'app.presentationUploder.fileToUpload',
defaultMessage: 'To be uploaded...',
},
2017-09-08 02:18:14 +08:00
uploadProcess: {
id: 'app.presentationUploder.upload.progress',
defaultMessage: 'Uploading ({progress}%)',
},
413: {
id: 'app.presentationUploder.upload.413',
defaultMessage: 'File is too large.',
2017-05-06 04:17:38 +08:00
},
conversionProcessingSlides: {
id: 'app.presentationUploder.conversion.conversionProcessingSlides',
defaultMessage: 'Processing page {current} of {total}',
},
genericConversionStatus: {
id: 'app.presentationUploder.conversion.genericConversionStatus',
defaultMessage: 'Converting file...',
},
GENERATING_THUMBNAIL: {
id: 'app.presentationUploder.conversion.generatingThumbnail',
defaultMessage: 'Generating thumbnails...',
},
GENERATED_SLIDE: {
id: 'app.presentationUploder.conversion.generatedSlides',
defaultMessage: 'Slides generated...',
},
2017-04-29 02:42:32 +08:00
});
2017-09-08 02:18:14 +08:00
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 }) },
}),
},
}),
};
});
}
2017-05-06 04:17:38 +08:00
class PresentationUploader extends Component {
2017-04-29 02:42:32 +08:00
constructor(props) {
super(props);
this.state = {
presentations: props.presentations,
2017-05-06 04:17:38 +08:00
preventClosing: false,
disableActions: false,
2017-04-29 02:42:32 +08:00
};
this.handleConfirm = this.handleConfirm.bind(this);
this.handleDismiss = this.handleDismiss.bind(this);
this.handleFiledrop = this.handleFiledrop.bind(this);
this.handleCurrentChange = this.handleCurrentChange.bind(this);
this.handleRemove = this.handleRemove.bind(this);
}
2017-05-06 04:17:38 +08:00
componentWillReceiveProps(nextProps) {
const presentationStateUpdated =
this.state.presentations.map(p =>
nextProps.presentations.find(_ => _.filename === p.filename));
const stillBusy = presentationStateUpdated.some(isProcessingOrUploading);
this.setState({
presentations: presentationStateUpdated,
preventClosing: stillBusy,
disableActions: stillBusy,
});
}
2017-04-29 02:42:32 +08:00
handleConfirm() {
const { presentations } = this.state;
2017-05-04 00:36:16 +08:00
2017-05-06 04:17:38 +08:00
this.setState({
disableActions: true,
preventClosing: true,
});
2017-05-04 00:36:16 +08:00
2017-05-06 04:17:38 +08:00
return this.props.handleSave(presentations);
2017-04-29 02:42:32 +08:00
}
handleDismiss() {
return new Promise((resolve) => {
2017-05-06 04:17:38 +08:00
this.setState({
preventClosing: false,
disableActions: false,
}, resolve);
});
2017-04-29 02:42:32 +08:00
}
handleFiledrop(files) {
const presentationsToUpload = files.map(file => ({
2017-05-04 00:36:16 +08:00
id: file.name,
file,
2017-04-29 02:42:32 +08:00
filename: file.name,
2017-09-08 02:18:14 +08:00
isCurrent: false,
conversion: { done: false, error: false },
2017-09-08 02:18:14 +08:00
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 });
},
2017-04-29 02:42:32 +08:00
}));
this.setState(({ presentations }) => ({
2017-09-08 02:18:14 +08:00
presentations: presentations.concat(presentationsToUpload),
2017-04-29 02:42:32 +08:00
}));
}
handleCurrentChange(item) {
const { presentations } = this.state;
2017-04-29 02:42:32 +08:00
const currentIndex = presentations.findIndex(p => p.isCurrent);
const newCurrentIndex = presentations.indexOf(item);
const commands = {};
2017-04-29 02:42:32 +08:00
commands[currentIndex] = {
$apply: (_) => {
const p = _;
2017-04-29 02:42:32 +08:00
p.isCurrent = false;
return p;
},
};
commands[newCurrentIndex] = {
$apply: (_) => {
const p = _;
2017-04-29 02:42:32 +08:00
p.isCurrent = true;
return p;
},
};
const presentationsUpdated = update(presentations, commands);
2017-04-29 02:42:32 +08:00
this.setState({
presentations: presentationsUpdated,
});
}
handleRemove(item) {
const { presentations } = this.state;
2017-04-29 02:42:32 +08:00
const toRemoveIndex = presentations.indexOf(item);
2017-05-06 04:17:38 +08:00
const toRemove = presentations[toRemoveIndex];
if (toRemove.isCurrent) {
const defaultPresentation = presentations.find(_ => _.filename === DEFAULT_FILENAME);
this.handleCurrentChange(defaultPresentation);
}
2017-04-29 02:42:32 +08:00
this.setState({
presentations: update(presentations, {
$splice: [[toRemoveIndex, 1]],
}),
});
}
renderPresentationList() {
const { presentations } = this.state;
2017-04-29 02:42:32 +08:00
const presentationsSorted = presentations
2017-05-06 04:17:38 +08:00
.sort((a, b) => b.filename === DEFAULT_FILENAME);
2017-04-29 02:42:32 +08:00
return (
2017-05-04 00:36:16 +08:00
<div className={styles.fileList}>
<table className={styles.table}>
<tbody>
{ presentationsSorted.map(item => this.renderPresentationItem(item))}
2017-05-04 00:36:16 +08:00
</tbody>
</table>
</div>
2017-04-29 02:42:32 +08:00
);
}
2017-05-06 04:17:38 +08:00
renderPresentationItemStatus(item) {
const { intl } = this.props;
2017-05-06 04:17:38 +08:00
2017-09-08 02:18:14 +08:00
if (!item.upload.done && item.upload.progress === 0) {
return intl.formatMessage(intlMessages.fileToUpload);
}
if (!item.upload.done && !item.upload.error) {
return intl.formatMessage(intlMessages.uploadProcess, {
progress: item.upload.progress,
});
}
2017-05-06 04:17:38 +08:00
2017-09-08 02:18:14 +08:00
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) {
2017-05-06 04:17:38 +08:00
const errorMessage = intlMessages[status] || intlMessages.genericError;
return intl.formatMessage(errorMessage);
}
2017-09-08 02:18:14 +08:00
if (!item.conversion.done && !item.conversion.error) {
if (item.conversion.pagesCompleted < item.conversion.numPages) {
2017-05-06 04:17:38 +08:00
return intl.formatMessage(intlMessages.conversionProcessingSlides, {
2017-09-08 02:18:14 +08:00
current: item.conversion.pagesCompleted,
total: item.conversion.numPages,
2017-05-06 04:17:38 +08:00
});
}
const conversionStatusMessage =
intlMessages[item.conversion.status] || intlMessages.genericConversionStatus;
return intl.formatMessage(conversionStatusMessage);
}
2017-09-08 02:18:14 +08:00
return null;
2017-05-06 04:17:38 +08:00
}
2017-04-29 02:42:32 +08:00
renderPresentationItem(item) {
2017-09-08 02:18:14 +08:00
const { disableActions } = this.state;
2017-05-04 00:36:16 +08:00
const itemClassName = {};
2017-04-29 02:42:32 +08:00
2017-05-06 04:17:38 +08:00
itemClassName[styles.tableItemNew] = item.id === item.filename;
2017-09-08 02:18:14 +08:00
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);
2017-04-29 02:42:32 +08:00
2017-09-08 02:18:14 +08:00
const hideRemove = (item.isCurrent && item.upload.done) || item.filename === DEFAULT_FILENAME;
2017-05-04 04:51:17 +08:00
2017-04-29 02:42:32 +08:00
return (
<tr
2017-05-04 00:36:16 +08:00
key={item.id}
2017-04-29 02:42:32 +08:00
className={cx(itemClassName)}
>
<td className={styles.tableItemIcon}>
<Icon iconName={'file'} />
2017-04-29 02:42:32 +08:00
</td>
<th className={styles.tableItemName}>
<span>{item.filename}</span>
</th>
2017-05-06 04:17:38 +08:00
<td className={styles.tableItemStatus}>
{this.renderPresentationItemStatus(item)}
2017-04-29 02:42:32 +08:00
</td>
<td className={styles.tableItemActions}>
2017-09-08 02:18:14 +08:00
<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>
2017-04-29 02:42:32 +08:00
</tr>
);
}
renderDropzone() {
2017-05-04 00:36:16 +08:00
const {
intl,
fileSizeMin,
fileSizeMax,
fileValidMimeTypes,
} = this.props;
2017-04-29 02:42:32 +08:00
2017-05-06 04:17:38 +08:00
const { disableActions } = this.state;
2017-09-08 02:18:14 +08:00
const hasSomeFileNotUploaded = this.state.presentations.some(_ => !_.upload.done);
2017-05-06 04:17:38 +08:00
if (hasSomeFileNotUploaded || disableActions) return null;
2017-05-04 04:51:17 +08:00
2017-04-29 02:42:32 +08:00
return (
<Dropzone
2017-09-08 02:18:14 +08:00
multiple
2017-04-29 02:42:32 +08:00
className={styles.dropzone}
activeClassName={styles.dropzoneActive}
rejectClassName={styles.dropzoneReject}
2017-05-04 00:36:16 +08:00
accept={fileValidMimeTypes.join()}
minSize={fileSizeMin}
maxSize={fileSizeMax}
disablePreview
2017-04-29 02:42:32 +08:00
onDrop={this.handleFiledrop}
>
<Icon className={styles.dropzoneIcon} iconName={'upload'} />
2017-04-29 02:42:32 +08:00
<p className={styles.dropzoneMessage}>
{intl.formatMessage(intlMessages.dropzoneLabel)}&nbsp;
<span className={styles.dropzoneLink}>
{intl.formatMessage(intlMessages.browseFilesLabel)}
</span>
</p>
</Dropzone>
);
}
render() {
const { intl } = this.props;
const { preventClosing, disableActions } = this.state;
return (
<ModalFullscreen
title={intl.formatMessage(intlMessages.title)}
preventClosing={preventClosing}
confirm={{
callback: this.handleConfirm,
label: intl.formatMessage(intlMessages.confirmLabel),
description: intl.formatMessage(intlMessages.confirmDesc),
disabled: disableActions,
}}
dismiss={{
callback: this.handleDismiss,
label: intl.formatMessage(intlMessages.dismissLabel),
description: intl.formatMessage(intlMessages.dismissDesc),
disabled: disableActions,
}}
>
<p>{intl.formatMessage(intlMessages.message)}</p>
{this.renderPresentationList()}
{this.renderDropzone()}
</ModalFullscreen>
);
}
}
2017-04-29 02:42:32 +08:00
2017-05-06 04:17:38 +08:00
export default injectIntl(PresentationUploader);