You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

VirtualBackgroundDialog.js 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. // @flow
  2. import Spinner from '@atlaskit/spinner';
  3. import Bourne from '@hapi/bourne';
  4. import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
  5. import { makeStyles } from '@material-ui/styles';
  6. import clsx from 'clsx';
  7. import React, { useState, useEffect, useCallback } from 'react';
  8. import { useSelector } from 'react-redux';
  9. import { Dialog, hideDialog, openDialog } from '../../base/dialog';
  10. import { translate } from '../../base/i18n';
  11. import { Icon, IconCloseSmall, IconShareDesktop } from '../../base/icons';
  12. import { browser, JitsiTrackErrors } from '../../base/lib-jitsi-meet';
  13. import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
  14. import { VIDEO_TYPE } from '../../base/media';
  15. import { connect } from '../../base/redux';
  16. import { updateSettings } from '../../base/settings';
  17. import { Tooltip } from '../../base/tooltip';
  18. import { getLocalVideoTrack } from '../../base/tracks';
  19. import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification } from '../../notifications';
  20. import { toggleBackgroundEffect, virtualBackgroundTrackChanged } from '../actions';
  21. import { IMAGES, BACKGROUNDS_LIMIT, VIRTUAL_BACKGROUND_TYPE, type Image } from '../constants';
  22. import { toDataURL } from '../functions';
  23. import logger from '../logger';
  24. import UploadImageButton from './UploadImageButton';
  25. import VirtualBackgroundPreview from './VirtualBackgroundPreview';
  26. type Props = {
  27. /**
  28. * The list of Images to choose from.
  29. */
  30. _images: Array<Image>,
  31. /**
  32. * The current local flip x status.
  33. */
  34. _localFlipX: boolean,
  35. /**
  36. * Returns the jitsi track that will have backgraund effect applied.
  37. */
  38. _jitsiTrack: Object,
  39. /**
  40. * Returns the selected thumbnail identifier.
  41. */
  42. _selectedThumbnail: string,
  43. /**
  44. * If the upload button should be displayed or not.
  45. */
  46. _showUploadButton: boolean,
  47. /**
  48. * Returns the selected virtual background object.
  49. */
  50. _virtualBackground: Object,
  51. /**
  52. * The redux {@code dispatch} function.
  53. */
  54. dispatch: Function,
  55. /**
  56. * The initial options copied in the state for the {@code VirtualBackground} component.
  57. *
  58. * NOTE: currently used only for electron in order to open the dialog in the correct state after desktop sharing
  59. * selection.
  60. */
  61. initialOptions: Object,
  62. /**
  63. * Invoked to obtain translated strings.
  64. */
  65. t: Function
  66. };
  67. const onError = event => {
  68. event.target.style.display = 'none';
  69. };
  70. /**
  71. * Maps (parts of) the redux state to the associated props for the
  72. * {@code VirtualBackground} component.
  73. *
  74. * @param {Object} state - The Redux state.
  75. * @private
  76. * @returns {{Props}}
  77. */
  78. function _mapStateToProps(state): Object {
  79. const { localFlipX } = state['features/base/settings'];
  80. const dynamicBrandingImages = state['features/dynamic-branding'].virtualBackgrounds;
  81. const hasBrandingImages = Boolean(dynamicBrandingImages.length);
  82. return {
  83. _localFlipX: Boolean(localFlipX),
  84. _images: (hasBrandingImages && dynamicBrandingImages) || IMAGES,
  85. _virtualBackground: state['features/virtual-background'],
  86. _selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
  87. _showUploadButton: !(hasBrandingImages || state['features/base/config'].disableAddingBackgroundImages),
  88. _jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack
  89. };
  90. }
  91. const VirtualBackgroundDialog = translate(connect(_mapStateToProps)(VirtualBackground));
  92. const useStyles = makeStyles(theme => {
  93. return {
  94. dialog: {
  95. marginLeft: '-10px',
  96. position: 'relative',
  97. maxHeight: '300px',
  98. color: 'white',
  99. display: 'inline-grid',
  100. gridTemplateColumns: 'auto auto auto auto auto',
  101. columnGap: '9px',
  102. cursor: 'pointer',
  103. [[ '& .desktop-share:hover',
  104. '& .thumbnail:hover',
  105. '& .blur:hover',
  106. '& .slight-blur:hover',
  107. '& .virtual-background-none:hover' ]]: {
  108. opacity: 0.5,
  109. border: '2px solid #99bbf3'
  110. },
  111. '& .background-option': {
  112. marginTop: `${theme.spacing(2)}px`,
  113. borderRadius: `${theme.shape.borderRadius}px`,
  114. height: '60px',
  115. width: '107px',
  116. textAlign: 'center',
  117. justifyContent: 'center',
  118. fontWeight: 'bold',
  119. boxSizing: 'border-box',
  120. display: 'flex',
  121. alignItems: 'center'
  122. },
  123. '& thumbnail-container': {
  124. position: 'relative',
  125. '&:focus-within .thumbnail ~ .delete-image-icon': {
  126. display: 'block'
  127. }
  128. },
  129. '& .thumbnail': {
  130. objectFit: 'cover'
  131. },
  132. '& .thumbnail:hover ~ .delete-image-icon': {
  133. display: 'block'
  134. },
  135. '& .thumbnail-selected': {
  136. objectFit: 'cover',
  137. border: '2px solid #246fe5'
  138. },
  139. '& .blur': {
  140. boxShadow: 'inset 0 0 12px #000000',
  141. background: '#7e8287',
  142. padding: '0 10px'
  143. },
  144. '& .blur-selected': {
  145. border: '2px solid #246fe5'
  146. },
  147. '& .slight-blur': {
  148. boxShadow: 'inset 0 0 12px #000000',
  149. background: '#a4a4a4',
  150. padding: '0 10px'
  151. },
  152. '& .slight-blur-selected': {
  153. border: '2px solid #246fe5'
  154. },
  155. '& .virtual-background-none': {
  156. background: '#525252',
  157. padding: '0 10px'
  158. },
  159. '& .none-selected': {
  160. border: '2px solid #246fe5'
  161. },
  162. '& .desktop-share': {
  163. background: '#525252'
  164. },
  165. '& .desktop-share-selected': {
  166. border: '2px solid #246fe5',
  167. padding: '0 10px'
  168. },
  169. '& delete-image-icon': {
  170. background: '#3d3d3d',
  171. position: 'absolute',
  172. display: 'none',
  173. left: '96px',
  174. bottom: '51px',
  175. '&:hover': {
  176. display: 'block'
  177. },
  178. '@media (max-width: 632px)': {
  179. left: '51px'
  180. }
  181. },
  182. '@media (max-width: 360px)': {
  183. gridTemplateColumns: 'auto auto auto'
  184. },
  185. '@media (max-width: 632px)': {
  186. fontSize: '1.5vw',
  187. [[ '& .desktop-share:hover',
  188. '& .thumbnail:hover',
  189. '& .blur:hover',
  190. '& .slight-blur:hover',
  191. '& .virtual-background-none:hover' ]]: {
  192. height: '60px',
  193. width: '60px'
  194. },
  195. [[ '& .desktop-share',
  196. '& .virtual-background-none,',
  197. '& .thumbnail,',
  198. '& .blur,',
  199. '& .slight-blur' ]]: {
  200. height: '60px',
  201. width: '60px'
  202. },
  203. [[ '& .desktop-share-selected',
  204. '& .thumbnail-selected',
  205. '& .none-selected',
  206. '& .blur-selected',
  207. '& .slight-blur-selected' ]]: {
  208. height: '60px',
  209. width: '60px'
  210. }
  211. }
  212. },
  213. dialogMarginTop: {
  214. marginTop: '44px'
  215. },
  216. virtualBackgroundLoading: {
  217. overflow: 'hidden',
  218. position: 'fixed',
  219. left: '50%',
  220. marginTop: '10px',
  221. transform: 'translateX(-50%)'
  222. }
  223. };
  224. });
  225. /**
  226. * Renders virtual background dialog.
  227. *
  228. * @returns {ReactElement}
  229. */
  230. function VirtualBackground({
  231. _images,
  232. _jitsiTrack,
  233. _localFlipX,
  234. _selectedThumbnail,
  235. _showUploadButton,
  236. _virtualBackground,
  237. dispatch,
  238. initialOptions,
  239. t
  240. }: Props) {
  241. const classes = useStyles();
  242. const [ previewIsLoaded, setPreviewIsLoaded ] = useState(false);
  243. const [ options, setOptions ] = useState({ ...initialOptions });
  244. const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
  245. const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && Bourne.parse(localImages)) || []);
  246. const [ loading, setLoading ] = useState(false);
  247. const { disableScreensharingVirtualBackground } = useSelector(state => state['features/base/config']);
  248. const [ activeDesktopVideo ] = useState(_virtualBackground?.virtualSource?.videoType === VIDEO_TYPE.DESKTOP
  249. ? _virtualBackground.virtualSource
  250. : null);
  251. const [ initialVirtualBackground ] = useState(_virtualBackground);
  252. const deleteStoredImage = useCallback(e => {
  253. const imageId = e.currentTarget.getAttribute('data-imageid');
  254. setStoredImages(storedImages.filter(item => item.id !== imageId));
  255. }, [ storedImages ]);
  256. const deleteStoredImageKeyPress = useCallback(e => {
  257. if (e.key === ' ' || e.key === 'Enter') {
  258. e.preventDefault();
  259. deleteStoredImage(e);
  260. }
  261. }, [ deleteStoredImage ]);
  262. /**
  263. * Updates stored images on local storage.
  264. */
  265. useEffect(() => {
  266. try {
  267. jitsiLocalStorage.setItem('virtualBackgrounds', JSON.stringify(storedImages));
  268. } catch (err) {
  269. // Preventing localStorage QUOTA_EXCEEDED_ERR
  270. err && setStoredImages(storedImages.slice(1));
  271. }
  272. if (storedImages.length === BACKGROUNDS_LIMIT) {
  273. setStoredImages(storedImages.slice(1));
  274. }
  275. }, [ storedImages ]);
  276. const enableBlur = useCallback(async () => {
  277. setOptions({
  278. backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
  279. enabled: true,
  280. blurValue: 25,
  281. selectedThumbnail: 'blur'
  282. });
  283. logger.info('"Blur" option setted for virtual background preview!');
  284. }, []);
  285. const enableBlurKeyPress = useCallback(e => {
  286. if (e.key === ' ' || e.key === 'Enter') {
  287. e.preventDefault();
  288. enableBlur();
  289. }
  290. }, [ enableBlur ]);
  291. const enableSlideBlur = useCallback(async () => {
  292. setOptions({
  293. backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
  294. enabled: true,
  295. blurValue: 8,
  296. selectedThumbnail: 'slight-blur'
  297. });
  298. logger.info('"Slight-blur" option setted for virtual background preview!');
  299. }, []);
  300. const enableSlideBlurKeyPress = useCallback(e => {
  301. if (e.key === ' ' || e.key === 'Enter') {
  302. e.preventDefault();
  303. enableSlideBlur();
  304. }
  305. }, [ enableSlideBlur ]);
  306. const shareDesktop = useCallback(async () => {
  307. if (disableScreensharingVirtualBackground) {
  308. return;
  309. }
  310. let isCancelled = false, url;
  311. try {
  312. url = await createLocalTrack('desktop', '');
  313. } catch (e) {
  314. if (e.name === JitsiTrackErrors.SCREENSHARING_USER_CANCELED) {
  315. isCancelled = true;
  316. } else {
  317. logger.error(e);
  318. }
  319. }
  320. if (!url) {
  321. if (!isCancelled) {
  322. dispatch(showErrorNotification({
  323. titleKey: 'virtualBackground.desktopShareError'
  324. }, NOTIFICATION_TIMEOUT_TYPE.LONG));
  325. logger.error('Could not create desktop share as a virtual background!');
  326. }
  327. /**
  328. * For electron createLocalTrack will open the {@code DesktopPicker} dialog and hide the
  329. * {@code VirtualBackgroundDialog}. That's why we need to reopen the {@code VirtualBackgroundDialog}
  330. * and restore the current state through {@code initialOptions} prop.
  331. */
  332. if (browser.isElectron()) {
  333. dispatch(openDialog(VirtualBackgroundDialog, { initialOptions: options }));
  334. }
  335. return;
  336. }
  337. const newOptions = {
  338. backgroundType: VIRTUAL_BACKGROUND_TYPE.DESKTOP_SHARE,
  339. enabled: true,
  340. selectedThumbnail: 'desktop-share',
  341. url
  342. };
  343. /**
  344. * For electron createLocalTrack will open the {@code DesktopPicker} dialog and hide the
  345. * {@code VirtualBackgroundDialog}. That's why we need to reopen the {@code VirtualBackgroundDialog}
  346. * and force it to show desktop share virtual background through {@code initialOptions} prop.
  347. */
  348. if (browser.isElectron()) {
  349. dispatch(openDialog(VirtualBackgroundDialog, { initialOptions: newOptions }));
  350. } else {
  351. setOptions(newOptions);
  352. logger.info('"Desktop-share" option setted for virtual background preview!');
  353. }
  354. }, [ dispatch, options ]);
  355. const shareDesktopKeyPress = useCallback(e => {
  356. if (e.key === ' ' || e.key === 'Enter') {
  357. e.preventDefault();
  358. shareDesktop();
  359. }
  360. }, [ shareDesktop ]);
  361. const removeBackground = useCallback(async () => {
  362. setOptions({
  363. enabled: false,
  364. selectedThumbnail: 'none'
  365. });
  366. logger.info('"None" option setted for virtual background preview!');
  367. }, []);
  368. const removeBackgroundKeyPress = useCallback(e => {
  369. if (e.key === ' ' || e.key === 'Enter') {
  370. e.preventDefault();
  371. removeBackground();
  372. }
  373. }, [ removeBackground ]);
  374. const setUploadedImageBackground = useCallback(async e => {
  375. const imageId = e.currentTarget.getAttribute('data-imageid');
  376. const image = storedImages.find(img => img.id === imageId);
  377. if (image) {
  378. setOptions({
  379. backgroundType: 'image',
  380. enabled: true,
  381. url: image.src,
  382. selectedThumbnail: image.id
  383. });
  384. logger.info('Uploaded image setted for virtual background preview!');
  385. }
  386. }, [ storedImages ]);
  387. const setImageBackground = useCallback(async e => {
  388. const imageId = e.currentTarget.getAttribute('data-imageid');
  389. const image = _images.find(img => img.id === imageId);
  390. if (image) {
  391. try {
  392. const url = await toDataURL(image.src);
  393. setOptions({
  394. backgroundType: 'image',
  395. enabled: true,
  396. url,
  397. selectedThumbnail: image.id
  398. });
  399. logger.info('Image set for virtual background preview!');
  400. } catch (err) {
  401. logger.error('Could not fetch virtual background image:', err);
  402. }
  403. setLoading(false);
  404. }
  405. }, []);
  406. const setImageBackgroundKeyPress = useCallback(e => {
  407. if (e.key === ' ' || e.key === 'Enter') {
  408. e.preventDefault();
  409. setImageBackground(e);
  410. }
  411. }, [ setImageBackground ]);
  412. const setUploadedImageBackgroundKeyPress = useCallback(e => {
  413. if (e.key === ' ' || e.key === 'Enter') {
  414. e.preventDefault();
  415. setUploadedImageBackground(e);
  416. }
  417. }, [ setUploadedImageBackground ]);
  418. const applyVirtualBackground = useCallback(async () => {
  419. if (activeDesktopVideo) {
  420. await activeDesktopVideo.dispose();
  421. }
  422. setLoading(true);
  423. await dispatch(toggleBackgroundEffect(options, _jitsiTrack));
  424. await setLoading(false);
  425. if (_localFlipX && options.backgroundType === VIRTUAL_BACKGROUND_TYPE.DESKTOP_SHARE) {
  426. dispatch(updateSettings({
  427. localFlipX: !_localFlipX
  428. }));
  429. } else {
  430. // Set x scale to default value.
  431. dispatch(updateSettings({
  432. localFlipX: true
  433. }));
  434. }
  435. dispatch(hideDialog());
  436. logger.info(`Virtual background type: '${typeof options.backgroundType === 'undefined'
  437. ? 'none' : options.backgroundType}' applied!`);
  438. dispatch(virtualBackgroundTrackChanged());
  439. }, [ dispatch, options, _localFlipX ]);
  440. // Prevent the selection of a new virtual background if it has not been applied by default
  441. const cancelVirtualBackground = useCallback(async () => {
  442. await setOptions({
  443. backgroundType: initialVirtualBackground.backgroundType,
  444. enabled: initialVirtualBackground.backgroundEffectEnabled,
  445. url: initialVirtualBackground.virtualSource,
  446. selectedThumbnail: initialVirtualBackground.selectedThumbnail,
  447. blurValue: initialVirtualBackground.blurValue
  448. });
  449. dispatch(hideDialog());
  450. });
  451. const loadedPreviewState = useCallback(async loaded => {
  452. await setPreviewIsLoaded(loaded);
  453. });
  454. return (
  455. <Dialog
  456. hideCancelButton = { false }
  457. okKey = { 'virtualBackground.apply' }
  458. onCancel = { cancelVirtualBackground }
  459. onSubmit = { applyVirtualBackground }
  460. submitDisabled = { !options || loading || !previewIsLoaded }
  461. titleKey = { 'virtualBackground.title' } >
  462. <VirtualBackgroundPreview
  463. loadedPreview = { loadedPreviewState }
  464. options = { options } />
  465. {loading ? (
  466. <div className = { classes.virtualBackgroundLoading }>
  467. <Spinner
  468. isCompleting = { false }
  469. size = 'medium' />
  470. </div>
  471. ) : (
  472. <div>
  473. {_showUploadButton
  474. && <UploadImageButton
  475. setLoading = { setLoading }
  476. setOptions = { setOptions }
  477. setStoredImages = { setStoredImages }
  478. showLabel = { previewIsLoaded }
  479. storedImages = { storedImages } />}
  480. <div
  481. className = { clsx(classes.dialog, { [classes.dialogMarginTop]: previewIsLoaded }) }
  482. role = 'radiogroup'
  483. tabIndex = '-1'>
  484. <Tooltip
  485. content = { t('virtualBackground.removeBackground') }
  486. position = { 'top' }>
  487. <div
  488. aria-checked = { _selectedThumbnail === 'none' }
  489. aria-label = { t('virtualBackground.removeBackground') }
  490. className = { clsx('background-option', 'virtual-background-none', {
  491. 'none-selected': _selectedThumbnail === 'none'
  492. }) }
  493. onClick = { removeBackground }
  494. onKeyPress = { removeBackgroundKeyPress }
  495. role = 'radio'
  496. tabIndex = { 0 } >
  497. {t('virtualBackground.none')}
  498. </div>
  499. </Tooltip>
  500. <Tooltip
  501. content = { t('virtualBackground.slightBlur') }
  502. position = { 'top' }>
  503. <div
  504. aria-checked = { _selectedThumbnail === 'slight-blur' }
  505. aria-label = { t('virtualBackground.slightBlur') }
  506. className = { clsx('background-option', 'slight-blur', {
  507. 'slight-blur-selected': _selectedThumbnail === 'slight-blur'
  508. }) }
  509. onClick = { enableSlideBlur }
  510. onKeyPress = { enableSlideBlurKeyPress }
  511. role = 'radio'
  512. tabIndex = { 0 }>
  513. {t('virtualBackground.slightBlur')}
  514. </div>
  515. </Tooltip>
  516. <Tooltip
  517. content = { t('virtualBackground.blur') }
  518. position = { 'top' }>
  519. <div
  520. aria-checked = { _selectedThumbnail === 'blur' }
  521. aria-label = { t('virtualBackground.blur') }
  522. className = { clsx('background-option', 'blur', {
  523. 'blur-selected': _selectedThumbnail === 'blur'
  524. }) }
  525. onClick = { enableBlur }
  526. onKeyPress = { enableBlurKeyPress }
  527. role = 'radio'
  528. tabIndex = { 0 }>
  529. {t('virtualBackground.blur')}
  530. </div>
  531. </Tooltip>
  532. {!disableScreensharingVirtualBackground && (
  533. <Tooltip
  534. content = { t('virtualBackground.desktopShare') }
  535. position = { 'top' }>
  536. <div
  537. aria-checked = { _selectedThumbnail === 'desktop-share' }
  538. aria-label = { t('virtualBackground.desktopShare') }
  539. className = { clsx('background-option', 'desktop-share', {
  540. 'desktop-share-selected': _selectedThumbnail === 'desktop-share'
  541. }) }
  542. onClick = { shareDesktop }
  543. onKeyPress = { shareDesktopKeyPress }
  544. role = 'radio'
  545. tabIndex = { 0 }>
  546. <Icon
  547. className = 'share-desktop-icon'
  548. size = { 30 }
  549. src = { IconShareDesktop } />
  550. </div>
  551. </Tooltip>
  552. )}
  553. {_images.map(image => (
  554. <Tooltip
  555. content = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
  556. key = { image.id }
  557. position = { 'top' }>
  558. <img
  559. alt = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
  560. aria-checked = { options.selectedThumbnail === image.id
  561. || _selectedThumbnail === image.id }
  562. className = {
  563. options.selectedThumbnail === image.id || _selectedThumbnail === image.id
  564. ? 'background-option thumbnail-selected' : 'background-option thumbnail' }
  565. data-imageid = { image.id }
  566. onClick = { setImageBackground }
  567. onError = { onError }
  568. onKeyPress = { setImageBackgroundKeyPress }
  569. role = 'radio'
  570. src = { image.src }
  571. tabIndex = { 0 } />
  572. </Tooltip>
  573. ))}
  574. {storedImages.map((image, index) => (
  575. <div
  576. className = { 'thumbnail-container' }
  577. key = { image.id }>
  578. <img
  579. alt = { t('virtualBackground.uploadedImage', { index: index + 1 }) }
  580. aria-checked = { _selectedThumbnail === image.id }
  581. className = { clsx('background-option', {
  582. 'thumbnail-selected': _selectedThumbnail === image.id,
  583. 'thumbnail': _selectedThumbnail !== image.id
  584. }) }
  585. data-imageid = { image.id }
  586. onClick = { setUploadedImageBackground }
  587. onError = { onError }
  588. onKeyPress = { setUploadedImageBackgroundKeyPress }
  589. role = 'radio'
  590. src = { image.src }
  591. tabIndex = { 0 } />
  592. <Icon
  593. ariaLabel = { t('virtualBackground.deleteImage') }
  594. className = { 'delete-image-icon' }
  595. data-imageid = { image.id }
  596. onClick = { deleteStoredImage }
  597. onKeyPress = { deleteStoredImageKeyPress }
  598. role = 'button'
  599. size = { 15 }
  600. src = { IconCloseSmall }
  601. tabIndex = { 0 } />
  602. </div>
  603. ))}
  604. </div>
  605. </div>
  606. )}
  607. </Dialog>
  608. );
  609. }
  610. export default VirtualBackgroundDialog;