[RN] Add Google Sign In to live streaming
This commit is contained in:
parent
9fe2b834eb
commit
d10d61fb7a
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -400,6 +400,7 @@ var config = {
|
|||
externalConnectUrl
|
||||
firefox_fake_device
|
||||
googleApiApplicationClientID
|
||||
googleApiIOSClientID
|
||||
iAmRecorder
|
||||
iAmSipGateway
|
||||
microsoftApiApplicationClientID
|
||||
|
|
|
@ -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.
|
Binary file not shown.
After Width: | Height: | Size: 7.8 KiB |
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -32,6 +32,16 @@
|
|||
<string>org.jitsi.meet</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.googleusercontent.apps</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.googleusercontent.apps.YOUR_ID_HERE</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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?",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -23,6 +23,8 @@ export const ColorPalette = {
|
|||
buttonUnderlay: '#495258',
|
||||
darkGrey: '#555555',
|
||||
green: '#40b183',
|
||||
lightGrey: '#AAAAAA',
|
||||
lighterGrey: '#EEEEEE',
|
||||
red: '#D00000',
|
||||
white: 'white',
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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<Props> {
|
||||
}
|
|
@ -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 (
|
||||
<TouchableOpacity
|
||||
onPress = { onClick }
|
||||
style = { styles.signOutButton } >
|
||||
<Text style = { styles.signOutButtonText }>
|
||||
{ t('liveStreaming.signOut') }
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress = { onClick }
|
||||
style = { styles.signInButton } >
|
||||
<Image
|
||||
resizeMode = { 'contain' }
|
||||
source = { GOOGLE_BRAND_IMAGE }
|
||||
style = { styles.signInImage } />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(GoogleSignInButton);
|
|
@ -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<Props> {
|
||||
class GoogleSignInButton extends AbstractGoogleSignInButton {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
|
@ -27,6 +20,8 @@ export default class GoogleSignInButton extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'google-sign-in'
|
||||
|
@ -35,9 +30,15 @@ export default class GoogleSignInButton extends Component<Props> {
|
|||
className = 'google-logo'
|
||||
src = 'images/googleLogo.svg' />
|
||||
<div className = 'google-cta'>
|
||||
{ this.props.text }
|
||||
{
|
||||
t(this.props.signedIn
|
||||
? 'liveStreaming.signOut'
|
||||
: 'liveStreaming.signIn')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(GoogleSignInButton);
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export { default as GoogleSignInButton } from './GoogleSignInButton';
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
||||
});
|
|
@ -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<string>}
|
||||
* 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';
|
||||
|
|
|
@ -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<Object>} 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();
|
|
@ -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('');
|
||||
}
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<Props, State> {
|
||||
export default class AbstractStartLiveStreamDialog<P: Props>
|
||||
extends Component<P, State> {
|
||||
_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
|
||||
* }}
|
||||
*/
|
||||
|
|
|
@ -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<Props> {
|
||||
/**
|
||||
* 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 (
|
||||
<View style = { styles.formWrapper }>
|
||||
<View style = { styles.helpText }>
|
||||
{ signedInUser ? <Text>
|
||||
{ `${t('liveStreaming.signedInAs')} ${signedInUser}` }
|
||||
</Text> : <Text>
|
||||
{ t('liveStreaming.signInCTA') }
|
||||
</Text> }
|
||||
</View>
|
||||
<GoogleSignInButton
|
||||
onClick = { this._onGoogleButtonPress }
|
||||
signedIn = {
|
||||
googleAPIState === GOOGLE_API_STATES.SIGNED_IN } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_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));
|
|
@ -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<Props> {
|
||||
/**
|
||||
* 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 (
|
||||
<View>
|
||||
<View style = { styles.startDialogWrapper }>
|
||||
<GoogleSigninForm
|
||||
clientId = { this.props._googleApiApplicationClientID }
|
||||
iOSClientId = { this.props._googleApiIOSClientID }
|
||||
onUserChanged = { this._onUserChanged } />
|
||||
<StreamKeyPicker
|
||||
broadcasts = { this.state.broadcasts }
|
||||
onChange = { this._onStreamKeyPick } />
|
||||
<StreamKeyForm
|
||||
onChange = { this._onStreamKeyChangeNative }
|
||||
value = { this.props._streamKey } />
|
||||
value = { this.state.streamKey || this.props._streamKey } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
|
|
|
@ -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<Props> {
|
||||
|
||||
/**
|
||||
* 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 = (
|
||||
<GoogleSignInButton
|
||||
onClick = { this._onGoogleSignIn }
|
||||
text = { t('liveStreaming.signIn') } />
|
||||
);
|
||||
googleContent
|
||||
= <GoogleSignInButton onClick = { this._onGoogleSignIn } />;
|
||||
helpText = t('liveStreaming.signInCTA');
|
||||
|
||||
break;
|
||||
|
||||
case GOOGLE_API_STATES.SIGNED_IN:
|
||||
googleContent = (
|
||||
<BroadcastsDropdown
|
||||
<StreamKeyPicker
|
||||
broadcasts = { broadcasts }
|
||||
onBroadcastSelected = { this._onYouTubeBroadcastIDSelected }
|
||||
selectedBoundStreamID = { selectedBoundStreamID } />
|
||||
|
@ -285,8 +297,7 @@ class StartLiveStreamDialog
|
|||
if (this.state.errorType !== undefined) {
|
||||
googleContent = (
|
||||
<GoogleSignInButton
|
||||
onClick = { this._onRequestGoogleSignIn }
|
||||
text = { t('liveStreaming.signIn') } />
|
||||
onClick = { this._onRequestGoogleSignIn } />
|
||||
);
|
||||
helpText = this._getGoogleErrorMessageToDisplay();
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ class StreamKeyForm extends AbstractStreamKeyForm {
|
|||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<View style = { styles.streamKeyFormWrapper }>
|
||||
<View style = { styles.formWrapper }>
|
||||
<Text style = { styles.streamKeyInputLabel }>
|
||||
{
|
||||
t('dialog.streamKey')
|
||||
|
|
|
@ -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<Object>,
|
||||
|
||||
/**
|
||||
* 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<Props, State> {
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<View style = { styles.formWrapper }>
|
||||
<View style = { styles.streamKeyPickerCta }>
|
||||
<Text>
|
||||
{ this.props.t('liveStreaming.choose') }
|
||||
</Text>
|
||||
</View>
|
||||
<View style = { styles.streamKeyPickerWrapper } >
|
||||
{ broadcasts.map((broadcast, index) =>
|
||||
(<TouchableHighlight
|
||||
activeOpacity = { ACTIVE_OPACITY }
|
||||
key = { index }
|
||||
onPress = { this._onStreamPick(broadcast.key) }
|
||||
style = { [
|
||||
styles.streamKeyPickerItem,
|
||||
this.state.streamKey === broadcast.key
|
||||
? styles.streamKeyPickerItemHighlight : null
|
||||
] }
|
||||
underlayColor = { TOUCHABLE_UNDERLAY }>
|
||||
<Text style = { styles.streamKeyPickerItemText }>
|
||||
{ broadcast.title }
|
||||
</Text>
|
||||
</TouchableHighlight>))
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_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);
|
|
@ -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);
|
|
@ -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'
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue