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 26KB

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