feat(recording): Implement dropbox integration
This commit is contained in:
parent
f10d42f8e4
commit
df0e107ea6
|
@ -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.
|
||||
|
|
|
@ -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 |
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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');
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
});
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './actions';
|
||||
|
||||
import './reducer';
|
|
@ -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;
|
||||
}
|
||||
});
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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);
|
|
@ -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));
|
|
@ -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);
|
|
@ -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 }) } (
|
||||
<a onClick = { this._onSignOutClick }>
|
||||
{ t('recording.signOut') }
|
||||
</a>
|
||||
)
|
||||
</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));
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue