diff --git a/lang/main.json b/lang/main.json index a7aa6e89f..020b25a5a 100644 --- a/lang/main.json +++ b/lang/main.json @@ -462,6 +462,9 @@ "busyTitle": "All recorders are currently busy", "buttonTooltip": "Start / Stop recording", "error": "Recording failed. Please try again.", + "expandedOff": "Recording has stopped", + "expandedOn": "The meeting is currently being recorded.", + "expandedPending": "Recording is being started...", "failedToStart": "Recording failed to start", "live": "LIVE", "off": "Recording stopped", @@ -483,6 +486,7 @@ "pending" : "Preparing to transcribe the meeting...", "off" : "Transcribing stopped", "error": "Transcribing failed. Please try again.", + "expandedLabel": "Transcribing is currently on", "failedToStart": "Transcribing failed to start", "tr": "TR", "labelToolTip": "The meeting is being transcribed", @@ -502,6 +506,9 @@ "error": "Live Streaming failed. Please try again.", "errorAPI": "An error occurred while accessing your YouTube broadcasts. Please try logging in again.", "errorLiveStreamNotEnabled": "Live Streaming is not enabled on __email__. Please enable live streaming or log into an account with live streaming enabled.", + "expandedOff": "The live streaming has stopped", + "expandedOn": "The meeting is currently being streamed to YouTube.", + "expandedPending": "The live streaming is being started...", "failedToStart": "Live Streaming failed to start", "off": "Live Streaming stopped", "on": "Live Streaming", @@ -546,6 +553,7 @@ }, "videoStatus": { "audioOnly": "AUD", + "audioOnlyExpanded": "You are in audio only mode. This mode saves bandwidth but you won't see videos of others.", "callQuality": "Call Quality", "hd": "HD", "hdTooltip": "Viewing high definition video", diff --git a/package-lock.json b/package-lock.json index e0e02cb47..85c441dac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -441,7 +441,7 @@ }, "@atlaskit/inline-dialog": { "version": "5.3.0", - "resolved": "http://registry.npmjs.org/@atlaskit/inline-dialog/-/inline-dialog-5.3.0.tgz", + "resolved": "https://registry.npmjs.org/@atlaskit/inline-dialog/-/inline-dialog-5.3.0.tgz", "integrity": "sha512-4bEeC5rZwtb4YO9BxW1UCJYCp/dyCVXqcygRW1BDnYVbveAI8wdym6qEi4BRvIwXCT4qgNhsVsqcxSrn0X6CKQ==", "requires": { "@atlaskit/layer": "^2.8.0", @@ -451,7 +451,7 @@ "dependencies": { "@atlaskit/layer": { "version": "2.9.1", - "resolved": "http://registry.npmjs.org/@atlaskit/layer/-/layer-2.9.1.tgz", + "resolved": "https://registry.npmjs.org/@atlaskit/layer/-/layer-2.9.1.tgz", "integrity": "sha512-nyIVGeS2OhuGR5gIMTYUfRmCG8z/9KMgUzTpbpsB70sH6+d4KSFhfkz+KhKNIa8gvKI6zBc+3UBYSlUW1t1qmQ==", "requires": { "styled-components": "1.4.6 - 3" diff --git a/react/features/base/label/components/AbstractCircularLabel.js b/react/features/base/label/components/AbstractCircularLabel.js index 3a69bfcf3..17e4e3a18 100644 --- a/react/features/base/label/components/AbstractCircularLabel.js +++ b/react/features/base/label/components/AbstractCircularLabel.js @@ -12,6 +12,7 @@ export type Props = { /** * Abstract class for the {@code CircularLabel} component. */ -export default class AbstractCircularLabel extends Component

{ +export default class AbstractCircularLabel + extends Component { } diff --git a/react/features/base/label/components/CircularLabel.native.js b/react/features/base/label/components/CircularLabel.native.js index 224ece6f8..864ac7694 100644 --- a/react/features/base/label/components/CircularLabel.native.js +++ b/react/features/base/label/components/CircularLabel.native.js @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import { Text, View } from 'react-native'; +import { Animated, Text } from 'react-native'; import { combineStyles, type StyleType } from '../../styles'; @@ -9,36 +9,142 @@ import AbstractCircularLabel, { } from './AbstractCircularLabel'; import styles from './styles'; +/** + * Const for status string 'in progress'. + */ +const STATUS_IN_PROGRESS = 'in_progress'; + +/** + * Const for status string 'off'. + */ +const STATUS_OFF = 'off'; + type Props = AbstractProps & { + /** + * Status of the label. This prop adds some additional styles based on its + * value. E.g. if status = off, it will render the label symbolising that + * the thing it displays (e.g. recording) is off. + */ + status: ('in_progress' | 'off' | 'on'), + /** * Style of the label. */ style?: ?StyleType }; +type State = { + + /** + * An animation object handling the opacity changes of the in progress + * label. + */ + pulseAnimation: Object +} + /** * Renders a circular indicator to be used for status icons, such as recording * on, audio-only conference, video quality and similar. */ -export default class CircularLabel extends AbstractCircularLabel { +export default class CircularLabel extends AbstractCircularLabel { + /** + * A reference to the started animation of this label. + */ + animationReference: Object; + + /** + * Instantiates a new instance of {@code CircularLabel}. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this.state = { + pulseAnimation: new Animated.Value(0) + }; + + this._maybeToggleAnimation({}, props); + } + + /** + * Implements {@code Component#componentWillReceiveProps}. + * + * @inheritdoc + */ + componentWillReceiveProps(newProps: Props) { + this._maybeToggleAnimation(this.props, newProps); + } + /** * Implements React {@link Component}'s render. * * @inheritdoc */ render() { - const { label, style } = this.props; + const { status, label, style } = this.props; + + let extraStyle = null; + + switch (status) { + case STATUS_IN_PROGRESS: + extraStyle = { + opacity: this.state.pulseAnimation + }; + break; + case STATUS_OFF: + extraStyle = styles.labelOff; + break; + } return ( - + { label } - + ); } + + /** + * Checks if the animation has to be started or stopped and acts + * accordingly. + * + * @param {Props} oldProps - The previous values of the Props. + * @param {Props} newProps - The new values of the Props. + * @returns {void} + */ + _maybeToggleAnimation(oldProps, newProps) { + const { status: oldStatus } = oldProps; + const { status: newStatus } = newProps; + const { pulseAnimation } = this.state; + + if (newStatus === STATUS_IN_PROGRESS + && oldStatus !== STATUS_IN_PROGRESS) { + // Animation must be started + this.animationReference = Animated.loop(Animated.sequence([ + Animated.timing(pulseAnimation, { + delay: 500, + toValue: 1, + useNativeDriver: true + }), + Animated.timing(pulseAnimation, { + toValue: 0.3, + useNativeDriver: true + }) + ])); + + this.animationReference.start(); + } else if (this.animationReference + && newStatus !== STATUS_IN_PROGRESS + && oldStatus === STATUS_IN_PROGRESS) { + // Animation must be stopped + this.animationReference.stop(); + } + } } diff --git a/react/features/base/label/components/CircularLabel.web.js b/react/features/base/label/components/CircularLabel.web.js index cc5cba3b3..0c8cb2639 100644 --- a/react/features/base/label/components/CircularLabel.web.js +++ b/react/features/base/label/components/CircularLabel.web.js @@ -25,7 +25,7 @@ type Props = AbstractProps & { * * @extends Component */ -export default class CircularLabel extends AbstractCircularLabel { +export default class CircularLabel extends AbstractCircularLabel { /** * Default values for {@code CircularLabel} component's properties. * diff --git a/react/features/base/label/components/ExpandedLabel.native.js b/react/features/base/label/components/ExpandedLabel.native.js new file mode 100644 index 000000000..46b2cb3c0 --- /dev/null +++ b/react/features/base/label/components/ExpandedLabel.native.js @@ -0,0 +1,135 @@ +// @flow + +import React, { Component } from 'react'; +import { Animated, Text, View } from 'react-native'; + +import styles, { DEFAULT_COLOR, LABEL_MARGIN, LABEL_SIZE } from './styles'; + +export type Props = { + + /** + * The position of the parent element (from right to left) to display the + * arrow. + */ + parentPosition: number +}; + +type State = { + + /** + * The opacity animation Object. + */ + opacityAnimation: Object, + + /** + * A boolean to descide to show or not show the arrow. This is required as + * we can't easily animate this transformed Component so we render it once + * the animation is done. + */ + showArrow: boolean +}; + +/** + * Offset to the arrow to be rendered in the right position. + */ +const ARROW_OFFSET = 0; + +/** + * A react {@code Component} that implements an expanded label as tooltip-like + * component to explain the meaning of the {@code Label}. + */ +export default class ExpandedLabel extends Component { + /** + * Instantiates a new {@code ExpandedLabel} instance. + * + * @inheritdoc + */ + constructor(props: P) { + super(props); + + this.state = { + opacityAnimation: new Animated.Value(0), + showArrow: false + }; + } + + /** + * Implements React {@code Component}'s componentDidMount. + * + * @inheritdoc + */ + componentDidMount() { + Animated.decay(this.state.opacityAnimation, { + toValue: 1, + velocity: 1, + useNativeDriver: true + }).start(({ finished }) => { + finished && this.setState({ + showArrow: true + }); + }); + } + + /** + * Implements React {@code Component}'s render. + * + * @inheritdoc + */ + render() { + const arrowPosition + = this.props.parentPosition - LABEL_MARGIN - (LABEL_SIZE / 2); + + return ( + + + + + { this._getLabel() } + + + + ); + } + + /** + * Returns the label that needs to be rendered in the box. To be implemented + * by its overriding classes. + * + * @returns {string} + */ + _getLabel: () => string + + _getColor: () => string + + /** + * Defines the color of the expanded label. This function returns a default + * value if implementing classes don't override it, but the goal is to have + * expanded labels matching to circular labels in color. + * If implementing classes return a falsy value, it also uses the default + * color. + * + * @returns {string} + */ + _getColor() { + return DEFAULT_COLOR; + } +} diff --git a/react/features/base/label/components/ExpandedLabel.web.js b/react/features/base/label/components/ExpandedLabel.web.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/base/label/components/index.js b/react/features/base/label/components/index.js index 37173931f..d80c17eb2 100644 --- a/react/features/base/label/components/index.js +++ b/react/features/base/label/components/index.js @@ -1 +1,2 @@ export { default as CircularLabel } from './CircularLabel'; +export { default as ExpandedLabel } from './ExpandedLabel'; diff --git a/react/features/base/label/components/styles.js b/react/features/base/label/components/styles.js index f51082e0f..74e47766f 100644 --- a/react/features/base/label/components/styles.js +++ b/react/features/base/label/components/styles.js @@ -1,30 +1,75 @@ // @flow -import { ColorPalette, createStyleSheet } from '../../styles'; +import { BoxModel, ColorPalette, createStyleSheet } from '../../styles'; + +/** + * The default color of the {@code Label} and {@code ExpandedLabel}. + */ +export const DEFAULT_COLOR = '#808080'; + +/** + * Margin of the {@Label} - to be reused when rendering the + * {@code ExpandedLabel}. + */ +export const LABEL_MARGIN = 5; + +/** + * Size of the {@Label} - to be reused when rendering the + * {@code ExpandedLabel}. + */ +export const LABEL_SIZE = 36; /** * The styles of the native base/label feature. */ export default createStyleSheet({ + expandedLabelArrow: { + backgroundColor: ColorPalette.blue, + height: 15, + transform: [ { rotate: '45deg' }, { translateX: 10 } ], + width: 15 + }, + + expandedLabelContainer: { + backgroundColor: ColorPalette.blue, + borderColor: ColorPalette.blue, + borderRadius: 6, + marginHorizontal: BoxModel.margin, + padding: BoxModel.padding + }, + + expandedLabelText: { + color: ColorPalette.white + }, + + expandedLabelWrapper: { + alignItems: 'flex-end', + flexDirection: 'column' + }, + /** * The outermost view. */ indicatorContainer: { alignItems: 'center', - backgroundColor: '#808080', - borderRadius: 18, + backgroundColor: DEFAULT_COLOR, + borderRadius: LABEL_SIZE / 2, borderWidth: 0, flex: 0, - height: 36, + height: LABEL_SIZE, justifyContent: 'center', - margin: 5, + margin: LABEL_MARGIN, opacity: 0.6, - width: 36 + width: LABEL_SIZE }, indicatorText: { color: ColorPalette.white, fontSize: 12 + }, + + labelOff: { + opacity: 0.3 } }); diff --git a/react/features/large-video/components/Labels.native.js b/react/features/large-video/components/Labels.native.js index 330517756..d0a2bdd58 100644 --- a/react/features/large-video/components/Labels.native.js +++ b/react/features/large-video/components/Labels.native.js @@ -1,14 +1,19 @@ // @flow import React from 'react'; -import { View } from 'react-native'; +import { TouchableOpacity, View } from 'react-native'; import { connect } from 'react-redux'; import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet'; +import { + RecordingExpandedLabel +} from '../../recording'; import { isNarrowAspectRatio, makeAspectRatioAware } from '../../base/responsive-ui'; +import { TranscribingExpandedLabel } from '../../transcribing'; +import { VideoQualityExpandedLabel } from '../../video-quality'; import AbstractLabels, { _abstractMapStateToProps, @@ -21,6 +26,11 @@ import styles from './styles'; */ type Props = AbstractLabelsProps & { + /** + * Function to translate i18n labels. + */ + t: Function, + /** * The indicator which determines whether the UI is reduced (to accommodate * smaller display areas). @@ -30,10 +40,108 @@ type Props = AbstractLabelsProps & { _reducedUI: boolean }; +type State = { + + /** + * Layout object of the outermost container. For stucture please see: + * https://facebook.github.io/react-native/docs/view#onlayout + */ + containerLayout: ?Object, + + /** + * Layout objects of the individual labels. This data type contains the same + * structure as the layout is defined here: + * https://facebook.github.io/react-native/docs/view#onlayout + * but keyed with the ID of the label its layout it contains. E.g. + * + * { + * transcribing: { + * { layout: { x, y, width, height } } + * }, + * ... + * } + */ + labelLayouts: Object, + + /** + * Position of the label to render the {@code ExpandedLabel} to. + */ + parentPosition: ?number, + + /** + * String to show which {@code ExpandedLabel} to be shown. (Equals to the + * label IDs below.) + */ + visibleExpandedLabel: ?string +} + +const LABEL_ID_QUALITY = 'quality'; +const LABEL_ID_RECORDING = 'recording'; +const LABEL_ID_STREAMING = 'streaming'; +const LABEL_ID_TRANSCRIBING = 'transcribing'; + +/** + * The {@code ExpandedLabel} components to be rendered for the individual + * {@code Label}s. + */ +const EXPANDED_LABELS = { + quality: VideoQualityExpandedLabel, + recording: { + component: RecordingExpandedLabel, + props: { + mode: JitsiRecordingConstants.mode.FILE + } + }, + streaming: { + component: RecordingExpandedLabel, + props: { + mode: JitsiRecordingConstants.mode.STREAM + } + }, + transcribing: TranscribingExpandedLabel +}; + +/** + * Timeout to hide the {@ExpandedLabel}. + */ +const EXPANDED_LABEL_TIMEOUT = 5000; + /** * A container that renders the conference indicators, if any. */ -class Labels extends AbstractLabels { +class Labels extends AbstractLabels { + /** + * Timeout for the expanded labels to disappear. + */ + expandedLabelTimeout: TimeoutID; + + /** + * Instantiates a new instance of {@code Labels}. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this.state = { + containerLayout: undefined, + labelLayouts: {}, + parentPosition: undefined, + visibleExpandedLabel: undefined + }; + + this._onTopViewLayout = this._onTopViewLayout.bind(this); + } + + /** + * Implements React {@code Component}'s componentWillUnmount. + * + * @inheritdoc + */ + componentWillUnmount() { + clearTimeout(this.expandedLabelTimeout); + } + /** * Implements React {@code Component}'s render. * @@ -46,34 +154,181 @@ class Labels extends AbstractLabels { return ( - { - this._renderRecordingLabel( - JitsiRecordingConstants.mode.FILE) - } - { - this._renderRecordingLabel( - JitsiRecordingConstants.mode.STREAM) - } - { - this._renderTranscribingLabel() - } - {/* - * Emil, Lyubomir, Nichole, and Zoli said that the Labels - * should not be rendered in Picture-in-Picture. Saul argued - * that the recording Labels should be rendered. As a temporary - * compromise, don't render the VideoQualityLabel at least - * because it's not that important. - */ - _reducedUI || this._renderVideoQualityLabel() - } + style = { styles.labelWrapper }> + + + { + this._renderRecordingLabel( + JitsiRecordingConstants.mode.FILE) + } + + + { + this._renderRecordingLabel( + JitsiRecordingConstants.mode.STREAM) + } + + + { + this._renderTranscribingLabel() + } + + {/* + * Emil, Lyubomir, Nichole, and Zoli said that the Labels + * should not be rendered in Picture-in-Picture. Saul + * argued that the recording Labels should be rendered. As + * a temporary compromise, don't render the + * VideoQualityLabel at least because it's not that + * important. + */ + _reducedUI || ( + + { this._renderVideoQualityLabel() } + + ) + } + + + { + this._renderExpandedLabel() + } + ); } + /** + * Creates a function to be invoked when the onLayout of the touchables are + * triggered. + * + * @param {string} label - The identifier of the label that's onLayout is + * triggered. + * @returns {Function} + */ + _createOnLayout(label) { + return ({ nativeEvent: { layout } }) => { + const { labelLayouts } = this.state; + const updatedLayout = {}; + + updatedLayout[label] = layout; + + this.setState({ + labelLayouts: { + ...labelLayouts, + ...updatedLayout + } + }); + }; + } + + /** + * Creates a function to be invoked when the onPress of the touchables are + * triggered. + * + * @param {string} label - The identifier of the label that's onLayout is + * triggered. + * @returns {Function} + */ + _createOnPress(label) { + return () => { + const { + containerLayout, + labelLayouts + } = this.state; + let { visibleExpandedLabel } = this.state; + + if (containerLayout) { + const labelLayout = labelLayouts[label]; + + // This calculation has to be changed if the labels are not + // positioned right anymore. + const right = containerLayout.width - labelLayout.x; + + visibleExpandedLabel + = visibleExpandedLabel === label ? undefined : label; + + clearTimeout(this.expandedLabelTimeout); + this.setState({ + parentPosition: right, + visibleExpandedLabel + }); + + if (visibleExpandedLabel) { + this.expandedLabelTimeout = setTimeout(() => { + this.setState({ + visibleExpandedLabel: undefined + }); + }, EXPANDED_LABEL_TIMEOUT); + } + } + }; + } + + _onTopViewLayout: Object => void + + /** + * Invoked when the View containing the {@code Label}s is laid out. + * + * @param {Object} layout - The native layout object. + * @returns {void} + */ + _onTopViewLayout({ nativeEvent: { layout } }) { + this.setState({ + containerLayout: layout + }); + } + + /** + * Rendes the expanded (explaining) label for the label that was touched. + * + * @returns {React$Element} + */ + _renderExpandedLabel() { + const { parentPosition, visibleExpandedLabel } = this.state; + + if (visibleExpandedLabel) { + const expandedLabel = EXPANDED_LABELS[visibleExpandedLabel]; + + if (expandedLabel) { + const component = expandedLabel.component || expandedLabel; + const expandedLabelProps = expandedLabel.props || {}; + + return React.createElement(component, { + ...expandedLabelProps, + parentPosition + }); + } + } + + return null; + } + _renderRecordingLabel: string => React$Element<*>; _renderTranscribingLabel: () => React$Element<*> diff --git a/react/features/large-video/components/styles.js b/react/features/large-video/components/styles.js index b4f61d44d..7f26a45b8 100644 --- a/react/features/large-video/components/styles.js +++ b/react/features/large-video/components/styles.js @@ -9,13 +9,26 @@ import { FILMSTRIP_SIZE } from '../../filmstrip'; export const AVATAR_SIZE = 200; export default createStyleSheet({ + /** * View that contains the indicators. */ indicatorContainer: { flex: 1, flexDirection: 'row', - margin: BoxModel.margin, + justifyContent: 'flex-end', + margin: BoxModel.margin + }, + + /** + * Indicator container for wide aspect ratio. + */ + indicatorContainerWide: { + marginRight: FILMSTRIP_SIZE + BoxModel.margin + }, + + labelWrapper: { + flexDirection: 'column', position: 'absolute', right: 0, @@ -25,13 +38,6 @@ export default createStyleSheet({ top: BoxModel.margin * 3 }, - /** - * Indicator container for wide aspect ratio. - */ - indicatorContainerWide: { - right: FILMSTRIP_SIZE - }, - /** * Large video container style. */ diff --git a/react/features/recording/components/AbstractRecordingLabel.js b/react/features/recording/components/AbstractRecordingLabel.js index ed0c775d0..412a93197 100644 --- a/react/features/recording/components/AbstractRecordingLabel.js +++ b/react/features/recording/components/AbstractRecordingLabel.js @@ -4,6 +4,8 @@ import { Component } from 'react'; import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet'; +import { getSessionStatusToShow } from '../functions'; + /** * NOTE: Web currently renders multiple indicators if multiple recording * sessions are running. This is however may not be a good UX as it's not @@ -12,13 +14,12 @@ import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet'; * running. These boolean are shared across the two components to make it * easier to align web's behaviour to mobile's later if necessary. */ -export type Props = { +type Props = { /** - * True if there is an active recording with the provided mode therefore the - * component must be rendered. + * The status of the highermost priority session. */ - _visible: boolean, + _status: ?string, /** * The recording mode this indicator should display. @@ -34,8 +35,8 @@ export type Props = { /** * Abstract class for the {@code RecordingLabel} component. */ -export default class AbstractRecordingLabel - extends Component

{ +export default class AbstractRecordingLabel + extends Component { /** * Implements React {@code Component}'s render. @@ -43,7 +44,7 @@ export default class AbstractRecordingLabel * @inheritdoc */ render() { - return this.props._visible ? this._renderLabel() : null; + return this.props._status ? this._renderLabel() : null; } _getLabelKey: () => ?string @@ -84,20 +85,13 @@ export default class AbstractRecordingLabel * @param {Props} ownProps - The component's own props. * @private * @returns {{ - * _visible: boolean + * _status: ?string * }} */ export function _mapStateToProps(state: Object, ownProps: Props) { const { mode } = ownProps; - const _recordingSessions = state['features/recording'].sessionDatas; - const _visible - = Array.isArray(_recordingSessions) - && _recordingSessions.some( - session => session.status === JitsiRecordingConstants.status.ON - && session.mode === mode - ); return { - _visible + _status: getSessionStatusToShow(state, mode) }; } diff --git a/react/features/recording/components/RecordingExpandedLabel.native.js b/react/features/recording/components/RecordingExpandedLabel.native.js new file mode 100644 index 000000000..298b44c4f --- /dev/null +++ b/react/features/recording/components/RecordingExpandedLabel.native.js @@ -0,0 +1,108 @@ +// @flow + +import { connect } from 'react-redux'; + +import { translate } from '../../base/i18n'; +import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet'; +import { + ExpandedLabel, + type Props as AbstractProps +} from '../../base/label'; + +import { getSessionStatusToShow } from '../functions'; + +import { LIVE_LABEL_COLOR, REC_LABEL_COLOR } from './styles'; + +type Props = AbstractProps & { + + /** + * The status of the highermost priority session. + */ + _status: ?string, + + /** + * The recording mode this indicator should display. + */ + mode: string, + + /** + * Function to be used to translate i18n labels. + */ + t: Function +} + +/** + * A react {@code Component} that implements an expanded label as tooltip-like + * component to explain the meaning of the {@code RecordingLabel}. + */ +class RecordingExpandedLabel extends ExpandedLabel { + /** + * Returns the color this expanded label should be rendered with. + * + * @returns {string} + */ + _getColor() { + switch (this.props.mode) { + case JitsiRecordingConstants.mode.STREAM: + return LIVE_LABEL_COLOR; + case JitsiRecordingConstants.mode.FILE: + return REC_LABEL_COLOR; + default: + return null; + } + } + + /** + * Returns the label specific text of this {@code ExpandedLabel}. + * + * @returns {string} + */ + _getLabel() { + const { _status, mode, t } = this.props; + let postfix = 'recording', prefix = 'expandedOn'; // Default values. + + switch (mode) { + case JitsiRecordingConstants.mode.STREAM: + prefix = 'liveStreaming'; + break; + case JitsiRecordingConstants.mode.FILE: + prefix = 'recording'; + break; + } + + switch (_status) { + case JitsiRecordingConstants.status.OFF: + postfix = 'expandedOff'; + break; + case JitsiRecordingConstants.status.PENDING: + postfix = 'expandedPending'; + break; + case JitsiRecordingConstants.status.ON: + postfix = 'expandedOn'; + break; + } + + return t(`${prefix}.${postfix}`); + } +} + +/** + * Maps (parts of) the Redux state to the associated + * {@code RecordingExpandedLabel}'s props. + * + * @param {Object} state - The Redux state. + * @param {Props} ownProps - The component's own props. + * @private + * @returns {{ + * _status: ?string + * }} + */ +function _mapStateToProps(state: Object, ownProps: Props) { + const { mode } = ownProps; + + return { + _status: getSessionStatusToShow(state, mode) + }; +} + +export default translate(connect(_mapStateToProps)(RecordingExpandedLabel)); diff --git a/react/features/recording/components/RecordingExpandedLabel.web.js b/react/features/recording/components/RecordingExpandedLabel.web.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/recording/components/RecordingLabel.native.js b/react/features/recording/components/RecordingLabel.native.js index 4260cd784..fdbb7d418 100644 --- a/react/features/recording/components/RecordingLabel.native.js +++ b/react/features/recording/components/RecordingLabel.native.js @@ -8,7 +8,6 @@ import { CircularLabel } from '../../base/label'; import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet'; import AbstractRecordingLabel, { - type Props, _mapStateToProps } from './AbstractRecordingLabel'; import styles from './styles'; @@ -19,7 +18,7 @@ import styles from './styles'; * * @extends {Component} */ -class RecordingLabel extends AbstractRecordingLabel { +class RecordingLabel extends AbstractRecordingLabel { /** * Renders the platform specific label component. @@ -41,9 +40,21 @@ class RecordingLabel extends AbstractRecordingLabel { return null; } + let status = 'on'; + + switch (this.props._status) { + case JitsiRecordingConstants.status.PENDING: + status = 'in_progress'; + break; + case JitsiRecordingConstants.status.OFF: + status = 'off'; + break; + } + return ( ); } diff --git a/react/features/recording/components/RecordingLabel.web.js b/react/features/recording/components/RecordingLabel.web.js index 7fd98e5b8..998b168f0 100644 --- a/react/features/recording/components/RecordingLabel.web.js +++ b/react/features/recording/components/RecordingLabel.web.js @@ -7,7 +7,6 @@ import { CircularLabel } from '../../base/label'; import { translate } from '../../base/i18n'; import AbstractRecordingLabel, { - type Props, _mapStateToProps } from './AbstractRecordingLabel'; @@ -17,7 +16,7 @@ import AbstractRecordingLabel, { * * @extends {Component} */ -class RecordingLabel extends AbstractRecordingLabel { +class RecordingLabel extends AbstractRecordingLabel { /** * Renders the platform specific label component. * diff --git a/react/features/recording/components/index.js b/react/features/recording/components/index.js index b00cf19ba..3fda26219 100644 --- a/react/features/recording/components/index.js +++ b/react/features/recording/components/index.js @@ -9,3 +9,4 @@ export { StopRecordingDialog } from './Recording'; export { default as RecordingLabel } from './RecordingLabel'; +export { default as RecordingExpandedLabel } from './RecordingExpandedLabel'; diff --git a/react/features/recording/components/styles.js b/react/features/recording/components/styles.js index 0d38df68e..e82e9c52c 100644 --- a/react/features/recording/components/styles.js +++ b/react/features/recording/components/styles.js @@ -2,6 +2,9 @@ import { ColorPalette, createStyleSheet } from '../../base/styles'; +export const LIVE_LABEL_COLOR = ColorPalette.blue; +export const REC_LABEL_COLOR = ColorPalette.red; + /** * The styles of the React {@code Components} of the feature recording. */ @@ -11,13 +14,13 @@ export default createStyleSheet({ * Style for the recording indicator. */ indicatorLive: { - backgroundColor: ColorPalette.blue + backgroundColor: LIVE_LABEL_COLOR }, /** * Style for the recording indicator. */ indicatorRecording: { - backgroundColor: ColorPalette.red + backgroundColor: REC_LABEL_COLOR } }); diff --git a/react/features/recording/constants.js b/react/features/recording/constants.js index a8e98b978..df7277002 100644 --- a/react/features/recording/constants.js +++ b/react/features/recording/constants.js @@ -1,5 +1,7 @@ // @flow +import { JitsiRecordingConstants } from '../base/lib-jitsi-meet'; + /** * The identifier of the sound to be played when a recording or live streaming * session is stopped. @@ -26,3 +28,15 @@ export const RECORDING_TYPES = { JIBRI: 'jibri', JIRECON: 'jirecon' }; + +/** + * An array defining the priorities of the recording (or live streaming) + * statuses, where the index of the array is the priority itself. + * + * @type {Array} + */ +export const RECORDING_STATUS_PRIORITIES = [ + JitsiRecordingConstants.status.OFF, + JitsiRecordingConstants.status.PENDING, + JitsiRecordingConstants.status.ON +]; diff --git a/react/features/recording/functions.js b/react/features/recording/functions.js index 8f543a063..0d0453fbb 100644 --- a/react/features/recording/functions.js +++ b/react/features/recording/functions.js @@ -2,6 +2,8 @@ import { JitsiRecordingConstants } from '../base/lib-jitsi-meet'; +import { RECORDING_STATUS_PRIORITIES } from './constants'; + /** * Searches in the passed in redux state for an active recording session of the * passed in mode. @@ -43,3 +45,30 @@ export function getSessionById(state: Object, id: string) { return state['features/recording'].sessionDatas.find( sessionData => sessionData.id === id); } + +/** + * Returns the recording session status that is to be shown in a label. E.g. if + * there is a session with the status OFF and one with PENDING, then the PENDING + * one will be shown, because that is likely more important for the user to see. + * + * @param {Object} state - The redux state to search in. + * @param {string} mode - The recording mode to get status for. + * @returns {string|undefined} + */ +export function getSessionStatusToShow(state: Object, mode: string): ?string { + const recordingSessions = state['features/recording'].sessionDatas; + let status; + + if (Array.isArray(recordingSessions)) { + for (const session of recordingSessions) { + if (session.mode === mode + && (!status + || (RECORDING_STATUS_PRIORITIES.indexOf(session.status) + > RECORDING_STATUS_PRIORITIES.indexOf(status)))) { + status = session.status; + } + } + } + + return status; +} diff --git a/react/features/transcribing/components/TranscribingExpandedLabel.native.js b/react/features/transcribing/components/TranscribingExpandedLabel.native.js new file mode 100644 index 000000000..0dbb0609f --- /dev/null +++ b/react/features/transcribing/components/TranscribingExpandedLabel.native.js @@ -0,0 +1,25 @@ +// @flow + +import { translate } from '../../base/i18n'; +import { ExpandedLabel, type Props as AbstractProps } from '../../base/label'; + +type Props = AbstractProps & { + t: Function +} + +/** + * A react {@code Component} that implements an expanded label as tooltip-like + * component to explain the meaning of the {@code TranscribingLabel}. + */ +class TranscribingExpandedLabel extends ExpandedLabel { + /** + * Returns the label specific text of this {@code ExpandedLabel}. + * + * @returns {string} + */ + _getLabel() { + return this.props.t('transcribing.expandedLabel'); + } +} + +export default translate(TranscribingExpandedLabel); diff --git a/react/features/transcribing/components/TranscribingExpandedLabel.web.js b/react/features/transcribing/components/TranscribingExpandedLabel.web.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/transcribing/components/index.js b/react/features/transcribing/components/index.js index 171f9b002..48286f0fa 100644 --- a/react/features/transcribing/components/index.js +++ b/react/features/transcribing/components/index.js @@ -1 +1,4 @@ export { default as TranscribingLabel } from './TranscribingLabel'; +export { + default as TranscribingExpandedLabel +} from './TranscribingExpandedLabel'; diff --git a/react/features/video-quality/components/VideoQualityExpandedLabel.native.js b/react/features/video-quality/components/VideoQualityExpandedLabel.native.js new file mode 100644 index 000000000..e3a070e7c --- /dev/null +++ b/react/features/video-quality/components/VideoQualityExpandedLabel.native.js @@ -0,0 +1,36 @@ +// @flow + +import { translate } from '../../base/i18n'; +import { ExpandedLabel, type Props as AbstractProps } from '../../base/label'; + +import { AUD_LABEL_COLOR } from './styles'; + +type Props = AbstractProps & { + t: Function +} + +/** + * A react {@code Component} that implements an expanded label as tooltip-like + * component to explain the meaning of the {@code VideoQualityLabel}. + */ +class VideoQualityExpandedLabel extends ExpandedLabel { + /** + * Returns the color this expanded label should be rendered with. + * + * @returns {string} + */ + _getColor() { + return AUD_LABEL_COLOR; + } + + /** + * Returns the label specific text of this {@code ExpandedLabel}. + * + * @returns {string} + */ + _getLabel() { + return this.props.t('videoStatus.audioOnlyExpanded'); + } +} + +export default translate(VideoQualityExpandedLabel); diff --git a/react/features/video-quality/components/VideoQualityExpandedLabel.web.js b/react/features/video-quality/components/VideoQualityExpandedLabel.web.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/video-quality/components/index.js b/react/features/video-quality/components/index.js index 39a2a72c5..a12857be5 100644 --- a/react/features/video-quality/components/index.js +++ b/react/features/video-quality/components/index.js @@ -3,3 +3,6 @@ export { } from './OverflowMenuVideoQualityItem'; export { default as VideoQualityDialog } from './VideoQualityDialog'; export { default as VideoQualityLabel } from './VideoQualityLabel'; +export { + default as VideoQualityExpandedLabel +} from './VideoQualityExpandedLabel'; diff --git a/react/features/video-quality/components/styles.js b/react/features/video-quality/components/styles.js index 61947752d..4c1fb453e 100644 --- a/react/features/video-quality/components/styles.js +++ b/react/features/video-quality/components/styles.js @@ -2,6 +2,8 @@ import { ColorPalette, createStyleSheet } from '../../base/styles'; +export const AUD_LABEL_COLOR = ColorPalette.green; + /** * The styles of the React {@code Components} of the feature video-quality. */ @@ -11,6 +13,6 @@ export default createStyleSheet({ * Style for the audio-only indicator. */ indicatorAudioOnly: { - backgroundColor: ColorPalette.green + backgroundColor: AUD_LABEL_COLOR } });