diff --git a/config.js b/config.js index 7993959f3cb9f..076293e18b543 100644 --- a/config.js +++ b/config.js @@ -762,15 +762,32 @@ var config = { // If true, tile view will not be enabled automatically when the participants count threshold is reached. // disableTileView: true, + // Controls the visibility and behavior of the top header conference info labels. + // If a label's id is not in any of the 2 arrays, it will not be visible at all on the header. + // conferenceInfo: { + // // those labels will not be hidden in tandem with the toolbox. + // alwaysVisible: ['recording', 'local-recording'], + // // those labels will be auto-hidden in tandem with the toolbox buttons. + // autoHide: [ + // 'subject', + // 'conference-timer', + // 'participants-count', + // 'e2ee', + // 'transcribing', + // 'video-quality', + // 'insecure-room' + // ] + // }, + // Hides the conference subject // hideConferenceSubject: true, - // Hides the recording label - // hideRecordingLabel: false, - // Hides the conference timer. // hideConferenceTimer: true, + // Hides the recording label + // hideRecordingLabel: false, + // Hides the participants stats // hideParticipantsStats: true, diff --git a/css/_subject.scss b/css/_subject.scss index 22087dd44f199..61ceaea6a4394 100644 --- a/css/_subject.scss +++ b/css/_subject.scss @@ -1,54 +1,22 @@ .subject { - box-sizing: border-box; color: #fff; - margin-top: 20px; - position: absolute; - top: -120px; - transition: top .3s ease-in; - width: 100%; + margin-top: -120px; + transition: margin-top .3s ease-in; z-index: $zindex3; &.visible { - top: 0; - } - - &.recording { - top: 0; - - .subject-details-container { - opacity: 0; - transition: opacity .3s ease-in; - } - - .subject-info-container .show-always { - transition: margin-left .3s ease-in; - } - - &.visible { - .subject-details-container { - opacity: 1; - } - } + margin-top: 20px; } } -.subject-details-container { - display: flex; -} - .subject-info-container { display: flex; justify-content: center; - max-width: calc(100% - 280px); margin: 0 auto; - - &--full-width { - max-width: 100%; - } + height: 28px; @media (max-width: 500px) { flex-wrap: wrap; - max-width: 100%; } } @@ -63,21 +31,47 @@ .subject-text { background: rgba(0, 0, 0, 0.6); border-radius: 3px 0px 0px 3px; + box-sizing: border-box; font-size: 14px; - line-height: 24px; - padding: 2px 16px; - height: 24px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + line-height: 28px; + padding: 0 16px; + height: 28px; + + @media (max-width: 700px) { + max-width: 100px; + } + + @media (max-width: 300px) { + display: none; + } + + &--content { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } .subject-timer { background: rgba(0, 0, 0, 0.8); border-radius: 0px 3px 3px 0px; - display: inline-block; + box-sizing: border-box; font-size: 12px; - line-height: 16px; + line-height: 28px; min-width: 34px; - padding: 6px 8px; + padding: 0 8px; + height: 28px; + + @media (max-width: 300px) { + display: none; + } +} + +.details-container { + width: 100%; + display: flex; + justify-content: center; + position: absolute; + top: 0; + height: 48px; } diff --git a/react/features/base/config/configWhitelist.js b/react/features/base/config/configWhitelist.js index 9ff4357c66443..944b7b927b09d 100644 --- a/react/features/base/config/configWhitelist.js +++ b/react/features/base/config/configWhitelist.js @@ -68,6 +68,7 @@ export default [ */ 'callUUID', + 'conferenceInfo', 'channelLastN', 'constraints', 'brandingRoomAlias', diff --git a/react/features/base/config/reducer.js b/react/features/base/config/reducer.js index 3df447e7ea415..49ff9b47e292e 100644 --- a/react/features/base/config/reducer.js +++ b/react/features/base/config/reducer.js @@ -2,6 +2,7 @@ import _ from 'lodash'; +import { CONFERENCE_INFO } from '../../conference/components/constants'; import { equals, ReducerRegistry } from '../redux'; import { @@ -172,6 +173,27 @@ function _setConfig(state, { config }) { return equals(state, newState) ? state : newState; } +/** + * Processes the conferenceInfo object against the defaults. + * + * @param {Object} config - The old config. + * @returns {Object} The processed conferenceInfo object. + */ +function _getConferenceInfo(config) { + const { conferenceInfo } = config; + + if (conferenceInfo) { + return { + alwaysVisible: conferenceInfo.alwaysVisible ?? [ ...CONFERENCE_INFO.alwaysVisible ], + autoHide: conferenceInfo.autoHide ?? [ ...CONFERENCE_INFO.autoHide ] + }; + } + + return { + ...CONFERENCE_INFO + }; +} + /** * Constructs a new config {@code Object}, if necessary, out of a specific * config {@code Object} which is in the latest format supported by jitsi-meet. @@ -194,6 +216,46 @@ function _translateLegacyConfig(oldValue: Object) { newValue.toolbarButtons = interfaceConfig.TOOLBAR_BUTTONS; } + const { + hideConferenceTimer, + hideConferenceSubject, + hideParticipantsStats, + hideRecordingLabel + } = oldValue; + + if (hideConferenceTimer || hideConferenceSubject || hideParticipantsStats || hideRecordingLabel) { + newValue.conferenceInfo = _getConferenceInfo(oldValue); + + if (hideConferenceTimer) { + newValue.conferenceInfo.alwaysVisible + = newValue.conferenceInfo.alwaysVisible.filter(c => c !== 'conference-timer'); + newValue.conferenceInfo.autoHide + = newValue.conferenceInfo.autoHide.filter(c => c !== 'conference-timer'); + } + if (hideConferenceSubject) { + newValue.conferenceInfo.alwaysVisible + = newValue.conferenceInfo.alwaysVisible.filter(c => c !== 'subject'); + newValue.conferenceInfo.autoHide + = newValue.conferenceInfo.autoHide.filter(c => c !== 'subject'); + } + if (hideParticipantsStats) { + newValue.conferenceInfo.alwaysVisible + = newValue.conferenceInfo.alwaysVisible.filter(c => c !== 'participants-count'); + newValue.conferenceInfo.autoHide + = newValue.conferenceInfo.autoHide.filter(c => c !== 'participants-count'); + } + + // hideRecordingLabel does not mean not render it at all, but autoHide it + if (hideRecordingLabel) { + const recValues = [ 'recording', 'local-recording' ]; + + newValue.conferenceInfo.alwaysVisible + = newValue.conferenceInfo.alwaysVisible.filter(c => !recValues.includes(c)); + newValue.conferenceInfo.autoHide + = _.union(newValue.conferenceInfo.autoHide, recValues); + } + } + if (oldValue.stereo || oldValue.opusMaxAverageBitrate) { newValue.audioQuality = { opusMaxAverageBitrate: oldValue.audioQuality?.opusMaxAverageBitrate ?? oldValue.opusMaxAverageBitrate, diff --git a/react/features/conference/components/constants.js b/react/features/conference/components/constants.js new file mode 100644 index 0000000000000..e7dd34d1afdd6 --- /dev/null +++ b/react/features/conference/components/constants.js @@ -0,0 +1,12 @@ +export const CONFERENCE_INFO = { + alwaysVisible: [ 'recording', 'local-recording' ], + autoHide: [ + 'subject', + 'conference-timer', + 'participants-count', + 'e2ee', + 'transcribing', + 'video-quality', + 'insecure-room' + ] +}; diff --git a/react/features/conference/components/functions.js b/react/features/conference/components/functions.js new file mode 100644 index 0000000000000..4a221e66137da --- /dev/null +++ b/react/features/conference/components/functions.js @@ -0,0 +1,22 @@ +// @flow + +import { CONFERENCE_INFO } from './constants'; + +/** + * Retrieves the conference info labels based on config values and defaults. + * + * @param {Object} state - The redux state. + * @returns {Object} The conferenceInfo object. + */ +export const getConferenceInfo = (state: Object) => { + const { conferenceInfo } = state['features/base/config']; + + if (conferenceInfo) { + return { + alwaysVisible: conferenceInfo.alwaysVisible ?? CONFERENCE_INFO.alwaysVisible, + autoHide: conferenceInfo.autoHide ?? CONFERENCE_INFO.autoHide + }; + } + + return CONFERENCE_INFO; +}; diff --git a/react/features/conference/components/web/ConferenceInfo.js b/react/features/conference/components/web/ConferenceInfo.js index f6cb594276951..2b5b6ea8f8508 100644 --- a/react/features/conference/components/web/ConferenceInfo.js +++ b/react/features/conference/components/web/ConferenceInfo.js @@ -1,22 +1,22 @@ /* @flow */ -import React from 'react'; +import React, { Component } from 'react'; -import { getConferenceName } from '../../../base/conference/functions'; import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet'; -import { getParticipantCount } from '../../../base/participants/functions'; import { connect } from '../../../base/redux'; import { E2EELabel } from '../../../e2ee'; import { LocalRecordingLabel } from '../../../local-recording'; -import { getSessionStatusToShow, RecordingLabel } from '../../../recording'; +import { RecordingLabel } from '../../../recording'; import { isToolboxVisible } from '../../../toolbox/functions.web'; import { TranscribingLabel } from '../../../transcribing'; import { VideoQualityLabel } from '../../../video-quality'; import ConferenceTimer from '../ConferenceTimer'; +import { getConferenceInfo } from '../functions'; +import ConferenceInfoContainer from './ConferenceInfoContainer'; +import InsecureRoomNameLabel from './InsecureRoomNameLabel'; import ParticipantsCount from './ParticipantsCount'; - -import { InsecureRoomNameLabel } from '.'; +import SubjectText from './SubjectText'; /** * The type of the React {@code Component} props of {@link Subject}. @@ -24,116 +24,150 @@ import { InsecureRoomNameLabel } from '.'; type Props = { /** - * Whether the info should span across the full width. + * The conference info labels to be shown in the conference header. */ - _fullWidth: boolean, + _conferenceInfo: Object, /** - * Whether the conference name and timer should be displayed or not. + * Indicates whether the component should be visible or not. */ - _hideConferenceNameAndTimer: boolean, + _visible: boolean +}; - /** - * Whether the conference timer should be shown or not. - */ - _hideConferenceTimer: boolean, +const COMPONENTS = [ + { + Component: SubjectText, + id: 'subject' + }, + { + Component: ConferenceTimer, + id: 'conference-timer' + }, + { + Component: ParticipantsCount, + id: 'participants-count' + }, + { + Component: E2EELabel, + id: 'e2ee' + }, + { + Component: () => ( + <> + + + + ), + id: 'recording' + }, + { + Component: LocalRecordingLabel, + id: 'local-recording' + }, + { + Component: TranscribingLabel, + id: 'transcribing' + }, + { + Component: VideoQualityLabel, + id: 'video-quality' + }, + { + Component: InsecureRoomNameLabel, + id: 'insecure-room' + } +]; +/** + * 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 { /** - * Whether the recording label should be shown or not. + * Initializes a new {@code ConferenceInfo} instance. + * + * @param {Props} props - The read-only React {@code Component} props with + * which the new instance is to be initialized. */ - _hideRecordingLabel: boolean, + constructor(props: Props) { + super(props); - /** - * Whether the participant count should be shown or not. - */ - _showParticipantCount: boolean, + this._renderAutoHide = this._renderAutoHide.bind(this); + this._renderAlwaysVisible = this._renderAlwaysVisible.bind(this); + } + + _renderAutoHide: () => void; /** - * The subject or the of the conference. - * Falls back to conference name. + * Renders auto-hidden info header labels. + * + * @returns {void} */ - _subject: string, + _renderAutoHide() { + const { autoHide } = this.props._conferenceInfo; + + if (!autoHide || !autoHide.length) { + return null; + } + + return ( + + { + COMPONENTS + .filter(comp => autoHide.includes(comp.id)) + .map(c => + + ) + } + + ); + } + + _renderAlwaysVisible: () => void; /** - * Indicates whether the component should be visible or not. + * Renders the always visible info header labels. + * + * @returns {void} */ - _visible: boolean, + _renderAlwaysVisible() { + const { alwaysVisible } = this.props._conferenceInfo; + + if (!alwaysVisible || !alwaysVisible.length) { + return null; + } + + return ( + + { + COMPONENTS + .filter(comp => alwaysVisible.includes(comp.id)) + .map(c => + + ) + } + + ); + } /** - * Whether or not the recording label is visible. + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} */ - _recordingLabel: boolean -}; - -const getLeftMargin = () => { - const subjectContainerWidth = document.getElementById('subject-container')?.clientWidth ?? 0; - const recContainerWidth = document.getElementById('rec-container')?.clientWidth ?? 0; - const subjectDetailsContainer = document.getElementById('subject-details-container')?.clientWidth ?? 0; - - return (subjectContainerWidth - recContainerWidth - subjectDetailsContainer) / 2; -}; - -/** - * The upper band of the meeing containing the conference name, timer and labels. - * - * @param {Object} props - The props of the component. - * @returns {React$None} - */ -function ConferenceInfo(props: Props) { - const { - _hideConferenceNameAndTimer, - _hideConferenceTimer, - _showParticipantCount, - _hideRecordingLabel, - _subject, - _fullWidth, - _visible, - _recordingLabel - } = props; - - return ( -
-
- {!_hideRecordingLabel &&
- - - -
- } -
- { - !_hideConferenceNameAndTimer - &&
- { _subject && { _subject }} - { !_hideConferenceTimer && } -
- } - { _showParticipantCount && } - - {_hideRecordingLabel && ( - <> - - - - - )} - - - -
+ render() { + return ( +
+ { [ + this._renderAlwaysVisible(), + this._renderAutoHide() + ] }
-
- ); + ); + } } /** @@ -143,40 +177,14 @@ function ConferenceInfo(props: Props) { * @param {Object} state - The Redux state. * @private * @returns {{ - * _hideConferenceTimer: boolean, - * _showParticipantCount: boolean, - * _subject: string, - * _visible: boolean + * _visible: boolean, + * _conferenceInfo: Object * }} */ function _mapStateToProps(state) { - const participantCount = getParticipantCount(state); - const { - hideConferenceTimer, - hideConferenceSubject, - hideParticipantsStats, - hideRecordingLabel, - iAmRecorder - } = state['features/base/config']; - const { clientWidth } = state['features/base/responsive-ui']; - - const shouldHideRecordingLabel = hideRecordingLabel || iAmRecorder; - const fileRecordingStatus = getSessionStatusToShow(state, JitsiRecordingConstants.mode.FILE); - const streamRecordingStatus = getSessionStatusToShow(state, JitsiRecordingConstants.mode.STREAM); - const isFileRecording = fileRecordingStatus ? fileRecordingStatus !== JitsiRecordingConstants.status.OFF : false; - const isStreamRecording = streamRecordingStatus - ? streamRecordingStatus !== JitsiRecordingConstants.status.OFF : false; - const { isEngaged } = state['features/local-recording']; - return { - _hideConferenceNameAndTimer: clientWidth < 300, - _hideConferenceTimer: Boolean(hideConferenceTimer), - _hideRecordingLabel: shouldHideRecordingLabel, - _fullWidth: state['features/video-layout'].tileViewEnabled, - _showParticipantCount: participantCount > 2 && !hideParticipantsStats, - _subject: hideConferenceSubject ? '' : getConferenceName(state), _visible: isToolboxVisible(state), - _recordingLabel: (isFileRecording || isStreamRecording || isEngaged) && !shouldHideRecordingLabel + _conferenceInfo: getConferenceInfo(state) }; } diff --git a/react/features/conference/components/web/ConferenceInfoContainer.js b/react/features/conference/components/web/ConferenceInfoContainer.js new file mode 100644 index 0000000000000..d3ec4ebcb3c44 --- /dev/null +++ b/react/features/conference/components/web/ConferenceInfoContainer.js @@ -0,0 +1,24 @@ +/* @flow */ + +import React from 'react'; + +type Props = { + + /** + * The children components. + */ + children: React$Node, + + /** + * Whether this conference info container should be visible or not. + */ + visible: boolean +} + +export default ({ visible, children }: Props) => ( +
+
+ {children} +
+
+); diff --git a/react/features/conference/components/web/ParticipantsCount.js b/react/features/conference/components/web/ParticipantsCount.js index bc4aaf424de4f..95b5e0b6eea06 100644 --- a/react/features/conference/components/web/ParticipantsCount.js +++ b/react/features/conference/components/web/ParticipantsCount.js @@ -19,7 +19,7 @@ type Props = { /** * Number of the conference participants. */ - count: string, + count: number, /** * Conference data. @@ -72,6 +72,12 @@ class ParticipantsCount extends PureComponent { * @returns {ReactElement} */ render() { + const { count } = this.props; + + if (count <= 2) { + return null; + } + return (
{
); } diff --git a/react/features/conference/components/web/SubjectText.js b/react/features/conference/components/web/SubjectText.js new file mode 100644 index 0000000000000..6e44ec447e1d5 --- /dev/null +++ b/react/features/conference/components/web/SubjectText.js @@ -0,0 +1,50 @@ +/* @flow */ + +import React from 'react'; + +import { getConferenceName } from '../../../base/conference/functions'; +import { connect } from '../../../base/redux'; +import { Tooltip } from '../../../base/tooltip'; + +type Props = { + + /** + * The conference display name. + */ + _subject: string +} + +/** + * Label for the conference name. + * + * @param {Props} props - The props of the component. + * @returns {ReactElement} + */ +const SubjectText = ({ _subject }: Props) => ( +
+ +
{ _subject }
+
+
+); + + +/** + * Maps (parts of) the Redux state to the associated + * {@code Subject}'s props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _subject: string, + * }} + */ +function _mapStateToProps(state) { + return { + _subject: getConferenceName(state) + }; +} + +export default connect(_mapStateToProps)(SubjectText); diff --git a/react/features/conference/components/web/index.js b/react/features/conference/components/web/index.js index 94b05b4877a71..dd62a4697fa16 100644 --- a/react/features/conference/components/web/index.js +++ b/react/features/conference/components/web/index.js @@ -5,3 +5,4 @@ export { default as renderConferenceTimer } from './ConferenceTimerDisplay'; export { default as InsecureRoomNameLabel } from './InsecureRoomNameLabel'; export { default as InviteMore } from './InviteMore'; export { default as ConferenceInfo } from './ConferenceInfo'; +export { default as SubjectText } from './SubjectText'; diff --git a/react/features/local-recording/components/LocalRecordingLabel.web.js b/react/features/local-recording/components/LocalRecordingLabel.web.js index 0316481c48c41..b4642db0ceed4 100644 --- a/react/features/local-recording/components/LocalRecordingLabel.web.js +++ b/react/features/local-recording/components/LocalRecordingLabel.web.js @@ -14,14 +14,19 @@ import { Tooltip } from '../../base/tooltip'; type Props = { /** - * Invoked to obtain translated strings. + * Whether this is the Jibri recorder participant. */ - t: Function, + _iAmRecorder: boolean, + + /** + * Whether local recording is engaged or not. + */ + _isEngaged: boolean, /** - * Whether local recording is engaged or not. + * Invoked to obtain translated strings. */ - isEngaged: boolean + t: Function, }; /** @@ -38,7 +43,7 @@ class LocalRecordingLabel extends Component { * @returns {ReactElement} */ render() { - if (!this.props.isEngaged) { + if (!this.props._isEngaged || this.props._iAmRecorder) { return null; } @@ -66,9 +71,11 @@ class LocalRecordingLabel extends Component { */ function _mapStateToProps(state) { const { isEngaged } = state['features/local-recording']; + const { iAmRecorder } = state['features/base/config']; return { - isEngaged + _isEngaged: isEngaged, + _iAmRecorder: iAmRecorder }; } diff --git a/react/features/recording/components/AbstractRecordingLabel.js b/react/features/recording/components/AbstractRecordingLabel.js index c829984233aec..4c710ef29b584 100644 --- a/react/features/recording/components/AbstractRecordingLabel.js +++ b/react/features/recording/components/AbstractRecordingLabel.js @@ -15,6 +15,11 @@ import { getSessionStatusToShow } from '../functions'; */ type Props = { + /** + * Whether this is the Jibri recorder participant. + */ + _iAmRecorder: boolean, + /** * The status of the highermost priority session. */ @@ -100,7 +105,7 @@ export default class AbstractRecordingLabel * @inheritdoc */ render() { - return this.props._status && !this.state.staleLabel + return this.props._status && !this.state.staleLabel && !this.props._iAmRecorder ? this._renderLabel() : null; } @@ -172,6 +177,7 @@ export function _mapStateToProps(state: Object, ownProps: Props) { const { mode } = ownProps; return { + _iAmRecorder: state['features/base/config'].iAmRecorder, _status: getSessionStatusToShow(state, mode) }; }