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

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