init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View 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>
);
}));

View 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);

View File

@@ -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>
);

View File

@@ -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>
);
}

View File

@@ -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));

View File

@@ -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;

View 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;

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -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;