浏览代码

feat(recording): Implement dropbox integration

j8
hristoterezov 6 年前
父节点
当前提交
df0e107ea6

+ 32
- 0
css/_recording.scss 查看文件

@@ -2,6 +2,38 @@
2 2
     vertical-align: top;
3 3
 }
4 4
 
5
+.recording-dialog {
6
+    .authorization-panel {
7
+        align-items: center;
8
+        border-bottom: 2px solid rgba(0, 0, 0, 0.3);
9
+        display: flex;
10
+        flex-direction: column;
11
+        margin-bottom: 10px;
12
+        padding-bottom: 10px;
13
+
14
+        .dropbox-sign-in {
15
+            background-color: #4285f4;
16
+            border-radius: 2px;
17
+            cursor: pointer;
18
+            display: inline-flex;
19
+            padding: 1px;
20
+            margin: 10px 0px;
21
+
22
+            .dropbox-logo {
23
+                background-color: white;
24
+                border-radius: 2px;
25
+                display: inline-block;
26
+                padding: 8px;
27
+                height: 18px;
28
+            }
29
+        }
30
+
31
+        .logged-in-pannel {
32
+            padding: 10px;
33
+        }
34
+    }
35
+}
36
+
5 37
 .live-stream-dialog {
6 38
     /**
7 39
      * Set font-size to be consistent with Atlaskit FieldText.

+ 42
- 0
images/dropboxLogo.svg 查看文件

@@ -0,0 +1,42 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
3
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
+	 width="324px" height="63.8px" viewBox="0 0 324 63.8" style="enable-background:new 0 0 324 63.8;" xml:space="preserve">
5
+<style type="text/css">
6
+	.st0{fill:#0061FF;}
7
+	.st1{display:none;}
8
+	.st2{display:inline;}
9
+	.st3{fill:none;}
10
+</style>
11
+<path class="st0" d="M37.6,12L18.8,24l18.8,12L18.8,48L0,35.9l18.8-12L0,12L18.8,0L37.6,12z M18.7,51.8l18.8-12l18.8,12l-18.8,12
12
+	L18.7,51.8z M37.6,35.9l18.8-12L37.6,12L56.3,0l18.8,12L56.3,24l18.8,12L56.3,48L37.6,35.9z"/>
13
+<path d="M89.8,12H105c9.7,0,17.7,5.6,17.7,18.4v2.7c0,12.9-7.5,18.7-17.4,18.7H89.8V12z M98.3,19.2v25.3h6.5c5.5,0,9.2-3.6,9.2-11.6
14
+	v-2.1c0-8-3.9-11.6-9.5-11.6H98.3z M127.2,19.6h6.8l1.1,7.5c1.3-5.1,4.6-7.8,10.6-7.8h2.1v8.6h-3.5c-6.9,0-8.6,2.4-8.6,9.2v14.8
15
+	h-8.4V19.6H127.2z M149.5,36.4v-0.9c0-10.8,6.9-16.7,16.3-16.7c9.6,0,16.3,5.9,16.3,16.7v0.9c0,10.6-6.5,16.3-16.3,16.3
16
+	C155.4,52.6,149.5,47,149.5,36.4z M173.5,36.3v-0.8c0-6-3-9.6-7.8-9.6c-4.7,0-7.8,3.3-7.8,9.6v0.8c0,5.8,3,9.1,7.8,9.1
17
+	C170.5,45.3,173.5,42.1,173.5,36.3z M186.5,19.6h7l0.8,6.1c1.7-4.1,5.3-6.9,10.6-6.9c8.2,0,13.6,5.9,13.6,16.8v0.9
18
+	c0,10.6-6,16.2-13.6,16.2c-5.1,0-8.6-2.3-10.3-6V63h-8.2L186.5,19.6L186.5,19.6z M210,36.3v-0.7c0-6.4-3.3-9.6-7.7-9.6
19
+	c-4.7,0-7.8,3.6-7.8,9.6v0.6c0,5.7,3,9.3,7.7,9.3C207,45.4,210,42.3,210,36.3z M230.9,45.9l-0.7,5.9H223v-43h8.2v16.5
20
+	c1.8-4.2,5.4-6.5,10.5-6.5c7.7,0.1,13.4,5.4,13.4,16.1v1c0,10.7-5.4,16.8-13.6,16.8C236.1,52.6,232.6,50.1,230.9,45.9z M246.5,35.9
21
+	v-0.8c0-5.9-3.2-9.2-7.7-9.2c-4.6,0-7.8,3.7-7.8,9.3v0.7c0,6,3.1,9.5,7.7,9.5C243.6,45.4,246.5,42.3,246.5,35.9z M258.7,36.4v-0.9
22
+	c0-10.8,6.9-16.7,16.3-16.7c9.6,0,16.3,5.9,16.3,16.7v0.9c0,10.6-6.6,16.3-16.3,16.3C264.6,52.6,258.7,47,258.7,36.4z M282.8,36.3
23
+	v-0.8c0-6-3-9.6-7.8-9.6c-4.7,0-7.8,3.3-7.8,9.6v0.8c0,5.8,3,9.1,7.8,9.1C279.8,45.3,282.8,42.1,282.8,36.3z M302.3,35.1L291,19.6
24
+	h9.7l6.5,9.7l6.6-9.7h9.6L311.9,35L324,51.8h-9.5l-7.4-10.7l-7.2,10.7H290L302.3,35.1z"/>
25
+<g id="Editble" class="st1">
26
+	<g class="st2">
27
+		<rect x="-105" y="5" class="st3" width="506" height="71.8"/>
28
+		<path d="M0.2,13.6h16.3c10.4,0,19,6.1,19,19.8v2.9c0,13.8-8,20-18.7,20H0.2V13.6z M9.4,21.3v27.2h7c5.9,0,9.9-3.9,9.9-12.5v-2.2
29
+			c0-8.6-4.1-12.5-10.2-12.5H9.4z M40.4,21.8h7.3l1.1,8c1.4-5.5,4.9-8.3,11.3-8.3h2.2v9.2h-3.7c-7.4,0-9.2,2.6-9.2,9.9v15.8h-9
30
+			C40.4,56.4,40.4,21.8,40.4,21.8z M64.3,39.8v-1c0-11.6,7.4-17.9,17.5-17.9c10.3,0,17.5,6.4,17.5,17.9v1c0,11.4-7,17.5-17.5,17.5
31
+			C70.6,57.3,64.3,51.2,64.3,39.8z M90.1,39.7v-0.8c0-6.5-3.2-10.3-8.3-10.3c-5,0-8.4,3.5-8.4,10.3v0.8c0,6.2,3.2,9.7,8.3,9.7
32
+			C86.9,49.4,90.1,46,90.1,39.7z M104,21.8h7.6l0.9,6.6c1.9-4.4,5.7-7.4,11.4-7.4c8.8,0,14.6,6.4,14.6,18v1
33
+			c0,11.4-6.4,17.3-14.6,17.3c-5.5,0-9.2-2.5-11-6.5v17.5H104V21.8z M129.3,39.8V39c0-6.9-3.5-10.3-8.3-10.3c-5,0-8.4,3.8-8.4,10.3
34
+			v0.7c0,6.1,3.2,10,8.2,10C126,49.5,129.3,46.1,129.3,39.8z M151.7,50.1l-0.7,6.3h-7.8V10.2h8.8V28c1.9-4.5,5.8-7,11.2-7
35
+			c8.2,0.1,14.3,5.8,14.3,17.3v1c0,11.5-5.8,18-14.6,18C157.3,57.3,153.5,54.5,151.7,50.1z M168.5,39.3v-0.8c0-6.4-3.5-9.8-8.3-9.8
36
+			c-5,0-8.4,4-8.4,10v0.7c0,6.5,3.3,10.2,8.3,10.2C165.3,49.5,168.5,46.1,168.5,39.3z M181.6,39.8v-1c0-11.6,7.4-17.9,17.5-17.9
37
+			c10.3,0,17.5,6.4,17.5,17.9v1c0,11.4-7.1,17.5-17.5,17.5C187.9,57.3,181.6,51.2,181.6,39.8z M207.4,39.7v-0.8
38
+			c0-6.5-3.2-10.3-8.3-10.3c-5,0-8.4,3.5-8.4,10.3v0.8c0,6.2,3.2,9.7,8.3,9.7C204.2,49.4,207.4,46,207.4,39.7z M228.3,38.4
39
+			l-12.1-16.7h10.4l7,10.4l7.1-10.4H251l-12.3,16.6l13,18h-10.2l-8-11.5l-7.7,11.5h-10.6L228.3,38.4z"/>
40
+	</g>
41
+</g>
42
+</svg>

+ 5
- 0
lang/main.json 查看文件

@@ -456,7 +456,12 @@
456 456
         "on": "Recording",
457 457
         "pending": "Preparing to record the meeting...",
458 458
         "rec": "REC",
459
+        "authDropboxText": "To start the recording you need to first authorize our Dropbox recording app. After the recording is finished the file will be uploaded to your Dropbox account.",
460
+        "authDropboxCompletedText": "Our Dropbox app has been authorized successfully. You should see the recorded file in your Dropbox account shortly after the recording has finished.",
459 461
         "serviceName": "Recording service",
462
+        "signOut": "Sign Out",
463
+        "loggedIn": "Logged in as __userName__",
464
+        "availableSpace": "Available space: __spaceLeft__ MB (approximately __duration__ minutes of recording)",
460 465
         "startRecordingBody": "Are you sure you would like to start recording?",
461 466
         "unavailable": "Oops! The __serviceName__ is currently unavailable. We're working on resolving the issue. Please try again later.",
462 467
         "unavailableTitle": "Recording unavailable"

+ 9
- 0
package-lock.json 查看文件

@@ -6033,6 +6033,15 @@
6033 6033
         "domelementtype": "1"
6034 6034
       }
6035 6035
     },
6036
+    "dropbox": {
6037
+      "version": "4.0.9",
6038
+      "resolved": "https://registry.npmjs.org/dropbox/-/dropbox-4.0.9.tgz",
6039
+      "integrity": "sha512-UeaKw7DY24ZGLRV8xboZvbZXhbTVrFjPjfpr0LfF/KVOzBUad9vJJwqz3udqTLNxD0FXbFlC9rlNLLNXaj9msg==",
6040
+      "requires": {
6041
+        "buffer": "^5.0.8",
6042
+        "moment": "^2.19.3"
6043
+      }
6044
+    },
6036 6045
     "duplexify": {
6037 6046
       "version": "3.5.4",
6038 6047
       "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.4.tgz",

+ 1
- 0
package.json 查看文件

@@ -37,6 +37,7 @@
37 37
     "@microsoft/microsoft-graph-client": "1.1.0",
38 38
     "@webcomponents/url": "0.7.1",
39 39
     "autosize": "1.18.13",
40
+    "dropbox": "4.0.9",
40 41
     "i18next": "8.4.3",
41 42
     "i18next-browser-languagedetector": "2.0.0",
42 43
     "i18next-xhr-backend": "1.4.2",

+ 11
- 0
react/features/base/oauth/actionTypes.js 查看文件

@@ -0,0 +1,11 @@
1
+// @flow
2
+
3
+/**
4
+ * The type of (redux) action to update the dropbox access token.
5
+ *
6
+ * {
7
+ *     type: UPDATE_DROPBOX_TOKEN,
8
+ *     token: string
9
+ * }
10
+ */
11
+export const UPDATE_DROPBOX_TOKEN = Symbol('UPDATE_DROPBOX_TOKEN');

+ 49
- 0
react/features/base/oauth/actions.js 查看文件

@@ -0,0 +1,49 @@
1
+// @flow
2
+
3
+import { Dropbox } from 'dropbox';
4
+
5
+import { getLocationContextRoot, parseStandardURIString } from '../util';
6
+import { parseURLParams } from '../config';
7
+
8
+import { authorize } from './functions';
9
+import { UPDATE_DROPBOX_TOKEN } from './actionTypes';
10
+
11
+/**
12
+ * Action to authorize the Jitsi Recording app in dropbox.
13
+ *
14
+ * @returns {Function}
15
+ */
16
+export function authorizeDropbox() {
17
+    return (dispatch: Function, getState: Function) => {
18
+        const state = getState();
19
+        const { locationURL } = state['features/base/connection'];
20
+        const { dropbox } = state['features/base/config'];
21
+        const redirectURI = `${locationURL.origin
22
+            + getLocationContextRoot(locationURL)}static/oauth.html`;
23
+        const dropboxAPI = new Dropbox({ clientId: dropbox.clientId });
24
+        const url = dropboxAPI.getAuthenticationUrl(redirectURI);
25
+
26
+        authorize(url).then(returnUrl => {
27
+            const params
28
+                = parseURLParams(parseStandardURIString(returnUrl), true) || {};
29
+
30
+            dispatch(updateDropboxToken(params.access_token));
31
+        });
32
+    };
33
+}
34
+
35
+/**
36
+ * Action to update the dropbox access token.
37
+ *
38
+ * @param {string} token - The new token.
39
+ * @returns {{
40
+ *     type: UPDATE_DROPBOX_TOKEN,
41
+ *     token: string
42
+ * }}
43
+ */
44
+export function updateDropboxToken(token: string) {
45
+    return {
46
+        type: UPDATE_DROPBOX_TOKEN,
47
+        token
48
+    };
49
+}

+ 29
- 0
react/features/base/oauth/functions.js 查看文件

@@ -0,0 +1,29 @@
1
+// @flow
2
+
3
+import { getJitsiMeetGlobalNS } from '../util';
4
+
5
+
6
+/**
7
+ * Executes the oauth flow.
8
+ *
9
+ * @param {string} authUrl - The URL to oauth service.
10
+ * @returns {Promise<string>} - The URL with the authorization details.
11
+ */
12
+export function authorize(authUrl: string): Promise<string> {
13
+    const windowName = `oauth${Date.now()}`;
14
+    const gloabalNS = getJitsiMeetGlobalNS();
15
+
16
+    gloabalNS.oauthCallbacks = gloabalNS.oauthCallbacks || {};
17
+
18
+    return new Promise(resolve => {
19
+        const popup = window.open(authUrl, windowName);
20
+
21
+        gloabalNS.oauthCallbacks[windowName] = () => {
22
+            const returnURL = popup.location.href;
23
+
24
+            popup.close();
25
+            delete gloabalNS.oauthCallbacks.windowName;
26
+            resolve(returnURL);
27
+        };
28
+    });
29
+}

+ 3
- 0
react/features/base/oauth/index.js 查看文件

@@ -0,0 +1,3 @@
1
+export * from './actions';
2
+
3
+import './reducer';

+ 38
- 0
react/features/base/oauth/reducer.js 查看文件

@@ -0,0 +1,38 @@
1
+// @flow
2
+
3
+import { ReducerRegistry } from '../redux';
4
+import { PersistenceRegistry } from '../storage';
5
+
6
+import { UPDATE_DROPBOX_TOKEN } from './actionTypes';
7
+
8
+/**
9
+ * The default state.
10
+ */
11
+const DEFAULT_STATE = {
12
+    dropbox: {}
13
+};
14
+
15
+/**
16
+ * The redux subtree of this feature.
17
+ */
18
+const STORE_NAME = 'features/base/oauth';
19
+
20
+/**
21
+ * Sets up the persistence of the feature {@code oauth}.
22
+ */
23
+PersistenceRegistry.register(STORE_NAME);
24
+
25
+ReducerRegistry.register('features/base/oauth',
26
+(state = DEFAULT_STATE, action) => {
27
+    switch (action.type) {
28
+    case UPDATE_DROPBOX_TOKEN:
29
+        return {
30
+            ...state,
31
+            dropbox: {
32
+                token: action.token
33
+            }
34
+        };
35
+    default:
36
+        return state;
37
+    }
38
+});

+ 3
- 1
react/features/recording/components/Recording/AbstractRecordButton.js 查看文件

@@ -119,6 +119,7 @@ export function _mapStateToProps(state: Object, ownProps: Props): Object {
119 119
         // its own to be visible or not.
120 120
         const isModerator = isLocalParticipantModerator(state);
121 121
         const {
122
+            dropbox = {},
122 123
             enableFeaturesBasedOnToken,
123 124
             fileRecordingsEnabled
124 125
         } = state['features/base/config'];
@@ -127,7 +128,8 @@ export function _mapStateToProps(state: Object, ownProps: Props): Object {
127 128
         visible = isModerator
128 129
             && fileRecordingsEnabled
129 130
             && (!enableFeaturesBasedOnToken
130
-                || String(features.recording) === 'true');
131
+                || String(features.recording) === 'true')
132
+            && typeof dropbox.clientId === 'string';
131 133
     }
132 134
 
133 135
     return {

+ 0
- 103
react/features/recording/components/Recording/AbstractStartRecordingDialog.js 查看文件

@@ -1,103 +0,0 @@
1
-// @flow
2
-
3
-import React, { Component } from 'react';
4
-
5
-import {
6
-    createRecordingDialogEvent,
7
-    sendAnalytics
8
-} from '../../../analytics';
9
-import { Dialog } from '../../../base/dialog';
10
-import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
11
-
12
-export type Props = {
13
-
14
-    /**
15
-     * The {@code JitsiConference} for the current conference.
16
-     */
17
-    _conference: Object,
18
-
19
-    /**
20
-     * Invoked to obtain translated strings.
21
-     */
22
-    t: Function
23
-}
24
-
25
-/**
26
- * Abstract class for {@code StartRecordingDialog} components.
27
- */
28
-export default class AbstractStartRecordingDialog<P: Props>
29
-    extends Component<P> {
30
-    /**
31
-     * Initializes a new {@code StartRecordingDialog} instance.
32
-     *
33
-     * @inheritdoc
34
-     */
35
-    constructor(props: P) {
36
-        super(props);
37
-
38
-        // Bind event handler so it is only bound once for every instance.
39
-        this._onSubmit = this._onSubmit.bind(this);
40
-    }
41
-
42
-    /**
43
-     * Implements React's {@link Component#render()}.
44
-     *
45
-     * @inheritdoc
46
-     * @returns {ReactElement}
47
-     */
48
-    render() {
49
-        return (
50
-            <Dialog
51
-                okTitleKey = 'dialog.confirm'
52
-                onSubmit = { this._onSubmit }
53
-                titleKey = 'dialog.recording'
54
-                width = 'small'>
55
-                { this._renderDialogContent() }
56
-            </Dialog>
57
-        );
58
-    }
59
-
60
-    _onSubmit: () => boolean;
61
-
62
-    /**
63
-     * Starts a file recording session.
64
-     *
65
-     * @private
66
-     * @returns {boolean} - True (to note that the modal should be closed).
67
-     */
68
-    _onSubmit() {
69
-        sendAnalytics(
70
-            createRecordingDialogEvent('start', 'confirm.button')
71
-        );
72
-
73
-        this.props._conference.startRecording({
74
-            mode: JitsiRecordingConstants.mode.FILE
75
-        });
76
-
77
-        return true;
78
-    }
79
-
80
-    /**
81
-     * Renders the platform specific dialog content.
82
-     *
83
-     * @protected
84
-     * @returns {React$Component}
85
-     */
86
-    _renderDialogContent: () => React$Component<*>
87
-}
88
-
89
-/**
90
- * Maps (parts of) the Redux state to the associated props for the
91
- * {@code StartRecordingDialog} component.
92
- *
93
- * @param {Object} state - The Redux state.
94
- * @private
95
- * @returns {{
96
- *     _conference: JitsiConference
97
- * }}
98
- */
99
-export function _mapStateToProps(state: Object) {
100
-    return {
101
-        _conference: state['features/base/conference'].conference
102
-    };
103
-}

+ 231
- 0
react/features/recording/components/Recording/StartRecordingDialog.js 查看文件

@@ -0,0 +1,231 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { connect } from 'react-redux';
5
+
6
+import {
7
+    createRecordingDialogEvent,
8
+    sendAnalytics
9
+} from '../../../analytics';
10
+import { Dialog } from '../../../base/dialog';
11
+import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
12
+
13
+import StartRecordingDialogContent from './StartRecordingDialogContent';
14
+import { getDropboxData } from '../../functions';
15
+
16
+type Props = {
17
+
18
+    /**
19
+     * The {@code JitsiConference} for the current conference.
20
+     */
21
+    _conference: Object,
22
+
23
+    /**
24
+     * The client id for the dropbox authentication.
25
+     */
26
+    _clientId: string,
27
+
28
+    /**
29
+     * The dropbox access token.
30
+     */
31
+    _token: string,
32
+
33
+    /**
34
+     * The redux dispatch function.
35
+     */
36
+    dispatch: Function,
37
+
38
+    /**
39
+     * Invoked to obtain translated strings.
40
+     */
41
+    t: Function
42
+}
43
+
44
+
45
+type State = {
46
+
47
+    /**
48
+     * <tt>true</tt> if we have valid oauth token.
49
+     */
50
+    isTokenValid: boolean,
51
+
52
+    /**
53
+     * <tt>true</tt> if we are in process of validating the oauth token.
54
+     */
55
+    isValidating: boolean,
56
+
57
+    /**
58
+     * The display name of the user's Dropbox account.
59
+     */
60
+    userName: ?string,
61
+
62
+    /**
63
+     * Number of MiB of available space in user's Dropbox account.
64
+     */
65
+    spaceLeft: ?number
66
+};
67
+
68
+/**
69
+ * Component for the recording start dialog.
70
+ */
71
+class StartRecordingDialog extends Component<Props, State> {
72
+    /**
73
+     * Initializes a new {@code StartRecordingDialog} instance.
74
+     *
75
+     * @inheritdoc
76
+     */
77
+    constructor(props: Props) {
78
+        super(props);
79
+
80
+        // Bind event handler so it is only bound once for every instance.
81
+        this._onSubmit = this._onSubmit.bind(this);
82
+
83
+        this.state = {
84
+            isTokenValid: false,
85
+            isValidating: false,
86
+            userName: undefined,
87
+            spaceLeft: undefined
88
+        };
89
+    }
90
+
91
+    /**
92
+     * Validates the oauth access token.
93
+     *
94
+     * @inheritdoc
95
+     * @returns {void}
96
+     */
97
+    componentDidMount() {
98
+        if (typeof this.props._token !== 'undefined') {
99
+            this._onTokenUpdated();
100
+        }
101
+    }
102
+
103
+    /**
104
+     * Validates the oauth access token.
105
+     *
106
+     * @inheritdoc
107
+     * @returns {void}
108
+     */
109
+    componentDidUpdate(prevProps) {
110
+        if (this.props._token !== prevProps._token) {
111
+            this._onTokenUpdated();
112
+        }
113
+    }
114
+
115
+    /**
116
+     * Validates the dropbox access token and fetches account information.
117
+     *
118
+     * @returns {void}
119
+     */
120
+    _onTokenUpdated() {
121
+        const { _clientId, _token } = this.props;
122
+
123
+        if (typeof _token === 'undefined') {
124
+            this.setState({
125
+                isTokenValid: false,
126
+                isValidating: false
127
+            });
128
+        } else {
129
+            this.setState({
130
+                isTokenValid: false,
131
+                isValidating: true
132
+            });
133
+            getDropboxData(_token, _clientId).then(data => {
134
+                if (typeof data === 'undefined') {
135
+                    this.setState({
136
+                        isTokenValid: false,
137
+                        isValidating: false
138
+                    });
139
+                } else {
140
+                    this.setState({
141
+                        isTokenValid: true,
142
+                        isValidating: false,
143
+                        ...data
144
+                    });
145
+                }
146
+            });
147
+        }
148
+    }
149
+
150
+    /**
151
+     * Implements React's {@link Component#render()}.
152
+     *
153
+     * @inheritdoc
154
+     * @returns {ReactElement}
155
+     */
156
+    render() {
157
+        const { isTokenValid, isValidating, spaceLeft, userName } = this.state;
158
+
159
+        return (
160
+            <Dialog
161
+                okDisabled = { !isTokenValid }
162
+                okTitleKey = 'dialog.confirm'
163
+                onSubmit = { this._onSubmit }
164
+                titleKey = 'dialog.recording'
165
+                width = 'small'>
166
+                <StartRecordingDialogContent
167
+                    isTokenValid = { isTokenValid }
168
+                    isValidating = { isValidating }
169
+                    spaceLeft = { spaceLeft }
170
+                    userName = { userName } />
171
+            </Dialog>
172
+        );
173
+    }
174
+
175
+    _onSubmit: () => boolean;
176
+
177
+    /**
178
+     * Starts a file recording session.
179
+     *
180
+     * @private
181
+     * @returns {boolean} - True (to note that the modal should be closed).
182
+     */
183
+    _onSubmit() {
184
+        sendAnalytics(
185
+            createRecordingDialogEvent('start', 'confirm.button')
186
+        );
187
+        const { _conference, _token } = this.props;
188
+
189
+        _conference.startRecording({
190
+            mode: JitsiRecordingConstants.mode.FILE,
191
+            appData: JSON.stringify({
192
+                'file_recording_metadata': {
193
+                    'upload_credentials': {
194
+                        'service_name': 'dropbox',
195
+                        'token': _token
196
+                    }
197
+                }
198
+            })
199
+        });
200
+
201
+        return true;
202
+    }
203
+
204
+    /**
205
+     * Renders the platform specific dialog content.
206
+     *
207
+     * @protected
208
+     * @returns {React$Component}
209
+     */
210
+    _renderDialogContent: () => React$Component<*>
211
+}
212
+
213
+/**
214
+ * Maps (parts of) the Redux state to the associated props for the
215
+ * {@code StartRecordingDialog} component.
216
+ *
217
+ * @param {Object} state - The Redux state.
218
+ * @private
219
+ * @returns {{
220
+ *     _conference: JitsiConference
221
+ * }}
222
+ */
223
+function mapStateToProps(state: Object) {
224
+    return {
225
+        _conference: state['features/base/conference'].conference,
226
+        _token: state['features/base/oauth'].dropbox.token,
227
+        _clientId: state['features/base/config'].dropbox.clientId
228
+    };
229
+}
230
+
231
+export default connect(mapStateToProps)(StartRecordingDialog);

+ 0
- 33
react/features/recording/components/Recording/StartRecordingDialog.web.js 查看文件

@@ -1,33 +0,0 @@
1
-// @flow
2
-
3
-import { connect } from 'react-redux';
4
-
5
-import { translate } from '../../../base/i18n';
6
-
7
-import AbstractStartRecordingDialog, {
8
-    type Props,
9
-    _mapStateToProps
10
-} from './AbstractStartRecordingDialog';
11
-
12
-/**
13
- * React Component for getting confirmation to start a file recording session.
14
- *
15
- * @extends Component
16
- */
17
-class StartRecordingDialog extends AbstractStartRecordingDialog<Props> {
18
-    /**
19
-     * Renders the platform specific dialog content.
20
-     *
21
-     * @protected
22
-     * @returns {React$Component}
23
-     */
24
-    _renderDialogContent() {
25
-        const { t } = this.props;
26
-
27
-        return (
28
-            t('recording.startRecordingBody')
29
-        );
30
-    }
31
-}
32
-
33
-export default translate(connect(_mapStateToProps)(StartRecordingDialog));

react/features/recording/components/Recording/StartRecordingDialog.native.js → react/features/recording/components/Recording/StartRecordingDialogContent.native.js 查看文件

@@ -1,30 +1,32 @@
1 1
 // @flow
2 2
 
3
-import React from 'react';
3
+import React, { Component } from 'react';
4 4
 import { Text, View } from 'react-native';
5
-import { connect } from 'react-redux';
6 5
 
7 6
 import { translate } from '../../../base/i18n';
8 7
 
9 8
 import styles from '../styles';
10 9
 
11
-import AbstractStartRecordingDialog, {
12
-    type Props,
13
-    _mapStateToProps
14
-} from './AbstractStartRecordingDialog';
10
+type Props = {
11
+
12
+    /**
13
+     * Invoked to obtain translated strings.
14
+     */
15
+    t: Function
16
+};
15 17
 
16 18
 /**
17 19
  * React Component for getting confirmation to start a file recording session.
18 20
  *
19 21
  * @extends Component
20 22
  */
21
-class StartRecordingDialog extends AbstractStartRecordingDialog<Props> {
23
+class StartRecordingDialogContent extends Component<Props> {
22 24
     /**
23 25
      * Renders the platform specific dialog content.
24 26
      *
25
-     * @inheritdoc
27
+     * @returns {void}
26 28
      */
27
-    _renderDialogContent() {
29
+    render() {
28 30
         const { t } = this.props;
29 31
 
30 32
         return (
@@ -37,4 +39,4 @@ class StartRecordingDialog extends AbstractStartRecordingDialog<Props> {
37 39
     }
38 40
 }
39 41
 
40
-export default translate(connect(_mapStateToProps)(StartRecordingDialog));
42
+export default translate(StartRecordingDialogContent);

+ 179
- 0
react/features/recording/components/Recording/StartRecordingDialogContent.web.js 查看文件

@@ -0,0 +1,179 @@
1
+// @flow
2
+
3
+import Spinner from '@atlaskit/spinner';
4
+import React, { Component } from 'react';
5
+import { connect } from 'react-redux';
6
+
7
+import { translate } from '../../../base/i18n';
8
+import { authorizeDropbox, updateDropboxToken } from '../../../base/oauth';
9
+
10
+type Props = {
11
+
12
+    /**
13
+     * The redux dispatch function.
14
+     */
15
+    dispatch: Function,
16
+
17
+    /**
18
+     * <tt>true</tt> if we have valid oauth token.
19
+     */
20
+    isTokenValid: boolean,
21
+
22
+    /**
23
+     * <tt>true</tt> if we are in process of validating the oauth token.
24
+     */
25
+    isValidating: boolean,
26
+
27
+    /**
28
+     * Number of MiB of available space in user's Dropbox account.
29
+     */
30
+    spaceLeft: ?number,
31
+
32
+    /**
33
+     * The translate function.
34
+     */
35
+    t: Function,
36
+
37
+    /**
38
+     * The display name of the user's Dropbox account.
39
+     */
40
+    userName: ?string,
41
+};
42
+
43
+/**
44
+ * React Component for getting confirmation to start a file recording session.
45
+ *
46
+ * @extends Component
47
+ */
48
+class StartRecordingDialogContent extends Component<Props> {
49
+    /**
50
+     * Initializes a new {@code StartRecordingDialogContent} instance.
51
+     *
52
+     * @inheritdoc
53
+     */
54
+    constructor(props) {
55
+        super(props);
56
+
57
+        // Bind event handler so it is only bound once for every instance.
58
+        this._onSignInClick = this._onSignInClick.bind(this);
59
+        this._onSignOutClick = this._onSignOutClick.bind(this);
60
+    }
61
+
62
+    /**
63
+     * Renders the platform specific dialog content.
64
+     *
65
+     * @protected
66
+     * @returns {React$Component}
67
+     */
68
+    render() {
69
+        const { isTokenValid, isValidating, t } = this.props;
70
+
71
+        let content = null;
72
+
73
+        if (isValidating) {
74
+            content = this._renderSpinner();
75
+        } else if (isTokenValid) {
76
+            content = this._renderSignOut();
77
+        } else {
78
+            content = this._renderSignIn();
79
+        }
80
+
81
+        return (
82
+            <div className = 'recording-dialog'>
83
+                <div className = 'authorization-panel'>
84
+                    { content }
85
+                </div>
86
+                <div>{ t('recording.startRecordingBody') }</div>
87
+            </div>
88
+        );
89
+    }
90
+
91
+    /**
92
+     * Renders a spinner component.
93
+     *
94
+     * @returns {React$Component}
95
+     */
96
+    _renderSpinner() {
97
+        return (
98
+            <Spinner
99
+                isCompleting = { false }
100
+                size = 'medium' />
101
+        );
102
+    }
103
+
104
+    /**
105
+     * Renders the sign in screen.
106
+     *
107
+     * @returns {React$Component}
108
+     */
109
+    _renderSignIn() {
110
+        return (
111
+            <div>
112
+                <div>{ this.props.t('recording.authDropboxText') }</div>
113
+                <div
114
+                    className = 'dropbox-sign-in'
115
+                    onClick = { this._onSignInClick }>
116
+                    <img
117
+                        className = 'dropbox-logo'
118
+                        src = 'images/dropboxLogo.svg' />
119
+                </div>
120
+            </div>);
121
+    }
122
+
123
+    /**
124
+     * Renders the screen with the account information of a logged in user.
125
+     *
126
+     * @returns {React$Component}
127
+     */
128
+    _renderSignOut() {
129
+        const { spaceLeft, t, userName } = this.props;
130
+
131
+        return (
132
+            <div>
133
+                <div>{ t('recording.authDropboxCompletedText') }</div>
134
+                <div className = 'logged-in-pannel'>
135
+                    <div>
136
+                        { t('recording.loggedIn', { userName }) }&nbsp;(&nbsp;
137
+                        <a onClick = { this._onSignOutClick }>
138
+                            { t('recording.signOut') }
139
+                        </a>
140
+                        &nbsp;)
141
+                    </div>
142
+                    <div>
143
+                        {
144
+                            t('recording.availableSpace', {
145
+                                spaceLeft,
146
+
147
+                                // assuming 1min -> 10MB recording:
148
+                                duration: Math.floor((spaceLeft || 0) / 10)
149
+                            })
150
+                        }
151
+                    </div>
152
+                </div>
153
+            </div>);
154
+    }
155
+
156
+    _onSignInClick: () => {};
157
+
158
+    /**
159
+     * Handles click events for the dropbox sign in button.
160
+     *
161
+     * @returns {void}
162
+     */
163
+    _onSignInClick() {
164
+        this.props.dispatch(authorizeDropbox());
165
+    }
166
+
167
+    _onSignOutClick: () => {};
168
+
169
+    /**
170
+     * Sings out an user from dropbox.
171
+     *
172
+     * @returns {void}
173
+     */
174
+    _onSignOutClick() {
175
+        this.props.dispatch(updateDropboxToken());
176
+    }
177
+}
178
+
179
+export default translate(connect()(StartRecordingDialogContent));

+ 36
- 2
react/features/recording/functions.js 查看文件

@@ -1,3 +1,7 @@
1
+// @flow
2
+
3
+import { Dropbox } from 'dropbox';
4
+
1 5
 import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
2 6
 
3 7
 /**
@@ -8,7 +12,7 @@ import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
8 12
  * @param {string} mode - Find an active recording session of the given mode.
9 13
  * @returns {Object|undefined}
10 14
  */
11
-export function getActiveSession(state, mode) {
15
+export function getActiveSession(state: Object, mode: string) {
12 16
     const { sessionDatas } = state['features/recording'];
13 17
     const { status: statusConstants } = JitsiRecordingConstants;
14 18
 
@@ -25,7 +29,37 @@ export function getActiveSession(state, mode) {
25 29
  * @param {string} id - The ID of the recording session to find.
26 30
  * @returns {Object|undefined}
27 31
  */
28
-export function getSessionById(state, id) {
32
+export function getSessionById(state: Object, id: string) {
29 33
     return state['features/recording'].sessionDatas.find(
30 34
         sessionData => sessionData.id === id);
31 35
 }
36
+
37
+/**
38
+ * Fetches information about the user's dropbox account.
39
+ *
40
+ * @param {string} token - The dropbox access token.
41
+ * @param {string} clientId - The Jitsi Recorder dropbox app ID.
42
+ * @returns {Promise<Object|undefined>}
43
+ */
44
+export function getDropboxData(
45
+        token: string,
46
+        clientId: string
47
+): Promise<?Object> {
48
+    const dropboxAPI = new Dropbox({
49
+        accessToken: token,
50
+        clientId
51
+    });
52
+
53
+    return Promise.all(
54
+        [ dropboxAPI.usersGetCurrentAccount(), dropboxAPI.usersGetSpaceUsage() ]
55
+    ).then(([ account, space ]) => {
56
+        const { allocation, used } = space;
57
+        const { allocated } = allocation;
58
+
59
+        return {
60
+            userName: account.name.display_name,
61
+            spaceLeft: Math.floor((allocated - used) / 1048576)// 1MiB=1048576B
62
+        };
63
+
64
+    }, () => undefined);
65
+}

+ 35
- 0
static/oauth.html 查看文件

@@ -0,0 +1,35 @@
1
+<html itemscope itemtype="http://schema.org/Product" prefix="og: http://ogp.me/ns#" xmlns="http://www.w3.org/1999/html">
2
+<head>
3
+    <meta charset="utf-8">
4
+    <meta http-equiv="content-type" content="text/html;charset=utf-8">
5
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+    <!--#include virtual="/base.html" -->
7
+    <!--#include virtual="/title.html" -->
8
+    <script>
9
+        function getParentWindowCallback() {
10
+            var windowName = window.name;
11
+            var parentWindow = window.opener;
12
+            if (parentWindow
13
+                && parentWindow.JitsiMeetJS
14
+                && parentWindow.JitsiMeetJS.app) {
15
+                var globalNS = parentWindow.JitsiMeetJS.app;
16
+                if( globalNS.oauthCallbacks
17
+                    && typeof globalNS.oauthCallbacks[windowName]
18
+                        === 'function') {
19
+                    return globalNS.oauthCallbacks[windowName];
20
+                }
21
+            }
22
+
23
+            return undefined;
24
+        }
25
+
26
+        var callback = getParentWindowCallback();
27
+        if (typeof callback === 'function') {
28
+            callback();
29
+        } else {
30
+            alert('Something went wrong!');
31
+        }
32
+    </script>
33
+</head>
34
+<body />
35
+</html>

正在加载...
取消
保存