feat(recording): Implement dropbox integration

This commit is contained in:
hristoterezov 2018-08-09 18:00:03 -05:00 committed by yanas
parent f10d42f8e4
commit df0e107ea6
18 changed files with 715 additions and 149 deletions

View File

@ -2,6 +2,38 @@
vertical-align: top;
}
.recording-dialog {
.authorization-panel {
align-items: center;
border-bottom: 2px solid rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
margin-bottom: 10px;
padding-bottom: 10px;
.dropbox-sign-in {
background-color: #4285f4;
border-radius: 2px;
cursor: pointer;
display: inline-flex;
padding: 1px;
margin: 10px 0px;
.dropbox-logo {
background-color: white;
border-radius: 2px;
display: inline-block;
padding: 8px;
height: 18px;
}
}
.logged-in-pannel {
padding: 10px;
}
}
}
.live-stream-dialog {
/**
* Set font-size to be consistent with Atlaskit FieldText.

42
images/dropboxLogo.svg Normal file
View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<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"
width="324px" height="63.8px" viewBox="0 0 324 63.8" style="enable-background:new 0 0 324 63.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#0061FF;}
.st1{display:none;}
.st2{display:inline;}
.st3{fill:none;}
</style>
<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
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"/>
<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
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
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
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
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
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
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
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
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
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
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
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"/>
<g id="Editble" class="st1">
<g class="st2">
<rect x="-105" y="5" class="st3" width="506" height="71.8"/>
<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
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
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
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
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
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
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
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
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
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
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
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"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -456,7 +456,12 @@
"on": "Recording",
"pending": "Preparing to record the meeting...",
"rec": "REC",
"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.",
"authDropboxCompletedText": "Our Dropbox app has been authorized successfully. You should see the recorded file in your Dropbox account shortly after the recording has finished.",
"serviceName": "Recording service",
"signOut": "Sign Out",
"loggedIn": "Logged in as __userName__",
"availableSpace": "Available space: __spaceLeft__ MB (approximately __duration__ minutes of recording)",
"startRecordingBody": "Are you sure you would like to start recording?",
"unavailable": "Oops! The __serviceName__ is currently unavailable. We're working on resolving the issue. Please try again later.",
"unavailableTitle": "Recording unavailable"

9
package-lock.json generated
View File

@ -6033,6 +6033,15 @@
"domelementtype": "1"
}
},
"dropbox": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/dropbox/-/dropbox-4.0.9.tgz",
"integrity": "sha512-UeaKw7DY24ZGLRV8xboZvbZXhbTVrFjPjfpr0LfF/KVOzBUad9vJJwqz3udqTLNxD0FXbFlC9rlNLLNXaj9msg==",
"requires": {
"buffer": "^5.0.8",
"moment": "^2.19.3"
}
},
"duplexify": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.4.tgz",

View File

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

View File

@ -0,0 +1,11 @@
// @flow
/**
* The type of (redux) action to update the dropbox access token.
*
* {
* type: UPDATE_DROPBOX_TOKEN,
* token: string
* }
*/
export const UPDATE_DROPBOX_TOKEN = Symbol('UPDATE_DROPBOX_TOKEN');

View File

@ -0,0 +1,49 @@
// @flow
import { Dropbox } from 'dropbox';
import { getLocationContextRoot, parseStandardURIString } from '../util';
import { parseURLParams } from '../config';
import { authorize } from './functions';
import { UPDATE_DROPBOX_TOKEN } from './actionTypes';
/**
* Action to authorize the Jitsi Recording app in dropbox.
*
* @returns {Function}
*/
export function authorizeDropbox() {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { locationURL } = state['features/base/connection'];
const { dropbox } = state['features/base/config'];
const redirectURI = `${locationURL.origin
+ getLocationContextRoot(locationURL)}static/oauth.html`;
const dropboxAPI = new Dropbox({ clientId: dropbox.clientId });
const url = dropboxAPI.getAuthenticationUrl(redirectURI);
authorize(url).then(returnUrl => {
const params
= parseURLParams(parseStandardURIString(returnUrl), true) || {};
dispatch(updateDropboxToken(params.access_token));
});
};
}
/**
* Action to update the dropbox access token.
*
* @param {string} token - The new token.
* @returns {{
* type: UPDATE_DROPBOX_TOKEN,
* token: string
* }}
*/
export function updateDropboxToken(token: string) {
return {
type: UPDATE_DROPBOX_TOKEN,
token
};
}

View File

@ -0,0 +1,29 @@
// @flow
import { getJitsiMeetGlobalNS } from '../util';
/**
* Executes the oauth flow.
*
* @param {string} authUrl - The URL to oauth service.
* @returns {Promise<string>} - The URL with the authorization details.
*/
export function authorize(authUrl: string): Promise<string> {
const windowName = `oauth${Date.now()}`;
const gloabalNS = getJitsiMeetGlobalNS();
gloabalNS.oauthCallbacks = gloabalNS.oauthCallbacks || {};
return new Promise(resolve => {
const popup = window.open(authUrl, windowName);
gloabalNS.oauthCallbacks[windowName] = () => {
const returnURL = popup.location.href;
popup.close();
delete gloabalNS.oauthCallbacks.windowName;
resolve(returnURL);
};
});
}

View File

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

View File

@ -0,0 +1,38 @@
// @flow
import { ReducerRegistry } from '../redux';
import { PersistenceRegistry } from '../storage';
import { UPDATE_DROPBOX_TOKEN } from './actionTypes';
/**
* The default state.
*/
const DEFAULT_STATE = {
dropbox: {}
};
/**
* The redux subtree of this feature.
*/
const STORE_NAME = 'features/base/oauth';
/**
* Sets up the persistence of the feature {@code oauth}.
*/
PersistenceRegistry.register(STORE_NAME);
ReducerRegistry.register('features/base/oauth',
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case UPDATE_DROPBOX_TOKEN:
return {
...state,
dropbox: {
token: action.token
}
};
default:
return state;
}
});

View File

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

View File

@ -1,103 +0,0 @@
// @flow
import React, { Component } from 'react';
import {
createRecordingDialogEvent,
sendAnalytics
} from '../../../analytics';
import { Dialog } from '../../../base/dialog';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
export type Props = {
/**
* The {@code JitsiConference} for the current conference.
*/
_conference: Object,
/**
* Invoked to obtain translated strings.
*/
t: Function
}
/**
* Abstract class for {@code StartRecordingDialog} components.
*/
export default class AbstractStartRecordingDialog<P: Props>
extends Component<P> {
/**
* Initializes a new {@code StartRecordingDialog} instance.
*
* @inheritdoc
*/
constructor(props: P) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onSubmit = this._onSubmit.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Dialog
okTitleKey = 'dialog.confirm'
onSubmit = { this._onSubmit }
titleKey = 'dialog.recording'
width = 'small'>
{ this._renderDialogContent() }
</Dialog>
);
}
_onSubmit: () => boolean;
/**
* Starts a file recording session.
*
* @private
* @returns {boolean} - True (to note that the modal should be closed).
*/
_onSubmit() {
sendAnalytics(
createRecordingDialogEvent('start', 'confirm.button')
);
this.props._conference.startRecording({
mode: JitsiRecordingConstants.mode.FILE
});
return true;
}
/**
* Renders the platform specific dialog content.
*
* @protected
* @returns {React$Component}
*/
_renderDialogContent: () => React$Component<*>
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code StartRecordingDialog} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _conference: JitsiConference
* }}
*/
export function _mapStateToProps(state: Object) {
return {
_conference: state['features/base/conference'].conference
};
}

View File

@ -0,0 +1,231 @@
// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
createRecordingDialogEvent,
sendAnalytics
} from '../../../analytics';
import { Dialog } from '../../../base/dialog';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import StartRecordingDialogContent from './StartRecordingDialogContent';
import { getDropboxData } from '../../functions';
type Props = {
/**
* The {@code JitsiConference} for the current conference.
*/
_conference: Object,
/**
* The client id for the dropbox authentication.
*/
_clientId: string,
/**
* The dropbox access token.
*/
_token: string,
/**
* The redux dispatch function.
*/
dispatch: Function,
/**
* Invoked to obtain translated strings.
*/
t: Function
}
type State = {
/**
* <tt>true</tt> if we have valid oauth token.
*/
isTokenValid: boolean,
/**
* <tt>true</tt> if we are in process of validating the oauth token.
*/
isValidating: boolean,
/**
* The display name of the user's Dropbox account.
*/
userName: ?string,
/**
* Number of MiB of available space in user's Dropbox account.
*/
spaceLeft: ?number
};
/**
* Component for the recording start dialog.
*/
class StartRecordingDialog extends Component<Props, State> {
/**
* Initializes a new {@code StartRecordingDialog} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onSubmit = this._onSubmit.bind(this);
this.state = {
isTokenValid: false,
isValidating: false,
userName: undefined,
spaceLeft: undefined
};
}
/**
* Validates the oauth access token.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
if (typeof this.props._token !== 'undefined') {
this._onTokenUpdated();
}
}
/**
* Validates the oauth access token.
*
* @inheritdoc
* @returns {void}
*/
componentDidUpdate(prevProps) {
if (this.props._token !== prevProps._token) {
this._onTokenUpdated();
}
}
/**
* Validates the dropbox access token and fetches account information.
*
* @returns {void}
*/
_onTokenUpdated() {
const { _clientId, _token } = this.props;
if (typeof _token === 'undefined') {
this.setState({
isTokenValid: false,
isValidating: false
});
} else {
this.setState({
isTokenValid: false,
isValidating: true
});
getDropboxData(_token, _clientId).then(data => {
if (typeof data === 'undefined') {
this.setState({
isTokenValid: false,
isValidating: false
});
} else {
this.setState({
isTokenValid: true,
isValidating: false,
...data
});
}
});
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { isTokenValid, isValidating, spaceLeft, userName } = this.state;
return (
<Dialog
okDisabled = { !isTokenValid }
okTitleKey = 'dialog.confirm'
onSubmit = { this._onSubmit }
titleKey = 'dialog.recording'
width = 'small'>
<StartRecordingDialogContent
isTokenValid = { isTokenValid }
isValidating = { isValidating }
spaceLeft = { spaceLeft }
userName = { userName } />
</Dialog>
);
}
_onSubmit: () => boolean;
/**
* Starts a file recording session.
*
* @private
* @returns {boolean} - True (to note that the modal should be closed).
*/
_onSubmit() {
sendAnalytics(
createRecordingDialogEvent('start', 'confirm.button')
);
const { _conference, _token } = this.props;
_conference.startRecording({
mode: JitsiRecordingConstants.mode.FILE,
appData: JSON.stringify({
'file_recording_metadata': {
'upload_credentials': {
'service_name': 'dropbox',
'token': _token
}
}
})
});
return true;
}
/**
* Renders the platform specific dialog content.
*
* @protected
* @returns {React$Component}
*/
_renderDialogContent: () => React$Component<*>
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code StartRecordingDialog} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _conference: JitsiConference
* }}
*/
function mapStateToProps(state: Object) {
return {
_conference: state['features/base/conference'].conference,
_token: state['features/base/oauth'].dropbox.token,
_clientId: state['features/base/config'].dropbox.clientId
};
}
export default connect(mapStateToProps)(StartRecordingDialog);

View File

@ -1,33 +0,0 @@
// @flow
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n';
import AbstractStartRecordingDialog, {
type Props,
_mapStateToProps
} from './AbstractStartRecordingDialog';
/**
* React Component for getting confirmation to start a file recording session.
*
* @extends Component
*/
class StartRecordingDialog extends AbstractStartRecordingDialog<Props> {
/**
* Renders the platform specific dialog content.
*
* @protected
* @returns {React$Component}
*/
_renderDialogContent() {
const { t } = this.props;
return (
t('recording.startRecordingBody')
);
}
}
export default translate(connect(_mapStateToProps)(StartRecordingDialog));

View File

@ -1,30 +1,32 @@
// @flow
import React from 'react';
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n';
import styles from '../styles';
import AbstractStartRecordingDialog, {
type Props,
_mapStateToProps
} from './AbstractStartRecordingDialog';
type Props = {
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* React Component for getting confirmation to start a file recording session.
*
* @extends Component
*/
class StartRecordingDialog extends AbstractStartRecordingDialog<Props> {
class StartRecordingDialogContent extends Component<Props> {
/**
* Renders the platform specific dialog content.
*
* @inheritdoc
* @returns {void}
*/
_renderDialogContent() {
render() {
const { t } = this.props;
return (
@ -37,4 +39,4 @@ class StartRecordingDialog extends AbstractStartRecordingDialog<Props> {
}
}
export default translate(connect(_mapStateToProps)(StartRecordingDialog));
export default translate(StartRecordingDialogContent);

View File

@ -0,0 +1,179 @@
// @flow
import Spinner from '@atlaskit/spinner';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n';
import { authorizeDropbox, updateDropboxToken } from '../../../base/oauth';
type Props = {
/**
* The redux dispatch function.
*/
dispatch: Function,
/**
* <tt>true</tt> if we have valid oauth token.
*/
isTokenValid: boolean,
/**
* <tt>true</tt> if we are in process of validating the oauth token.
*/
isValidating: boolean,
/**
* Number of MiB of available space in user's Dropbox account.
*/
spaceLeft: ?number,
/**
* The translate function.
*/
t: Function,
/**
* The display name of the user's Dropbox account.
*/
userName: ?string,
};
/**
* React Component for getting confirmation to start a file recording session.
*
* @extends Component
*/
class StartRecordingDialogContent extends Component<Props> {
/**
* Initializes a new {@code StartRecordingDialogContent} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onSignInClick = this._onSignInClick.bind(this);
this._onSignOutClick = this._onSignOutClick.bind(this);
}
/**
* Renders the platform specific dialog content.
*
* @protected
* @returns {React$Component}
*/
render() {
const { isTokenValid, isValidating, t } = this.props;
let content = null;
if (isValidating) {
content = this._renderSpinner();
} else if (isTokenValid) {
content = this._renderSignOut();
} else {
content = this._renderSignIn();
}
return (
<div className = 'recording-dialog'>
<div className = 'authorization-panel'>
{ content }
</div>
<div>{ t('recording.startRecordingBody') }</div>
</div>
);
}
/**
* Renders a spinner component.
*
* @returns {React$Component}
*/
_renderSpinner() {
return (
<Spinner
isCompleting = { false }
size = 'medium' />
);
}
/**
* Renders the sign in screen.
*
* @returns {React$Component}
*/
_renderSignIn() {
return (
<div>
<div>{ this.props.t('recording.authDropboxText') }</div>
<div
className = 'dropbox-sign-in'
onClick = { this._onSignInClick }>
<img
className = 'dropbox-logo'
src = 'images/dropboxLogo.svg' />
</div>
</div>);
}
/**
* Renders the screen with the account information of a logged in user.
*
* @returns {React$Component}
*/
_renderSignOut() {
const { spaceLeft, t, userName } = this.props;
return (
<div>
<div>{ t('recording.authDropboxCompletedText') }</div>
<div className = 'logged-in-pannel'>
<div>
{ t('recording.loggedIn', { userName }) }&nbsp;(&nbsp;
<a onClick = { this._onSignOutClick }>
{ t('recording.signOut') }
</a>
&nbsp;)
</div>
<div>
{
t('recording.availableSpace', {
spaceLeft,
// assuming 1min -> 10MB recording:
duration: Math.floor((spaceLeft || 0) / 10)
})
}
</div>
</div>
</div>);
}
_onSignInClick: () => {};
/**
* Handles click events for the dropbox sign in button.
*
* @returns {void}
*/
_onSignInClick() {
this.props.dispatch(authorizeDropbox());
}
_onSignOutClick: () => {};
/**
* Sings out an user from dropbox.
*
* @returns {void}
*/
_onSignOutClick() {
this.props.dispatch(updateDropboxToken());
}
}
export default translate(connect()(StartRecordingDialogContent));

View File

@ -1,3 +1,7 @@
// @flow
import { Dropbox } from 'dropbox';
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
/**
@ -8,7 +12,7 @@ import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
* @param {string} mode - Find an active recording session of the given mode.
* @returns {Object|undefined}
*/
export function getActiveSession(state, mode) {
export function getActiveSession(state: Object, mode: string) {
const { sessionDatas } = state['features/recording'];
const { status: statusConstants } = JitsiRecordingConstants;
@ -25,7 +29,37 @@ export function getActiveSession(state, mode) {
* @param {string} id - The ID of the recording session to find.
* @returns {Object|undefined}
*/
export function getSessionById(state, id) {
export function getSessionById(state: Object, id: string) {
return state['features/recording'].sessionDatas.find(
sessionData => sessionData.id === id);
}
/**
* Fetches information about the user's dropbox account.
*
* @param {string} token - The dropbox access token.
* @param {string} clientId - The Jitsi Recorder dropbox app ID.
* @returns {Promise<Object|undefined>}
*/
export function getDropboxData(
token: string,
clientId: string
): Promise<?Object> {
const dropboxAPI = new Dropbox({
accessToken: token,
clientId
});
return Promise.all(
[ dropboxAPI.usersGetCurrentAccount(), dropboxAPI.usersGetSpaceUsage() ]
).then(([ account, space ]) => {
const { allocation, used } = space;
const { allocated } = allocation;
return {
userName: account.name.display_name,
spaceLeft: Math.floor((allocated - used) / 1048576)// 1MiB=1048576B
};
}, () => undefined);
}

35
static/oauth.html Normal file
View File

@ -0,0 +1,35 @@
<html itemscope itemtype="http://schema.org/Product" prefix="og: http://ogp.me/ns#" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="utf-8">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--#include virtual="/base.html" -->
<!--#include virtual="/title.html" -->
<script>
function getParentWindowCallback() {
var windowName = window.name;
var parentWindow = window.opener;
if (parentWindow
&& parentWindow.JitsiMeetJS
&& parentWindow.JitsiMeetJS.app) {
var globalNS = parentWindow.JitsiMeetJS.app;
if( globalNS.oauthCallbacks
&& typeof globalNS.oauthCallbacks[windowName]
=== 'function') {
return globalNS.oauthCallbacks[windowName];
}
}
return undefined;
}
var callback = getParentWindowCallback();
if (typeof callback === 'function') {
callback();
} else {
alert('Something went wrong!');
}
</script>
</head>
<body />
</html>