From c5071093d9655005594cbf4136799f3ca90e503e Mon Sep 17 00:00:00 2001 From: Joao Siebel <joaos_desenv@imdt.com.br> Date: Tue, 20 Apr 2021 14:21:12 -0300 Subject: [PATCH] Improve feedback to user after a failed file upload --- .../api/presentations/server/eventHandlers.js | 1 + .../handlers/presentationConversionUpdate.js | 14 +- .../presentation-uploader/component.jsx | 334 +++++++++--------- .../presentation-uploader/container.jsx | 6 +- .../presentation-uploader/service.js | 27 +- .../presentation-uploader/styles.scss | 2 +- .../private/config/settings.yml | 2 - bigbluebutton-html5/public/locales/en.json | 6 +- 8 files changed, 206 insertions(+), 186 deletions(-) diff --git a/bigbluebutton-html5/imports/api/presentations/server/eventHandlers.js b/bigbluebutton-html5/imports/api/presentations/server/eventHandlers.js index ebc83d57e4..75a842e820 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/presentations/server/eventHandlers.js @@ -9,6 +9,7 @@ RedisPubSub.on('PdfConversionInvalidErrorEvtMsg', handlePresentationConversionUp RedisPubSub.on('PresentationPageGeneratedEvtMsg', handlePresentationConversionUpdate); RedisPubSub.on('PresentationPageCountErrorEvtMsg', handlePresentationConversionUpdate); RedisPubSub.on('PresentationConversionUpdateEvtMsg', handlePresentationConversionUpdate); +RedisPubSub.on('PresentationUploadedFileTooLargeErrorEvtMsg', handlePresentationConversionUpdate); RedisPubSub.on('PresentationConversionCompletedEvtMsg', handlePresentationAdded); RedisPubSub.on('RemovePresentationEvtMsg', handlePresentationRemove); RedisPubSub.on('SetCurrentPresentationEvtMsg', handlePresentationCurrentSet); diff --git a/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationConversionUpdate.js b/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationConversionUpdate.js index 96bd6b5761..5f6421ff05 100755 --- a/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationConversionUpdate.js +++ b/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationConversionUpdate.js @@ -11,6 +11,7 @@ const PAGE_COUNT_FAILED_KEY = 'PAGE_COUNT_FAILED'; const PAGE_COUNT_EXCEEDED_KEY = 'PAGE_COUNT_EXCEEDED'; const PDF_HAS_BIG_PAGE_KEY = 'PDF_HAS_BIG_PAGE'; const GENERATED_SLIDE_KEY = 'GENERATED_SLIDE'; +const FILE_TOO_LARGE_KEY = 'FILE_TOO_LARGE'; // const GENERATING_THUMBNAIL_KEY = 'GENERATING_THUMBNAIL'; // const GENERATED_THUMBNAIL_KEY = 'GENERATED_THUMBNAIL'; // const GENERATING_TEXTFILES_KEY = 'GENERATING_TEXTFILES'; @@ -27,7 +28,7 @@ export default function handlePresentationConversionUpdate({ body }, meetingId) } = body; check(meetingId, String); - check(presentationId, String); + check(presentationId, Match.Maybe(String)); check(podId, String); check(status, String); @@ -43,17 +44,20 @@ export default function handlePresentationConversionUpdate({ body }, meetingId) statusModifier.name = presentationName; break; + case FILE_TOO_LARGE_KEY: + statusModifier['conversion.maxFileSize'] = body.maxFileSize; case UNSUPPORTED_DOCUMENT_KEY: case OFFICE_DOC_CONVERSION_FAILED_KEY: case OFFICE_DOC_CONVERSION_INVALID_KEY: case PAGE_COUNT_FAILED_KEY: case PAGE_COUNT_EXCEEDED_KEY: + statusModifier['conversion.maxNumberPages'] = body.maxNumberPages; case PDF_HAS_BIG_PAGE_KEY: - statusModifier.id = presentationId; - statusModifier.name = presentationName; + statusModifier.id = presentationId ?? body.presentationToken; + statusModifier.name = presentationName ?? body.presentationName; statusModifier['conversion.error'] = true; + statusModifier['conversion.bigPageSize'] = body.bigPageSize; break; - case GENERATED_SLIDE_KEY: statusModifier['conversion.pagesCompleted'] = body.pagesCompleted; statusModifier['conversion.numPages'] = body.numberOfPages; @@ -66,7 +70,7 @@ export default function handlePresentationConversionUpdate({ body }, meetingId) const selector = { meetingId, podId, - id: presentationId, + id: presentationId ?? body.presentationToken, }; const modifier = { 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 b9a3ab428d..c9ffda0d57 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx @@ -19,8 +19,6 @@ const { isMobile } = deviceInfo; const propTypes = { intl: PropTypes.object.isRequired, defaultFileName: PropTypes.string.isRequired, - fileSizeMin: PropTypes.number.isRequired, - fileSizeMax: PropTypes.number.isRequired, handleSave: PropTypes.func.isRequired, dispatchTogglePresentationDownloadable: PropTypes.func.isRequired, fileValidMimeTypes: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -242,7 +240,7 @@ class PresentationUploader extends Component { } componentDidUpdate(prevProps) { - const { selectedToBeNextCurrent, isOpen, presentations: propPresentations } = this.props; + const { isOpen, presentations: propPresentations } = this.props; const { presentations } = this.state; // cleared local presetation state errors and set to presentations available on the server @@ -350,114 +348,79 @@ class PresentationUploader extends Component { } } - renderToastItem(item) { - 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; - - const { - intl, selectedToBeNextCurrent, - } = this.props; - - const itemClassName = { - [styles.done]: !isProcessing && !hasError, - [styles.err]: hasError, - [styles.loading]: isProcessing, - }; + handleRemove(item, withErr = false) { + if (withErr) { + const { presentations } = this.props; + this.hasError = false; + return this.setState({ + presentations, + disableActions: false, + }); + } - const statusInfoStyle = { - [styles.textErr]: hasError, - [styles.textInfo]: !hasError, - }; + 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 currentIndex = updatedPresentations.findIndex((p) => p.isCurrent); + const actualCurrentIndex = updatedPresentations.findIndex((p) => p.id === oldCurrentId); - let icon = isProcessing ? 'blank' : 'check'; - if (hasError) icon = 'circle_close'; + if (currentIndex === -1 && updatedPresentations.length > 0) { + const commands = {}; + const newCurrentIndex = actualCurrentIndex === -1 ? 0 : actualCurrentIndex; + commands[newCurrentIndex] = { + $apply: (presentation) => { + const p = presentation; + p.isCurrent = true; + return p; + }, + }; - return ( - <div - key={item.id} - className={styles.uploadRow} - onClick={() => { - if (hasError || isProcessing) Session.set('showUploadPresentationView', true); - }} - > - <div className={styles.fileLine}> - <span className={styles.fileIcon}> - <Icon iconName="file" /> - </span> - <span className={styles.toastFileName}> - <span>{item.filename}</span> - </span> - <span className={styles.statusIcon}> - <Icon iconName={icon} className={cx(itemClassName)} /> - </span> - </div> - <div className={styles.statusInfo}> - <span className={cx(statusInfoStyle)}>{this.renderPresentationItemStatus(item)}</span> - </div> - </div> - ); + const updatedCurrent = update(updatedPresentations, commands); + this.setState({ presentations: updatedCurrent }); + } + }); } - handleToggleDownloadable(item) { - const { dispatchTogglePresentationDownloadable } = this.props; - const { presentations } = this.state; + handleCurrentChange(id) { + const { presentations, disableActions } = this.state; - const oldDownloadableState = item.isDownloadable; + if (disableActions) return; - const outOfDatePresentationIndex = presentations.findIndex(p => p.id === item.id); + const currentIndex = presentations.findIndex((p) => p.isCurrent); + const newCurrentIndex = presentations.findIndex((p) => p.id === id); const commands = {}; - commands[outOfDatePresentationIndex] = { + + // 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] = { $apply: (presentation) => { const p = presentation; - p.isDownloadable = !oldDownloadableState; + p.isCurrent = true; return p; }, }; - const presentationsUpdated = update(presentations, commands); - - this.setState({ - presentations: presentationsUpdated, - }); - // If the presentation has not be uploaded yet, adjusting the state suffices - // otherwise set previously uploaded presentation to [not] be downloadable - if (item.upload.done) { - dispatchTogglePresentationDownloadable(item, !oldDownloadableState); - } - } - - 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, - }, - }), - }, - }), - }; - }); + const presentationsUpdated = update(presentations, commands); + this.setState({ presentations: presentationsUpdated }); } - handleDismiss() { - const { presentations } = this.state; - const { presentations: propPresentations } = this.props; - const ids = new Set(propPresentations.map(d => d.ID)); - const merged = [ - ...propPresentations, - ...presentations.filter(d => !ids.has(d.ID)), - ]; - this.setState( - { presentations: merged }, - Session.set('showUploadPresentationView', false), - ); + deepMergeUpdateFileKey(id, key, value) { + const applyValue = (toUpdate) => update(toUpdate, { $merge: value }); + this.updateFileKey(id, key, applyValue, '$apply'); } handleConfirm(hasNewUpload) { @@ -487,7 +450,7 @@ class PresentationUploader extends Component { Session.set('showUploadPresentationView', false); return handleSave(presentationsToSave) .then(() => { - const hasError = presentations.some(p => p.upload.error || p.conversion.error); + const hasError = presentations.some((p) => p.upload.error || p.conversion.error); if (!hasError) { this.setState({ disableActions: false, @@ -501,7 +464,7 @@ class PresentationUploader extends Component { // preventClosing: true, }, () => { // if the selected current has error we revert back to the old one - const newCurrent = presentations.find(p => p.isCurrent); + const newCurrent = presentations.find((p) => p.isCurrent); if (newCurrent.upload.error || newCurrent.conversion.error) { this.handleCurrentChange(selectedToBeNextCurrent); } @@ -519,81 +482,112 @@ class PresentationUploader extends Component { return null; } - deepMergeUpdateFileKey(id, key, value) { - const applyValue = toUpdate => update(toUpdate, { $merge: value }); - this.updateFileKey(id, key, applyValue, '$apply'); + handleDismiss() { + const { presentations } = this.state; + const { presentations: propPresentations } = this.props; + const ids = new Set(propPresentations.map((d) => d.ID)); + const merged = [ + ...propPresentations, + ...presentations.filter((d) => !ids.has(d.ID)), + ]; + this.setState( + { presentations: merged }, + Session.set('showUploadPresentationView', false), + ); } - handleCurrentChange(id) { - const { presentations, disableActions } = this.state; + handleToggleDownloadable(item) { + const { dispatchTogglePresentationDownloadable } = this.props; + const { presentations } = this.state; - if (disableActions) return; + const oldDownloadableState = item.isDownloadable; - const currentIndex = presentations.findIndex(p => p.isCurrent); - const newCurrentIndex = presentations.findIndex(p => p.id === id); + const outOfDatePresentationIndex = presentations.findIndex((p) => p.id === item.id); 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] = { + commands[outOfDatePresentationIndex] = { $apply: (presentation) => { const p = presentation; - p.isCurrent = true; + p.isDownloadable = !oldDownloadableState; return p; }, }; - const presentationsUpdated = update(presentations, commands); - this.setState({ presentations: presentationsUpdated }); - } - handleRemove(item, withErr = false) { - if (withErr) { - const { presentations } = this.props; - this.hasError = false; - return this.setState({ - presentations, - disableActions: false, - }); + this.setState({ + presentations: presentationsUpdated, + }); + + // If the presentation has not be uploaded yet, adjusting the state suffices + // otherwise set previously uploaded presentation to [not] be downloadable + if (item.upload.done) { + dispatchTogglePresentationDownloadable(item, !oldDownloadableState); } + } - 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 currentIndex = updatedPresentations.findIndex(p => p.isCurrent); - const actualCurrentIndex = updatedPresentations.findIndex(p => p.id === oldCurrentId); + updateFileKey(id, key, value, operation = '$set') { + this.setState(({ presentations }) => { + const fileIndex = presentations.findIndex((f) => f.id === id); - if (currentIndex === -1 && updatedPresentations.length > 0) { - const commands = {}; - const newCurrentIndex = actualCurrentIndex === -1 ? 0 : actualCurrentIndex; - commands[newCurrentIndex] = { - $apply: (presentation) => { - const p = presentation; - p.isCurrent = true; - return p; + return fileIndex === -1 ? false : { + presentations: update(presentations, { + [fileIndex]: { + $apply: (file) => update(file, { + [key]: { + [operation]: value, + }, + }), }, - }; - - const updatedCurrent = update(updatedPresentations, commands); - this.setState({ presentations: updatedCurrent }); - } + }), + }; }); } + renderToastItem(item) { + 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; + + const itemClassName = { + [styles.done]: !isProcessing && !hasError, + [styles.err]: hasError, + [styles.loading]: isProcessing, + }; + + const statusInfoStyle = { + [styles.textErr]: hasError, + [styles.textInfo]: !hasError, + }; + + let icon = isProcessing ? 'blank' : 'check'; + if (hasError) icon = 'circle_close'; + + return ( + <div + key={item.id} + className={styles.uploadRow} + onClick={() => { + if (hasError || isProcessing) Session.set('showUploadPresentationView', true); + }} + > + <div className={styles.fileLine}> + <span className={styles.fileIcon}> + <Icon iconName="file" /> + </span> + <span className={styles.toastFileName}> + <span>{item.filename}</span> + </span> + <span className={styles.statusIcon}> + <Icon iconName={icon} className={cx(itemClassName)} /> + </span> + </div> + <div className={styles.statusInfo}> + <span className={cx(statusInfoStyle)}>{this.renderPresentationItemStatus(item)}</span> + </div> + </div> + ); + } + renderPresentationList() { const { presentations } = this.state; const { intl } = this.props; @@ -685,7 +679,7 @@ class PresentationUploader extends Component { <div className={styles.innerToast}> <div> <div> - {presentationsSorted.map(item => this.renderToastItem(item))} + {presentationsSorted.map((item) => this.renderToastItem(item))} </div> </div> </div> @@ -794,8 +788,6 @@ class PresentationUploader extends Component { renderDropzone() { const { intl, - fileSizeMin, - fileSizeMax, fileValidMimeTypes, } = this.props; @@ -823,9 +815,7 @@ class PresentationUploader extends Component { multiple className={styles.dropzone} activeClassName={styles.dropzoneActive} - accept={fileValidMimeTypes.map(fileValid => fileValid.extension)} - minSize={fileSizeMin} - maxSize={fileSizeMax} + accept={fileValidMimeTypes.map((fileValid) => fileValid.extension)} disablepreview="true" onDrop={this.handleFiledrop} > @@ -844,8 +834,6 @@ class PresentationUploader extends Component { renderPicDropzone() { const { intl, - fileSizeMin, - fileSizeMax, } = this.props; const { disableActions } = this.state; @@ -871,8 +859,6 @@ class PresentationUploader extends Component { activeClassName={styles.dropzoneActive} rejectClassName={styles.dropzoneReject} accept="image/*" - minSize={fileSizeMin} - maxSize={fileSizeMax} disablepreview="true" data-test="fileUploadDropZone" onDrop={this.handleFiledrop} @@ -901,14 +887,32 @@ class PresentationUploader extends Component { }); } + const constraint = {}; + if (item.upload.done && item.upload.error) { + if (item.conversion.status === 'FILE_TOO_LARGE') { + constraint['0'] = ((item.conversion.maxFileSize) / 1000 / 1000).toFixed(2); + } + const errorMessage = intlMessages[item.upload.status] || intlMessages.genericError; - return intl.formatMessage(errorMessage); + return intl.formatMessage(errorMessage, constraint); } if (!item.conversion.done && item.conversion.error) { const errorMessage = intlMessages[item.conversion.status] || intlMessages.genericConversionStatus; - return intl.formatMessage(errorMessage); + + switch (item.conversion.status) { + case 'PAGE_COUNT_EXCEEDED': + constraint['0'] = item.conversion.maxNumberPages; + break; + case 'PDF_HAS_BIG_PAGE': + constraint['0'] = (item.conversion.bigPageSize / 1000 / 1000).toFixed(2); + break; + default: + break; + } + + return intl.formatMessage(errorMessage, constraint); } if (!item.conversion.done && !item.conversion.error) { diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx index 938aa1bb3c..0fac2c8ad5 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx @@ -9,7 +9,7 @@ import PresentationUploader from './component'; const PRESENTATION_CONFIG = Meteor.settings.public.presentation; -const PresentationUploaderContainer = props => ( +const PresentationUploaderContainer = (props) => ( props.isPresenter && ( <ErrorBoundary Fallback={() => <FallbackModal />}> @@ -29,10 +29,8 @@ export default withTracker(() => { return { presentations: currentPresentations, defaultFileName: PRESENTATION_CONFIG.defaultPresentationFile, - fileSizeMin: PRESENTATION_CONFIG.uploadSizeMin, - fileSizeMax: PRESENTATION_CONFIG.uploadSizeMax, fileValidMimeTypes: PRESENTATION_CONFIG.uploadValidMimeTypes, - handleSave: presentations => Service.persistPresentationChanges( + handleSave: (presentations) => Service.persistPresentationChanges( currentPresentations, presentations, PRESENTATION_CONFIG.uploadEndpoint, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js index 95d4a3d699..fa46226be1 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js @@ -85,11 +85,25 @@ const observePresentationConversion = ( const query = Presentations.find({ meetingId }); query.observe({ + added: (doc) => { + if (doc.name !== filename) return; + + if (doc.conversion.status === 'FILE_TOO_LARGE') { + onConversion(doc.conversion); + c.stop(); + clearTimeout(conversionTimeout); + } + }, changed: (newDoc) => { if (newDoc.name !== filename) return; onConversion(newDoc.conversion); + if (newDoc.conversion.error) { + c.stop(); + clearTimeout(conversionTimeout); + } + if (newDoc.conversion.done) { c.stop(); didValidate(newDoc); @@ -178,6 +192,7 @@ const uploadAndConvertPresentation = ( error, }, }, 'Generic presentation upload exception catcher'); + observePresentationConversion(meetingId, file.name, onConversion); onUpload({ error: true, done: true, status: error.code }); return Promise.resolve(); }); @@ -188,7 +203,7 @@ const uploadAndConvertPresentations = ( meetingId, podId, uploadEndpoint, -) => Promise.all(presentationsToUpload.map(p => uploadAndConvertPresentation( +) => Promise.all(presentationsToUpload.map((p) => uploadAndConvertPresentation( p.file, p.isDownloadable, podId, meetingId, uploadEndpoint, p.onUpload, p.onProgress, p.onConversion, ))); @@ -206,13 +221,13 @@ const removePresentation = (presentationId, podId) => { const removePresentations = ( presentationsToRemove, podId, -) => Promise.all(presentationsToRemove.map(p => removePresentation(p.id, podId))); +) => Promise.all(presentationsToRemove.map((p) => removePresentation(p.id, podId))); const persistPresentationChanges = (oldState, newState, uploadEndpoint, podId) => { - const presentationsToUpload = newState.filter(p => !p.upload.done); - const presentationsToRemove = oldState.filter(p => !_.find(newState, ['id', p.id])); + const presentationsToUpload = newState.filter((p) => !p.upload.done); + const presentationsToRemove = oldState.filter((p) => !_.find(newState, ['id', p.id])); - let currentPresentation = newState.find(p => p.isCurrent); + let currentPresentation = newState.find((p) => p.isCurrent); return uploadAndConvertPresentations(presentationsToUpload, Auth.meetingID, podId, uploadEndpoint) .then((presentations) => { @@ -233,7 +248,7 @@ const persistPresentationChanges = (oldState, newState, uploadEndpoint, podId) = // If its a newly uploaded presentation we need to get it from promise result if (!currentPresentation.conversion.done) { - const currentIndex = presentationsToUpload.findIndex(p => p === currentPresentation); + const currentIndex = presentationsToUpload.findIndex((p) => p === currentPresentation); currentPresentation = presentations[currentIndex]; } 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 3a3fc64767..11a67195d2 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.scss @@ -12,7 +12,7 @@ --fileLineWidth: 16.75rem; --itemActionsWidth: 68px; // size of the 2 icons (check/trash) --uploadListHeight: 30vh; - --modalInnerWidth: 37.5rem; + --modalInnerWidth: 40rem; } @keyframes bar-stripes { diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index aa09382a63..235f3ef5f2 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -456,8 +456,6 @@ public: panZoomThrottle: 32 restoreOnUpdate: false uploadEndpoint: '/bigbluebutton/presentation/upload' - uploadSizeMin: 0 - uploadSizeMax: 50000000 uploadValidMimeTypes: - extension: .pdf mime: application/pdf diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index 88b72067bd..d2221d011a 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -191,7 +191,7 @@ "app.presentationUploder.currentBadge": "Current", "app.presentationUploder.rejectedError": "The selected file(s) have been rejected. Please check the file type(s).", "app.presentationUploder.upload.progress": "Uploading ({0}%)", - "app.presentationUploder.upload.413": "File is too large. Please split into multiple files.", + "app.presentationUploder.upload.413": "File is too large, exceeded the maximum of {0} MB", "app.presentationUploder.genericError": "Oops, Something went wrong ...", "app.presentationUploder.upload.408": "Request upload token timeout.", "app.presentationUploder.upload.404": "404: Invalid upload token", @@ -201,10 +201,10 @@ "app.presentationUploder.conversion.generatingThumbnail": "Generating thumbnails ...", "app.presentationUploder.conversion.generatedSlides": "Slides generated ...", "app.presentationUploder.conversion.generatingSvg": "Generating SVG images ...", - "app.presentationUploder.conversion.pageCountExceeded": "Number of pages exceeded. Please break file into multiple files.", + "app.presentationUploder.conversion.pageCountExceeded": "Number of pages exceeded maximum of {0}", "app.presentationUploder.conversion.officeDocConversionInvalid": "Failed to process office document. Please upload a PDF instead.", "app.presentationUploder.conversion.officeDocConversionFailed": "Failed to process office document. Please upload a PDF instead.", - "app.presentationUploder.conversion.pdfHasBigPage": "We could not convert the PDF file, please try optimizing it", + "app.presentationUploder.conversion.pdfHasBigPage": "We could not convert the PDF file, please try optimizing it. Max page size {0}", "app.presentationUploder.conversion.timeout": "Ops, the conversion took too long", "app.presentationUploder.conversion.pageCountFailed": "Failed to determine the number of pages.", "app.presentationUploder.isDownloadableLabel": "Presentation download is not allowed - click to allow presentation to be downloaded", -- GitLab