Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

VirtualBackgroundDialog.tsx 22KB

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