diff --git a/config.js b/config.js index 15a727ec9..d15526799 100644 --- a/config.js +++ b/config.js @@ -1032,7 +1032,7 @@ var config = { // 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'], + // alwaysVisible: ['recording', 'local-recording', 'raised-hands-count'], // // those labels will be auto-hidden in tandem with the toolbox buttons. // autoHide: [ // 'subject', diff --git a/css/_subject.scss b/css/_subject.scss index f63d45b50..47b374d4b 100644 --- a/css/_subject.scss +++ b/css/_subject.scss @@ -1,11 +1,25 @@ .subject { color: #fff; - margin-top: -120px; - transition: margin-top .3s ease-in; + transition: opacity .3s ease-in; z-index: $zindex3; + margin-top: 20px; + opacity: 0; &.visible { - margin-top: 20px; + opacity: 1; + } + + &#autoHide.with-always-on { + overflow: hidden; + animation: hideSubject forwards .3s ease-out; + + & > .subject-info-container { + justify-content: flex-start; + } + + &.visible { + animation: showSubject forwards .3s ease-out; + } } } @@ -36,10 +50,7 @@ line-height: 28px; padding: 0 16px; height: 28px; - - @media (max-width: 700px) { - max-width: 100px; - } + max-width: 324px; @media (max-width: 300px) { display: none; @@ -74,8 +85,29 @@ position: absolute; top: 0; height: 48px; + max-width: calc(100% - 24px); } .shift-right .details-container { margin-left: calc(#{$sidebarWidth} / 2); } + +@keyframes hideSubject { + 0% { + max-width: 100%; + } + + 100% { + max-width: 0; + } +} + +@keyframes showSubject { + 0% { + max-width: 0%; + } + + 100% { + max-width: 100%; + } +} diff --git a/lang/main.json b/lang/main.json index de96003de..5d945743c 100644 --- a/lang/main.json +++ b/lang/main.json @@ -784,6 +784,7 @@ "title": "Profile" }, "raisedHand": "Would like to speak", + "raisedHandsLabel": "Number of raised hands", "recording": { "limitNotificationDescriptionWeb": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try {{app}}.", "limitNotificationDescriptionNative": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <3>{{app}}.", diff --git a/react/features/base/label/components/Label.web.js b/react/features/base/label/components/Label.web.js index ef818dcf2..42cc5d6ea 100644 --- a/react/features/base/label/components/Label.web.js +++ b/react/features/base/label/components/Label.web.js @@ -34,6 +34,11 @@ type Props = AbstractProps & { */ id: string, + /** + * Color for the icon. + */ + iconColor?: string, + /** * Click handler if any. */ @@ -103,6 +108,7 @@ class Label extends AbstractLabel { className, color, icon, + iconColor, id, onClick, text @@ -120,6 +126,7 @@ class Label extends AbstractLabel { id = { id } onClick = { onClick }> { icon && } { text && {text} } diff --git a/react/features/conference/components/constants.js b/react/features/conference/components/constants.js index e7dd34d1a..81a621db9 100644 --- a/react/features/conference/components/constants.js +++ b/react/features/conference/components/constants.js @@ -1,5 +1,5 @@ export const CONFERENCE_INFO = { - alwaysVisible: [ 'recording', 'local-recording' ], + alwaysVisible: [ 'recording', 'local-recording', 'raised-hands-count' ], autoHide: [ 'subject', 'conference-timer', diff --git a/react/features/conference/components/functions.js b/react/features/conference/components/functions.any.js similarity index 100% rename from react/features/conference/components/functions.js rename to react/features/conference/components/functions.any.js diff --git a/react/features/conference/components/functions.native.js b/react/features/conference/components/functions.native.js new file mode 100644 index 000000000..fb2a6bc39 --- /dev/null +++ b/react/features/conference/components/functions.native.js @@ -0,0 +1 @@ +export * from './functions.any'; diff --git a/react/features/conference/components/functions.web.js b/react/features/conference/components/functions.web.js new file mode 100644 index 000000000..90dc0f236 --- /dev/null +++ b/react/features/conference/components/functions.web.js @@ -0,0 +1,14 @@ +// @flow + +export * from './functions.any'; + +/** + * Whether or not there are always on labels. + * + * @returns {boolean} + */ +export function isAlwaysOnTitleBarEmpty() { + const bar = document.querySelector('#alwaysVisible>div'); + + return bar?.childNodes.length === 0; +} diff --git a/react/features/conference/components/web/ConferenceInfo.js b/react/features/conference/components/web/ConferenceInfo.js index 260385ebf..834da37e5 100644 --- a/react/features/conference/components/web/ConferenceInfo.js +++ b/react/features/conference/components/web/ConferenceInfo.js @@ -18,6 +18,7 @@ import { getConferenceInfo } from '../functions'; import ConferenceInfoContainer from './ConferenceInfoContainer'; import InsecureRoomNameLabel from './InsecureRoomNameLabel'; import ParticipantsCount from './ParticipantsCount'; +import RaisedHandsCountLabel from './RaisedHandsCountLabel'; import SubjectText from './SubjectText'; /** @@ -66,6 +67,10 @@ const COMPONENTS = [ Component: LocalRecordingLabel, id: 'local-recording' }, + { + Component: RaisedHandsCountLabel, + id: 'raised-hands-count' + }, { Component: TranscribingLabel, id: 'transcribing' @@ -115,7 +120,9 @@ class ConferenceInfo extends Component { } return ( - + { COMPONENTS .filter(comp => autoHide.includes(comp.id)) @@ -142,7 +149,9 @@ class ConferenceInfo extends Component { } return ( - + { COMPONENTS .filter(comp => alwaysVisible.includes(comp.id)) diff --git a/react/features/conference/components/web/ConferenceInfoContainer.js b/react/features/conference/components/web/ConferenceInfoContainer.js index d3ec4ebcb..2bfbbad4e 100644 --- a/react/features/conference/components/web/ConferenceInfoContainer.js +++ b/react/features/conference/components/web/ConferenceInfoContainer.js @@ -2,21 +2,30 @@ import React from 'react'; +import { isAlwaysOnTitleBarEmpty } from '../functions.web'; + type Props = { /** * The children components. */ - children: React$Node, + children: React$Node, - /** - * Whether this conference info container should be visible or not. - */ - visible: boolean + /** + * Id of the component. + */ + id?: string, + + /** + * Whether this conference info container should be visible or not. + */ + visible: boolean } -export default ({ visible, children }: Props) => ( -
+export default ({ visible, children, id }: Props) => ( +
{children}
diff --git a/react/features/conference/components/web/RaisedHandsCountLabel.js b/react/features/conference/components/web/RaisedHandsCountLabel.js new file mode 100644 index 000000000..19753ae56 --- /dev/null +++ b/react/features/conference/components/web/RaisedHandsCountLabel.js @@ -0,0 +1,45 @@ +import { makeStyles } from '@material-ui/styles'; +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; + +import { IconRaisedHand } from '../../../base/icons'; +import { Label } from '../../../base/label'; +import { Tooltip } from '../../../base/tooltip'; +import BaseTheme from '../../../base/ui/components/BaseTheme'; +import { open as openParticipantsPane } from '../../../participants-pane/actions'; + +const useStyles = makeStyles(theme => { + return { + label: { + backgroundColor: theme.palette.warning02, + color: theme.palette.uiBackground, + marginRight: theme.spacing(1) + } + }; +}); + +const RaisedHandsCountLabel = () => { + const styles = useStyles(); + const dispatch = useDispatch(); + const raisedHandsCount = useSelector(state => + (state['features/base/participants'].raisedHandsQueue || []).length); + const { t } = useTranslation(); + const onClick = useCallback(() => { + dispatch(openParticipantsPane()); + }, []); + + return raisedHandsCount > 0 && ( + ); +}; + +export default RaisedHandsCountLabel;