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.tsx 27KB

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