From d10d61fb7a2bb09884e027d48b6d4134f81c75f0 Mon Sep 17 00:00:00 2001 From: Bettenbuk Zoltan Date: Fri, 10 Aug 2018 12:30:00 +0200 Subject: [PATCH] [RN] Add Google Sign In to live streaming --- android/app/build.gradle | 6 + android/build.gradle | 2 + android/sdk/build.gradle | 3 + .../meet/sdk/ReactInstanceManagerHolder.java | 1 + android/settings.gradle | 2 + config.js | 1 + doc/mobile-google-auth.md | 22 ++ images/btn_google_signin_dark_normal.png | Bin 0 -> 8001 bytes ios/Podfile | 2 + ios/Podfile.lock | 37 ++- ios/app/src/Info.plist | 10 + ios/sdk/sdk.xcodeproj/project.pbxproj | 4 + lang/main.json | 2 + package-lock.json | 5 + package.json | 1 + .../styles/components/styles/ColorPalette.js | 2 + react/features/google-api/actions.js | 27 +- .../components/AbstractGoogleSignInButton.js | 34 +++ .../components/GoogleSignInButton.native.js | 62 +++++ .../components/GoogleSignInButton.web.js | 27 +- react/features/google-api/components/index.js | 2 + .../features/google-api/components/styles.js | 54 ++++ react/features/google-api/constants.js | 44 ++- react/features/google-api/googleApi.native.js | 173 ++++++++++++ .../{googleApi.js => googleApi.web.js} | 53 +--- react/features/google-api/index.js | 6 +- react/features/google-api/reducer.js | 3 +- .../AbstractStartLiveStreamDialog.js | 19 +- .../LiveStream/GoogleSigninForm.native.js | 254 ++++++++++++++++++ ...down.native.js => GoogleSigninForm.web.js} | 0 .../StartLiveStreamDialog.native.js | 108 ++++++-- .../LiveStream/StartLiveStreamDialog.web.js | 31 ++- .../LiveStream/StreamKeyForm.native.js | 2 +- .../LiveStream/StreamKeyPicker.native.js | 122 +++++++++ ...Dropdown.web.js => StreamKeyPicker.web.js} | 10 +- .../components/LiveStream/styles.native.js | 81 +++++- react/features/recording/reducer.js | 8 - 37 files changed, 1091 insertions(+), 129 deletions(-) create mode 100644 doc/mobile-google-auth.md create mode 100644 images/btn_google_signin_dark_normal.png create mode 100644 react/features/google-api/components/AbstractGoogleSignInButton.js create mode 100644 react/features/google-api/components/styles.js create mode 100644 react/features/google-api/googleApi.native.js rename react/features/google-api/{googleApi.js => googleApi.web.js} (89%) create mode 100644 react/features/recording/components/LiveStream/GoogleSigninForm.native.js rename react/features/recording/components/LiveStream/{BroadcastsDropdown.native.js => GoogleSigninForm.web.js} (100%) create mode 100644 react/features/recording/components/LiveStream/StreamKeyPicker.native.js rename react/features/recording/components/LiveStream/{BroadcastsDropdown.web.js => StreamKeyPicker.web.js} (94%) diff --git a/android/app/build.gradle b/android/app/build.gradle index 422f82ab0..9afd235a7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -41,9 +41,15 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:22.2.0' + compile 'com.google.android.gms:play-services-auth:15.0.0' implementation project(':sdk') debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1' releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1' } + +if (project.file('google-services.json').exists()) { + apply plugin: 'com.google.gms.google-services' +} diff --git a/android/build.gradle b/android/build.gradle index acfd3cd98..e10548390 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -8,6 +8,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.google.gms:google-services:3.2.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files. @@ -16,6 +17,7 @@ buildscript { allprojects { repositories { + maven { url "https://maven.google.com" } google() jcenter() maven { url "$rootDir/../node_modules/jsc-android/dist" } diff --git a/android/sdk/build.gradle b/android/sdk/build.gradle index eb07b4bbb..dadd27caa 100644 --- a/android/sdk/build.gradle +++ b/android/sdk/build.gradle @@ -26,6 +26,9 @@ dependencies { compile project(':react-native-background-timer') compile project(':react-native-fast-image') + compile(project(":react-native-google-signin")) { + exclude group: 'com.google.android.gms' + } compile project(':react-native-immersive') compile project(':react-native-keep-awake') compile project(':react-native-linear-gradient') diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java index 3edb63811..42962e40d 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java @@ -119,6 +119,7 @@ class ReactInstanceManagerHolder { .setApplication(application) .setBundleAssetName("index.android.bundle") .setJSMainModulePath("index.android") + .addPackage(new co.apptailor.googlesignin.RNGoogleSigninPackage()) .addPackage(new com.BV.LinearGradient.LinearGradientPackage()) .addPackage(new com.calendarevents.CalendarEventsPackage()) .addPackage(new com.corbt.keepawake.KCKeepAwakePackage()) diff --git a/android/settings.gradle b/android/settings.gradle index e5ba6ed61..2ab422e7b 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -5,6 +5,8 @@ include ':react-native-background-timer' project(':react-native-background-timer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-timer/android') include ':react-native-fast-image' project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fast-image/android') +include ':react-native-google-signin' +project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-google-signin/android') include ':react-native-immersive' project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android') include ':react-native-keep-awake' diff --git a/config.js b/config.js index 5b60f4856..0b490247d 100644 --- a/config.js +++ b/config.js @@ -400,6 +400,7 @@ var config = { externalConnectUrl firefox_fake_device googleApiApplicationClientID + googleApiIOSClientID iAmRecorder iAmSipGateway microsoftApiApplicationClientID diff --git a/doc/mobile-google-auth.md b/doc/mobile-google-auth.md new file mode 100644 index 000000000..ad499eb0e --- /dev/null +++ b/doc/mobile-google-auth.md @@ -0,0 +1,22 @@ +# Setting up Google Authentication + +- Create a Firebase project here: https://firebase.google.com/. You'll need a +signed Android build for that, that can be a debug auto-signed build too, just +retrieve the signing hash. +- Place the generated ```google-services.json``` file in ```android/app``` +for Android and the ```GoogleService-Info.plist``` into ```ios/app/src``` for +iOS (you can stop at that step, no need for the driver and the code changes they +suggest in the wizard). +- You may want to exclude these files in YOUR GIT config (do not exclude them in +the ```.gitignore``` of the application itself!). +- Your WEB and iOS client IDs are auto generated during the Firebase project + creation. Find them in the Google Developer console: + https://console.developers.google.com/ +- Make sure your config reflects these IDs so then the Redux state of the + feature ```features/base/config``` contains variables + ```googleApiApplicationClientID``` and ```googleApiIOSClientID``` with the + respective values. +- Add your iOS client ID as an application URL schema into +```ios/app/src/Info.plist``` (replacing placeholder). +- Enable YouTube API access on the developer console (see above) for live +streaming. diff --git a/images/btn_google_signin_dark_normal.png b/images/btn_google_signin_dark_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..f27bb2433042aea5fc34e19fcf90944430ec331b GIT binary patch literal 8001 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 literal 0 HcmV?d00001 diff --git a/ios/Podfile b/ios/Podfile index f05124501..bee5566b8 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -35,6 +35,8 @@ target 'JitsiMeet' do pod 'react-native-locale-detector', :path => '../node_modules/react-native-locale-detector' pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc' + pod 'RNGoogleSignin', + :path => '../node_modules/react-native-google-signin' pod 'RNSound', :path => '../node_modules/react-native-sound' pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons' pod 'react-native-calendar-events', diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 281cfed2b..ae30eb1bb 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -7,6 +7,26 @@ PODS: - DoubleConversion - glog - glog (0.3.4) + - GoogleSignIn (4.2.0): + - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)" + - "GoogleToolboxForMac/NSString+URLArguments (~> 2.1)" + - GTMOAuth2 (~> 1.0) + - GTMSessionFetcher/Core (~> 1.1) + - GoogleToolboxForMac/DebugUtils (2.1.4): + - GoogleToolboxForMac/Defines (= 2.1.4) + - GoogleToolboxForMac/Defines (2.1.4) + - "GoogleToolboxForMac/NSDictionary+URLArguments (2.1.4)": + - GoogleToolboxForMac/DebugUtils (= 2.1.4) + - GoogleToolboxForMac/Defines (= 2.1.4) + - "GoogleToolboxForMac/NSString+URLArguments (= 2.1.4)" + - "GoogleToolboxForMac/NSString+URLArguments (2.1.4)" + - GTMOAuth2 (1.1.6): + - GTMSessionFetcher (~> 1.1) + - GTMSessionFetcher (1.2.0): + - GTMSessionFetcher/Full (= 1.2.0) + - GTMSessionFetcher/Core (1.2.0) + - GTMSessionFetcher/Full (1.2.0): + - GTMSessionFetcher/Core (= 1.2.0) - React (0.55.4): - React/Core (= 0.55.4) - react-native-background-timer (2.0.0): @@ -63,6 +83,9 @@ PODS: - React/Core - React/fishhook - React/RCTBlob + - RNGoogleSignin (1.0.0-rc3): + - GoogleSignIn + - React - RNSound (0.10.9): - React/Core - RNSound/Core (= 0.10.9) @@ -96,6 +119,7 @@ DEPENDENCIES: - React/RCTNetwork (from `../node_modules/react-native`) - React/RCTText (from `../node_modules/react-native`) - React/RCTWebSocket (from `../node_modules/react-native`) + - RNGoogleSignin (from `../node_modules/react-native-google-signin`) - RNSound (from `../node_modules/react-native-sound`) - RNVectorIcons (from `../node_modules/react-native-vector-icons`) - yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -104,6 +128,10 @@ SPEC REPOS: https://github.com/cocoapods/specs.git: - boost-for-react-native - FLAnimatedImage + - GoogleSignIn + - GoogleToolboxForMac + - GTMOAuth2 + - GTMSessionFetcher - SDWebImage EXTERNAL SOURCES: @@ -127,6 +155,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-locale-detector" react-native-webrtc: :path: "../node_modules/react-native-webrtc" + RNGoogleSignin: + :path: "../node_modules/react-native-google-signin" RNSound: :path: "../node_modules/react-native-sound" RNVectorIcons: @@ -140,6 +170,10 @@ SPEC CHECKSUMS: FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31 Folly: 211775e49d8da0ca658aebc8eab89d642935755c glog: 1de0bb937dccdc981596d3b5825ebfb765017ded + GoogleSignIn: 591e46382014e591269f862ba6e7bc0fbd793532 + GoogleToolboxForMac: 91c824d21e85b31c2aae9bb011c5027c9b4e738f + GTMOAuth2: c77fe325e4acd453837e72d91e3b5f13116857b2 + GTMSessionFetcher: 0c4baf0a73acd0041bf9f71ea018deedab5ea84e React: aa2040dbb6f317b95314968021bd2888816e03d5 react-native-background-timer: 63dcbf37dbcf294b5c6c071afcdc661fa06a7594 react-native-calendar-events: fe6fbc8ed337a7423c98f2c9012b25f20444de09 @@ -147,11 +181,12 @@ SPEC CHECKSUMS: react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94 react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1 react-native-webrtc: 31b6d3f1e3e2ce373aa43fd682b04367250f807d + RNGoogleSignin: 44debd8c359a662c0e2d585952e88b985bf78008 RNSound: b360b3862d3118ed1c74bb9825696b5957686ac4 RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44 SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681 yoga: a23273df0088bf7f2bb7e5d7b00044ea57a2a54a -PODFILE CHECKSUM: 69d3df0b8baa54d636bd653b412ed45db771a3b6 +PODFILE CHECKSUM: da74c08f6eb674668c49d8d799f8d9e2476a9fc5 COCOAPODS: 1.5.3 diff --git a/ios/app/src/Info.plist b/ios/app/src/Info.plist index ca7cd2ab8..7a4da56f1 100644 --- a/ios/app/src/Info.plist +++ b/ios/app/src/Info.plist @@ -32,6 +32,16 @@ org.jitsi.meet + + CFBundleTypeRole + Editor + CFBundleURLName + com.googleusercontent.apps + CFBundleURLSchemes + + com.googleusercontent.apps.YOUR_ID_HERE + + CFBundleVersion 1 diff --git a/ios/sdk/sdk.xcodeproj/project.pbxproj b/ios/sdk/sdk.xcodeproj/project.pbxproj index af0f612dd..0b3d5869b 100644 --- a/ios/sdk/sdk.xcodeproj/project.pbxproj +++ b/ios/sdk/sdk.xcodeproj/project.pbxproj @@ -376,6 +376,8 @@ ); inputPaths = ( "${SRCROOT}/../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet-resources.sh", + "${PODS_ROOT}/GTMOAuth2/Source/Touch/GTMOAuth2ViewTouch.xib", + "${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf", @@ -390,6 +392,8 @@ ); name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMOAuth2ViewTouch.nib", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Feather.ttf", diff --git a/lang/main.json b/lang/main.json index 6ca9a4c52..81df029b1 100644 --- a/lang/main.json +++ b/lang/main.json @@ -502,7 +502,9 @@ "on": "Live Streaming", "pending": "Starting Live Stream...", "serviceName": "Live Streaming service", + "signedInAs": "You are currently signed in as:", "signIn": "Sign in with Google", + "signOut": "Sign out", "signInCTA": "Sign in or enter your live stream key from YouTube.", "start": "Start a live stream", "streamIdHelp": "What's this?", diff --git a/package-lock.json b/package-lock.json index 858b28d0b..98966e056 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12763,6 +12763,11 @@ "prop-types": "^15.5.10" } }, + "react-native-google-signin": { + "version": "1.0.0-rc3", + "resolved": "https://registry.npmjs.org/react-native-google-signin/-/react-native-google-signin-1.0.0-rc3.tgz", + "integrity": "sha512-2isJRj262B+48hYRSAwL7feDdPEeiGkhwOE6MPbEkKButra5KJfP4ylcRO/XD99560XDK+/gMTp2ZPIKKCFKaQ==" + }, "react-native-immersive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/react-native-immersive/-/react-native-immersive-1.1.0.tgz", diff --git a/package.json b/package.json index a37b3177c..0b4fe48ac 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "react-native-calendar-events": "github:wmcmahan/react-native-calendar-events#cb2731db6684a49b4343e09de7f9c2fcc68bcd9b", "react-native-callstats": "3.52.0", "react-native-fast-image": "github:jitsi/react-native-fast-image#1f8c93a5584869848d75cc9b946beb9688efe285", + "react-native-google-signin": "1.0.0-rc3", "react-native-immersive": "1.1.0", "react-native-keep-awake": "2.0.6", "react-native-linear-gradient": "2.4.0", diff --git a/react/features/base/styles/components/styles/ColorPalette.js b/react/features/base/styles/components/styles/ColorPalette.js index 11ddef4e6..ac3e8bfd6 100644 --- a/react/features/base/styles/components/styles/ColorPalette.js +++ b/react/features/base/styles/components/styles/ColorPalette.js @@ -23,6 +23,8 @@ export const ColorPalette = { buttonUnderlay: '#495258', darkGrey: '#555555', green: '#40b183', + lightGrey: '#AAAAAA', + lighterGrey: '#EEEEEE', red: '#D00000', white: 'white', diff --git a/react/features/google-api/actions.js b/react/features/google-api/actions.js index 1cda39738..b26cb3602 100644 --- a/react/features/google-api/actions.js +++ b/react/features/google-api/actions.js @@ -40,15 +40,11 @@ export function loadGoogleAPI(clientId: string) { return Promise.resolve(); }) - .then(() => dispatch({ - type: SET_GOOGLE_API_STATE, - googleAPIState: GOOGLE_API_STATES.LOADED })) + .then(() => dispatch(setGoogleAPIState(GOOGLE_API_STATES.LOADED))) .then(() => googleApi.isSignedIn()) .then(isSignedIn => { if (isSignedIn) { - dispatch({ - type: SET_GOOGLE_API_STATE, - googleAPIState: GOOGLE_API_STATES.SIGNED_IN }); + dispatch(setGoogleAPIState(GOOGLE_API_STATES.SIGNED_IN)); } }); } @@ -115,6 +111,25 @@ export function requestLiveStreamsForYouTubeBroadcast(boundStreamID: string) { }); } +/** + * Sets the current Google API state. + * + * @param {number} googleAPIState - The state to be set. + * @param {Object} googleResponse - The last response from Google. + * @returns {{ + * type: SET_GOOGLE_API_STATE, + * googleAPIState: number + * }} + */ +export function setGoogleAPIState( + googleAPIState: number, googleResponse: ?Object) { + return { + type: SET_GOOGLE_API_STATE, + googleAPIState, + googleResponse + }; +} + /** * Forces the Google web client application to prompt for a sign in, such as * when changing account, and will then fetch available YouTube broadcasts. diff --git a/react/features/google-api/components/AbstractGoogleSignInButton.js b/react/features/google-api/components/AbstractGoogleSignInButton.js new file mode 100644 index 000000000..50dec27d8 --- /dev/null +++ b/react/features/google-api/components/AbstractGoogleSignInButton.js @@ -0,0 +1,34 @@ +// @flow + +import { Component } from 'react'; + +/** + * {@code AbstractGoogleSignInButton} component's property types. + */ +type Props = { + + /** + * The callback to invoke when the button is clicked. + */ + onClick: Function, + + /** + * True if the user is signed in, so it needs to render a different label + * and maybe different style (for the future). + */ + signedIn?: boolean, + + /** + * Function to be used to translate i18n labels. + */ + t: Function +}; + +/** + * Abstract class of the {@code GoogleSignInButton} to share platform + * independent code. + * + * @inheritdoc + */ +export default class AbstractGoogleSignInButton extends Component { +} diff --git a/react/features/google-api/components/GoogleSignInButton.native.js b/react/features/google-api/components/GoogleSignInButton.native.js index e69de29bb..fd237fe11 100644 --- a/react/features/google-api/components/GoogleSignInButton.native.js +++ b/react/features/google-api/components/GoogleSignInButton.native.js @@ -0,0 +1,62 @@ +// @flow + +import React from 'react'; +import { Image, Text, TouchableOpacity } from 'react-native'; + +import { translate } from '../../base/i18n'; + +import AbstractGoogleSignInButton from './AbstractGoogleSignInButton'; +import styles from './styles'; + +/** + * The Google Brand image for Sign In. + * + * NOTE: iOS doesn't handle the react-native-google-signin button component + * well due to our CocoaPods build process (the lib is not intended to be used + * this way), hence the custom button implementation. + */ +const GOOGLE_BRAND_IMAGE + = require('../../../../images/btn_google_signin_dark_normal.png'); + +/** + * A React Component showing a button to sign in with Google. + * + * @extends Component + */ +class GoogleSignInButton extends AbstractGoogleSignInButton { + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { onClick, signedIn, t } = this.props; + + if (signedIn) { + return ( + + + { t('liveStreaming.signOut') } + + + ); + } + + return ( + + + + ); + } +} + +export default translate(GoogleSignInButton); diff --git a/react/features/google-api/components/GoogleSignInButton.web.js b/react/features/google-api/components/GoogleSignInButton.web.js index 14380387b..d79824048 100644 --- a/react/features/google-api/components/GoogleSignInButton.web.js +++ b/react/features/google-api/components/GoogleSignInButton.web.js @@ -1,25 +1,18 @@ // @flow -import React, { Component } from 'react'; +import React from 'react'; -/** - * The type of the React {@code Component} props of {@link GoogleSignInButton}. - */ -type Props = { +import { translate } from '../../base/i18n'; - // The callback to invoke when {@code GoogleSignInButton} is clicked. - onClick: Function, - - // The text to display within {@code GoogleSignInButton}. - text: string -}; +import AbstractGoogleSignInButton from './AbstractGoogleSignInButton'; /** * A React Component showing a button to sign in with Google. * * @extends Component */ -export default class GoogleSignInButton extends Component { +class GoogleSignInButton extends AbstractGoogleSignInButton { + /** * Implements React's {@link Component#render()}. * @@ -27,6 +20,8 @@ export default class GoogleSignInButton extends Component { * @returns {ReactElement} */ render() { + const { t } = this.props; + return (
{ className = 'google-logo' src = 'images/googleLogo.svg' />
- { this.props.text } + { + t(this.props.signedIn + ? 'liveStreaming.signOut' + : 'liveStreaming.signIn') + }
); } } + +export default translate(GoogleSignInButton); diff --git a/react/features/google-api/components/index.js b/react/features/google-api/components/index.js index fa4409239..386614a23 100644 --- a/react/features/google-api/components/index.js +++ b/react/features/google-api/components/index.js @@ -1 +1,3 @@ +// @flow + export { default as GoogleSignInButton } from './GoogleSignInButton'; diff --git a/react/features/google-api/components/styles.js b/react/features/google-api/components/styles.js new file mode 100644 index 000000000..72158a779 --- /dev/null +++ b/react/features/google-api/components/styles.js @@ -0,0 +1,54 @@ +// @flow + +import { ColorPalette, createStyleSheet } from '../../base/styles'; + +/** + * For styling explanations, see: + * https://developers.google.com/identity/branding-guidelines + */ +const BUTTON_HEIGHT = 40; + +/** + * The styles of the React {@code Components} of google-api. + */ +export default createStyleSheet({ + + /** + * Image of the sign in button (Google branded). + */ + signInImage: { + flex: 1 + }, + + /** + * An image-based button for sign in. + */ + signInButton: { + alignItems: 'center', + height: BUTTON_HEIGHT, + justifyContent: 'center' + }, + + /** + * A text-based button for sign out (no sign out button guidance for + * Google). + */ + signOutButton: { + alignItems: 'center', + borderColor: ColorPalette.lightGrey, + borderRadius: 3, + borderWidth: 1, + height: BUTTON_HEIGHT, + justifyContent: 'center' + }, + + /** + * Text of the sign out button. + */ + signOutButtonText: { + color: ColorPalette.blue, + fontSize: 14, + fontWeight: 'bold' + } + +}); diff --git a/react/features/google-api/constants.js b/react/features/google-api/constants.js index ae3e35cf0..ce77b6a3a 100644 --- a/react/features/google-api/constants.js +++ b/react/features/google-api/constants.js @@ -1,14 +1,23 @@ // @flow /** - * The Google API scopes to request access for streaming and calendar. + * Google API URL to retreive streams for a live broadcast of a user. * - * @type {Array} + * NOTE: The URL must be appended by a broadcast ID returned by a call towards + * {@code API_URL_LIVE_BROADCASTS}. + * + * @type {string} */ -export const GOOGLE_API_SCOPES = [ - 'https://www.googleapis.com/auth/youtube.readonly', - 'https://www.googleapis.com/auth/calendar' -]; +// eslint-disable-next-line max-len +export const API_URL_BROADCAST_STREAMS = 'https://content.googleapis.com/youtube/v3/liveStreams?part=id%2Csnippet%2Ccdn%2Cstatus&id='; + +/** + * Google API URL to retreive live broadcasts of a user. + * + * @type {string} + */ +// eslint-disable-next-line max-len +export const API_URL_LIVE_BROADCASTS = 'https://content.googleapis.com/youtube/v3/liveBroadcasts?broadcastType=all&mine=true&part=id%2Csnippet%2CcontentDetails%2Cstatus'; /** * Array of API discovery doc URLs for APIs used by the googleApi. @@ -38,5 +47,26 @@ export const GOOGLE_API_STATES = { /** * The state in which a user has been logged in through the Google API. */ - SIGNED_IN: 2 + SIGNED_IN: 2, + + /** + * The state in which the Google authentication is not available (e.g. Play + * services are not installed on Android). + */ + NOT_AVAILABLE: 3 }; + +/** + * Google API auth scope to access Google calendar. + * + * @type {string} + */ +export const GOOGLE_SCOPE_CALENDAR = 'https://www.googleapis.com/auth/calendar'; + +/** + * Google API auth scope to access YouTube streams. + * + * @type {string} + */ +export const GOOGLE_SCOPE_YOUTUBE + = 'https://www.googleapis.com/auth/youtube.readonly'; diff --git a/react/features/google-api/googleApi.native.js b/react/features/google-api/googleApi.native.js new file mode 100644 index 000000000..f1fcc8f5b --- /dev/null +++ b/react/features/google-api/googleApi.native.js @@ -0,0 +1,173 @@ +// @flow + +import { + GoogleSignin +} from 'react-native-google-signin'; + +import { + API_URL_BROADCAST_STREAMS, + API_URL_LIVE_BROADCASTS +} from './constants'; + +/** + * Class to encapsulate Google API functionalities and provide a similar + * interface to what WEB has. The methods are different, but the point is that + * the export object is similar so no need for different export logic. + * + * For more detailed documentation of the {@code GoogleSignin} API, please visit + * https://github.com/react-native-community/react-native-google-signin. + */ +class GoogleApi { + /** + * Wraps the {@code GoogleSignin.configure} method. + * + * @param {Object} config - The config object to be passed to + * {@code GoogleSignin.configure}. + * @returns {void} + */ + configure(config: Object) { + GoogleSignin.configure(config); + } + + /** + * Retrieves the available YouTube streams the user can use for live + * streaming. + * + * @param {string} accessToken - The Google auth token. + * @returns {Promise} + */ + getYouTubeLiveStreams(accessToken: string): Promise<*> { + return new Promise((resolve, reject) => { + + // Fetching the list of available broadcasts first. + this._fetchGoogleEndpoint(accessToken, + API_URL_LIVE_BROADCASTS) + .then(broadcasts => { + // Then fetching all the available live streams that the + // user has access to with the broadcasts we retreived + // earlier. + this._getLiveStreamsForBroadcasts( + accessToken, broadcasts).then(resolve, reject); + }, reject); + }); + } + + /** + * Wraps the {@code GoogleSignin.hasPlayServices} method. + * + * @returns {Promise<*>} + */ + hasPlayServices() { + return GoogleSignin.hasPlayServices(); + } + + /** + * Wraps the {@code GoogleSignin.signIn} method. + * + * @returns {Promise<*>} + */ + signIn() { + return GoogleSignin.signIn(); + } + + /** + * Wraps the {@code GoogleSignin.signInSilently} method. + * + * @returns {Promise<*>} + */ + signInSilently() { + return GoogleSignin.signInSilently(); + } + + /** + * Wraps the {@code GoogleSignin.signOut} method. + * + * @returns {Promise<*>} + */ + signOut() { + return GoogleSignin.signOut(); + } + + /** + * Helper method to fetch a Google API endpoint in a generic way. + * + * @private + * @param {string} accessToken - The access token used for the API call. + * @param {string} endpoint - The endpoint to fetch, including the URL + * params if needed. + * @returns {Promise} + */ + _fetchGoogleEndpoint(accessToken, endpoint): Promise<*> { + return new Promise((resolve, reject) => { + const headers = { + Authorization: `Bearer ${accessToken}` + }; + + fetch(endpoint, { + headers + }).then(response => response.json()) + .then(responseJSON => { + if (responseJSON.error) { + reject(responseJSON.error.message); + } else { + resolve(responseJSON.items || []); + } + }, reject); + }); + } + + /** + * Retrieves the available YouTube streams that are available for the + * provided broadcast IDs. + * + * @private + * @param {string} accessToken - The Google access token. + * @param {Array} broadcasts - The list of broadcasts that we want + * to retreive streams for. + * @returns {Promise} + */ + _getLiveStreamsForBroadcasts(accessToken, broadcasts): Promise<*> { + return new Promise((resolve, reject) => { + const ids = []; + + for (const broadcast of broadcasts) { + broadcast.contentDetails + && broadcast.contentDetails.boundStreamId + && ids.push(broadcast.contentDetails.boundStreamId); + } + + this._fetchGoogleEndpoint( + accessToken, + `${API_URL_BROADCAST_STREAMS}${ids.join(',')}`) + .then(streams => { + const keys = []; + + // We construct an array of keys bind with the broadcast + // name for a nice display. + for (const stream of streams) { + const key = stream.cdn.ingestionInfo.streamName; + let title; + + // Finding title from the broadcast with the same + // channelId. If not found (unknown scenario), we use + // the key as title again. + for (const broadcast of broadcasts) { + if (broadcast.snippet.channelId + === stream.snippet.channelId) { + title = broadcast.snippet.title; + } + } + + keys.push({ + key, + title: title || key + }); + } + + resolve(keys); + }, reject); + }); + } +} + +export default new GoogleApi(); diff --git a/react/features/google-api/googleApi.js b/react/features/google-api/googleApi.web.js similarity index 89% rename from react/features/google-api/googleApi.js rename to react/features/google-api/googleApi.web.js index 878bc3310..6e6c5deb8 100644 --- a/react/features/google-api/googleApi.js +++ b/react/features/google-api/googleApi.web.js @@ -1,4 +1,10 @@ -import { GOOGLE_API_SCOPES, DISCOVERY_DOCS } from './constants'; +import { + API_URL_BROADCAST_STREAMS, + API_URL_LIVE_BROADCASTS, + DISCOVERY_DOCS, + GOOGLE_SCOPE_CALENDAR, + GOOGLE_SCOPE_YOUTUBE +} from './constants'; const GOOGLE_API_CLIENT_LIBRARY_URL = 'https://apis.google.com/js/api.js'; @@ -68,7 +74,10 @@ const googleApi = { api.client.init({ clientId, discoveryDocs: DISCOVERY_DOCS, - scope: GOOGLE_API_SCOPES.join(' ') + scope: [ + GOOGLE_SCOPE_CALENDAR, + GOOGLE_SCOPE_YOUTUBE + ].join(' ') }) .then(resolve) .catch(reject); @@ -137,10 +146,8 @@ const googleApi = { * @returns {Promise} */ requestAvailableYouTubeBroadcasts() { - const url = this._getURLForLiveBroadcasts(); - return this.get() - .then(api => api.client.request(url)); + .then(api => api.client.request(API_URL_LIVE_BROADCASTS)); }, /** @@ -152,10 +159,9 @@ const googleApi = { * @returns {Promise} */ requestLiveStreamsForYouTubeBroadcast(boundStreamID) { - const url = this._getURLForLiveStreams(boundStreamID); - return this.get() - .then(api => api.client.request(url)); + .then(api => api.client.request( + `${API_URL_BROADCAST_STREAMS}${boundStreamID}`)); }, /** @@ -353,37 +359,6 @@ const googleApi = { */ _getGoogleApiClient() { return window.gapi; - }, - - /** - * Returns the URL to the Google API endpoint for retrieving the currently - * signed in user's YouTube broadcasts. - * - * @private - * @returns {string} - */ - _getURLForLiveBroadcasts() { - return [ - 'https://content.googleapis.com/youtube/v3/liveBroadcasts', - '?broadcastType=all', - '&mine=true&part=id%2Csnippet%2CcontentDetails%2Cstatus' - ].join(''); - }, - - /** - * Returns the URL to the Google API endpoint for retrieving the live - * streams associated with a YouTube broadcast's bound stream. - * - * @param {string} boundStreamID - The bound stream ID associated with a - * broadcast in YouTube. - * @returns {string} - */ - _getURLForLiveStreams(boundStreamID) { - return [ - 'https://content.googleapis.com/youtube/v3/liveStreams', - '?part=id%2Csnippet%2Ccdn%2Cstatus', - `&id=${boundStreamID}` - ].join(''); } }; diff --git a/react/features/google-api/index.js b/react/features/google-api/index.js index 6663f5dde..085a37171 100644 --- a/react/features/google-api/index.js +++ b/react/features/google-api/index.js @@ -1,6 +1,8 @@ -export { GOOGLE_API_STATES } from './constants'; -export { default as googleApi } from './googleApi'; +// @flow + export * from './actions'; export * from './components'; +export * from './constants'; +export { default as googleApi } from './googleApi'; import './reducer'; diff --git a/react/features/google-api/reducer.js b/react/features/google-api/reducer.js index 3e48e30e8..c0297ec03 100644 --- a/react/features/google-api/reducer.js +++ b/react/features/google-api/reducer.js @@ -27,7 +27,8 @@ ReducerRegistry.register('features/google-api', case SET_GOOGLE_API_STATE: return { ...state, - googleAPIState: action.googleAPIState + googleAPIState: action.googleAPIState, + googleResponse: action.googleResponse }; case SET_GOOGLE_API_PROFILE: return { diff --git a/react/features/recording/components/LiveStream/AbstractStartLiveStreamDialog.js b/react/features/recording/components/LiveStream/AbstractStartLiveStreamDialog.js index bad3bab84..21818ee9c 100644 --- a/react/features/recording/components/LiveStream/AbstractStartLiveStreamDialog.js +++ b/react/features/recording/components/LiveStream/AbstractStartLiveStreamDialog.js @@ -91,8 +91,8 @@ export type State = { * but the abstraction of its properties are already present in this abstract * class. */ -export default class AbstractStartLiveStreamDialog - extends Component { +export default class AbstractStartLiveStreamDialog + 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, + + /** + * Callback to be invoked when the user picked a broadcast. To be invoked + * with a single key (string). + */ + onChange: Function, + + /** + * Function to be used to translate i18n labels. + */ + t: Function +} + +type State = { + + /** + * The key of the currently selected stream. + */ + streamKey: ?string +} + +/** + * Class to implement a stream key picker (dropdown) component to allow the user + * to choose from the available Google Broadcasts/Streams. + * + * NOTE: This component is currently only used on mobile, but it is advised at + * a later point to unify mobile and web logic for this functionality. But it's + * out of the scope for now of the mobile live streaming functionality. + */ +class StreamKeyPicker extends Component { + + /** + * Instantiates a new instance of StreamKeyPicker. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this.state = { + streamKey: null + }; + + this._onStreamPick = this._onStreamPick.bind(this); + } + + /** + * Renders the component. + * + * @inheritdoc + */ + render() { + const { broadcasts } = this.props; + + if (!broadcasts || !broadcasts.length) { + return null; + } + + return ( + + + + { this.props.t('liveStreaming.choose') } + + + + { broadcasts.map((broadcast, index) => + ( + + { broadcast.title } + + )) + } + + + ); + } + + _onStreamPick: string => Function + + /** + * Callback to be invoked when the user picks a stream from the list. + * + * @private + * @param {string} streamKey - The key of the stream selected. + * @returns {Function} + */ + _onStreamPick(streamKey) { + return () => { + this.setState({ + streamKey + }); + this.props.onChange(streamKey); + }; + } +} + +export default translate(StreamKeyPicker); diff --git a/react/features/recording/components/LiveStream/BroadcastsDropdown.web.js b/react/features/recording/components/LiveStream/StreamKeyPicker.web.js similarity index 94% rename from react/features/recording/components/LiveStream/BroadcastsDropdown.web.js rename to react/features/recording/components/LiveStream/StreamKeyPicker.web.js index 07c4e7e3d..e38e395d3 100644 --- a/react/features/recording/components/LiveStream/BroadcastsDropdown.web.js +++ b/react/features/recording/components/LiveStream/StreamKeyPicker.web.js @@ -13,7 +13,7 @@ import { translate } from '../../../base/i18n'; * * @extends Component */ -class BroadcastsDropdown extends PureComponent { +class StreamKeyPicker extends PureComponent { /** * Default values for {@code StreamKeyForm} component's properties. * @@ -24,7 +24,7 @@ class BroadcastsDropdown extends PureComponent { }; /** - * {@code BroadcastsDropdown} component's property types. + * {@code StreamKeyPicker} component's property types. */ static propTypes = { /** @@ -64,10 +64,10 @@ class BroadcastsDropdown extends PureComponent { }; /** - * Initializes a new {@code BroadcastsDropdown} instance. + * Initializes a new {@code StreamKeyPicker} instance. * * @param {Props} props - The React {@code Component} props to initialize - * the new {@code BroadcastsDropdown} instance with. + * the new {@code StreamKeyPicker} instance with. */ constructor(props) { super(props); @@ -166,4 +166,4 @@ class BroadcastsDropdown extends PureComponent { } } -export default translate(BroadcastsDropdown); +export default translate(StreamKeyPicker); diff --git a/react/features/recording/components/LiveStream/styles.native.js b/react/features/recording/components/LiveStream/styles.native.js index a4e15fcf7..07fc2a17c 100644 --- a/react/features/recording/components/LiveStream/styles.native.js +++ b/react/features/recording/components/LiveStream/styles.native.js @@ -2,6 +2,16 @@ import { BoxModel, ColorPalette, createStyleSheet } from '../../../base/styles'; +/** + * Opacity of the TouchableHighlight. + */ +export const ACTIVE_OPACITY = 0.3; + +/** + * Underlay of the TouchableHighlight. + */ +export const TOUCHABLE_UNDERLAY = ColorPalette.lightGrey; + /** * The styles of the React {@code Components} of LiveStream. */ @@ -20,22 +30,91 @@ export default createStyleSheet({ fontWeight: 'bold' }, - streamKeyFormWrapper: { + /** + * Generic component to wrap form sections into achieving a unified look. + */ + formWrapper: { + alignItems: 'stretch', flexDirection: 'column', padding: BoxModel.padding }, + /** + * Explaining text on the top of the sign in form. + */ + helpText: { + marginBottom: BoxModel.margin + }, + + /** + * Wrapper for the StartLiveStreamDialog form. + */ + startDialogWrapper: { + flexDirection: 'column' + }, + + /** + * Helper link text. + */ streamKeyHelp: { alignSelf: 'flex-end' }, + /** + * Input field to manually enter stream key. + */ streamKeyInput: { alignSelf: 'stretch', height: 50 }, + /** + * Label for the previous field. + */ streamKeyInputLabel: { alignSelf: 'flex-start' + }, + + /** + * Custom component to pick a broadcast from the list fetched from Google. + */ + streamKeyPicker: { + alignSelf: 'stretch', + flex: 1, + height: 40, + marginHorizontal: 4, + width: 300 + }, + + /** + * CTA (label) of the picker. + */ + streamKeyPickerCta: { + marginBottom: 8 + }, + + /** + * Style of a single item in the list. + */ + streamKeyPickerItem: { + padding: 4 + }, + + /** + * Additional style for the selected item. + */ + streamKeyPickerItemHighlight: { + backgroundColor: ColorPalette.lighterGrey + }, + + /** + * Overall wrapper for the picker. + */ + streamKeyPickerWrapper: { + borderColor: ColorPalette.lightGrey, + borderRadius: 3, + borderWidth: 1, + flexDirection: 'column' } }); diff --git a/react/features/recording/reducer.js b/react/features/recording/reducer.js index c5ba2c906..486036c75 100644 --- a/react/features/recording/reducer.js +++ b/react/features/recording/reducer.js @@ -1,5 +1,4 @@ import { ReducerRegistry } from '../base/redux'; -import { PersistenceRegistry } from '../base/storage'; import { CLEAR_RECORDING_SESSIONS, RECORDING_SESSION_UPDATED, @@ -17,13 +16,6 @@ const DEFAULT_STATE = { */ const STORE_NAME = 'features/recording'; -/** - * Sets up the persistence of the feature {@code recording}. - */ -PersistenceRegistry.register(STORE_NAME, { - streamKey: true -}, DEFAULT_STATE); - /** * Reduces the Redux actions of the feature features/recording. */