This commit is contained in:
497
react/features/conference/components/web/Conference.tsx
Normal file
497
react/features/conference/components/web/Conference.tsx
Normal file
@@ -0,0 +1,497 @@
|
||||
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>
|
||||
);
|
||||
}));
|
||||
226
react/features/conference/components/web/ConferenceInfo.tsx
Normal file
226
react/features/conference/components/web/ConferenceInfo.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import E2EELabel from '../../../e2ee/components/E2EELabel';
|
||||
import HighlightButton from '../../../recording/components/Recording/web/HighlightButton';
|
||||
import RecordingLabel from '../../../recording/components/web/RecordingLabel';
|
||||
import { showToolbox } from '../../../toolbox/actions.web';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.web';
|
||||
import VideoQualityLabel from '../../../video-quality/components/VideoQualityLabel.web';
|
||||
import VisitorsCountLabel from '../../../visitors/components/web/VisitorsCountLabel';
|
||||
import ConferenceTimer from '../ConferenceTimer';
|
||||
import { getConferenceInfo } from '../functions.web';
|
||||
|
||||
import ConferenceInfoContainer from './ConferenceInfoContainer';
|
||||
import InsecureRoomNameLabel from './InsecureRoomNameLabel';
|
||||
import RaisedHandsCountLabel from './RaisedHandsCountLabel';
|
||||
import SpeakerStatsLabel from './SpeakerStatsLabel';
|
||||
import SubjectText from './SubjectText';
|
||||
import ToggleTopPanelLabel from './ToggleTopPanelLabel';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Subject}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The conference info labels to be shown in the conference header.
|
||||
*/
|
||||
_conferenceInfo: {
|
||||
alwaysVisible?: string[];
|
||||
autoHide?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Indicates whether the component should be visible or not.
|
||||
*/
|
||||
_visible: boolean;
|
||||
|
||||
/**
|
||||
* Invoked to active other features of the app.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
const COMPONENTS: Array<{
|
||||
Component: React.ComponentType<any>;
|
||||
id: string;
|
||||
}> = [
|
||||
{
|
||||
Component: HighlightButton,
|
||||
id: 'highlight-moment'
|
||||
},
|
||||
{
|
||||
Component: SubjectText,
|
||||
id: 'subject'
|
||||
},
|
||||
{
|
||||
Component: ConferenceTimer,
|
||||
id: 'conference-timer'
|
||||
},
|
||||
{
|
||||
Component: SpeakerStatsLabel,
|
||||
id: 'participants-count'
|
||||
},
|
||||
{
|
||||
Component: E2EELabel,
|
||||
id: 'e2ee'
|
||||
},
|
||||
{
|
||||
Component: () => (
|
||||
<>
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
|
||||
</>
|
||||
),
|
||||
id: 'recording'
|
||||
},
|
||||
{
|
||||
Component: RaisedHandsCountLabel,
|
||||
id: 'raised-hands-count'
|
||||
},
|
||||
{
|
||||
Component: VideoQualityLabel,
|
||||
id: 'video-quality'
|
||||
},
|
||||
{
|
||||
Component: VisitorsCountLabel,
|
||||
id: 'visitors-count'
|
||||
},
|
||||
{
|
||||
Component: InsecureRoomNameLabel,
|
||||
id: 'insecure-room'
|
||||
},
|
||||
{
|
||||
Component: ToggleTopPanelLabel,
|
||||
id: 'top-panel-toggle'
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* The upper band of the meeing containing the conference name, timer and labels.
|
||||
*
|
||||
* @param {Object} props - The props of the component.
|
||||
* @returns {React$None}
|
||||
*/
|
||||
class ConferenceInfo extends Component<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code ConferenceInfo} instance.
|
||||
*
|
||||
* @param {IProps} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._renderAutoHide = this._renderAutoHide.bind(this);
|
||||
this._renderAlwaysVisible = this._renderAlwaysVisible.bind(this);
|
||||
this._onTabIn = this._onTabIn.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when the component is focused to show the conference
|
||||
* info if necessary.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTabIn() {
|
||||
if (this.props._conferenceInfo.autoHide?.length && !this.props._visible) {
|
||||
this.props.dispatch(showToolbox());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders auto-hidden info header labels.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_renderAutoHide() {
|
||||
const { autoHide } = this.props._conferenceInfo;
|
||||
|
||||
if (!autoHide?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConferenceInfoContainer
|
||||
id = 'autoHide'
|
||||
visible = { this.props._visible }>
|
||||
{
|
||||
COMPONENTS
|
||||
.filter(comp => autoHide.includes(comp.id))
|
||||
.map(c =>
|
||||
<c.Component key = { c.id } />
|
||||
)
|
||||
}
|
||||
</ConferenceInfoContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the always visible info header labels.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_renderAlwaysVisible() {
|
||||
const { alwaysVisible } = this.props._conferenceInfo;
|
||||
|
||||
if (!alwaysVisible?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConferenceInfoContainer
|
||||
id = 'alwaysVisible'
|
||||
visible = { true } >
|
||||
{
|
||||
COMPONENTS
|
||||
.filter(comp => alwaysVisible.includes(comp.id))
|
||||
.map(c =>
|
||||
<c.Component key = { c.id } />
|
||||
)
|
||||
}
|
||||
</ConferenceInfoContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<div
|
||||
className = 'details-container'
|
||||
onFocus = { this._onTabIn }>
|
||||
{ this._renderAlwaysVisible() }
|
||||
{ this._renderAutoHide() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated
|
||||
* {@code Subject}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _visible: boolean,
|
||||
* _conferenceInfo: Object
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_visible: isToolboxVisible(state),
|
||||
_conferenceInfo: getConferenceInfo(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(ConferenceInfo);
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
import { isAlwaysOnTitleBarEmpty } from '../functions.web';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The children components.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
|
||||
/**
|
||||
* Id of the component.
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* Whether this conference info container should be visible or not.
|
||||
*/
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export default ({ visible, children, id }: IProps) => (
|
||||
<div
|
||||
className = { `subject${isAlwaysOnTitleBarEmpty() ? '' : ' with-always-on'}${visible ? ' visible' : ''}` }
|
||||
id = { id }>
|
||||
<div className = { 'subject-info-container' }>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IDisplayProps } from '../ConferenceTimer';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
timer: {
|
||||
...theme.typography.labelRegular,
|
||||
color: theme.palette.text01,
|
||||
padding: '6px 8px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
boxSizing: 'border-box',
|
||||
height: '28px',
|
||||
borderRadius: `0 ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0`,
|
||||
marginRight: '2px',
|
||||
|
||||
'@media (max-width: 300px)': {
|
||||
display: 'none'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns web element to be rendered.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
export default function ConferenceTimerDisplay({ timerValue, textStyle: _textStyle }: IDisplayProps) {
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<span className = { classes.timer }>{ timerValue }</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconExclamationTriangle } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/web/Label';
|
||||
import { COLORS } from '../../../base/label/constants';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
import getUnsafeRoomText from '../../../base/util/getUnsafeRoomText.web';
|
||||
import AbstractInsecureRoomNameLabel, { _mapStateToProps } from '../AbstractInsecureRoomNameLabel';
|
||||
|
||||
/**
|
||||
* Renders a label indicating that we are in a room with an insecure name.
|
||||
*/
|
||||
class InsecureRoomNameLabel extends AbstractInsecureRoomNameLabel {
|
||||
/**
|
||||
* Renders the platform dependent content.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _render() {
|
||||
return (
|
||||
<Tooltip
|
||||
content = { getUnsafeRoomText(this.props.t, 'meeting') }
|
||||
position = 'bottom'>
|
||||
<Label
|
||||
color = { COLORS.red }
|
||||
icon = { IconExclamationTriangle } />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(InsecureRoomNameLabel));
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
dialog: {
|
||||
marginBottom: theme.spacing(1)
|
||||
},
|
||||
|
||||
text: {
|
||||
fontSize: '1.25rem'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link LeaveReasonDialog}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Callback invoked when {@code LeaveReasonDialog} is unmounted.
|
||||
*/
|
||||
onClose: () => void;
|
||||
|
||||
/**
|
||||
* The title to display in the dialog.
|
||||
*/
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A React {@code Component} for displaying a dialog with a reason that ended the conference.
|
||||
*
|
||||
* @param {IProps} props - Component's props.
|
||||
* @returns {JSX}
|
||||
*/
|
||||
const LeaveReasonDialog = ({ onClose, title }: IProps) => {
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => () => {
|
||||
onClose?.();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ hidden: true }}
|
||||
onSubmit = { onClose }
|
||||
size = 'medium'
|
||||
testId = 'dialog.leaveReason'>
|
||||
<div className = { classes.dialog }>
|
||||
{title ? <div className = { classes.text }>{t(title)}</div> : null}
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeaveReasonDialog;
|
||||
43
react/features/conference/components/web/Notice.tsx
Normal file
43
react/features/conference/components/web/Notice.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
notice: {
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
zIndex: 3,
|
||||
marginTop: theme.spacing(2),
|
||||
transform: 'translateX(-50%)'
|
||||
},
|
||||
|
||||
message: {
|
||||
backgroundColor: theme.palette.uiBackground,
|
||||
color: theme.palette.text01,
|
||||
padding: '3px',
|
||||
borderRadius: '5px'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const Notice = () => {
|
||||
const message = useSelector((state: IReduxState) => state['features/base/config'].noticeMessage);
|
||||
const { classes } = useStyles();
|
||||
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { classes.notice }>
|
||||
<span className = { classes.message } >
|
||||
{message}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notice;
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { IconRaiseHand } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/web/Label';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
import { open as openParticipantsPane } from '../../../participants-pane/actions.web';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
label: {
|
||||
backgroundColor: theme.palette.warning02,
|
||||
color: theme.palette.uiBackground
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const RaisedHandsCountLabel = () => {
|
||||
const { classes: styles, theme } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const raisedHandsCount = useSelector((state: IReduxState) =>
|
||||
(state['features/base/participants'].raisedHandsQueue || []).length);
|
||||
const { t } = useTranslation();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(openParticipantsPane());
|
||||
}, []);
|
||||
|
||||
return raisedHandsCount > 0 ? (<Tooltip
|
||||
content = { t('raisedHandsLabel') }
|
||||
position = { 'bottom' }>
|
||||
<Label
|
||||
accessibilityText = { t('raisedHandsLabel') }
|
||||
className = { styles.label }
|
||||
icon = { IconRaiseHand }
|
||||
iconColor = { theme.palette.icon04 }
|
||||
id = 'raisedHandsCountLabel'
|
||||
onClick = { onClick }
|
||||
text = { `${raisedHandsCount}` } />
|
||||
</Tooltip>) : null;
|
||||
};
|
||||
|
||||
export default RaisedHandsCountLabel;
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconUsers } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/web/Label';
|
||||
import { COLORS } from '../../../base/label/constants';
|
||||
import { getParticipantCountForDisplay } from '../../../base/participants/functions';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
import SpeakerStats from '../../../speaker-stats/components/web/SpeakerStats';
|
||||
import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
|
||||
|
||||
/**
|
||||
* ParticipantsCount react component.
|
||||
* Displays the number of participants and opens Speaker stats on click.
|
||||
*
|
||||
* @class ParticipantsCount
|
||||
*/
|
||||
function SpeakerStatsLabel() {
|
||||
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
|
||||
const count = useSelector(getParticipantCountForDisplay);
|
||||
const _isSpeakerStatsDisabled = useSelector(isSpeakerStatsDisabled);
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClick = () => {
|
||||
dispatch(openDialog(SpeakerStats, { conference }));
|
||||
};
|
||||
|
||||
if (count <= 2 || _isSpeakerStatsDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content = { t('speakerStats.labelTooltip', { count }) }
|
||||
position = { 'bottom' }>
|
||||
<Label
|
||||
color = { COLORS.white }
|
||||
icon = { IconUsers }
|
||||
iconColor = '#fff'
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick = { onClick }
|
||||
text = { `${count}` } />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpeakerStatsLabel;
|
||||
54
react/features/conference/components/web/SubjectText.tsx
Normal file
54
react/features/conference/components/web/SubjectText.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { getConferenceName } from '../../../base/conference/functions';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
...theme.typography.bodyLongRegular,
|
||||
color: theme.palette.text01,
|
||||
padding: '2px 16px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
maxWidth: '324px',
|
||||
boxSizing: 'border-box',
|
||||
height: '28px',
|
||||
borderRadius: `${theme.shape.borderRadius}px 0 0 ${theme.shape.borderRadius}px`,
|
||||
marginLeft: '2px',
|
||||
|
||||
'@media (max-width: 300px)': {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
content: {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Label for the conference name.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
const SubjectText = () => {
|
||||
const subject = useSelector(getConferenceName);
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content = { subject }
|
||||
position = 'bottom'>
|
||||
<div className = { classes.container }>
|
||||
<div className = { clsx('subject-text--content', classes.content) }>{subject}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubjectText;
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { IconArrowDown } from '../../../base/icons/svg/index';
|
||||
import Label from '../../../base/label/components/web/Label';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
import { setTopPanelVisible } from '../../../filmstrip/actions.web';
|
||||
|
||||
const ToggleTopPanelLabel = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const topPanelHidden = !useSelector((state: IReduxState) => state['features/filmstrip'].topPanelVisible);
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(setTopPanelVisible(true));
|
||||
}, []);
|
||||
|
||||
return topPanelHidden ? (<Tooltip
|
||||
content = { t('toggleTopPanelLabel') }
|
||||
position = { 'bottom' }>
|
||||
<Label
|
||||
icon = { IconArrowDown }
|
||||
onClick = { onClick } />
|
||||
</Tooltip>) : null;
|
||||
};
|
||||
|
||||
export default ToggleTopPanelLabel;
|
||||
Reference in New Issue
Block a user