/* eslint-disable lines-around-comment */ import Spinner from '@atlaskit/spinner'; // @ts-ignore import Bourne from '@hapi/bourne'; // @ts-ignore import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage'; import React, { useCallback, useEffect, useState } from 'react'; import { WithTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { makeStyles } from 'tss-react/mui'; import { IReduxState } from '../../app/types'; import { getMultipleVideoSendingSupportFeatureFlag } from '../../base/config/functions.any'; import { translate } from '../../base/i18n/functions'; import Icon from '../../base/icons/components/Icon'; import { IconCloseLarge } from '../../base/icons/svg'; import { withPixelLineHeight } from '../../base/styles/functions.web'; // @ts-ignore import { Tooltip } from '../../base/tooltip'; import { BACKGROUNDS_LIMIT, IMAGES, type Image, VIRTUAL_BACKGROUND_TYPE } from '../constants'; import { toDataURL } from '../functions'; import logger from '../logger'; import UploadImageButton from './UploadImageButton'; import VirtualBackgroundPreview from './VirtualBackgroundPreview'; /* eslint-enable lines-around-comment */ interface IProps extends WithTranslation { /** * The list of Images to choose from. */ _images: Array; /** * Returns the jitsi track that will have background effect applied. */ _jitsiTrack: Object; /** * The current local flip x status. */ _localFlipX: boolean; /** * Whether or not multi-stream send support is enabled. */ _multiStreamModeEnabled: boolean; /** * If the upload button should be displayed or not. */ _showUploadButton: boolean; /** * Returns the selected virtual background object. */ _virtualBackground: any; /** * The redux {@code dispatch} function. */ dispatch: Function; /** * The initial options copied in the state for the {@code VirtualBackground} component. * * NOTE: currently used only for electron in order to open the dialog in the correct state after desktop sharing * selection. */ initialOptions?: Object; /** * Options change handler. */ onOptionsChange: Function; /** * Virtual background options. */ options: any; /** * Returns the selected thumbnail identifier. */ selectedThumbnail: string; } const onError = (event: any) => { event.target.style.display = 'none'; }; const useStyles = makeStyles()(theme => { return { virtualBackgroundLoading: { width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', height: '50px' }, container: { width: '100%', display: 'flex', flexDirection: 'column' }, thumbnailContainer: { width: '100%', display: 'inline-grid', gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr', gap: theme.spacing(1), '@media (min-width: 608px) and (max-width: 712px)': { gridTemplateColumns: '1fr 1fr 1fr 1fr' }, '@media (max-width: 607px)': { gridTemplateColumns: '1fr 1fr 1fr', gap: theme.spacing(2) } }, thumbnail: { height: '54px', width: '100%', borderRadius: '4px', boxSizing: 'border-box', display: 'flex', alignItems: 'center', justifyContent: 'center', ...withPixelLineHeight(theme.typography.labelBold), color: theme.palette.text01, objectFit: 'cover', [[ '&:hover', '&:focus' ] as any]: { opacity: 0.5, cursor: 'pointer', '& ~ .delete-image-icon': { display: 'block' } }, '@media (max-width: 607px)': { height: '70px' } }, selectedThumbnail: { border: `2px solid ${theme.palette.action01Hover}` }, noneThumbnail: { backgroundColor: theme.palette.ui04 }, slightBlur: { boxShadow: 'inset 0 0 12px #000000', background: '#a4a4a4' }, blur: { boxShadow: 'inset 0 0 12px #000000', background: '#7e8287' }, storedImageContainer: { position: 'relative', display: 'flex', flexDirection: 'column', '&:focus-within .delete-image-container': { display: 'block' } }, deleteImageIcon: { position: 'absolute', top: '3px', right: '3px', background: theme.palette.ui03, borderRadius: '3px', cursor: 'pointer', display: 'none', '@media (max-width: 607px)': { display: 'block', padding: '3px' }, [[ '&:hover', '&:focus' ] as any]: { display: 'block' } } }; }); /** * Renders virtual background dialog. * * @returns {ReactElement} */ function VirtualBackgrounds({ _images, _jitsiTrack, _localFlipX, selectedThumbnail, _showUploadButton, _virtualBackground, onOptionsChange, options, initialOptions, t }: IProps) { const { classes, cx } = useStyles(); const [ previewIsLoaded, setPreviewIsLoaded ] = useState(false); const localImages = jitsiLocalStorage.getItem('virtualBackgrounds'); const [ storedImages, setStoredImages ] = useState>((localImages && Bourne.parse(localImages)) || []); const [ loading, setLoading ] = useState(false); useEffect(() => { onOptionsChange({ ...initialOptions }); }, []); const deleteStoredImage = useCallback(e => { const imageId = e.currentTarget.getAttribute('data-imageid'); setStoredImages(storedImages.filter(item => item.id !== imageId)); }, [ storedImages ]); const deleteStoredImageKeyPress = useCallback(e => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); deleteStoredImage(e); } }, [ deleteStoredImage ]); /** * Updates stored images on local storage. */ useEffect(() => { try { jitsiLocalStorage.setItem('virtualBackgrounds', JSON.stringify(storedImages)); } catch (err) { // Preventing localStorage QUOTA_EXCEEDED_ERR err && setStoredImages(storedImages.slice(1)); } if (storedImages.length === BACKGROUNDS_LIMIT) { setStoredImages(storedImages.slice(1)); } }, [ storedImages ]); const enableBlur = useCallback(async () => { onOptionsChange({ backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR, enabled: true, blurValue: 25, selectedThumbnail: 'blur' }); logger.info('"Blur" option set for virtual background preview!'); }, []); const enableBlurKeyPress = useCallback(e => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); enableBlur(); } }, [ enableBlur ]); const enableSlideBlur = useCallback(async () => { onOptionsChange({ backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR, enabled: true, blurValue: 8, selectedThumbnail: 'slight-blur' }); logger.info('"Slight-blur" option set for virtual background preview!'); }, []); const enableSlideBlurKeyPress = useCallback(e => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); enableSlideBlur(); } }, [ enableSlideBlur ]); const removeBackground = useCallback(async () => { onOptionsChange({ enabled: false, selectedThumbnail: 'none' }); logger.info('"None" option set for virtual background preview!'); }, []); const removeBackgroundKeyPress = useCallback(e => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); removeBackground(); } }, [ removeBackground ]); const setUploadedImageBackground = useCallback(async e => { const imageId = e.currentTarget.getAttribute('data-imageid'); const image = storedImages.find(img => img.id === imageId); if (image) { onOptionsChange({ backgroundType: 'image', enabled: true, url: image.src, selectedThumbnail: image.id }); logger.info('Uploaded image set for virtual background preview!'); } }, [ storedImages ]); const setImageBackground = useCallback(async e => { const imageId = e.currentTarget.getAttribute('data-imageid'); const image = _images.find(img => img.id === imageId); if (image) { try { const url = await toDataURL(image.src); onOptionsChange({ backgroundType: 'image', enabled: true, url, selectedThumbnail: image.id }); logger.info('Image set for virtual background preview!'); } catch (err) { logger.error('Could not fetch virtual background image:', err); } setLoading(false); } }, []); const setImageBackgroundKeyPress = useCallback(e => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); setImageBackground(e); } }, [ setImageBackground ]); const setUploadedImageBackgroundKeyPress = useCallback(e => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); setUploadedImageBackground(e); } }, [ setUploadedImageBackground ]); const loadedPreviewState = useCallback(async loaded => { await setPreviewIsLoaded(loaded); }, []); return ( <> {loading ? (
) : (
{_showUploadButton && }
{t('virtualBackground.none')}
{t('virtualBackground.slightBlur')}
{t('virtualBackground.blur')}
{_images.map(image => ( { ))} {storedImages.map((image, index) => (
{
))}
)} ); } /** * Maps (parts of) the redux state to the associated props for the * {@code VirtualBackground} component. * * @param {Object} state - The Redux state. * @private * @returns {{Props}} */ function _mapStateToProps(state: IReduxState) { const { localFlipX } = state['features/base/settings']; const dynamicBrandingImages = state['features/dynamic-branding'].virtualBackgrounds; const hasBrandingImages = Boolean(dynamicBrandingImages.length); return { _localFlipX: Boolean(localFlipX), _images: (hasBrandingImages && dynamicBrandingImages) || IMAGES, _virtualBackground: state['features/virtual-background'], _showUploadButton: !state['features/base/config'].disableAddingBackgroundImages, _multiStreamModeEnabled: getMultipleVideoSendingSupportFeatureFlag(state) }; } export default connect(_mapStateToProps)(translate(VirtualBackgrounds));