improvement(virtual background): better error handling, notifications, performance

This commit is contained in:
Joao Victor 2022-07-06 10:38:40 -03:00
parent 3b820b60a9
commit aaf7f8e78e
7 changed files with 349 additions and 90 deletions

View File

@ -0,0 +1,179 @@
import React, { useRef } from 'react';
import _ from 'lodash';
import { defineMessages } from 'react-intl';
import { toast } from 'react-toastify';
import { notify } from '/imports/ui/services/notification';
import Icon from '/imports/ui/components/common/icon/component';
import Styled from './styles';
const intlMessages = defineMessages({
maximumSizeExceeded: {
id: 'app.video.virtualBackground.maximumFileSizeExceeded',
description: 'Label for the maximum file size exceeded notification',
},
typeNotAllowed: {
id: 'app.video.virtualBackground.typeNotAllowed',
description: 'Label for the file type not allowed notification',
},
errorOnRead: {
id: 'app.video.virtualBackground.errorOnRead',
description: 'Label for the error on read notification',
},
uploaded: {
id: 'app.video.virtualBackground.uploaded',
description: 'Label for when the file is uploaded',
},
uploading: {
id: 'app.video.virtualBackground.uploading',
description: 'Label for when the file is uploading',
},
});
const STATUS = {
LOADING: 'loading',
DONE: 'done',
ERROR: 'error',
};
/**
* HOC for injecting a file reader utility.
* @param {React.Component} Component
* @param {string[]} mimeTypesAllowed String array containing MIME types allowed.
* @param {number} maxFileSize Max file size allowed in Mbytes.
* @returns A new component which accepts the same props as the wrapped component plus
* a function called readFile.
*/
const withFileReader = (
Component,
mimeTypesAllowed,
maxFileSize,
) => (props) => {
const { intl } = props;
const toastId = useRef(null);
const parseFilename = (filename = '') => {
const substrings = filename.split('.');
substrings.pop();
const filenameWithoutExtension = substrings.join('');
return filenameWithoutExtension;
};
const renderToastContent = (text, status) => {
let icon;
let statusMessage;
switch (status) {
case STATUS.LOADING:
icon = 'blank';
statusMessage = intl.formatMessage(intlMessages.uploading);
break;
case STATUS.DONE:
icon = 'check';
statusMessage = intl.formatMessage(intlMessages.uploaded);
break;
case STATUS.ERROR:
default:
icon = 'circle_close'
statusMessage = intl.formatMessage(intlMessages.errorOnRead);
}
return (
<Styled.Content>
<Styled.FileLine>
<Icon iconName="file" />
<Styled.ToastFileName>{text}</Styled.ToastFileName>
<span>
<Styled.ToastIcon
iconName={icon}
loading={status === STATUS.LOADING}
done={status === STATUS.DONE}
error={status === STATUS.ERROR}
/>
</span>
</Styled.FileLine>
<Styled.Status>
<span>
{statusMessage}
</span>
</Styled.Status>
</Styled.Content>
);
};
const renderToast = (text = '', status = STATUS.DONE, callback) => {
if (toastId.current) {
toast.dismiss(toastId.current);
}
toastId.current = toast.info(renderToastContent(text, status), {
hideProgressBar: status === STATUS.DONE ? false : true,
autoClose: status === STATUS.DONE ? 5000 : false,
newestOnTop: true,
closeOnClick: true,
onClose: () => {
toastId.current = null;
},
onOpen: () => {
if (typeof callback === 'function') callback();
},
});
};
const readFile = (
file,
onSuccess = () => {},
onError = () => {},
) => {
if (!file) return;
const { name, size, type } = file;
const sizeInKB = size / 1024;
if (sizeInKB > maxFileSize) {
notify(
intl.formatMessage(
intlMessages.maximumSizeExceeded,
{ 0: (maxFileSize / 1000).toFixed(0) },
),
'error',
);
return onError(new Error('Maximum file size exceeded.'));
}
if (!mimeTypesAllowed.includes(type)) {
notify(
intl.formatMessage(intlMessages.typeNotAllowed),
'error',
);
return onError(new Error('File type not allowed.'));
}
const filenameWithoutExtension = parseFilename(name);
const reader = new FileReader();
reader.onload = (e) => {
const data = {
filename: filenameWithoutExtension,
data: e.target.result,
uniqueId: _.uniqueId(),
};
renderToast(name, STATUS.DONE, () => { onSuccess(data); });
};
reader.onerror = () => {
renderToast(name, STATUS.ERROR, () => {
onError(new Error('Something went wrong when reading the file.'));
});
};
reader.onloadstart = () => {
renderToast(name, STATUS.LOADING);
};
reader.readAsDataURL(file);
};
return <Component readFile={readFile} {...props} />;
}
export default withFileReader;

View File

@ -0,0 +1,93 @@
import styled, { css, keyframes } from 'styled-components';
import Icon from '/imports/ui/components/common/icon/component';
import {
colorDanger,
colorGray,
colorGrayLightest,
colorSuccess,
} from '/imports/ui/stylesheets/styled-components/palette';
import {
fileLineWidth,
iconPaddingMd,
mdPaddingY,
statusIconSize,
} from '/imports/ui/stylesheets/styled-components/general';
import {
headingsFontWeight,
} from '/imports/ui/stylesheets/styled-components/typography';
const rotate = keyframes`
0% { transform: rotate(0); }
100% { transform: rotate(360deg); }
`;
const ToastIcon = styled(Icon)`
font-size: 117%;
width: ${statusIconSize};
height: ${statusIconSize};
position: relative;
left: 8px;
[dir="rtl"] & {
left: unset;
right: 8px;
}
${({ done }) => done && `
color: ${colorSuccess};
`}
${({ error }) => error && `
color: ${colorDanger};
`}
${({ loading }) => loading && css`
color: ${colorGrayLightest};
border: 1px solid;
border-radius: 50%;
border-right-color: ${colorGray};
animation: ${rotate} 1s linear infinite;
`}
`;
const FileLine = styled.div`
display: flex;
flex-direction: row;
align-items: center;
width: ${fileLineWidth};
padding-bottom: ${iconPaddingMd};
`;
const ToastFileName = styled.span`
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
margin-left: ${mdPaddingY};
width: auto;
text-align: left;
font-weight: ${headingsFontWeight};
[dir="rtl"] & {
margin-right: ${mdPaddingY};
margin-left: 0;
text-align: right;
}
`;
const Content = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
`;
const Status = styled.span`
font-size: 70%;
`;
export default {
ToastIcon,
FileLine,
ToastFileName,
Content,
Status,
};

View File

@ -12,6 +12,10 @@ import {
} from '/imports/ui/services/virtual-background/service';
import { CustomVirtualBackgroundsContext } from './context';
import VirtualBgService from '/imports/ui/components/video-preview/virtual-background/service';
import logger from '/imports/startup/client/logger';
import withFileReader from '/imports/ui/components/common/file-reader/component';
const { MIME_TYPES_ALLOWED, MAX_FILE_SIZE } = VirtualBgService;
const propTypes = {
intl: PropTypes.shape({
@ -83,6 +87,7 @@ const VirtualBgSelector = ({
showThumbnails,
initialVirtualBgState,
isVisualEffects,
readFile,
}) => {
const [currentVirtualBg, setCurrentVirtualBg] = useState({
...initialVirtualBgState,
@ -160,21 +165,26 @@ const VirtualBgSelector = ({
const handleCustomBgChange = (event) => {
const file = event.target.files[0];
const { readFile } = VirtualBgService;
readFile(
file,
(background) => {
dispatch({
type: 'new',
background,
});
},
(error) => {
// Add some logging, notification, etc.
}
);
}
const onSuccess = (background) => {
dispatch({
type: 'new',
background,
});
};
const onError = (error) => {
logger.warn({
logCode: 'read_file_error',
extraInfo: {
errorName: error.name,
errorMessage: error.message,
},
}, error.message);
};
readFile(file, onSuccess, onError);
};
const renderThumbnailSelector = () => {
const disabled = locked || !isVirtualBackgroundSupported();
@ -375,4 +385,4 @@ VirtualBgSelector.defaultProps = {
},
};
export default injectIntl(VirtualBgSelector);
export default injectIntl(withFileReader(VirtualBgSelector, MIME_TYPES_ALLOWED, MAX_FILE_SIZE));

View File

@ -1,6 +1,4 @@
import _ from 'lodash';
import logger from '/imports/startup/client/logger';
import { notify } from '/imports/ui/services/notification';
const MIME_TYPES_ALLOWED = ['image/png', 'image/jpeg'];
const MAX_FILE_SIZE = 5000; // KBytes
@ -29,18 +27,16 @@ const withObjectStore = ({
const genericErrorHandlerBuilder = (
code,
errorMessage,
notifyMessage,
message,
callback,
) => (e) => {
notify(notifyMessage, 'error', 'warning');
logger.warn({
logCode: code,
extraInfo: {
errorName: e.name,
errorMessage: e.message,
},
}, `${errorMessage}: ${e.message}`);
}, `${message}: ${e.message}`);
if (callback) callback(e);
};
@ -49,8 +45,7 @@ const load = (onError, onSuccess) => {
withObjectStore({
onError: genericErrorHandlerBuilder(
'IndexedDB_access',
'Error on load custom backgrounds to IndexedDB',
'Something wrong while loading custom backgrounds',
'Error on load custom backgrounds from IndexedDB',
onError,
),
onSuccess: (objectStore) => {
@ -70,12 +65,12 @@ const load = (onError, onSuccess) => {
});
};
const save = (background) => {
const save = (background, onError) => {
withObjectStore({
onError: genericErrorHandlerBuilder(
'IndexedDB_access',
'Error on save custom background to IndexedDB',
'Something wrong while saving custom background',
onError,
),
onSuccess: (objectStore) => {
objectStore.add(background);
@ -83,12 +78,12 @@ const save = (background) => {
});
};
const del = (key) => {
const del = (key, onError) => {
withObjectStore({
onError: genericErrorHandlerBuilder(
'IndexedDB_access',
'Error on delete custom background to IndexedDB',
'Something wrong while deleting custom background',
'Error on delete custom background from IndexedDB',
onError,
),
onSuccess: (objectStore) => {
objectStore.delete(key);
@ -96,44 +91,10 @@ const del = (key) => {
});
};
const parseFilename = (filename = '') => {
const substrings = filename.split('.');
substrings.pop();
const filenameWithoutExtension = substrings.join('');
return filenameWithoutExtension;
};
const readFile = (file, onSuccess, onError) => {
const { name, size, type } = file;
const sizeInKB = size / 1024;
if (sizeInKB > MAX_FILE_SIZE) {
return onError(new Error('Maximum file size exceeded.'));
}
if (!MIME_TYPES_ALLOWED.includes(type)) {
return onError(new Error('File type not allowed.'));
}
const filenameWithoutExtension = parseFilename(name);
const reader = new FileReader();
reader.onload = function (e) {
const background = {
filename: filenameWithoutExtension,
data: e.target.result,
uniqueId: _.uniqueId(),
};
onSuccess(background);
}
reader.readAsDataURL(file);
};
export default {
load,
save,
del,
readFile,
MIME_TYPES_ALLOWED,
MAX_FILE_SIZE,
};

View File

@ -23,7 +23,7 @@ const VideoListItem = (props) => {
const {
name, voiceUser, isFullscreenContext, layoutContextDispatch, user, onHandleVideoFocus,
cameraId, numOfStreams, focused, onVideoItemMount, onVideoItemUnmount, onVirtualBgDrop,
makeDragOperations, isRTL
makeDragOperations, dragging, draggingOver, isRTL
} = props;
const [videoIsReady, setVideoIsReady] = useState(false);
@ -201,7 +201,11 @@ const VideoListItem = (props) => {
fullscreen={isFullscreenContext}
data-test={talking ? 'webcamItemTalkingUser' : 'webcamItem'}
animations={animations}
{...makeDragOperations(onVirtualBgDrop, user?.userId)}
{...{
...makeDragOperations(onVirtualBgDrop, user?.userId),
dragging,
draggingOver,
}}
>
{
videoIsReady

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useState, useCallback } from 'react';
import { injectIntl, defineMessages } from 'react-intl';
import Auth from '/imports/ui/services/auth';
import ConfirmationModal from '/imports/ui/components/common/modal/confirmation/component';
@ -6,6 +6,10 @@ import { CustomVirtualBackgroundsContext } from '/imports/ui/components/video-pr
import { EFFECT_TYPES } from '/imports/ui/services/virtual-background/service';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import VirtualBgService from '/imports/ui/components/video-preview/virtual-background/service';
import logger from '/imports/startup/client/logger';
import withFileReader from '/imports/ui/components/common/file-reader/component';
const { MIME_TYPES_ALLOWED, MAX_FILE_SIZE } = VirtualBgService;
const intlMessages = defineMessages({
confirmationTitle: {
@ -21,7 +25,7 @@ const intlMessages = defineMessages({
const ENABLE_WEBCAM_BACKGROUND_UPLOAD = Meteor.settings.public.virtualBackgrounds.enableVirtualBackgroundUpload;
const DragAndDrop = (props) => {
const { children, mountModal, intl } = props;
const { children, mountModal, intl, readFile } = props;
const [dragging, setDragging] = useState(false);
const [draggingOver, setDraggingOver] = useState(false);
@ -57,34 +61,38 @@ const DragAndDrop = (props) => {
};
}, []);
const makeDragOperations = (onAction, userId) => {
const makeDragOperations = useCallback((onAction, userId) => {
if (!userId || Auth.userID !== userId || !ENABLE_WEBCAM_BACKGROUND_UPLOAD) return {};
const startAndSaveVirtualBackground = (file) => {
const { readFile } = VirtualBgService;
readFile(
file,
(background) => {
const { filename, data } = background;
onAction(EFFECT_TYPES.IMAGE_TYPE, filename, data).then(() => {
dispatchCustomBackground({
type: 'new',
background,
});
const onSuccess = (background) => {
const { filename, data } = background;
onAction(EFFECT_TYPES.IMAGE_TYPE, filename, data).then(() => {
dispatchCustomBackground({
type: 'new',
background,
});
},
(error) => {
// Add some logging, notification, etc.
}
);
});
};
const onError = (error) => {
logger.warn({
logCode: 'read_file_error',
extraInfo: {
errorName: error.name,
errorMessage: error.message,
},
}, error.message);
};
readFile(file, onSuccess, onError);
};
const onDragOverHandler = (e) => {
resetEvent(e);
setDraggingOver(true);
setDragging(false);
}
};
const onDropHandler = (e) => {
resetEvent(e);
@ -118,18 +126,16 @@ const DragAndDrop = (props) => {
resetEvent(e);
setDragging(false);
setDraggingOver(false);
}
};
return {
onDragOver: onDragOverHandler,
onDrop: onDropHandler,
onDragLeave: onDragLeaveHandler,
dragging,
draggingOver,
};
}
}, [Auth.userID]);
return React.cloneElement(children, { ...props, makeDragOperations })
return React.cloneElement(children, { ...props, dragging, draggingOver, makeDragOperations });
};
const Wrapper = (Component) => (props) => (
@ -138,4 +144,5 @@ const Wrapper = (Component) => (props) => (
</DragAndDrop>
);
export const withDragAndDrop = (Component) => withModalMounter(injectIntl(Wrapper(Component)));
export const withDragAndDrop = (Component) =>
withModalMounter(injectIntl(withFileReader(Wrapper(Component), MIME_TYPES_ALLOWED, MAX_FILE_SIZE)));

View File

@ -859,6 +859,11 @@
"app.video.virtualBackground.remove": "Remove added image",
"app.video.virtualBackground.genericError": "Failed to apply camera effect. Try again.",
"app.video.virtualBackground.camBgAriaDesc": "Sets webcam virtual background to {0}",
"app.video.virtualBackground.maximumFileSizeExceeded": "Maximum file size exceeded. ({0}MB)",
"app.video.virtualBackground.typeNotAllowed": "File type not allowed.",
"app.video.virtualBackground.errorOnRead": "Something went wrong when reading the file.",
"app.video.virtualBackground.uploaded": "Uploaded",
"app.video.virtualBackground.uploading": "Uploading...",
"app.video.camCapReached": "You cannot share more cameras",
"app.video.meetingCamCapReached": "Meeting reached it's simultaneous cameras limit",
"app.video.dropZoneLabel": "Drop here",