Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled
498 lines
16 KiB
TypeScript
498 lines
16 KiB
TypeScript
import { throttle } from 'lodash-es';
|
|
import React, { useCallback, useState } from 'react';
|
|
import { WithTranslation } from 'react-i18next';
|
|
import { connect as reactReduxConnect, useDispatch, useSelector, useStore } from 'react-redux';
|
|
|
|
// @ts-expect-error
|
|
import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
|
|
import { IReduxState, IStore } from '../../../app/types';
|
|
import { getConferenceNameForTitle } from '../../../base/conference/functions';
|
|
import { hangup } from '../../../base/connection/actions.web';
|
|
import { isMobileBrowser } from '../../../base/environment/utils';
|
|
import { translate } from '../../../base/i18n/functions';
|
|
import { setColorAlpha } from '../../../base/util/helpers';
|
|
import { openChat, setFocusedTab } from '../../../chat/actions.web';
|
|
import Chat from '../../../chat/components/web/Chat';
|
|
import { ChatTabs } from '../../../chat/constants';
|
|
import { isFileUploadingEnabled, processFiles } from '../../../file-sharing/functions.any';
|
|
import MainFilmstrip from '../../../filmstrip/components/web/MainFilmstrip';
|
|
import ScreenshareFilmstrip from '../../../filmstrip/components/web/ScreenshareFilmstrip';
|
|
import StageFilmstrip from '../../../filmstrip/components/web/StageFilmstrip';
|
|
import CalleeInfoContainer from '../../../invite/components/callee-info/CalleeInfoContainer';
|
|
import LargeVideo from '../../../large-video/components/LargeVideo.web';
|
|
import LobbyScreen from '../../../lobby/components/web/LobbyScreen';
|
|
import { getIsLobbyVisible } from '../../../lobby/functions';
|
|
import { getOverlayToRender } from '../../../overlay/functions.web';
|
|
import ParticipantsPane from '../../../participants-pane/components/web/ParticipantsPane';
|
|
import Prejoin from '../../../prejoin/components/web/Prejoin';
|
|
import { isPrejoinPageVisible } from '../../../prejoin/functions.web';
|
|
import ReactionAnimations from '../../../reactions/components/web/ReactionsAnimations';
|
|
import { toggleToolboxVisible } from '../../../toolbox/actions.any';
|
|
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
|
|
import JitsiPortal from '../../../toolbox/components/web/JitsiPortal';
|
|
import Toolbox from '../../../toolbox/components/web/Toolbox';
|
|
import { LAYOUT_CLASSNAMES } from '../../../video-layout/constants';
|
|
import { getCurrentLayout } from '../../../video-layout/functions.any';
|
|
import VisitorsQueue from '../../../visitors/components/web/VisitorsQueue';
|
|
import { showVisitorsQueue } from '../../../visitors/functions';
|
|
import { init } from '../../actions.web';
|
|
import { maybeShowSuboptimalExperienceNotification } from '../../functions.web';
|
|
import {
|
|
AbstractConference,
|
|
type AbstractProps,
|
|
abstractMapStateToProps
|
|
} from '../AbstractConference';
|
|
|
|
import ConferenceInfo from './ConferenceInfo';
|
|
import { default as Notice } from './Notice';
|
|
|
|
/**
|
|
* DOM events for when full screen mode has changed. Different browsers need
|
|
* different vendor prefixes.
|
|
*
|
|
* @private
|
|
* @type {Array<string>}
|
|
*/
|
|
const FULL_SCREEN_EVENTS = [
|
|
'webkitfullscreenchange',
|
|
'mozfullscreenchange',
|
|
'fullscreenchange'
|
|
];
|
|
|
|
/**
|
|
* The type of the React {@code Component} props of {@link Conference}.
|
|
*/
|
|
interface IProps extends AbstractProps, WithTranslation {
|
|
|
|
/**
|
|
* The alpha(opacity) of the background.
|
|
*/
|
|
_backgroundAlpha?: number;
|
|
|
|
/**
|
|
* Are any overlays visible?
|
|
*/
|
|
_isAnyOverlayVisible: boolean;
|
|
|
|
/**
|
|
* The CSS class to apply to the root of {@link Conference} to modify the
|
|
* application layout.
|
|
*/
|
|
_layoutClassName: string;
|
|
|
|
/**
|
|
* The config specified interval for triggering mouseMoved iframe api events.
|
|
*/
|
|
_mouseMoveCallbackInterval?: number;
|
|
|
|
/**
|
|
*Whether or not the notifications should be displayed in the overflow drawer.
|
|
*/
|
|
_overflowDrawer: boolean;
|
|
|
|
/**
|
|
* Name for this conference room.
|
|
*/
|
|
_roomName: string;
|
|
|
|
/**
|
|
* If lobby page is visible or not.
|
|
*/
|
|
_showLobby: boolean;
|
|
|
|
/**
|
|
* If prejoin page is visible or not.
|
|
*/
|
|
_showPrejoin: boolean;
|
|
|
|
/**
|
|
* If visitors queue page is visible or not.
|
|
* NOTE: This should be set to true once we received an error on connect. Before the first connect this will always
|
|
* be false.
|
|
*/
|
|
_showVisitorsQueue: boolean;
|
|
|
|
dispatch: IStore['dispatch'];
|
|
}
|
|
|
|
/**
|
|
* Returns true if the prejoin screen should be displayed and false otherwise.
|
|
*
|
|
* @param {IProps} props - The props object.
|
|
* @returns {boolean} - True if the prejoin screen should be displayed and false otherwise.
|
|
*/
|
|
function shouldShowPrejoin({ _showLobby, _showPrejoin, _showVisitorsQueue }: IProps) {
|
|
return _showPrejoin && !_showVisitorsQueue && !_showLobby;
|
|
}
|
|
|
|
/**
|
|
* The conference page of the Web application.
|
|
*/
|
|
class Conference extends AbstractConference<IProps, any> {
|
|
_originalOnMouseMove: Function;
|
|
_originalOnShowToolbar: Function;
|
|
|
|
/**
|
|
* Initializes a new Conference instance.
|
|
*
|
|
* @param {Object} props - The read-only properties with which the new
|
|
* instance is to be initialized.
|
|
*/
|
|
constructor(props: IProps) {
|
|
super(props);
|
|
|
|
const { _mouseMoveCallbackInterval } = props;
|
|
|
|
// Throttle and bind this component's mousemove handler to prevent it
|
|
// from firing too often.
|
|
this._originalOnShowToolbar = this._onShowToolbar;
|
|
this._originalOnMouseMove = this._onMouseMove;
|
|
|
|
this._onShowToolbar = throttle(
|
|
() => this._originalOnShowToolbar(),
|
|
100,
|
|
{
|
|
leading: true,
|
|
trailing: false
|
|
});
|
|
|
|
this._onMouseMove = throttle(
|
|
event => this._originalOnMouseMove(event),
|
|
_mouseMoveCallbackInterval,
|
|
{
|
|
leading: true,
|
|
trailing: false
|
|
});
|
|
|
|
// Bind event handler so it is only bound once for every instance.
|
|
this._onFullScreenChange = this._onFullScreenChange.bind(this);
|
|
this._onVideospaceTouchStart = this._onVideospaceTouchStart.bind(this);
|
|
this._setBackground = this._setBackground.bind(this);
|
|
}
|
|
|
|
/**
|
|
* Start the connection and get the UI ready for the conference.
|
|
*
|
|
* @inheritdoc
|
|
*/
|
|
override componentDidMount() {
|
|
document.title = `${this.props._roomName} | ${interfaceConfig.APP_NAME}`;
|
|
this._start();
|
|
}
|
|
|
|
/**
|
|
* Calls into legacy UI to update the application layout, if necessary.
|
|
*
|
|
* @inheritdoc
|
|
* returns {void}
|
|
*/
|
|
override componentDidUpdate(prevProps: IProps) {
|
|
if (this.props._shouldDisplayTileView
|
|
=== prevProps._shouldDisplayTileView) {
|
|
return;
|
|
}
|
|
|
|
// TODO: For now VideoLayout is being called as LargeVideo and Filmstrip
|
|
// sizing logic is still handled outside of React. Once all components
|
|
// are in react they should calculate size on their own as much as
|
|
// possible and pass down sizings.
|
|
VideoLayout.refreshLayout();
|
|
}
|
|
|
|
/**
|
|
* Disconnect from the conference when component will be
|
|
* unmounted.
|
|
*
|
|
* @inheritdoc
|
|
*/
|
|
override componentWillUnmount() {
|
|
APP.UI.unbindEvents();
|
|
|
|
FULL_SCREEN_EVENTS.forEach(name =>
|
|
document.removeEventListener(name, this._onFullScreenChange));
|
|
|
|
APP.conference.isJoined() && this.props.dispatch(hangup());
|
|
}
|
|
|
|
/**
|
|
* Implements React's {@link Component#render()}.
|
|
*
|
|
* @inheritdoc
|
|
* @returns {ReactElement}
|
|
*/
|
|
override render() {
|
|
const {
|
|
_isAnyOverlayVisible,
|
|
_layoutClassName,
|
|
_notificationsVisible,
|
|
_overflowDrawer,
|
|
_showLobby,
|
|
_showPrejoin,
|
|
_showVisitorsQueue,
|
|
t
|
|
} = this.props;
|
|
|
|
return (
|
|
<div
|
|
id = 'layout_wrapper'
|
|
onMouseEnter = { this._onMouseEnter }
|
|
onMouseLeave = { this._onMouseLeave }
|
|
onMouseMove = { this._onMouseMove }
|
|
ref = { this._setBackground }>
|
|
<Chat />
|
|
<div
|
|
className = { _layoutClassName }
|
|
id = 'videoconference_page'
|
|
onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }>
|
|
{ _showPrejoin || _showLobby || <ConferenceInfo /> }
|
|
<Notice />
|
|
<div
|
|
id = 'videospace'
|
|
onTouchStart = { this._onVideospaceTouchStart }>
|
|
<LargeVideo />
|
|
{
|
|
_showPrejoin || _showLobby || (<>
|
|
<StageFilmstrip />
|
|
<ScreenshareFilmstrip />
|
|
<MainFilmstrip />
|
|
</>)
|
|
}
|
|
</div>
|
|
|
|
{ _showPrejoin || _showLobby || (
|
|
<>
|
|
<span
|
|
aria-level = { 1 }
|
|
className = 'sr-only'
|
|
role = 'heading'>
|
|
{ t('toolbar.accessibilityLabel.heading') }
|
|
</span>
|
|
<Toolbox />
|
|
</>
|
|
)}
|
|
|
|
{_notificationsVisible && !_isAnyOverlayVisible && (_overflowDrawer
|
|
? <JitsiPortal className = 'notification-portal'>
|
|
{this.renderNotificationsContainer({ portal: true })}
|
|
</JitsiPortal>
|
|
: this.renderNotificationsContainer())
|
|
}
|
|
|
|
<CalleeInfoContainer />
|
|
|
|
{ shouldShowPrejoin(this.props) && <Prejoin />}
|
|
{ (_showLobby && !_showVisitorsQueue) && <LobbyScreen />}
|
|
{ _showVisitorsQueue && <VisitorsQueue />}
|
|
</div>
|
|
<ParticipantsPane />
|
|
<ReactionAnimations />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sets custom background opacity based on config. It also applies the
|
|
* opacity on parent element, as the parent element is not accessible directly,
|
|
* only though it's child.
|
|
*
|
|
* @param {Object} element - The DOM element for which to apply opacity.
|
|
*
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_setBackground(element: HTMLDivElement) {
|
|
if (!element) {
|
|
return;
|
|
}
|
|
|
|
if (this.props._backgroundAlpha !== undefined) {
|
|
const elemColor = element.style.background;
|
|
const alphaElemColor = setColorAlpha(elemColor, this.props._backgroundAlpha);
|
|
|
|
element.style.background = alphaElemColor;
|
|
if (element.parentElement) {
|
|
const parentColor = element.parentElement.style.background;
|
|
const alphaParentColor = setColorAlpha(parentColor, this.props._backgroundAlpha);
|
|
|
|
element.parentElement.style.background = alphaParentColor;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler used for touch start on Video container.
|
|
*
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onVideospaceTouchStart() {
|
|
this.props.dispatch(toggleToolboxVisible());
|
|
}
|
|
|
|
/**
|
|
* Updates the Redux state when full screen mode has been enabled or
|
|
* disabled.
|
|
*
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onFullScreenChange() {
|
|
this.props.dispatch(fullScreenChanged(APP.UI.isFullScreen()));
|
|
}
|
|
|
|
/**
|
|
* Triggers iframe API mouseEnter event.
|
|
*
|
|
* @param {MouseEvent} event - The mouse event.
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onMouseEnter(event: React.MouseEvent) {
|
|
APP.API.notifyMouseEnter(event);
|
|
}
|
|
|
|
/**
|
|
* Triggers iframe API mouseLeave event.
|
|
*
|
|
* @param {MouseEvent} event - The mouse event.
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onMouseLeave(event: React.MouseEvent) {
|
|
APP.API.notifyMouseLeave(event);
|
|
}
|
|
|
|
/**
|
|
* Triggers iframe API mouseMove event.
|
|
*
|
|
* @param {MouseEvent} event - The mouse event.
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onMouseMove(event: React.MouseEvent) {
|
|
APP.API.notifyMouseMove(event);
|
|
}
|
|
|
|
/**
|
|
* Displays the toolbar.
|
|
*
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onShowToolbar() {
|
|
this.props.dispatch(showToolbox());
|
|
}
|
|
|
|
/**
|
|
* Until we don't rewrite UI using react components
|
|
* we use UI.start from old app. Also method translates
|
|
* component right after it has been mounted.
|
|
*
|
|
* @inheritdoc
|
|
*/
|
|
_start() {
|
|
APP.UI.start();
|
|
APP.UI.bindEvents();
|
|
|
|
FULL_SCREEN_EVENTS.forEach(name =>
|
|
document.addEventListener(name, this._onFullScreenChange));
|
|
|
|
const { dispatch, t } = this.props;
|
|
|
|
// if we will be showing prejoin we don't want to call connect from init.
|
|
// Connect will be dispatched from prejoin screen.
|
|
dispatch(init(!shouldShowPrejoin(this.props)));
|
|
|
|
maybeShowSuboptimalExperienceNotification(dispatch, t);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maps (parts of) the Redux state to the associated props for the
|
|
* {@code Conference} component.
|
|
*
|
|
* @param {Object} state - The Redux state.
|
|
* @private
|
|
* @returns {IProps}
|
|
*/
|
|
function _mapStateToProps(state: IReduxState) {
|
|
const { backgroundAlpha, mouseMoveCallbackInterval } = state['features/base/config'];
|
|
const { overflowDrawer } = state['features/toolbox'];
|
|
|
|
return {
|
|
...abstractMapStateToProps(state),
|
|
_backgroundAlpha: backgroundAlpha,
|
|
_isAnyOverlayVisible: Boolean(getOverlayToRender(state)),
|
|
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state) ?? ''],
|
|
_mouseMoveCallbackInterval: mouseMoveCallbackInterval,
|
|
_overflowDrawer: overflowDrawer,
|
|
_roomName: getConferenceNameForTitle(state),
|
|
_showLobby: getIsLobbyVisible(state),
|
|
_showPrejoin: isPrejoinPageVisible(state),
|
|
_showVisitorsQueue: showVisitorsQueue(state)
|
|
};
|
|
}
|
|
|
|
export default reactReduxConnect(_mapStateToProps)(translate(props => {
|
|
const dispatch = useDispatch();
|
|
const store = useStore();
|
|
|
|
const [ isDragging, setIsDragging ] = useState(false);
|
|
|
|
const { isOpen: isChatOpen } = useSelector((state: IReduxState) => state['features/chat']);
|
|
const isFileUploadEnabled = useSelector(isFileUploadingEnabled);
|
|
|
|
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setIsDragging(true);
|
|
}, []);
|
|
|
|
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setIsDragging(false);
|
|
}, []);
|
|
|
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (!isFileUploadEnabled) {
|
|
return;
|
|
}
|
|
|
|
if (isDragging) {
|
|
if (!isChatOpen) {
|
|
dispatch(openChat());
|
|
}
|
|
dispatch(setFocusedTab(ChatTabs.FILE_SHARING));
|
|
}
|
|
}, [ isChatOpen, isDragging, isFileUploadEnabled ]);
|
|
|
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setIsDragging(false);
|
|
|
|
if (!isFileUploadEnabled) {
|
|
return;
|
|
}
|
|
|
|
if (e.dataTransfer.files?.length > 0) {
|
|
processFiles(e.dataTransfer.files, store);
|
|
}
|
|
}, [ isFileUploadEnabled, processFiles ]);
|
|
|
|
return (
|
|
<div
|
|
onDragEnter = { handleDragEnter }
|
|
onDragLeave = { handleDragLeave }
|
|
onDragOver = { handleDragOver }
|
|
onDrop = { handleDrop }>
|
|
{/* @ts-ignore */}
|
|
<Conference { ...props } />
|
|
</div>
|
|
);
|
|
}));
|