Compare commits

..

13 Commits

Author SHA1 Message Date
Horatiu Muresan
8cd62bc132 fix(external-api) Unpin all participants when participant id is null (#12921) 2023-02-16 18:14:10 +02:00
Jaya Allamsetty
123a74b38b fix(video-quality) Add pinned participants to selectedSources.
When mulltiple videos are pinned to the stage filmstrip, the expectation is that the bridge will forward all the videos even if they are of lower quality. For this, the video sources need to be added to selectedSources instead of onStageSources.
2023-02-16 10:36:22 -05:00
Christoph Settgast
dbeca806bb lang: update German translation 2023-02-15 14:03:20 -06:00
Jaya Allamsetty
f790d3e3ed chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1582.0.0+829f5ac0...v1583.0.0+931ca368
2023-02-15 13:59:05 -05:00
Horatiu Muresan
a12f7fc4d2 fix(follow-me-pinning) Fix pin/unpin when follow-me (#12911) 2023-02-15 19:08:08 +02:00
Gabriel Borlea
456ce38a10 fix(context-menu): set height for context menu when it does not have enough space at top 2023-02-15 19:06:57 +02:00
Gabriel Borlea
72ef1668f2 fix(video-background): set dialog add button margin to right size 2023-02-15 19:06:57 +02:00
Jaya Allamsetty
fce8f52574 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1578.0.0+5855ca72...v1582.0.0+829f5ac0
2023-02-15 09:15:01 -05:00
Titus Moldovan
8fcfd7a308 fix(rn) makes the preferedCode vp8 and enabled p2p 2023-02-14 16:40:38 +01:00
Saúl Ibarra Corretgé
04a41395c8 fix(notifications) remove dead code 2023-02-14 11:51:47 +01:00
Robert Pintilii
18e8201167 fix(participants-counter) Style fix (#12907) 2023-02-14 12:40:29 +02:00
Robert Pintilii
27b8794d8c feat(video-picker) Redesign (#12902)
Convert some files to TS
Implement redesign
Add Virtual background and Flip video to picker menu
2023-02-14 12:15:37 +02:00
Calinteodor
3cb0df579c feat(overlay): native page reload dialog (#12667)
feat(overlay): native feature removal + replaced PageReloadOverlay with PageReloadDialog
2023-02-14 11:50:46 +02:00
59 changed files with 652 additions and 675 deletions

View File

@@ -141,7 +141,7 @@ import {
showNotification,
showWarningNotification
} from './react/features/notifications';
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay';
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay/actions';
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: 8px;
margin-bottom: 4px;
max-height: 456px;
overflow: auto;
width: 300px;

View File

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

View File

@@ -916,6 +916,7 @@
"localRecordingVideoWarning": "Um Ihr eigenes Kamerabild aufzuzeichnen, müssen Sie Ihre Kamera beim Start der Aufnahme einschalten",
"localRecordingWarning": "Bitte prüfen Sie, dass das aktuelle Tab auswählen, um Bild und Ton aufzuzeichnen. Die Länge der Aufzeichnung ist aktuell auf 1GB beschränkt, was ungefähr 100 Minuten entspricht.",
"loggedIn": "Als {{userName}} angemeldet",
"noMicPermission": "Zugriff auf Mikrofon fehlgeschlagen. Bitte erlauben Sie den Zugriff auf das Mikrofon.",
"noStreams": "Kein Ton oder Video erkannt.",
"off": "Aufnahme gestoppt",
"offBy": "{{name}} stoppte die Aufnahme",
@@ -1285,6 +1286,7 @@
"grantModerator": "Moderationsrechte vergeben",
"hideSelfView": "Eigene Ansicht ausblenden",
"kick": "Hinauswerfen",
"mirrorVideo": "Mein Video spiegeln",
"moderator": "Moderation",
"mute": "Person ist stumm geschaltet",
"muted": "Stummgeschaltet",

View File

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

View File

@@ -71,8 +71,13 @@ import {
import { appendSuffix } from '../../react/features/display-name';
import { isEnabled as isDropboxEnabled } from '../../react/features/dropbox';
import { setMediaEncryptionKey, toggleE2EE } from '../../react/features/e2ee/actions';
import { addStageParticipant, resizeFilmStrip, setVolume } from '../../react/features/filmstrip/actions.web';
import { isStageFilmstripAvailable } from '../../react/features/filmstrip/functions.web';
import {
addStageParticipant,
resizeFilmStrip,
setVolume,
togglePinStageParticipant
} from '../../react/features/filmstrip/actions.web';
import { getPinnedActiveParticipants, isStageFilmstripAvailable } from '../../react/features/filmstrip/functions.web';
import { invite } from '../../react/features/invite';
import {
selectParticipantInLargeVideo
@@ -241,6 +246,22 @@ function initCommands() {
logger.debug('Pin participant command received');
const state = APP.store.getState();
// if id not provided, unpin everybody.
if (!id) {
if (isStageFilmstripAvailable(state)) {
const pinnedParticipants = getPinnedActiveParticipants(state);
pinnedParticipants?.forEach(p => {
APP.store.dispatch(togglePinStageParticipant(p.participantId));
});
} else {
APP.store.dispatch(pinParticipant());
}
return;
}
const participant = videoType === VIDEO_TYPE.DESKTOP
? getVirtualScreenshareParticipantByOwnerId(state, id) : getParticipantById(state, id);
@@ -254,7 +275,7 @@ function initCommands() {
const participantId = participant.id;
if (isStageFilmstripAvailable(APP.store.getState())) {
if (isStageFilmstripAvailable(state)) {
APP.store.dispatch(addStageParticipant(participantId, true));
} else {
APP.store.dispatch(pinParticipant(participantId));

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/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1583.0.0+931ca368/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/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
"integrity": "sha512-AAEClrQNOVHNO1lKr/F1SOyiduZfI6bql3eiIxC3LZ5cBcyoRVmwI6uiAbLail0VkuKnTecEWcfYZ9lvokxrMw==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1583.0.0+931ca368/lib-jitsi-meet.tgz",
"integrity": "sha512-lzgRkJtdlZ7bfq2seBuONRM6UND8NJVjMOZPlVoq7uP4UuxffBztsoHGc0g5Y5zEqi1AnfYLwVZZvXkpd82iew==",
"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/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
"integrity": "sha512-AAEClrQNOVHNO1lKr/F1SOyiduZfI6bql3eiIxC3LZ5cBcyoRVmwI6uiAbLail0VkuKnTecEWcfYZ9lvokxrMw==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1583.0.0+931ca368/lib-jitsi-meet.tgz",
"integrity": "sha512-lzgRkJtdlZ7bfq2seBuONRM6UND8NJVjMOZPlVoq7uP4UuxffBztsoHGc0g5Y5zEqi1AnfYLwVZZvXkpd82iew==",
"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/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1583.0.0+931ca368/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",

View File

@@ -30,8 +30,6 @@ 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';
@@ -177,7 +175,6 @@ 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,7 +20,6 @@ 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 {
@@ -222,7 +221,6 @@ 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,10 +1,7 @@
// @flow
import React, { Fragment } from 'react';
import React from 'react';
import { BaseApp } from '../../base/app';
import { toURLString } from '../../base/util';
import { OverlayContainer } from '../../overlay';
import { appNavigate } from '../actions';
import { getDefaultURL } from '../functions';
@@ -73,23 +70,7 @@ export class AbstractApp extends BaseApp<Props, *> {
}
}
/**
* 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<*>;
_createMainElement: (React.ReactElement, Object) => ?React.ReactElement;
/**
* Gets the default URL to be opened when this {@code App} mounts.

View File

@@ -1,12 +1,11 @@
// @flow
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import React from 'react';
import React, { Fragment } 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';
@@ -14,12 +13,30 @@ 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,9 +52,7 @@ function _setConfig({ dispatch, getState }: IStore, next: Function, action: AnyA
const settings = state['features/base/settings'];
const config: IConfig = {};
// FIXME: P2P is currently temporality disabled on mobile.
// eslint-disable-next-line no-constant-condition
if (false && typeof settings.disableP2P !== 'undefined') {
if (typeof settings.disableP2P !== 'undefined') {
config.p2p = { enabled: !settings.disableP2P };
}

View File

@@ -57,10 +57,8 @@ 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: 'h264'
preferredCodec: 'vp8'
},
videoQuality: {

View File

@@ -1,43 +0,0 @@
// @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,5 +1,3 @@
// @flow
export * from './_';
export { default as DialogContent } from './DialogContent';

View File

@@ -0,0 +1,226 @@
/* 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,6 +5,7 @@ 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

@@ -1,14 +0,0 @@
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

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

View File

@@ -1,9 +1,12 @@
/* 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,5 +1,4 @@
// @flow
// @ts-ignore
import Video from './web/Video';
export default Video;

View File

@@ -7,6 +7,7 @@ import JitsiPortal from '../../../../toolbox/components/web/JitsiPortal';
import { showOverflowDrawer } from '../../../../toolbox/functions.web';
import participantsPaneTheme from '../../../components/themes/participantsPaneTheme.json';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { spacing } from '../../Tokens';
/**
* Get a style property from a style declaration as a float.
@@ -34,6 +35,9 @@ const getComputedOuterHeight = (element: HTMLElement) => {
interface IProps {
/**
* ARIA attributes.
*/
[key: `aria-${string}`]: string;
/**
@@ -106,6 +110,11 @@ interface IProps {
*/
onMouseLeave?: (e?: React.MouseEvent) => void;
/**
* Container role.
*/
role?: string;
/**
* Tab index for the menu.
*/
@@ -167,7 +176,9 @@ const ContextMenu = ({
onDrawerClose,
onMouseEnter,
onMouseLeave,
tabIndex
role,
tabIndex,
...aria
}: IProps) => {
const [ isHidden, setIsHidden ] = useState(true);
const containerRef = useRef<HTMLDivElement | null>(null);
@@ -184,9 +195,22 @@ const ContextMenu = ({
&& offsetTarget.offsetParent instanceof HTMLElement
) {
const { current: container } = containerRef;
// make sure the max height is not set
// @ts-ignore
container.style.maxHeight = null;
const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
const outerHeight = getComputedOuterHeight(container);
const height = Math.min(MAX_HEIGHT, outerHeight);
let outerHeight = getComputedOuterHeight(container);
let height = Math.min(MAX_HEIGHT, outerHeight);
if (offsetTop + height > offsetHeight + scrollTop && height > offsetTop) {
// top offset and + padding + border
container.style.maxHeight = `${offsetTop - ((spacing[2] * 2) + 2)}px`;
}
// get the height after style changes
outerHeight = getComputedOuterHeight(container);
height = Math.min(MAX_HEIGHT, outerHeight);
container.style.top = offsetTop + height > offsetHeight + scrollTop
? `${offsetTop - outerHeight}`
@@ -225,6 +249,7 @@ const ContextMenu = ({
</Drawer>
</JitsiPortal>
: <div
{ ...aria }
aria-label = { accessibilityLabel }
className = { cx(participantsPaneTheme.ignoredChildClassName,
styles.contextMenu,
@@ -237,7 +262,7 @@ const ContextMenu = ({
onMouseEnter = { onMouseEnter }
onMouseLeave = { onMouseLeave }
ref = { containerRef }
role = 'menu'
role = { role ?? 'menu' }
tabIndex = { tabIndex }>
{children}
</div>;

View File

@@ -16,6 +16,7 @@ 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';
@@ -33,6 +34,7 @@ import type { AbstractProps } from '../AbstractConference';
import ConferenceInfo from './ConferenceInfo';
import { default as Notice } from './Notice';
declare var APP: Object;
declare var interfaceConfig: Object;
@@ -59,6 +61,11 @@ 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.
@@ -198,6 +205,7 @@ class Conference extends AbstractConference<Props, *> {
*/
render() {
const {
_isAnyOverlayVisible,
_layoutClassName,
_notificationsVisible,
_overflowDrawer,
@@ -234,7 +242,7 @@ class Conference extends AbstractConference<Props, *> {
{ _showPrejoin || _showLobby || <Toolbox /> }
{_notificationsVisible && (_overflowDrawer
{_notificationsVisible && !_isAnyOverlayVisible && (_overflowDrawer
? <JitsiPortal className = 'notification-portal'>
{this.renderNotificationsContainer({ portal: true })}
</JitsiPortal>
@@ -383,6 +391,7 @@ 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,10 +12,7 @@ import { getOverlayToRender } from '../overlay/functions';
*/
export function shouldDisplayNotifications(stateful: IStateful) {
const state = toState(stateful);
const isAnyOverlayVisible = Boolean(getOverlayToRender(state));
const { calleeInfoVisible } = state['features/invite'];
return areThereNotifications(state)
&& !isAnyOverlayVisible
&& !calleeInfoVisible;
return areThereNotifications(state) && !calleeInfoVisible;
}

View File

@@ -9,7 +9,7 @@ import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { updateSettings } from '../base/settings/actions';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { addStageParticipant, setFilmstripVisible } from '../filmstrip/actions';
import { addStageParticipant, removeStageParticipant, setFilmstripVisible } from '../filmstrip/actions';
import { setTileView } from '../video-layout/actions.any';
import {
@@ -180,9 +180,19 @@ function _onFollowMeCommand(attributes: any = {}, id: string, store: IStore) {
if (attributes.pinnedStageParticipants !== undefined) {
const stageParticipants = JSON.parse(attributes.pinnedStageParticipants);
let oldStageParticipants = [];
if (!_.isEqual(stageParticipants, oldState.pinnedStageParticipants)) {
stageParticipants.forEach((p: { participantId: string; }) =>
if (oldState.pinnedStageParticipants !== undefined) {
oldStageParticipants = JSON.parse(oldState.pinnedStageParticipants);
}
if (!_.isEqual(stageParticipants, oldStageParticipants)) {
const toRemove = _.differenceWith(oldStageParticipants, stageParticipants, _.isEqual);
const toAdd = _.differenceWith(stageParticipants, oldStageParticipants, _.isEqual);
toRemove.forEach((p: { participantId: string; }) =>
store.dispatch(removeStageParticipant(p.participantId)));
toAdd.forEach((p: { participantId: string; }) =>
store.dispatch(addStageParticipant(p.participantId, true)));
}
}

View File

@@ -40,7 +40,6 @@ 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';
@@ -214,16 +213,6 @@ 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,14 +1,18 @@
// @flow
/* eslint-disable lines-around-comment */
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';
import { LoadingIndicator } from '../../../base/react';
// @ts-ignore
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
// @ts-ignore
import { TEXT_COLOR, navigationStyles } from './styles';
const ConnectingPage = () => {
const { t } = useTranslation();

View File

@@ -1,16 +1,25 @@
/* 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 { connect } from '../../../base/redux';
import { DialInSummary } from '../../../invite';
import { IReduxState } from '../../../app/types';
import { connect } from '../../../base/redux/functions';
// @ts-ignore
import DialInSummary from '../../../invite/components/dial-in-summary/native/DialInSummary';
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,
@@ -18,6 +27,7 @@ import {
navigationContainerTheme,
preJoinScreenOptions,
welcomeScreenOptions
// @ts-ignore
} from '../screenOptions';
import ConnectingPage from './ConnectingPage';
@@ -32,13 +42,13 @@ type Props = {
/**
* Redux dispatch function.
*/
dispatch: Function,
dispatch: Function;
/**
* Is welcome page available?
*/
isWelcomePageAvailable: boolean
}
isWelcomePageAvailable: boolean;
};
const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: Props) => {
@@ -100,7 +110,7 @@ const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: Props) =>
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function mapStateToProps(state: Object) {
function mapStateToProps(state: IReduxState) {
return {
isWelcomePageAvailable: isWelcomePageEnabled(state)
};

View File

@@ -13,26 +13,3 @@
*/
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

@@ -0,0 +1,31 @@
/* 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

@@ -1,62 +0,0 @@
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

@@ -0,0 +1,32 @@
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

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

View File

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

View File

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

View File

@@ -1,38 +0,0 @@
// @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

@@ -1,95 +0,0 @@
// @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

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

View File

@@ -1,24 +0,0 @@
// @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

@@ -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 './web/ReloadButton';
import ReloadButton from './ReloadButton';
declare var APP: Object;
@@ -91,6 +91,7 @@ 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}.
@@ -100,34 +101,18 @@ export default class AbstractPageReloadOverlay<P: Props>
* {@code false}, otherwise.
*/
static needsRender(state: Object) {
// 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 { error: conferenceError } = state['features/base/conference'];
const { error: configError } = state['features/base/config'];
const { error: connectionError } = state['features/base/connection'];
/**
* 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;
const jitsiConnectionError
return (
(connectionError && isFatalJitsiConnectionError(connectionError))
|| (conferenceError
&& isFatalJitsiConferenceError(conferenceError))
|| configError);
// @ts-ignore
= connectionError && isFatalJitsiConnectionError(connectionError);
const jitsiConferenceError
= conferenceError && isFatalJitsiConferenceError(conferenceError);
return jitsiConnectionError || jitsiConferenceError || configError;
}
_interval: ?IntervalID;

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

View File

@@ -1,8 +1,4 @@
// @flow
import React, { Component } from 'react';
declare var interfaceConfig: Object;
import React, { Component, ReactChildren } from 'react';
/**
* The type of the React {@code Component} props of {@link OverlayFrame}.
@@ -12,18 +8,18 @@ type Props = {
/**
* The children components to be displayed into the overlay frame.
*/
children: React$Node,
children: ReactChildren;
/**
* 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';
import { connect } from '../../../base/redux';
import { translate } from '../../../base/i18n/functions';
import { connect } from '../../../base/redux/functions';
import AbstractPageReloadOverlay, {
type Props,
abstractMapStateToProps
} from '../AbstractPageReloadOverlay';
} from './AbstractPageReloadOverlay';
import OverlayFrame from './OverlayFrame';
/**

View File

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

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

View File

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

View File

@@ -1,15 +0,0 @@
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

@@ -1,20 +0,0 @@
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,17 +1,13 @@
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from '../base/config/actionTypes';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { assign, set } from '../base/redux/functions';
import { assign } from '../base/redux/functions';
import { MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED } from './actionTypes';
import {
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
SET_FATAL_ERROR
} from './actionTypes';
export interface IOverlayState {
browser?: string;
fatalError?: Error;
isMediaPermissionPromptVisible?: boolean;
loadConfigOverlayVisible?: boolean;
}
/**
@@ -21,18 +17,8 @@ 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;
@@ -56,29 +42,3 @@ 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,7 +19,6 @@ const useStyles = makeStyles()(theme => {
right: '-4px',
top: '-3px',
textAlign: 'center',
boxSizing: 'border-box',
paddingTop: '2px'
}
};

View File

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

View File

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

View File

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

View File

@@ -463,7 +463,17 @@ function _updateReceiverVideoConstraints({ getState }: IStore) {
}
if (getCurrentLayout(state) === LAYOUTS.STAGE_FILMSTRIP_VIEW && activeParticipantsSources.length > 0) {
const onStageSources = [ ...activeParticipantsSources ];
const selectedSources: string[] = [];
const onStageSources: string[] = [];
// If more than one video source is pinned to the stage filmstrip, they need to be added to the
// 'selectedSources' so that the bridge can allocate bandwidth for all the sources as opposed to doing
// greedy allocation for the sources (which happens when they are added to 'onStageSources').
if (activeParticipantsSources.length > 1) {
selectedSources.push(...activeParticipantsSources);
} else {
onStageSources.push(activeParticipantsSources[0]);
}
activeParticipantsSources.forEach(sourceName => {
const isScreenSharing = remoteScreenShares.includes(sourceName);
@@ -485,6 +495,7 @@ function _updateReceiverVideoConstraints({ getState }: IStore) {
}
receiverConstraints.onStageSources = onStageSources;
receiverConstraints.selectedSources = selectedSources;
} else if (largeVideoSourceName) {
let quality = VIDEO_QUALITY_UNLIMITED;

View File

@@ -41,7 +41,10 @@ interface IProps extends WithTranslation {
const useStyles = makeStyles()(theme => {
return {
addBackground: {
marginRight: theme.spacing(2)
marginRight: theme.spacing(2),
'& svg': {
fill: '#669aec !important'
}
},
button: {
display: 'none'

View File

@@ -256,7 +256,7 @@ const useStyles = makeStyles()(theme => {
}
},
dialogMarginTop: {
marginTop: '44px'
marginTop: '8px'
},
virtualBackgroundLoading: {
overflow: 'hidden',