Przeglądaj źródła

[RN] Add Google Sign In to live streaming

master
Bettenbuk Zoltan 6 lat temu
rodzic
commit
d10d61fb7a
37 zmienionych plików z 1091 dodań i 129 usunięć
  1. 6
    0
      android/app/build.gradle
  2. 2
    0
      android/build.gradle
  3. 3
    0
      android/sdk/build.gradle
  4. 1
    0
      android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java
  5. 2
    0
      android/settings.gradle
  6. 1
    0
      config.js
  7. 22
    0
      doc/mobile-google-auth.md
  8. BIN
      images/btn_google_signin_dark_normal.png
  9. 2
    0
      ios/Podfile
  10. 36
    1
      ios/Podfile.lock
  11. 10
    0
      ios/app/src/Info.plist
  12. 4
    0
      ios/sdk/sdk.xcodeproj/project.pbxproj
  13. 2
    0
      lang/main.json
  14. 5
    0
      package-lock.json
  15. 1
    0
      package.json
  16. 2
    0
      react/features/base/styles/components/styles/ColorPalette.js
  17. 21
    6
      react/features/google-api/actions.js
  18. 34
    0
      react/features/google-api/components/AbstractGoogleSignInButton.js
  19. 62
    0
      react/features/google-api/components/GoogleSignInButton.native.js
  20. 14
    13
      react/features/google-api/components/GoogleSignInButton.web.js
  21. 2
    0
      react/features/google-api/components/index.js
  22. 54
    0
      react/features/google-api/components/styles.js
  23. 37
    7
      react/features/google-api/constants.js
  24. 173
    0
      react/features/google-api/googleApi.native.js
  25. 14
    39
      react/features/google-api/googleApi.web.js
  26. 4
    2
      react/features/google-api/index.js
  27. 2
    1
      react/features/google-api/reducer.js
  28. 5
    14
      react/features/recording/components/LiveStream/AbstractStartLiveStreamDialog.js
  29. 254
    0
      react/features/recording/components/LiveStream/GoogleSigninForm.native.js
  30. 0
    0
      react/features/recording/components/LiveStream/GoogleSigninForm.web.js
  31. 87
    21
      react/features/recording/components/LiveStream/StartLiveStreamDialog.native.js
  32. 21
    10
      react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js
  33. 1
    1
      react/features/recording/components/LiveStream/StreamKeyForm.native.js
  34. 122
    0
      react/features/recording/components/LiveStream/StreamKeyPicker.native.js
  35. 5
    5
      react/features/recording/components/LiveStream/StreamKeyPicker.web.js
  36. 80
    1
      react/features/recording/components/LiveStream/styles.native.js
  37. 0
    8
      react/features/recording/reducer.js

+ 6
- 0
android/app/build.gradle Wyświetl plik

@@ -41,9 +41,15 @@ android {
41 41
 
42 42
 dependencies {
43 43
     compile fileTree(dir: 'libs', include: ['*.jar'])
44
+    compile 'com.android.support:appcompat-v7:22.2.0'
45
+    compile 'com.google.android.gms:play-services-auth:15.0.0'
44 46
 
45 47
     implementation project(':sdk')
46 48
 
47 49
     debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
48 50
     releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
49 51
 }
52
+
53
+if (project.file('google-services.json').exists()) {
54
+   apply plugin: 'com.google.gms.google-services'
55
+}

+ 2
- 0
android/build.gradle Wyświetl plik

@@ -8,6 +8,7 @@ buildscript {
8 8
     }
9 9
     dependencies {
10 10
         classpath 'com.android.tools.build:gradle:3.0.1'
11
+        classpath 'com.google.gms:google-services:3.2.1'
11 12
 
12 13
         // NOTE: Do not place your application dependencies here; they belong
13 14
         // in the individual module build.gradle files.
@@ -16,6 +17,7 @@ buildscript {
16 17
 
17 18
 allprojects {
18 19
     repositories {
20
+        maven { url "https://maven.google.com" }
19 21
         google()
20 22
         jcenter()
21 23
         maven { url "$rootDir/../node_modules/jsc-android/dist" }

+ 3
- 0
android/sdk/build.gradle Wyświetl plik

@@ -26,6 +26,9 @@ dependencies {
26 26
 
27 27
     compile project(':react-native-background-timer')
28 28
     compile project(':react-native-fast-image')
29
+    compile(project(":react-native-google-signin")) {
30
+        exclude group: 'com.google.android.gms'
31
+    }
29 32
     compile project(':react-native-immersive')
30 33
     compile project(':react-native-keep-awake')
31 34
     compile project(':react-native-linear-gradient')

+ 1
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java Wyświetl plik

@@ -119,6 +119,7 @@ class ReactInstanceManagerHolder {
119 119
                 .setApplication(application)
120 120
                 .setBundleAssetName("index.android.bundle")
121 121
                 .setJSMainModulePath("index.android")
122
+                .addPackage(new co.apptailor.googlesignin.RNGoogleSigninPackage())
122 123
                 .addPackage(new com.BV.LinearGradient.LinearGradientPackage())
123 124
                 .addPackage(new com.calendarevents.CalendarEventsPackage())
124 125
                 .addPackage(new com.corbt.keepawake.KCKeepAwakePackage())

+ 2
- 0
android/settings.gradle Wyświetl plik

@@ -5,6 +5,8 @@ include ':react-native-background-timer'
5 5
 project(':react-native-background-timer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-timer/android')
6 6
 include ':react-native-fast-image'
7 7
 project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fast-image/android')
8
+include ':react-native-google-signin'
9
+project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-google-signin/android')
8 10
 include ':react-native-immersive'
9 11
 project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
10 12
 include ':react-native-keep-awake'

+ 1
- 0
config.js Wyświetl plik

@@ -400,6 +400,7 @@ var config = {
400 400
      externalConnectUrl
401 401
      firefox_fake_device
402 402
      googleApiApplicationClientID
403
+     googleApiIOSClientID
403 404
      iAmRecorder
404 405
      iAmSipGateway
405 406
      microsoftApiApplicationClientID

+ 22
- 0
doc/mobile-google-auth.md Wyświetl plik

@@ -0,0 +1,22 @@
1
+# Setting up Google Authentication
2
+
3
+- Create a Firebase project here: https://firebase.google.com/. You'll need a
4
+signed Android build for that, that can be a debug auto-signed build too, just
5
+retrieve the signing hash.
6
+- Place the generated ```google-services.json``` file in ```android/app```
7
+for Android and the ```GoogleService-Info.plist``` into ```ios/app/src``` for
8
+iOS (you can stop at that step, no need for the driver and the code changes they
9
+suggest in the wizard).
10
+- You may want to exclude these files in YOUR GIT config (do not exclude them in
11
+the ```.gitignore``` of the application itself!).
12
+- Your WEB and iOS client IDs are auto generated during the Firebase project
13
+ creation. Find them in the Google Developer console:
14
+ https://console.developers.google.com/
15
+- Make sure your config reflects these IDs so then the Redux state of the
16
+ feature ```features/base/config``` contains variables
17
+ ```googleApiApplicationClientID``` and ```googleApiIOSClientID``` with the
18
+ respective values.
19
+- Add your iOS client ID as an application URL schema into
20
+```ios/app/src/Info.plist``` (replacing placeholder).
21
+- Enable YouTube API access on the developer console (see above) for live
22
+streaming.

BIN
images/btn_google_signin_dark_normal.png Wyświetl plik


+ 2
- 0
ios/Podfile Wyświetl plik

@@ -35,6 +35,8 @@ target 'JitsiMeet' do
35 35
   pod 'react-native-locale-detector',
36 36
     :path => '../node_modules/react-native-locale-detector'
37 37
   pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
38
+  pod 'RNGoogleSignin',
39
+    :path => '../node_modules/react-native-google-signin'
38 40
   pod 'RNSound', :path => '../node_modules/react-native-sound'
39 41
   pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
40 42
   pod 'react-native-calendar-events',

+ 36
- 1
ios/Podfile.lock Wyświetl plik

@@ -7,6 +7,26 @@ PODS:
7 7
     - DoubleConversion
8 8
     - glog
9 9
   - glog (0.3.4)
10
+  - GoogleSignIn (4.2.0):
11
+    - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)"
12
+    - "GoogleToolboxForMac/NSString+URLArguments (~> 2.1)"
13
+    - GTMOAuth2 (~> 1.0)
14
+    - GTMSessionFetcher/Core (~> 1.1)
15
+  - GoogleToolboxForMac/DebugUtils (2.1.4):
16
+    - GoogleToolboxForMac/Defines (= 2.1.4)
17
+  - GoogleToolboxForMac/Defines (2.1.4)
18
+  - "GoogleToolboxForMac/NSDictionary+URLArguments (2.1.4)":
19
+    - GoogleToolboxForMac/DebugUtils (= 2.1.4)
20
+    - GoogleToolboxForMac/Defines (= 2.1.4)
21
+    - "GoogleToolboxForMac/NSString+URLArguments (= 2.1.4)"
22
+  - "GoogleToolboxForMac/NSString+URLArguments (2.1.4)"
23
+  - GTMOAuth2 (1.1.6):
24
+    - GTMSessionFetcher (~> 1.1)
25
+  - GTMSessionFetcher (1.2.0):
26
+    - GTMSessionFetcher/Full (= 1.2.0)
27
+  - GTMSessionFetcher/Core (1.2.0)
28
+  - GTMSessionFetcher/Full (1.2.0):
29
+    - GTMSessionFetcher/Core (= 1.2.0)
10 30
   - React (0.55.4):
11 31
     - React/Core (= 0.55.4)
12 32
   - react-native-background-timer (2.0.0):
@@ -63,6 +83,9 @@ PODS:
63 83
     - React/Core
64 84
     - React/fishhook
65 85
     - React/RCTBlob
86
+  - RNGoogleSignin (1.0.0-rc3):
87
+    - GoogleSignIn
88
+    - React
66 89
   - RNSound (0.10.9):
67 90
     - React/Core
68 91
     - RNSound/Core (= 0.10.9)
@@ -96,6 +119,7 @@ DEPENDENCIES:
96 119
   - React/RCTNetwork (from `../node_modules/react-native`)
97 120
   - React/RCTText (from `../node_modules/react-native`)
98 121
   - React/RCTWebSocket (from `../node_modules/react-native`)
122
+  - RNGoogleSignin (from `../node_modules/react-native-google-signin`)
99 123
   - RNSound (from `../node_modules/react-native-sound`)
100 124
   - RNVectorIcons (from `../node_modules/react-native-vector-icons`)
101 125
   - yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -104,6 +128,10 @@ SPEC REPOS:
104 128
   https://github.com/cocoapods/specs.git:
105 129
     - boost-for-react-native
106 130
     - FLAnimatedImage
131
+    - GoogleSignIn
132
+    - GoogleToolboxForMac
133
+    - GTMOAuth2
134
+    - GTMSessionFetcher
107 135
     - SDWebImage
108 136
 
109 137
 EXTERNAL SOURCES:
@@ -127,6 +155,8 @@ EXTERNAL SOURCES:
127 155
     :path: "../node_modules/react-native-locale-detector"
128 156
   react-native-webrtc:
129 157
     :path: "../node_modules/react-native-webrtc"
158
+  RNGoogleSignin:
159
+    :path: "../node_modules/react-native-google-signin"
130 160
   RNSound:
131 161
     :path: "../node_modules/react-native-sound"
132 162
   RNVectorIcons:
@@ -140,6 +170,10 @@ SPEC CHECKSUMS:
140 170
   FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
141 171
   Folly: 211775e49d8da0ca658aebc8eab89d642935755c
142 172
   glog: 1de0bb937dccdc981596d3b5825ebfb765017ded
173
+  GoogleSignIn: 591e46382014e591269f862ba6e7bc0fbd793532
174
+  GoogleToolboxForMac: 91c824d21e85b31c2aae9bb011c5027c9b4e738f
175
+  GTMOAuth2: c77fe325e4acd453837e72d91e3b5f13116857b2
176
+  GTMSessionFetcher: 0c4baf0a73acd0041bf9f71ea018deedab5ea84e
143 177
   React: aa2040dbb6f317b95314968021bd2888816e03d5
144 178
   react-native-background-timer: 63dcbf37dbcf294b5c6c071afcdc661fa06a7594
145 179
   react-native-calendar-events: fe6fbc8ed337a7423c98f2c9012b25f20444de09
@@ -147,11 +181,12 @@ SPEC CHECKSUMS:
147 181
   react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94
148 182
   react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1
149 183
   react-native-webrtc: 31b6d3f1e3e2ce373aa43fd682b04367250f807d
184
+  RNGoogleSignin: 44debd8c359a662c0e2d585952e88b985bf78008
150 185
   RNSound: b360b3862d3118ed1c74bb9825696b5957686ac4
151 186
   RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
152 187
   SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681
153 188
   yoga: a23273df0088bf7f2bb7e5d7b00044ea57a2a54a
154 189
 
155
-PODFILE CHECKSUM: 69d3df0b8baa54d636bd653b412ed45db771a3b6
190
+PODFILE CHECKSUM: da74c08f6eb674668c49d8d799f8d9e2476a9fc5
156 191
 
157 192
 COCOAPODS: 1.5.3

+ 10
- 0
ios/app/src/Info.plist Wyświetl plik

@@ -32,6 +32,16 @@
32 32
 				<string>org.jitsi.meet</string>
33 33
 			</array>
34 34
 		</dict>
35
+		<dict>
36
+			<key>CFBundleTypeRole</key>
37
+			<string>Editor</string>
38
+			<key>CFBundleURLName</key>
39
+			<string>com.googleusercontent.apps</string>
40
+			<key>CFBundleURLSchemes</key>
41
+			<array>
42
+				<string>com.googleusercontent.apps.YOUR_ID_HERE</string>
43
+			</array>
44
+		</dict>
35 45
 	</array>
36 46
 	<key>CFBundleVersion</key>
37 47
 	<string>1</string>

+ 4
- 0
ios/sdk/sdk.xcodeproj/project.pbxproj Wyświetl plik

@@ -376,6 +376,8 @@
376 376
 			);
377 377
 			inputPaths = (
378 378
 				"${SRCROOT}/../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet-resources.sh",
379
+				"${PODS_ROOT}/GTMOAuth2/Source/Touch/GTMOAuth2ViewTouch.xib",
380
+				"${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle",
379 381
 				"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
380 382
 				"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
381 383
 				"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf",
@@ -390,6 +392,8 @@
390 392
 			);
391 393
 			name = "[CP] Copy Pods Resources";
392 394
 			outputPaths = (
395
+				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMOAuth2ViewTouch.nib",
396
+				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
393 397
 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
394 398
 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
395 399
 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Feather.ttf",

+ 2
- 0
lang/main.json Wyświetl plik

@@ -502,7 +502,9 @@
502 502
         "on": "Live Streaming",
503 503
         "pending": "Starting Live Stream...",
504 504
         "serviceName": "Live Streaming service",
505
+        "signedInAs": "You are currently signed in as:",
505 506
         "signIn": "Sign in with Google",
507
+        "signOut": "Sign out",
506 508
         "signInCTA": "Sign in or enter your live stream key from YouTube.",
507 509
         "start": "Start a live stream",
508 510
         "streamIdHelp": "What's this?",

+ 5
- 0
package-lock.json Wyświetl plik

@@ -12763,6 +12763,11 @@
12763 12763
         "prop-types": "^15.5.10"
12764 12764
       }
12765 12765
     },
12766
+    "react-native-google-signin": {
12767
+      "version": "1.0.0-rc3",
12768
+      "resolved": "https://registry.npmjs.org/react-native-google-signin/-/react-native-google-signin-1.0.0-rc3.tgz",
12769
+      "integrity": "sha512-2isJRj262B+48hYRSAwL7feDdPEeiGkhwOE6MPbEkKButra5KJfP4ylcRO/XD99560XDK+/gMTp2ZPIKKCFKaQ=="
12770
+    },
12766 12771
     "react-native-immersive": {
12767 12772
       "version": "1.1.0",
12768 12773
       "resolved": "https://registry.npmjs.org/react-native-immersive/-/react-native-immersive-1.1.0.tgz",

+ 1
- 0
package.json Wyświetl plik

@@ -65,6 +65,7 @@
65 65
     "react-native-calendar-events": "github:wmcmahan/react-native-calendar-events#cb2731db6684a49b4343e09de7f9c2fcc68bcd9b",
66 66
     "react-native-callstats": "3.52.0",
67 67
     "react-native-fast-image": "github:jitsi/react-native-fast-image#1f8c93a5584869848d75cc9b946beb9688efe285",
68
+    "react-native-google-signin": "1.0.0-rc3",
68 69
     "react-native-immersive": "1.1.0",
69 70
     "react-native-keep-awake": "2.0.6",
70 71
     "react-native-linear-gradient": "2.4.0",

+ 2
- 0
react/features/base/styles/components/styles/ColorPalette.js Wyświetl plik

@@ -23,6 +23,8 @@ export const ColorPalette = {
23 23
     buttonUnderlay: '#495258',
24 24
     darkGrey: '#555555',
25 25
     green: '#40b183',
26
+    lightGrey: '#AAAAAA',
27
+    lighterGrey: '#EEEEEE',
26 28
     red: '#D00000',
27 29
     white: 'white',
28 30
 

+ 21
- 6
react/features/google-api/actions.js Wyświetl plik

@@ -40,15 +40,11 @@ export function loadGoogleAPI(clientId: string) {
40 40
 
41 41
             return Promise.resolve();
42 42
         })
43
-        .then(() => dispatch({
44
-            type: SET_GOOGLE_API_STATE,
45
-            googleAPIState: GOOGLE_API_STATES.LOADED }))
43
+        .then(() => dispatch(setGoogleAPIState(GOOGLE_API_STATES.LOADED)))
46 44
         .then(() => googleApi.isSignedIn())
47 45
         .then(isSignedIn => {
48 46
             if (isSignedIn) {
49
-                dispatch({
50
-                    type: SET_GOOGLE_API_STATE,
51
-                    googleAPIState: GOOGLE_API_STATES.SIGNED_IN });
47
+                dispatch(setGoogleAPIState(GOOGLE_API_STATES.SIGNED_IN));
52 48
             }
53 49
         });
54 50
 }
@@ -115,6 +111,25 @@ export function requestLiveStreamsForYouTubeBroadcast(boundStreamID: string) {
115 111
             });
116 112
 }
117 113
 
114
+/**
115
+ * Sets the current Google API state.
116
+ *
117
+ * @param {number} googleAPIState - The state to be set.
118
+ * @param {Object} googleResponse - The last response from Google.
119
+ * @returns {{
120
+ *     type: SET_GOOGLE_API_STATE,
121
+ *     googleAPIState: number
122
+ * }}
123
+ */
124
+export function setGoogleAPIState(
125
+        googleAPIState: number, googleResponse: ?Object) {
126
+    return {
127
+        type: SET_GOOGLE_API_STATE,
128
+        googleAPIState,
129
+        googleResponse
130
+    };
131
+}
132
+
118 133
 /**
119 134
  * Forces the Google web client application to prompt for a sign in, such as
120 135
  * when changing account, and will then fetch available YouTube broadcasts.

+ 34
- 0
react/features/google-api/components/AbstractGoogleSignInButton.js Wyświetl plik

@@ -0,0 +1,34 @@
1
+// @flow
2
+
3
+import { Component } from 'react';
4
+
5
+/**
6
+ * {@code AbstractGoogleSignInButton} component's property types.
7
+ */
8
+type Props = {
9
+
10
+    /**
11
+     * The callback to invoke when the button is clicked.
12
+     */
13
+    onClick: Function,
14
+
15
+    /**
16
+     * True if the user is signed in, so it needs to render a different label
17
+     * and maybe different style (for the future).
18
+     */
19
+    signedIn?: boolean,
20
+
21
+    /**
22
+     * Function to be used to translate i18n labels.
23
+     */
24
+    t: Function
25
+};
26
+
27
+/**
28
+ * Abstract class of the {@code GoogleSignInButton} to share platform
29
+ * independent code.
30
+ *
31
+ * @inheritdoc
32
+ */
33
+export default class AbstractGoogleSignInButton extends Component<Props> {
34
+}

+ 62
- 0
react/features/google-api/components/GoogleSignInButton.native.js Wyświetl plik

@@ -0,0 +1,62 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { Image, Text, TouchableOpacity } from 'react-native';
5
+
6
+import { translate } from '../../base/i18n';
7
+
8
+import AbstractGoogleSignInButton from './AbstractGoogleSignInButton';
9
+import styles from './styles';
10
+
11
+/**
12
+ * The Google Brand image for Sign In.
13
+ *
14
+ * NOTE: iOS doesn't handle the react-native-google-signin button component
15
+ * well due to our CocoaPods build process (the lib is not intended to be used
16
+ * this way), hence the custom button implementation.
17
+ */
18
+const GOOGLE_BRAND_IMAGE
19
+    = require('../../../../images/btn_google_signin_dark_normal.png');
20
+
21
+/**
22
+ * A React Component showing a button to sign in with Google.
23
+ *
24
+ * @extends Component
25
+ */
26
+class GoogleSignInButton extends AbstractGoogleSignInButton {
27
+
28
+    /**
29
+     * Implements React's {@link Component#render()}.
30
+     *
31
+     * @inheritdoc
32
+     * @returns {ReactElement}
33
+     */
34
+    render() {
35
+        const { onClick, signedIn, t } = this.props;
36
+
37
+        if (signedIn) {
38
+            return (
39
+                <TouchableOpacity
40
+                    onPress = { onClick }
41
+                    style = { styles.signOutButton } >
42
+                    <Text style = { styles.signOutButtonText }>
43
+                        { t('liveStreaming.signOut') }
44
+                    </Text>
45
+                </TouchableOpacity>
46
+            );
47
+        }
48
+
49
+        return (
50
+            <TouchableOpacity
51
+                onPress = { onClick }
52
+                style = { styles.signInButton } >
53
+                <Image
54
+                    resizeMode = { 'contain' }
55
+                    source = { GOOGLE_BRAND_IMAGE }
56
+                    style = { styles.signInImage } />
57
+            </TouchableOpacity>
58
+        );
59
+    }
60
+}
61
+
62
+export default translate(GoogleSignInButton);

+ 14
- 13
react/features/google-api/components/GoogleSignInButton.web.js Wyświetl plik

@@ -1,25 +1,18 @@
1 1
 // @flow
2 2
 
3
-import React, { Component } from 'react';
3
+import React from 'react';
4 4
 
5
-/**
6
- * The type of the React {@code Component} props of {@link GoogleSignInButton}.
7
- */
8
-type Props = {
9
-
10
-    // The callback to invoke when {@code GoogleSignInButton} is clicked.
11
-    onClick: Function,
5
+import { translate } from '../../base/i18n';
12 6
 
13
-    // The text to display within {@code GoogleSignInButton}.
14
-    text: string
15
-};
7
+import AbstractGoogleSignInButton from './AbstractGoogleSignInButton';
16 8
 
17 9
 /**
18 10
  * A React Component showing a button to sign in with Google.
19 11
  *
20 12
  * @extends Component
21 13
  */
22
-export default class GoogleSignInButton extends Component<Props> {
14
+class GoogleSignInButton extends AbstractGoogleSignInButton {
15
+
23 16
     /**
24 17
      * Implements React's {@link Component#render()}.
25 18
      *
@@ -27,6 +20,8 @@ export default class GoogleSignInButton extends Component<Props> {
27 20
      * @returns {ReactElement}
28 21
      */
29 22
     render() {
23
+        const { t } = this.props;
24
+
30 25
         return (
31 26
             <div
32 27
                 className = 'google-sign-in'
@@ -35,9 +30,15 @@ export default class GoogleSignInButton extends Component<Props> {
35 30
                     className = 'google-logo'
36 31
                     src = 'images/googleLogo.svg' />
37 32
                 <div className = 'google-cta'>
38
-                    { this.props.text }
33
+                    {
34
+                        t(this.props.signedIn
35
+                            ? 'liveStreaming.signOut'
36
+                            : 'liveStreaming.signIn')
37
+                    }
39 38
                 </div>
40 39
             </div>
41 40
         );
42 41
     }
43 42
 }
43
+
44
+export default translate(GoogleSignInButton);

+ 2
- 0
react/features/google-api/components/index.js Wyświetl plik

@@ -1 +1,3 @@
1
+// @flow
2
+
1 3
 export { default as GoogleSignInButton } from './GoogleSignInButton';

+ 54
- 0
react/features/google-api/components/styles.js Wyświetl plik

@@ -0,0 +1,54 @@
1
+// @flow
2
+
3
+import { ColorPalette, createStyleSheet } from '../../base/styles';
4
+
5
+/**
6
+ * For styling explanations, see:
7
+ * https://developers.google.com/identity/branding-guidelines
8
+ */
9
+const BUTTON_HEIGHT = 40;
10
+
11
+/**
12
+ * The styles of the React {@code Components} of google-api.
13
+ */
14
+export default createStyleSheet({
15
+
16
+    /**
17
+     * Image of the sign in button (Google branded).
18
+     */
19
+    signInImage: {
20
+        flex: 1
21
+    },
22
+
23
+    /**
24
+     * An image-based button for sign in.
25
+     */
26
+    signInButton: {
27
+        alignItems: 'center',
28
+        height: BUTTON_HEIGHT,
29
+        justifyContent: 'center'
30
+    },
31
+
32
+    /**
33
+     * A text-based button for sign out (no sign out button guidance for
34
+     * Google).
35
+     */
36
+    signOutButton: {
37
+        alignItems: 'center',
38
+        borderColor: ColorPalette.lightGrey,
39
+        borderRadius: 3,
40
+        borderWidth: 1,
41
+        height: BUTTON_HEIGHT,
42
+        justifyContent: 'center'
43
+    },
44
+
45
+    /**
46
+     * Text of the sign out button.
47
+     */
48
+    signOutButtonText: {
49
+        color: ColorPalette.blue,
50
+        fontSize: 14,
51
+        fontWeight: 'bold'
52
+    }
53
+
54
+});

+ 37
- 7
react/features/google-api/constants.js Wyświetl plik

@@ -1,14 +1,23 @@
1 1
 // @flow
2 2
 
3 3
 /**
4
- * The Google API scopes to request access for streaming and calendar.
4
+ * Google API URL to retreive streams for a live broadcast of a user.
5 5
  *
6
- * @type {Array<string>}
6
+ * NOTE: The URL must be appended by a broadcast ID returned by a call towards
7
+ * {@code API_URL_LIVE_BROADCASTS}.
8
+ *
9
+ * @type {string}
10
+ */
11
+// eslint-disable-next-line max-len
12
+export const API_URL_BROADCAST_STREAMS = 'https://content.googleapis.com/youtube/v3/liveStreams?part=id%2Csnippet%2Ccdn%2Cstatus&id=';
13
+
14
+/**
15
+ * Google API URL to retreive live broadcasts of a user.
16
+ *
17
+ * @type {string}
7 18
  */
8
-export const GOOGLE_API_SCOPES = [
9
-    'https://www.googleapis.com/auth/youtube.readonly',
10
-    'https://www.googleapis.com/auth/calendar'
11
-];
19
+// eslint-disable-next-line max-len
20
+export const API_URL_LIVE_BROADCASTS = 'https://content.googleapis.com/youtube/v3/liveBroadcasts?broadcastType=all&mine=true&part=id%2Csnippet%2CcontentDetails%2Cstatus';
12 21
 
13 22
 /**
14 23
  * Array of API discovery doc URLs for APIs used by the googleApi.
@@ -38,5 +47,26 @@ export const GOOGLE_API_STATES = {
38 47
     /**
39 48
      * The state in which a user has been logged in through the Google API.
40 49
      */
41
-    SIGNED_IN: 2
50
+    SIGNED_IN: 2,
51
+
52
+    /**
53
+     * The state in which the Google authentication is not available (e.g. Play
54
+     * services are not installed on Android).
55
+     */
56
+    NOT_AVAILABLE: 3
42 57
 };
58
+
59
+/**
60
+ * Google API auth scope to access Google calendar.
61
+ *
62
+ * @type {string}
63
+ */
64
+export const GOOGLE_SCOPE_CALENDAR = 'https://www.googleapis.com/auth/calendar';
65
+
66
+/**
67
+ * Google API auth scope to access YouTube streams.
68
+ *
69
+ * @type {string}
70
+ */
71
+export const GOOGLE_SCOPE_YOUTUBE
72
+    = 'https://www.googleapis.com/auth/youtube.readonly';

+ 173
- 0
react/features/google-api/googleApi.native.js Wyświetl plik

@@ -0,0 +1,173 @@
1
+// @flow
2
+
3
+import {
4
+    GoogleSignin
5
+} from 'react-native-google-signin';
6
+
7
+import {
8
+    API_URL_BROADCAST_STREAMS,
9
+    API_URL_LIVE_BROADCASTS
10
+} from './constants';
11
+
12
+/**
13
+ * Class to encapsulate Google API functionalities and provide a similar
14
+ * interface to what WEB has. The methods are different, but the point is that
15
+ * the export object is similar so no need for different export logic.
16
+ *
17
+ * For more detailed documentation of the {@code GoogleSignin} API, please visit
18
+ * https://github.com/react-native-community/react-native-google-signin.
19
+ */
20
+class GoogleApi {
21
+    /**
22
+     * Wraps the {@code GoogleSignin.configure} method.
23
+     *
24
+     * @param {Object} config - The config object to be passed to
25
+     * {@code GoogleSignin.configure}.
26
+     * @returns {void}
27
+     */
28
+    configure(config: Object) {
29
+        GoogleSignin.configure(config);
30
+    }
31
+
32
+    /**
33
+     * Retrieves the available YouTube streams the user can use for live
34
+     * streaming.
35
+     *
36
+     * @param {string} accessToken - The Google auth token.
37
+     * @returns {Promise}
38
+     */
39
+    getYouTubeLiveStreams(accessToken: string): Promise<*> {
40
+        return new Promise((resolve, reject) => {
41
+
42
+            // Fetching the list of available broadcasts first.
43
+            this._fetchGoogleEndpoint(accessToken,
44
+                API_URL_LIVE_BROADCASTS)
45
+            .then(broadcasts => {
46
+                // Then fetching all the available live streams that the
47
+                // user has access to with the broadcasts we retreived
48
+                // earlier.
49
+                this._getLiveStreamsForBroadcasts(
50
+                    accessToken, broadcasts).then(resolve, reject);
51
+            }, reject);
52
+        });
53
+    }
54
+
55
+    /**
56
+     * Wraps the {@code GoogleSignin.hasPlayServices} method.
57
+     *
58
+     * @returns {Promise<*>}
59
+     */
60
+    hasPlayServices() {
61
+        return GoogleSignin.hasPlayServices();
62
+    }
63
+
64
+    /**
65
+     * Wraps the {@code GoogleSignin.signIn} method.
66
+     *
67
+     * @returns {Promise<*>}
68
+     */
69
+    signIn() {
70
+        return GoogleSignin.signIn();
71
+    }
72
+
73
+    /**
74
+     * Wraps the {@code GoogleSignin.signInSilently} method.
75
+     *
76
+     * @returns {Promise<*>}
77
+     */
78
+    signInSilently() {
79
+        return GoogleSignin.signInSilently();
80
+    }
81
+
82
+    /**
83
+     * Wraps the {@code GoogleSignin.signOut} method.
84
+     *
85
+     * @returns {Promise<*>}
86
+     */
87
+    signOut() {
88
+        return GoogleSignin.signOut();
89
+    }
90
+
91
+    /**
92
+     * Helper method to fetch a Google API endpoint in a generic way.
93
+     *
94
+     * @private
95
+     * @param {string} accessToken - The access token used for the API call.
96
+     * @param {string} endpoint - The endpoint to fetch, including the URL
97
+     * params if needed.
98
+     * @returns {Promise}
99
+     */
100
+    _fetchGoogleEndpoint(accessToken, endpoint): Promise<*> {
101
+        return new Promise((resolve, reject) => {
102
+            const headers = {
103
+                Authorization: `Bearer ${accessToken}`
104
+            };
105
+
106
+            fetch(endpoint, {
107
+                headers
108
+            }).then(response => response.json())
109
+            .then(responseJSON => {
110
+                if (responseJSON.error) {
111
+                    reject(responseJSON.error.message);
112
+                } else {
113
+                    resolve(responseJSON.items || []);
114
+                }
115
+            }, reject);
116
+        });
117
+    }
118
+
119
+    /**
120
+     * Retrieves the available YouTube streams that are available for the
121
+     * provided broadcast IDs.
122
+     *
123
+     * @private
124
+     * @param {string} accessToken - The Google access token.
125
+     * @param {Array<Object>} broadcasts - The list of broadcasts that we want
126
+     * to retreive streams for.
127
+     * @returns {Promise}
128
+     */
129
+    _getLiveStreamsForBroadcasts(accessToken, broadcasts): Promise<*> {
130
+        return new Promise((resolve, reject) => {
131
+            const ids = [];
132
+
133
+            for (const broadcast of broadcasts) {
134
+                broadcast.contentDetails
135
+                    && broadcast.contentDetails.boundStreamId
136
+                    && ids.push(broadcast.contentDetails.boundStreamId);
137
+            }
138
+
139
+            this._fetchGoogleEndpoint(
140
+                accessToken,
141
+                `${API_URL_BROADCAST_STREAMS}${ids.join(',')}`)
142
+                .then(streams => {
143
+                    const keys = [];
144
+
145
+                    // We construct an array of keys bind with the broadcast
146
+                    // name for a nice display.
147
+                    for (const stream of streams) {
148
+                        const key = stream.cdn.ingestionInfo.streamName;
149
+                        let title;
150
+
151
+                        // Finding title from the broadcast with the same
152
+                        // channelId. If not found (unknown scenario), we use
153
+                        // the key as title again.
154
+                        for (const broadcast of broadcasts) {
155
+                            if (broadcast.snippet.channelId
156
+                                    === stream.snippet.channelId) {
157
+                                title = broadcast.snippet.title;
158
+                            }
159
+                        }
160
+
161
+                        keys.push({
162
+                            key,
163
+                            title: title || key
164
+                        });
165
+                    }
166
+
167
+                    resolve(keys);
168
+                }, reject);
169
+        });
170
+    }
171
+}
172
+
173
+export default new GoogleApi();

react/features/google-api/googleApi.js → react/features/google-api/googleApi.web.js Wyświetl plik

@@ -1,4 +1,10 @@
1
-import { GOOGLE_API_SCOPES, DISCOVERY_DOCS } from './constants';
1
+import {
2
+    API_URL_BROADCAST_STREAMS,
3
+    API_URL_LIVE_BROADCASTS,
4
+    DISCOVERY_DOCS,
5
+    GOOGLE_SCOPE_CALENDAR,
6
+    GOOGLE_SCOPE_YOUTUBE
7
+} from './constants';
2 8
 
3 9
 const GOOGLE_API_CLIENT_LIBRARY_URL = 'https://apis.google.com/js/api.js';
4 10
 
@@ -68,7 +74,10 @@ const googleApi = {
68 74
                     api.client.init({
69 75
                         clientId,
70 76
                         discoveryDocs: DISCOVERY_DOCS,
71
-                        scope: GOOGLE_API_SCOPES.join(' ')
77
+                        scope: [
78
+                            GOOGLE_SCOPE_CALENDAR,
79
+                            GOOGLE_SCOPE_YOUTUBE
80
+                        ].join(' ')
72 81
                     })
73 82
                     .then(resolve)
74 83
                     .catch(reject);
@@ -137,10 +146,8 @@ const googleApi = {
137 146
      * @returns {Promise}
138 147
      */
139 148
     requestAvailableYouTubeBroadcasts() {
140
-        const url = this._getURLForLiveBroadcasts();
141
-
142 149
         return this.get()
143
-            .then(api => api.client.request(url));
150
+            .then(api => api.client.request(API_URL_LIVE_BROADCASTS));
144 151
     },
145 152
 
146 153
     /**
@@ -152,10 +159,9 @@ const googleApi = {
152 159
      * @returns {Promise}
153 160
      */
154 161
     requestLiveStreamsForYouTubeBroadcast(boundStreamID) {
155
-        const url = this._getURLForLiveStreams(boundStreamID);
156
-
157 162
         return this.get()
158
-            .then(api => api.client.request(url));
163
+            .then(api => api.client.request(
164
+                `${API_URL_BROADCAST_STREAMS}${boundStreamID}`));
159 165
     },
160 166
 
161 167
     /**
@@ -353,37 +359,6 @@ const googleApi = {
353 359
      */
354 360
     _getGoogleApiClient() {
355 361
         return window.gapi;
356
-    },
357
-
358
-    /**
359
-     * Returns the URL to the Google API endpoint for retrieving the currently
360
-     * signed in user's YouTube broadcasts.
361
-     *
362
-     * @private
363
-     * @returns {string}
364
-     */
365
-    _getURLForLiveBroadcasts() {
366
-        return [
367
-            'https://content.googleapis.com/youtube/v3/liveBroadcasts',
368
-            '?broadcastType=all',
369
-            '&mine=true&part=id%2Csnippet%2CcontentDetails%2Cstatus'
370
-        ].join('');
371
-    },
372
-
373
-    /**
374
-     * Returns the URL to the Google API endpoint for retrieving the live
375
-     * streams associated with a YouTube broadcast's bound stream.
376
-     *
377
-     * @param {string} boundStreamID - The bound stream ID associated with a
378
-     * broadcast in YouTube.
379
-     * @returns {string}
380
-     */
381
-    _getURLForLiveStreams(boundStreamID) {
382
-        return [
383
-            'https://content.googleapis.com/youtube/v3/liveStreams',
384
-            '?part=id%2Csnippet%2Ccdn%2Cstatus',
385
-            `&id=${boundStreamID}`
386
-        ].join('');
387 362
     }
388 363
 };
389 364
 

+ 4
- 2
react/features/google-api/index.js Wyświetl plik

@@ -1,6 +1,8 @@
1
-export { GOOGLE_API_STATES } from './constants';
2
-export { default as googleApi } from './googleApi';
1
+// @flow
2
+
3 3
 export * from './actions';
4 4
 export * from './components';
5
+export * from './constants';
6
+export { default as googleApi } from './googleApi';
5 7
 
6 8
 import './reducer';

+ 2
- 1
react/features/google-api/reducer.js Wyświetl plik

@@ -27,7 +27,8 @@ ReducerRegistry.register('features/google-api',
27 27
         case SET_GOOGLE_API_STATE:
28 28
             return {
29 29
                 ...state,
30
-                googleAPIState: action.googleAPIState
30
+                googleAPIState: action.googleAPIState,
31
+                googleResponse: action.googleResponse
31 32
             };
32 33
         case SET_GOOGLE_API_PROFILE:
33 34
             return {

+ 5
- 14
react/features/recording/components/LiveStream/AbstractStartLiveStreamDialog.js Wyświetl plik

@@ -91,8 +91,8 @@ export type State = {
91 91
  * but the abstraction of its properties are already present in this abstract
92 92
  * class.
93 93
  */
94
-export default class AbstractStartLiveStreamDialog
95
-    extends Component<Props, State> {
94
+export default class AbstractStartLiveStreamDialog<P: Props>
95
+    extends Component<P, State> {
96 96
     _isMounted: boolean;
97 97
 
98 98
     /**
@@ -100,7 +100,7 @@ export default class AbstractStartLiveStreamDialog
100 100
      *
101 101
      * @inheritdoc
102 102
      */
103
-    constructor(props: Props) {
103
+    constructor(props: P) {
104 104
         super(props);
105 105
 
106 106
         this.state = {
@@ -134,10 +134,6 @@ export default class AbstractStartLiveStreamDialog
134 134
      */
135 135
     componentDidMount() {
136 136
         this._isMounted = true;
137
-
138
-        if (this.props._googleApiApplicationClientID) {
139
-            this._onInitializeGoogleApi();
140
-        }
141 137
     }
142 138
 
143 139
     /**
@@ -197,13 +193,6 @@ export default class AbstractStartLiveStreamDialog
197 193
      */
198 194
     _onGetYouTubeBroadcasts: () => Promise<*>;
199 195
 
200
-    /**
201
-     * Loads the Google client application used for fetching stream keys.
202
-     * If the user is already logged in, then a request for available YouTube
203
-     * broadcasts is also made.
204
-     */
205
-    _onInitializeGoogleApi: () => Object;
206
-
207 196
     _onStreamKeyChange: string => void;
208 197
 
209 198
     /**
@@ -291,6 +280,8 @@ export default class AbstractStartLiveStreamDialog
291 280
  * @returns {{
292 281
  *     _conference: Object,
293 282
  *     _googleApiApplicationClientID: string,
283
+ *     _googleAPIState: number,
284
+ *     _googleProfileEmail: string,
294 285
  *     _streamKey: string
295 286
  * }}
296 287
  */

+ 254
- 0
react/features/recording/components/LiveStream/GoogleSigninForm.native.js Wyświetl plik

@@ -0,0 +1,254 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { Text, View } from 'react-native';
5
+import { connect } from 'react-redux';
6
+
7
+import { translate } from '../../../base/i18n';
8
+
9
+import {
10
+    GOOGLE_API_STATES,
11
+    GOOGLE_SCOPE_YOUTUBE,
12
+    googleApi,
13
+    GoogleSignInButton,
14
+    setGoogleAPIState
15
+} from '../../../google-api';
16
+
17
+import styles from './styles';
18
+
19
+const logger = require('jitsi-meet-logger').getLogger(__filename);
20
+
21
+/**
22
+ * Prop type of the component {@code GoogleSigninForm}.
23
+ */
24
+type Props = {
25
+
26
+    /**
27
+     * The ID for the Google client application used for making stream key
28
+     * related requests.
29
+     */
30
+    clientId: string,
31
+
32
+    /**
33
+     * The Redux dispatch Function.
34
+     */
35
+    dispatch: Function,
36
+
37
+    /**
38
+     * The current state of the Google api as defined in {@code constants.js}.
39
+     */
40
+    googleAPIState: number,
41
+
42
+    /**
43
+     * The recently received Google response.
44
+     */
45
+    googleResponse: Object,
46
+
47
+    /**
48
+     * The ID for the Google client application used for making stream key
49
+     * related requests on iOS.
50
+     */
51
+    iOSClientId: string,
52
+
53
+    /**
54
+     * A callback to be invoked when an authenticated user changes, so
55
+     * then we can get (or clear) the YouTube stream key.
56
+     */
57
+    onUserChanged: Function,
58
+
59
+    /**
60
+     * Function to be used to translate i18n labels.
61
+     */
62
+    t: Function
63
+};
64
+
65
+/**
66
+ * Class to render a google sign in form, or a google stream picker dialog.
67
+ *
68
+ * @extends Component
69
+ */
70
+class GoogleSigninForm extends Component<Props> {
71
+    /**
72
+     * Instantiates a new {@code GoogleSigninForm} component.
73
+     *
74
+     * @inheritdoc
75
+     */
76
+    constructor(props: Props) {
77
+        super(props);
78
+
79
+        this._logGoogleError = this._logGoogleError.bind(this);
80
+        this._onGoogleButtonPress = this._onGoogleButtonPress.bind(this);
81
+    }
82
+
83
+    /**
84
+     * Implements React's Component.componentDidMount.
85
+     *
86
+     * @inheritdoc
87
+     */
88
+    componentDidMount() {
89
+        if (!this.props.clientId) {
90
+            // NOTE: This is a developer error message, not intended for the
91
+            // user to see.
92
+            logger.error('Missing clientID');
93
+            this._setApiState(GOOGLE_API_STATES.NOT_AVAILABLE);
94
+
95
+            return;
96
+        }
97
+
98
+        googleApi.hasPlayServices()
99
+            .then(() => {
100
+                googleApi.configure({
101
+                    iosClientId: this.props.iOSClientId,
102
+                    offlineAccess: false,
103
+                    scopes: [ GOOGLE_SCOPE_YOUTUBE ],
104
+                    webClientId: this.props.clientId
105
+                });
106
+
107
+                googleApi.signInSilently().then(response => {
108
+                    this._setApiState(response
109
+                        ? GOOGLE_API_STATES.SIGNED_IN
110
+                        : GOOGLE_API_STATES.LOADED,
111
+                        response);
112
+                }, () => {
113
+                    this._setApiState(GOOGLE_API_STATES.LOADED);
114
+                });
115
+            })
116
+            .catch(error => {
117
+                this._logGoogleError(error);
118
+                this._setApiState(GOOGLE_API_STATES.NOT_AVAILABLE);
119
+            });
120
+    }
121
+
122
+    /**
123
+     * Renders the component.
124
+     *
125
+     * @inheritdoc
126
+     */
127
+    render() {
128
+        const { t } = this.props;
129
+        const { googleAPIState, googleResponse } = this.props;
130
+        const signedInUser = googleResponse
131
+            && googleResponse.user
132
+            && googleResponse.user.email;
133
+
134
+        if (googleAPIState === GOOGLE_API_STATES.NOT_AVAILABLE
135
+                || googleAPIState === GOOGLE_API_STATES.NEEDS_LOADING
136
+                || typeof googleAPIState === 'undefined') {
137
+            return null;
138
+        }
139
+
140
+        return (
141
+            <View style = { styles.formWrapper }>
142
+                <View style = { styles.helpText }>
143
+                    { signedInUser ? <Text>
144
+                        { `${t('liveStreaming.signedInAs')} ${signedInUser}` }
145
+                    </Text> : <Text>
146
+                        { t('liveStreaming.signInCTA') }
147
+                    </Text> }
148
+                </View>
149
+                <GoogleSignInButton
150
+                    onClick = { this._onGoogleButtonPress }
151
+                    signedIn = {
152
+                        googleAPIState === GOOGLE_API_STATES.SIGNED_IN } />
153
+            </View>
154
+        );
155
+    }
156
+
157
+    _logGoogleError: Object => void
158
+
159
+    /**
160
+     * A helper function to log developer related errors.
161
+     *
162
+     * @private
163
+     * @param {Object} error - The error to be logged.
164
+     * @returns {void}
165
+     */
166
+    _logGoogleError(error) {
167
+        // NOTE: This is a developer error message, not intended for the
168
+        // user to see.
169
+        logger.error('Google API error. Possible cause: bad config.', error);
170
+    }
171
+
172
+    _onGoogleButtonPress: () => void
173
+
174
+    /**
175
+     * Callback to be invoked when the user presses the Google button,
176
+     * regardless of being logged in or out.
177
+     *
178
+     * @private
179
+     * @returns {void}
180
+     */
181
+    _onGoogleButtonPress() {
182
+        const { googleResponse } = this.props;
183
+
184
+        if (googleResponse && googleResponse.user) {
185
+            // the user is signed in
186
+            this._onSignOut();
187
+        } else {
188
+            this._onSignIn();
189
+        }
190
+    }
191
+
192
+    _onSignIn: () => void
193
+
194
+    /**
195
+     * Initiates a sign in if the user is not signed in yet.
196
+     *
197
+     * @private
198
+     * @returns {void}
199
+     */
200
+    _onSignIn() {
201
+        googleApi.signIn().then(response => {
202
+            this._setApiState(GOOGLE_API_STATES.SIGNED_IN, response);
203
+        }, this._logGoogleError);
204
+    }
205
+
206
+    _onSignOut: () => void
207
+
208
+    /**
209
+     * Initiates a sign out if the user is signed in.
210
+     *
211
+     * @private
212
+     * @returns {void}
213
+     */
214
+    _onSignOut() {
215
+        googleApi.signOut().then(response => {
216
+            this._setApiState(GOOGLE_API_STATES.LOADED, response);
217
+        }, this._logGoogleError);
218
+    }
219
+
220
+    /**
221
+     * Updates the API (Google Auth) state.
222
+     *
223
+     * @private
224
+     * @param {number} apiState - The state of the API.
225
+     * @param {?Object} googleResponse - The response from the API.
226
+     * @returns {void}
227
+     */
228
+    _setApiState(apiState, googleResponse) {
229
+        this.props.onUserChanged(googleResponse);
230
+        this.props.dispatch(setGoogleAPIState(apiState, googleResponse));
231
+    }
232
+}
233
+
234
+/**
235
+ * Maps (parts of) the redux state to the associated props for the
236
+ * {@code GoogleSigninForm} component.
237
+ *
238
+ * @param {Object} state - The Redux state.
239
+ * @private
240
+ * @returns {{
241
+ *     googleAPIState: number,
242
+  *    googleResponse: Object
243
+ * }}
244
+ */
245
+function _mapStateToProps(state: Object) {
246
+    const { googleAPIState, googleResponse } = state['features/google-api'];
247
+
248
+    return {
249
+        googleAPIState,
250
+        googleResponse
251
+    };
252
+}
253
+
254
+export default translate(connect(_mapStateToProps)(GoogleSigninForm));

react/features/recording/components/LiveStream/BroadcastsDropdown.native.js → react/features/recording/components/LiveStream/GoogleSigninForm.web.js Wyświetl plik


+ 87
- 21
react/features/recording/components/LiveStream/StartLiveStreamDialog.native.js Wyświetl plik

@@ -5,20 +5,34 @@ import { View } from 'react-native';
5 5
 import { connect } from 'react-redux';
6 6
 
7 7
 import { translate } from '../../../base/i18n';
8
+import { googleApi } from '../../../google-api';
9
+
8 10
 
9 11
 import { setLiveStreamKey } from '../../actions';
10 12
 
11 13
 import AbstractStartLiveStreamDialog, {
12
-    _mapStateToProps,
13
-    type Props
14
+    _mapStateToProps as _abstractMapStateToProps,
15
+    type Props as AbstractProps
14 16
 } from './AbstractStartLiveStreamDialog';
17
+import GoogleSigninForm from './GoogleSigninForm';
15 18
 import StreamKeyForm from './StreamKeyForm';
19
+import StreamKeyPicker from './StreamKeyPicker';
20
+import styles from './styles';
21
+
22
+type Props = AbstractProps & {
23
+
24
+    /**
25
+     * The ID for the Google client application used for making stream key
26
+     * related requests on iOS.
27
+     */
28
+    _googleApiIOSClientID: string
29
+};
16 30
 
17 31
 /**
18 32
  * A React Component for requesting a YouTube stream key to use for live
19 33
  * streaming of the current conference.
20 34
  */
21
-class StartLiveStreamDialog extends AbstractStartLiveStreamDialog {
35
+class StartLiveStreamDialog extends AbstractStartLiveStreamDialog<Props> {
22 36
     /**
23 37
      * Constructor of the component.
24 38
      *
@@ -28,27 +42,13 @@ class StartLiveStreamDialog extends AbstractStartLiveStreamDialog {
28 42
         super(props);
29 43
 
30 44
         // Bind event handlers so they are only bound once per instance.
31
-        this._onInitializeGoogleApi = this._onInitializeGoogleApi.bind(this);
32 45
         this._onStreamKeyChangeNative
33 46
             = this._onStreamKeyChangeNative.bind(this);
47
+        this._onStreamKeyPick = this._onStreamKeyPick.bind(this);
48
+        this._onUserChanged = this._onUserChanged.bind(this);
34 49
         this._renderDialogContent = this._renderDialogContent.bind(this);
35 50
     }
36 51
 
37
-    _onInitializeGoogleApi: () => Promise<*>
38
-
39
-    /**
40
-     * Loads the Google client application used for fetching stream keys.
41
-     * If the user is already logged in, then a request for available YouTube
42
-     * broadcasts is also made.
43
-     *
44
-     * @private
45
-     * @returns {Promise}
46
-     */
47
-    _onInitializeGoogleApi() {
48
-        // This is a placeholder method for the Google feature.
49
-        return Promise.resolve();
50
-    }
51
-
52 52
     _onStreamKeyChange: string => void
53 53
 
54 54
     _onStreamKeyChangeNative: string => void;
@@ -70,6 +70,49 @@ class StartLiveStreamDialog extends AbstractStartLiveStreamDialog {
70 70
         this._onStreamKeyChange(streamKey);
71 71
     }
72 72
 
73
+    _onStreamKeyPick: string => void
74
+
75
+    /**
76
+     * Callback to be invoked when the user selects a stream from the picker.
77
+     *
78
+     * @private
79
+     * @param {string} streamKey - The key of the selected stream.
80
+     * @returns {void}
81
+     */
82
+    _onStreamKeyPick(streamKey) {
83
+        this.setState({
84
+            streamKey
85
+        });
86
+    }
87
+
88
+    _onUserChanged: Object => void
89
+
90
+    /**
91
+     * A callback to be invoked when an authenticated user changes, so
92
+     * then we can get (or clear) the YouTube stream key.
93
+     *
94
+     * TODO: handle errors by showing some indication to the user.
95
+     *
96
+     * @private
97
+     * @param {Object} response - The retreived signin response.
98
+     * @returns {void}
99
+     */
100
+    _onUserChanged(response) {
101
+        if (response && response.accessToken) {
102
+            googleApi.getYouTubeLiveStreams(response.accessToken)
103
+            .then(broadcasts => {
104
+                this.setState({
105
+                    broadcasts
106
+                });
107
+            });
108
+        } else {
109
+            this.setState({
110
+                broadcasts: undefined,
111
+                streamKey: undefined
112
+            });
113
+        }
114
+    }
115
+
73 116
     _renderDialogContent: () => React$Component<*>
74 117
 
75 118
     /**
@@ -79,14 +122,37 @@ class StartLiveStreamDialog extends AbstractStartLiveStreamDialog {
79 122
      */
80 123
     _renderDialogContent() {
81 124
         return (
82
-            <View>
125
+            <View style = { styles.startDialogWrapper }>
126
+                <GoogleSigninForm
127
+                    clientId = { this.props._googleApiApplicationClientID }
128
+                    iOSClientId = { this.props._googleApiIOSClientID }
129
+                    onUserChanged = { this._onUserChanged } />
130
+                <StreamKeyPicker
131
+                    broadcasts = { this.state.broadcasts }
132
+                    onChange = { this._onStreamKeyPick } />
83 133
                 <StreamKeyForm
84 134
                     onChange = { this._onStreamKeyChangeNative }
85
-                    value = { this.props._streamKey } />
135
+                    value = { this.state.streamKey || this.props._streamKey } />
86 136
             </View>
87 137
         );
88 138
     }
89 139
 
90 140
 }
91 141
 
142
+/**
143
+ * Maps part of the Redux state to the component's props.
144
+ *
145
+ * @param {Object} state - The Redux state.
146
+ * @returns {{
147
+ *     _googleApiApplicationClientID: string
148
+ * }}
149
+ */
150
+function _mapStateToProps(state: Object) {
151
+    return {
152
+        ..._abstractMapStateToProps(state),
153
+        _googleApiIOSClientID:
154
+            state['features/base/config'].googleApiIOSClientID
155
+    };
156
+}
157
+
92 158
 export default translate(connect(_mapStateToProps)(StartLiveStreamDialog));

+ 21
- 10
react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js Wyświetl plik

@@ -21,7 +21,7 @@ import AbstractStartLiveStreamDialog, {
21 21
     _mapStateToProps,
22 22
     type Props
23 23
 } from './AbstractStartLiveStreamDialog';
24
-import BroadcastsDropdown from './BroadcastsDropdown';
24
+import StreamKeyPicker from './StreamKeyPicker';
25 25
 import StreamKeyForm from './StreamKeyForm';
26 26
 
27 27
 /**
@@ -31,7 +31,7 @@ import StreamKeyForm from './StreamKeyForm';
31 31
  * @extends Component
32 32
  */
33 33
 class StartLiveStreamDialog
34
-    extends AbstractStartLiveStreamDialog {
34
+    extends AbstractStartLiveStreamDialog<Props> {
35 35
 
36 36
     /**
37 37
      * Initializes a new {@code StartLiveStreamDialog} instance.
@@ -53,6 +53,21 @@ class StartLiveStreamDialog
53 53
         this._renderDialogContent = this._renderDialogContent.bind(this);
54 54
     }
55 55
 
56
+    /**
57
+     * Implements {@link Component#componentDidMount()}. Invoked immediately
58
+     * after this component is mounted.
59
+     *
60
+     * @inheritdoc
61
+     * @returns {void}
62
+     */
63
+    componentDidMount() {
64
+        super.componentDidMount();
65
+
66
+        if (this.props._googleApiApplicationClientID) {
67
+            this._onInitializeGoogleApi();
68
+        }
69
+    }
70
+
56 71
     _onInitializeGoogleApi: () => Promise<*>;
57 72
 
58 73
     /**
@@ -237,18 +252,15 @@ class StartLiveStreamDialog
237 252
 
238 253
         switch (this.props._googleAPIState) {
239 254
         case GOOGLE_API_STATES.LOADED:
240
-            googleContent = (
241
-                <GoogleSignInButton
242
-                    onClick = { this._onGoogleSignIn }
243
-                    text = { t('liveStreaming.signIn') } />
244
-            );
255
+            googleContent
256
+                = <GoogleSignInButton onClick = { this._onGoogleSignIn } />;
245 257
             helpText = t('liveStreaming.signInCTA');
246 258
 
247 259
             break;
248 260
 
249 261
         case GOOGLE_API_STATES.SIGNED_IN:
250 262
             googleContent = (
251
-                <BroadcastsDropdown
263
+                <StreamKeyPicker
252 264
                     broadcasts = { broadcasts }
253 265
                     onBroadcastSelected = { this._onYouTubeBroadcastIDSelected }
254 266
                     selectedBoundStreamID = { selectedBoundStreamID } />
@@ -285,8 +297,7 @@ class StartLiveStreamDialog
285 297
         if (this.state.errorType !== undefined) {
286 298
             googleContent = (
287 299
                 <GoogleSignInButton
288
-                    onClick = { this._onRequestGoogleSignIn }
289
-                    text = { t('liveStreaming.signIn') } />
300
+                    onClick = { this._onRequestGoogleSignIn } />
290 301
             );
291 302
             helpText = this._getGoogleErrorMessageToDisplay();
292 303
         }

+ 1
- 1
react/features/recording/components/LiveStream/StreamKeyForm.native.js Wyświetl plik

@@ -39,7 +39,7 @@ class StreamKeyForm extends AbstractStreamKeyForm {
39 39
         const { t } = this.props;
40 40
 
41 41
         return (
42
-            <View style = { styles.streamKeyFormWrapper }>
42
+            <View style = { styles.formWrapper }>
43 43
                 <Text style = { styles.streamKeyInputLabel }>
44 44
                     {
45 45
                         t('dialog.streamKey')

+ 122
- 0
react/features/recording/components/LiveStream/StreamKeyPicker.native.js Wyświetl plik

@@ -0,0 +1,122 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { Text, TouchableHighlight, View } from 'react-native';
5
+
6
+import { translate } from '../../../base/i18n';
7
+
8
+import styles, { ACTIVE_OPACITY, TOUCHABLE_UNDERLAY } from './styles';
9
+
10
+type Props = {
11
+
12
+    /**
13
+     * The list of broadcasts the user can pick from.
14
+     */
15
+    broadcasts: ?Array<Object>,
16
+
17
+    /**
18
+     * Callback to be invoked when the user picked a broadcast. To be invoked
19
+     * with a single key (string).
20
+     */
21
+    onChange: Function,
22
+
23
+    /**
24
+     * Function to be used to translate i18n labels.
25
+     */
26
+    t: Function
27
+}
28
+
29
+type State = {
30
+
31
+     /**
32
+      * The key of the currently selected stream.
33
+      */
34
+     streamKey: ?string
35
+}
36
+
37
+/**
38
+ * Class to implement a stream key picker (dropdown) component to allow the user
39
+ * to choose from the available Google Broadcasts/Streams.
40
+ *
41
+ * NOTE: This component is currently only used on mobile, but it is advised at
42
+ * a later point to unify mobile and web logic for this functionality. But it's
43
+ * out of the scope for now of the mobile live streaming functionality.
44
+ */
45
+class StreamKeyPicker extends Component<Props, State> {
46
+
47
+    /**
48
+     * Instantiates a new instance of StreamKeyPicker.
49
+     *
50
+     * @inheritdoc
51
+     */
52
+    constructor(props: Props) {
53
+        super(props);
54
+
55
+        this.state = {
56
+            streamKey: null
57
+        };
58
+
59
+        this._onStreamPick = this._onStreamPick.bind(this);
60
+    }
61
+
62
+    /**
63
+     * Renders the component.
64
+     *
65
+     * @inheritdoc
66
+     */
67
+    render() {
68
+        const { broadcasts } = this.props;
69
+
70
+        if (!broadcasts || !broadcasts.length) {
71
+            return null;
72
+        }
73
+
74
+        return (
75
+            <View style = { styles.formWrapper }>
76
+                <View style = { styles.streamKeyPickerCta }>
77
+                    <Text>
78
+                        { this.props.t('liveStreaming.choose') }
79
+                    </Text>
80
+                </View>
81
+                <View style = { styles.streamKeyPickerWrapper } >
82
+                    { broadcasts.map((broadcast, index) =>
83
+                        (<TouchableHighlight
84
+                            activeOpacity = { ACTIVE_OPACITY }
85
+                            key = { index }
86
+                            onPress = { this._onStreamPick(broadcast.key) }
87
+                            style = { [
88
+                                styles.streamKeyPickerItem,
89
+                                this.state.streamKey === broadcast.key
90
+                                    ? styles.streamKeyPickerItemHighlight : null
91
+                            ] }
92
+                            underlayColor = { TOUCHABLE_UNDERLAY }>
93
+                            <Text style = { styles.streamKeyPickerItemText }>
94
+                                { broadcast.title }
95
+                            </Text>
96
+                        </TouchableHighlight>))
97
+                    }
98
+                </View>
99
+            </View>
100
+        );
101
+    }
102
+
103
+    _onStreamPick: string => Function
104
+
105
+    /**
106
+     * Callback to be invoked when the user picks a stream from the list.
107
+     *
108
+     * @private
109
+     * @param {string} streamKey - The key of the stream selected.
110
+     * @returns {Function}
111
+     */
112
+    _onStreamPick(streamKey) {
113
+        return () => {
114
+            this.setState({
115
+                streamKey
116
+            });
117
+            this.props.onChange(streamKey);
118
+        };
119
+    }
120
+}
121
+
122
+export default translate(StreamKeyPicker);

react/features/recording/components/LiveStream/BroadcastsDropdown.web.js → react/features/recording/components/LiveStream/StreamKeyPicker.web.js Wyświetl plik

@@ -13,7 +13,7 @@ import { translate } from '../../../base/i18n';
13 13
  *
14 14
  * @extends Component
15 15
  */
16
-class BroadcastsDropdown extends PureComponent {
16
+class StreamKeyPicker extends PureComponent {
17 17
     /**
18 18
      * Default values for {@code StreamKeyForm} component's properties.
19 19
      *
@@ -24,7 +24,7 @@ class BroadcastsDropdown extends PureComponent {
24 24
     };
25 25
 
26 26
     /**
27
-     * {@code BroadcastsDropdown} component's property types.
27
+     * {@code StreamKeyPicker} component's property types.
28 28
      */
29 29
     static propTypes = {
30 30
         /**
@@ -64,10 +64,10 @@ class BroadcastsDropdown extends PureComponent {
64 64
     };
65 65
 
66 66
     /**
67
-     * Initializes a new {@code BroadcastsDropdown} instance.
67
+     * Initializes a new {@code StreamKeyPicker} instance.
68 68
      *
69 69
      * @param {Props} props - The React {@code Component} props to initialize
70
-     * the new {@code BroadcastsDropdown} instance with.
70
+     * the new {@code StreamKeyPicker} instance with.
71 71
      */
72 72
     constructor(props) {
73 73
         super(props);
@@ -166,4 +166,4 @@ class BroadcastsDropdown extends PureComponent {
166 166
     }
167 167
 }
168 168
 
169
-export default translate(BroadcastsDropdown);
169
+export default translate(StreamKeyPicker);

+ 80
- 1
react/features/recording/components/LiveStream/styles.native.js Wyświetl plik

@@ -2,6 +2,16 @@
2 2
 
3 3
 import { BoxModel, ColorPalette, createStyleSheet } from '../../../base/styles';
4 4
 
5
+/**
6
+ * Opacity of the TouchableHighlight.
7
+ */
8
+export const ACTIVE_OPACITY = 0.3;
9
+
10
+/**
11
+ * Underlay of the TouchableHighlight.
12
+ */
13
+export const TOUCHABLE_UNDERLAY = ColorPalette.lightGrey;
14
+
5 15
 /**
6 16
  * The styles of the React {@code Components} of LiveStream.
7 17
  */
@@ -20,22 +30,91 @@ export default createStyleSheet({
20 30
         fontWeight: 'bold'
21 31
     },
22 32
 
23
-    streamKeyFormWrapper: {
33
+    /**
34
+     * Generic component to wrap form sections into achieving a unified look.
35
+     */
36
+    formWrapper: {
37
+        alignItems: 'stretch',
24 38
         flexDirection: 'column',
25 39
         padding: BoxModel.padding
26 40
     },
27 41
 
42
+    /**
43
+     * Explaining text on the top of the sign in form.
44
+     */
45
+    helpText: {
46
+        marginBottom: BoxModel.margin
47
+    },
48
+
49
+    /**
50
+     * Wrapper for the StartLiveStreamDialog form.
51
+     */
52
+    startDialogWrapper: {
53
+        flexDirection: 'column'
54
+    },
55
+
56
+    /**
57
+     * Helper link text.
58
+     */
28 59
     streamKeyHelp: {
29 60
         alignSelf: 'flex-end'
30 61
     },
31 62
 
63
+    /**
64
+     * Input field to manually enter stream key.
65
+     */
32 66
     streamKeyInput: {
33 67
         alignSelf: 'stretch',
34 68
         height: 50
35 69
     },
36 70
 
71
+    /**
72
+     * Label for the previous field.
73
+     */
37 74
     streamKeyInputLabel: {
38 75
         alignSelf: 'flex-start'
76
+    },
77
+
78
+    /**
79
+     * Custom component to pick a broadcast from the list fetched from Google.
80
+     */
81
+    streamKeyPicker: {
82
+        alignSelf: 'stretch',
83
+        flex: 1,
84
+        height: 40,
85
+        marginHorizontal: 4,
86
+        width: 300
87
+    },
88
+
89
+    /**
90
+     * CTA (label) of the picker.
91
+     */
92
+    streamKeyPickerCta: {
93
+        marginBottom: 8
94
+    },
95
+
96
+    /**
97
+     * Style of a single item in the list.
98
+     */
99
+    streamKeyPickerItem: {
100
+        padding: 4
101
+    },
102
+
103
+    /**
104
+     * Additional style for the selected item.
105
+     */
106
+    streamKeyPickerItemHighlight: {
107
+        backgroundColor: ColorPalette.lighterGrey
108
+    },
109
+
110
+    /**
111
+     * Overall wrapper for the picker.
112
+     */
113
+    streamKeyPickerWrapper: {
114
+        borderColor: ColorPalette.lightGrey,
115
+        borderRadius: 3,
116
+        borderWidth: 1,
117
+        flexDirection: 'column'
39 118
     }
40 119
 
41 120
 });

+ 0
- 8
react/features/recording/reducer.js Wyświetl plik

@@ -1,5 +1,4 @@
1 1
 import { ReducerRegistry } from '../base/redux';
2
-import { PersistenceRegistry } from '../base/storage';
3 2
 import {
4 3
     CLEAR_RECORDING_SESSIONS,
5 4
     RECORDING_SESSION_UPDATED,
@@ -17,13 +16,6 @@ const DEFAULT_STATE = {
17 16
  */
18 17
 const STORE_NAME = 'features/recording';
19 18
 
20
-/**
21
- * Sets up the persistence of the feature {@code recording}.
22
- */
23
-PersistenceRegistry.register(STORE_NAME, {
24
-    streamKey: true
25
-}, DEFAULT_STATE);
26
-
27 19
 /**
28 20
  * Reduces the Redux actions of the feature features/recording.
29 21
  */

Ładowanie…
Anuluj
Zapisz