Compare commits
17 Commits
jitihouse/
...
quitrk-pat
Author | SHA1 | Date |
---|---|---|
Avram Tudor | 5e61122b3a | |
Avram Tudor | 1bd5294885 | |
tudordan7 | e68ccd5420 | |
hmuresan | 213a5fb89a | |
Avram Tudor | fe8e2c18bc | |
Vlad Piersec | af7c316827 | |
Vlad Piersec | 9290d9fec0 | |
Jaya Allamsetty | 0f338e4dcf | |
Hristo Terezov | deca2e55cc | |
Hristo Terezov | e95544f667 | |
Hristo Terezov | 598eed1d75 | |
Jaya Allamsetty | 56f45d92be | |
hmuresan | fa0c6d043e | |
Vlad Piersec | a31d176074 | |
robertpin | fc69c61d04 | |
Horatiu Muresan | 6440729a91 | |
Vlad Piersec | 34cb0b77cb |
|
@ -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,
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -46,3 +46,7 @@
|
|||
padding: 16px 24px;
|
||||
z-index: $popoverZ;
|
||||
}
|
||||
|
||||
.padded-content {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -501,6 +501,7 @@
|
|||
"expandedPending": "The live streaming is being started...",
|
||||
"failedToStart": "Live Streaming failed to start",
|
||||
"getStreamKeyManually": "We weren’t 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.",
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -109,6 +109,7 @@ export default [
|
|||
'disableRemoteMute',
|
||||
'disableResponsiveTiles',
|
||||
'disableRtx',
|
||||
'disableScreensharingVirtualBackground',
|
||||
'disableShortcuts',
|
||||
'disableShowMoreStats',
|
||||
'disableRemoveRaisedHandOnFocus',
|
||||
|
|
|
@ -292,13 +292,14 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPrejoinPageVisible(store.getState())) {
|
||||
dispatch(showNotification({
|
||||
description,
|
||||
titleKey,
|
||||
customActionNameKey: 'notify.newDeviceAction',
|
||||
customActionHandler: _useDevice.bind(undefined, store, devicesArray)
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,14 +278,11 @@ class Popover extends Component<Props, State> {
|
|||
*/
|
||||
_onShowDialog(event) {
|
||||
event && event.stopPropagation();
|
||||
if (!this.props.disablePopover) {
|
||||
this.setState({ showDialog: true });
|
||||
|
||||
if (this.props.onPopoverOpen) {
|
||||
if (!this.props.disablePopover) {
|
||||
this.props.onPopoverOpen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onThumbClick: (Object) => void;
|
||||
|
||||
|
@ -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);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export * from './native';
|
||||
export { default as PrivateMessageButton } from './PrivateMessageButton';
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export * from './web';
|
||||
|
||||
export { default as PrivateMessageButton } from './PrivateMessageButton';
|
||||
|
|
|
@ -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 & {
|
||||
|
|
|
@ -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.
|
||||
*/
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
|
@ -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.
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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') }
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 && (
|
||||
<>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
return function(dispatch) {
|
||||
if (isMobileBrowser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: SET_TOOLBOX_TIMEOUT,
|
||||
handler,
|
||||
timeoutMS
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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'}`);
|
||||
batch(() => {
|
||||
dispatch(setTileView(value));
|
||||
dispatch(setOverflowMenuVisible(false));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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.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 */
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,6 +444,7 @@ function VirtualBackground({
|
|||
{t('virtualBackground.blur')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{!disableScreensharingVirtualBackground && (
|
||||
<Tooltip
|
||||
content = { t('virtualBackground.desktopShare') }
|
||||
position = { 'top' }>
|
||||
|
@ -457,6 +464,7 @@ function VirtualBackground({
|
|||
src = { IconShareDesktop } />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{_images.map(image => (
|
||||
<Tooltip
|
||||
content = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
|
||||
|
|
Loading…
Reference in New Issue