diff --git a/css/_recording.scss b/css/_recording.scss index 3c7491267..d2e42f823 100644 --- a/css/_recording.scss +++ b/css/_recording.scss @@ -65,6 +65,8 @@ } .form-footer { + display: flex; + margin-top: 5px; text-align: right; } @@ -95,9 +97,15 @@ .stream-key-form { .helper-link { - display: inline-block; cursor: pointer; - margin-top: 5px; + display: inline-block; + flex-shrink: 0; + margin-left: auto; + } + + .validation-error { + color:#FFD740; + font-size: 12px; } } } diff --git a/lang/main.json b/lang/main.json index 194551d42..6a0f2049b 100644 --- a/lang/main.json +++ b/lang/main.json @@ -515,6 +515,7 @@ "expandedOn": "The meeting is currently being streamed to YouTube.", "expandedPending": "The live streaming is being started...", "failedToStart": "Live Streaming failed to start", + "invalidStreamKey": "Live stream key may be incorrect.", "off": "Live Streaming stopped", "on": "Live Streaming", "pending": "Starting Live Stream...", diff --git a/react/features/recording/components/LiveStream/AbstractStartLiveStreamDialog.js b/react/features/recording/components/LiveStream/AbstractStartLiveStreamDialog.js index cd80db46b..29e8415f3 100644 --- a/react/features/recording/components/LiveStream/AbstractStartLiveStreamDialog.js +++ b/react/features/recording/components/LiveStream/AbstractStartLiveStreamDialog.js @@ -194,7 +194,8 @@ export default class AbstractStartLiveStreamDialog */ _onSubmit() { const { broadcasts, selectedBoundStreamID } = this.state; - const key = this.state.streamKey || this.props._streamKey; + const key + = (this.state.streamKey || this.props._streamKey || '').trim(); if (!key) { return false; diff --git a/react/features/recording/components/LiveStream/AbstractStreamKeyForm.js b/react/features/recording/components/LiveStream/AbstractStreamKeyForm.js index 278a03c74..d2b5ccf58 100644 --- a/react/features/recording/components/LiveStream/AbstractStreamKeyForm.js +++ b/react/features/recording/components/LiveStream/AbstractStreamKeyForm.js @@ -1,5 +1,6 @@ // @flow +import debounce from 'lodash/debounce'; import { Component } from 'react'; declare var interfaceConfig: Object; @@ -33,14 +34,27 @@ export type Props = { value: string }; +/** + * The state of the component. + */ +type State = { + + /** + * Whether or not to show the warnings that the passed in value seems like + * an improperly formatted stream key. + */ + showValidationError: boolean +}; + /** * An abstract React Component for entering a key for starting a YouTube live * stream. * * @extends Component */ -export default class AbstractStreamKeyForm extends Component { +export default class AbstractStreamKeyForm extends Component { helpURL: string; + _debouncedUpdateValidationErrorVisibility: Function; /** * Constructor for the component. @@ -50,14 +64,45 @@ export default class AbstractStreamKeyForm extends Component { constructor(props: Props) { super(props); + this.state = { + showValidationError: Boolean(this.props.value) + && !this._validateStreamKey(this.props.value) + }; + this.helpURL = (typeof interfaceConfig !== 'undefined' && interfaceConfig.LIVE_STREAMING_HELP_LINK) || LIVE_STREAMING_HELP_LINK; + this._debouncedUpdateValidationErrorVisibility = debounce( + this._updateValidationErrorVisibility.bind(this), + 800, + { leading: false } + ); + // Bind event handlers so they are only bound once per instance. this._onInputChange = this._onInputChange.bind(this); } + /** + * Implements React Component's componentDidUpdate. + * + * @inheritdoc + */ + componentDidUpdate(prevProps: Props) { + if (this.props.value !== prevProps.value) { + this._debouncedUpdateValidationErrorVisibility(); + } + } + + /** + * Implements React Component's componentWillUnmount. + * + * @inheritdoc + */ + componentWillUnmount() { + this._debouncedUpdateValidationErrorVisibility.cancel(); + } + _onInputChange: Object => void /** @@ -75,4 +120,38 @@ export default class AbstractStreamKeyForm extends Component { this.props.onChange(value); } + + /** + * Checks if the stream key value seems like a valid stream key and sets the + * state for showing or hiding the notification about the stream key seeming + * invalid. + * + * @private + * @returns {boolean} + */ + _updateValidationErrorVisibility() { + const newShowValidationError = Boolean(this.props.value) + && !this._validateStreamKey(this.props.value); + + if (newShowValidationError !== this.state.showValidationError) { + this.setState({ + showValidationError: newShowValidationError + }); + } + } + + /** + * Checks if a passed in stream key appears to be in a valid format. + * + * @param {string} streamKey - The stream key to check for valid formatting. + * @returns {void} + * @returns {boolean} + */ + _validateStreamKey(streamKey = '') { + const trimmedKey = streamKey.trim(); + const fourGroupsDashSeparated = /^(?:[a-zA-Z0-9]{4}(?:-(?!$)|$)){4}/; + const match = fourGroupsDashSeparated.exec(trimmedKey); + + return Boolean(match); + } } diff --git a/react/features/recording/components/LiveStream/native/StreamKeyForm.js b/react/features/recording/components/LiveStream/native/StreamKeyForm.js index 094d3aba0..b7ad959ff 100644 --- a/react/features/recording/components/LiveStream/native/StreamKeyForm.js +++ b/react/features/recording/components/LiveStream/native/StreamKeyForm.js @@ -52,15 +52,28 @@ class StreamKeyForm extends AbstractStreamKeyForm { placeholderTextColor = { PLACEHOLDER_COLOR } style = { styles.streamKeyInput } value = { this.props.value } /> - - - { - t('liveStreaming.streamIdHelp') - } - - + + { + this.state.showValidationError + ? + + { t('liveStreaming.invalidStreamKey') } + + + : null + } + + + + { + t('liveStreaming.streamIdHelp') + } + + + + ); } diff --git a/react/features/recording/components/LiveStream/native/styles.js b/react/features/recording/components/LiveStream/native/styles.js index c07bb3c63..7a31c1f72 100644 --- a/react/features/recording/components/LiveStream/native/styles.js +++ b/react/features/recording/components/LiveStream/native/styles.js @@ -48,6 +48,20 @@ export default createStyleSheet({ padding: BoxModel.padding }, + /** + * Wrapper for the last element in the form. + */ + formFooter: { + flexDirection: 'row' + }, + + /** + * Wrapper for individual children in the last element of the form. + */ + formFooterItem: { + flex: 1 + }, + /** * Explaining text on the top of the sign in form. */ @@ -133,6 +147,12 @@ export default createStyleSheet({ text: { color: ColorPalette.white - } + }, + /** + * A different colored text to indicate information needing attention. + */ + warningText: { + color: ColorPalette.Y200 + } }); diff --git a/react/features/recording/components/LiveStream/web/StreamKeyForm.js b/react/features/recording/components/LiveStream/web/StreamKeyForm.js index 334029d00..130e3a9f5 100644 --- a/react/features/recording/components/LiveStream/web/StreamKeyForm.js +++ b/react/features/recording/components/LiveStream/web/StreamKeyForm.js @@ -36,7 +36,7 @@ class StreamKeyForm extends AbstractStreamKeyForm { * @returns {ReactElement} */ render() { - const { value, t } = this.props; + const { t, value } = this.props; return (
@@ -52,16 +52,23 @@ class StreamKeyForm extends AbstractStreamKeyForm { shouldFitContainer = { true } type = 'text' value = { this.props.value } /> - { this.helpURL - ? - : null - } + : null + } +
); }