// @flow import Spinner from '@atlaskit/spinner'; import React from 'react'; import { connect } from 'react-redux'; import { translate } from '../../../base/i18n'; import { updateProfile, GOOGLE_API_STATES, loadGoogleAPI, requestAvailableYouTubeBroadcasts, requestLiveStreamsForYouTubeBroadcast, showAccountSelection, signIn } from '../../../google-api'; import AbstractStartLiveStreamDialog, { _mapStateToProps, type Props } from './AbstractStartLiveStreamDialog'; import BroadcastsDropdown from './BroadcastsDropdown'; import GoogleSignInButton from './GoogleSignInButton'; import StreamKeyForm from './StreamKeyForm'; /** * A React Component for requesting a YouTube stream key to use for live * streaming of the current conference. * * @extends Component */ class StartLiveStreamDialog extends AbstractStartLiveStreamDialog { /** * Initializes a new {@code StartLiveStreamDialog} instance. * * @param {Props} props - The React {@code Component} props to initialize * the new {@code StartLiveStreamDialog} instance with. */ constructor(props: Props) { super(props); // Bind event handlers so they are only bound once per instance. this._onGetYouTubeBroadcasts = this._onGetYouTubeBroadcasts.bind(this); this._onInitializeGoogleApi = this._onInitializeGoogleApi.bind(this); this._onGoogleSignIn = this._onGoogleSignIn.bind(this); this._onRequestGoogleSignIn = this._onRequestGoogleSignIn.bind(this); this._onYouTubeBroadcastIDSelected = this._onYouTubeBroadcastIDSelected.bind(this); this._renderDialogContent = this._renderDialogContent.bind(this); } _onInitializeGoogleApi: () => Promise<*>; /** * Loads the Google web client application used for fetching stream keys. * If the user is already logged in, then a request for available YouTube * broadcasts is also made. * * @private * @returns {Promise} */ _onInitializeGoogleApi() { this.props.dispatch( loadGoogleAPI(this.props._googleApiApplicationClientID)) .catch(response => this._parseErrorFromResponse(response)); } /** * Automatically selects the input field's value after starting to edit the * display name. * * @inheritdoc * @returns {void} */ componentDidUpdate(previousProps) { if (previousProps._googleAPIState === GOOGLE_API_STATES.LOADED && this.props._googleAPIState === GOOGLE_API_STATES.SIGNED_IN) { this._onGetYouTubeBroadcasts(); } } _onGetYouTubeBroadcasts: () => Promise<*>; /** * Asks the user to sign in, if not already signed in, and then requests a * list of the user's YouTube broadcasts. * * @private * @returns {void} */ _onGetYouTubeBroadcasts() { this.props.dispatch(updateProfile()) .catch(response => this._parseErrorFromResponse(response)); this.props.dispatch(requestAvailableYouTubeBroadcasts()) .then(broadcasts => { this._setStateIfMounted({ broadcasts }); if (broadcasts.length === 1) { const broadcast = broadcasts[0]; this._onYouTubeBroadcastIDSelected(broadcast.boundStreamID); } }) .catch(response => this._parseErrorFromResponse(response)); } _onGoogleSignIn: () => Object; /** * Forces the Google web client application to prompt for a sign in, such as * when changing account, and will then fetch available YouTube broadcasts. * * @private * @returns {Promise} */ _onGoogleSignIn() { this.props.dispatch(signIn()) .catch(response => this._parseErrorFromResponse(response)); } _onRequestGoogleSignIn: () => Object; /** * Forces the Google web client application to prompt for a sign in, such as * when changing account, and will then fetch available YouTube broadcasts. * * @private * @returns {Promise} */ _onRequestGoogleSignIn() { // when there is an error we show the google sign-in button. // once we click it we want to clear the error from the state this.props.dispatch(showAccountSelection()) .then(() => this._setStateIfMounted({ broadcasts: undefined, errorType: undefined })) .then(() => this._onGetYouTubeBroadcasts()); } _onStreamKeyChange: string => void; _onYouTubeBroadcastIDSelected: (string) => Object; /** * Fetches the stream key for a YouTube broadcast and updates the internal * state to display the associated stream key as being entered. * * @param {string} boundStreamID - The bound stream ID associated with the * broadcast from which to get the stream key. * @private * @returns {Promise} */ _onYouTubeBroadcastIDSelected(boundStreamID) { this.props.dispatch( requestLiveStreamsForYouTubeBroadcast(boundStreamID)) .then(({ streamKey, selectedBoundStreamID }) => this._setStateIfMounted({ streamKey, selectedBoundStreamID })); } /** * Only show an error if an external request was made with the Google api. * Do not error if the login in canceled. * And searches in a Google API error response for the error type. * * @param {Object} response - The Google API response that may contain an * error. * @private * @returns {string|null} */ _parseErrorFromResponse(response) { if (!response || !response.result) { return; } const result = response.result; const error = result.error; const errors = error && error.errors; const firstError = errors && errors[0]; this._setStateIfMounted({ errorType: (firstError && firstError.reason) || null }); } _renderDialogContent: () => React$Component<*> /** * Renders the platform specific dialog content. * * @returns {React$Component} */ _renderDialogContent() { const { _googleApiApplicationClientID } = this.props; return (
{ _googleApiApplicationClientID ? this._renderYouTubePanel() : null }
); } /** * Renders a React Element for authenticating with the Google web client. * * @private * @returns {ReactElement} */ _renderYouTubePanel() { const { t, _googleProfileEmail } = this.props; const { broadcasts, selectedBoundStreamID } = this.state; let googleContent, helpText; switch (this.props._googleAPIState) { case GOOGLE_API_STATES.LOADED: googleContent = ( ); helpText = t('liveStreaming.signInCTA'); break; case GOOGLE_API_STATES.SIGNED_IN: googleContent = ( ); /** * FIXME: Ideally this help text would be one translation string * that also accepts the anchor. This can be done using the Trans * component of react-i18next but I couldn't get it working... */ helpText = (
{ `${t('liveStreaming.chooseCTA', { email: _googleProfileEmail })} ` } { t('liveStreaming.changeSignIn') }
); break; case GOOGLE_API_STATES.NEEDS_LOADING: default: googleContent = ( ); break; } if (this.state.errorType !== undefined) { googleContent = ( ); helpText = this._getGoogleErrorMessageToDisplay(); } return (
{ helpText }
{ googleContent }
); } _setStateIfMounted: Object => void /** * Returns the error message to display for the current error state. * * @private * @returns {string} The error message to display. */ _getGoogleErrorMessageToDisplay() { let text; switch (this.state.errorType) { case 'liveStreamingNotEnabled': text = this.props.t( 'liveStreaming.errorLiveStreamNotEnabled', { email: this.props._googleProfileEmail }); break; default: text = this.props.t('liveStreaming.errorAPI'); break; } return
{ text }
; } } export default translate(connect(_mapStateToProps)(StartLiveStreamDialog));