diff --git a/lang/main.json b/lang/main.json index dce7185c2..72d8533f7 100644 --- a/lang/main.json +++ b/lang/main.json @@ -1350,7 +1350,7 @@ "none": "None", "pleaseWait": "Please wait...", "removeBackground": "Remove background", - "slightBlur": "Slight Blur", + "slightBlur": "Half Blur", "title": "Virtual backgrounds", "uploadedImage": "Uploaded image {{index}}", "webAssemblyWarning": "WebAssembly not supported", diff --git a/modules/API/API.js b/modules/API/API.js index c86df333e..27525ec4f 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -106,6 +106,8 @@ import { startAudioScreenShareFlow, startScreenShareFlow } from '../../react/fea import { isScreenAudioSupported } from '../../react/features/screen-share/functions'; import { toggleScreenshotCaptureSummary } from '../../react/features/screenshot-capture'; 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 { extractYoutubeIdOrURL } from '../../react/features/shared-video/functions'; 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 { muteAllParticipants } from '../../react/features/video-menu/actions'; import { setVideoQuality } from '../../react/features/video-quality'; -import VirtualBackgroundDialog from '../../react/features/virtual-background/components/VirtualBackgroundDialog'; import { getJitsiMeetTransport } from '../transport'; import { API_ID, ENDPOINT_TEXT_MESSAGE_NAME } from './constants'; @@ -798,7 +799,8 @@ function initCommands() { APP.store.dispatch(overwriteConfig(whitelistedConfig)); }, 'toggle-virtual-background': () => { - APP.store.dispatch(toggleDialog(VirtualBackgroundDialog)); + APP.store.dispatch(toggleDialog(SettingsDialog, { + defaultTab: SETTINGS_TABS.VIRTUAL_BACKGROUND })); }, 'end-conference': () => { APP.store.dispatch(endConference()); diff --git a/react/features/base/ui/components/web/DialogWithTabs.tsx b/react/features/base/ui/components/web/DialogWithTabs.tsx index 1f5042585..eed895f3c 100644 --- a/react/features/base/ui/components/web/DialogWithTabs.tsx +++ b/react/features/base/ui/components/web/DialogWithTabs.tsx @@ -146,6 +146,7 @@ interface IObject { } export interface IDialogTab

{ + cancel?: Function; className?: string; component: ComponentType; icon: Function; @@ -214,7 +215,12 @@ const DialogWithTabs = ({ } }, [ isMobile, userSelected, selectedTab ]); - const onClose = useCallback(() => { + const onClose = useCallback((isCancel = true) => { + if (isCancel) { + tabs.forEach(({ cancel }) => { + cancel && dispatch(cancel()); + }); + } dispatch(hideDialog()); }, []); @@ -268,7 +274,7 @@ const DialogWithTabs = ({ tabs.forEach(({ submit }, idx) => { submit?.(tabStates[idx]); }); - onClose(); + onClose(false); }, [ tabs, tabStates ]); const selectedTabIndex = useMemo(() => { diff --git a/react/features/settings/actions.ts b/react/features/settings/actions.ts index bc0704456..80a6a127e 100644 --- a/react/features/settings/actions.ts +++ b/react/features/settings/actions.ts @@ -11,6 +11,8 @@ import { import { openDialog } from '../base/dialog/actions'; import i18next from '../base/i18n/i18next'; import { updateSettings } from '../base/settings/actions'; +import { toggleBackgroundEffect } from '../virtual-background/actions'; +import virtualBackgroundLogger from '../virtual-background/logger'; import { SET_AUDIO_SETTINGS_VISIBILITY, @@ -24,7 +26,8 @@ import { getMoreTabProps, getNotificationsTabProps, getProfileTabProps, - getShortcutsTabProps + getShortcutsTabProps, + getVirtualBackgroundTabProps } 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!`); + } + } + }; +} diff --git a/react/features/settings/components/web/SettingsDialog.tsx b/react/features/settings/components/web/SettingsDialog.tsx index 76e73f4c9..26cd9be3a 100644 --- a/react/features/settings/components/web/SettingsDialog.tsx +++ b/react/features/settings/components/web/SettingsDialog.tsx @@ -8,6 +8,7 @@ import { IconCalendar, IconGear, IconHost, + IconImage, IconShortcuts, IconUser, IconVideo, @@ -24,12 +25,14 @@ import { getAudioDeviceSelectionDialogProps, getVideoDeviceSelectionDialogProps } from '../../../device-selection/functions.web'; +import { checkBlurSupport } from '../../../virtual-background/functions'; import { submitModeratorTab, submitMoreTab, submitNotificationsTab, submitProfileTab, - submitShortcutsTab + submitShortcutsTab, + submitVirtualBackgroundTab } from '../../actions'; import { SETTINGS_TABS } from '../../constants'; import { @@ -38,7 +41,8 @@ import { getNotificationsMap, getNotificationsTabProps, getProfileTabProps, - getShortcutsTabProps + getShortcutsTabProps, + getVirtualBackgroundTabProps } from '../../functions'; // @ts-ignore @@ -48,6 +52,7 @@ import MoreTab from './MoreTab'; import NotificationsTab from './NotificationsTab'; import ProfileTab from './ProfileTab'; import ShortcutsTab from './ShortcutsTab'; +import VirtualBackgroundTab from './VirtualBackgroundTab'; /** * 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 enabledNotifications = getNotificationsMap(state); const showNotificationsSettings = Object.keys(enabledNotifications).length > 0; + const virtualBackgroundSupported = checkBlurSupport(); const tabs: IDialogTab[] = []; 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) { tabs.push({ name: SETTINGS_TABS.NOTIFICATIONS, component: NotificationsTab, labelKey: 'settings.notifications', - propsUpdateFunction: (tabState: any, newProps: any) => { + propsUpdateFunction: (tabState: any, newProps: ReturnType) => { return { ...newProps, enabledNotifications: tabState?.enabledNotifications || {} @@ -373,7 +404,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) { component: ShortcutsTab, labelKey: 'settings.shortcuts', props: getShortcutsTabProps(state, isDisplayedOnWelcomePage), - propsUpdateFunction: (tabState: any, newProps: any) => { + propsUpdateFunction: (tabState: any, newProps: ReturnType) => { // Updates tab props, keeping users selection return { diff --git a/react/features/settings/components/web/VirtualBackgroundTab.tsx b/react/features/settings/components/web/VirtualBackgroundTab.tsx new file mode 100644 index 000000000..83c6d7d5c --- /dev/null +++ b/react/features/settings/components/web/VirtualBackgroundTab.tsx @@ -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 { + /** + * 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 ( +

+ +
+ ); + } +} + +export default withStyles(styles)(translate(VirtualBackgroundTab)); diff --git a/react/features/settings/components/web/video/VideoSettingsContent.tsx b/react/features/settings/components/web/video/VideoSettingsContent.tsx index 383783590..4378f2608 100644 --- a/react/features/settings/components/web/video/VideoSettingsContent.tsx +++ b/react/features/settings/components/web/video/VideoSettingsContent.tsx @@ -3,7 +3,6 @@ import { WithTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { IReduxState, IStore } from '../../../../app/types'; -import { openDialog } from '../../../../base/dialog/actions'; import { translate } from '../../../../base/i18n/functions'; import { IconImage } from '../../../../base/icons/svg'; 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 ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem'; 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'; const videoClassName = 'video-preview-video flipVideoX'; @@ -297,7 +297,7 @@ const mapStateToProps = (state: IReduxState) => { const mapDispatchToProps = (dispatch: IStore['dispatch']) => { return { - selectBackground: () => dispatch(openDialog(VirtualBackgroundDialog)), + selectBackground: () => dispatch(openSettingsDialog(SETTINGS_TABS.VIRTUAL_BACKGROUND)), changeFlip: (flip: boolean) => { dispatch(updateSettings({ localFlipX: flip diff --git a/react/features/settings/constants.ts b/react/features/settings/constants.ts index 69911a5e2..d71c27e00 100644 --- a/react/features/settings/constants.ts +++ b/react/features/settings/constants.ts @@ -6,7 +6,8 @@ export const SETTINGS_TABS = { NOTIFICATIONS: 'notifications_tab', PROFILE: 'profile_tab', SHORTCUTS: 'shortcuts_tab', - VIDEO: 'video_tab' + VIDEO: 'video_tab', + VIRTUAL_BACKGROUND: 'virtual-background_tab' }; /** diff --git a/react/features/settings/functions.any.ts b/react/features/settings/functions.any.ts index 4e0276da3..33445ad9a 100644 --- a/react/features/settings/functions.any.ts +++ b/react/features/settings/functions.any.ts @@ -12,6 +12,7 @@ import { } from '../base/participants/functions'; import { toState } from '../base/redux/functions'; import { getHideSelfView } from '../base/settings/functions'; +import { getLocalVideoTrack } from '../base/tracks/functions.any'; import { parseStandardURIString } from '../base/util/uri'; import { isStageFilmstripEnabled } from '../filmstrip/functions'; import { isFollowMeActive } from '../follow-me/functions'; @@ -293,3 +294,22 @@ export function getShortcutsTabProps(stateful: IStateful, isDisplayedOnWelcomePa 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 + }; +} diff --git a/react/features/virtual-background/components/UploadImageButton.tsx b/react/features/virtual-background/components/UploadImageButton.tsx index e4310ded6..98c323ef1 100644 --- a/react/features/virtual-background/components/UploadImageButton.tsx +++ b/react/features/virtual-background/components/UploadImageButton.tsx @@ -6,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import { translate } from '../../base/i18n/functions'; import Icon from '../../base/icons/components/Icon'; import { IconPlus } from '../../base/icons/svg'; +import { withPixelLineHeight } from '../../base/styles/functions.web'; import { type Image, VIRTUAL_BACKGROUND_TYPE } from '../constants'; import { resizeImage } from '../functions'; import logger from '../logger'; @@ -40,24 +41,25 @@ interface IProps extends WithTranslation { const useStyles = makeStyles()(theme => { return { + label: { + ...withPixelLineHeight(theme.typography.bodyShortBold), + color: theme.palette.link01, + marginBottom: theme.spacing(3), + cursor: 'pointer', + display: 'flex', + alignItems: 'center' + }, + addBackground: { - marginRight: theme.spacing(2), + marginRight: theme.spacing(3), + '& svg': { - fill: '#669aec !important' + fill: `${theme.palette.link01} !important` } }, - button: { + + input: { 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 } > {t('virtualBackground.addBackground')} } { _handleClick() { const { dispatch } = this.props; - dispatch(openDialog(VirtualBackgroundDialog)); + dispatch(openSettingsDialog(SETTINGS_TABS.VIRTUAL_BACKGROUND)); } /** diff --git a/react/features/virtual-background/components/VirtualBackgroundPreview.tsx b/react/features/virtual-background/components/VirtualBackgroundPreview.tsx index 9dffea444..deb77632d 100644 --- a/react/features/virtual-background/components/VirtualBackgroundPreview.tsx +++ b/react/features/virtual-background/components/VirtualBackgroundPreview.tsx @@ -8,8 +8,6 @@ import { connect } from 'react-redux'; import { IReduxState } from '../../app/types'; import { hideDialog } from '../../base/dialog/actions'; import { translate } from '../../base/i18n/functions'; -// eslint-disable-next-line lines-around-comment -// @ts-ignore import Video from '../../base/media/components/Video'; import { equals } from '../../base/redux/functions'; import { getCurrentCameraDeviceId } from '../../base/settings/functions.web'; @@ -83,39 +81,28 @@ interface IState { const styles = (theme: Theme) => { return { virtualBackgroundPreview: { - '& .video-preview': { - height: '250px' - }, - - '& .video-background-preview-entry': { - height: '250px', - width: '570px', - marginBottom: theme.spacing(2), - zIndex: 2, - - '@media (max-width: 632px)': { - maxWidth: '336px' - } - }, + height: 'auto', + width: '100%', + overflow: 'hidden', + marginBottom: theme.spacing(3), + zIndex: 2, + borderRadius: '3px', + backgroundColor: theme.palette.uiBackground, + position: 'relative' as const, '& .video-preview-loader': { - borderRadius: '6px', - backgroundColor: 'transparent', - height: '250px', - marginBottom: theme.spacing(2), - width: '572px', - position: 'fixed', - zIndex: 2, + height: '220px', '& svg': { - position: 'absolute', + position: 'absolute' as const, top: '40%', 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 { */ _renderPreviewEntry(data: Object) { const { t } = this.props; - const className = 'video-background-preview-entry'; if (this.state.loading) { return this._loadVideoPreview(); } if (!data) { return ( -
-
{t('deviceSelection.previewUnavailable')}
-
+
{t('deviceSelection.previewUnavailable')}
); } - const props: Object = { - className - }; return ( -
-
+