Compare commits

..

No commits in common. "jitihouse/main" and "jitsi-meet_8375" have entirely different histories.

25 changed files with 471 additions and 635 deletions

View File

@ -1,67 +0,0 @@
@keyframes rotateAroundY {
from { transform: rotateY(0deg); }
to { transform: rotateY(360deg); }
}
@keyframes rainbowRoad {
to {
background-position: 400% 0 !important;
}
}
body {
font-family: "Comic Sans MS", "Comic Sans", sans-serif !important;
}
a {
background: linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(255,154,0,1) 10%, rgba(208,222,33,1) 20%, rgba(79,220,74,1) 30%, rgba(63,218,216,1) 40%, rgba(47,201,226,1) 50%, rgba(28,127,238,1) 60%, rgba(95,21,242,1) 70%, rgba(186,12,248,1) 80%, rgba(251,7,217,1) 90%, rgba(255,0,0,1) 100%) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
-moz-background-clip: text !important;
-moz-text-fill-color: transparent !important;
animation: rainbowRoad 8s linear infinite !important;
}
a:hover {
}
.dominant-speaker {
box-shadow: inset 0px 0px 0px 4px rgba(255,0,255,0.33) !important;
}
.display-avatar-only {
background-image: url("");
}
.videocontainer:nth-child(odd) {
transform: rotate(1.5deg);
}
.videocontainer:nth-child(even) {
transform: rotate(-1.3deg);
}
#largeVideoContainer.videocontainer {
transform: none;
}
.sideToolbarContainer {
transform: rotate(-1.1deg);
}
.displayname:before {
content: "♡︎ ";
}
.displayname:after {
content: " ♡︎";
}
.avatar, .userAvatar {
transform: rotateY(0deg);
}
.avatar:hover, .userAvatar:hover {
animation: rotateAroundY 3.6s linear infinite;
}

View File

@ -94,9 +94,3 @@ $flagsImagePath: "../images/";
@import 'notifications';
/* Modules END */
/* Jeet crew BEGIN */
@import 'jiti';
/* Jeet crew END */

View File

@ -44,3 +44,61 @@
-webkit-animation-timing-function: ease-in-out;
animation-timing-function: ease-in-out
}
.feedback-dialog {
margin-bottom: 5px;
.details {
textarea {
min-height: 100px;
}
}
.input-control {
background-color: $feedbackInputBg;
color: $feedbackInputTextColor;
&::-webkit-input-placeholder {
color: $feedbackInputPlaceholderColor;
}
&::-moz-placeholder { /* Firefox 19+ */
color: $feedbackInputPlaceholderColor;
}
&:-ms-input-placeholder {
color: $feedbackInputPlaceholderColor;
}
}
.rating {
line-height: 1.2;
margin-top: 10px;
text-align: center;
.star-label {
font-size: 14px;
height: 16px;
}
.star-btn {
color: inherit;
cursor: pointer;
display: inline-block;
font-size: 34px;
outline: none;
position: relative;
text-decoration: none;
@include transition(all .2s ease);
&.active,
&:hover,
&.starHover {
color: #36B37E;
};
}
.star-btn:focus,
.star-btn:active {
outline: 1px solid #B8C7E0;
}
}
}

View File

@ -9,7 +9,7 @@
*/
var interfaceConfig = {
APP_NAME: 'JitSea 🏴‍☠️',
APP_NAME: 'Jitsi Meet',
AUDIO_LEVEL_PRIMARY_COLOR: 'rgba(255,255,255,0.4)',
AUDIO_LEVEL_SECONDARY_COLOR: 'rgba(255,255,255,0.2)',

View File

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

View File

@ -106,8 +106,6 @@ 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';
@ -115,6 +113,7 @@ 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';
@ -799,8 +798,7 @@ function initCommands() {
APP.store.dispatch(overwriteConfig(whitelistedConfig));
},
'toggle-virtual-background': () => {
APP.store.dispatch(toggleDialog(SettingsDialog, {
defaultTab: SETTINGS_TABS.VIRTUAL_BACKGROUND }));
APP.store.dispatch(toggleDialog(VirtualBackgroundDialog));
},
'end-conference': () => {
APP.store.dispatch(endConference());

10
package-lock.json generated
View File

@ -72,7 +72,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1589.0.0+d43c349d/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1588.0.0+04e906cc/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@ -13416,8 +13416,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1589.0.0+d43c349d/lib-jitsi-meet.tgz",
"integrity": "sha512-6QuR109o4sq24c9EU73NGLWAdJO+piiEylsqtmOL/B+I2GMTFeIras0tMOl6eQpncpZS5nD9gqiJmTNDnZqWbw==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1588.0.0+04e906cc/lib-jitsi-meet.tgz",
"integrity": "sha512-f9TGluJXEKj+YbLHtk51rNzh3R1HsVduN3pVY5onMuSsYAKtf8IF5qy4EQolwzj7JzHLg0foBJKDka2NmcaIaQ==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.0.0",
@ -30308,8 +30308,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1589.0.0+d43c349d/lib-jitsi-meet.tgz",
"integrity": "sha512-6QuR109o4sq24c9EU73NGLWAdJO+piiEylsqtmOL/B+I2GMTFeIras0tMOl6eQpncpZS5nD9gqiJmTNDnZqWbw==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1588.0.0+04e906cc/lib-jitsi-meet.tgz",
"integrity": "sha512-f9TGluJXEKj+YbLHtk51rNzh3R1HsVduN3pVY5onMuSsYAKtf8IF5qy4EQolwzj7JzHLg0foBJKDka2NmcaIaQ==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",

View File

@ -77,7 +77,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1589.0.0+d43c349d/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1588.0.0+04e906cc/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.7158 3.03843C12.4964 2.33696 11.5037 2.33696 11.2842 3.03843L9.54263 8.60636C9.44381 8.92229 9.14957 9.13606 8.81858 9.13242L2.98497 9.0682C2.25003 9.06011 1.94325 10.0043 2.54258 10.4297L7.29982 13.8067C7.56974 13.9983 7.68213 14.3442 7.57638 14.6579L5.71262 20.1861C5.47782 20.8826 6.28099 21.4661 6.87081 21.0276L11.5525 17.5467C11.8182 17.3492 12.1819 17.3492 12.4475 17.5467L17.1293 21.0276C17.7191 21.4661 18.5223 20.8826 18.2875 20.1861L16.4237 14.6579C16.3179 14.3442 16.4303 13.9983 16.7003 13.8067L21.4575 10.4297C22.0568 10.0043 21.75 9.06011 21.0151 9.0682L15.1815 9.13242C14.8505 9.13606 14.5563 8.92228 14.4574 8.60636L12.7158 3.03843Z" />
</svg>

Before

Width:  |  Height:  |  Size: 814 B

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 5.77465L10.9742 9.05416C10.6778 10.0019 9.79505 10.6433 8.80206 10.6323L5.36607 10.5945L8.16808 12.5835C8.97785 13.1583 9.31502 14.1961 8.99778 15.1371L7.90002 18.3932L10.6576 16.343C11.4545 15.7505 12.5456 15.7505 13.3425 16.343L16.1001 18.3932L15.0023 15.1371C14.6851 14.1961 15.0222 13.1583 15.832 12.5835L18.634 10.5945L15.198 10.6323C14.205 10.6433 13.3223 10.0019 13.0258 9.05416L12 5.77465ZM12.7158 3.03843C12.4964 2.33696 11.5037 2.33696 11.2842 3.03843L9.54263 8.60636C9.44381 8.92229 9.14957 9.13606 8.81858 9.13242L2.98497 9.0682C2.25003 9.06011 1.94325 10.0043 2.54258 10.4297L7.29982 13.8067C7.56974 13.9983 7.68213 14.3442 7.57638 14.6579L5.71262 20.1861C5.47782 20.8826 6.28099 21.4661 6.87081 21.0276L11.5525 17.5467C11.8182 17.3492 12.1819 17.3492 12.4475 17.5467L17.1293 21.0276C17.7191 21.4661 18.5223 20.8826 18.2875 20.1861L16.4237 14.6579C16.3179 14.3442 16.4303 13.9983 16.7003 13.8067L21.4575 10.4297C22.0568 10.0043 21.75 9.06011 21.0151 9.0682L15.1815 9.13242C14.8505 9.13606 14.5563 8.92228 14.4574 8.60636L12.7158 3.03843Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -39,8 +39,6 @@ export { default as IconExclamationSolid } from './exclamation-solid.svg';
export { default as IconExclamationTriangle } from './exclamation-triangle.svg';
export { default as IconExitFullscreen } from './exit-fullscreen.svg';
export { default as IconFaceSmile } from './face-smile.svg';
export { default as IconFavorite } from './favorite.svg';
export { default as IconFavoriteSolid } from './favorite-solid.svg';
export { default as IconFeedback } from './feedback.svg';
export { default as IconGear } from './gear.svg';
export { default as IconGoogle } from './google.svg';

View File

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

View File

@ -1,74 +1,23 @@
import { Theme } from '@mui/material';
import { ClassNameMap, withStyles } from '@mui/styles';
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
// @flow
import { createFeedbackOpenEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState, IStore } from '../../app/types';
import { IJitsiConference } from '../../base/conference/reducer';
import StarIcon from '@atlaskit/icon/glyph/star';
import StarFilledIcon from '@atlaskit/icon/glyph/star-filled';
import React, { Component } from 'react';
import type { Dispatch } from 'redux';
import {
createFeedbackOpenEvent,
sendAnalytics
} from '../../analytics';
import { isMobileBrowser } from '../../base/environment/utils';
import { translate } from '../../base/i18n/functions';
import Icon from '../../base/icons/components/Icon';
import { IconFavorite, IconFavoriteSolid } from '../../base/icons/svg';
import { withPixelLineHeight } from '../../base/styles/functions.web';
import { translate } from '../../base/i18n';
import { connect } from '../../base/redux';
import Dialog from '../../base/ui/components/web/Dialog';
import Input from '../../base/ui/components/web/Input';
import { cancelFeedback, submitFeedback } from '../actions';
const styles = (theme: Theme) => {
return {
dialog: {
marginBottom: theme.spacing(1)
},
rating: {
display: 'flex',
flexDirection: 'column' as const,
alignItems: 'center',
justifyContent: 'center',
marginTop: theme.spacing(4),
marginBottom: theme.spacing(3)
},
ratingLabel: {
...withPixelLineHeight(theme.typography.bodyShortBold),
color: theme.palette.text01,
marginBottom: theme.spacing(2),
height: '20px'
},
stars: {
display: 'flex'
},
starBtn: {
display: 'inline-block',
cursor: 'pointer',
marginRight: theme.spacing(3),
'&:last-of-type': {
marginRight: 0
},
'&.active svg': {
fill: theme.palette.success01
},
'&:focus': {
outline: `1px solid ${theme.palette.action01}`,
borderRadius: '4px'
}
},
details: {
'& textarea': {
minHeight: '122px'
}
}
};
};
declare var APP: Object;
declare var interfaceConfig: Object;
const scoreAnimationClass
= interfaceConfig.ENABLE_FEEDBACK_ANIMATION ? 'shake-rotate' : '';
@ -85,51 +34,49 @@ const SCORES = [
'feedback.veryGood'
];
const ICON_SIZE = 32;
type Scrollable = {
scroll: Function;
};
scroll: Function
}
/**
* The type of the React {@code Component} props of {@link FeedbackDialog}.
*/
interface IProps extends WithTranslation {
type Props = {
/**
* The cached feedback message, if any, that was set when closing a previous
* instance of {@code FeedbackDialog}.
*/
_message: string;
_message: string,
/**
* The cached feedback score, if any, that was set when closing a previous
* instance of {@code FeedbackDialog}.
*/
_score: number;
/**
* An object containing the CSS classes.
*/
classes: ClassNameMap<string>;
_score: number,
/**
* The JitsiConference that is being rated. The conference is passed in
* because feedback can occur after a conference has been left, so
* references to it may no longer exist in redux.
*/
conference: IJitsiConference;
conference: Object,
/**
* Invoked to signal feedback submission or canceling.
*/
dispatch: IStore['dispatch'];
dispatch: Dispatch<any>,
/**
* Callback invoked when {@code FeedbackDialog} is unmounted.
*/
onClose: Function;
}
onClose: Function,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* The type of the React {@code Component} state of {@link FeedbackDialog}.
@ -139,20 +86,20 @@ type State = {
/**
* The currently entered feedback message.
*/
message: string;
message: string,
/**
* The score selection index which is currently being hovered. The value -1
* is used as a sentinel value to match store behavior of using -1 for no
* score having been selected.
*/
mousedOverScore: number;
mousedOverScore: number,
/**
* The currently selected score selection index. The score will not be 0
* indexed so subtract one to map with SCORES.
*/
score: number;
score: number
};
/**
@ -162,19 +109,13 @@ type State = {
*
* @augments Component
*/
class FeedbackDialog extends Component<IProps, State> {
class FeedbackDialog extends Component<Props, State> {
/**
* An array of objects with click handlers for each of the scores listed in
* the constant SCORES. This pattern is used for binding event handlers only
* once for each score selection icon.
*/
_scoreClickConfigurations: Array<{
_onClick: (e: React.MouseEvent) => void;
_onKeyDown: (e: React.KeyboardEvent) => void;
_onMouseOver: (e: React.MouseEvent) => void;
}>;
_onScrollTop: (node: Scrollable | null) => void;
_scoreClickConfigurations: Array<Object>;
/**
* Initializes a new {@code FeedbackDialog} instance.
@ -182,7 +123,7 @@ class FeedbackDialog extends Component<IProps, State> {
* @param {Object} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: IProps) {
constructor(props: Props) {
super(props);
const { _message, _score } = this.props;
@ -216,9 +157,8 @@ class FeedbackDialog extends Component<IProps, State> {
this._scoreClickConfigurations = SCORES.map((textKey, index) => {
return {
_onClick: () => this._onScoreSelect(index),
_onKeyDown: (e: React.KeyboardEvent) => {
_onKeyPres: e => {
if (e.key === ' ' || e.key === 'Enter') {
e.stopPropagation();
e.preventDefault();
this._onScoreSelect(index);
}
@ -236,8 +176,8 @@ class FeedbackDialog extends Component<IProps, State> {
// On some mobile browsers opening Feedback dialog scrolls down the whole content because of the keyboard.
// By scrolling to the top we prevent hiding the feedback stars so the user knows those exist.
this._onScrollTop = (node: Scrollable | null) => {
node?.scroll?.(0, 0);
this._onScrollTop = (node: ?Scrollable) => {
node && node.scroll && node.scroll(0, 0);
};
}
@ -275,14 +215,14 @@ class FeedbackDialog extends Component<IProps, State> {
const scoreToDisplayAsSelected
= mousedOverScore > -1 ? mousedOverScore : score;
const { classes, t } = this.props;
const { t } = this.props;
const scoreIcons = this._scoreClickConfigurations.map(
(config, index) => {
const isFilled = index <= scoreToDisplayAsSelected;
const activeClass = isFilled ? 'active' : '';
const className
= `${classes.starBtn} ${scoreAnimationClass} ${activeClass}`;
= `star-btn ${scoreAnimationClass} ${activeClass}`;
return (
<span
@ -290,19 +230,19 @@ class FeedbackDialog extends Component<IProps, State> {
className = { className }
key = { index }
onClick = { config._onClick }
onKeyDown = { config._onKeyDown }
onKeyPress = { config._onKeyPres }
role = 'button'
tabIndex = { 0 }
{ ...(isMobileBrowser() ? {} : {
onMouseOver: config._onMouseOver
}) }>
{ isFilled
? <Icon
size = { ICON_SIZE }
src = { IconFavoriteSolid } />
: <Icon
size = { ICON_SIZE }
src = { IconFavorite } /> }
? <StarFilledIcon
label = 'star-filled'
size = 'xlarge' />
: <StarIcon
label = 'star'
size = 'xlarge' /> }
</span>
);
});
@ -315,24 +255,23 @@ class FeedbackDialog extends Component<IProps, State> {
}}
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
size = 'large'
titleKey = 'feedback.rateExperience'>
<div className = { classes.dialog }>
<div className = { classes.rating }>
<div className = 'feedback-dialog'>
<div className = 'rating'>
<div
aria-label = { this.props.t('feedback.star') }
className = { classes.ratingLabel } >
className = 'star-label' >
<p id = 'starLabel'>
{ t(SCORES[scoreToDisplayAsSelected]) }
</p>
</div>
<div
className = { classes.stars }
className = 'stars'
onMouseLeave = { this._onScoreContainerMouseLeave }>
{ scoreIcons }
</div>
</div>
<div className = { classes.details }>
<div className = 'details'>
<Input
autoFocus = { true }
id = 'feedbackTextArea'
@ -346,6 +285,8 @@ class FeedbackDialog extends Component<IProps, State> {
);
}
_onCancel: () => boolean;
/**
* Dispatches an action notifying feedback was not submitted. The submitted
* score will have one added as the rest of the app does not expect 0
@ -363,6 +304,8 @@ class FeedbackDialog extends Component<IProps, State> {
return true;
}
_onMessageChange: (Object) => void;
/**
* Updates the known entered feedback message.
*
@ -371,7 +314,7 @@ class FeedbackDialog extends Component<IProps, State> {
* @private
* @returns {void}
*/
_onMessageChange(newValue: string) {
_onMessageChange(newValue) {
this.setState({ message: newValue });
}
@ -382,10 +325,12 @@ class FeedbackDialog extends Component<IProps, State> {
* @private
* @returns {void}
*/
_onScoreSelect(score: number) {
_onScoreSelect(score) {
this.setState({ score });
}
_onScoreContainerMouseLeave: () => void;
/**
* Sets the currently hovered score to null to indicate no hover is
* occurring.
@ -405,10 +350,12 @@ class FeedbackDialog extends Component<IProps, State> {
* @private
* @returns {void}
*/
_onScoreMouseOver(mousedOverScore: number) {
_onScoreMouseOver(mousedOverScore) {
this.setState({ mousedOverScore });
}
_onSubmit: () => void;
/**
* Dispatches the entered feedback for submission. The submitted score will
* have one added as the rest of the app does not expect 0 indexing.
@ -426,6 +373,8 @@ class FeedbackDialog extends Component<IProps, State> {
return true;
}
_onScrollTop: (node: ?Scrollable) => void;
}
/**
@ -437,7 +386,7 @@ class FeedbackDialog extends Component<IProps, State> {
* @returns {{
* }}
*/
function _mapStateToProps(state: IReduxState) {
function _mapStateToProps(state) {
const { message, score } = state['features/feedback'];
return {
@ -458,4 +407,4 @@ function _mapStateToProps(state: IReduxState) {
};
}
export default withStyles(styles)(translate(connect(_mapStateToProps)(FeedbackDialog)));
export default translate(connect(_mapStateToProps)(FeedbackDialog));

View File

@ -11,8 +11,6 @@ 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,
@ -26,8 +24,7 @@ import {
getMoreTabProps,
getNotificationsTabProps,
getProfileTabProps,
getShortcutsTabProps,
getVirtualBackgroundTabProps
getShortcutsTabProps
} from './functions';
/**
@ -252,31 +249,3 @@ 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,7 +8,6 @@ import {
IconCalendar,
IconGear,
IconHost,
IconImage,
IconShortcuts,
IconUser,
IconVideo,
@ -25,14 +24,12 @@ import {
getAudioDeviceSelectionDialogProps,
getVideoDeviceSelectionDialogProps
} from '../../../device-selection/functions.web';
import { checkBlurSupport } from '../../../virtual-background/functions';
import {
submitModeratorTab,
submitMoreTab,
submitNotificationsTab,
submitProfileTab,
submitShortcutsTab,
submitVirtualBackgroundTab
submitShortcutsTab
} from '../../actions';
import { SETTINGS_TABS } from '../../constants';
import {
@ -41,8 +38,7 @@ import {
getNotificationsMap,
getNotificationsTabProps,
getProfileTabProps,
getShortcutsTabProps,
getVirtualBackgroundTabProps
getShortcutsTabProps
} from '../../functions';
// @ts-ignore
@ -52,7 +48,6 @@ 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
@ -259,7 +254,6 @@ 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<any>[] = [];
if (showDeviceSettings) {
@ -311,37 +305,12 @@ 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: ReturnType<typeof getNotificationsTabProps>) => {
propsUpdateFunction: (tabState: any, newProps: any) => {
return {
...newProps,
enabledNotifications: tabState?.enabledNotifications || {}
@ -404,7 +373,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
component: ShortcutsTab,
labelKey: 'settings.shortcuts',
props: getShortcutsTabProps(state, isDisplayedOnWelcomePage),
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getShortcutsTabProps>) => {
propsUpdateFunction: (tabState: any, newProps: any) => {
// Updates tab props, keeping users selection
return {

View File

@ -1,107 +0,0 @@
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,6 +3,7 @@ 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';
@ -12,8 +13,7 @@ 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 { openSettingsDialog } from '../../../actions';
import { SETTINGS_TABS } from '../../../constants';
import VirtualBackgroundDialog from '../../../../virtual-background/components/VirtualBackgroundDialog';
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(openSettingsDialog(SETTINGS_TABS.VIRTUAL_BACKGROUND)),
selectBackground: () => dispatch(openDialog(VirtualBackgroundDialog)),
changeFlip: (flip: boolean) => {
dispatch(updateSettings({
localFlipX: flip

View File

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

View File

@ -12,7 +12,6 @@ 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';
@ -294,22 +293,3 @@ 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
};
}

View File

@ -6,7 +6,6 @@ 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';
@ -41,25 +40,24 @@ 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(3),
marginRight: theme.spacing(2),
'& svg': {
fill: `${theme.palette.link01} !important`
fill: '#669aec !important'
}
},
input: {
button: {
display: 'none'
},
label: {
fontSize: '14px',
fontWeight: 600,
lineHeight: '20px',
marginTop: theme.spacing(3),
marginBottom: theme.spacing(2),
color: '#669aec',
display: 'inline-flex',
cursor: 'pointer'
}
};
});
@ -129,14 +127,14 @@ function UploadImageButton({
tabIndex = { 0 } >
<Icon
className = { classes.addBackground }
size = { 24 }
size = { 20 }
src = { IconPlus } />
{t('virtualBackground.addBackground')}
</label>}
<input
accept = 'image/*'
className = { classes.input }
className = { classes.button }
id = 'file-upload'
onChange = { uploadImage }
ref = { uploadImageButton }

View File

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

View File

@ -6,22 +6,27 @@ import Bourne from '@hapi/bourne';
import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
import React, { useCallback, useEffect, useState } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../app/types';
import { getMultipleVideoSendingSupportFeatureFlag } from '../../base/config/functions.any';
import { hideDialog } from '../../base/dialog/actions';
import { translate } from '../../base/i18n/functions';
import Icon from '../../base/icons/components/Icon';
import { IconCloseLarge } from '../../base/icons/svg';
import { withPixelLineHeight } from '../../base/styles/functions.web';
import { connect } from '../../base/redux/functions';
import { updateSettings } from '../../base/settings/actions';
// @ts-ignore
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 { toDataURL } from '../functions';
import logger from '../logger';
import UploadImageButton from './UploadImageButton';
// @ts-ignore
import VirtualBackgroundPreview from './VirtualBackgroundPreview';
/* eslint-enable lines-around-comment */
@ -33,7 +38,7 @@ interface IProps extends WithTranslation {
_images: Array<Image>;
/**
* Returns the jitsi track that will have background effect applied.
* Returns the jitsi track that will have backgraund effect applied.
*/
_jitsiTrack: Object;
@ -47,6 +52,11 @@ interface IProps extends WithTranslation {
*/
_multiStreamModeEnabled: boolean;
/**
* Returns the selected thumbnail identifier.
*/
_selectedThumbnail: string;
/**
* If the upload button should be displayed or not.
*/
@ -68,131 +78,192 @@ interface IProps extends WithTranslation {
* NOTE: currently used only for electron in order to open the dialog in the correct state after desktop sharing
* selection.
*/
initialOptions?: Object;
/**
* Options change handler.
*/
onOptionsChange: Function;
/**
* Virtual background options.
*/
options: any;
/**
* Returns the selected thumbnail identifier.
*/
selectedThumbnail: string;
initialOptions: Object;
}
const onError = (event: any) => {
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 => {
return {
virtualBackgroundLoading: {
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '50px'
dialogContainer: {
width: 'auto'
},
container: {
width: '100%',
display: 'flex',
flexDirection: 'column'
},
thumbnailContainer: {
width: '100%',
dialog: {
alignSelf: 'flex-start',
position: 'relative',
maxHeight: '300px',
color: 'white',
display: 'inline-grid',
gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr',
gap: theme.spacing(1),
gridTemplateColumns: 'auto auto auto auto auto',
columnGap: '9px',
cursor: 'pointer',
'@media (min-width: 608px) and (max-width: 712px)': {
gridTemplateColumns: '1fr 1fr 1fr 1fr'
},
'@media (max-width: 607px)': {
gridTemplateColumns: '1fr 1fr 1fr',
gap: theme.spacing(2)
}
},
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]: {
// @ts-ignore
[[ '& .desktop-share:hover',
'& .thumbnail:hover',
'& .blur:hover',
'& .slight-blur:hover',
'& .virtual-background-none:hover' ]]: {
opacity: 0.5,
cursor: 'pointer',
'& ~ .delete-image-icon': {
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'
}
},
'@media (max-width: 607px)': {
height: '70px'
}
},
selectedThumbnail: {
border: `2px solid ${theme.palette.action01Hover}`
},
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'
'& .thumbnail': {
objectFit: 'cover'
},
[[ '&:hover', '&:focus' ] as any]: {
'& .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
[[ '& .desktop-share:hover',
'& .thumbnail:hover',
'& .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'
},
virtualBackgroundLoading: {
overflow: 'hidden',
position: 'fixed',
left: '50%',
marginTop: '10px',
transform: 'translateX(-50%)'
}
};
});
@ -202,28 +273,24 @@ const useStyles = makeStyles()(theme => {
*
* @returns {ReactElement}
*/
function VirtualBackgrounds({
function VirtualBackground({
_images,
_jitsiTrack,
_localFlipX,
selectedThumbnail,
_selectedThumbnail,
_showUploadButton,
_virtualBackground,
onOptionsChange,
options,
dispatch,
initialOptions,
t
}: IProps) {
const { classes, cx } = useStyles();
const [ previewIsLoaded, setPreviewIsLoaded ] = useState(false);
const [ options, setOptions ] = useState<any>({ ...initialOptions });
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && Bourne.parse(localImages)) || []);
const [ loading, setLoading ] = useState(false);
useEffect(() => {
onOptionsChange({ ...initialOptions });
}, []);
const [ initialVirtualBackground ] = useState(_virtualBackground);
const deleteStoredImage = useCallback(e => {
const imageId = e.currentTarget.getAttribute('data-imageid');
@ -253,7 +320,7 @@ function VirtualBackgrounds({
}, [ storedImages ]);
const enableBlur = useCallback(async () => {
onOptionsChange({
setOptions({
backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
enabled: true,
blurValue: 25,
@ -271,7 +338,7 @@ function VirtualBackgrounds({
}, [ enableBlur ]);
const enableSlideBlur = useCallback(async () => {
onOptionsChange({
setOptions({
backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
enabled: true,
blurValue: 8,
@ -289,7 +356,7 @@ function VirtualBackgrounds({
}, [ enableSlideBlur ]);
const removeBackground = useCallback(async () => {
onOptionsChange({
setOptions({
enabled: false,
selectedThumbnail: 'none'
});
@ -309,7 +376,7 @@ function VirtualBackgrounds({
const image = storedImages.find(img => img.id === imageId);
if (image) {
onOptionsChange({
setOptions({
backgroundType: 'image',
enabled: true,
url: image.src,
@ -327,7 +394,7 @@ function VirtualBackgrounds({
try {
const url = await toDataURL(image.src);
onOptionsChange({
setOptions({
backgroundType: 'image',
enabled: true,
url,
@ -356,12 +423,48 @@ function VirtualBackgrounds({
}
}, [ 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 => {
await setPreviewIsLoaded(loaded);
}, []);
return (
<>
<Dialog
className = { classes.dialogContainer }
ok = {{
disabled: !options || loading || !previewIsLoaded,
translationKey: 'virtualBackground.apply'
}}
onCancel = { cancelVirtualBackground }
onSubmit = { applyVirtualBackground }
size = 'large'
titleKey = 'virtualBackground.title' >
<VirtualBackgroundPreview
loadedPreview = { loadedPreviewState }
options = { options } />
@ -378,22 +481,23 @@ function VirtualBackgrounds({
{_showUploadButton
&& <UploadImageButton
setLoading = { setLoading }
setOptions = { onOptionsChange }
setOptions = { setOptions }
setStoredImages = { setStoredImages }
showLabel = { previewIsLoaded }
storedImages = { storedImages } />}
<div
className = { classes.thumbnailContainer }
className = { cx(classes.dialog, { [classes.dialogMarginTop]: previewIsLoaded }) }
role = 'radiogroup'
tabIndex = { -1 }>
<Tooltip
content = { t('virtualBackground.removeBackground') }
position = { 'top' }>
<div
aria-checked = { selectedThumbnail === 'none' }
aria-checked = { _selectedThumbnail === 'none' }
aria-label = { t('virtualBackground.removeBackground') }
className = { cx(classes.thumbnail, classes.noneThumbnail,
selectedThumbnail === 'none' && classes.selectedThumbnail) }
className = { cx('background-option', 'virtual-background-none', {
'none-selected': _selectedThumbnail === 'none'
}) }
onClick = { removeBackground }
onKeyPress = { removeBackgroundKeyPress }
role = 'radio'
@ -405,10 +509,11 @@ function VirtualBackgrounds({
content = { t('virtualBackground.slightBlur') }
position = { 'top' }>
<div
aria-checked = { selectedThumbnail === 'slight-blur' }
aria-checked = { _selectedThumbnail === 'slight-blur' }
aria-label = { t('virtualBackground.slightBlur') }
className = { cx(classes.thumbnail, classes.slightBlur,
selectedThumbnail === 'slight-blur' && classes.selectedThumbnail) }
className = { cx('background-option', 'slight-blur', {
'slight-blur-selected': _selectedThumbnail === 'slight-blur'
}) }
onClick = { enableSlideBlur }
onKeyPress = { enableSlideBlurKeyPress }
role = 'radio'
@ -420,10 +525,11 @@ function VirtualBackgrounds({
content = { t('virtualBackground.blur') }
position = { 'top' }>
<div
aria-checked = { selectedThumbnail === 'blur' }
aria-checked = { _selectedThumbnail === 'blur' }
aria-label = { t('virtualBackground.blur') }
className = { cx(classes.thumbnail, classes.blur,
selectedThumbnail === 'blur' && classes.selectedThumbnail) }
className = { cx('background-option', 'blur', {
'blur-selected': _selectedThumbnail === 'blur'
}) }
onClick = { enableBlur }
onKeyPress = { enableBlurKeyPress }
role = 'radio'
@ -438,11 +544,11 @@ function VirtualBackgrounds({
position = { 'top' }>
<img
alt = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
aria-checked = { options?.selectedThumbnail === image.id
|| selectedThumbnail === image.id }
className = { cx(classes.thumbnail,
(options?.selectedThumbnail === image.id
|| selectedThumbnail === image.id) && classes.selectedThumbnail) }
aria-checked = { options.selectedThumbnail === image.id
|| _selectedThumbnail === image.id }
className = {
options.selectedThumbnail === image.id || _selectedThumbnail === image.id
? 'background-option thumbnail-selected' : 'background-option thumbnail' }
data-imageid = { image.id }
onClick = { setImageBackground }
onError = { onError }
@ -454,13 +560,15 @@ function VirtualBackgrounds({
))}
{storedImages.map((image, index) => (
<div
className = { classes.storedImageContainer }
className = { 'thumbnail-container' }
key = { image.id }>
<img
alt = { t('virtualBackground.uploadedImage', { index: index + 1 }) }
aria-checked = { selectedThumbnail === image.id }
className = { cx(classes.thumbnail,
selectedThumbnail === image.id && classes.selectedThumbnail) }
aria-checked = { _selectedThumbnail === image.id }
className = { cx('background-option', {
'thumbnail-selected': _selectedThumbnail === image.id,
'thumbnail': _selectedThumbnail !== image.id
}) }
data-imageid = { image.id }
onClick = { setUploadedImageBackground }
onError = { onError }
@ -471,12 +579,12 @@ function VirtualBackgrounds({
<Icon
ariaLabel = { t('virtualBackground.deleteImage') }
className = { cx(classes.deleteImageIcon, 'delete-image-icon') }
className = { 'delete-image-icon' }
data-imageid = { image.id }
onClick = { deleteStoredImage }
onKeyPress = { deleteStoredImageKeyPress }
role = 'button'
size = { 16 }
size = { 15 }
src = { IconCloseLarge }
tabIndex = { 0 } />
</div>
@ -484,30 +592,8 @@ function VirtualBackgrounds({
</div>
</div>
)}
</>
</Dialog>
);
}
/**
* 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));
export default VirtualBackgroundDialog;

View File

@ -8,6 +8,8 @@ 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';
@ -81,28 +83,39 @@ interface IState {
const styles = (theme: Theme) => {
return {
virtualBackgroundPreview: {
height: 'auto',
width: '100%',
overflow: 'hidden',
marginBottom: theme.spacing(3),
zIndex: 2,
borderRadius: '3px',
backgroundColor: theme.palette.uiBackground,
position: 'relative' as const,
'& .video-preview': {
height: '250px'
},
'& .video-preview-loader': {
height: '220px',
'& .video-background-preview-entry': {
height: '250px',
width: '570px',
marginBottom: theme.spacing(2),
zIndex: 2,
'& svg': {
position: 'absolute' as const,
top: '40%',
left: '45%'
'@media (max-width: 632px)': {
maxWidth: '336px'
}
},
'& .video-preview-error': {
height: '220px',
position: 'relative'
'& .video-preview-loader': {
borderRadius: '6px',
backgroundColor: 'transparent',
height: '250px',
marginBottom: theme.spacing(2),
width: '572px',
position: 'fixed',
zIndex: 2,
'& svg': {
position: 'absolute',
top: '40%',
left: '45%'
},
'@media (min-width: 432px) and (max-width: 632px)': {
width: '340px'
}
}
}
};
@ -225,21 +238,31 @@ class VirtualBackgroundPreview extends PureComponent<IProps, IState> {
*/
_renderPreviewEntry(data: Object) {
const { t } = this.props;
const className = 'video-background-preview-entry';
if (this.state.loading) {
return this._loadVideoPreview();
}
if (!data) {
return (
<div className = 'video-preview-error'>{t('deviceSelection.previewUnavailable')}</div>
<div
className = { className }
video-preview-container = { true }>
<div className = 'video-preview-error'>{t('deviceSelection.previewUnavailable')}</div>
</div>
);
}
const props: Object = {
className
};
return (
<Video
className = { videoClassName }
playsinline = { true }
videoTrack = {{ jitsiTrack: data }} />
<div { ...props }>
<Video
className = { videoClassName }
playsinline = { true }
videoTrack = {{ jitsiTrack: data }} />
</div>
);
}
@ -287,8 +310,8 @@ class VirtualBackgroundPreview extends PureComponent<IProps, IState> {
return (<div className = { classes.virtualBackgroundPreview }>
{jitsiTrack
? this._renderPreviewEntry(jitsiTrack)
: this._loadVideoPreview()
? <div className = 'video-preview'>{this._renderPreviewEntry(jitsiTrack)}</div>
: <div className = 'video-preview-loader'>{this._loadVideoPreview()}</div>
}</div>);
}
}

View File

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

View File

@ -1,10 +1,9 @@
<title>JitSea 🏴‍☠️</title>
<meta property="og:title" content="JitSea 🏴‍☠️"/>
<title>Jitsi Meet</title>
<meta property="og:title" content="Jitsi Meet"/>
<meta property="og:image" content="images/jitsilogo.png?v=1"/>
<meta property="og:description" content="meow meow meowmeow meow"/>
<meta description="meow meow meowmeow meow"/>
<meta itemprop="name" content="JitSea 🏴‍☠️"/>
<meta itemprop="description" content="meow meow meowmeow meow"/>
<meta property="og:description" content="Join a WebRTC video conference powered by the Jitsi Videobridge"/>
<meta description="Join a WebRTC video conference powered by the Jitsi Videobridge"/>
<meta itemprop="name" content="Jitsi Meet"/>
<meta itemprop="description" content="Join a WebRTC video conference powered by the Jitsi Videobridge"/>
<meta itemprop="image" content="images/jitsilogo.png?v=1"/>
<link rel="icon" type="image/png" href="images/favicon.ico?v=1"/>