+ extends Component {
_isMounted: boolean;
/**
@@ -100,7 +100,7 @@ export default class AbstractStartLiveStreamDialog
*
* @inheritdoc
*/
- constructor(props: Props) {
+ constructor(props: P) {
super(props);
this.state = {
@@ -134,10 +134,6 @@ export default class AbstractStartLiveStreamDialog
*/
componentDidMount() {
this._isMounted = true;
-
- if (this.props._googleApiApplicationClientID) {
- this._onInitializeGoogleApi();
- }
}
/**
@@ -197,13 +193,6 @@ export default class AbstractStartLiveStreamDialog
*/
_onGetYouTubeBroadcasts: () => Promise<*>;
- /**
- * Loads the Google client application used for fetching stream keys.
- * If the user is already logged in, then a request for available YouTube
- * broadcasts is also made.
- */
- _onInitializeGoogleApi: () => Object;
-
_onStreamKeyChange: string => void;
/**
@@ -291,6 +280,8 @@ export default class AbstractStartLiveStreamDialog
* @returns {{
* _conference: Object,
* _googleApiApplicationClientID: string,
+ * _googleAPIState: number,
+ * _googleProfileEmail: string,
* _streamKey: string
* }}
*/
diff --git a/react/features/recording/components/LiveStream/GoogleSigninForm.native.js b/react/features/recording/components/LiveStream/GoogleSigninForm.native.js
new file mode 100644
index 000000000..7bba22e0c
--- /dev/null
+++ b/react/features/recording/components/LiveStream/GoogleSigninForm.native.js
@@ -0,0 +1,254 @@
+// @flow
+
+import React, { Component } from 'react';
+import { Text, View } from 'react-native';
+import { connect } from 'react-redux';
+
+import { translate } from '../../../base/i18n';
+
+import {
+ GOOGLE_API_STATES,
+ GOOGLE_SCOPE_YOUTUBE,
+ googleApi,
+ GoogleSignInButton,
+ setGoogleAPIState
+} from '../../../google-api';
+
+import styles from './styles';
+
+const logger = require('jitsi-meet-logger').getLogger(__filename);
+
+/**
+ * Prop type of the component {@code GoogleSigninForm}.
+ */
+type Props = {
+
+ /**
+ * The ID for the Google client application used for making stream key
+ * related requests.
+ */
+ clientId: string,
+
+ /**
+ * The Redux dispatch Function.
+ */
+ dispatch: Function,
+
+ /**
+ * The current state of the Google api as defined in {@code constants.js}.
+ */
+ googleAPIState: number,
+
+ /**
+ * The recently received Google response.
+ */
+ googleResponse: Object,
+
+ /**
+ * The ID for the Google client application used for making stream key
+ * related requests on iOS.
+ */
+ iOSClientId: string,
+
+ /**
+ * A callback to be invoked when an authenticated user changes, so
+ * then we can get (or clear) the YouTube stream key.
+ */
+ onUserChanged: Function,
+
+ /**
+ * Function to be used to translate i18n labels.
+ */
+ t: Function
+};
+
+/**
+ * Class to render a google sign in form, or a google stream picker dialog.
+ *
+ * @extends Component
+ */
+class GoogleSigninForm extends Component {
+ /**
+ * Instantiates a new {@code GoogleSigninForm} component.
+ *
+ * @inheritdoc
+ */
+ constructor(props: Props) {
+ super(props);
+
+ this._logGoogleError = this._logGoogleError.bind(this);
+ this._onGoogleButtonPress = this._onGoogleButtonPress.bind(this);
+ }
+
+ /**
+ * Implements React's Component.componentDidMount.
+ *
+ * @inheritdoc
+ */
+ componentDidMount() {
+ if (!this.props.clientId) {
+ // NOTE: This is a developer error message, not intended for the
+ // user to see.
+ logger.error('Missing clientID');
+ this._setApiState(GOOGLE_API_STATES.NOT_AVAILABLE);
+
+ return;
+ }
+
+ googleApi.hasPlayServices()
+ .then(() => {
+ googleApi.configure({
+ iosClientId: this.props.iOSClientId,
+ offlineAccess: false,
+ scopes: [ GOOGLE_SCOPE_YOUTUBE ],
+ webClientId: this.props.clientId
+ });
+
+ googleApi.signInSilently().then(response => {
+ this._setApiState(response
+ ? GOOGLE_API_STATES.SIGNED_IN
+ : GOOGLE_API_STATES.LOADED,
+ response);
+ }, () => {
+ this._setApiState(GOOGLE_API_STATES.LOADED);
+ });
+ })
+ .catch(error => {
+ this._logGoogleError(error);
+ this._setApiState(GOOGLE_API_STATES.NOT_AVAILABLE);
+ });
+ }
+
+ /**
+ * Renders the component.
+ *
+ * @inheritdoc
+ */
+ render() {
+ const { t } = this.props;
+ const { googleAPIState, googleResponse } = this.props;
+ const signedInUser = googleResponse
+ && googleResponse.user
+ && googleResponse.user.email;
+
+ if (googleAPIState === GOOGLE_API_STATES.NOT_AVAILABLE
+ || googleAPIState === GOOGLE_API_STATES.NEEDS_LOADING
+ || typeof googleAPIState === 'undefined') {
+ return null;
+ }
+
+ return (
+
+
+ { signedInUser ?
+ { `${t('liveStreaming.signedInAs')} ${signedInUser}` }
+ :
+ { t('liveStreaming.signInCTA') }
+ }
+
+
+
+ );
+ }
+
+ _logGoogleError: Object => void
+
+ /**
+ * A helper function to log developer related errors.
+ *
+ * @private
+ * @param {Object} error - The error to be logged.
+ * @returns {void}
+ */
+ _logGoogleError(error) {
+ // NOTE: This is a developer error message, not intended for the
+ // user to see.
+ logger.error('Google API error. Possible cause: bad config.', error);
+ }
+
+ _onGoogleButtonPress: () => void
+
+ /**
+ * Callback to be invoked when the user presses the Google button,
+ * regardless of being logged in or out.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onGoogleButtonPress() {
+ const { googleResponse } = this.props;
+
+ if (googleResponse && googleResponse.user) {
+ // the user is signed in
+ this._onSignOut();
+ } else {
+ this._onSignIn();
+ }
+ }
+
+ _onSignIn: () => void
+
+ /**
+ * Initiates a sign in if the user is not signed in yet.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onSignIn() {
+ googleApi.signIn().then(response => {
+ this._setApiState(GOOGLE_API_STATES.SIGNED_IN, response);
+ }, this._logGoogleError);
+ }
+
+ _onSignOut: () => void
+
+ /**
+ * Initiates a sign out if the user is signed in.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onSignOut() {
+ googleApi.signOut().then(response => {
+ this._setApiState(GOOGLE_API_STATES.LOADED, response);
+ }, this._logGoogleError);
+ }
+
+ /**
+ * Updates the API (Google Auth) state.
+ *
+ * @private
+ * @param {number} apiState - The state of the API.
+ * @param {?Object} googleResponse - The response from the API.
+ * @returns {void}
+ */
+ _setApiState(apiState, googleResponse) {
+ this.props.onUserChanged(googleResponse);
+ this.props.dispatch(setGoogleAPIState(apiState, googleResponse));
+ }
+}
+
+/**
+ * Maps (parts of) the redux state to the associated props for the
+ * {@code GoogleSigninForm} component.
+ *
+ * @param {Object} state - The Redux state.
+ * @private
+ * @returns {{
+ * googleAPIState: number,
+ * googleResponse: Object
+ * }}
+ */
+function _mapStateToProps(state: Object) {
+ const { googleAPIState, googleResponse } = state['features/google-api'];
+
+ return {
+ googleAPIState,
+ googleResponse
+ };
+}
+
+export default translate(connect(_mapStateToProps)(GoogleSigninForm));
diff --git a/react/features/recording/components/LiveStream/BroadcastsDropdown.native.js b/react/features/recording/components/LiveStream/GoogleSigninForm.web.js
similarity index 100%
rename from react/features/recording/components/LiveStream/BroadcastsDropdown.native.js
rename to react/features/recording/components/LiveStream/GoogleSigninForm.web.js
diff --git a/react/features/recording/components/LiveStream/StartLiveStreamDialog.native.js b/react/features/recording/components/LiveStream/StartLiveStreamDialog.native.js
index 66cf0005d..e179e33ff 100644
--- a/react/features/recording/components/LiveStream/StartLiveStreamDialog.native.js
+++ b/react/features/recording/components/LiveStream/StartLiveStreamDialog.native.js
@@ -5,20 +5,34 @@ import { View } from 'react-native';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n';
+import { googleApi } from '../../../google-api';
+
import { setLiveStreamKey } from '../../actions';
import AbstractStartLiveStreamDialog, {
- _mapStateToProps,
- type Props
+ _mapStateToProps as _abstractMapStateToProps,
+ type Props as AbstractProps
} from './AbstractStartLiveStreamDialog';
+import GoogleSigninForm from './GoogleSigninForm';
import StreamKeyForm from './StreamKeyForm';
+import StreamKeyPicker from './StreamKeyPicker';
+import styles from './styles';
+
+type Props = AbstractProps & {
+
+ /**
+ * The ID for the Google client application used for making stream key
+ * related requests on iOS.
+ */
+ _googleApiIOSClientID: string
+};
/**
* A React Component for requesting a YouTube stream key to use for live
* streaming of the current conference.
*/
-class StartLiveStreamDialog extends AbstractStartLiveStreamDialog {
+class StartLiveStreamDialog extends AbstractStartLiveStreamDialog {
/**
* Constructor of the component.
*
@@ -28,27 +42,13 @@ class StartLiveStreamDialog extends AbstractStartLiveStreamDialog {
super(props);
// Bind event handlers so they are only bound once per instance.
- this._onInitializeGoogleApi = this._onInitializeGoogleApi.bind(this);
this._onStreamKeyChangeNative
= this._onStreamKeyChangeNative.bind(this);
+ this._onStreamKeyPick = this._onStreamKeyPick.bind(this);
+ this._onUserChanged = this._onUserChanged.bind(this);
this._renderDialogContent = this._renderDialogContent.bind(this);
}
- _onInitializeGoogleApi: () => Promise<*>
-
- /**
- * Loads the Google 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 is a placeholder method for the Google feature.
- return Promise.resolve();
- }
-
_onStreamKeyChange: string => void
_onStreamKeyChangeNative: string => void;
@@ -70,6 +70,49 @@ class StartLiveStreamDialog extends AbstractStartLiveStreamDialog {
this._onStreamKeyChange(streamKey);
}
+ _onStreamKeyPick: string => void
+
+ /**
+ * Callback to be invoked when the user selects a stream from the picker.
+ *
+ * @private
+ * @param {string} streamKey - The key of the selected stream.
+ * @returns {void}
+ */
+ _onStreamKeyPick(streamKey) {
+ this.setState({
+ streamKey
+ });
+ }
+
+ _onUserChanged: Object => void
+
+ /**
+ * A callback to be invoked when an authenticated user changes, so
+ * then we can get (or clear) the YouTube stream key.
+ *
+ * TODO: handle errors by showing some indication to the user.
+ *
+ * @private
+ * @param {Object} response - The retreived signin response.
+ * @returns {void}
+ */
+ _onUserChanged(response) {
+ if (response && response.accessToken) {
+ googleApi.getYouTubeLiveStreams(response.accessToken)
+ .then(broadcasts => {
+ this.setState({
+ broadcasts
+ });
+ });
+ } else {
+ this.setState({
+ broadcasts: undefined,
+ streamKey: undefined
+ });
+ }
+ }
+
_renderDialogContent: () => React$Component<*>
/**
@@ -79,14 +122,37 @@ class StartLiveStreamDialog extends AbstractStartLiveStreamDialog {
*/
_renderDialogContent() {
return (
-
+
+
+
+ value = { this.state.streamKey || this.props._streamKey } />
);
}
}
+/**
+ * Maps part of the Redux state to the component's props.
+ *
+ * @param {Object} state - The Redux state.
+ * @returns {{
+ * _googleApiApplicationClientID: string
+ * }}
+ */
+function _mapStateToProps(state: Object) {
+ return {
+ ..._abstractMapStateToProps(state),
+ _googleApiIOSClientID:
+ state['features/base/config'].googleApiIOSClientID
+ };
+}
+
export default translate(connect(_mapStateToProps)(StartLiveStreamDialog));
diff --git a/react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js b/react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js
index 081d0fe1e..0ef912310 100644
--- a/react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js
+++ b/react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js
@@ -21,7 +21,7 @@ import AbstractStartLiveStreamDialog, {
_mapStateToProps,
type Props
} from './AbstractStartLiveStreamDialog';
-import BroadcastsDropdown from './BroadcastsDropdown';
+import StreamKeyPicker from './StreamKeyPicker';
import StreamKeyForm from './StreamKeyForm';
/**
@@ -31,7 +31,7 @@ import StreamKeyForm from './StreamKeyForm';
* @extends Component
*/
class StartLiveStreamDialog
- extends AbstractStartLiveStreamDialog {
+ extends AbstractStartLiveStreamDialog {
/**
* Initializes a new {@code StartLiveStreamDialog} instance.
@@ -53,6 +53,21 @@ class StartLiveStreamDialog
this._renderDialogContent = this._renderDialogContent.bind(this);
}
+ /**
+ * Implements {@link Component#componentDidMount()}. Invoked immediately
+ * after this component is mounted.
+ *
+ * @inheritdoc
+ * @returns {void}
+ */
+ componentDidMount() {
+ super.componentDidMount();
+
+ if (this.props._googleApiApplicationClientID) {
+ this._onInitializeGoogleApi();
+ }
+ }
+
_onInitializeGoogleApi: () => Promise<*>;
/**
@@ -237,18 +252,15 @@ class StartLiveStreamDialog
switch (this.props._googleAPIState) {
case GOOGLE_API_STATES.LOADED:
- googleContent = (
-
- );
+ googleContent
+ = ;
helpText = t('liveStreaming.signInCTA');
break;
case GOOGLE_API_STATES.SIGNED_IN:
googleContent = (
-
@@ -285,8 +297,7 @@ class StartLiveStreamDialog
if (this.state.errorType !== undefined) {
googleContent = (
+ onClick = { this._onRequestGoogleSignIn } />
);
helpText = this._getGoogleErrorMessageToDisplay();
}
diff --git a/react/features/recording/components/LiveStream/StreamKeyForm.native.js b/react/features/recording/components/LiveStream/StreamKeyForm.native.js
index dab1d4cbf..409fc4efc 100644
--- a/react/features/recording/components/LiveStream/StreamKeyForm.native.js
+++ b/react/features/recording/components/LiveStream/StreamKeyForm.native.js
@@ -39,7 +39,7 @@ class StreamKeyForm extends AbstractStreamKeyForm {
const { t } = this.props;
return (
-
+
{
t('dialog.streamKey')
diff --git a/react/features/recording/components/LiveStream/StreamKeyPicker.native.js b/react/features/recording/components/LiveStream/StreamKeyPicker.native.js
new file mode 100644
index 000000000..89eb45197
--- /dev/null
+++ b/react/features/recording/components/LiveStream/StreamKeyPicker.native.js
@@ -0,0 +1,122 @@
+// @flow
+
+import React, { Component } from 'react';
+import { Text, TouchableHighlight, View } from 'react-native';
+
+import { translate } from '../../../base/i18n';
+
+import styles, { ACTIVE_OPACITY, TOUCHABLE_UNDERLAY } from './styles';
+
+type Props = {
+
+ /**
+ * The list of broadcasts the user can pick from.
+ */
+ broadcasts: ?Array