Compare commits

...

17 Commits

Author SHA1 Message Date
Avram Tudor 5e61122b3a Create test 2021-11-18 15:03:39 +02:00
Avram Tudor 1bd5294885
chore(deps) lib-jitsi-meet@latest(release) (#10389)
Reverts changes added with previous ljm update and limits the scope of
this update to commits contained on the ljm release branch
2021-11-18 15:00:11 +02:00
tudordan7 e68ccd5420 feat(app-notifications): Remove device notifications in the prejoin screen. 2021-11-18 13:36:19 +01:00
hmuresan 213a5fb89a fix(notify-button-clicked) Fix crash on mobile browsers 2021-11-18 13:36:03 +01:00
Avram Tudor fe8e2c18bc chore(deps) lib-jitsi-meet@latest (#10368)
* fix(browser) Mark safari <14 as unsupported

51f77cbd51...b337778da8
2021-11-15 15:47:38 +02:00
Vlad Piersec af7c316827 fix(Prejoin): Make prejoin name noneditable only when taken from jwt 2021-11-15 15:42:10 +02:00
Vlad Piersec 9290d9fec0 fix(Chat): Fix private message reply button not working 2021-11-11 14:56:33 +02:00
Jaya Allamsetty 0f338e4dcf fix(prejoin): Add audio tracks on Safari always.
This fixes a bug where remote audio is not being played out if the user joins audio and video muted from pre-join screen.
2021-11-10 15:47:10 -05:00
Hristo Terezov deca2e55cc fix(recorder): "already started" notification 2021-11-09 13:26:32 -06:00
Hristo Terezov e95544f667 feat: Handle recording already started error 2021-11-09 12:35:20 -06:00
Hristo Terezov 598eed1d75 chore(deps): lib-jitsi-meet@latest 2021-11-09 12:29:47 -06:00
Jaya Allamsetty 56f45d92be fix(large-video) Call play() whenever a new stream is attached to large-video.
This fixes an issue on Safari where black video is rendered sometimes whenever a new stream is attached to the large video container.
2021-11-09 15:37:14 +01:00
hmuresan fa0c6d043e fix(raise-hand): Remove participant left from raised hand queue 2021-11-08 19:03:18 +02:00
Vlad Piersec a31d176074 fix(Drawer): Close drawer on item click
Clicking on an item when the popup drawer is displayed would keep it open.
Now clicking on any item should automatically close the drawer.

Popup was also refactored and no longer uses refs.
2021-11-04 11:26:01 +02:00
robertpin fc69c61d04 feat(virtual-bg) Added config to disable screen sharing as virtual bg 2021-11-04 10:45:31 +02:00
Horatiu Muresan 6440729a91 feat(external-api): Add recording download link available event (#10229) 2021-11-03 09:41:17 +02:00
Vlad Piersec 34cb0b77cb fix(toolbar): Hide/Show toolbar on tap on mobile web.
* A tap on video space will toggle the toolbar.
* Double tapping on a tile will pin the participant.
2021-11-02 11:11:38 +02:00
42 changed files with 569 additions and 286 deletions

View File

@ -921,6 +921,9 @@ var config = {
// Only the default ones from will be available.
// disableAddingBackgroundImages: false,
// Disables using screensharing as virtual background.
// disableScreensharingVirtualBackground: false,
// Sets the background transparency level. '0' is fully transparent, '1' is opaque.
// backgroundAlpha: 1,

View File

@ -11,12 +11,6 @@
{
@extend %connection-info;
/**
* Apply negative margin to reduce the appearance of padding in AtlasKit
* InlineDialog.
*/
margin: -15px;
> table {
white-space: nowrap;
@extend %connection-info;

View File

@ -46,3 +46,7 @@
padding: 16px 24px;
z-index: $popoverZ;
}
.padded-content {
padding: 4px 8px;
}

View File

@ -5,6 +5,7 @@
.popupmenu {
background-color: $menuBG;
border-radius: 3px;
list-style-type: none;
min-width: 150px;
text-align: left;
padding: 0px;
@ -38,6 +39,11 @@
}
}
&__list {
margin: 0;
padding: 0;
}
&__text {
display: inline-block;
margin-left: 8px;

View File

@ -501,6 +501,7 @@
"expandedPending": "The live streaming is being started...",
"failedToStart": "Live Streaming failed to start",
"getStreamKeyManually": "We werent able to fetch any live streams. Try getting your live stream key from YouTube.",
"inProgress": "Recording or live streaming in progress",
"invalidStreamKey": "Live stream key may be incorrect.",
"off": "Live Streaming stopped",
"offBy": "{{name}} stopped the live streaming",
@ -508,6 +509,7 @@
"onBy": "{{name}} started the live streaming",
"pending": "Starting Live Stream...",
"serviceName": "Live Streaming service",
"sessionAlreadyActive": "This session is already being recorded or live streamed.",
"signedInAs": "You are currently signed in as:",
"signIn": "Sign in with Google",
"signInCTA": "Sign in or enter your live stream key from YouTube.",
@ -758,6 +760,7 @@
"expandedPending": "Recording is being started...",
"failedToStart": "Recording failed to start",
"fileSharingdescription": "Share recording with meeting participants",
"inProgress": "Recording or live streaming in progress",
"linkGenerated": "We have generated a link to your recording.",
"live": "LIVE",
"loggedIn": "Logged in as {{userName}}",
@ -770,6 +773,7 @@
"serviceDescription": "Your recording will be saved by the recording service",
"serviceDescriptionCloud": "Cloud recording",
"serviceName": "Recording service",
"sessionAlreadyActive": "This session is already being recorded or live streamed.",
"signIn": "Sign in",
"signOut": "Sign out",
"unavailable": "Oops! The {{serviceName}} is currently unavailable. We're working on resolving the issue. Please try again later.",

View File

@ -1462,6 +1462,20 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that the current recording link is
* available.
*
* @param {string} link - The recording download link.
* @returns {void}
*/
notifyRecordingLinkAvailable(link: string) {
this._sendEvent({
name: 'recording-link-available',
link
});
}
/**
* Notify external application (if API is enabled) that a participant is knocking in the lobby.
*

View File

@ -112,6 +112,7 @@ const events = {
'password-required': 'passwordRequired',
'proxy-connection-event': 'proxyConnectionEvent',
'raise-hand-updated': 'raiseHandUpdated',
'recording-link-available': 'recordingLinkAvailable',
'recording-status-changed': 'recordingStatusChanged',
'video-ready-to-close': 'readyToClose',
'video-conference-joined': 'videoConferenceJoined',

View File

@ -495,6 +495,10 @@ export class VideoContainer extends LargeContainer {
stream.attach(this.$video[0]);
// Ensure large video gets play() called on it when a new stream is attached to it. This is necessary in the
// case of Safari as autoplay doesn't kick-in automatically on Safari 15 and newer versions.
browser.isWebKitBased() && this.$video[0].play();
const flipX = stream.isLocal() && this.localFlipX && !this.isScreenSharing();
this.$video.css({

12
package-lock.json generated
View File

@ -60,7 +60,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#42c675249aeef632aaf169e0544eeba240f7f962",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#e8e0115b84d5b2f8814cf9b967ddc6d9d3bec6b5",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.1",
@ -13018,8 +13018,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#42c675249aeef632aaf169e0544eeba240f7f962",
"integrity": "sha512-n9SUvINfAh47orcLkC1y6DzN792gvLWgPls+p5m8s44gpUfTRjKYK5Nu4utu1GNQ6A39sUFkcFJgRoQ51M1aUQ==",
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#e8e0115b84d5b2f8814cf9b967ddc6d9d3bec6b5",
"integrity": "sha512-KklpKCyMMFIO6Him3Kydx723/nqI+TAfSYhP/hw+9D6VocO0HxdB1VQjCE+ChxHXolMrByjq5WKAKgmhDRT15w==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@ -32282,9 +32282,9 @@
}
},
"lib-jitsi-meet": {
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#42c675249aeef632aaf169e0544eeba240f7f962",
"integrity": "sha512-n9SUvINfAh47orcLkC1y6DzN792gvLWgPls+p5m8s44gpUfTRjKYK5Nu4utu1GNQ6A39sUFkcFJgRoQ51M1aUQ==",
"from": "lib-jitsi-meet@github:jitsi/lib-jitsi-meet#42c675249aeef632aaf169e0544eeba240f7f962",
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#e8e0115b84d5b2f8814cf9b967ddc6d9d3bec6b5",
"integrity": "sha512-KklpKCyMMFIO6Him3Kydx723/nqI+TAfSYhP/hw+9D6VocO0HxdB1VQjCE+ChxHXolMrByjq5WKAKgmhDRT15w==",
"from": "lib-jitsi-meet@github:jitsi/lib-jitsi-meet#e8e0115b84d5b2f8814cf9b967ddc6d9d3bec6b5",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/sdp-interop": "github:jitsi/sdp-interop#4669790bb9020cc8f10c1d1f3823c26b08497547",

View File

@ -65,7 +65,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#42c675249aeef632aaf169e0544eeba240f7f962",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#e8e0115b84d5b2f8814cf9b967ddc6d9d3bec6b5",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.1",

View File

@ -109,6 +109,7 @@ export default [
'disableRemoteMute',
'disableResponsiveTiles',
'disableRtx',
'disableScreensharingVirtualBackground',
'disableShortcuts',
'disableShowMoreStats',
'disableRemoveRaisedHandOnFocus',

View File

@ -292,13 +292,14 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
break;
}
}
dispatch(showNotification({
description,
titleKey,
customActionNameKey: 'notify.newDeviceAction',
customActionHandler: _useDevice.bind(undefined, store, devicesArray)
}));
if (!isPrejoinPageVisible(store.getState())) {
dispatch(showNotification({
description,
titleKey,
customActionNameKey: 'notify.newDeviceAction',
customActionHandler: _useDevice.bind(undefined, store, devicesArray)
}));
}
});
}

View File

@ -276,6 +276,7 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
}
state.sortedRemoteParticipants.delete(id);
state.raisedHandsQueue = state.raisedHandsQueue.filter(pid => pid.id !== id);
if (!state.everyoneIsModerator && !isParticipantModerator(oldParticipant)) {
state.everyoneIsModerator = _isEveryoneModerator(state);

View File

@ -1,9 +1,10 @@
/* @flow */
import clsx from 'clsx';
import React, { Component } from 'react';
import { Drawer, JitsiPortal, DialogPortal } from '../../../toolbox/components/web';
import { isMobileBrowser } from '../../environment/utils';
import { connect } from '../../redux';
import { getContextMenuStyle } from '../functions.web';
/**
@ -57,7 +58,17 @@ type Props = {
* From which side of the dialog trigger the dialog should display. The
* value will be passed to {@code InlineDialog}.
*/
position: string
position: string,
/**
* Whether the content show have some padding.
*/
paddedContent: ?boolean,
/**
* Whether the popover is visible or not.
*/
visible: boolean
};
/**
@ -68,12 +79,7 @@ type State = {
/**
* The style to apply to the context menu in order to position it correctly.
*/
contextMenuStyle: Object,
/**
* Whether or not the {@code InlineDialog} should be displayed.
*/
showDialog: boolean
contextMenuStyle: Object
};
/**
@ -110,7 +116,6 @@ class Popover extends Component<Props, State> {
super(props);
this.state = {
showDialog: false,
contextMenuStyle: null
};
@ -127,16 +132,6 @@ class Popover extends Component<Props, State> {
this._getCustomDialogStyle = this._getCustomDialogStyle.bind(this);
}
/**
* Public method for triggering showing the context menu dialog.
*
* @returns {void}
* @public
*/
showDialog() {
this._onShowDialog();
}
/**
* Sets up a touch event listener to attach.
*
@ -164,7 +159,7 @@ class Popover extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const { children, className, content, id, overflowDrawer } = this.props;
const { children, className, content, id, overflowDrawer, visible } = this.props;
if (overflowDrawer) {
return (
@ -175,7 +170,7 @@ class Popover extends Component<Props, State> {
{ children }
<JitsiPortal>
<Drawer
isOpen = { this.state.showDialog }
isOpen = { visible }
onClose = { this._onHideDialog }>
{ content }
</Drawer>
@ -193,7 +188,7 @@ class Popover extends Component<Props, State> {
onMouseEnter = { this._onShowDialog }
onMouseLeave = { this._onHideDialog }
ref = { this._containerRef }>
{ this.state.showDialog && (
{ visible && (
<DialogPortal
getRef = { this._setContextMenuRef }
setSize = { this._setContextMenuStyle }
@ -244,7 +239,7 @@ class Popover extends Component<Props, State> {
* @returns {void}
*/
_onTouchStart(event) {
if (this.state.showDialog
if (this.props.visible
&& !this.props.overflowDrawer
&& this._contextMenuRef
&& this._contextMenuRef.contains
@ -263,7 +258,6 @@ class Popover extends Component<Props, State> {
*/
_onHideDialog() {
this.setState({
showDialog: false,
contextMenuStyle: null
});
@ -284,12 +278,9 @@ class Popover extends Component<Props, State> {
*/
_onShowDialog(event) {
event && event.stopPropagation();
if (!this.props.disablePopover) {
this.setState({ showDialog: true });
if (this.props.onPopoverOpen) {
this.props.onPopoverOpen();
}
if (!this.props.disablePopover) {
this.props.onPopoverOpen();
}
}
@ -319,7 +310,7 @@ class Popover extends Component<Props, State> {
_onKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
if (this.state.showDialog) {
if (this.props.visible) {
this._onHideDialog();
} else {
this._onShowDialog(e);
@ -340,7 +331,7 @@ class Popover extends Component<Props, State> {
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
if (this.state.showDialog) {
if (this.props.visible) {
this._onHideDialog();
}
}
@ -373,11 +364,15 @@ class Popover extends Component<Props, State> {
* @returns {ReactElement}
*/
_renderContent() {
const { content } = this.props;
const { content, paddedContent } = this.props;
const className = clsx(
'popover popupmenu',
paddedContent && 'padded-content'
);
return (
<div
className = 'popover popupmenu'
className = { className }
onKeyDown = { this._onEscKey }>
{ content }
{!isMobileBrowser() && (
@ -392,4 +387,18 @@ class Popover extends Component<Props, State> {
}
}
export default Popover;
/**
* Maps (parts of) the Redux state to the associated {@code Popover}'s props.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
overflowDrawer: state['features/toolbox'].overflowDrawer
};
}
export default connect(_mapStateToProps)(Popover);

View File

@ -1,4 +1,3 @@
// @flow
export * from './native';
export { default as PrivateMessageButton } from './PrivateMessageButton';

View File

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

View File

@ -12,8 +12,8 @@ import { type StyleType } from '../../../base/styles';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants';
import { replaceNonUnicodeEmojis } from '../../functions';
import AbstractChatMessage, { type Props as AbstractProps } from '../AbstractChatMessage';
import PrivateMessageButton from '../PrivateMessageButton';
import PrivateMessageButton from './PrivateMessageButton';
import styles from './styles';
type Props = AbstractProps & {

View File

@ -1,13 +1,13 @@
// @flow
import { CHAT_ENABLED, getFeatureFlag } from '../../base/flags';
import { translate } from '../../base/i18n';
import { IconMessage, IconReply } from '../../base/icons';
import { getParticipantById } from '../../base/participants';
import { connect } from '../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { navigate } from '../../conference/components/native/ConferenceNavigationContainerRef';
import { screen } from '../../conference/components/native/routes';
import { CHAT_ENABLED, getFeatureFlag } from '../../../base/flags';
import { translate } from '../../../base/i18n';
import { IconMessage, IconReply } from '../../../base/icons';
import { getParticipantById } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { navigate } from '../../../conference/components/native/ConferenceNavigationContainerRef';
import { screen } from '../../../conference/components/native/routes';
export type Props = AbstractButtonProps & {
@ -26,11 +26,6 @@ export type Props = AbstractButtonProps & {
*/
t: Function,
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* True if the polls feature is disabled.
*/

View File

@ -3,14 +3,12 @@
import React from 'react';
import { toArray } from 'react-emoji-render';
import { translate } from '../../../base/i18n';
import { Linkify } from '../../../base/react';
import { MESSAGE_TYPE_LOCAL } from '../../constants';
import AbstractChatMessage, {
type Props
} from '../AbstractChatMessage';
import PrivateMessageButton from '../PrivateMessageButton';
import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
import PrivateMessageButton from './PrivateMessageButton';
/**
* Renders a single chat message.

View File

@ -0,0 +1,90 @@
// @flow
import { CHAT_ENABLED, getFeatureFlag } from '../../../base/flags';
import { translate } from '../../../base/i18n';
import { IconMessage, IconReply } from '../../../base/icons';
import { getParticipantById } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { openChat } from '../../actions';
export type Props = AbstractButtonProps & {
/**
* The ID of the participant that the message is to be sent.
*/
participantID: string,
/**
* True if the button is rendered as a reply button.
*/
reply: boolean,
/**
* Function to be used to translate i18n labels.
*/
t: Function,
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The participant object retrieved from Redux.
*/
_participant: Object,
};
/**
* Class to render a button that initiates the sending of a private message through chet.
*/
class PrivateMessageButton extends AbstractButton<Props, any> {
accessibilityLabel = 'toolbar.accessibilityLabel.privateMessage';
icon = IconMessage;
label = 'toolbar.privateMessage';
toggledIcon = IconReply;
/**
* Handles clicking / pressing the button, and kicks the participant.
*
* @private
* @returns {void}
*/
_handleClick() {
const { _participant, dispatch } = this.props;
dispatch(openChat(_participant));
}
/**
* Helper function to be implemented by subclasses, which must return a
* {@code boolean} value indicating if this button is toggled or not.
*
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props.reply;
}
}
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Props}
*/
export function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> {
const enabled = getFeatureFlag(state, CHAT_ENABLED, true);
const { visible = enabled } = ownProps;
return {
_participant: getParticipantById(state, ownProps.participantID),
visible
};
}
export default translate(connect(_mapStateToProps)(PrivateMessageButton));

View File

@ -6,6 +6,7 @@ import React from 'react';
import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
import { getConferenceNameForTitle } from '../../../base/conference';
import { connect, disconnect } from '../../../base/connection';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { connect as reactReduxConnect } from '../../../base/redux';
import { setColorAlpha } from '../../../base/util';
@ -18,6 +19,7 @@ import { getIsLobbyVisible } from '../../../lobby/functions';
import { ParticipantsPane } from '../../../participants-pane/components/web';
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
import { toggleToolboxVisible } from '../../../toolbox/actions.any';
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
import { JitsiPortal, Toolbox } from '../../../toolbox/components/web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
@ -119,6 +121,7 @@ class Conference extends AbstractConference<Props, *> {
_onMouseLeave: Function;
_onMouseMove: Function;
_onShowToolbar: Function;
_onVidespaceTouchStart: Function;
_originalOnMouseMove: Function;
_originalOnShowToolbar: Function;
_setBackground: Function;
@ -157,6 +160,7 @@ class Conference extends AbstractConference<Props, *> {
// Bind event handler so it is only bound once for every instance.
this._onFullScreenChange = this._onFullScreenChange.bind(this);
this._onVidespaceTouchStart = this._onVidespaceTouchStart.bind(this);
this._setBackground = this._setBackground.bind(this);
}
@ -229,12 +233,14 @@ class Conference extends AbstractConference<Props, *> {
<div
className = { _layoutClassName }
id = 'videoconference_page'
onMouseMove = { this._onShowToolbar }
onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }
ref = { this._setBackground }>
<ConferenceInfo />
<Notice />
<div id = 'videospace'>
<div
id = 'videospace'
onTouchStart = { this._onVidespaceTouchStart }>
<LargeVideo />
{!_isParticipantsPaneVisible
&& <div id = 'notification-participant-list'>
@ -292,6 +298,16 @@ class Conference extends AbstractConference<Props, *> {
}
}
/**
* Handler used for touch start on Video container.
*
* @private
* @returns {void}
*/
_onVidespaceTouchStart() {
this.props.dispatch(toggleToolboxVisible());
}
/**
* Updates the Redux state when full screen mode has been enabled or
* disabled.

View File

@ -109,13 +109,21 @@ type Props = AbstractProps & {
t: Function,
};
type State = AbstractState & {
/**
* Whether popover is ivisible or not.
*/
popoverVisible: boolean
}
/**
* Implements a React {@link Component} which displays the current connection
* quality percentage and has a popover to show more detailed connection stats.
*
* @extends {Component}
*/
class ConnectionIndicator extends AbstractConnectionIndicator<Props, AbstractState> {
class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
/**
* Initializes a new {@code ConnectionIndicator} instance.
*
@ -126,10 +134,12 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, AbstractSta
super(props);
this.state = {
autoHideTimeout: undefined,
showIndicator: false,
stats: {}
stats: {},
popoverVisible: false
};
this._onShowPopover = this._onShowPopover.bind(this);
this._onHidePopover = this._onHidePopover.bind(this);
}
/**
@ -139,6 +149,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, AbstractSta
* @returns {ReactElement}
*/
render() {
const { iconSize, enableStatsDisplay, participantId, statsPopoverPosition } = this.props;
const visibilityClass = this._getVisibilityClass();
const rootClassNames = `indicator-container ${visibilityClass}`;
@ -151,13 +162,19 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, AbstractSta
className = { rootClassNames }
content = { <ConnectionIndicatorContent
inheritedStats = { this.state.stats }
participantId = { this.props.participantId } /> }
disablePopover = { !this.props.enableStatsDisplay }
position = { this.props.statsPopoverPosition }>
participantId = { participantId } /> }
disablePopover = { !enableStatsDisplay }
id = 'participant-connection-indicator'
noPaddingContent = { true }
onPopoverClose = { this._onHidePopover }
onPopoverOpen = { this._onShowPopover }
paddedContent = { true }
position = { statsPopoverPosition }
visible = { this.state.popoverVisible }>
<div className = 'popover-trigger'>
<div
className = { indicatorContainerClassNames }
style = {{ fontSize: this.props.iconSize }}>
style = {{ fontSize: iconSize }}>
<div className = 'connection indicatoricon'>
{ this._renderIcon() }
</div>
@ -222,6 +239,18 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, AbstractSta
? 'show-connection-indicator' : 'hide-connection-indicator';
}
_onHidePopover: () => void;
/**
* Hides popover.
*
* @private
* @returns {void}
*/
_onHidePopover() {
this.setState({ popoverVisible: false });
}
/**
* Creates a ReactElement for displaying an icon that represents the current
* connection quality.
@ -282,6 +311,18 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, AbstractSta
</span>
];
}
_onShowPopover: () => void;
/**
* Shows popover.
*
* @private
* @returns {void}
*/
_onShowPopover() {
this.setState({ popoverVisible: true });
}
}
/**

View File

@ -155,6 +155,7 @@ class Filmstrip extends PureComponent <Props> {
this._listItemKey = this._listItemKey.bind(this);
this._onGridItemsRendered = this._onGridItemsRendered.bind(this);
this._onListItemsRendered = this._onListItemsRendered.bind(this);
this._onTouchStart = this._onTouchStart.bind(this);
}
/**
@ -500,6 +501,20 @@ class Filmstrip extends PureComponent <Props> {
this._doToggleFilmstrip();
}
_onTouchStart: (SyntheticEvent<HTMLButtonElement>) => void;
/**
* Handler for onTouchStart.
*
* @private
* @param {Object} e - The synthetic event.
* @returns {void}
*/
_onTouchStart(e: SyntheticEvent<HTMLButtonElement>) {
// Don't propagate the touchStart event so the toolbar doesn't get toggled.
e.stopPropagation();
}
/**
* Creates a React Element for changing the visibility of the filmstrip when
* clicked.
@ -520,6 +535,7 @@ class Filmstrip extends PureComponent <Props> {
id = 'toggleFilmstripButton'
onClick = { this._onToolbarToggleFilmstrip }
onFocus = { this._onTabIn }
onTouchStart = { this._onTouchStart }
tabIndex = { 0 }>
<Icon
aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }

View File

@ -66,7 +66,12 @@ export type State = {|
/**
* Indicates whether the thumbnail is hovered or not.
*/
isHovered: boolean
isHovered: boolean,
/**
* Whether popover is visible or not.
*/
popoverVisible: boolean
|};
/**
@ -258,6 +263,12 @@ class Thumbnail extends Component<Props, State> {
*/
videoMenuTriggerRef: Object;
/**
* Timeout used to detect double tapping.
* It is active while user has tapped once.
*/
_firstTap: ?TimeoutID;
/**
* Initializes a new Thumbnail instance.
*
@ -271,17 +282,19 @@ class Thumbnail extends Component<Props, State> {
audioLevel: 0,
canPlayEventReceived: false,
isHovered: false,
displayMode: DISPLAY_VIDEO
displayMode: DISPLAY_VIDEO,
popoverVisible: false
};
this.state = {
...state,
displayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, state))
displayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, state)),
popoverVisible: false
};
this.timeoutHandle = null;
this.videoMenuTriggerRef = null;
this._setInstance = this._setInstance.bind(this);
this._clearDoubleClickTimeout = this._clearDoubleClickTimeout.bind(this);
this._updateAudioLevel = this._updateAudioLevel.bind(this);
this._onCanPlay = this._onCanPlay.bind(this);
this._onClick = this._onClick.bind(this);
@ -292,7 +305,8 @@ class Thumbnail extends Component<Props, State> {
this._onTouchStart = this._onTouchStart.bind(this);
this._onTouchEnd = this._onTouchEnd.bind(this);
this._onTouchMove = this._onTouchMove.bind(this);
this._showPopupMenu = this._showPopupMenu.bind(this);
this._showPopover = this._showPopover.bind(this);
this._hidePopover = this._hidePopover.bind(this);
}
/**
@ -437,6 +451,18 @@ class Thumbnail extends Component<Props, State> {
this._stopListeningForAudioUpdates(this.props._audioTrack);
}
_clearDoubleClickTimeout: () => void
/**
* Clears the first click timeout.
*
* @returns {void}
*/
_clearDoubleClickTimeout() {
clearTimeout(this._firstTap);
this._firstTap = undefined;
}
/**
* Starts listening for audio level updates from the library.
*
@ -484,6 +510,34 @@ class Thumbnail extends Component<Props, State> {
});
}
_showPopover: () => void;
/**
* Shows popover.
*
* @private
* @returns {void}
*/
_showPopover() {
this.setState({
popoverVisible: true
});
}
_hidePopover: () => void;
/**
* Hides popover.
*
* @private
* @returns {void}
*/
_hidePopover() {
this.setState({
popoverVisible: false
});
}
/**
* Returns an object with the styles for thumbnail.
*
@ -564,28 +618,24 @@ class Thumbnail extends Component<Props, State> {
this.setState({ isHovered: false });
}
_showPopupMenu: () => void;
/**
* Triggers showing the popover context menu.
*
* @returns {void}
*/
_showPopupMenu() {
if (this.videoMenuTriggerRef) {
this.videoMenuTriggerRef.showContextMenu();
}
}
_onTouchStart: () => void;
/**
* Set showing popover context menu after x miliseconds.
* Handler for touch start.
*
* @returns {void}
*/
_onTouchStart() {
this.timeoutHandle = setTimeout(this._showPopupMenu, SHOW_TOOLBAR_CONTEXT_MENU_AFTER);
this.timeoutHandle = setTimeout(this._showPopover, SHOW_TOOLBAR_CONTEXT_MENU_AFTER);
if (this._firstTap) {
this._clearDoubleClickTimeout();
this._onClick();
return;
}
this._firstTap = setTimeout(this._clearDoubleClickTimeout, 300);
}
_onTouchEnd: () => void;
@ -790,7 +840,6 @@ class Thumbnail extends Component<Props, State> {
<span
className = { containerClassName }
id = 'localVideoContainer'
onClick = { this._onClick }
{ ...(_isMobile
? {
onTouchEnd: this._onTouchEnd,
@ -798,6 +847,7 @@ class Thumbnail extends Component<Props, State> {
onTouchStart: this._onTouchStart
}
: {
onClick: this._onClick,
onMouseEnter: this._onMouseEnter,
onMouseLeave: this._onMouseLeave
}
@ -832,7 +882,9 @@ class Thumbnail extends Component<Props, State> {
</span>
<span className = 'localvideomenu'>
<LocalVideoMenuTriggerButton
getRef = { this._setInstance } />
hidePopover = { this._hidePopover }
popoverVisible = { this.state.popoverVisible }
showPopover = { this._showPopover } />
</span>
</span>
@ -878,19 +930,6 @@ class Thumbnail extends Component<Props, State> {
dispatch(updateLastTrackVideoMediaEvent(jitsiVideoTrack, event.type));
}
_setInstance: Object => void;
/**
* Stores the local or remote video menu button instance in a variable.
*
* @param {Object} instance - The local or remote video menu trigger instance.
*
* @returns {void}
*/
_setInstance(instance) {
this.videoMenuTriggerRef = instance;
}
/**
* Renders a remote participant's 'thumbnail.
*
@ -932,7 +971,7 @@ class Thumbnail extends Component<Props, State> {
<span
className = { containerClassName }
id = { `participant_${id}` }
onClick = { this._onClick }
onClick = { _isMobile ? undefined : this._onClick }
{ ...(_isMobile
? {
onTouchEnd: this._onTouchEnd,
@ -977,10 +1016,12 @@ class Thumbnail extends Component<Props, State> {
</span>
<span className = 'remotevideomenu'>
<RemoteVideoMenuTriggerButton
getRef = { this._setInstance }
hidePopover = { this._hidePopover }
initialVolumeValue = { _volume }
onVolumeChange = { onVolumeChange }
participantID = { id } />
participantID = { id }
popoverVisible = { this.state.popoverVisible }
showPopover = { this._showPopover } />
</span>
</span>
);

View File

@ -308,10 +308,9 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
* @returns {void}
*/
_onSendPrivateMessage() {
const { closeDrawer, dispatch, overflowDrawer } = this.props;
const { dispatch } = this.props;
dispatch(openChatById(this._getCurrentParticipantId()));
overflowDrawer && closeDrawer();
}
_position: () => void;
@ -444,7 +443,7 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
) : (
<>
{_isLocalModerator && (
<ContextMenuItemGroup>
<ContextMenuItemGroup onClick = { closeDrawer }>
<>
{overflowDrawer && (_isAudioForceMuted || _isVideoForceMuted)
&& <ContextMenuItem onClick = { this._onAskToUnmute }>
@ -481,7 +480,7 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
</ContextMenuItemGroup>
)}
<ContextMenuItemGroup>
<ContextMenuItemGroup onClick = { closeDrawer }>
{
_isLocalModerator && (
<>

View File

@ -6,8 +6,7 @@ declare var APP: Object;
import uuid from 'uuid';
import { getDialOutStatusUrl, getDialOutUrl, updateConfig } from '../base/config';
import { isIosMobileBrowser } from '../base/environment/utils';
import { createLocalTrack } from '../base/lib-jitsi-meet';
import { browser, createLocalTrack } from '../base/lib-jitsi-meet';
import { isVideoMutedByUser, MEDIA_TYPE } from '../base/media';
import { updateSettings } from '../base/settings';
import {
@ -240,10 +239,10 @@ export function joinConference(options?: Object, ignoreJoiningInProgress: boolea
// Do not signal audio/video tracks if the user joins muted.
for (const track of localTracks) {
// Always add the audio track on mobile Safari because of a known issue where audio playout doesn't happen
// Always add the audio track on Safari because of a known issue where audio playout doesn't happen
// if the user joins audio and video muted.
if (track.muted
&& !(isIosMobileBrowser() && track.jitsiTrack && track.jitsiTrack.getType() === MEDIA_TYPE.AUDIO)) {
&& !(browser.isWebKitBased() && track.jitsiTrack && track.jitsiTrack.getType() === MEDIA_TYPE.AUDIO)) {
try {
await dispatch(replaceLocalTrack(track.jitsiTrack, null));
} catch (error) {

View File

@ -4,7 +4,6 @@ import InlineDialog from '@atlaskit/inline-dialog';
import React, { Component } from 'react';
import { getRoomName } from '../../base/conference';
import { isNameReadOnly } from '../../base/config';
import { translate } from '../../base/i18n';
import { Icon, IconArrowDown, IconArrowUp, IconPhone, IconVolumeOff } from '../../base/icons';
import { isVideoMutedByUser } from '../../base/media';
@ -21,7 +20,8 @@ import {
isDeviceStatusVisible,
isDisplayNameRequired,
isJoinByPhoneButtonVisible,
isJoinByPhoneDialogVisible
isJoinByPhoneDialogVisible,
isPrejoinNameReadOnly
} from '../functions';
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
@ -401,7 +401,7 @@ function mapStateToProps(state): Object {
showDialog: isJoinByPhoneDialogVisible(state),
showErrorOnJoin,
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
readOnlyName: isNameReadOnly(state),
readOnlyName: isPrejoinNameReadOnly(state),
showCameraPreview: !isVideoMutedByUser(state),
videoTrack: getLocalJitsiVideoTrack(state)
};

View File

@ -36,6 +36,16 @@ export function isDisplayNameRequired(state: Object): boolean {
|| state['features/base/config'].requireDisplayName;
}
/**
* Selector for determining if the display name from prejoin page is read only.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function isPrejoinNameReadOnly(state: Object): boolean {
return Boolean(state['features/base/jwt']?.user?.name);
}
/**
* Selector for determining if the user has chosen to skip prejoin page.
*

View File

@ -9,7 +9,8 @@ import {
NOTIFICATION_TIMEOUT,
hideNotification,
showErrorNotification,
showNotification
showNotification,
showWarningNotification
} from '../notifications';
import {
@ -22,6 +23,8 @@ import {
import { getRecordingLink, getResourceId, isSavingRecordingOnDropbox } from './functions';
import logger from './logger';
declare var APP: Object;
/**
* Clears the data of every recording sessions.
*
@ -113,6 +116,16 @@ export function showRecordingError(props: Object) {
return showErrorNotification(props);
}
/**
* Signals that the recording warning notification should be shown.
*
* @param {Object} props - The Props needed to render the notification.
* @returns {showWarningNotification}
*/
export function showRecordingWarning(props: Object) {
return showWarningNotification(props);
}
/**
* Signals that the stopped recording notification should be shown on the
* screen for a given period.
@ -188,6 +201,10 @@ export function showStartedRecordingNotification(
try {
const link = await getRecordingLink(recordingSharingUrl, sessionId, region, tenant);
if (typeof APP === 'object') {
APP.API.notifyRecordingLinkAvailable(link);
}
// add the option to copy recording link
dialogProps.customActionNameKey = 'recording.copyLink';
dialogProps.customActionHandler = () => copyText(link);

View File

@ -27,6 +27,7 @@ import {
showPendingRecordingNotification,
showRecordingError,
showRecordingLimitNotification,
showRecordingWarning,
showStartedRecordingNotification,
showStoppedRecordingNotification,
updateRecordingSessionData
@ -261,6 +262,14 @@ function _showRecordingErrorNotification(recorderSession, dispatch) {
: 'recording.busyTitle'
}));
break;
case JitsiMeetJS.constants.recording.error.UNEXPECTED_REQUEST:
dispatch(showRecordingWarning({
descriptionKey: isStreamMode
? 'liveStreaming.sessionAlreadyActive'
: 'recording.sessionAlreadyActive',
titleKey: isStreamMode ? 'liveStreaming.inProgress' : 'recording.inProgress'
}));
break;
default:
dispatch(showRecordingError({
descriptionKey: isStreamMode

View File

@ -4,7 +4,8 @@ import type { Dispatch } from 'redux';
import {
SET_TOOLBOX_ENABLED,
SET_TOOLBOX_VISIBLE
SET_TOOLBOX_VISIBLE,
TOGGLE_TOOLBOX_VISIBLE
} from './actionTypes';
/**
@ -43,3 +44,24 @@ export function setToolboxVisible(visible: boolean): Object {
});
};
}
/**
* Action to toggle the toolbox visibility.
*
* @returns {Function}
*/
export function toggleToolboxVisible() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { toolbarConfig: { alwaysVisible } } = state['features/base/config'];
const { visible } = state['features/toolbox'];
if (visible && alwaysVisible) {
return;
}
dispatch({
type: TOGGLE_TOOLBOX_VISIBLE
});
};
}

View File

@ -1,28 +1,2 @@
// @flow
import type { Dispatch } from 'redux';
import { TOGGLE_TOOLBOX_VISIBLE } from './actionTypes';
export * from './actions.any';
/**
* Action to toggle the toolbox visibility.
*
* @returns {Function}
*/
export function toggleToolboxVisible() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { toolbarConfig: { alwaysVisible } } = state['features/base/config'];
const { visible } = state['features/toolbox'];
if (visible && alwaysVisible) {
return;
}
dispatch({
type: TOGGLE_TOOLBOX_VISIBLE
});
};
}

View File

@ -3,6 +3,7 @@
import type { Dispatch } from 'redux';
import { overwriteConfig } from '../base/config';
import { isMobileBrowser } from '../base/environment/utils';
import {
CLEAR_TOOLBOX_TIMEOUT,
@ -220,7 +221,8 @@ export function setToolbarHovered(hovered: boolean): Object {
}
/**
* Dispatches an action which sets new timeout and clears the previous one.
* Dispatches an action which sets new timeout for the toolbox visibility and clears the previous one.
* On mobile browsers the toolbox does not hide on timeout. It is toggled on simple tap.
*
* @param {Function} handler - Function to be invoked after the timeout.
* @param {number} timeoutMS - Delay.
@ -231,10 +233,15 @@ export function setToolbarHovered(hovered: boolean): Object {
* }}
*/
export function setToolboxTimeout(handler: Function, timeoutMS: number): Object {
return {
type: SET_TOOLBOX_TIMEOUT,
handler,
timeoutMS
return function(dispatch) {
if (isMobileBrowser()) {
return;
}
dispatch({
type: SET_TOOLBOX_TIMEOUT,
handler,
timeoutMS
});
};
}

View File

@ -797,7 +797,10 @@ class Toolbox extends Component<Props> {
}
Object.values(buttons).forEach((button: any) => {
if (this.props._buttonsWithNotifyClick.includes(button.key)) {
if (
typeof button === 'object'
&& this.props._buttonsWithNotifyClick.includes(button.key)
) {
button.handleClick = () => APP.API.notifyToolbarButtonClicked(button.key);
}
});

View File

@ -1,5 +1,5 @@
// @flow
import { batch } from 'react-redux';
import type { Dispatch } from 'redux';
import {
@ -11,6 +11,7 @@ import { translate } from '../../base/i18n';
import { IconTileView } from '../../base/icons';
import { connect } from '../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { setOverflowMenuVisible } from '../../toolbox/actions';
import { setTileView } from '../actions';
import { shouldDisplayTileView } from '../functions';
import logger from '../logger';
@ -68,7 +69,11 @@ class TileViewButton<P: Props> extends AbstractButton<P, *> {
}));
logger.debug(`Tile view ${value ? 'enable' : 'disable'}`);
dispatch(setTileView(value));
batch(() => {
dispatch(setTileView(value));
dispatch(setOverflowMenuVisible(false));
});
}
/**

View File

@ -23,6 +23,11 @@ type Props = {
*/
dispatch: Function,
/**
* Click handler executed aside from the main action.
*/
onClick?: Function,
/**
* Invoked to obtain translated strings.
*/
@ -77,8 +82,9 @@ class FlipLocalVideoButton extends PureComponent<Props> {
* @returns {void}
*/
_onClick() {
const { _localFlipX, dispatch } = this.props;
const { _localFlipX, dispatch, onClick } = this.props;
onClick && onClick();
dispatch(updateSettings({
localFlipX: !_localFlipX
}));

View File

@ -38,6 +38,21 @@ type Props = {
*/
getRef: Function,
/**
* Hides popover.
*/
hidePopover: Function,
/**
* Whether the popover is visible or not.
*/
popoverVisible: boolean,
/**
* Shows popover.
*/
showPopover: Function,
/**
* The id of the local participant.
*/
@ -78,10 +93,6 @@ type Props = {
* @extends {Component}
*/
class LocalVideoMenuTriggerButton extends Component<Props> {
/**
* Reference to the Popover instance.
*/
popoverRef: Object;
/**
* Initializes a new LocalVideoMenuTriggerButton instance.
@ -92,45 +103,10 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
constructor(props: Props) {
super(props);
this.popoverRef = React.createRef();
this._onPopoverClose = this._onPopoverClose.bind(this);
this._onPopoverOpen = this._onPopoverOpen.bind(this);
}
/**
* Triggers showing the popover's context menu.
*
* @returns {void}
*/
showContextMenu() {
if (this.popoverRef && this.popoverRef.current) {
this.popoverRef.current.showDialog();
}
}
/**
* Calls the ref(instance) getter.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
if (this.props.getRef) {
this.props.getRef(this);
}
}
/**
* Calls the ref(instance) getter.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
if (this.props.getRef) {
this.props.getRef(null);
}
}
/**
* Implements React's {@link Component#render()}.
@ -145,6 +121,8 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
_showConnectionInfo,
_overflowDrawer,
_showLocalVideoFlipButton,
hidePopover,
popoverVisible,
t
} = this.props;
@ -152,7 +130,7 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
? <ConnectionIndicatorContent participantId = { _localParticipantId } />
: (
<VideoMenu id = 'localVideoMenu'>
<FlipLocalVideoButton />
<FlipLocalVideoButton onClick = { hidePopover } />
{ isMobileBrowser()
&& <ConnectionStatusButton participantId = { _localParticipantId } />
}
@ -163,11 +141,12 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
isMobileBrowser() || _showLocalVideoFlipButton
? <Popover
content = { content }
id = 'local-video-menu-trigger'
onPopoverClose = { this._onPopoverClose }
onPopoverOpen = { this._onPopoverOpen }
overflowDrawer = { _overflowDrawer }
position = { _menuPosition }
ref = { this.popoverRef }>
visible = { popoverVisible }>
{!_overflowDrawer && (
<span
className = 'popover-trigger local-video-menu-trigger'>
@ -194,7 +173,10 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
* @returns {void}
*/
_onPopoverOpen() {
this.props.dispatch(setParticipantContextMenuOpen(true));
const { dispatch, showPopover } = this.props;
showPopover();
dispatch(setParticipantContextMenuOpen(true));
}
_onPopoverClose: () => void;
@ -205,8 +187,9 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
* @returns {void}
*/
_onPopoverClose() {
const { dispatch } = this.props;
const { hidePopover, dispatch } = this.props;
hidePopover();
batch(() => {
dispatch(setParticipantContextMenuOpen(false));
dispatch(renderConnectionStatus(false));

View File

@ -9,7 +9,7 @@ import { openChat } from '../../../chat/';
import {
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
} from '../../../chat/components/PrivateMessageButton';
} from '../../../chat/components/web/PrivateMessageButton';
import { isButtonEnabled } from '../../../toolbox/functions.web';
import VideoMenuButton from './VideoMenuButton';

View File

@ -1,5 +1,6 @@
// @flow
/* eslint-disable react/jsx-handler-names */
import React, { Component } from 'react';
import { batch } from 'react-redux';
@ -41,6 +42,21 @@ declare var $: Object;
*/
type Props = {
/**
* Hides popover.
*/
hidePopover: Function,
/**
* Whether the popover is visible or not.
*/
popoverVisible: boolean,
/**
* Shows popover.
*/
showPopover: Function,
/**
* Whether or not to display the kick button.
*/
@ -128,10 +144,6 @@ type Props = {
* @extends {Component}
*/
class RemoteVideoMenuTriggerButton extends Component<Props> {
/**
* Reference to the Popover instance.
*/
popoverRef: Object;
/**
* Initializes a new RemoteVideoMenuTriggerButton instance.
@ -142,46 +154,10 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
constructor(props: Props) {
super(props);
this.popoverRef = React.createRef();
this._onPopoverClose = this._onPopoverClose.bind(this);
this._onPopoverOpen = this._onPopoverOpen.bind(this);
}
/**
* Triggers showing the popover's context menu.
*
* @returns {void}
*/
showContextMenu() {
if (this.popoverRef && this.popoverRef.current) {
this.popoverRef.current.showDialog();
}
}
/**
* Calls the ref(instance) getter.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
if (this.props.getRef) {
this.props.getRef(this);
}
}
/**
* Calls the ref(instance) getter.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
if (this.props.getRef) {
this.props.getRef(null);
}
}
/**
* Implements React's {@link Component#render()}.
*
@ -189,7 +165,13 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { _overflowDrawer, _showConnectionInfo, _participantDisplayName, participantID } = this.props;
const {
_overflowDrawer,
_showConnectionInfo,
_participantDisplayName,
participantID,
popoverVisible
} = this.props;
const content = _showConnectionInfo
? <ConnectionIndicatorContent participantId = { participantID } />
: this._renderRemoteVideoMenu();
@ -203,11 +185,11 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
return (
<Popover
content = { content }
id = 'remote-video-menu-trigger'
onPopoverClose = { this._onPopoverClose }
onPopoverOpen = { this._onPopoverOpen }
overflowDrawer = { _overflowDrawer }
position = { this.props._menuPosition }
ref = { this.popoverRef }>
visible = { popoverVisible }>
{!_overflowDrawer && (
<span className = 'popover-trigger remote-video-menu-trigger'>
{!isMobileBrowser() && <Icon
@ -232,7 +214,10 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
* @returns {void}
*/
_onPopoverOpen() {
this.props.dispatch(setParticipantContextMenuOpen(true));
const { dispatch, showPopover } = this.props;
showPopover();
dispatch(setParticipantContextMenuOpen(true));
}
_onPopoverClose: () => void;
@ -243,8 +228,9 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
* @returns {void}
*/
_onPopoverClose() {
const { dispatch } = this.props;
const { dispatch, hidePopover } = this.props;
hidePopover();
batch(() => {
dispatch(setParticipantContextMenuOpen(false));
dispatch(renderConnectionStatus(false));
@ -271,6 +257,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
participantID
} = this.props;
const actions = [];
const buttons = [];
const showVolumeSlider = !isIosMobileBrowser()
&& onVolumeChange
@ -343,7 +330,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
);
if (isMobileBrowser()) {
buttons.push(
actions.push(
<ConnectionStatusButton
key = 'conn-status'
participantId = { participantID } />
@ -351,7 +338,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
}
if (showVolumeSlider) {
buttons.push(
actions.push(
<VolumeSlider
initialValue = { initialVolumeValue }
key = 'volume-slider'
@ -359,10 +346,27 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
);
}
if (buttons.length > 0) {
if (buttons.length > 0 || actions.length > 0) {
return (
<VideoMenu id = { participantID }>
{ buttons }
<>
{ buttons.length > 0
&& <li onClick = { this.props.hidePopover }>
<ul className = 'popupmenu__list'>
{ buttons }
</ul>
</li>
}
</>
<>
{ actions.length > 0
&& <li>
<ul className = 'popupmenu__list'>
{actions}
</ul>
</li>
}
</>
</VideoMenu>
);
}
@ -437,3 +441,4 @@ function _mapStateToProps(state, ownProps) {
}
export default translate(connect(_mapStateToProps)(RemoteVideoMenuTriggerButton));
/* eslint-enable react/jsx-handler-names */

View File

@ -61,8 +61,7 @@ export default class VideoMenuButton extends Component<Props> {
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @param {Object} e - The synthetic event.
* @returns {void}
*/
_onKeyPress(e) {

View File

@ -4,6 +4,7 @@ import Spinner from '@atlaskit/spinner';
import Bourne from '@hapi/bourne';
import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
import React, { useState, useEffect, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { Dialog, hideDialog, openDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
@ -126,6 +127,7 @@ function VirtualBackground({
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && Bourne.parse(localImages)) || []);
const [ loading, setLoading ] = useState(false);
const { disableScreensharingVirtualBackground } = useSelector(state => state['features/base/config']);
const [ activeDesktopVideo ] = useState(_virtualBackground?.virtualSource?.videoType === VIDEO_TYPE.DESKTOP
? _virtualBackground.virtualSource
@ -197,6 +199,10 @@ function VirtualBackground({
const shareDesktop = useCallback(async () => {
if (disableScreensharingVirtualBackground) {
return;
}
let isCancelled = false, url;
try {
@ -438,25 +444,27 @@ function VirtualBackground({
{t('virtualBackground.blur')}
</div>
</Tooltip>
<Tooltip
content = { t('virtualBackground.desktopShare') }
position = { 'top' }>
<div
aria-checked = { _selectedThumbnail === 'desktop-share' }
aria-label = { t('virtualBackground.desktopShare') }
className = { _selectedThumbnail === 'desktop-share'
? 'background-option desktop-share-selected'
: 'background-option desktop-share' }
onClick = { shareDesktop }
onKeyPress = { shareDesktopKeyPress }
role = 'radio'
tabIndex = { 0 }>
<Icon
className = 'share-desktop-icon'
size = { 30 }
src = { IconShareDesktop } />
</div>
</Tooltip>
{!disableScreensharingVirtualBackground && (
<Tooltip
content = { t('virtualBackground.desktopShare') }
position = { 'top' }>
<div
aria-checked = { _selectedThumbnail === 'desktop-share' }
aria-label = { t('virtualBackground.desktopShare') }
className = { _selectedThumbnail === 'desktop-share'
? 'background-option desktop-share-selected'
: 'background-option desktop-share' }
onClick = { shareDesktop }
onKeyPress = { shareDesktopKeyPress }
role = 'radio'
tabIndex = { 0 }>
<Icon
className = 'share-desktop-icon'
size = { 30 }
src = { IconShareDesktop } />
</div>
</Tooltip>
)}
{_images.map(image => (
<Tooltip
content = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }

1
test Normal file
View File

@ -0,0 +1 @@