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

433 lines
15 KiB
React
Raw Normal View History

import Presentations from '/imports/api/presentations';
import React, { useState } 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';
import _ from 'lodash';
import { UploadingPresentations } from '/imports/api/presentations';
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',
},
2023-01-08 23:15:16 +08:00
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 (
<Styled.UploadRow
key={item.id || item.temporaryPresentationId}
onClick={() => {
if (hasError || isProcessing) Session.set('showUploadPresentationView', true);
}}
>
<Styled.FileLine>
<span>
<Icon iconName="file" />
</span>
<Styled.ToastFileName>
<span>{item.filename || item.name}</span>
</Styled.ToastFileName>
<Styled.StatusIcon>
<Styled.ToastItemIcon
done={!isProcessing && !hasError}
error={hasError}
loading={ isProcessing }
iconName={icon}
/>
</Styled.StatusIcon>
</Styled.FileLine>
<Styled.StatusInfo>
<Styled.StatusInfoSpan data-test="presentationStatusInfo" styles={hasError ? 'error' : 'info'}>
{renderPresentationItemStatus(item, intl)}
</Styled.StatusInfoSpan>
</Styled.StatusInfo>
</Styled.UploadRow>
);
}
const renderToastList = (presentations, intl) => {
let converted = 0;
let 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 (
<Styled.ToastWrapper data-test="presentationUploadProgressToast" >
<Styled.UploadToastHeader>
<Styled.UploadIcon iconName="upload" />
<Styled.UploadToastTitle>{toastHeading}</Styled.UploadToastTitle>
</Styled.UploadToastHeader>
<Styled.InnerToast>
<div>
<div>
{presentationsSorted.map((item) => renderToastItem(item, intl))}
</div>
</div>
</Styled.InnerToast>
</Styled.ToastWrapper>
);
}
function handleDismissToast(toastId) {
return toast.dismiss(toastId);
}
2022-12-08 22:20:17 +08:00
2023-01-25 00:13:38 +08:00
let alreadyRenderedPresList = [];
let enteredConversion = {};
2022-12-08 22:20:17 +08:00
export const PresentationUploaderToast = ({ intl }) => {
useTracker(() => {
const presentationsRenderedFalseAndConversionFalse = Presentations.find({ $or: [{renderedInToast: false}, {"conversion.done": false}] }).fetch();
2022-12-08 22:20:17 +08:00
const convertingPresentations = presentationsRenderedFalseAndConversionFalse.filter(p => !p.renderedInToast );
2023-01-28 03:29:40 +08:00
let toRemoveFromUploadingPresentations = [];
convertingPresentations.map(p => {
if ("conversion" in p && p.conversion.error){
// if presentation has an error status - we don't want to have it pending as ploading
toRemoveFromUploadingPresentations.push({temporaryPresentationId: p.temporaryPresentationId, id: p.id});
}
});
2023-01-25 00:13:38 +08:00
UploadingPresentations.find().fetch().map(p => { // main goal of this mapping is to sort out what doesn't need to be displayed
if (
(
2023-01-28 03:29:40 +08:00
"upload" in p // null check
2023-01-25 00:13:38 +08:00
&& p.upload.done // if presentation is marked as done - it's potentially to be removed
)
&& !p.subscriptionId // at upload stage or already converted
) {
if(convertingPresentations[0]) { //there are presentations being converted
convertingPresentations.forEach(cp => {
if (cp.temporaryPresentationId == p.temporaryPresentationId) { // if this presentation is being converted we don't want it to be marked as still uploading
2023-01-28 03:29:40 +08:00
toRemoveFromUploadingPresentations.push({temporaryPresentationId: p.temporaryPresentationId, id: p.id});
2023-01-25 00:13:38 +08:00
}
});
} else if (!enteredConversion[p.temporaryPresentationId]) { // upload stage is done and pesentation is entering conversion stage
enteredConversion[p.temporaryPresentationId] = true; // we mark that it has entered conversion stage
} else { // presentations don't enter conversion stage twice, thus it's a border case of presenter change during conversion and this one is to be rmoved
2023-01-28 03:29:40 +08:00
toRemoveFromUploadingPresentations.push({temporaryPresentationId: p.temporaryPresentationId, id: p.id});
2023-01-25 00:13:38 +08:00
}
}
2022-12-08 22:20:17 +08:00
});
2023-01-28 03:29:40 +08:00
toRemoveFromUploadingPresentations.forEach(p => UploadingPresentations.remove({$or: [{temporaryPresentationId: p.temporaryPresentationId }, {id: p.id}]}));
2022-12-08 22:20:17 +08:00
const uploadingPresentations = UploadingPresentations.find().fetch();
2022-12-08 22:20:17 +08:00
let presentationsToConvert = convertingPresentations.concat(uploadingPresentations);
// Updating or populating the "state" presentation list
presentationsToConvert.map(p => {
return {
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 => {
return (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 {
alreadyRenderedPresList[docIndexAlreadyInList].temporaryPresentationId = p.temporaryPresentationId;
alreadyRenderedPresList[docIndexAlreadyInList].presentationId = p.presentationId;
alreadyRenderedPresList[docIndexAlreadyInList].lastModifiedUploader = p.lastModifiedUploader;
alreadyRenderedPresList[docIndexAlreadyInList].hasError = p.hasError;
}
})
let activeToast = Session.get("presentationUploaderToastId");
const showToast = presentationsToConvert.length > 0;
2022-12-08 22:20:17 +08:00
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),
});
}
let temporaryPresentationIdListToSetAsRendered = presentationsToConvert.filter(p =>
("conversion" in p && (p.conversion.done || p.conversion.error)))
temporaryPresentationIdListToSetAsRendered = temporaryPresentationIdListToSetAsRendered.map(p => {
index = alreadyRenderedPresList.findIndex(pres => (pres.temporaryPresentationId === p.temporaryPresentationId || pres.presentationId === p.id));
if (index !== -1) {
alreadyRenderedPresList[index].rendered = true;
}
return p.temporaryPresentationId
});
if (alreadyRenderedPresList.every((pres) => pres.rendered && !pres.hasError)) {
setTimeout(() => {
makeCall('setPresentationRenderedInToast');
alreadyRenderedPresList.length = 0;
}, TIMEOUT_CLOSE_TOAST * 1000);
}
2022-12-08 22:20:17 +08:00
}, []);
return null;
}
export default {
handleDismissToast,
renderPresentationItemStatus,
}