mirror of https://github.com/jitsi/jitsi-meet
This involves redesign of the web recording dialog in order to look the same as the mobile one.pull/3474/merge
parent
ae7a882188
commit
af37141e3d
@ -0,0 +1,50 @@ |
||||
// @flow
|
||||
export * from './functions'; |
||||
|
||||
import { getDisplayName, getSpaceUsage } from './functions'; |
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename); |
||||
|
||||
/** |
||||
* Information related to the user's dropbox account. |
||||
*/ |
||||
type DropboxUserData = { |
||||
|
||||
/** |
||||
* The available space left in MB into the user's Dropbox account. |
||||
*/ |
||||
spaceLeft: number, |
||||
|
||||
/** |
||||
* The display name of the user in Dropbox. |
||||
*/ |
||||
userName: string |
||||
}; |
||||
|
||||
/** |
||||
* 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<DropboxUserData|undefined>} |
||||
*/ |
||||
export function getDropboxData( |
||||
token: string, |
||||
clientId: string |
||||
): Promise<?DropboxUserData> { |
||||
return Promise.all( |
||||
[ getDisplayName(token, clientId), getSpaceUsage(token, clientId) ] |
||||
).then(([ userName, space ]) => { |
||||
const { allocated, used } = space; |
||||
|
||||
return { |
||||
userName, |
||||
spaceLeft: Math.floor((allocated - used) / 1048576)// 1MiB=1048576B
|
||||
}; |
||||
|
||||
}, error => { |
||||
logger.error(error); |
||||
|
||||
return undefined; |
||||
}); |
||||
} |
@ -1,39 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import { Dropbox } from 'dropbox'; |
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename); |
||||
|
||||
/** |
||||
* 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
|
||||
}; |
||||
|
||||
}, error => { |
||||
logger.error(error); |
||||
|
||||
return undefined; |
||||
}); |
||||
} |
@ -0,0 +1,52 @@ |
||||
// @flow
|
||||
|
||||
import { NativeModules } from 'react-native'; |
||||
|
||||
const { Dropbox } = NativeModules; |
||||
|
||||
/** |
||||
* Returns the display name for the current dropbox account. |
||||
* |
||||
* @param {string} token - The dropbox access token. |
||||
* @returns {Promise<string>} - The promise will be resolved with the display |
||||
* name or rejected with an error. |
||||
*/ |
||||
export function getDisplayName(token: string) { |
||||
return Dropbox.getDisplayName(token); |
||||
} |
||||
|
||||
/** |
||||
* Returns information about the space usage for the current dropbox account. |
||||
* |
||||
* @param {string} token - The dropbox access token. |
||||
* @returns {Promise<{ used: number, allocated: number}>} - The promise will be |
||||
* resolved with the object with information about the space usage (the used |
||||
* space and the allocated space) for the current dropbox account or rejected |
||||
* with an error. |
||||
*/ |
||||
export function getSpaceUsage(token: string) { |
||||
return Dropbox.getSpaceUsage(token); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Action to authorize the Jitsi Recording app in dropbox. |
||||
* |
||||
* @param {string} clientId - The Jitsi Recorder dropbox app ID. |
||||
* @param {string} redirectURI - The return URL. |
||||
* @returns {Promise<string>} - The promise will be resolved with the dropbox |
||||
* access token or rejected with an error. |
||||
*/ |
||||
export function _authorizeDropbox(): Promise<string> { |
||||
return Dropbox.authorize(); |
||||
} |
||||
|
||||
/** |
||||
* Returns <tt>true</tt> if the dropbox features is enabled and <tt>false</tt> |
||||
* otherwise. |
||||
* |
||||
* @returns {boolean} |
||||
*/ |
||||
export function isEnabled() { |
||||
return Dropbox.ENABLED; |
||||
} |
@ -0,0 +1,112 @@ |
||||
// @flow
|
||||
|
||||
import { Dropbox } from 'dropbox'; |
||||
|
||||
import { |
||||
getJitsiMeetGlobalNS, |
||||
parseStandardURIString |
||||
} from '../base/util'; |
||||
import { parseURLParams } from '../base/config'; |
||||
|
||||
/** |
||||
* Returns the display name for the current dropbox account. |
||||
* |
||||
* @param {string} token - The dropbox access token. |
||||
* @param {string} clientId - The Jitsi Recorder dropbox app ID. |
||||
* @returns {Promise<string>} |
||||
*/ |
||||
export function getDisplayName(token: string, clientId: string) { |
||||
const dropboxAPI = new Dropbox({ |
||||
accessToken: token, |
||||
clientId |
||||
}); |
||||
|
||||
return ( |
||||
dropboxAPI.usersGetCurrentAccount() |
||||
.then(account => account.name.display_name)); |
||||
} |
||||
|
||||
/** |
||||
* Returns information about the space usage for the current dropbox account. |
||||
* |
||||
* @param {string} token - The dropbox access token. |
||||
* @param {string} clientId - The Jitsi Recorder dropbox app ID. |
||||
* @returns {Promise<Object>} |
||||
*/ |
||||
export function getSpaceUsage(token: string, clientId: string) { |
||||
const dropboxAPI = new Dropbox({ |
||||
accessToken: token, |
||||
clientId |
||||
}); |
||||
|
||||
return dropboxAPI.usersGetSpaceUsage().then(space => { |
||||
const { allocation, used } = space; |
||||
const { allocated } = allocation; |
||||
|
||||
return { |
||||
used, |
||||
allocated |
||||
}; |
||||
}); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Executes the oauth flow. |
||||
* |
||||
* @param {string} authUrl - The URL to oauth service. |
||||
* @returns {Promise<string>} - The URL with the authorization details. |
||||
*/ |
||||
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); |
||||
}; |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Action to authorize the Jitsi Recording app in dropbox. |
||||
* |
||||
* @param {string} clientId - The Jitsi Recorder dropbox app ID. |
||||
* @param {string} redirectURI - The return URL. |
||||
* @returns {Promise<string>} |
||||
*/ |
||||
export function _authorizeDropbox( |
||||
clientId: string, |
||||
redirectURI: string |
||||
): Promise<string> { |
||||
const dropboxAPI = new Dropbox({ clientId }); |
||||
const url = dropboxAPI.getAuthenticationUrl(redirectURI); |
||||
|
||||
return authorize(url).then(returnUrl => { |
||||
const params |
||||
= parseURLParams(parseStandardURIString(returnUrl), true) || {}; |
||||
|
||||
return params.access_token; |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Returns <tt>true</tt> if the dropbox features is enabled and <tt>false</tt> |
||||
* otherwise. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @returns {boolean} |
||||
*/ |
||||
export function isEnabled(state: Object) { |
||||
const { dropbox = {} } = state['features/base/config']; |
||||
|
||||
return typeof dropbox.clientId === 'string'; |
||||
} |
@ -1,4 +1,4 @@ |
||||
export * from './actions'; |
||||
export * from './functions'; |
||||
export * from './functions.any'; |
||||
|
||||
import './reducer'; |
||||
|
@ -0,0 +1,216 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { |
||||
createRecordingDialogEvent, |
||||
sendAnalytics |
||||
} from '../../../analytics'; |
||||
import { translate } from '../../../base/i18n'; |
||||
import { |
||||
Container, |
||||
LoadingIndicator, |
||||
Switch, |
||||
Text |
||||
} from '../../../base/react'; |
||||
import { authorizeDropbox, updateDropboxToken } from '../../../dropbox'; |
||||
|
||||
import styles from './styles'; |
||||
import { getRecordingDurationEstimation } from '../../functions'; |
||||
|
||||
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._signIn = this._signIn.bind(this); |
||||
this._signOut = this._signOut.bind(this); |
||||
this._onSwitchChange = this._onSwitchChange.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Renders the component. |
||||
* |
||||
* @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 { // Sign in screen:
|
||||
// We don't need to render any additional information.
|
||||
// }
|
||||
|
||||
return ( |
||||
<Container |
||||
className = 'recording-dialog' |
||||
style = { styles.container }> |
||||
<Container |
||||
className = 'recording-header' |
||||
style = { styles.header }> |
||||
<Text |
||||
className = 'recording-title' |
||||
style = { styles.title }> |
||||
{ t('recording.authDropboxText') } |
||||
</Text> |
||||
<Switch |
||||
disabled = { isValidating } |
||||
onValueChange = { this._onSwitchChange } |
||||
style = { styles.switch } |
||||
value = { isTokenValid } /> |
||||
</Container> |
||||
<Container |
||||
className = 'authorization-panel'> |
||||
{ content } |
||||
</Container> |
||||
</Container> |
||||
); |
||||
} |
||||
|
||||
_onSwitchChange: boolean => void; |
||||
|
||||
/** |
||||
* Handler for onValueChange events from the Switch component. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_onSwitchChange() { |
||||
if (this.props.isTokenValid) { |
||||
this._signOut(); |
||||
} else { |
||||
this._signIn(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Renders a spinner component. |
||||
* |
||||
* @returns {React$Component} |
||||
*/ |
||||
_renderSpinner() { |
||||
return ( |
||||
<LoadingIndicator |
||||
isCompleting = { false } |
||||
size = 'medium' /> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Renders the screen with the account information of a logged in user. |
||||
* |
||||
* @returns {React$Component} |
||||
*/ |
||||
_renderSignOut() { |
||||
const { spaceLeft, t, userName } = this.props; |
||||
const duration = getRecordingDurationEstimation(spaceLeft); |
||||
|
||||
return ( |
||||
<Container> |
||||
<Container |
||||
className = 'logged-in-panel' |
||||
style = { styles.loggedIn }> |
||||
<Container> |
||||
<Text> |
||||
{ t('recording.loggedIn', { userName }) } |
||||
</Text> |
||||
</Container> |
||||
<Container> |
||||
<Text> |
||||
{ |
||||
t('recording.availableSpace', { |
||||
spaceLeft, |
||||
duration |
||||
}) |
||||
} |
||||
</Text> |
||||
</Container> |
||||
</Container> |
||||
<Container style = { styles.startRecordingText }> |
||||
<Text>{ t('recording.startRecordingBody') }</Text> |
||||
</Container> |
||||
</Container> |
||||
); |
||||
} |
||||
|
||||
_signIn: () => {}; |
||||
|
||||
/** |
||||
* Sings in a user. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_signIn() { |
||||
sendAnalytics( |
||||
createRecordingDialogEvent('start', 'signIn.button') |
||||
); |
||||
this.props.dispatch(authorizeDropbox()); |
||||
} |
||||
|
||||
_signOut: () => {}; |
||||
|
||||
/** |
||||
* Sings out an user from dropbox. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_signOut() { |
||||
sendAnalytics( |
||||
createRecordingDialogEvent('start', 'signOut.button') |
||||
); |
||||
this.props.dispatch(updateDropboxToken()); |
||||
} |
||||
} |
||||
|
||||
export default translate(connect()(StartRecordingDialogContent)); |
@ -1,38 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import { DialogContent } from '../../../base/dialog'; |
||||
import { translate } from '../../../base/i18n'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* React Component for getting confirmation to start a file recording session. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class StartRecordingDialogContent extends Component<Props> { |
||||
/** |
||||
* Renders the platform specific dialog content. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
render() { |
||||
const { t } = this.props; |
||||
|
||||
return ( |
||||
<DialogContent> |
||||
{ t('recording.startRecordingBody') } |
||||
</DialogContent> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default translate(StartRecordingDialogContent); |
@ -1,194 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import Spinner from '@atlaskit/spinner'; |
||||
import React, { Component } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { |
||||
createRecordingDialogEvent, |
||||
sendAnalytics |
||||
} from '../../../analytics'; |
||||
import { translate } from '../../../base/i18n'; |
||||
import { authorizeDropbox, updateDropboxToken } from '../../../dropbox'; |
||||
|
||||
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() { |
||||
const { t } = this.props; |
||||
|
||||
return ( |
||||
<div> |
||||
<div>{ t('recording.authDropboxText') }</div> |
||||
<div |
||||
className = 'dropbox-sign-in' |
||||
onClick = { this._onSignInClick }> |
||||
<img |
||||
className = 'dropbox-logo' |
||||
src = 'images/dropboxLogo.svg' /> |
||||
<span>{ t('recording.signIn') }</span> |
||||
</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-panel'> |
||||
<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() { |
||||
sendAnalytics( |
||||
createRecordingDialogEvent('start', 'signIn.button') |
||||
); |
||||
this.props.dispatch(authorizeDropbox()); |
||||
} |
||||
|
||||
_onSignOutClick: () => {}; |
||||
|
||||
/** |
||||
* Sings out an user from dropbox. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_onSignOutClick() { |
||||
sendAnalytics( |
||||
createRecordingDialogEvent('start', 'signOut.button') |
||||
); |
||||
this.props.dispatch(updateDropboxToken()); |
||||
} |
||||
} |
||||
|
||||
export default translate(connect()(StartRecordingDialogContent)); |
@ -0,0 +1,43 @@ |
||||
// @flow
|
||||
|
||||
import { BoxModel, createStyleSheet } from '../../../base/styles'; |
||||
|
||||
// XXX The "standard" {@code BoxModel.padding} has been deemed insufficient in
|
||||
// the special case(s) of the recording feature bellow.
|
||||
const _PADDING = BoxModel.padding * 1.5; |
||||
|
||||
/** |
||||
* The styles of the React {@code Components} of the feature recording. |
||||
*/ |
||||
export default createStyleSheet({ |
||||
container: { |
||||
flex: 0, |
||||
flexDirection: 'column' |
||||
}, |
||||
|
||||
header: { |
||||
alignItems: 'center', |
||||
flex: 0, |
||||
flexDirection: 'row', |
||||
justifyContent: 'space-between', |
||||
paddingBottom: _PADDING, |
||||
paddingTop: _PADDING |
||||
}, |
||||
|
||||
loggedIn: { |
||||
paddingBottom: _PADDING |
||||
}, |
||||
|
||||
startRecordingText: { |
||||
paddingBottom: _PADDING |
||||
}, |
||||
|
||||
switch: { |
||||
paddingRight: BoxModel.padding |
||||
}, |
||||
|
||||
title: { |
||||
fontSize: 16, |
||||
fontWeight: 'bold' |
||||
} |
||||
}); |
@ -0,0 +1,3 @@ |
||||
// XXX CSS is used on Web, JavaScript styles are use only for mobile. Export an
|
||||
// (empty) object so that styles[*] statements on Web don't trigger errors.
|
||||
export default {}; |
Loading…
Reference in new issue