[RN] Add ExpandedLabel

This commit is contained in:
Bettenbuk Zoltan 2018-09-11 12:16:01 +02:00 committed by Saúl Ibarra Corretgé
parent d604cdfe27
commit e5cc732b72
27 changed files with 860 additions and 75 deletions

View File

@ -462,6 +462,9 @@
"busyTitle": "All recorders are currently busy", "busyTitle": "All recorders are currently busy",
"buttonTooltip": "Start / Stop recording", "buttonTooltip": "Start / Stop recording",
"error": "Recording failed. Please try again.", "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", "failedToStart": "Recording failed to start",
"live": "LIVE", "live": "LIVE",
"off": "Recording stopped", "off": "Recording stopped",
@ -483,6 +486,7 @@
"pending" : "Preparing to transcribe the meeting...", "pending" : "Preparing to transcribe the meeting...",
"off" : "Transcribing stopped", "off" : "Transcribing stopped",
"error": "Transcribing failed. Please try again.", "error": "Transcribing failed. Please try again.",
"expandedLabel": "Transcribing is currently on",
"failedToStart": "Transcribing failed to start", "failedToStart": "Transcribing failed to start",
"tr": "TR", "tr": "TR",
"labelToolTip": "The meeting is being transcribed", "labelToolTip": "The meeting is being transcribed",
@ -502,6 +506,9 @@
"error": "Live Streaming failed. Please try again.", "error": "Live Streaming failed. Please try again.",
"errorAPI": "An error occurred while accessing your YouTube broadcasts. Please try logging in 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.", "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", "failedToStart": "Live Streaming failed to start",
"off": "Live Streaming stopped", "off": "Live Streaming stopped",
"on": "Live Streaming", "on": "Live Streaming",
@ -546,6 +553,7 @@
}, },
"videoStatus": { "videoStatus": {
"audioOnly": "AUD", "audioOnly": "AUD",
"audioOnlyExpanded": "You are in audio only mode. This mode saves bandwidth but you won't see videos of others.",
"callQuality": "Call Quality", "callQuality": "Call Quality",
"hd": "HD", "hd": "HD",
"hdTooltip": "Viewing high definition video", "hdTooltip": "Viewing high definition video",

4
package-lock.json generated
View File

@ -441,7 +441,7 @@
}, },
"@atlaskit/inline-dialog": { "@atlaskit/inline-dialog": {
"version": "5.3.0", "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==", "integrity": "sha512-4bEeC5rZwtb4YO9BxW1UCJYCp/dyCVXqcygRW1BDnYVbveAI8wdym6qEi4BRvIwXCT4qgNhsVsqcxSrn0X6CKQ==",
"requires": { "requires": {
"@atlaskit/layer": "^2.8.0", "@atlaskit/layer": "^2.8.0",
@ -451,7 +451,7 @@
"dependencies": { "dependencies": {
"@atlaskit/layer": { "@atlaskit/layer": {
"version": "2.9.1", "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==", "integrity": "sha512-nyIVGeS2OhuGR5gIMTYUfRmCG8z/9KMgUzTpbpsB70sH6+d4KSFhfkz+KhKNIa8gvKI6zBc+3UBYSlUW1t1qmQ==",
"requires": { "requires": {
"styled-components": "1.4.6 - 3" "styled-components": "1.4.6 - 3"

View File

@ -12,6 +12,7 @@ export type Props = {
/** /**
* Abstract class for the {@code CircularLabel} component. * Abstract class for the {@code CircularLabel} component.
*/ */
export default class AbstractCircularLabel<P: Props> extends Component<P> { export default class AbstractCircularLabel<P: Props, S: *>
extends Component<P, S> {
} }

View File

@ -1,6 +1,6 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { Text, View } from 'react-native'; import { Animated, Text } from 'react-native';
import { combineStyles, type StyleType } from '../../styles'; import { combineStyles, type StyleType } from '../../styles';
@ -9,36 +9,142 @@ import AbstractCircularLabel, {
} from './AbstractCircularLabel'; } from './AbstractCircularLabel';
import styles from './styles'; 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 & { 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 of the label.
*/ */
style?: ?StyleType 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 * Renders a circular indicator to be used for status icons, such as recording
* on, audio-only conference, video quality and similar. * on, audio-only conference, video quality and similar.
*/ */
export default class CircularLabel extends AbstractCircularLabel<Props> { export default class CircularLabel extends AbstractCircularLabel<Props, State> {
/**
* 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. * Implements React {@link Component}'s render.
* *
* @inheritdoc * @inheritdoc
*/ */
render() { 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 ( return (
<View <Animated.View
style = { style = { [
combineStyles(styles.indicatorContainer, style) combineStyles(styles.indicatorContainer, style),
}> extraStyle
] }>
<Text style = { styles.indicatorText }> <Text style = { styles.indicatorText }>
{ label } { label }
</Text> </Text>
</View> </Animated.View>
); );
} }
/**
* 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();
}
}
} }

View File

@ -25,7 +25,7 @@ type Props = AbstractProps & {
* *
* @extends Component * @extends Component
*/ */
export default class CircularLabel extends AbstractCircularLabel<Props> { export default class CircularLabel extends AbstractCircularLabel<Props, {}> {
/** /**
* Default values for {@code CircularLabel} component's properties. * Default values for {@code CircularLabel} component's properties.
* *

View File

@ -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<P: Props> extends Component<P, State> {
/**
* 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 (
<Animated.View
style = { [
styles.expandedLabelWrapper,
{
opacity: this.state.opacityAnimation
}
] } >
<View
style = { [
styles.expandedLabelArrow,
{
backgroundColor: this._getColor() || DEFAULT_COLOR,
marginRight: arrowPosition + ARROW_OFFSET
}
] } />
<View
style = { [
styles.expandedLabelContainer,
{
backgroundColor: this._getColor() || DEFAULT_COLOR
}
] }>
<Text style = { styles.expandedLabelText }>
{ this._getLabel() }
</Text>
</View>
</Animated.View>
);
}
/**
* 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;
}
}

View File

@ -1 +1,2 @@
export { default as CircularLabel } from './CircularLabel'; export { default as CircularLabel } from './CircularLabel';
export { default as ExpandedLabel } from './ExpandedLabel';

View File

@ -1,30 +1,75 @@
// @flow // @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. * The styles of the native base/label feature.
*/ */
export default createStyleSheet({ 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. * The outermost view.
*/ */
indicatorContainer: { indicatorContainer: {
alignItems: 'center', alignItems: 'center',
backgroundColor: '#808080', backgroundColor: DEFAULT_COLOR,
borderRadius: 18, borderRadius: LABEL_SIZE / 2,
borderWidth: 0, borderWidth: 0,
flex: 0, flex: 0,
height: 36, height: LABEL_SIZE,
justifyContent: 'center', justifyContent: 'center',
margin: 5, margin: LABEL_MARGIN,
opacity: 0.6, opacity: 0.6,
width: 36 width: LABEL_SIZE
}, },
indicatorText: { indicatorText: {
color: ColorPalette.white, color: ColorPalette.white,
fontSize: 12 fontSize: 12
},
labelOff: {
opacity: 0.3
} }
}); });

View File

@ -1,14 +1,19 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { TouchableOpacity, View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet'; import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
import {
RecordingExpandedLabel
} from '../../recording';
import { import {
isNarrowAspectRatio, isNarrowAspectRatio,
makeAspectRatioAware makeAspectRatioAware
} from '../../base/responsive-ui'; } from '../../base/responsive-ui';
import { TranscribingExpandedLabel } from '../../transcribing';
import { VideoQualityExpandedLabel } from '../../video-quality';
import AbstractLabels, { import AbstractLabels, {
_abstractMapStateToProps, _abstractMapStateToProps,
@ -21,6 +26,11 @@ import styles from './styles';
*/ */
type Props = AbstractLabelsProps & { type Props = AbstractLabelsProps & {
/**
* Function to translate i18n labels.
*/
t: Function,
/** /**
* The indicator which determines whether the UI is reduced (to accommodate * The indicator which determines whether the UI is reduced (to accommodate
* smaller display areas). * smaller display areas).
@ -30,10 +40,108 @@ type Props = AbstractLabelsProps & {
_reducedUI: boolean _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. * A container that renders the conference indicators, if any.
*/ */
class Labels extends AbstractLabels<Props, *> { class Labels extends AbstractLabels<Props, State> {
/**
* 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. * Implements React {@code Component}'s render.
* *
@ -46,34 +154,181 @@ class Labels extends AbstractLabels<Props, *> {
return ( return (
<View <View
pointerEvents = 'box-none' pointerEvents = 'box-none'
style = { [ style = { styles.labelWrapper }>
styles.indicatorContainer, <View
wide && _filmstripVisible && styles.indicatorContainerWide onLayout = { this._onTopViewLayout }
] }> pointerEvents = 'box-none'
{ style = { [
this._renderRecordingLabel( styles.indicatorContainer,
JitsiRecordingConstants.mode.FILE) wide && _filmstripVisible
} && styles.indicatorContainerWide
{ ] }>
this._renderRecordingLabel( <TouchableOpacity
JitsiRecordingConstants.mode.STREAM) onLayout = { this._createOnLayout(LABEL_ID_RECORDING) }
} onPress = { this._createOnPress(LABEL_ID_RECORDING) } >
{ {
this._renderTranscribingLabel() this._renderRecordingLabel(
} JitsiRecordingConstants.mode.FILE)
{/* }
* Emil, Lyubomir, Nichole, and Zoli said that the Labels </TouchableOpacity>
* should not be rendered in Picture-in-Picture. Saul argued <TouchableOpacity
* that the recording Labels should be rendered. As a temporary onLayout = { this._createOnLayout(LABEL_ID_STREAMING) }
* compromise, don't render the VideoQualityLabel at least onPress = { this._createOnPress(LABEL_ID_STREAMING) } >
* because it's not that important. {
*/ this._renderRecordingLabel(
_reducedUI || this._renderVideoQualityLabel() JitsiRecordingConstants.mode.STREAM)
} }
</TouchableOpacity>
<TouchableOpacity
onLayout = {
this._createOnLayout(LABEL_ID_TRANSCRIBING)
}
onPress = {
this._createOnPress(LABEL_ID_TRANSCRIBING)
} >
{
this._renderTranscribingLabel()
}
</TouchableOpacity>
{/*
* 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 || (
<TouchableOpacity
onLayout = {
this._createOnLayout(LABEL_ID_QUALITY) }
onPress = {
this._createOnPress(LABEL_ID_QUALITY) } >
{ this._renderVideoQualityLabel() }
</TouchableOpacity>
)
}
</View>
<View
style = { [
styles.indicatorContainer,
wide && _filmstripVisible
&& styles.indicatorContainerWide
] }>
{
this._renderExpandedLabel()
}
</View>
</View> </View>
); );
} }
/**
* 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<*>; _renderRecordingLabel: string => React$Element<*>;
_renderTranscribingLabel: () => React$Element<*> _renderTranscribingLabel: () => React$Element<*>

View File

@ -9,13 +9,26 @@ import { FILMSTRIP_SIZE } from '../../filmstrip';
export const AVATAR_SIZE = 200; export const AVATAR_SIZE = 200;
export default createStyleSheet({ export default createStyleSheet({
/** /**
* View that contains the indicators. * View that contains the indicators.
*/ */
indicatorContainer: { indicatorContainer: {
flex: 1, flex: 1,
flexDirection: 'row', 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', position: 'absolute',
right: 0, right: 0,
@ -25,13 +38,6 @@ export default createStyleSheet({
top: BoxModel.margin * 3 top: BoxModel.margin * 3
}, },
/**
* Indicator container for wide aspect ratio.
*/
indicatorContainerWide: {
right: FILMSTRIP_SIZE
},
/** /**
* Large video container style. * Large video container style.
*/ */

View File

@ -4,6 +4,8 @@ import { Component } from 'react';
import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet'; import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
import { getSessionStatusToShow } from '../functions';
/** /**
* NOTE: Web currently renders multiple indicators if multiple recording * 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 * 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 * running. These boolean are shared across the two components to make it
* easier to align web's behaviour to mobile's later if necessary. * 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 * The status of the highermost priority session.
* component must be rendered.
*/ */
_visible: boolean, _status: ?string,
/** /**
* The recording mode this indicator should display. * The recording mode this indicator should display.
@ -34,8 +35,8 @@ export type Props = {
/** /**
* Abstract class for the {@code RecordingLabel} component. * Abstract class for the {@code RecordingLabel} component.
*/ */
export default class AbstractRecordingLabel<P: Props> export default class AbstractRecordingLabel
extends Component<P> { extends Component<Props> {
/** /**
* Implements React {@code Component}'s render. * Implements React {@code Component}'s render.
@ -43,7 +44,7 @@ export default class AbstractRecordingLabel<P: Props>
* @inheritdoc * @inheritdoc
*/ */
render() { render() {
return this.props._visible ? this._renderLabel() : null; return this.props._status ? this._renderLabel() : null;
} }
_getLabelKey: () => ?string _getLabelKey: () => ?string
@ -84,20 +85,13 @@ export default class AbstractRecordingLabel<P: Props>
* @param {Props} ownProps - The component's own props. * @param {Props} ownProps - The component's own props.
* @private * @private
* @returns {{ * @returns {{
* _visible: boolean * _status: ?string
* }} * }}
*/ */
export function _mapStateToProps(state: Object, ownProps: Props) { export function _mapStateToProps(state: Object, ownProps: Props) {
const { mode } = ownProps; 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 { return {
_visible _status: getSessionStatusToShow(state, mode)
}; };
} }

View File

@ -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<Props> {
/**
* 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));

View File

@ -8,7 +8,6 @@ import { CircularLabel } from '../../base/label';
import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet'; import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
import AbstractRecordingLabel, { import AbstractRecordingLabel, {
type Props,
_mapStateToProps _mapStateToProps
} from './AbstractRecordingLabel'; } from './AbstractRecordingLabel';
import styles from './styles'; import styles from './styles';
@ -19,7 +18,7 @@ import styles from './styles';
* *
* @extends {Component} * @extends {Component}
*/ */
class RecordingLabel extends AbstractRecordingLabel<Props> { class RecordingLabel extends AbstractRecordingLabel {
/** /**
* Renders the platform specific label component. * Renders the platform specific label component.
@ -41,9 +40,21 @@ class RecordingLabel extends AbstractRecordingLabel<Props> {
return null; 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 ( return (
<CircularLabel <CircularLabel
label = { this.props.t(this._getLabelKey()) } label = { this.props.t(this._getLabelKey()) }
status = { status }
style = { indicatorStyle } /> style = { indicatorStyle } />
); );
} }

View File

@ -7,7 +7,6 @@ import { CircularLabel } from '../../base/label';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import AbstractRecordingLabel, { import AbstractRecordingLabel, {
type Props,
_mapStateToProps _mapStateToProps
} from './AbstractRecordingLabel'; } from './AbstractRecordingLabel';
@ -17,7 +16,7 @@ import AbstractRecordingLabel, {
* *
* @extends {Component} * @extends {Component}
*/ */
class RecordingLabel extends AbstractRecordingLabel<Props> { class RecordingLabel extends AbstractRecordingLabel {
/** /**
* Renders the platform specific label component. * Renders the platform specific label component.
* *

View File

@ -9,3 +9,4 @@ export {
StopRecordingDialog StopRecordingDialog
} from './Recording'; } from './Recording';
export { default as RecordingLabel } from './RecordingLabel'; export { default as RecordingLabel } from './RecordingLabel';
export { default as RecordingExpandedLabel } from './RecordingExpandedLabel';

View File

@ -2,6 +2,9 @@
import { ColorPalette, createStyleSheet } from '../../base/styles'; 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. * The styles of the React {@code Components} of the feature recording.
*/ */
@ -11,13 +14,13 @@ export default createStyleSheet({
* Style for the recording indicator. * Style for the recording indicator.
*/ */
indicatorLive: { indicatorLive: {
backgroundColor: ColorPalette.blue backgroundColor: LIVE_LABEL_COLOR
}, },
/** /**
* Style for the recording indicator. * Style for the recording indicator.
*/ */
indicatorRecording: { indicatorRecording: {
backgroundColor: ColorPalette.red backgroundColor: REC_LABEL_COLOR
} }
}); });

View File

@ -1,5 +1,7 @@
// @flow // @flow
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
/** /**
* The identifier of the sound to be played when a recording or live streaming * The identifier of the sound to be played when a recording or live streaming
* session is stopped. * session is stopped.
@ -26,3 +28,15 @@ export const RECORDING_TYPES = {
JIBRI: 'jibri', JIBRI: 'jibri',
JIRECON: 'jirecon' 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<string>}
*/
export const RECORDING_STATUS_PRIORITIES = [
JitsiRecordingConstants.status.OFF,
JitsiRecordingConstants.status.PENDING,
JitsiRecordingConstants.status.ON
];

View File

@ -2,6 +2,8 @@
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet'; 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 * Searches in the passed in redux state for an active recording session of the
* passed in mode. * passed in mode.
@ -43,3 +45,30 @@ export function getSessionById(state: Object, id: string) {
return state['features/recording'].sessionDatas.find( return state['features/recording'].sessionDatas.find(
sessionData => sessionData.id === id); 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;
}

View File

@ -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<Props> {
/**
* Returns the label specific text of this {@code ExpandedLabel}.
*
* @returns {string}
*/
_getLabel() {
return this.props.t('transcribing.expandedLabel');
}
}
export default translate(TranscribingExpandedLabel);

View File

@ -1 +1,4 @@
export { default as TranscribingLabel } from './TranscribingLabel'; export { default as TranscribingLabel } from './TranscribingLabel';
export {
default as TranscribingExpandedLabel
} from './TranscribingExpandedLabel';

View File

@ -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<Props> {
/**
* 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);

View File

@ -3,3 +3,6 @@ export {
} from './OverflowMenuVideoQualityItem'; } from './OverflowMenuVideoQualityItem';
export { default as VideoQualityDialog } from './VideoQualityDialog'; export { default as VideoQualityDialog } from './VideoQualityDialog';
export { default as VideoQualityLabel } from './VideoQualityLabel'; export { default as VideoQualityLabel } from './VideoQualityLabel';
export {
default as VideoQualityExpandedLabel
} from './VideoQualityExpandedLabel';

View File

@ -2,6 +2,8 @@
import { ColorPalette, createStyleSheet } from '../../base/styles'; 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. * 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. * Style for the audio-only indicator.
*/ */
indicatorAudioOnly: { indicatorAudioOnly: {
backgroundColor: ColorPalette.green backgroundColor: AUD_LABEL_COLOR
} }
}); });