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

1264 lines
41 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';
import { defineMessages, injectIntl } from 'react-intl';
import { PresentationUploaderToast } from '/imports/ui/components/presentation/presentation-toast/presentation-uploader-toast/component';
2021-08-30 08:53:06 +08:00
import { TAB } from '/imports/utils/keyCodes';
2021-04-01 01:13:36 +08:00
import deviceInfo from '/imports/utils/deviceInfo';
2022-02-15 04:20:50 +08:00
import Button from '/imports/ui/components/common/button/component';
2022-02-15 22:51:51 +08:00
import Icon from '/imports/ui/components/common/icon/component';
2017-05-04 00:36:16 +08:00
import update from 'immutability-helper';
import logger from '/imports/startup/client/logger';
2020-02-26 03:29:14 +08:00
import { notify } from '/imports/ui/services/notification';
import { toast } from 'react-toastify';
import _ from 'lodash';
2021-09-05 06:36:48 +08:00
import { registerTitleView, unregisterTitleView } from '/imports/utils/dom-utils';
import Styled from './styles';
import Settings from '/imports/ui/services/settings';
2022-12-15 04:03:23 +08:00
import Radio from '/imports/ui/components/common/radio/component';
import { isPresentationEnabled } from '/imports/ui/services/features';
2021-04-01 01:13:36 +08:00
const { isMobile } = deviceInfo;
2017-09-08 02:18:14 +08:00
const propTypes = {
allowDownloadable: PropTypes.bool.isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
fileUploadConstraintsHint: PropTypes.bool.isRequired,
fileSizeMax: PropTypes.number.isRequired,
filePagesMax: PropTypes.number.isRequired,
2017-09-08 02:18:14 +08:00
handleSave: PropTypes.func.isRequired,
dispatchTogglePresentationDownloadable: PropTypes.func.isRequired,
fileValidMimeTypes: PropTypes.arrayOf(PropTypes.shape).isRequired,
2017-09-08 02:18:14 +08:00
presentations: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
filename: PropTypes.string.isRequired,
isCurrent: PropTypes.bool.isRequired,
conversion: PropTypes.shape,
upload: PropTypes.shape,
2017-09-08 02:18:14 +08:00
})).isRequired,
2020-02-26 03:29:14 +08:00
isOpen: PropTypes.bool.isRequired,
handleFiledrop: PropTypes.func.isRequired,
selectedToBeNextCurrent: PropTypes.string,
renderPresentationItemStatus: PropTypes.func.isRequired,
externalUploadData: PropTypes.shape({
presentationUploadExternalDescription: PropTypes.string.isRequired,
presentationUploadExternalUrl: PropTypes.string.isRequired,
}).isRequired,
isPresenter: PropTypes.bool.isRequired,
exportPresentationToChat: PropTypes.func.isRequired,
2017-09-08 02:18:14 +08:00
};
const defaultProps = {
selectedToBeNextCurrent: '',
2017-09-08 02:18:14 +08:00
};
2017-04-29 02:42:32 +08:00
const intlMessages = defineMessages({
2017-12-05 00:10:37 +08:00
current: {
id: 'app.presentationUploder.currentBadge',
},
2017-04-29 02:42:32 +08:00
title: {
id: 'app.presentationUploder.title',
2017-11-02 00:13:18 +08:00
description: 'title of the modal',
2017-04-29 02:42:32 +08:00
},
message: {
id: 'app.presentationUploder.message',
2017-11-02 00:13:18 +08:00
description: 'message warning the types of files accepted',
2017-04-29 02:42:32 +08:00
},
uploadLabel: {
id: 'app.presentationUploder.uploadLabel',
description: 'confirm label when presentations are to be uploaded',
2017-04-29 02:42:32 +08:00
},
confirmLabel: {
id: 'app.presentationUploder.confirmLabel',
description: 'confirm label when no presentations are to be uploaded',
2017-04-29 02:42:32 +08:00
},
confirmDesc: {
id: 'app.presentationUploder.confirmDesc',
2017-11-02 00:13:18 +08:00
description: 'description of the confirm',
2017-04-29 02:42:32 +08:00
},
dismissLabel: {
id: 'app.presentationUploder.dismissLabel',
2017-11-02 00:13:18 +08:00
description: 'used in the button that close modal',
2017-04-29 02:42:32 +08:00
},
dismissDesc: {
id: 'app.presentationUploder.dismissDesc',
2017-11-02 00:13:18 +08:00
description: 'description of the dismiss',
2017-04-29 02:42:32 +08:00
},
dropzoneLabel: {
id: 'app.presentationUploder.dropzoneLabel',
2017-11-02 00:13:18 +08:00
description: 'message warning where drop files for upload',
2017-04-29 02:42:32 +08:00
},
externalUploadTitle: {
id: 'app.presentationUploder.externalUploadTitle',
description: 'title for external upload area',
},
externalUploadLabel: {
id: 'app.presentationUploder.externalUploadLabel',
description: 'message of external upload button',
},
dropzoneImagesLabel: {
id: 'app.presentationUploder.dropzoneImagesLabel',
description: 'message warning where drop images for upload',
},
2017-04-29 02:42:32 +08:00
browseFilesLabel: {
id: 'app.presentationUploder.browseFilesLabel',
2017-11-02 00:13:18 +08:00
description: 'message use on the file browser',
2017-04-29 02:42:32 +08:00
},
browseImagesLabel: {
id: 'app.presentationUploder.browseImagesLabel',
description: 'message use on the image browser',
},
2017-05-06 04:17:38 +08:00
fileToUpload: {
id: 'app.presentationUploder.fileToUpload',
2017-11-02 00:13:18 +08:00
description: 'message used in the file selected for upload',
2017-05-06 04:17:38 +08:00
},
extraHint: {
id: 'app.presentationUploder.extraHint',
description: 'message used to indicate upload file max sizes',
},
rejectedError: {
id: 'app.presentationUploder.rejectedError',
description: 'some files rejected, please check the file mime types',
},
badConnectionError: {
id: 'app.presentationUploder.connectionClosedError',
description: 'message indicating that the connection was closed',
},
2017-09-08 02:18:14 +08:00
uploadProcess: {
id: 'app.presentationUploder.upload.progress',
2017-11-02 00:13:18 +08:00
description: 'message that indicates the percentage of the upload',
2017-09-08 02:18:14 +08:00
},
413: {
id: 'app.presentationUploder.upload.413',
2017-11-02 00:13:18 +08:00
description: 'error that file exceed the size limit',
2017-05-06 04:17:38 +08:00
},
408: {
id: 'app.presentationUploder.upload.408',
description: 'error for token request timeout',
},
404: {
id: 'app.presentationUploder.upload.404',
description: 'error not found',
},
401: {
id: 'app.presentationUploder.upload.401',
description: 'error for failed upload token request.',
},
2017-05-06 04:17:38 +08:00
conversionProcessingSlides: {
id: 'app.presentationUploder.conversion.conversionProcessingSlides',
2017-11-02 00:13:18 +08:00
description: 'indicates how many slides were converted',
2017-05-06 04:17:38 +08:00
},
2020-03-12 21:56:12 +08:00
genericError: {
id: 'app.presentationUploder.genericError',
description: 'generic error while uploading/converting',
},
2017-05-06 04:17:38 +08:00
genericConversionStatus: {
id: 'app.presentationUploder.conversion.genericConversionStatus',
2017-11-02 00:13:18 +08:00
description: 'indicates that file is being converted',
2017-05-06 04:17:38 +08:00
},
TIMEOUT: {
id: 'app.presentationUploder.conversion.timeout',
},
CONVERSION_TIMEOUT: {
id: 'app.presentationUploder.conversion.conversionTimeout',
description: 'warns the user that the presentation timed out in the back-end in specific page of the document',
},
2017-05-06 04:17:38 +08:00
GENERATING_THUMBNAIL: {
id: 'app.presentationUploder.conversion.generatingThumbnail',
description: 'indicatess that it is generating thumbnails',
2017-05-06 04:17:38 +08:00
},
2017-09-27 03:45:33 +08:00
GENERATING_SVGIMAGES: {
id: 'app.presentationUploder.conversion.generatingSvg',
2017-11-02 00:13:18 +08:00
description: 'warns that it is generating svg images',
2017-09-27 03:45:33 +08:00
},
2017-05-06 04:17:38 +08:00
GENERATED_SLIDE: {
id: 'app.presentationUploder.conversion.generatedSlides',
2017-11-02 00:13:18 +08:00
description: 'warns that were slides generated',
2017-05-06 04:17:38 +08:00
},
2017-11-28 20:26:00 +08:00
PAGE_COUNT_EXCEEDED: {
id: 'app.presentationUploder.conversion.pageCountExceeded',
description: 'warns the user that the conversion failed because of the page count',
},
PDF_HAS_BIG_PAGE: {
id: 'app.presentationUploder.conversion.pdfHasBigPage',
description: 'warns the user that the conversion failed because of the pdf page siz that exceeds the allowed limit',
},
2021-04-28 03:45:14 +08:00
OFFICE_DOC_CONVERSION_INVALID: {
id: 'app.presentationUploder.conversion.officeDocConversionInvalid',
description: '',
},
OFFICE_DOC_CONVERSION_FAILED: {
id: 'app.presentationUploder.conversion.officeDocConversionFailed',
description: 'warns the user that the conversion failed because of wrong office file',
},
UNSUPPORTED_DOCUMENT: {
id: 'app.presentationUploder.conversion.unsupportedDocument',
description: 'warns the user that the file extension is not supported',
},
2019-02-21 06:44:44 +08:00
isDownloadable: {
id: 'app.presentationUploder.isDownloadableLabel',
description: 'presentation is available for downloading by all viewers',
2019-02-21 06:44:44 +08:00
},
isNotDownloadable: {
id: 'app.presentationUploder.isNotDownloadableLabel',
description: 'presentation is not available for downloading the viewers',
2019-02-21 06:44:44 +08:00
},
removePresentation: {
id: 'app.presentationUploder.removePresentationLabel',
description: 'select to delete this presentation',
},
setAsCurrentPresentation: {
id: 'app.presentationUploder.setAsCurrentPresentation',
description: 'set this presentation to be the current one',
},
2019-05-15 00:51:13 +08:00
status: {
id: 'app.presentationUploder.tableHeading.status',
description: 'aria label status table heading',
},
options: {
id: 'app.presentationUploder.tableHeading.options',
description: 'aria label for options table heading',
},
filename: {
id: 'app.presentationUploder.tableHeading.filename',
description: 'aria label for file name table heading',
},
uploading: {
id: 'app.presentationUploder.uploading',
description: 'uploading label for toast notification',
},
uploadStatus: {
id: 'app.presentationUploder.uploadStatus',
description: 'upload status for toast notification',
},
completed: {
id: 'app.presentationUploder.completed',
description: 'uploads complete label for toast notification',
},
item: {
id: 'app.presentationUploder.item',
description: 'single item label',
},
itemPlural: {
id: 'app.presentationUploder.itemPlural',
description: 'plural item label',
},
clearErrors: {
id: 'app.presentationUploder.clearErrors',
description: 'button label for clearing upload errors',
},
clearErrorsDesc: {
id: 'app.presentationUploder.clearErrorsDesc',
description: 'aria description for button clearing upload error',
},
2021-09-05 06:36:48 +08:00
uploadViewTitle: {
id: 'app.presentationUploder.uploadViewTitle',
description: 'view name apended to document title',
},
exportHint: {
id: 'app.presentationUploader.exportHint',
description: 'message to indicate the export presentation option',
},
exportToastHeader: {
id: 'app.presentationUploader.exportToastHeader',
description: 'exporting toast header',
},
exportToastHeaderPlural: {
id: 'app.presentationUploader.exportToastHeaderPlural',
description: 'exporting toast header in plural',
},
export: {
id: 'app.presentationUploader.export',
description: 'send presentation to chat',
},
exporting: {
id: 'app.presentationUploader.exporting',
description: 'presentation is being sent to chat',
},
currentLabel: {
id: 'app.presentationUploader.currentPresentationLabel',
description: 'current presentation label',
},
downloadLabel: {
id: 'app.presentation.downloadLabel',
description: 'download label',
},
2022-07-26 05:56:26 +08:00
sending: {
id: 'app.presentationUploader.sending',
description: 'sending label',
},
2022-08-24 21:31:20 +08:00
collecting: {
id: 'app.presentationUploader.collecting',
description: 'collecting label',
},
processing: {
id: 'app.presentationUploader.processing',
description: 'processing label',
},
2022-07-26 05:56:26 +08:00
sent: {
id: 'app.presentationUploader.sent',
description: 'sent label',
},
exportingTimeout: {
id: 'app.presentationUploader.exportingTimeout',
description: 'exporting timeout label',
},
linkAvailable: {
id: 'app.presentationUploader.export.linkAvailable',
description: 'download presentation link available on public chat',
},
2017-04-29 02:42:32 +08:00
});
2022-07-26 05:56:26 +08:00
const EXPORT_STATUSES = {
RUNNING: 'RUNNING',
2022-08-24 21:31:20 +08:00
COLLECTING: 'COLLECTING',
PROCESSING: 'PROCESSING',
2022-07-26 05:56:26 +08:00
TIMEOUT: 'TIMEOUT',
EXPORTED: 'EXPORTED',
};
const handleDismissToast = (id) => toast.dismiss(id);
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 = {
2020-02-26 03:29:14 +08:00
presentations: [],
2017-05-06 04:17:38 +08:00
disableActions: false,
2022-07-26 05:56:26 +08:00
presExporting: new Set(),
2017-04-29 02:42:32 +08:00
};
2020-02-26 03:29:14 +08:00
this.toastId = null;
2020-03-12 21:56:12 +08:00
this.hasError = null;
this.exportToastId = null;
2020-02-26 03:29:14 +08:00
const { handleFiledrop } = this.props;
2020-02-26 03:29:14 +08:00
// handlers
this.handleFiledrop = handleFiledrop;
2017-04-29 02:42:32 +08:00
this.handleConfirm = this.handleConfirm.bind(this);
this.handleDismiss = this.handleDismiss.bind(this);
this.handleRemove = this.handleRemove.bind(this);
2020-02-26 03:29:14 +08:00
this.handleCurrentChange = this.handleCurrentChange.bind(this);
this.handleSendToChat = this.handleSendToChat.bind(this);
2020-02-26 03:29:14 +08:00
// renders
this.renderDropzone = this.renderDropzone.bind(this);
this.renderExternalUpload = this.renderExternalUpload.bind(this);
2020-02-26 03:29:14 +08:00
this.renderPicDropzone = this.renderPicDropzone.bind(this);
this.renderPresentationList = this.renderPresentationList.bind(this);
this.renderPresentationItem = this.renderPresentationItem.bind(this);
2022-07-26 05:56:26 +08:00
this.renderExportToast = this.renderExportToast.bind(this);
this.renderToastExportItem = this.renderToastExportItem.bind(this);
this.renderExportationStatus = this.renderExportationStatus.bind(this);
2020-02-26 03:29:14 +08:00
// utilities
2017-09-27 03:45:33 +08:00
this.deepMergeUpdateFileKey = this.deepMergeUpdateFileKey.bind(this);
2020-02-26 03:29:14 +08:00
this.updateFileKey = this.updateFileKey.bind(this);
2022-07-26 05:56:26 +08:00
this.getPresentationsToShow = this.getPresentationsToShow.bind(this);
}
2020-02-26 03:29:14 +08:00
componentDidUpdate(prevProps) {
2021-09-05 06:36:48 +08:00
const { isOpen, presentations: propPresentations, intl } = this.props;
2020-02-26 03:29:14 +08:00
const { presentations } = this.state;
const { presentations: prevPropPresentations } = prevProps;
let shouldUpdateState = isOpen && !prevProps.isOpen;
const presState = Object.values({
...JSON.parse(JSON.stringify(propPresentations)),
...JSON.parse(JSON.stringify(presentations)),
});
if (propPresentations.length > prevPropPresentations.length) {
shouldUpdateState = true;
const propsDiffs = propPresentations.filter(
(p) => !prevPropPresentations.some(
(presentation) => p.id === presentation.id
|| p.temporaryPresentationId === presentation.temporaryPresentationId,
),
);
propsDiffs.forEach((p) => {
const index = presState.findIndex(
(pres) => pres.temporaryPresentationId === p.temporaryPresentationId || pres.id === p.id,
);
if (index === -1) {
presState.push(p);
}
});
}
2022-08-24 00:42:28 +08:00
const presStateFiltered = presState.filter((presentation) => {
2022-08-12 00:29:48 +08:00
const currentPropPres = propPresentations.find((pres) => pres.id === presentation.id);
2022-08-25 00:34:50 +08:00
const prevPropPres = prevPropPresentations.find((pres) => pres.id === presentation.id);
const hasConversionError = presentation?.conversion?.error;
const finishedConversion = presentation?.conversion?.done
|| currentPropPres?.conversion?.done;
2022-08-24 00:42:28 +08:00
const hasTemporaryId = presentation.id.startsWith(presentation.filename);
2022-08-12 00:29:48 +08:00
2022-08-24 00:42:28 +08:00
if (hasConversionError || (!finishedConversion && hasTemporaryId)) return true;
2022-08-12 00:29:48 +08:00
if (!currentPropPres) return false;
if (presentation?.conversion?.done !== finishedConversion) {
2022-08-25 00:34:50 +08:00
shouldUpdateState = true;
}
const modPresentation = presentation;
2022-08-25 00:34:50 +08:00
if (currentPropPres.isCurrent !== prevPropPres?.isCurrent) {
modPresentation.isCurrent = currentPropPres.isCurrent;
2022-08-25 00:34:50 +08:00
}
modPresentation.conversion = currentPropPres.conversion;
modPresentation.isRemovable = currentPropPres.isRemovable;
2022-08-12 00:29:48 +08:00
2022-08-24 00:42:28 +08:00
return true;
}).filter((presentation) => {
2022-08-24 00:42:28 +08:00
const duplicated = presentations.find(
(pres) => pres.filename === presentation.filename
&& pres.id !== presentation.id,
2022-08-24 00:42:28 +08:00
);
if (duplicated
&& duplicated.id.startsWith(presentation.filename)
&& !presentation.id.startsWith(presentation.filename)
&& presentation?.conversion?.done === duplicated?.conversion?.done) {
return false; // Prioritizing propPresentations (the one with id from back-end)
2022-08-24 00:42:28 +08:00
}
2022-08-12 00:29:48 +08:00
return true;
});
if (shouldUpdateState) {
this.setState({
presentations: _.uniqBy(presStateFiltered, 'id'),
});
}
2021-09-05 06:36:48 +08:00
if (!isOpen && prevProps.isOpen) {
unregisterTitleView();
}
// Updates presentation list when chat modal opens to avoid missing presentations
if (isOpen && !prevProps.isOpen) {
2021-09-05 06:36:48 +08:00
registerTitleView(intl.formatMessage(intlMessages.uploadViewTitle));
const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const modal = document.getElementById('upload-modal');
const firstFocusableElement = modal?.querySelectorAll(focusableElements)[0];
const focusableContent = modal?.querySelectorAll(focusableElements);
const lastFocusableElement = focusableContent[focusableContent.length - 1];
firstFocusableElement.focus();
modal.addEventListener('keydown', (e) => {
const tab = e.key === 'Tab' || e.keyCode === TAB;
if (!tab) return;
if (e.shiftKey) {
if (document.activeElement === firstFocusableElement) {
lastFocusableElement.focus();
e.preventDefault();
}
} else if (document.activeElement === lastFocusableElement) {
firstFocusableElement.focus();
e.preventDefault();
}
});
2021-05-07 21:52:12 +08:00
}
if (presentations.length > 0) {
2021-05-07 22:06:03 +08:00
const selected = propPresentations.filter((p) => p.isCurrent);
if (selected.length > 0) Session.set('selectedToBeNextCurrent', selected[0].id);
2020-02-26 03:29:14 +08:00
}
if (this.exportToastId) {
if (!prevProps.isOpen && isOpen) {
handleDismissToast(this.exportToastId);
}
toast.update(this.exportToastId, {
render: this.renderExportToast(),
});
}
2017-09-27 03:45:33 +08:00
}
componentWillUnmount() {
const id = Session.get('presentationUploaderToastId');
2023-02-15 05:43:35 +08:00
if (id) {
toast.dismiss(id);
Session.set('presentationUploaderToastId', null);
2023-02-15 05:43:35 +08:00
}
Session.set('showUploadPresentationView', false);
}
handleRemove(item, withErr = false) {
if (withErr) {
const { presentations } = this.state;
const { presentations: propPresentations } = this.props;
const filteredPropPresentations = propPresentations.filter(d => d.upload.done && d.conversion?.done);
const ids = new Set(filteredPropPresentations.map((d) => d.id));
const filteredPresentations = presentations.filter((d) => {
d.isCurrent = false;
return !ids.has(d.id) && !(d.upload.error || d.conversion.error) && !(d.upload.done && d.conversion.done)});
const merged = [
...filteredPresentations,
...filteredPropPresentations,
];
let hasUploading
merged.forEach(d => {
if (!d.upload?.done || !d.conversion?.done) {
hasUploading = true;
}})
this.hasError = false;
if (hasUploading) {
return this.setState({
presentations: merged,
});
} else {
return this.setState({
presentations: merged,
disableActions: false,
});
}
}
2020-02-26 03:29:14 +08:00
const { presentations } = this.state;
const toRemoveIndex = presentations.indexOf(item);
return this.setState({
presentations: update(presentations, {
$splice: [[toRemoveIndex, 1]],
}),
}, () => {
const { presentations: updatedPresentations, oldCurrentId } = this.state;
const commands = {};
const currentIndex = updatedPresentations.findIndex((p) => p.isCurrent);
const actualCurrentIndex = updatedPresentations.findIndex((p) => p.id === oldCurrentId);
2020-02-26 03:29:14 +08:00
if (currentIndex === -1 && updatedPresentations.length > 0) {
const newCurrentIndex = actualCurrentIndex === -1 ? 0 : actualCurrentIndex;
commands[newCurrentIndex] = {
$apply: (presentation) => {
const p = presentation;
p.isCurrent = true;
return p;
},
};
}
const updatedCurrent = update(updatedPresentations, commands);
this.setState({ presentations: updatedCurrent });
});
2020-02-26 03:29:14 +08:00
}
handleCurrentChange(id) {
const { presentations, disableActions } = this.state;
2020-02-26 03:29:14 +08:00
if (disableActions) return;
2020-02-26 03:29:14 +08:00
const currentIndex = presentations.findIndex((p) => p.isCurrent);
const newCurrentIndex = presentations.findIndex((p) => p.id === id);
2020-02-26 03:29:14 +08:00
const commands = {};
// we can end up without a current presentation
if (currentIndex !== -1) {
commands[currentIndex] = {
$apply: (presentation) => {
const p = presentation;
p.isCurrent = false;
return p;
},
};
}
commands[newCurrentIndex] = {
2020-02-26 03:29:14 +08:00
$apply: (presentation) => {
const p = presentation;
p.isCurrent = true;
2020-02-26 03:29:14 +08:00
return p;
},
};
const presentationsUpdated = update(presentations, commands);
this.setState({ presentations: presentationsUpdated });
2020-02-26 03:29:14 +08:00
}
handleConfirm() {
2020-02-26 03:29:14 +08:00
const {
handleSave,
selectedToBeNextCurrent,
presentations: propPresentations,
dispatchTogglePresentationDownloadable,
2020-02-26 03:29:14 +08:00
} = this.props;
const { disableActions, presentations } = this.state;
const presentationsToSave = presentations;
if (!isPresentationEnabled()) {
this.setState(
{ presentations: [] },
Session.set('showUploadPresentationView', false),
);
return null;
}
2020-02-26 03:29:14 +08:00
this.setState({ disableActions: true });
presentations.forEach((item) => {
if (item.upload.done) {
const didDownloadableStateChange = propPresentations.some(
(p) => p.id === item.id && p.isDownloadable !== item.isDownloadable,
);
if (didDownloadableStateChange) {
dispatchTogglePresentationDownloadable(item, item.isDownloadable);
}
}
});
2020-02-26 03:29:14 +08:00
if (!disableActions) {
Session.set('showUploadPresentationView', false);
2020-02-26 03:29:14 +08:00
return handleSave(presentationsToSave)
.then(() => {
const hasError = presentations.some((p) => p.upload.error || p.conversion.error);
2020-02-26 03:29:14 +08:00
if (!hasError) {
this.setState({
disableActions: false,
});
return;
}
// if there's error we don't want to close the modal
this.setState({
disableActions: true,
// preventClosing: true,
}, () => {
// if the selected current has error we revert back to the old one
const newCurrent = presentations.find((p) => p.isCurrent);
2020-02-26 03:29:14 +08:00
if (newCurrent.upload.error || newCurrent.conversion.error) {
this.handleCurrentChange(selectedToBeNextCurrent);
2020-02-26 03:29:14 +08:00
}
});
})
.catch((error) => {
logger.error({
logCode: 'presentationuploader_component_save_error',
extraInfo: { error },
}, 'Presentation uploader catch error on confirm');
});
}
Session.set('showUploadPresentationView', false);
2020-02-26 03:29:14 +08:00
return null;
}
handleDismiss() {
const { presentations } = this.state;
const { presentations: propPresentations } = this.props;
const ids = new Set(propPresentations.map((d) => d.id));
const filteredPresentations = presentations.filter((d) => {
return !ids.has(d.id) && (d.upload.done || d.upload.progress !== 0)});
const isThereStateCurrentPres = filteredPresentations.some(p => p.isCurrent);
const merged = [
...filteredPresentations,
...propPresentations.filter(p => {
if (isThereStateCurrentPres) {
p.isCurrent = false;
}
return true;
}),
];
this.setState(
{ presentations: merged },
Session.set('showUploadPresentationView', false),
);
2020-02-26 03:29:14 +08:00
}
handleSendToChat(item) {
const {
exportPresentationToChat,
intl,
} = this.props;
2017-09-27 03:45:33 +08:00
const observer = (exportation, stopped) => {
this.deepMergeUpdateFileKey(item.id, 'exportation', exportation);
if (exportation.status === EXPORT_STATUSES.EXPORTED && stopped) {
notify(intl.formatMessage(intlMessages.linkAvailable, { 0: item.filename }), 'success');
}
if ([
EXPORT_STATUSES.RUNNING,
2022-08-24 21:31:20 +08:00
EXPORT_STATUSES.COLLECTING,
EXPORT_STATUSES.PROCESSING,
].includes(exportation.status)) {
2022-07-26 05:56:26 +08:00
this.setState((prevState) => {
prevState.presExporting.add(item.id);
return {
presExporting: prevState.presExporting,
};
}, () => {
if (this.exportToastId) {
toast.update(this.exportToastId, {
render: this.renderExportToast(),
});
} else {
this.exportToastId = toast.info(this.renderExportToast(), {
hideProgressBar: true,
autoClose: false,
newestOnTop: true,
closeOnClick: true,
onClose: () => {
this.exportToastId = null;
const presToShow = this.getPresentationsToShow();
const isAnyRunning = presToShow.some(
(p) => p.exportation.status === EXPORT_STATUSES.RUNNING
|| p.exportation.status === EXPORT_STATUSES.COLLECTING
|| p.exportation.status === EXPORT_STATUSES.PROCESSING,
2022-07-26 05:56:26 +08:00
);
if (!isAnyRunning) {
this.setState({ presExporting: new Set() });
}
},
});
}
});
}
2017-04-29 02:42:32 +08:00
};
exportPresentationToChat(item.id, observer);
Session.set('showUploadPresentationView', false);
}
2017-09-27 03:45:33 +08:00
getPresentationsToShow() {
const { presentations, presExporting } = this.state;
return Array.from(presExporting)
.map((id) => presentations.find((p) => p.id === id))
.filter((p) => p);
}
deepMergeUpdateFileKey(id, key, value) {
const applyValue = (toUpdate) => update(toUpdate, { $merge: value });
this.updateFileKey(id, key, applyValue, '$apply');
}
updateFileKey(id, key, value, operation = '$set') {
this.setState(({ presentations }) => {
const fileIndex = presentations.findIndex((f) => f.id === id);
return fileIndex === -1 ? false : {
presentations: update(presentations, {
[fileIndex]: {
$apply: (file) => update(file, {
[key]: {
[operation]: value,
},
}),
},
}),
};
});
}
renderExtraHint() {
const {
intl,
fileSizeMax,
filePagesMax,
} = this.props;
const options = {
0: fileSizeMax / 1000000,
1: filePagesMax,
};
return (
<Styled.ExtraHint>
{intl.formatMessage(intlMessages.extraHint, options)}
</Styled.ExtraHint>
);
}
2017-04-29 02:42:32 +08:00
renderPresentationList() {
const { presentations } = this.state;
const { intl, allowDownloadable } = this.props;
2017-04-29 02:42:32 +08:00
let presentationsSorted = presentations;
try {
presentationsSorted = presentations
.sort((a, b) => a.uploadTimestamp - b.uploadTimestamp)
.sort((a, b) => a.filename.localeCompare(b.filename))
.sort((a, b) => b.upload.progress - a.upload.progress)
.sort((a, b) => b.conversion.done - a.conversion.done)
.sort((a, b) => {
const aUploadNotTriggeredYet = !a.upload.done && a.upload.progress === 0;
const bUploadNotTriggeredYet = !b.upload.done && b.upload.progress === 0;
return bUploadNotTriggeredYet - aUploadNotTriggeredYet;
});
} catch (error) {
logger.error({
logCode: 'presentationuploader_component_render_error',
extraInfo: { error },
}, 'Presentation uploader catch error on render presentation list');
}
2017-04-29 02:42:32 +08:00
return (
<Styled.FileList>
<Styled.Table>
2019-05-15 03:45:12 +08:00
<thead>
<tr>
2022-07-22 04:20:03 +08:00
<Styled.VisuallyHidden>
{intl.formatMessage(intlMessages.setAsCurrentPresentation)}
</Styled.VisuallyHidden>
<Styled.VisuallyHidden colSpan={2}>
{intl.formatMessage(intlMessages.filename)}
</Styled.VisuallyHidden>
<Styled.VisuallyHidden>
{intl.formatMessage(intlMessages.status)}
</Styled.VisuallyHidden>
<Styled.VisuallyHidden>
{intl.formatMessage(intlMessages.options)}
</Styled.VisuallyHidden>
2019-05-15 03:45:12 +08:00
</tr>
2022-07-22 04:20:03 +08:00
<Styled.Head>
<th colSpan={4}>{intl.formatMessage(intlMessages.currentLabel)}</th>
{
allowDownloadable ? <th>{intl.formatMessage(intlMessages.downloadLabel)}</th> : null
}
2022-07-22 04:20:03 +08:00
</Styled.Head>
2019-05-15 03:45:12 +08:00
</thead>
2017-05-04 00:36:16 +08:00
<tbody>
2022-08-24 00:42:28 +08:00
{_.uniqBy(presentationsSorted, 'id').map((item) => this.renderPresentationItem(item))}
2017-05-04 00:36:16 +08:00
</tbody>
</Styled.Table>
</Styled.FileList>
2017-04-29 02:42:32 +08:00
);
}
renderExportToast() {
const { intl } = this.props;
2022-07-26 05:56:26 +08:00
const { presExporting } = this.state;
2022-07-26 05:56:26 +08:00
const presToShow = this.getPresentationsToShow();
2022-07-26 05:56:26 +08:00
const isAllExported = presToShow.every(
2022-08-24 21:31:20 +08:00
(p) => p.exportation.status === EXPORT_STATUSES.EXPORTED,
2022-07-26 05:56:26 +08:00
);
const shouldDismiss = isAllExported && this.exportToastId;
if (shouldDismiss) {
handleDismissToast(this.exportToastId);
2022-07-26 05:56:26 +08:00
if (presExporting.size) {
this.setState({ presExporting: new Set() });
}
return null;
}
2022-07-26 05:56:26 +08:00
const presToShowSorted = [
...presToShow.filter((p) => p.exportation.status === EXPORT_STATUSES.RUNNING),
2022-08-24 21:31:20 +08:00
...presToShow.filter((p) => p.exportation.status === EXPORT_STATUSES.COLLECTING),
...presToShow.filter((p) => p.exportation.status === EXPORT_STATUSES.PROCESSING),
2022-07-26 05:56:26 +08:00
...presToShow.filter((p) => p.exportation.status === EXPORT_STATUSES.TIMEOUT),
...presToShow.filter((p) => p.exportation.status === EXPORT_STATUSES.EXPORTED),
];
const headerLabelId = presToShowSorted.length === 1
? 'exportToastHeader'
: 'exportToastHeaderPlural';
return (
2022-11-08 22:32:55 +08:00
<Styled.ToastWrapper data-test="downloadPresentationToast">
<Styled.UploadToastHeader>
<Styled.UploadIcon iconName="download" />
<Styled.UploadToastTitle>
2022-07-26 05:56:26 +08:00
{intl.formatMessage(intlMessages[headerLabelId], { 0: presToShowSorted.length })}
</Styled.UploadToastTitle>
</Styled.UploadToastHeader>
<Styled.InnerToast>
<div>
<div>
2022-07-26 05:56:26 +08:00
{presToShowSorted.map((item) => this.renderToastExportItem(item))}
</div>
</div>
</Styled.InnerToast>
</Styled.ToastWrapper>
);
}
renderToastExportItem(item) {
2022-07-26 05:56:26 +08:00
const { status } = item.exportation;
2022-08-24 21:31:20 +08:00
const loading = (status === EXPORT_STATUSES.RUNNING
|| status === EXPORT_STATUSES.COLLECTING
|| status === EXPORT_STATUSES.PROCESSING);
2022-07-26 05:56:26 +08:00
const done = status === EXPORT_STATUSES.EXPORTED;
let icon;
switch (status) {
case EXPORT_STATUSES.RUNNING:
2022-08-24 21:31:20 +08:00
icon = 'blank';
break;
case EXPORT_STATUSES.COLLECTING:
icon = 'blank';
break;
case EXPORT_STATUSES.PROCESSING:
icon = 'blank';
2022-07-26 05:56:26 +08:00
break;
case EXPORT_STATUSES.EXPORTED:
2022-08-24 21:31:20 +08:00
icon = 'check';
2022-07-26 05:56:26 +08:00
break;
case EXPORT_STATUSES.TIMEOUT:
2022-08-24 21:31:20 +08:00
icon = 'warning';
2022-07-26 05:56:26 +08:00
break;
default:
break;
}
return (
<Styled.UploadRow
key={item.id || item.temporaryPresentationId}
>
2022-07-26 05:56:26 +08:00
<Styled.FileLine>
<span>
<Icon iconName="file" />
</span>
<Styled.ToastFileName>
<span>{item.filename}</span>
</Styled.ToastFileName>
<Styled.StatusIcon>
<Styled.ToastItemIcon
loading={loading}
done={done}
iconName={icon}
color="#0F70D7"
/>
</Styled.StatusIcon>
</Styled.FileLine>
<Styled.StatusInfo>
<Styled.StatusInfoSpan>
{this.renderExportationStatus(item)}
</Styled.StatusInfoSpan>
</Styled.StatusInfo>
</Styled.UploadRow>
);
}
2022-07-26 05:56:26 +08:00
renderExportationStatus(item) {
const { intl } = this.props;
switch (item.exportation.status) {
case EXPORT_STATUSES.RUNNING:
return intl.formatMessage(intlMessages.sending);
2022-08-24 21:31:20 +08:00
case EXPORT_STATUSES.COLLECTING:
return intl.formatMessage(intlMessages.collecting,
{ 0: item.exportation.pageNumber, 1: item.exportation.totalPages });
case EXPORT_STATUSES.PROCESSING:
return intl.formatMessage(intlMessages.processing,
{ 0: item.exportation.pageNumber, 1: item.exportation.totalPages });
2022-07-26 05:56:26 +08:00
case EXPORT_STATUSES.TIMEOUT:
return intl.formatMessage(intlMessages.exportingTimeout);
case EXPORT_STATUSES.EXPORTED:
return intl.formatMessage(intlMessages.sent);
default:
return '';
}
}
renderDownloadableWithAnnotationsHint() {
const {
intl,
allowDownloadable,
} = this.props;
return allowDownloadable ? (
<Styled.ExportHint>
{intl.formatMessage(intlMessages.exportHint)}
</Styled.ExportHint>
)
: null;
}
2017-04-29 02:42:32 +08:00
renderPresentationItem(item) {
2021-09-01 19:48:46 +08:00
const { disableActions } = this.state;
2020-02-26 03:29:14 +08:00
const {
intl,
selectedToBeNextCurrent,
allowDownloadable,
renderPresentationItemStatus,
2020-02-26 03:29:14 +08:00
} = this.props;
2017-05-04 00:36:16 +08:00
const isActualCurrent = selectedToBeNextCurrent
? item.id === selectedToBeNextCurrent
: item.isCurrent;
2017-11-28 20:26:00 +08:00
const isUploading = !item.upload.done && item.upload.progress > 0;
const isConverting = !item.conversion.done && item.upload.done;
const hasError = item.conversion.error || item.upload.error;
const isProcessing = (isUploading || isConverting) && !hasError;
2021-09-01 19:48:46 +08:00
if (hasError) {
2020-03-12 21:56:12 +08:00
this.hasError = true;
2020-02-26 03:29:14 +08:00
}
const { animations } = Settings.application;
2017-05-04 04:51:17 +08:00
2022-07-26 05:56:26 +08:00
const { isRemovable, exportation: { status } } = item;
2022-07-26 05:56:26 +08:00
const isExporting = status === 'RUNNING';
2020-02-26 03:29:14 +08:00
const shouldDisableExportButton = isExporting
|| !item.conversion.done
|| hasError
|| disableActions;
const formattedDownloadLabel = isExporting
? intl.formatMessage(intlMessages.exporting)
: intl.formatMessage(intlMessages.export);
2020-02-26 03:29:14 +08:00
const formattedDownloadAriaLabel = `${formattedDownloadLabel} ${item.filename}`;
2017-04-29 02:42:32 +08:00
return (
<Styled.PresentationItem
2017-05-04 00:36:16 +08:00
key={item.id}
isNew={item.id.indexOf(item.filename) !== -1}
uploading={isUploading}
converting={isConverting}
error={hasError}
animated={isProcessing}
animations={animations}
2017-04-29 02:42:32 +08:00
>
<Styled.SetCurrentAction>
2022-12-15 04:03:23 +08:00
<Radio
animations={animations}
ariaLabel={`${intl.formatMessage(intlMessages.setAsCurrentPresentation)} ${item.filename}`}
checked={item.isCurrent}
keyValue={item.id}
onChange={() => this.handleCurrentChange(item.id)}
disabled={disableActions || hasError}
/>
</Styled.SetCurrentAction>
<Styled.TableItemName colSpan={!isActualCurrent ? 2 : 0}>
<span>{item.filename}</span>
</Styled.TableItemName>
2017-12-05 00:10:37 +08:00
{
2018-12-06 01:42:31 +08:00
isActualCurrent
? (
<Styled.TableItemCurrent>
<Styled.CurrentLabel>
{intl.formatMessage(intlMessages.current)}
</Styled.CurrentLabel>
</Styled.TableItemCurrent>
2018-12-06 01:42:31 +08:00
)
: null
2017-12-05 00:10:37 +08:00
}
<Styled.TableItemStatus colSpan={hasError ? 2 : 0}>
{renderPresentationItemStatus(item, intl)}
</Styled.TableItemStatus>
{hasError ? null : (
<Styled.TableItemActions notDownloadable={!allowDownloadable}>
{allowDownloadable ? (
<Styled.DownloadButton
disabled={shouldDisableExportButton}
label={intl.formatMessage(intlMessages.export)}
data-test="exportPresentationToPublicChat"
aria-label={formattedDownloadAriaLabel}
size="sm"
color="primary"
onClick={() => this.handleSendToChat(item)}
animations={animations}
/>
) : null}
{isRemovable ? (
2022-02-03 23:03:26 +08:00
<Styled.RemoveButton
disabled={disableActions}
label={intl.formatMessage(intlMessages.removePresentation)}
data-test="removePresentation"
aria-label={`${intl.formatMessage(intlMessages.removePresentation)} ${item.filename}`}
size="sm"
icon="delete"
hideLabel
onClick={() => this.handleRemove(item)}
animations={animations}
/>
) : null}
</Styled.TableItemActions>
2017-11-28 20:26:00 +08:00
)}
</Styled.PresentationItem>
2017-04-29 02:42:32 +08:00
);
}
2020-02-26 03:29:14 +08:00
renderDropzone() {
const {
intl,
2020-02-26 03:29:14 +08:00
fileValidMimeTypes,
} = this.props;
const { disableActions } = this.state;
2020-03-12 21:56:12 +08:00
if (disableActions && !this.hasError) return null;
2020-03-12 21:56:12 +08:00
return this.hasError ? (
2020-02-26 03:29:14 +08:00
<div>
<Button
color="danger"
onClick={() => this.handleRemove(null, true)}
label={intl.formatMessage(intlMessages.clearErrors)}
aria-describedby="clearErrorDesc"
/>
<div id="clearErrorDesc" style={{ display: 'none' }}>
{intl.formatMessage(intlMessages.clearErrorsDesc)}
</div>
</div>
) : (
// Until the Dropzone package has fixed the mime type hover validation, the rejectClassName
// prop is being remove to prevent the error styles from being applied to valid file types.
// Error handling is being done in the onDrop prop.
<Styled.UploaderDropzone
multiple
activeClassName="dropzoneActive"
accept={fileValidMimeTypes.map((fileValid) => fileValid.extension)}
2020-02-26 03:29:14 +08:00
disablepreview="true"
2023-02-24 00:52:51 +08:00
onDrop={(files, files2) => this.handleFiledrop(files, files2, this, intl, intlMessages)}
>
<Styled.DropzoneIcon iconName="upload" />
<Styled.DropzoneMessage>
2020-02-26 03:29:14 +08:00
{intl.formatMessage(intlMessages.dropzoneLabel)}
&nbsp;
<Styled.DropzoneLink>
2020-02-26 03:29:14 +08:00
{intl.formatMessage(intlMessages.browseFilesLabel)}
</Styled.DropzoneLink>
</Styled.DropzoneMessage>
</Styled.UploaderDropzone>
);
}
renderExternalUpload() {
const { externalUploadData, intl } = this.props;
const {
presentationUploadExternalDescription, presentationUploadExternalUrl,
} = externalUploadData;
2022-09-12 22:04:13 +08:00
if (!presentationUploadExternalDescription || !presentationUploadExternalUrl) return null;
return (
<Styled.ExternalUpload>
<div>
<Styled.ExternalUploadTitle>
{intl.formatMessage(intlMessages.externalUploadTitle)}
</Styled.ExternalUploadTitle>
2022-09-12 22:04:13 +08:00
<p>{presentationUploadExternalDescription}</p>
</div>
<Styled.ExternalUploadButton
color="default"
2022-09-12 22:04:13 +08:00
onClick={() => window.open(`${presentationUploadExternalUrl}`)}
label={intl.formatMessage(intlMessages.externalUploadLabel)}
aria-describedby={intl.formatMessage(intlMessages.externalUploadLabel)}
/>
</Styled.ExternalUpload>
);
}
2020-02-26 03:29:14 +08:00
renderPicDropzone() {
2017-05-04 00:36:16 +08:00
const {
intl,
} = this.props;
2017-04-29 02:42:32 +08:00
2017-05-06 04:17:38 +08:00
const { disableActions } = this.state;
2020-03-12 21:56:12 +08:00
if (disableActions && !this.hasError) return null;
2017-05-04 04:51:17 +08:00
2020-03-12 21:56:12 +08:00
return this.hasError ? (
2020-02-26 03:29:14 +08:00
<div>
<Button
color="danger"
onClick={() => this.handleRemove(null, true)}
label={intl.formatMessage(intlMessages.clearErrors)}
aria-describedby="clearErrorDesc"
/>
<div id="clearErrorDesc" style={{ display: 'none' }}>
{intl.formatMessage(intlMessages.clearErrorsDesc)}
</div>
</div>
) : (
<Styled.UploaderDropzone
2017-09-08 02:18:14 +08:00
multiple
2020-02-26 03:29:14 +08:00
accept="image/*"
2018-12-06 01:42:31 +08:00
disablepreview="true"
2020-07-18 03:13:47 +08:00
data-test="fileUploadDropZone"
2023-02-24 00:52:51 +08:00
onDrop={(files, files2) => this.handleFiledrop(files, files2, this, intl, intlMessages)}
2017-04-29 02:42:32 +08:00
>
<Styled.DropzoneIcon iconName="upload" />
<Styled.DropzoneMessage>
2020-02-26 03:29:14 +08:00
{intl.formatMessage(intlMessages.dropzoneImagesLabel)}
&nbsp;
<Styled.DropzoneLink>
2020-02-26 03:29:14 +08:00
{intl.formatMessage(intlMessages.browseImagesLabel)}
</Styled.DropzoneLink>
</Styled.DropzoneMessage>
</Styled.UploaderDropzone>
2017-04-29 02:42:32 +08:00
);
}
render() {
const {
isOpen,
isPresenter,
intl,
fileUploadConstraintsHint,
2020-02-26 03:29:14 +08:00
} = this.props;
if (!isPresenter) return null;
2020-02-26 03:29:14 +08:00
const { presentations, disableActions } = this.state;
let hasNewUpload = false;
2022-04-08 01:06:51 +08:00
presentations.forEach((item) => {
2020-02-26 03:29:14 +08:00
if (item.id.indexOf(item.filename) !== -1 && item.upload.progress === 0) hasNewUpload = true;
});
return (
<>
<PresentationUploaderToast intl={intl} />
{isOpen
? (
<Styled.UploaderModal id="upload-modal">
<Styled.ModalInner>
<Styled.ModalHeader>
<Styled.Title>{intl.formatMessage(intlMessages.title)}</Styled.Title>
<Styled.ActionWrapper>
<Styled.DismissButton
color="secondary"
onClick={this.handleDismiss}
label={intl.formatMessage(intlMessages.dismissLabel)}
aria-describedby={intl.formatMessage(intlMessages.dismissDesc)}
/>
<Styled.ConfirmButton
data-test="confirmManagePresentation"
color="primary"
onClick={() => this.handleConfirm()}
disabled={disableActions}
label={hasNewUpload
? intl.formatMessage(intlMessages.uploadLabel)
: intl.formatMessage(intlMessages.confirmLabel)}
/>
</Styled.ActionWrapper>
</Styled.ModalHeader>
<Styled.ModalHint>
{`${intl.formatMessage(intlMessages.message)}`}
{fileUploadConstraintsHint ? this.renderExtraHint() : null}
</Styled.ModalHint>
{this.renderPresentationList()}
{this.renderDownloadableWithAnnotationsHint()}
{isMobile ? this.renderPicDropzone() : null}
{this.renderDropzone()}
{this.renderExternalUpload()}
</Styled.ModalInner>
</Styled.UploaderModal>
)
: null}
</>
);
}
}
2017-04-29 02:42:32 +08:00
2017-09-23 04:45:31 +08:00
PresentationUploader.propTypes = propTypes;
PresentationUploader.defaultProps = defaultProps;
export default injectIntl(PresentationUploader);