feat(virtual-background) Move dialog to SettingsDialog tab (#13005)

Implement redesign
This commit is contained in:
Robert Pintilii 2023-03-08 13:15:07 +02:00 committed by GitHub
parent c8f1690057
commit 8982f17ce1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 423 additions and 334 deletions

View File

@ -1350,7 +1350,7 @@
"none": "None", "none": "None",
"pleaseWait": "Please wait...", "pleaseWait": "Please wait...",
"removeBackground": "Remove background", "removeBackground": "Remove background",
"slightBlur": "Slight Blur", "slightBlur": "Half Blur",
"title": "Virtual backgrounds", "title": "Virtual backgrounds",
"uploadedImage": "Uploaded image {{index}}", "uploadedImage": "Uploaded image {{index}}",
"webAssemblyWarning": "WebAssembly not supported", "webAssemblyWarning": "WebAssembly not supported",

View File

@ -106,6 +106,8 @@ import { startAudioScreenShareFlow, startScreenShareFlow } from '../../react/fea
import { isScreenAudioSupported } from '../../react/features/screen-share/functions'; import { isScreenAudioSupported } from '../../react/features/screen-share/functions';
import { toggleScreenshotCaptureSummary } from '../../react/features/screenshot-capture'; import { toggleScreenshotCaptureSummary } from '../../react/features/screenshot-capture';
import { isScreenshotCaptureEnabled } from '../../react/features/screenshot-capture/functions'; import { isScreenshotCaptureEnabled } from '../../react/features/screenshot-capture/functions';
import SettingsDialog from '../../react/features/settings/components/web/SettingsDialog';
import { SETTINGS_TABS } from '../../react/features/settings/constants';
import { playSharedVideo, stopSharedVideo } from '../../react/features/shared-video/actions.any'; import { playSharedVideo, stopSharedVideo } from '../../react/features/shared-video/actions.any';
import { extractYoutubeIdOrURL } from '../../react/features/shared-video/functions'; import { extractYoutubeIdOrURL } from '../../react/features/shared-video/functions';
import { setRequestingSubtitles, toggleRequestingSubtitles } from '../../react/features/subtitles/actions'; import { setRequestingSubtitles, toggleRequestingSubtitles } from '../../react/features/subtitles/actions';
@ -113,7 +115,6 @@ import { isAudioMuteButtonDisabled } from '../../react/features/toolbox/function
import { setTileView, toggleTileView } from '../../react/features/video-layout'; import { setTileView, toggleTileView } from '../../react/features/video-layout';
import { muteAllParticipants } from '../../react/features/video-menu/actions'; import { muteAllParticipants } from '../../react/features/video-menu/actions';
import { setVideoQuality } from '../../react/features/video-quality'; import { setVideoQuality } from '../../react/features/video-quality';
import VirtualBackgroundDialog from '../../react/features/virtual-background/components/VirtualBackgroundDialog';
import { getJitsiMeetTransport } from '../transport'; import { getJitsiMeetTransport } from '../transport';
import { API_ID, ENDPOINT_TEXT_MESSAGE_NAME } from './constants'; import { API_ID, ENDPOINT_TEXT_MESSAGE_NAME } from './constants';
@ -798,7 +799,8 @@ function initCommands() {
APP.store.dispatch(overwriteConfig(whitelistedConfig)); APP.store.dispatch(overwriteConfig(whitelistedConfig));
}, },
'toggle-virtual-background': () => { 'toggle-virtual-background': () => {
APP.store.dispatch(toggleDialog(VirtualBackgroundDialog)); APP.store.dispatch(toggleDialog(SettingsDialog, {
defaultTab: SETTINGS_TABS.VIRTUAL_BACKGROUND }));
}, },
'end-conference': () => { 'end-conference': () => {
APP.store.dispatch(endConference()); APP.store.dispatch(endConference());

View File

@ -146,6 +146,7 @@ interface IObject {
} }
export interface IDialogTab<P> { export interface IDialogTab<P> {
cancel?: Function;
className?: string; className?: string;
component: ComponentType<any>; component: ComponentType<any>;
icon: Function; icon: Function;
@ -214,7 +215,12 @@ const DialogWithTabs = ({
} }
}, [ isMobile, userSelected, selectedTab ]); }, [ isMobile, userSelected, selectedTab ]);
const onClose = useCallback(() => { const onClose = useCallback((isCancel = true) => {
if (isCancel) {
tabs.forEach(({ cancel }) => {
cancel && dispatch(cancel());
});
}
dispatch(hideDialog()); dispatch(hideDialog());
}, []); }, []);
@ -268,7 +274,7 @@ const DialogWithTabs = ({
tabs.forEach(({ submit }, idx) => { tabs.forEach(({ submit }, idx) => {
submit?.(tabStates[idx]); submit?.(tabStates[idx]);
}); });
onClose(); onClose(false);
}, [ tabs, tabStates ]); }, [ tabs, tabStates ]);
const selectedTabIndex = useMemo(() => { const selectedTabIndex = useMemo(() => {

View File

@ -11,6 +11,8 @@ import {
import { openDialog } from '../base/dialog/actions'; import { openDialog } from '../base/dialog/actions';
import i18next from '../base/i18n/i18next'; import i18next from '../base/i18n/i18next';
import { updateSettings } from '../base/settings/actions'; import { updateSettings } from '../base/settings/actions';
import { toggleBackgroundEffect } from '../virtual-background/actions';
import virtualBackgroundLogger from '../virtual-background/logger';
import { import {
SET_AUDIO_SETTINGS_VISIBILITY, SET_AUDIO_SETTINGS_VISIBILITY,
@ -24,7 +26,8 @@ import {
getMoreTabProps, getMoreTabProps,
getNotificationsTabProps, getNotificationsTabProps,
getProfileTabProps, getProfileTabProps,
getShortcutsTabProps getShortcutsTabProps,
getVirtualBackgroundTabProps
} from './functions'; } from './functions';
/** /**
@ -249,3 +252,31 @@ export function submitShortcutsTab(newState: any) {
} }
}; };
} }
/**
* Submits the settings from the "Virtual Background" tab of the settings dialog.
*
* @param {Object} newState - The new settings.
* @param {boolean} isCancel - Whether the change represents a cancel.
* @returns {Function}
*/
export function submitVirtualBackgroundTab(newState: any, isCancel = false) {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const currentState = getVirtualBackgroundTabProps(getState());
if (newState.options?.selectedThumbnail) {
await dispatch(toggleBackgroundEffect(newState.options, currentState._jitsiTrack));
if (!isCancel) {
// Set x scale to default value.
dispatch(updateSettings({
localFlipX: true
}));
virtualBackgroundLogger.info(`Virtual background type: '${
typeof newState.options.backgroundType === 'undefined'
? 'none' : newState.options.backgroundType}' applied!`);
}
}
};
}

View File

@ -8,6 +8,7 @@ import {
IconCalendar, IconCalendar,
IconGear, IconGear,
IconHost, IconHost,
IconImage,
IconShortcuts, IconShortcuts,
IconUser, IconUser,
IconVideo, IconVideo,
@ -24,12 +25,14 @@ import {
getAudioDeviceSelectionDialogProps, getAudioDeviceSelectionDialogProps,
getVideoDeviceSelectionDialogProps getVideoDeviceSelectionDialogProps
} from '../../../device-selection/functions.web'; } from '../../../device-selection/functions.web';
import { checkBlurSupport } from '../../../virtual-background/functions';
import { import {
submitModeratorTab, submitModeratorTab,
submitMoreTab, submitMoreTab,
submitNotificationsTab, submitNotificationsTab,
submitProfileTab, submitProfileTab,
submitShortcutsTab submitShortcutsTab,
submitVirtualBackgroundTab
} from '../../actions'; } from '../../actions';
import { SETTINGS_TABS } from '../../constants'; import { SETTINGS_TABS } from '../../constants';
import { import {
@ -38,7 +41,8 @@ import {
getNotificationsMap, getNotificationsMap,
getNotificationsTabProps, getNotificationsTabProps,
getProfileTabProps, getProfileTabProps,
getShortcutsTabProps getShortcutsTabProps,
getVirtualBackgroundTabProps
} from '../../functions'; } from '../../functions';
// @ts-ignore // @ts-ignore
@ -48,6 +52,7 @@ import MoreTab from './MoreTab';
import NotificationsTab from './NotificationsTab'; import NotificationsTab from './NotificationsTab';
import ProfileTab from './ProfileTab'; import ProfileTab from './ProfileTab';
import ShortcutsTab from './ShortcutsTab'; import ShortcutsTab from './ShortcutsTab';
import VirtualBackgroundTab from './VirtualBackgroundTab';
/** /**
* The type of the React {@code Component} props of * The type of the React {@code Component} props of
@ -254,6 +259,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
const showSoundsSettings = configuredTabs.includes('sounds'); const showSoundsSettings = configuredTabs.includes('sounds');
const enabledNotifications = getNotificationsMap(state); const enabledNotifications = getNotificationsMap(state);
const showNotificationsSettings = Object.keys(enabledNotifications).length > 0; const showNotificationsSettings = Object.keys(enabledNotifications).length > 0;
const virtualBackgroundSupported = checkBlurSupport();
const tabs: IDialogTab<any>[] = []; const tabs: IDialogTab<any>[] = [];
if (showDeviceSettings) { if (showDeviceSettings) {
@ -305,12 +311,37 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
}); });
} }
if (virtualBackgroundSupported) {
tabs.push({
name: SETTINGS_TABS.VIRTUAL_BACKGROUND,
component: VirtualBackgroundTab,
labelKey: 'virtualBackground.title',
props: getVirtualBackgroundTabProps(state),
className: `settings-pane ${classes.settingsDialog}`,
submit: (newState: any) => submitVirtualBackgroundTab(newState),
cancel: () => {
const { _virtualBackground } = getVirtualBackgroundTabProps(state);
return submitVirtualBackgroundTab({
options: {
backgroundType: _virtualBackground.backgroundType,
enabled: _virtualBackground.backgroundEffectEnabled,
url: _virtualBackground.virtualSource,
selectedThumbnail: _virtualBackground.selectedThumbnail,
blurValue: _virtualBackground.blurValue
}
}, true);
},
icon: IconImage
});
}
if (showSoundsSettings || showNotificationsSettings) { if (showSoundsSettings || showNotificationsSettings) {
tabs.push({ tabs.push({
name: SETTINGS_TABS.NOTIFICATIONS, name: SETTINGS_TABS.NOTIFICATIONS,
component: NotificationsTab, component: NotificationsTab,
labelKey: 'settings.notifications', labelKey: 'settings.notifications',
propsUpdateFunction: (tabState: any, newProps: any) => { propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getNotificationsTabProps>) => {
return { return {
...newProps, ...newProps,
enabledNotifications: tabState?.enabledNotifications || {} enabledNotifications: tabState?.enabledNotifications || {}
@ -373,7 +404,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
component: ShortcutsTab, component: ShortcutsTab,
labelKey: 'settings.shortcuts', labelKey: 'settings.shortcuts',
props: getShortcutsTabProps(state, isDisplayedOnWelcomePage), props: getShortcutsTabProps(state, isDisplayedOnWelcomePage),
propsUpdateFunction: (tabState: any, newProps: any) => { propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getShortcutsTabProps>) => {
// Updates tab props, keeping users selection // Updates tab props, keeping users selection
return { return {

View File

@ -0,0 +1,107 @@
import { withStyles } from '@mui/styles';
import React from 'react';
import { WithTranslation } from 'react-i18next';
import AbstractDialogTab, {
IProps as AbstractDialogTabProps
} from '../../../base/dialog/components/web/AbstractDialogTab';
import { translate } from '../../../base/i18n/functions';
import VirtualBackgrounds from '../../../virtual-background/components/VirtualBackgrounds';
/**
* The type of the React {@code Component} props of {@link VirtualBackgroundTab}.
*/
export interface IProps extends AbstractDialogTabProps, WithTranslation {
/**
* Returns the jitsi track that will have background effect applied.
*/
_jitsiTrack: Object;
/**
* CSS classes object.
*/
classes: any;
/**
* Virtual background options.
*/
options: any;
/**
* The selected thumbnail identifier.
*/
selectedThumbnail: string;
}
const styles = () => {
return {
container: {
width: '100%',
display: 'flex',
flexDirection: 'column' as const
}
};
};
/**
* React {@code Component} for modifying language and moderator settings.
*
* @augments Component
*/
class VirtualBackgroundTab extends AbstractDialogTab<IProps, any> {
/**
* Initializes a new {@code ModeratorTab} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onOptionsChanged = this._onOptionsChanged.bind(this);
}
/**
* Callback invoked to select if follow-me mode
* should be activated.
*
* @param {Object} options - The new background options.
*
* @returns {void}
*/
_onOptionsChanged(options: any) {
super._onChange({ options });
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
classes,
options,
selectedThumbnail,
_jitsiTrack
} = this.props;
return (
<div
className = { classes.container }
id = 'virtual-background-dialog'
key = 'virtual-background'>
<VirtualBackgrounds
_jitsiTrack = { _jitsiTrack }
onOptionsChange = { this._onOptionsChanged }
options = { options }
selectedThumbnail = { selectedThumbnail } />
</div>
);
}
}
export default withStyles(styles)(translate(VirtualBackgroundTab));

View File

@ -3,7 +3,6 @@ import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../../app/types'; import { IReduxState, IStore } from '../../../../app/types';
import { openDialog } from '../../../../base/dialog/actions';
import { translate } from '../../../../base/i18n/functions'; import { translate } from '../../../../base/i18n/functions';
import { IconImage } from '../../../../base/icons/svg'; import { IconImage } from '../../../../base/icons/svg';
import Video from '../../../../base/media/components/Video.web'; import Video from '../../../../base/media/components/Video.web';
@ -13,7 +12,8 @@ import Checkbox from '../../../../base/ui/components/web/Checkbox';
import ContextMenu from '../../../../base/ui/components/web/ContextMenu'; import ContextMenu from '../../../../base/ui/components/web/ContextMenu';
import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem'; import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem';
import ContextMenuItemGroup from '../../../../base/ui/components/web/ContextMenuItemGroup'; import ContextMenuItemGroup from '../../../../base/ui/components/web/ContextMenuItemGroup';
import VirtualBackgroundDialog from '../../../../virtual-background/components/VirtualBackgroundDialog'; import { openSettingsDialog } from '../../../actions';
import { SETTINGS_TABS } from '../../../constants';
import { createLocalVideoTracks } from '../../../functions.web'; import { createLocalVideoTracks } from '../../../functions.web';
const videoClassName = 'video-preview-video flipVideoX'; const videoClassName = 'video-preview-video flipVideoX';
@ -297,7 +297,7 @@ const mapStateToProps = (state: IReduxState) => {
const mapDispatchToProps = (dispatch: IStore['dispatch']) => { const mapDispatchToProps = (dispatch: IStore['dispatch']) => {
return { return {
selectBackground: () => dispatch(openDialog(VirtualBackgroundDialog)), selectBackground: () => dispatch(openSettingsDialog(SETTINGS_TABS.VIRTUAL_BACKGROUND)),
changeFlip: (flip: boolean) => { changeFlip: (flip: boolean) => {
dispatch(updateSettings({ dispatch(updateSettings({
localFlipX: flip localFlipX: flip

View File

@ -6,7 +6,8 @@ export const SETTINGS_TABS = {
NOTIFICATIONS: 'notifications_tab', NOTIFICATIONS: 'notifications_tab',
PROFILE: 'profile_tab', PROFILE: 'profile_tab',
SHORTCUTS: 'shortcuts_tab', SHORTCUTS: 'shortcuts_tab',
VIDEO: 'video_tab' VIDEO: 'video_tab',
VIRTUAL_BACKGROUND: 'virtual-background_tab'
}; };
/** /**

View File

@ -12,6 +12,7 @@ import {
} from '../base/participants/functions'; } from '../base/participants/functions';
import { toState } from '../base/redux/functions'; import { toState } from '../base/redux/functions';
import { getHideSelfView } from '../base/settings/functions'; import { getHideSelfView } from '../base/settings/functions';
import { getLocalVideoTrack } from '../base/tracks/functions.any';
import { parseStandardURIString } from '../base/util/uri'; import { parseStandardURIString } from '../base/util/uri';
import { isStageFilmstripEnabled } from '../filmstrip/functions'; import { isStageFilmstripEnabled } from '../filmstrip/functions';
import { isFollowMeActive } from '../follow-me/functions'; import { isFollowMeActive } from '../follow-me/functions';
@ -293,3 +294,22 @@ export function getShortcutsTabProps(stateful: IStateful, isDisplayedOnWelcomePa
keyboardShortcutsEnabled: keyboardShortcut.getEnabled() keyboardShortcutsEnabled: keyboardShortcut.getEnabled()
}; };
} }
/**
* Returns the properties for the "Virtual Background" tab from settings dialog from Redux
* state.
*
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state.
* @returns {Object} - The properties for the "Shortcuts" tab from settings
* dialog.
*/
export function getVirtualBackgroundTabProps(stateful: IStateful) {
const state = toState(stateful);
return {
_virtualBackground: state['features/virtual-background'],
selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
_jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack
};
}

View File

@ -6,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid';
import { translate } from '../../base/i18n/functions'; import { translate } from '../../base/i18n/functions';
import Icon from '../../base/icons/components/Icon'; import Icon from '../../base/icons/components/Icon';
import { IconPlus } from '../../base/icons/svg'; import { IconPlus } from '../../base/icons/svg';
import { withPixelLineHeight } from '../../base/styles/functions.web';
import { type Image, VIRTUAL_BACKGROUND_TYPE } from '../constants'; import { type Image, VIRTUAL_BACKGROUND_TYPE } from '../constants';
import { resizeImage } from '../functions'; import { resizeImage } from '../functions';
import logger from '../logger'; import logger from '../logger';
@ -40,24 +41,25 @@ interface IProps extends WithTranslation {
const useStyles = makeStyles()(theme => { const useStyles = makeStyles()(theme => {
return { return {
label: {
...withPixelLineHeight(theme.typography.bodyShortBold),
color: theme.palette.link01,
marginBottom: theme.spacing(3),
cursor: 'pointer',
display: 'flex',
alignItems: 'center'
},
addBackground: { addBackground: {
marginRight: theme.spacing(2), marginRight: theme.spacing(3),
'& svg': { '& svg': {
fill: '#669aec !important' fill: `${theme.palette.link01} !important`
} }
}, },
button: {
input: {
display: 'none' display: 'none'
},
label: {
fontSize: '14px',
fontWeight: 600,
lineHeight: '20px',
marginTop: theme.spacing(3),
marginBottom: theme.spacing(2),
color: '#669aec',
display: 'inline-flex',
cursor: 'pointer'
} }
}; };
}); });
@ -127,14 +129,14 @@ function UploadImageButton({
tabIndex = { 0 } > tabIndex = { 0 } >
<Icon <Icon
className = { classes.addBackground } className = { classes.addBackground }
size = { 20 } size = { 24 }
src = { IconPlus } /> src = { IconPlus } />
{t('virtualBackground.addBackground')} {t('virtualBackground.addBackground')}
</label>} </label>}
<input <input
accept = 'image/*' accept = 'image/*'
className = { classes.button } className = { classes.input }
id = 'file-upload' id = 'file-upload'
onChange = { uploadImage } onChange = { uploadImage }
ref = { uploadImageButton } ref = { uploadImageButton }

View File

@ -1,15 +1,14 @@
// @flow // @flow
import { openDialog } from '../../base/dialog';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { IconImage } from '../../base/icons'; import { IconImage } from '../../base/icons';
import { connect } from '../../base/redux'; import { connect } from '../../base/redux';
import { AbstractButton } from '../../base/toolbox/components'; import { AbstractButton } from '../../base/toolbox/components';
import type { AbstractButtonProps } from '../../base/toolbox/components'; import type { AbstractButtonProps } from '../../base/toolbox/components';
import { openSettingsDialog } from '../../settings/actions';
import { SETTINGS_TABS } from '../../settings/constants';
import { checkBlurSupport } from '../functions'; import { checkBlurSupport } from '../functions';
import { VirtualBackgroundDialog } from './index';
/** /**
* The type of the React {@code Component} props of {@link VideoBackgroundButton}. * The type of the React {@code Component} props of {@link VideoBackgroundButton}.
*/ */
@ -45,7 +44,7 @@ class VideoBackgroundButton extends AbstractButton<Props, *> {
_handleClick() { _handleClick() {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(openDialog(VirtualBackgroundDialog)); dispatch(openSettingsDialog(SETTINGS_TABS.VIRTUAL_BACKGROUND));
} }
/** /**

View File

@ -8,8 +8,6 @@ import { connect } from 'react-redux';
import { IReduxState } from '../../app/types'; import { IReduxState } from '../../app/types';
import { hideDialog } from '../../base/dialog/actions'; import { hideDialog } from '../../base/dialog/actions';
import { translate } from '../../base/i18n/functions'; import { translate } from '../../base/i18n/functions';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import Video from '../../base/media/components/Video'; import Video from '../../base/media/components/Video';
import { equals } from '../../base/redux/functions'; import { equals } from '../../base/redux/functions';
import { getCurrentCameraDeviceId } from '../../base/settings/functions.web'; import { getCurrentCameraDeviceId } from '../../base/settings/functions.web';
@ -83,39 +81,28 @@ interface IState {
const styles = (theme: Theme) => { const styles = (theme: Theme) => {
return { return {
virtualBackgroundPreview: { virtualBackgroundPreview: {
'& .video-preview': { height: 'auto',
height: '250px' width: '100%',
}, overflow: 'hidden',
marginBottom: theme.spacing(3),
'& .video-background-preview-entry': { zIndex: 2,
height: '250px', borderRadius: '3px',
width: '570px', backgroundColor: theme.palette.uiBackground,
marginBottom: theme.spacing(2), position: 'relative' as const,
zIndex: 2,
'@media (max-width: 632px)': {
maxWidth: '336px'
}
},
'& .video-preview-loader': { '& .video-preview-loader': {
borderRadius: '6px', height: '220px',
backgroundColor: 'transparent',
height: '250px',
marginBottom: theme.spacing(2),
width: '572px',
position: 'fixed',
zIndex: 2,
'& svg': { '& svg': {
position: 'absolute', position: 'absolute' as const,
top: '40%', top: '40%',
left: '45%' left: '45%'
},
'@media (min-width: 432px) and (max-width: 632px)': {
width: '340px'
} }
},
'& .video-preview-error': {
height: '220px',
position: 'relative'
} }
} }
}; };
@ -238,31 +225,21 @@ class VirtualBackgroundPreview extends PureComponent<IProps, IState> {
*/ */
_renderPreviewEntry(data: Object) { _renderPreviewEntry(data: Object) {
const { t } = this.props; const { t } = this.props;
const className = 'video-background-preview-entry';
if (this.state.loading) { if (this.state.loading) {
return this._loadVideoPreview(); return this._loadVideoPreview();
} }
if (!data) { if (!data) {
return ( return (
<div <div className = 'video-preview-error'>{t('deviceSelection.previewUnavailable')}</div>
className = { className }
video-preview-container = { true }>
<div className = 'video-preview-error'>{t('deviceSelection.previewUnavailable')}</div>
</div>
); );
} }
const props: Object = {
className
};
return ( return (
<div { ...props }> <Video
<Video className = { videoClassName }
className = { videoClassName } playsinline = { true }
playsinline = { true } videoTrack = {{ jitsiTrack: data }} />
videoTrack = {{ jitsiTrack: data }} />
</div>
); );
} }
@ -310,8 +287,8 @@ class VirtualBackgroundPreview extends PureComponent<IProps, IState> {
return (<div className = { classes.virtualBackgroundPreview }> return (<div className = { classes.virtualBackgroundPreview }>
{jitsiTrack {jitsiTrack
? <div className = 'video-preview'>{this._renderPreviewEntry(jitsiTrack)}</div> ? this._renderPreviewEntry(jitsiTrack)
: <div className = 'video-preview-loader'>{this._loadVideoPreview()}</div> : this._loadVideoPreview()
}</div>); }</div>);
} }
} }

View File

@ -6,27 +6,22 @@ import Bourne from '@hapi/bourne';
import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage'; import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { WithTranslation } from 'react-i18next'; import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui'; import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../app/types'; import { IReduxState } from '../../app/types';
import { getMultipleVideoSendingSupportFeatureFlag } from '../../base/config/functions.any'; import { getMultipleVideoSendingSupportFeatureFlag } from '../../base/config/functions.any';
import { hideDialog } from '../../base/dialog/actions';
import { translate } from '../../base/i18n/functions'; import { translate } from '../../base/i18n/functions';
import Icon from '../../base/icons/components/Icon'; import Icon from '../../base/icons/components/Icon';
import { IconCloseLarge } from '../../base/icons/svg'; import { IconCloseLarge } from '../../base/icons/svg';
import { connect } from '../../base/redux/functions'; import { withPixelLineHeight } from '../../base/styles/functions.web';
import { updateSettings } from '../../base/settings/actions';
// @ts-ignore // @ts-ignore
import { Tooltip } from '../../base/tooltip'; import { Tooltip } from '../../base/tooltip';
import { getLocalVideoTrack } from '../../base/tracks/functions';
import Dialog from '../../base/ui/components/web/Dialog';
import { toggleBackgroundEffect } from '../actions';
import { BACKGROUNDS_LIMIT, IMAGES, type Image, VIRTUAL_BACKGROUND_TYPE } from '../constants'; import { BACKGROUNDS_LIMIT, IMAGES, type Image, VIRTUAL_BACKGROUND_TYPE } from '../constants';
import { toDataURL } from '../functions'; import { toDataURL } from '../functions';
import logger from '../logger'; import logger from '../logger';
import UploadImageButton from './UploadImageButton'; import UploadImageButton from './UploadImageButton';
// @ts-ignore
import VirtualBackgroundPreview from './VirtualBackgroundPreview'; import VirtualBackgroundPreview from './VirtualBackgroundPreview';
/* eslint-enable lines-around-comment */ /* eslint-enable lines-around-comment */
@ -38,7 +33,7 @@ interface IProps extends WithTranslation {
_images: Array<Image>; _images: Array<Image>;
/** /**
* Returns the jitsi track that will have backgraund effect applied. * Returns the jitsi track that will have background effect applied.
*/ */
_jitsiTrack: Object; _jitsiTrack: Object;
@ -52,11 +47,6 @@ interface IProps extends WithTranslation {
*/ */
_multiStreamModeEnabled: boolean; _multiStreamModeEnabled: boolean;
/**
* Returns the selected thumbnail identifier.
*/
_selectedThumbnail: string;
/** /**
* If the upload button should be displayed or not. * If the upload button should be displayed or not.
*/ */
@ -78,192 +68,131 @@ interface IProps extends WithTranslation {
* NOTE: currently used only for electron in order to open the dialog in the correct state after desktop sharing * NOTE: currently used only for electron in order to open the dialog in the correct state after desktop sharing
* selection. * selection.
*/ */
initialOptions: Object; initialOptions?: Object;
/**
* Options change handler.
*/
onOptionsChange: Function;
/**
* Virtual background options.
*/
options: any;
/**
* Returns the selected thumbnail identifier.
*/
selectedThumbnail: string;
} }
const onError = (event: any) => { const onError = (event: any) => {
event.target.style.display = 'none'; event.target.style.display = 'none';
}; };
/**
* Maps (parts of) the redux state to the associated props for the
* {@code VirtualBackground} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{Props}}
*/
function _mapStateToProps(state: IReduxState): Object {
const { localFlipX } = state['features/base/settings'];
const dynamicBrandingImages = state['features/dynamic-branding'].virtualBackgrounds;
const hasBrandingImages = Boolean(dynamicBrandingImages.length);
return {
_localFlipX: Boolean(localFlipX),
_images: (hasBrandingImages && dynamicBrandingImages) || IMAGES,
_virtualBackground: state['features/virtual-background'],
_selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
_showUploadButton: state['features/base/config'].disableAddingBackgroundImages,
_jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack,
_multiStreamModeEnabled: getMultipleVideoSendingSupportFeatureFlag(state)
};
}
const VirtualBackgroundDialog = translate(connect(_mapStateToProps)(VirtualBackground));
const useStyles = makeStyles()(theme => { const useStyles = makeStyles()(theme => {
return { return {
dialogContainer: { virtualBackgroundLoading: {
width: 'auto' width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '50px'
}, },
container: { container: {
width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column' flexDirection: 'column'
}, },
dialog: {
alignSelf: 'flex-start', thumbnailContainer: {
position: 'relative', width: '100%',
maxHeight: '300px',
color: 'white',
display: 'inline-grid', display: 'inline-grid',
gridTemplateColumns: 'auto auto auto auto auto', gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr',
columnGap: '9px', gap: theme.spacing(1),
cursor: 'pointer',
// @ts-ignore '@media (min-width: 608px) and (max-width: 712px)': {
[[ '& .desktop-share:hover', gridTemplateColumns: '1fr 1fr 1fr 1fr'
'& .thumbnail:hover',
'& .blur:hover',
'& .slight-blur:hover',
'& .virtual-background-none:hover' ]]: {
opacity: 0.5,
border: '2px solid #99bbf3'
}, },
'& .background-option': {
marginTop: theme.spacing(2),
borderRadius: `${theme.shape.borderRadius}px`,
height: '60px',
width: '107px',
textAlign: 'center',
justifyContent: 'center',
fontWeight: 'bold',
boxSizing: 'border-box',
display: 'flex',
alignItems: 'center'
},
'& thumbnail-container': {
position: 'relative',
'&:focus-within .thumbnail ~ .delete-image-icon': {
display: 'block'
}
},
'& .thumbnail': {
objectFit: 'cover'
},
'& .thumbnail:hover ~ .delete-image-icon': {
display: 'block'
},
'& .thumbnail-selected': {
objectFit: 'cover',
border: '2px solid #246fe5'
},
'& .blur': {
boxShadow: 'inset 0 0 12px #000000',
background: '#7e8287',
padding: '0 10px'
},
'& .blur-selected': {
border: '2px solid #246fe5'
},
'& .slight-blur': {
boxShadow: 'inset 0 0 12px #000000',
background: '#a4a4a4',
padding: '0 10px'
},
'& .slight-blur-selected': {
border: '2px solid #246fe5'
},
'& .virtual-background-none': {
background: '#525252',
padding: '0 10px'
},
'& .none-selected': {
border: '2px solid #246fe5'
},
'& .desktop-share': {
background: '#525252'
},
'& .desktop-share-selected': {
border: '2px solid #246fe5',
padding: '0 10px'
},
'& delete-image-icon': {
background: '#3d3d3d',
position: 'absolute',
display: 'none',
left: '96px',
bottom: '51px',
'&:hover': {
display: 'block'
},
'@media (max-width: 632px)': {
left: '51px'
}
},
'@media (max-width: 720px)': {
gridTemplateColumns: 'auto auto auto auto'
},
'@media (max-width: 632px)': {
gridTemplateColumns: 'auto auto auto auto auto',
fontSize: '1.5vw',
// @ts-ignore '@media (max-width: 607px)': {
[[ '& .desktop-share:hover', gridTemplateColumns: '1fr 1fr 1fr',
'& .thumbnail:hover', gap: theme.spacing(2)
'& .blur:hover',
'& .slight-blur:hover',
'& .virtual-background-none:hover' ]]: {
height: '60px',
width: '60px'
},
// @ts-ignore
[[ '& .desktop-share',
'& .virtual-background-none,',
'& .thumbnail,',
'& .blur,',
'& .slight-blur' ]]: {
height: '60px',
width: '60px'
},
// @ts-ignore
[[ '& .desktop-share-selected',
'& .thumbnail-selected',
'& .none-selected',
'& .blur-selected',
'& .slight-blur-selected' ]]: {
height: '60px',
width: '60px'
}
},
'@media (max-width: 360px)': {
gridTemplateColumns: 'auto auto auto auto'
},
'@media (max-width: 319px)': {
gridTemplateColumns: 'auto auto'
} }
}, },
dialogMarginTop: {
marginTop: '8px' thumbnail: {
height: '54px',
width: '100%',
borderRadius: '4px',
boxSizing: 'border-box',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
...withPixelLineHeight(theme.typography.labelBold),
color: theme.palette.text01,
objectFit: 'cover',
[[ '&:hover', '&:focus' ] as any]: {
opacity: 0.5,
cursor: 'pointer',
'& ~ .delete-image-icon': {
display: 'block'
}
},
'@media (max-width: 607px)': {
height: '70px'
}
}, },
virtualBackgroundLoading: {
overflow: 'hidden', selectedThumbnail: {
position: 'fixed', border: `2px solid ${theme.palette.action01Hover}`
left: '50%', },
marginTop: '10px',
transform: 'translateX(-50%)' noneThumbnail: {
backgroundColor: theme.palette.ui04
},
slightBlur: {
boxShadow: 'inset 0 0 12px #000000',
background: '#a4a4a4'
},
blur: {
boxShadow: 'inset 0 0 12px #000000',
background: '#7e8287'
},
storedImageContainer: {
position: 'relative',
display: 'flex',
flexDirection: 'column',
'&:focus-within .delete-image-container': {
display: 'block'
}
},
deleteImageIcon: {
position: 'absolute',
top: '3px',
right: '3px',
background: theme.palette.ui03,
borderRadius: '3px',
cursor: 'pointer',
display: 'none',
'@media (max-width: 607px)': {
display: 'block',
padding: '3px'
},
[[ '&:hover', '&:focus' ] as any]: {
display: 'block'
}
} }
}; };
}); });
@ -273,24 +202,28 @@ const useStyles = makeStyles()(theme => {
* *
* @returns {ReactElement} * @returns {ReactElement}
*/ */
function VirtualBackground({ function VirtualBackgrounds({
_images, _images,
_jitsiTrack, _jitsiTrack,
_localFlipX, _localFlipX,
_selectedThumbnail, selectedThumbnail,
_showUploadButton, _showUploadButton,
_virtualBackground, _virtualBackground,
dispatch, onOptionsChange,
options,
initialOptions, initialOptions,
t t
}: IProps) { }: IProps) {
const { classes, cx } = useStyles(); const { classes, cx } = useStyles();
const [ previewIsLoaded, setPreviewIsLoaded ] = useState(false); const [ previewIsLoaded, setPreviewIsLoaded ] = useState(false);
const [ options, setOptions ] = useState<any>({ ...initialOptions });
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds'); const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && Bourne.parse(localImages)) || []); const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && Bourne.parse(localImages)) || []);
const [ loading, setLoading ] = useState(false); const [ loading, setLoading ] = useState(false);
const [ initialVirtualBackground ] = useState(_virtualBackground);
useEffect(() => {
onOptionsChange({ ...initialOptions });
}, []);
const deleteStoredImage = useCallback(e => { const deleteStoredImage = useCallback(e => {
const imageId = e.currentTarget.getAttribute('data-imageid'); const imageId = e.currentTarget.getAttribute('data-imageid');
@ -320,7 +253,7 @@ function VirtualBackground({
}, [ storedImages ]); }, [ storedImages ]);
const enableBlur = useCallback(async () => { const enableBlur = useCallback(async () => {
setOptions({ onOptionsChange({
backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR, backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
enabled: true, enabled: true,
blurValue: 25, blurValue: 25,
@ -338,7 +271,7 @@ function VirtualBackground({
}, [ enableBlur ]); }, [ enableBlur ]);
const enableSlideBlur = useCallback(async () => { const enableSlideBlur = useCallback(async () => {
setOptions({ onOptionsChange({
backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR, backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
enabled: true, enabled: true,
blurValue: 8, blurValue: 8,
@ -356,7 +289,7 @@ function VirtualBackground({
}, [ enableSlideBlur ]); }, [ enableSlideBlur ]);
const removeBackground = useCallback(async () => { const removeBackground = useCallback(async () => {
setOptions({ onOptionsChange({
enabled: false, enabled: false,
selectedThumbnail: 'none' selectedThumbnail: 'none'
}); });
@ -376,7 +309,7 @@ function VirtualBackground({
const image = storedImages.find(img => img.id === imageId); const image = storedImages.find(img => img.id === imageId);
if (image) { if (image) {
setOptions({ onOptionsChange({
backgroundType: 'image', backgroundType: 'image',
enabled: true, enabled: true,
url: image.src, url: image.src,
@ -394,7 +327,7 @@ function VirtualBackground({
try { try {
const url = await toDataURL(image.src); const url = await toDataURL(image.src);
setOptions({ onOptionsChange({
backgroundType: 'image', backgroundType: 'image',
enabled: true, enabled: true,
url, url,
@ -423,48 +356,12 @@ function VirtualBackground({
} }
}, [ setUploadedImageBackground ]); }, [ setUploadedImageBackground ]);
const applyVirtualBackground = useCallback(async () => {
setLoading(true);
await dispatch(toggleBackgroundEffect(options, _jitsiTrack));
await setLoading(false);
// Set x scale to default value.
dispatch(updateSettings({
localFlipX: true
}));
dispatch(hideDialog());
logger.info(`Virtual background type: '${typeof options.backgroundType === 'undefined'
? 'none' : options.backgroundType}' applied!`);
}, [ dispatch, options, _localFlipX ]);
// Prevent the selection of a new virtual background if it has not been applied by default
const cancelVirtualBackground = useCallback(async () => {
await setOptions({
backgroundType: initialVirtualBackground.backgroundType,
enabled: initialVirtualBackground.backgroundEffectEnabled,
url: initialVirtualBackground.virtualSource,
selectedThumbnail: initialVirtualBackground.selectedThumbnail,
blurValue: initialVirtualBackground.blurValue
});
dispatch(hideDialog());
}, []);
const loadedPreviewState = useCallback(async loaded => { const loadedPreviewState = useCallback(async loaded => {
await setPreviewIsLoaded(loaded); await setPreviewIsLoaded(loaded);
}, []); }, []);
return ( return (
<Dialog <>
className = { classes.dialogContainer }
ok = {{
disabled: !options || loading || !previewIsLoaded,
translationKey: 'virtualBackground.apply'
}}
onCancel = { cancelVirtualBackground }
onSubmit = { applyVirtualBackground }
size = 'large'
titleKey = 'virtualBackground.title' >
<VirtualBackgroundPreview <VirtualBackgroundPreview
loadedPreview = { loadedPreviewState } loadedPreview = { loadedPreviewState }
options = { options } /> options = { options } />
@ -481,23 +378,22 @@ function VirtualBackground({
{_showUploadButton {_showUploadButton
&& <UploadImageButton && <UploadImageButton
setLoading = { setLoading } setLoading = { setLoading }
setOptions = { setOptions } setOptions = { onOptionsChange }
setStoredImages = { setStoredImages } setStoredImages = { setStoredImages }
showLabel = { previewIsLoaded } showLabel = { previewIsLoaded }
storedImages = { storedImages } />} storedImages = { storedImages } />}
<div <div
className = { cx(classes.dialog, { [classes.dialogMarginTop]: previewIsLoaded }) } className = { classes.thumbnailContainer }
role = 'radiogroup' role = 'radiogroup'
tabIndex = { -1 }> tabIndex = { -1 }>
<Tooltip <Tooltip
content = { t('virtualBackground.removeBackground') } content = { t('virtualBackground.removeBackground') }
position = { 'top' }> position = { 'top' }>
<div <div
aria-checked = { _selectedThumbnail === 'none' } aria-checked = { selectedThumbnail === 'none' }
aria-label = { t('virtualBackground.removeBackground') } aria-label = { t('virtualBackground.removeBackground') }
className = { cx('background-option', 'virtual-background-none', { className = { cx(classes.thumbnail, classes.noneThumbnail,
'none-selected': _selectedThumbnail === 'none' selectedThumbnail === 'none' && classes.selectedThumbnail) }
}) }
onClick = { removeBackground } onClick = { removeBackground }
onKeyPress = { removeBackgroundKeyPress } onKeyPress = { removeBackgroundKeyPress }
role = 'radio' role = 'radio'
@ -509,11 +405,10 @@ function VirtualBackground({
content = { t('virtualBackground.slightBlur') } content = { t('virtualBackground.slightBlur') }
position = { 'top' }> position = { 'top' }>
<div <div
aria-checked = { _selectedThumbnail === 'slight-blur' } aria-checked = { selectedThumbnail === 'slight-blur' }
aria-label = { t('virtualBackground.slightBlur') } aria-label = { t('virtualBackground.slightBlur') }
className = { cx('background-option', 'slight-blur', { className = { cx(classes.thumbnail, classes.slightBlur,
'slight-blur-selected': _selectedThumbnail === 'slight-blur' selectedThumbnail === 'slight-blur' && classes.selectedThumbnail) }
}) }
onClick = { enableSlideBlur } onClick = { enableSlideBlur }
onKeyPress = { enableSlideBlurKeyPress } onKeyPress = { enableSlideBlurKeyPress }
role = 'radio' role = 'radio'
@ -525,11 +420,10 @@ function VirtualBackground({
content = { t('virtualBackground.blur') } content = { t('virtualBackground.blur') }
position = { 'top' }> position = { 'top' }>
<div <div
aria-checked = { _selectedThumbnail === 'blur' } aria-checked = { selectedThumbnail === 'blur' }
aria-label = { t('virtualBackground.blur') } aria-label = { t('virtualBackground.blur') }
className = { cx('background-option', 'blur', { className = { cx(classes.thumbnail, classes.blur,
'blur-selected': _selectedThumbnail === 'blur' selectedThumbnail === 'blur' && classes.selectedThumbnail) }
}) }
onClick = { enableBlur } onClick = { enableBlur }
onKeyPress = { enableBlurKeyPress } onKeyPress = { enableBlurKeyPress }
role = 'radio' role = 'radio'
@ -544,11 +438,11 @@ function VirtualBackground({
position = { 'top' }> position = { 'top' }>
<img <img
alt = { image.tooltip && t(`virtualBackground.${image.tooltip}`) } alt = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
aria-checked = { options.selectedThumbnail === image.id aria-checked = { options?.selectedThumbnail === image.id
|| _selectedThumbnail === image.id } || selectedThumbnail === image.id }
className = { className = { cx(classes.thumbnail,
options.selectedThumbnail === image.id || _selectedThumbnail === image.id (options?.selectedThumbnail === image.id
? 'background-option thumbnail-selected' : 'background-option thumbnail' } || selectedThumbnail === image.id) && classes.selectedThumbnail) }
data-imageid = { image.id } data-imageid = { image.id }
onClick = { setImageBackground } onClick = { setImageBackground }
onError = { onError } onError = { onError }
@ -560,15 +454,13 @@ function VirtualBackground({
))} ))}
{storedImages.map((image, index) => ( {storedImages.map((image, index) => (
<div <div
className = { 'thumbnail-container' } className = { classes.storedImageContainer }
key = { image.id }> key = { image.id }>
<img <img
alt = { t('virtualBackground.uploadedImage', { index: index + 1 }) } alt = { t('virtualBackground.uploadedImage', { index: index + 1 }) }
aria-checked = { _selectedThumbnail === image.id } aria-checked = { selectedThumbnail === image.id }
className = { cx('background-option', { className = { cx(classes.thumbnail,
'thumbnail-selected': _selectedThumbnail === image.id, selectedThumbnail === image.id && classes.selectedThumbnail) }
'thumbnail': _selectedThumbnail !== image.id
}) }
data-imageid = { image.id } data-imageid = { image.id }
onClick = { setUploadedImageBackground } onClick = { setUploadedImageBackground }
onError = { onError } onError = { onError }
@ -579,12 +471,12 @@ function VirtualBackground({
<Icon <Icon
ariaLabel = { t('virtualBackground.deleteImage') } ariaLabel = { t('virtualBackground.deleteImage') }
className = { 'delete-image-icon' } className = { cx(classes.deleteImageIcon, 'delete-image-icon') }
data-imageid = { image.id } data-imageid = { image.id }
onClick = { deleteStoredImage } onClick = { deleteStoredImage }
onKeyPress = { deleteStoredImageKeyPress } onKeyPress = { deleteStoredImageKeyPress }
role = 'button' role = 'button'
size = { 15 } size = { 16 }
src = { IconCloseLarge } src = { IconCloseLarge }
tabIndex = { 0 } /> tabIndex = { 0 } />
</div> </div>
@ -592,8 +484,30 @@ function VirtualBackground({
</div> </div>
</div> </div>
)} )}
</Dialog> </>
); );
} }
export default VirtualBackgroundDialog; /**
* Maps (parts of) the redux state to the associated props for the
* {@code VirtualBackground} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{Props}}
*/
function _mapStateToProps(state: IReduxState) {
const { localFlipX } = state['features/base/settings'];
const dynamicBrandingImages = state['features/dynamic-branding'].virtualBackgrounds;
const hasBrandingImages = Boolean(dynamicBrandingImages.length);
return {
_localFlipX: Boolean(localFlipX),
_images: (hasBrandingImages && dynamicBrandingImages) || IMAGES,
_virtualBackground: state['features/virtual-background'],
_showUploadButton: !state['features/base/config'].disableAddingBackgroundImages,
_multiStreamModeEnabled: getMultipleVideoSendingSupportFeatureFlag(state)
};
}
export default connect(_mapStateToProps)(translate(VirtualBackgrounds));

View File

@ -1,2 +1 @@
export { default as VideoBackgroundButton } from './VideoBackgroundButton'; export { default as VideoBackgroundButton } from './VideoBackgroundButton';
export { default as VirtualBackgroundDialog } from './VirtualBackgroundDialog';