improvement(virtual background): better error handling, notifications, performance
This commit is contained in:
parent
3b820b60a9
commit
aaf7f8e78e
@ -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;
|
@ -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,
|
||||
};
|
@ -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));
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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)));
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user