import Presentations, { UploadingPresentations } from '/imports/api/presentations'; import React from 'react'; import { useTracker } from 'meteor/react-meteor-data'; import Icon from '/imports/ui/components/common/icon/component'; import { makeCall } from '/imports/ui/services/api'; import Styled from '/imports/ui/components/presentation/presentation-uploader/styles'; import { toast } from 'react-toastify'; import { defineMessages } from 'react-intl'; const TIMEOUT_CLOSE_TOAST = 1; // second const intlMessages = defineMessages({ item: { id: 'app.presentationUploder.item', description: 'single item label', }, itemPlural: { id: 'app.presentationUploder.itemPlural', description: 'plural item label', }, 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', }, GENERATING_THUMBNAIL: { id: 'app.presentationUploder.conversion.generatingThumbnail', description: 'indicatess that it is generating thumbnails', }, GENERATING_SVGIMAGES: { id: 'app.presentationUploder.conversion.generatingSvg', description: 'warns that it is generating svg images', }, GENERATED_SLIDE: { id: 'app.presentationUploder.conversion.generatedSlides', description: 'warns that were slides generated', }, 413: { id: 'app.presentationUploder.upload.413', description: 'error that file exceed the size limit', }, 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', }, FILE_TOO_LARGE: { id: 'app.presentationUploder.upload.413', description: 'error that file exceed the size limit', }, IVALID_MIME_TYPE: { id: 'app.presentationUploder.conversion.invalidMimeType', description: 'warns user that the file\'s mime type is not supported or it doesn\'t match the extension', }, 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', }, 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', }, 204: { id: 'app.presentationUploder.conversion.204', description: 'error indicating that the file has no content to capture', }, fileToUpload: { id: 'app.presentationUploder.fileToUpload', description: 'message used in the file selected for upload', }, uploadProcess: { id: 'app.presentationUploder.upload.progress', description: 'message that indicates the percentage of the upload', }, badConnectionError: { id: 'app.presentationUploder.connectionClosedError', description: 'message indicating that the connection was closed', }, conversionProcessingSlides: { id: 'app.presentationUploder.conversion.conversionProcessingSlides', description: 'indicates how many slides were converted', }, genericError: { id: 'app.presentationUploder.genericError', description: 'generic error while uploading/converting', }, genericConversionStatus: { id: 'app.presentationUploder.conversion.genericConversionStatus', description: 'indicates that file is being converted', }, }); function renderPresentationItemStatus(item, intl) { if ((('progress' in item) && item.progress === 0) || (('upload' in item) && item.upload.progress === 0 && !item.upload.error)) { return intl.formatMessage(intlMessages.fileToUpload); } if (('progress' in item) && item.progress < 100 && !('conversion' in item)) { return intl.formatMessage(intlMessages.uploadProcess, { 0: Math.floor(item.progress).toString(), }); } const constraint = {}; if (('upload' in item) && (item.upload.done && item.upload.error)) { if (item.conversion.status === 'FILE_TOO_LARGE' || item.upload.status !== 413) { constraint['0'] = ((item.conversion.maxFileSize) / 1000 / 1000).toFixed(2); } else if (item.progress < 100) { const errorMessage = intlMessages.badConnectionError; return intl.formatMessage(errorMessage); } const errorMessage = intlMessages[item.upload.status] || intlMessages.genericError; return intl.formatMessage(errorMessage, constraint); } if (('conversion' in item) && (!item.conversion.done && item.conversion.error)) { const errorMessage = intlMessages[item.conversion.status] || intlMessages.genericConversionStatus; switch (item.conversion.status) { case 'CONVERSION_TIMEOUT': constraint['0'] = item.conversion.numberPageError; constraint['1'] = item.conversion.maxNumberOfAttempts; break; case 'FILE_TOO_LARGE': constraint['0'] = ((item.conversion.maxFileSize) / 1000 / 1000).toFixed(2); break; 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; case 'IVALID_MIME_TYPE': constraint['0'] = item.conversion.fileExtension; constraint['1'] = item.conversion.fileMime; break; default: break; } return intl.formatMessage(errorMessage, constraint); } if ((('conversion' in item) && (!item.conversion.done && !item.conversion.error)) || (('progress' in item) && item.progress === 100)) { let conversionStatusMessage; if ('conversion' in item) { if (item.conversion.pagesCompleted < item.conversion.numPages) { return intl.formatMessage(intlMessages.conversionProcessingSlides, { 0: item.conversion.pagesCompleted, 1: item.conversion.numPages, }); } conversionStatusMessage = intlMessages[item.conversion.status] || intlMessages.genericConversionStatus; } else { conversionStatusMessage = intlMessages.genericConversionStatus; } return intl.formatMessage(conversionStatusMessage); } return null; } function renderToastItem(item, intl) { const isUploading = ('progress' in item) && item.progress <= 100; const isConverting = ('conversion' in item) && !item.conversion.done; const hasError = ((('conversion' in item) && item.conversion.error) || (('upload' in item) && item.upload.error)); const isProcessing = (isUploading || isConverting) && !hasError; let icon = isProcessing ? 'blank' : 'check'; if (hasError) icon = 'circle_close'; return ( { if (hasError || isProcessing) Session.set('showUploadPresentationView', true); }} > {item.filename || item.name} {renderPresentationItemStatus(item, intl)} ); } const renderToastList = (presentations, intl) => { let converted = 0; const presentationsSorted = presentations .sort((a, b) => a.uploadTimestamp - b.uploadTimestamp) .sort((a, b) => { const presADone = a.conversion ? a.conversion.done : false; const presBDone = b.conversion ? b.conversion.done : false; return presADone - presBDone; }); presentationsSorted .forEach((p) => { const presDone = p.conversion ? p.conversion.done : false; if (presDone) converted += 1; return p; }); let toastHeading = ''; const itemLabel = presentationsSorted.length > 1 ? intl.formatMessage(intlMessages.itemPlural) : intl.formatMessage(intlMessages.item); if (converted === 0) { toastHeading = intl.formatMessage(intlMessages.uploading, { 0: presentationsSorted.length, 1: itemLabel, }); } if (converted > 0 && converted !== presentationsSorted.length) { toastHeading = intl.formatMessage(intlMessages.uploadStatus, { 0: converted, 1: presentationsSorted.length, }); } if (converted === presentationsSorted.length) { toastHeading = intl.formatMessage(intlMessages.completed, { 0: converted, }); } return ( {toastHeading} {presentationsSorted.map((item) => renderToastItem(item, intl))} ); }; function handleDismissToast(toastId) { return toast.dismiss(toastId); } const alreadyRenderedPresList = []; const enteredConversion = {}; export const PresentationUploaderToast = ({ intl }) => { useTracker(() => { const presentationsRenderedFalseAndConversionFalse = Presentations.find({ $or: [{ renderedInToast: false }, { 'conversion.done': false }] }).fetch(); const convertingPresentations = presentationsRenderedFalseAndConversionFalse .filter((p) => !p.renderedInToast); // removing ones with errors. // If presentation has an error status - we don't want to have it pending as uploading convertingPresentations.forEach((p) => { if ('conversion' in p && p.conversion.error) { UploadingPresentations.remove( { $or: [{ temporaryPresentationId: p.temporaryPresentationId }, { id: p.id }] }, ); } }); const toRemoveFromUploadingPresentations = []; // main goal of this mapping is to sort out what doesn't need to be displayed UploadingPresentations.find().fetch().forEach((p) => { if ( (('upload' in p && p.upload.done) // if presentation is marked as done - it's potentially to be removed && !p.subscriptionId) // at upload stage or already converted || (p.lastModifiedUploader === false) // if presentation uploaded internally (e.g., breakout capture) ) { if (convertingPresentations[0]) { // there are presentations being converted convertingPresentations.forEach((cp) => { // if this presentation is being converted // we don't want it to be marked as still uploading if (cp.temporaryPresentationId === p.temporaryPresentationId) { toRemoveFromUploadingPresentations .push({ temporaryPresentationId: p.temporaryPresentationId, id: p.id }); } }); // upload stage is done and presentation is entering conversion stage } else if (!enteredConversion[p.temporaryPresentationId]) { // we mark that it has entered conversion stage enteredConversion[p.temporaryPresentationId] = true; } else { // presentation doesn't normally enter conversion twice so we remove // the inconsistencies between UploadingPresentation and Presentation (corner case) const presentationsAlreadyRenderedIds = Presentations .find({ renderedInToast: true }).fetch().map((pr) => ( { id: pr.id, temporaryPresentationId: pr.temporaryPresentationId, } )); presentationsAlreadyRenderedIds.forEach((pr) => { UploadingPresentations.remove({ $or: [{ temporaryPresentationId: pr.temporaryPresentationId }, { id: pr.id }], }); }); } } }); toRemoveFromUploadingPresentations.forEach((p) => { UploadingPresentations .remove({ $or: [{ temporaryPresentationId: p.temporaryPresentationId }, { id: p.id }] }); }); const uploadingPresentations = UploadingPresentations.find().fetch(); let presentationsToConvert = convertingPresentations.concat(uploadingPresentations); // Updating or populating the "state" presentation list presentationsToConvert.map((p) => ( { filename: p.name || p.filename, temporaryPresentationId: p.temporaryPresentationId, presentationId: p.id, hasError: p.conversion?.error || p.upload?.error, lastModifiedUploader: p.lastModifiedUploader, } )).forEach((p) => { const docIndexAlreadyInList = alreadyRenderedPresList.findIndex((pres) => ( (pres.temporaryPresentationId === p.temporaryPresentationId || pres.presentationId === p.presentationId || ( pres.lastModifiedUploader !== undefined && !pres.lastModifiedUploader && pres.filename === p.filename ) ) )); if (docIndexAlreadyInList === -1) { alreadyRenderedPresList.push({ filename: p.filename, temporaryPresentationId: p.temporaryPresentationId, presentationId: p.presentationId, rendered: false, lastModifiedUploader: p.lastModifiedUploader, hasError: p.hasError, }); } else { const presAlreadyRendered = alreadyRenderedPresList[docIndexAlreadyInList]; presAlreadyRendered.temporaryPresentationId = p.temporaryPresentationId; presAlreadyRendered.presentationId = p.presentationId; presAlreadyRendered.lastModifiedUploader = p.lastModifiedUploader; presAlreadyRendered.hasError = p.hasError; } }); let activeToast = Session.get('presentationUploaderToastId'); const showToast = presentationsToConvert.length > 0; if (showToast && !activeToast) { activeToast = toast.info(() => renderToastList(presentationsToConvert, intl), { hideProgressBar: true, autoClose: false, newestOnTop: true, closeOnClick: true, className: 'presentationUploaderToast toastClass', onClose: () => { presentationsToConvert = []; if (alreadyRenderedPresList.every((pres) => pres.rendered)) { makeCall('setPresentationRenderedInToast').then(() => { Session.set('presentationUploaderToastId', null); }); alreadyRenderedPresList.length = 0; } }, }); Session.set('presentationUploaderToastId', activeToast); } else if (!showToast && activeToast) { handleDismissToast(activeToast); Session.set('presentationUploaderToastId', null); } else { toast.update(activeToast, { render: renderToastList(presentationsToConvert, intl), }); } const temporaryPresentationIdListToSetAsRendered = presentationsToConvert.filter((p) => ( 'conversion' in p && (p.conversion.done || p.conversion.error) )); temporaryPresentationIdListToSetAsRendered.forEach((p) => { const index = alreadyRenderedPresList.findIndex((pres) => ( pres.temporaryPresentationId === p.temporaryPresentationId || pres.presentationId === p.id )); if (index !== -1) { alreadyRenderedPresList[index].rendered = true; } }); if (alreadyRenderedPresList.every((pres) => pres.rendered && !pres.hasError)) { setTimeout(() => { makeCall('setPresentationRenderedInToast'); alreadyRenderedPresList.length = 0; }, TIMEOUT_CLOSE_TOAST * 1000); } }, []); return null; }; export default { handleDismissToast, renderPresentationItemStatus, };