Compare commits

..

1 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
38068b33e5 fix(notifications) remove dead code 2023-02-14 10:11:08 +01:00
53 changed files with 665 additions and 581 deletions

View File

@@ -141,7 +141,7 @@ import {
showNotification,
showWarningNotification
} from './react/features/notifications';
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay/actions';
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay';
import { suspendDetected } from './react/features/power-monitor';
import { initPrejoin, makePrecallTest, setJoiningInProgress } from './react/features/prejoin/actions';
import { isPrejoinPageVisible } from './react/features/prejoin/functions';

View File

@@ -4,7 +4,7 @@
&-content {
position: relative;
right: auto;
margin-bottom: 4px;
margin-bottom: 8px;
max-height: 456px;
overflow: auto;
width: 300px;

View File

@@ -3,38 +3,49 @@
display: inline-block;
&-container {
max-height: 456px;
max-height: 344px;
background: $menuBG;
border-radius: 3px;
overflow: auto;
margin-bottom: 4px;
position: relative;
right: auto;
padding: 8px;
margin-bottom: 8px;
}
&-entry {
cursor: pointer;
height: 138px;
width: 244px;
height: 168px;
margin-bottom: 8px;
position: relative;
margin: 0 7px 4px;
border-radius: 6px;
box-sizing: border-box;
overflow: hidden;
width: 284px;
&:last-child {
margin-bottom: 0;
}
&--selected {
border: 2px solid #4687ED;
border: 3px solid #31B76A;
border-radius: 3px;
cursor: default;
height: 162px;
width: 278px;
}
}
&-video {
border-radius: 3px;
height: 100%;
object-fit: cover;
width: 100%;
}
&-overlay {
background: rgba(42, 58, 75, 0.6);
height: 100%;
position: absolute;
width: 100%;
z-index: 1;
}
&-error {
align-items: center;
display: flex;
@@ -45,22 +56,23 @@
}
&-label {
bottom: 8px;
color: #fff;
position: absolute;
bottom: 0;
left: 0;
right: 0;
max-width: 100%;
padding: 8px;
width: 100%;
z-index: 2;
&-container {
margin: 0 16px;
}
&-text {
background-color: rgba(0, 0, 0, 0.7);
border-radius: 4px;
padding: 4px 8px;
color: #fff;
font-size: 12px;
line-height: 16px;
font-weight: 600;
background-color: #131519;
border-radius: 3px;
padding: 2px 8px;
font-size: 13px;
line-height: 20px;
margin: 0 auto;
max-width: calc(100% - 16px);
overflow: hidden;
text-overflow: ellipsis;
@@ -68,8 +80,8 @@
white-space: nowrap;
}
}
&-checkbox-container {
padding: 10px 14px;
// Override @atlaskit/InlineDialog container which is made with styled components
& > div:nth-child(2) {
padding: 0;
}
}

View File

@@ -1286,7 +1286,6 @@
"grantModerator": "Grant Moderator Rights",
"hideSelfView": "Hide self view",
"kick": "Kick out",
"mirrorVideo": "Mirror my video",
"moderator": "Moderator",
"mute": "Participant is muted",
"muted": "Muted",

10
package-lock.json generated
View File

@@ -73,7 +73,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/v1582.0.0+829f5ac0/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -13553,8 +13553,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1582.0.0+829f5ac0/lib-jitsi-meet.tgz",
"integrity": "sha512-w66tUaV9sYoUxTlLG+kHUq9SrUGEZ7IX4TMG0kS+3TkOUzaXWHf9W021nq2ighP3OTMdQRXXk6mHrrrnnrS/NQ==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
"integrity": "sha512-AAEClrQNOVHNO1lKr/F1SOyiduZfI6bql3eiIxC3LZ5cBcyoRVmwI6uiAbLail0VkuKnTecEWcfYZ9lvokxrMw==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.0.0",
@@ -30611,8 +30611,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1582.0.0+829f5ac0/lib-jitsi-meet.tgz",
"integrity": "sha512-w66tUaV9sYoUxTlLG+kHUq9SrUGEZ7IX4TMG0kS+3TkOUzaXWHf9W021nq2ighP3OTMdQRXXk6mHrrrnnrS/NQ==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
"integrity": "sha512-AAEClrQNOVHNO1lKr/F1SOyiduZfI6bql3eiIxC3LZ5cBcyoRVmwI6uiAbLail0VkuKnTecEWcfYZ9lvokxrMw==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",

View File

@@ -78,7 +78,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/v1582.0.0+829f5ac0/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",

View File

@@ -30,6 +30,8 @@ import {
// @ts-ignore
import { screen } from '../mobile/navigation/routes';
import { clearNotifications } from '../notifications/actions';
// @ts-ignore
import { setFatalError } from '../overlay';
import { addTrackStateToURL, getDefaultURL } from './functions.native';
import logger from './logger';
@@ -175,6 +177,7 @@ export function maybeRedirectToWelcomePage(options: any) { // eslint-disable-lin
*/
export function reloadNow() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
dispatch(setFatalError(undefined));
const state = getState();
const { locationURL } = state['features/base/connection'];

View File

@@ -20,6 +20,7 @@ import {
import { isVpaasMeeting } from '../jaas/functions';
import { clearNotifications, showNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { setFatalError } from '../overlay/actions';
import { isWelcomePageEnabled } from '../welcome/functions';
import {
@@ -221,6 +222,7 @@ export function maybeRedirectToWelcomePage(options: { feedbackSubmitted?: boolea
*/
export function reloadNow() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
dispatch(setFatalError(undefined));
const state = getState();
const { locationURL } = state['features/base/connection'];

View File

@@ -1,7 +1,10 @@
import React from 'react';
// @flow
import React, { Fragment } from 'react';
import { BaseApp } from '../../base/app';
import { toURLString } from '../../base/util';
import { OverlayContainer } from '../../overlay';
import { appNavigate } from '../actions';
import { getDefaultURL } from '../functions';
@@ -70,7 +73,23 @@ export class AbstractApp extends BaseApp<Props, *> {
}
}
_createMainElement: (React.ReactElement, Object) => ?React.ReactElement;
/**
* Creates an extra {@link ReactElement}s to be added (unconditionally)
* alongside the main element.
*
* @abstract
* @protected
* @returns {ReactElement}
*/
_createExtraElement() {
return (
<Fragment>
<OverlayContainer />
</Fragment>
);
}
_createMainElement: (React$Element<*>, Object) => ?React$Element<*>;
/**
* Gets the default URL to be opened when this {@code App} mounts.

View File

@@ -1,11 +1,12 @@
// @flow
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import React, { Fragment } from 'react';
import React from 'react';
import GlobalStyles from '../../base/ui/components/GlobalStyles.web';
import JitsiThemeProvider from '../../base/ui/components/JitsiThemeProvider.web';
import DialogContainer from '../../base/ui/components/web/DialogContainer';
import { ChromeExtensionBanner } from '../../chrome-extension-banner';
import OverlayContainer from '../../overlay/components/web/OverlayContainer';
import { AbstractApp } from './AbstractApp';
@@ -13,30 +14,12 @@ import { AbstractApp } from './AbstractApp';
import '../middlewares';
import '../reducers';
/**
* Root app {@code Component} on Web/React.
*
* @augments AbstractApp
*/
export class App extends AbstractApp {
/**
* Creates an extra {@link ReactElement}s to be added (unconditionally)
* alongside the main element.
*
* @abstract
* @protected
* @returns {ReactElement}
*/
_createExtraElement() {
return (
<Fragment>
<OverlayContainer />
</Fragment>
);
}
/**
* Overrides the parent method to inject {@link AtlasKitThemeProvider} as
* the top most component.

View File

@@ -52,7 +52,9 @@ function _setConfig({ dispatch, getState }: IStore, next: Function, action: AnyA
const settings = state['features/base/settings'];
const config: IConfig = {};
if (typeof settings.disableP2P !== 'undefined') {
// FIXME: P2P is currently temporality disabled on mobile.
// eslint-disable-next-line no-constant-condition
if (false && typeof settings.disableP2P !== 'undefined') {
config.p2p = { enabled: !settings.disableP2P };
}

View File

@@ -57,8 +57,10 @@ const INITIAL_RN_STATE: IConfig = {
// than requiring this override here...
p2p: {
// Temporarily disable P2P on mobile while we sort out some (codec?) issues.
enabled: false,
disabledCodec: 'vp9',
preferredCodec: 'vp8'
preferredCodec: 'h264'
},
videoQuality: {

View File

@@ -0,0 +1,43 @@
// @flow
import React, { Component } from 'react';
import { Container, Text } from '../../react';
import { type StyleType } from '../../styles';
import styles from './styles';
type Props = {
/**
* Children of the component.
*/
children: string | React$Node,
style: ?StyleType
};
/**
* Generic dialog content container to provide the same styling for all custom
* dialogs.
*/
export default class DialogContent extends Component<Props> {
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
const { children, style } = this.props;
const childrenComponent = typeof children === 'string'
? <Text style = { style }>{ children }</Text>
: children;
return (
<Container style = { styles.dialogContainer }>
{ childrenComponent }
</Container>
);
}
}

View File

@@ -1,3 +1,5 @@
// @flow
export * from './_';
export { default as DialogContent } from './DialogContent';

View File

@@ -1,226 +0,0 @@
/* eslint-disable lines-around-comment */
// @ts-ignore
import { randomInt } from '@jitsi/js-utils/random';
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import type { Dispatch } from 'redux';
import { appNavigate, reloadNow } from '../../../../app/actions.native';
import { IReduxState } from '../../../../app/types';
import { translate } from '../../../i18n/functions';
import { isFatalJitsiConnectionError } from '../../../lib-jitsi-meet/functions.native';
import { connect } from '../../../redux/functions';
// @ts-ignore
import logger from '../../logger';
// @ts-ignore
import ConfirmDialog from './ConfirmDialog';
/**
* The type of the React {@code Component} props of
* {@link PageReloadDialog}.
*/
interface IPageReloadDialogProps extends WithTranslation {
dispatch: Dispatch<any>;
isNetworkFailure: boolean;
reason: string;
}
/**
* The type of the React {@code Component} state of
* {@link PageReloadDialog}.
*/
interface IPageReloadDialogState {
message: string;
timeLeft: number;
timeoutSeconds: number;
title: string;
}
/**
* Implements a React Component that is shown before the
* conference is reloaded.
* Shows a warning message and counts down towards the re-load.
*/
class PageReloadDialog extends Component<IPageReloadDialogProps, IPageReloadDialogState> {
// @ts-ignore
_interval: IntervalID;
/**
* Initializes a new PageReloadOverlay instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
* @public
*/
constructor(props: IPageReloadDialogProps) {
super(props);
const timeoutSeconds = 10 + randomInt(0, 20);
let message, title;
if (this.props.isNetworkFailure) {
title = 'dialog.conferenceDisconnectTitle';
message = 'dialog.conferenceDisconnectMsg';
} else {
title = 'dialog.conferenceReloadTitle';
message = 'dialog.conferenceReloadMsg';
}
this.state = {
message,
timeLeft: timeoutSeconds,
timeoutSeconds,
title
};
this._onCancel = this._onCancel.bind(this);
this._onReloadNow = this._onReloadNow.bind(this);
}
/**
* React Component method that executes once component is mounted.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
const { dispatch } = this.props;
const { timeLeft } = this.state;
logger.info(
`The conference will be reloaded after ${
this.state.timeoutSeconds} seconds.`);
this._interval
= setInterval(
() => {
if (timeLeft === 0) {
if (this._interval) {
clearInterval(this._interval);
this._interval = undefined;
}
dispatch(reloadNow());
} else {
this.setState(prevState => {
return {
timeLeft: prevState.timeLeft - 1
};
});
}
},
1000);
}
/**
* Clears the timer interval.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
if (this._interval) {
clearInterval(this._interval);
this._interval = undefined;
}
}
/**
* Handle clicking of the "Cancel" button. It will navigate back to the
* welcome page.
*
* @private
* @returns {boolean}
*/
_onCancel() {
clearInterval(this._interval);
this.props.dispatch(appNavigate(undefined));
return true;
}
/**
* Handle clicking on the "Reload Now" button. It will navigate to the same
* conference URL as before immediately, without waiting for the timer to
* kick in.
*
* @private
* @returns {boolean}
*/
_onReloadNow() {
clearInterval(this._interval);
this.props.dispatch(reloadNow());
return true;
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const { message, timeLeft, title } = this.state;
return (
<ConfirmDialog
cancelLabel = 'dialog.Cancel'
confirmLabel = 'dialog.rejoinNow'
descriptionKey = { `${t(message, { seconds: timeLeft })}` }
onCancel = { this._onCancel }
onSubmit = { this._onReloadNow }
title = { title } />
);
}
}
/**
* Maps (parts of) the redux state to the associated component's props.
*
* @param {Object} state - The redux state.
* @protected
* @returns {{
* message: string,
* reason: string,
* title: string
* }}
*/
function mapStateToProps(state: IReduxState) {
const { error: conferenceError } = state['features/base/conference'];
const { error: configError } = state['features/base/config'];
const { error: connectionError } = state['features/base/connection'];
const { fatalError } = state['features/overlay'];
const fatalConnectionError
// @ts-ignore
= connectionError && isFatalJitsiConnectionError(connectionError);
const fatalConfigError = fatalError === configError;
const isNetworkFailure = fatalConfigError || fatalConnectionError;
let reason;
if (conferenceError) {
reason = `error.conference.${conferenceError.name}`;
} else if (connectionError) {
reason = `error.conference.${connectionError.name}`;
} else if (configError) {
reason = `error.config.${configError.name}`;
} else {
logger.error('No reload reason defined!');
}
return {
isNetworkFailure,
reason
};
}
export default translate(connect(mapStateToProps)(PageReloadDialog));

View File

@@ -5,7 +5,6 @@ export { default as ConfirmDialog } from './ConfirmDialog';
export { default as DialogContainer } from './DialogContainer';
export { default as AlertDialog } from './AlertDialog';
export { default as InputDialog } from './InputDialog';
export { default as PageReloadDialog } from './PageReloadDialog';
// NOTE: Some dialogs reuse the style of these base classes for consistency
// and as we're in a /native namespace, it's safe to export the styles.

View File

@@ -0,0 +1,14 @@
import { BoxModel, createStyleSheet } from '../../styles';
/**
* The React {@code Component} styles of {@code Dialog}.
*/
export default createStyleSheet({
/**
* Unified container for a consistent Dialog style.
*/
dialogContainer: {
paddingHorizontal: BoxModel.padding,
paddingVertical: 1.5 * BoxModel.padding
}
});

View File

@@ -0,0 +1,5 @@
/**
* Placeholder styles for web to be able to use cross platform components
* unmodified such as {@code DialogContent}.
*/
export default {};

View File

@@ -1,12 +1,9 @@
/* eslint-disable lines-around-comment */
import { IStateful } from '../app/types';
import { toState } from '../redux/functions';
// @ts-ignore
import JitsiMeetJS from './_';
const JitsiConferenceErrors = JitsiMeetJS.errors.conference;
const JitsiConnectionErrors = JitsiMeetJS.errors.connection;

View File

@@ -1,4 +1,5 @@
// @ts-ignore
// @flow
import Video from './web/Video';
export default Video;

View File

@@ -34,9 +34,6 @@ const getComputedOuterHeight = (element: HTMLElement) => {
interface IProps {
/**
* ARIA attributes.
*/
[key: `aria-${string}`]: string;
/**
@@ -109,11 +106,6 @@ interface IProps {
*/
onMouseLeave?: (e?: React.MouseEvent) => void;
/**
* Container role.
*/
role?: string;
/**
* Tab index for the menu.
*/
@@ -175,9 +167,7 @@ const ContextMenu = ({
onDrawerClose,
onMouseEnter,
onMouseLeave,
role,
tabIndex,
...aria
tabIndex
}: IProps) => {
const [ isHidden, setIsHidden ] = useState(true);
const containerRef = useRef<HTMLDivElement | null>(null);
@@ -235,7 +225,6 @@ const ContextMenu = ({
</Drawer>
</JitsiPortal>
: <div
{ ...aria }
aria-label = { accessibilityLabel }
className = { cx(participantsPaneTheme.ignoredChildClassName,
styles.contextMenu,
@@ -248,7 +237,7 @@ const ContextMenu = ({
onMouseEnter = { onMouseEnter }
onMouseLeave = { onMouseLeave }
ref = { containerRef }
role = { role ?? 'menu' }
role = 'menu'
tabIndex = { tabIndex }>
{children}
</div>;

View File

@@ -16,7 +16,6 @@ import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { LobbyScreen } from '../../../lobby';
import { getIsLobbyVisible } from '../../../lobby/functions';
import { getOverlayToRender } from '../../../overlay/functions.web';
import { ParticipantsPane } from '../../../participants-pane/components/web';
import Prejoin from '../../../prejoin/components/web/Prejoin';
import { isPrejoinPageVisible } from '../../../prejoin/functions';
@@ -34,7 +33,6 @@ import type { AbstractProps } from '../AbstractConference';
import ConferenceInfo from './ConferenceInfo';
import { default as Notice } from './Notice';
declare var APP: Object;
declare var interfaceConfig: Object;
@@ -61,11 +59,6 @@ type Props = AbstractProps & {
*/
_backgroundAlpha: number,
/**
* Are any overlays visible?
*/
_isAnyOverlayVisible: boolean,
/**
* The CSS class to apply to the root of {@link Conference} to modify the
* application layout.
@@ -205,7 +198,6 @@ class Conference extends AbstractConference<Props, *> {
*/
render() {
const {
_isAnyOverlayVisible,
_layoutClassName,
_notificationsVisible,
_overflowDrawer,
@@ -242,7 +234,7 @@ class Conference extends AbstractConference<Props, *> {
{ _showPrejoin || _showLobby || <Toolbox /> }
{_notificationsVisible && !_isAnyOverlayVisible && (_overflowDrawer
{_notificationsVisible && (_overflowDrawer
? <JitsiPortal className = 'notification-portal'>
{this.renderNotificationsContainer({ portal: true })}
</JitsiPortal>
@@ -391,7 +383,6 @@ function _mapStateToProps(state) {
return {
...abstractMapStateToProps(state),
_backgroundAlpha: backgroundAlpha,
_isAnyOverlayVisible: Boolean(getOverlayToRender(state)),
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
_mouseMoveCallbackInterval: mouseMoveCallbackInterval,
_overflowDrawer: overflowDrawer,

View File

@@ -1,7 +1,7 @@
import { IStateful } from '../base/app/types';
import { toState } from '../base/redux/functions';
import { areThereNotifications } from '../notifications/functions';
import { getOverlayToRender } from '../overlay/functions';
/**
* Tells whether or not the notifications should be displayed within
@@ -12,7 +12,10 @@ import { areThereNotifications } from '../notifications/functions';
*/
export function shouldDisplayNotifications(stateful: IStateful) {
const state = toState(stateful);
const isAnyOverlayVisible = Boolean(getOverlayToRender(state));
const { calleeInfoVisible } = state['features/invite'];
return areThereNotifications(state) && !calleeInfoVisible;
return areThereNotifications(state)
&& !isAnyOverlayVisible
&& !calleeInfoVisible;
}

View File

@@ -40,6 +40,7 @@ import { getLocalTracks, isLocalTrackMuted, toggleScreensharing } from '../../ba
import { CLOSE_CHAT, OPEN_CHAT } from '../../chat';
import { openChat } from '../../chat/actions';
import { closeChat, sendMessage, setPrivateMessageRecipient } from '../../chat/actions.any';
import { SET_PAGE_RELOAD_OVERLAY_CANCELED } from '../../overlay/actionTypes';
import { setRequestingSubtitles } from '../../subtitles';
import { muteLocal } from '../../video-menu/actions';
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
@@ -213,6 +214,16 @@ MiddlewareRegistry.register(store => next => action => {
}
break;
case SET_PAGE_RELOAD_OVERLAY_CANCELED:
sendEvent(
store,
CONFERENCE_TERMINATED,
/* data */ {
error: _toErrorString(action.error),
url: _normalizeUrl(store.getState()['features/base/connection'].locationURL)
});
break;
case SET_VIDEO_MUTED:
sendEvent(
store,

View File

@@ -1,18 +1,14 @@
/* eslint-disable lines-around-comment */
// @flow
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SafeAreaView, Text, View } from 'react-native';
// @ts-ignore
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
// @ts-ignore
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
import { LoadingIndicator } from '../../../base/react';
// @ts-ignore
import { TEXT_COLOR, navigationStyles } from './styles';
const ConnectingPage = () => {
const { t } = useTranslation();

View File

@@ -1,25 +1,16 @@
/* eslint-disable lines-around-comment */
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import React, { useCallback } from 'react';
import { StatusBar } from 'react-native';
import { IReduxState } from '../../../app/types';
import { connect } from '../../../base/redux/functions';
// @ts-ignore
import DialInSummary from '../../../invite/components/dial-in-summary/native/DialInSummary';
import { connect } from '../../../base/redux';
import { DialInSummary } from '../../../invite';
import Prejoin from '../../../prejoin/components/native/Prejoin';
// @ts-ignore
import WelcomePage from '../../../welcome/components/WelcomePage';
import { isWelcomePageEnabled } from '../../../welcome/functions';
// @ts-ignore
import { _ROOT_NAVIGATION_READY } from '../actionTypes';
// @ts-ignore
import { rootNavigationRef } from '../rootNavigationContainerRef';
// @ts-ignore
import { screen } from '../routes';
// @ts-ignore
import {
conferenceNavigationContainerScreenOptions,
connectingScreenOptions,
@@ -27,7 +18,6 @@ import {
navigationContainerTheme,
preJoinScreenOptions,
welcomeScreenOptions
// @ts-ignore
} from '../screenOptions';
import ConnectingPage from './ConnectingPage';
@@ -42,13 +32,13 @@ type Props = {
/**
* Redux dispatch function.
*/
dispatch: Function;
dispatch: Function,
/**
* Is welcome page available?
*/
isWelcomePageAvailable: boolean;
};
isWelcomePageAvailable: boolean
}
const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: Props) => {
@@ -110,7 +100,7 @@ const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: Props) =>
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function mapStateToProps(state: IReduxState) {
function mapStateToProps(state: Object) {
return {
isWelcomePageAvailable: isWelcomePageEnabled(state)
};

View File

@@ -13,3 +13,26 @@
*/
export const MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED
= 'MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED';
/**
* Adjust the state of the fatal error which shows/hides the reload screen. See
* action methods's description for more info about each of the fields.
*
* {
* type: SET_FATAL_ERROR,
* fatalError: ?Object
* }
* @public
*/
export const SET_FATAL_ERROR = 'SET_FATAL_ERROR';
/**
* The type of the Redux action which signals that the overlay was canceled.
*
* {
* type: export const SET_PAGE_RELOAD_OVERLAY_CANCELED
* }
* @public
*/
export const SET_PAGE_RELOAD_OVERLAY_CANCELED
= 'SET_PAGE_RELOAD_OVERLAY_CANCELED';

View File

@@ -1,31 +0,0 @@
/* eslint-disable max-len */
// @ts-ignore
import { PageReloadDialog, openDialog } from '../base/dialog';
/**
* Signals that the prompt for media permission is visible or not.
*
* @param {boolean} _isVisible - If the value is true - the prompt for media
* permission is visible otherwise the value is false/undefined.
* @param {string} _browser - The name of the current browser.
* @public
* @returns {{
* type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
* browser: {string},
* isVisible: {boolean}
* }}
*/
export function mediaPermissionPromptVisibilityChanged(_isVisible: boolean, _browser: string) {
// Dummy.
}
/**
* Opens {@link PageReloadDialog}.
*
* @returns {Function}
*/
export function openPageReloadDialog() {
return openDialog(PageReloadDialog);
}

View File

@@ -0,0 +1,62 @@
import {
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
SET_FATAL_ERROR,
SET_PAGE_RELOAD_OVERLAY_CANCELED
} from './actionTypes';
/**
* Signals that the prompt for media permission is visible or not.
*
* @param {boolean} isVisible - If the value is true - the prompt for media
* permission is visible otherwise the value is false/undefined.
* @param {string} browser - The name of the current browser.
* @public
* @returns {{
* type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
* browser: {string},
* isVisible: {boolean}
* }}
*/
export function mediaPermissionPromptVisibilityChanged(isVisible: boolean, browser: string) {
return {
type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
browser,
isVisible
};
}
/**
* The action indicates that an unrecoverable error has occurred and the reload
* screen will be displayed or hidden.
*
* @param {Object} fatalError - A critical error which was not claimed by any
* feature for error recovery (the recoverable flag was not set). If
* {@code undefined} then any fatal error currently stored will be discarded.
* @returns {{
* type: SET_FATAL_ERROR,
* fatalError: ?Error
* }}
*/
export function setFatalError(fatalError?: Object) {
return {
type: SET_FATAL_ERROR,
fatalError
};
}
/**
* The action indicates that the overlay was canceled.
*
* @param {Object} error - The error that caused the display of the overlay.
*
* @returns {{
* type: SET_PAGE_RELOAD_OVERLAY_CANCELED,
* error: ?Error
* }}
*/
export function setPageReloadOverlayCanceled(error: Object) {
return {
type: SET_PAGE_RELOAD_OVERLAY_CANCELED,
error
};
}

View File

@@ -1,32 +0,0 @@
import { MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED } from './actionTypes';
/**
* Signals that the prompt for media permission is visible or not.
*
* @param {boolean} isVisible - If the value is true - the prompt for media
* permission is visible otherwise the value is false/undefined.
* @param {string} browser - The name of the current browser.
* @public
* @returns {{
* type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
* browser: {string},
* isVisible: {boolean}
* }}
*/
export function mediaPermissionPromptVisibilityChanged(isVisible: boolean, browser: string) {
return {
type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
browser,
isVisible
};
}
/**
* Opens {@link PageReloadDialog}.
*
* @returns {Function}
*/
export function openPageReloadDialog() {
// Dummy
}

View File

@@ -7,15 +7,15 @@ import type { Dispatch } from 'redux';
import {
createPageReloadScheduledEvent,
sendAnalytics
} from '../../../analytics';
import { reloadNow } from '../../../app/actions';
} from '../../analytics';
import { reloadNow } from '../../app/actions';
import {
isFatalJitsiConferenceError,
isFatalJitsiConnectionError
} from '../../../base/lib-jitsi-meet/functions';
import logger from '../../logger';
} from '../../base/lib-jitsi-meet/functions';
import logger from '../logger';
import ReloadButton from './ReloadButton';
import ReloadButton from './web/ReloadButton';
declare var APP: Object;
@@ -91,7 +91,6 @@ type State = {
*/
export default class AbstractPageReloadOverlay<P: Props>
extends Component<P, State> {
/**
* Determines whether this overlay needs to be rendered (according to a
* specific redux state). Called by {@link OverlayContainer}.
@@ -101,18 +100,34 @@ export default class AbstractPageReloadOverlay<P: Props>
* {@code false}, otherwise.
*/
static needsRender(state: Object) {
const { error: conferenceError } = state['features/base/conference'];
const { error: configError } = state['features/base/config'];
const { error: connectionError } = state['features/base/connection'];
// FIXME web does not rely on the 'recoverable' flag set on an error
// action, but on a predefined list of fatal errors. Because of that
// the value of 'fatalError' which relies on the flag should not be used
// on web yet (until conference/connection and their errors handling is
// not unified).
return typeof APP === 'undefined'
? Boolean(state['features/overlay'].fatalError)
: this.needsRenderWeb(state);
}
const jitsiConnectionError
/**
* Determines whether this overlay needs to be rendered (according to a
* specific redux state). Called by {@link OverlayContainer}.
*
* @param {Object} state - The redux state.
* @returns {boolean} - If this overlay needs to be rendered, {@code true};
* {@code false}, otherwise.
*/
static needsRenderWeb(state: Object) {
const conferenceError = state['features/base/conference'].error;
const configError = state['features/base/config'].error;
const connectionError = state['features/base/connection'].error;
// @ts-ignore
= connectionError && isFatalJitsiConnectionError(connectionError);
const jitsiConferenceError
= conferenceError && isFatalJitsiConferenceError(conferenceError);
return jitsiConnectionError || jitsiConferenceError || configError;
return (
(connectionError && isFatalJitsiConnectionError(connectionError))
|| (conferenceError
&& isFatalJitsiConferenceError(conferenceError))
|| configError);
}
_interval: ?IntervalID;

View File

@@ -1,9 +1,11 @@
// @flow
import React, { Component } from 'react';
import { IReduxState } from '../../../app/types';
import { connect } from '../../../base/redux/functions';
import { getOverlayToRender } from '../../functions.web';
import { connect } from '../../base/redux';
import { getOverlayToRender } from '../functions';
declare var interfaceConfig: Object;
/**
* The type of the React {@link Component} props of {@code OverlayContainer}.
@@ -14,8 +16,8 @@ type Props = {
* The React {@link Component} type of overlay to be rendered by the
* associated {@code OverlayContainer}.
*/
overlay: any;
};
overlay: ?React$ComponentType<*>
}
/**
* Implements a React {@link Component} that will display the correct overlay
@@ -46,7 +48,7 @@ class OverlayContainer extends Component<Props> {
* overlay: ?Object
* }}
*/
function _mapStateToProps(state: IReduxState) {
function _mapStateToProps(state) {
return {
/**
* The React {@link Component} type of overlay to be rendered by the

View File

@@ -0,0 +1,3 @@
// @flow
export * from './native';

View File

@@ -0,0 +1,3 @@
// @flow
export * from './web';

View File

@@ -0,0 +1,4 @@
// @flow
export { default as OverlayContainer } from './OverlayContainer';
export * from './_';

View File

@@ -0,0 +1,38 @@
// @flow
import React, { Component, type Node } from 'react';
import { SafeAreaView, View } from 'react-native';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@code OverlayFrame}.
*/
type Props = {
/**
* The children components to be displayed into the overlay frame.
*/
children: Node,
};
/**
* Implements a React component to act as the frame for overlays.
*/
export default class OverlayFrame extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<View style = { styles.container }>
<SafeAreaView style = { styles.safeContainer } >
{ this.props.children }
</SafeAreaView>
</View>
);
}
}

View File

@@ -0,0 +1,95 @@
// @flow
import React from 'react';
import { appNavigate, reloadNow } from '../../../app/actions';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { setFatalError, setPageReloadOverlayCanceled } from '../../actions';
import AbstractPageReloadOverlay, {
type Props,
abstractMapStateToProps
} from '../AbstractPageReloadOverlay';
import OverlayFrame from './OverlayFrame';
/**
* Implements a React Component for page reload overlay. Shown before the
* conference is reloaded. Shows a warning message and counts down towards the
* reload.
*/
class PageReloadOverlay extends AbstractPageReloadOverlay<Props> {
_interval: IntervalID;
/**
* Initializes a new PageReloadOverlay instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
* @public
*/
constructor(props) {
super(props);
this._onCancel = this._onCancel.bind(this);
this._onReloadNow = this._onReloadNow.bind(this);
}
_onCancel: () => void;
/**
* Handle clicking of the "Cancel" button. It will navigate back to the
* welcome page.
*
* @private
* @returns {void}
*/
_onCancel() {
clearInterval(this._interval);
this.props.dispatch(setPageReloadOverlayCanceled(this.props.error));
this.props.dispatch(setFatalError(undefined));
this.props.dispatch(appNavigate(undefined));
}
_onReloadNow: () => void;
/**
* Handle clicking on the "Reload Now" button. It will navigate to the same
* conference URL as before immediately, without waiting for the timer to
* kick in.
*
* @private
* @returns {void}
*/
_onReloadNow() {
clearInterval(this._interval);
this.props.dispatch(reloadNow());
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const { message, timeLeft, title } = this.state;
return (
<OverlayFrame>
<ConfirmDialog
cancelLabel = 'dialog.Cancel'
confirmLabel = 'dialog.rejoinNow'
descriptionKey = { `${t(message, { seconds: timeLeft })}` }
onCancel = { this._onCancel }
onSubmit = { this._onReloadNow }
title = { title } />
</OverlayFrame>
);
}
}
export default translate(connect(abstractMapStateToProps)(PageReloadOverlay));

View File

@@ -0,0 +1,4 @@
// @flow
export { default as OverlayFrame } from './OverlayFrame';
export { default as PageReloadOverlay } from './PageReloadOverlay';

View File

@@ -0,0 +1,24 @@
// @flow
import { StyleSheet } from 'react-native';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
/**
* The React {@code Component} styles of the overlay feature.
*/
export default {
/**
* Style for a backdrop overlay covering the screen the the overlay is
* rendered.
*/
container: {
...StyleSheet.absoluteFillObject,
backgroundColor: BaseTheme.palette.ui00
},
safeContainer: {
flex: 1
}
};

View File

@@ -28,6 +28,6 @@ export default class AbstractSuspendedOverlay extends Component<Props> {
* {@code false}, otherwise.
*/
static needsRender(state: Object) {
return state['features/power-monitor']?.suspendDetected;
return state['features/power-monitor'].suspendDetected;
}
}

View File

@@ -1,4 +1,8 @@
import React, { Component, ReactChildren } from 'react';
// @flow
import React, { Component } from 'react';
declare var interfaceConfig: Object;
/**
* The type of the React {@code Component} props of {@link OverlayFrame}.
@@ -8,18 +12,18 @@ type Props = {
/**
* The children components to be displayed into the overlay frame.
*/
children: ReactChildren;
children: React$Node,
/**
* Indicates the css style of the overlay. If true, then lighter; darker,
* otherwise.
*/
isLightOverlay?: boolean;
isLightOverlay?: boolean,
/**
* The style property.
*/
style: Object;
style: Object
};
/**

View File

@@ -2,13 +2,13 @@
import React from 'react';
import { translate } from '../../../base/i18n/functions';
import { connect } from '../../../base/redux/functions';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractPageReloadOverlay, {
type Props,
abstractMapStateToProps
} from './AbstractPageReloadOverlay';
} from '../AbstractPageReloadOverlay';
import OverlayFrame from './OverlayFrame';
/**

View File

@@ -0,0 +1,7 @@
// @flow
export { default as OverlayFrame } from './OverlayFrame';
export { default as PageReloadOverlay } from './PageReloadOverlay';
export { default as SuspendedOverlay } from './SuspendedOverlay';
export { default as UserMediaPermissionsOverlay } from './UserMediaPermissionsOverlay';

View File

@@ -1,13 +1,7 @@
/* eslint-disable lines-around-comment */
import { IReduxState } from '../app/types';
// @ts-ignore
import PageReloadOverlay from './components/web/PageReloadOverlay';
// @ts-ignore
import SuspendedOverlay from './components/web/SuspendedOverlay';
// @ts-ignore
import UserMediaPermissionsOverlay from './components/web/UserMediaPermissionsOverlay';
import { getOverlays } from './overlays';
/**
* Returns the overlay to be currently rendered.
*
@@ -15,13 +9,7 @@ import UserMediaPermissionsOverlay from './components/web/UserMediaPermissionsOv
* @returns {?React$ComponentType<*>}
*/
export function getOverlayToRender(state: IReduxState) {
const overlays = [
PageReloadOverlay,
SuspendedOverlay,
UserMediaPermissionsOverlay
];
for (const overlay of overlays) {
for (const overlay of getOverlays()) {
// react-i18n / react-redux wrap components and thus we cannot access
// the wrapped component's static methods directly.
// @ts-ignore
@@ -34,3 +22,13 @@ export function getOverlayToRender(state: IReduxState) {
return undefined;
}
/**
* Returns the visibility of the media permissions prompt.
*
* @param {IReduxState} state - The Redux state.
* @returns {boolean}
*/
export function getMediaPermissionPromptVisibility(state: IReduxState) {
return state['features/overlay'].isMediaPermissionPromptVisible;
}

View File

@@ -0,0 +1,5 @@
// @flow
export * from './actions';
export * from './components';
export * from './functions';

View File

@@ -1,15 +1,12 @@
/* eslint-disable lines-around-comment */
import { IStore } from '../app/types';
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
import {
isFatalJitsiConferenceError,
isFatalJitsiConnectionError
} from '../base/lib-jitsi-meet/functions.any';
} from '../base/lib-jitsi-meet/functions';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { openPageReloadDialog } from './actions';
import { setFatalError } from './actions';
/**
* Error type. Basically like Error, but augmented with a recoverable property.
@@ -50,11 +47,12 @@ const ERROR_TYPES = {
/**
* Gets the error type and whether it's fatal or not.
*
* @param {Object} state - The redux state.
* @param {Function} getState - The redux function for fetching the current state.
* @param {Object|string} error - The error to process.
* @returns {void}
*/
const getErrorExtraInfo = (state: any, error: ErrorType) => {
const getErrorExtraInfo = (getState: IStore['getState'], error: ErrorType) => {
const state = getState();
const { error: conferenceError } = state['features/base/conference'];
const { error: configError } = state['features/base/config'];
const { error: connectionError } = state['features/base/connection'];
@@ -94,26 +92,20 @@ StateListenerRegistry.register(
return configError || connectionError || conferenceError;
},
/* listener */ (error: ErrorType, store: IStore) => {
const state = store.getState();
/* listener */ (error: ErrorType, { dispatch, getState }) => {
if (!error) {
return;
}
// eslint-disable-next-line no-negated-condition
if (typeof APP !== 'undefined') {
APP.API.notifyError({
...error,
...getErrorExtraInfo(state, error)
...getErrorExtraInfo(getState, error)
});
}
if (NON_OVERLAY_ERRORS.indexOf(error.name) === -1 && typeof error.recoverable === 'undefined') {
setTimeout(() => {
// @ts-ignore
store.dispatch(openPageReloadDialog());
}, 500);
dispatch(setFatalError(error));
}
}
);

View File

@@ -0,0 +1,15 @@
import { ReactElement } from 'react';
// @ts-ignore
import { PageReloadOverlay } from './components/native';
/**
* Returns the list of available platform specific overlays.
*
* @returns {Array<ReactElement>}
*/
export function getOverlays(): Array<ReactElement> {
return [
PageReloadOverlay
];
}

View File

@@ -0,0 +1,20 @@
import {
PageReloadOverlay,
SuspendedOverlay,
UserMediaPermissionsOverlay
// @ts-ignore
} from './components/web';
/**
* Returns the list of available platform specific overlays.
*
* @returns {Array<Object>}
*/
export function getOverlays(): Array<Object> {
return [
PageReloadOverlay,
SuspendedOverlay,
UserMediaPermissionsOverlay
];
}

View File

@@ -1,13 +1,17 @@
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from '../base/config/actionTypes';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { assign } from '../base/redux/functions';
import { MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED } from './actionTypes';
import { assign, set } from '../base/redux/functions';
import {
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
SET_FATAL_ERROR
} from './actionTypes';
export interface IOverlayState {
browser?: string;
fatalError?: Error;
isMediaPermissionPromptVisible?: boolean;
loadConfigOverlayVisible?: boolean;
}
/**
@@ -17,8 +21,18 @@ export interface IOverlayState {
*/
ReducerRegistry.register<IOverlayState>('features/overlay', (state = {}, action): IOverlayState => {
switch (action.type) {
case CONFIG_WILL_LOAD:
return _setShowLoadConfigOverlay(state, Boolean(action.room));
case LOAD_CONFIG_ERROR:
case SET_CONFIG:
return _setShowLoadConfigOverlay(state, false);
case MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED:
return _mediaPermissionPromptVisibilityChanged(state, action);
case SET_FATAL_ERROR:
return _setFatalError(state, action);
}
return state;
@@ -42,3 +56,29 @@ function _mediaPermissionPromptVisibilityChanged(
isMediaPermissionPromptVisible: isVisible
});
}
/**
* Sets the {@code LoadConfigOverlay} overlay visible or not.
*
* @param {Object} state - The redux state of the feature overlay.
* @param {boolean} show - Whether to show or not the overlay.
* @returns {Object} The new state of the feature overlay after the reduction of
* the specified action.
*/
function _setShowLoadConfigOverlay(state: IOverlayState, show?: boolean) {
return set(state, 'loadConfigOverlayVisible', show);
}
/**
* Reduces a specific redux action {@code SET_FATAL_ERROR} of the feature
* overlay.
*
* @param {Object} state - The redux state of the feature overlay.
* @param {Error} fatalError - If the value is set it indicates that a fatal
* error has occurred and that the reload screen is to be displayed.
* @returns {Object}
* @private
*/
function _setFatalError(state: IOverlayState, { fatalError }: { fatalError?: Error; }) {
return set(state, 'fatalError', fatalError);
}

View File

@@ -19,6 +19,7 @@ const useStyles = makeStyles()(theme => {
right: '-4px',
top: '-3px',
textAlign: 'center',
boxSizing: 'border-box',
paddingTop: '2px'
}
};

View File

@@ -1,63 +1,45 @@
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
// @flow
import React, { Component } from 'react';
import { translate } from '../../../../base/i18n';
import Video from '../../../../base/media/components/Video';
import { equals } from '../../../../base/redux';
import { createLocalVideoTracks } from '../../../functions';
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';
import { equals } from '../../../../base/redux/functions';
import { updateSettings } from '../../../../base/settings/actions';
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 { createLocalVideoTracks } from '../../../functions.web';
const videoClassName = 'video-preview-video flipVideoX';
/**
* The type of the React {@code Component} props of {@link VideoSettingsContent}.
*/
export interface IProps extends WithTranslation {
/**
* Callback to change the flip state.
*/
changeFlip: (flip: boolean) => void;
export type Props = {
/**
* The deviceId of the camera device currently being used.
*/
currentCameraDeviceId: string;
/**
* Whether or not the local video is flipped.
*/
localFlipX: boolean;
/**
* Open virtual background dialog.
*/
selectBackground: () => void;
currentCameraDeviceId: string,
/**
* Callback invoked to change current camera.
*/
setVideoInputDevice: Function;
setVideoInputDevice: Function,
/**
* Invoked to obtain translated strings.
*/
t: Function,
/**
* Callback invoked to toggle the settings popup visibility.
*/
toggleVideoSettings: Function;
toggleVideoSettings: Function,
/**
* All the camera device ids currently connected.
*/
videoDeviceIds: string[];
}
videoDeviceIds: string[],
};
/**
* The type of the React {@code Component} state of {@link VideoSettingsContent}.
@@ -67,7 +49,7 @@ type State = {
/**
* An array of all the jitsiTracks and eventual errors.
*/
trackData: { deviceId: string; error?: string; jitsiTrack: any | null; }[];
trackData: Object[],
};
/**
@@ -76,8 +58,9 @@ type State = {
*
* @augments Component
*/
class VideoSettingsContent extends Component<IProps, State> {
class VideoSettingsContent extends Component<Props, State> {
_componentWasUnmounted: boolean;
_videoContentRef: Object;
/**
* Initializes a new {@code VideoSettingsContent} instance.
@@ -85,9 +68,10 @@ class VideoSettingsContent extends Component<IProps, State> {
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: IProps) {
constructor(props) {
super(props);
this._onToggleFlip = this._onToggleFlip.bind(this);
this._onEscClick = this._onEscClick.bind(this);
this._videoContentRef = React.createRef();
this.state = {
trackData: new Array(props.videoDeviceIds.length).fill({
@@ -95,16 +79,20 @@ class VideoSettingsContent extends Component<IProps, State> {
})
};
}
_onEscClick: (KeyboardEvent) => void;
/**
* Toggles local video flip state.
* Click handler for the video entries.
*
* @param {KeyboardEvent} event - Esc key click to close the popup.
* @returns {void}
*/
_onToggleFlip() {
const { localFlipX, changeFlip } = this.props;
changeFlip(!localFlipX);
_onEscClick(event) {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
this._videoContentRef.current.style.display = 'none';
}
}
/**
@@ -134,9 +122,9 @@ class VideoSettingsContent extends Component<IProps, State> {
* @param {Object[]} trackData - An array of tracks that are to be disposed.
* @returns {Promise<void>}
*/
_disposeTracks(trackData: { jitsiTrack: any; }[]) {
_disposeTracks(trackData) {
trackData.forEach(({ jitsiTrack }) => {
jitsiTrack?.dispose();
jitsiTrack && jitsiTrack.dispose();
});
}
@@ -146,7 +134,7 @@ class VideoSettingsContent extends Component<IProps, State> {
* @param {string} deviceId - The id of the camera device.
* @returns {Function}
*/
_onEntryClick(deviceId: string) {
_onEntryClick(deviceId) {
return () => {
this.props.setVideoInputDevice(deviceId);
this.props.toggleVideoSettings();
@@ -160,7 +148,7 @@ class VideoSettingsContent extends Component<IProps, State> {
* @param {number} index - The index of the entry.
* @returns {React$Node}
*/
_renderPreviewEntry(data: { deviceId: string; error?: string; jitsiTrack: any | null; }, index: number) {
_renderPreviewEntry(data, index) {
const { error, jitsiTrack, deviceId } = data;
const { currentCameraDeviceId, t } = this.props;
const isSelected = deviceId === currentCameraDeviceId;
@@ -179,19 +167,19 @@ class VideoSettingsContent extends Component<IProps, State> {
);
}
const props: any = {
const props: Object = {
className,
key,
tabIndex
};
const label = jitsiTrack?.getTrackLabel();
const label = jitsiTrack && jitsiTrack.getTrackLabel();
if (isSelected) {
props['aria-checked'] = true;
props.className = `${className} video-preview-entry--selected`;
} else {
props.onClick = this._onEntryClick(deviceId);
props.onKeyPress = (e: React.KeyboardEvent) => {
props.onKeyPress = e => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
props.onClick();
@@ -204,10 +192,12 @@ class VideoSettingsContent extends Component<IProps, State> {
{ ...props }
role = 'radio'>
<div className = 'video-preview-label'>
{label && <div className = 'video-preview-label-text'>
<span>{label}</span>
{label && <div className = 'video-preview-label-container'>
<div className = 'video-preview-label-text'>
<span>{label}</span></div>
</div>}
</div>
<div className = 'video-preview-overlay' />
<Video
className = { videoClassName }
playsinline = { true }
@@ -240,7 +230,7 @@ class VideoSettingsContent extends Component<IProps, State> {
*
* @inheritdoc
*/
componentDidUpdate(prevProps: IProps) {
componentDidUpdate(prevProps) {
if (!equals(this.props.videoDeviceIds, prevProps.videoDeviceIds)) {
this._setTracks();
}
@@ -253,57 +243,21 @@ class VideoSettingsContent extends Component<IProps, State> {
*/
render() {
const { trackData } = this.state;
const { selectBackground, t, localFlipX } = this.props;
return (
<ContextMenu
<div
aria-labelledby = 'video-settings-button'
className = 'video-preview-container'
hidden = { false }
id = 'video-settings-dialog'
onKeyDown = { this._onEscClick }
ref = { this._videoContentRef }
role = 'radiogroup'
tabIndex = { -1 }>
<ContextMenuItemGroup>
{trackData.map((data, i) => this._renderPreviewEntry(data, i))}
</ContextMenuItemGroup>
<ContextMenuItemGroup>
<ContextMenuItem
accessibilityLabel = 'virtualBackground.title'
icon = { IconImage }
onClick = { selectBackground }
text = { t('virtualBackground.title') } />
<div
className = 'video-preview-checkbox-container'
// eslint-disable-next-line react/jsx-no-bind
onClick = { e => e.stopPropagation() }>
<Checkbox
checked = { localFlipX }
label = { t('videothumbnail.mirrorVideo') }
onChange = { this._onToggleFlip } />
</div>
</ContextMenuItemGroup>
</ContextMenu>
tabIndex = '-1'>
{trackData.map((data, i) => this._renderPreviewEntry(data, i))}
</div>
);
}
}
const mapStateToProps = (state: IReduxState) => {
const { localFlipX } = state['features/base/settings'];
return {
localFlipX: Boolean(localFlipX)
};
};
const mapDispatchToProps = (dispatch: IStore['dispatch']) => {
return {
selectBackground: () => dispatch(openDialog(VirtualBackgroundDialog)),
changeFlip: (flip: boolean) => {
dispatch(updateSettings({
localFlipX: flip
}));
}
};
};
export default translate(connect(mapStateToProps, mapDispatchToProps)(VideoSettingsContent));
export default translate(VideoSettingsContent);

View File

@@ -1,7 +1,7 @@
import React, { ReactNode } from 'react';
import { connect } from 'react-redux';
// @flow
import React from 'react';
import { IReduxState } from '../../../../app/types';
import {
setVideoInputDeviceAndUpdateSettings
} from '../../../../base/devices/actions.web';
@@ -9,35 +9,36 @@ import {
getVideoDeviceIds
} from '../../../../base/devices/functions.web';
import Popover from '../../../../base/popover/components/Popover.web';
import { connect } from '../../../../base/redux';
import { SMALL_MOBILE_WIDTH } from '../../../../base/responsive-ui/constants';
import { getCurrentCameraDeviceId } from '../../../../base/settings/functions.web';
import { getCurrentCameraDeviceId } from '../../../../base/settings';
import { toggleVideoSettings } from '../../../actions';
import { getVideoSettingsVisibility } from '../../../functions.web';
import { getVideoSettingsVisibility } from '../../../functions';
import VideoSettingsContent, { type IProps as VideoSettingsProps } from './VideoSettingsContent';
import VideoSettingsContent, { type Props as VideoSettingsProps } from './VideoSettingsContent';
interface IProps extends VideoSettingsProps {
type Props = VideoSettingsProps & {
/**
* Component children (the Video button).
*/
children: ReactNode;
children: React$Node,
/**
* Flag controlling the visibility of the popup.
*/
isOpen: boolean;
isOpen: boolean,
/**
* Callback executed when the popup closes.
*/
onClose: Function;
onClose: Function,
/**
* The popup placement enum value.
*/
popupPlacement: string;
popupPlacement: string
}
/**
@@ -53,7 +54,7 @@ function VideoSettingsPopup({
popupPlacement,
setVideoInputDevice,
videoDeviceIds
}: IProps) {
}: Props) {
return (
<div className = 'video-preview'>
<Popover
@@ -79,14 +80,14 @@ function VideoSettingsPopup({
* @param {Object} state - Redux state.
* @returns {Object}
*/
function mapStateToProps(state: IReduxState) {
function mapStateToProps(state) {
const { clientWidth } = state['features/base/responsive-ui'];
return {
currentCameraDeviceId: getCurrentCameraDeviceId(state),
isOpen: Boolean(getVideoSettingsVisibility(state)),
popupPlacement: clientWidth <= Number(SMALL_MOBILE_WIDTH) ? 'auto' : 'top-end',
videoDeviceIds: getVideoDeviceIds(state) ?? []
isOpen: getVideoSettingsVisibility(state),
popupPlacement: clientWidth <= SMALL_MOBILE_WIDTH ? 'auto' : 'top-end',
videoDeviceIds: getVideoDeviceIds(state)
};
}

View File

@@ -377,7 +377,7 @@ const styles = () => {
rowGap: '8px',
margin: 0,
padding: '16px',
marginBottom: '4px'
marginBottom: '8px'
}
};
};