Updates recording dialog. (#3953)

* Updates recording dialog.

* Update config.js doc.

* Adds comment and make a check more intuitive.

* Changes of using enum for recording types.
This commit is contained in:
Дамян Минков 2019-03-11 16:17:21 +00:00 committed by virtuacoplenny
parent f439ad2999
commit 12d0aef686
22 changed files with 468 additions and 97 deletions

View File

@ -180,6 +180,11 @@ var config = {
// redirectURI:
// 'https://jitsi-meet.example.com/subfolder/static/oauth.html'
// },
// When integrations like dropbox are enabled only that will be shown,
// by enabling fileRecordingsServiceEnabled, we show both the integrations
// and the generic recording service (its configuration and storage type
// depends on jibri configuration)
// fileRecordingsServiceEnabled: false
// Whether to enable live streaming or not.
// liveStreamingEnabled: false,

View File

@ -11,42 +11,37 @@
flex: 0;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-top: 32px;
.recording-title {
display: inline-flex;
align-items: center;
font-size: 16px;
font-weight: bold;
margin-left: 16px;
}
}
.recording-icon-container {
display: inline-flex;
align-items: center;
}
.recording-icon {
width: 32px;
height: 32px;
object-fit: contain;
}
.recording-switch {
margin-left: auto;
}
.authorization-panel {
display: flex;
flex-direction: column;
margin-bottom: 10px;
margin: 0 40px 10px 40px;
padding-bottom: 10px;
.dropbox-sign-in {
align-items: center;
border: 1px solid #4285f4;
background-color: white;
border-radius: 2px;
cursor: pointer;
display: inline-flex;
padding: 10px;
font-size: 18px;
font-weight: 600;
margin: 10px 0px;
color: #4285f4;
.dropbox-logo {
background-color: white;
border-radius: 2px;
display: inline-block;
padding-right: 5px;
height: 18px;
}
}
.logged-in-panel {
padding: 10px;
}

BIN
images/dropboxLogo_square.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
images/jitsiLogo_square.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -518,9 +518,10 @@
"on": "Recording",
"pending": "Preparing to record the meeting...",
"rec": "REC",
"serviceDescription": "Your recording will be saved by the recording service",
"serviceName": "Recording service",
"signIn": "sign in",
"signOut": "Sign Out",
"signIn": "Sign in",
"signOut": "Sign out",
"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"

View File

@ -338,18 +338,40 @@ export function createProfilePanelButtonEvent(buttonName, attributes = {}) {
* @param {string} dialogName - The name of the dialog (e.g. 'start' or 'stop').
* @param {string} buttonName - The name of the button (e.g. 'confirm' or
* 'cancel').
* @param {Object} attributes - Attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createRecordingDialogEvent(dialogName, buttonName) {
export function createRecordingDialogEvent(
dialogName, buttonName, attributes = {}) {
return {
action: 'clicked',
actionSubject: buttonName,
attributes,
source: `${dialogName}.recording.dialog`,
type: TYPE_UI
};
}
/**
* Creates an event which indicates that a specific button on one of the
* liveStreaming-related dialogs was clicked.
*
* @param {string} dialogName - The name of the dialog (e.g. 'start' or 'stop').
* @param {string} buttonName - The name of the button (e.g. 'confirm' or
* 'cancel').
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createLiveStreamingDialogEvent(dialogName, buttonName) {
return {
action: 'clicked',
actionSubject: buttonName,
source: `${dialogName}.liveStreaming.dialog`,
type: TYPE_UI
};
}
/**
* Creates an event which indicates that an action related to recording has
* occured.

View File

@ -0,0 +1,44 @@
/* @flow */
import React, { Component } from 'react';
import { Text, TouchableOpacity } from 'react-native';
type Props = {
/**
* React Elements to display within the component.
*/
children: React$Node | Object,
/**
* Handler called when the user presses the button.
*/
onValueChange: Function,
/**
* The component's external style
*/
style: Object
};
/**
* Renders a button.
*/
export default class ButtonImpl extends Component<Props> {
/**
* Implements React's {@link Component#render()}, renders the button.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<TouchableOpacity
onPress = { this.props.onValueChange } >
<Text style = { this.props.style }>
{ this.props.children }
</Text>
</TouchableOpacity>
);
}
}

View File

@ -0,0 +1,41 @@
// @flow
import React, { Component } from 'react';
import { Image } from 'react-native';
/**
* The type of the React {@code Component} props of {@link Image}.
*/
type Props = {
/**
* The URL to be rendered as image.
*/
src: string,
/**
* The component's external style
*/
style: Object
};
/**
* A component rendering aN IMAGE.
*
* @extends Component
*/
export default class ImageImpl extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Image
source = { this.props.src }
style = { this.props.style } />
);
}
}

View File

@ -2,10 +2,12 @@
export { default as AvatarListItem } from './AvatarListItem';
export { default as BackButton } from './BackButton';
export { default as Button } from './Button';
export { default as Container } from './Container';
export { default as ForwardButton } from './ForwardButton';
export { default as Header } from './Header';
export { default as HeaderLabel } from './HeaderLabel';
export { default as Image } from './Image';
export { default as Link } from './Link';
export { default as LoadingIndicator } from './LoadingIndicator';
export { default as Modal } from './Modal';

View File

@ -0,0 +1,41 @@
/* @flow */
import Button from '@atlaskit/button';
import React, { Component } from 'react';
type Props = {
/**
* React Elements to display within the component.
*/
children: React$Node | Object,
/**
* Handler called when the user presses the button.
*/
onValueChange: Function
};
/**
* Renders a button.
*/
export default class ButtonImpl extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { onValueChange } = this.props;
return (
<Button
appearance = 'primary'
onClick = { onValueChange }
type = 'button'>
{ this.props.children }
</Button>
);
}
}

View File

@ -0,0 +1,19 @@
import React, { Component } from 'react';
/**
* Implements a React/Web {@link Component} for displaying image
* in order to facilitate cross-platform source code.
*
* @extends Component
*/
export default class Image extends Component {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return React.createElement('img', this.props);
}
}

View File

@ -1,4 +1,6 @@
export { default as Button } from './Button';
export { default as Container } from './Container';
export { default as Image } from './Image';
export { default as LoadingIndicator } from './LoadingIndicator';
export { default as MeetingsList } from './MeetingsList';
export { default as MultiSelectAutocomplete } from './MultiSelectAutocomplete';

View File

@ -44,8 +44,11 @@ export function getSpaceUsage(token: string) {
* 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() {
return Dropbox.ENABLED;
export function isEnabled(state: Object) {
const { dropbox = {} } = state['features/base/config'];
return Dropbox.ENABLED && typeof dropbox.appKey === 'string';
}

View File

@ -3,7 +3,7 @@
import { Component } from 'react';
import {
createRecordingDialogEvent,
createLiveStreamingDialogEvent,
sendAnalytics
} from '../../../analytics';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
@ -149,7 +149,7 @@ export default class AbstractStartLiveStreamDialog<P: Props>
* @returns {boolean} True is returned to close the modal.
*/
_onCancel() {
sendAnalytics(createRecordingDialogEvent('start', 'cancel.button'));
sendAnalytics(createLiveStreamingDialogEvent('start', 'cancel.button'));
return true;
}
@ -211,7 +211,7 @@ export default class AbstractStartLiveStreamDialog<P: Props>
}
sendAnalytics(
createRecordingDialogEvent('start', 'confirm.button'));
createLiveStreamingDialogEvent('start', 'confirm.button'));
this.props._conference.startRecording({
broadcastId: selectedBroadcastID,

View File

@ -3,7 +3,7 @@
import { Component } from 'react';
import {
createRecordingDialogEvent,
createLiveStreamingDialogEvent,
sendAnalytics
} from '../../../analytics';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
@ -61,7 +61,7 @@ export default class AbstractStopLiveStreamDialog extends Component<Props> {
* @returns {boolean} True to close the modal.
*/
_onSubmit() {
sendAnalytics(createRecordingDialogEvent('stop', 'confirm.button'));
sendAnalytics(createLiveStreamingDialogEvent('stop', 'confirm.button'));
const { _session } = this.props;

View File

@ -11,6 +11,7 @@ import {
getDropboxData,
isEnabled as isDropboxEnabled
} from '../../../dropbox';
import { RECORDING_TYPES } from '../../constants';
type Props = {
@ -24,6 +25,12 @@ type Props = {
*/
_appKey: string,
/**
* Whether to show file recordings service, even if integrations
* are enabled.
*/
_fileRecordingsServiceEnabled: boolean,
/**
* If true the dropbox integration is enabled, otherwise - disabled.
*/
@ -165,23 +172,28 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
* @returns {boolean} - True (to note that the modal should be closed).
*/
_onSubmit() {
sendAnalytics(
createRecordingDialogEvent('start', 'confirm.button')
);
const { _conference, _isDropboxEnabled, _token } = this.props;
let appData;
const attributes = {};
if (_isDropboxEnabled) {
if (_isDropboxEnabled && _token) {
appData = JSON.stringify({
'file_recording_metadata': {
'upload_credentials': {
'service_name': 'dropbox',
'service_name': RECORDING_TYPES.DROPBOX,
'token': _token
}
}
});
attributes.type = RECORDING_TYPES.DROPBOX;
} else {
attributes.type = RECORDING_TYPES.JITSI_REC_SERVICE;
}
sendAnalytics(
createRecordingDialogEvent('start', 'confirm.button', attributes)
);
_conference.startRecording({
mode: JitsiRecordingConstants.mode.FILE,
appData
@ -212,11 +224,15 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
* }}
*/
export function mapStateToProps(state: Object) {
const { dropbox = {} } = state['features/base/config'];
const {
fileRecordingsServiceEnabled = false,
dropbox = {}
} = state['features/base/config'];
return {
_appKey: dropbox.appKey,
_conference: state['features/base/conference'].conference,
_fileRecordingsServiceEnabled: fileRecordingsServiceEnabled,
_isDropboxEnabled: isDropboxEnabled(state),
_token: state['features/dropbox'].token
};

View File

@ -8,20 +8,27 @@ import {
sendAnalytics
} from '../../../analytics';
import {
DialogContent,
_abstractMapStateToProps
} from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import {
Button,
Container,
Image,
LoadingIndicator,
Switch,
Text
} from '../../../base/react';
import { StyleType } from '../../../base/styles';
import { ColorPalette, StyleType } from '../../../base/styles';
import { authorizeDropbox, updateDropboxToken } from '../../../dropbox';
import styles from './styles';
import {
default as styles,
DROPBOX_LOGO,
JITSI_LOGO
} from './styles';
import { RECORDING_TYPES } from '../../constants';
import { getRecordingDurationEstimation } from '../../functions';
type Props = {
@ -36,6 +43,12 @@ type Props = {
*/
dispatch: Function,
/**
* Whether to show file recordings service, even if integrations
* are enabled.
*/
fileRecordingsServiceEnabled: boolean,
/**
* If true the content related to the integrations will be shown.
*/
@ -67,12 +80,24 @@ type Props = {
userName: ?string,
};
/**
* State of the component.
*/
type State = {
/**
* The currently selected recording service of type: RECORDING_TYPES.
*/
selectedRecordingService: string
};
/**
* React Component for getting confirmation to start a file recording session.
*
* @extends Component
*/
class StartRecordingDialogContent extends Component<Props> {
class StartRecordingDialogContent extends Component<Props, State> {
/**
* Initializes a new {@code StartRecordingDialogContent} instance.
*
@ -82,9 +107,18 @@ class StartRecordingDialogContent extends Component<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);
this._onSignIn = this._onSignIn.bind(this);
this._onSignOut = this._onSignOut.bind(this);
this._onDropboxSwitchChange
= this._onDropboxSwitchChange.bind(this);
this._onRecordingServiceSwitchChange
= this._onRecordingServiceSwitchChange.bind(this);
// the initial state is jitsi rec service is always selected
// if only one type of recording is enabled this state will be ignored
this.state = {
selectedRecordingService: RECORDING_TYPES.JITSI_REC_SERVICE
};
}
/**
@ -94,11 +128,14 @@ class StartRecordingDialogContent extends Component<Props> {
* @returns {React$Component}
*/
render() {
if (this.props.integrationsEnabled === true) { // explicit true needed
return this._renderIntegrationsContent();
}
return this._renderNoIntegrationsContent();
return (
<Container
className = 'recording-dialog'
style = { styles.container }>
{ this._renderNoIntegrationsContent() }
{ this._renderIntegrationsContent() }
</Container>
);
}
/**
@ -107,10 +144,51 @@ class StartRecordingDialogContent extends Component<Props> {
* @returns {React$Component}
*/
_renderNoIntegrationsContent() {
// show the non integrations part only if fileRecordingsServiceEnabled
// is enabled or when there are no integrations enabled
if (!(this.props.fileRecordingsServiceEnabled
|| !this.props.integrationsEnabled)) {
return null;
}
const { _dialogStyles, isValidating, t } = this.props;
const switchContent
= this.props.integrationsEnabled
? (
<Switch
className = 'recording-switch'
disabled = { isValidating }
onValueChange
= { this._onRecordingServiceSwitchChange }
style = { styles.switch }
trackColor = {{ false: ColorPalette.lightGrey }}
value = {
this.state.selectedRecordingService
=== RECORDING_TYPES.JITSI_REC_SERVICE } />
) : null;
return (
<DialogContent style = { this.props._dialogStyles.text }>
{ this.props.t('recording.startRecordingBody') }
</DialogContent>
<Container
className = 'recording-header'
style = { styles.header }>
<Container className = 'recording-icon-container'>
<Image
className = 'recording-icon'
src = { JITSI_LOGO }
style = { styles.recordingIcon } />
</Container>
<Text
className = 'recording-title'
style = {{
..._dialogStyles.text,
...styles.title
}}>
{ t('recording.serviceDescription') }
</Text>
{ switchContent }
</Container>
);
}
@ -121,27 +199,67 @@ class StartRecordingDialogContent extends Component<Props> {
* @returns {React$Component}
*/
_renderIntegrationsContent() {
if (!this.props.integrationsEnabled) {
return null;
}
const { _dialogStyles, isTokenValid, isValidating, t } = this.props;
let content = null;
let switchContent = null;
if (isValidating) {
content = this._renderSpinner();
switchContent = <Container className = 'recording-switch' />;
} else if (isTokenValid) {
content = this._renderSignOut();
switchContent = (
<Container className = 'recording-switch'>
<Button
onValueChange = { this._onSignOut }
style = { styles.signButton }>
{ t('recording.signOut') }
</Button>
</Container>
);
} else {
switchContent = (
<Container className = 'recording-switch'>
<Button
onValueChange = { this._onSignIn }
style = { styles.signButton }>
{ t('recording.signIn') }
</Button>
</Container>
);
}
// else { // Sign in screen:
// We don't need to render any additional information.
// }
if (this.props.fileRecordingsServiceEnabled) {
switchContent = (
<Switch
className = 'recording-switch'
disabled = { isValidating }
onValueChange = { this._onDropboxSwitchChange }
style = { styles.switch }
trackColor = {{ false: ColorPalette.lightGrey }}
value = { this.state.selectedRecordingService
=== RECORDING_TYPES.DROPBOX } />
);
}
return (
<Container
className = 'recording-dialog'
style = { styles.container }>
<Container>
<Container
className = 'recording-header'
style = { styles.header }>
<Container
className = 'recording-icon-container'>
<Image
className = 'recording-icon'
src = { DROPBOX_LOGO }
style = { styles.recordingIcon } />
</Container>
<Text
className = 'recording-title'
style = {{
@ -150,11 +268,7 @@ class StartRecordingDialogContent extends Component<Props> {
}}>
{ t('recording.authDropboxText') }
</Text>
<Switch
disabled = { isValidating }
onValueChange = { this._onSwitchChange }
style = { styles.switch }
value = { isTokenValid } />
{ switchContent }
</Container>
<Container
className = 'authorization-panel'>
@ -164,18 +278,49 @@ class StartRecordingDialogContent extends Component<Props> {
);
}
_onSwitchChange: boolean => void;
_onDropboxSwitchChange: boolean => void;
_onRecordingServiceSwitchChange: boolean => void;
/**
* Handler for onValueChange events from the Switch component.
*
* @returns {void}
*/
_onSwitchChange() {
_onRecordingServiceSwitchChange() {
// act like group, cannot toggle off
if (this.state.selectedRecordingService
=== RECORDING_TYPES.JITSI_REC_SERVICE) {
return;
}
this.setState({
selectedRecordingService: RECORDING_TYPES.JITSI_REC_SERVICE
});
if (this.props.isTokenValid) {
this._signOut();
} else {
this._signIn();
this._onSignOut();
}
}
/**
* Handler for onValueChange events from the Switch component.
*
* @returns {void}
*/
_onDropboxSwitchChange() {
// act like group, cannot toggle off
if (this.state.selectedRecordingService
=== RECORDING_TYPES.DROPBOX) {
return;
}
this.setState({
selectedRecordingService: RECORDING_TYPES.DROPBOX
});
if (!this.props.isTokenValid) {
this._onSignIn();
}
}
@ -222,37 +367,32 @@ class StartRecordingDialogContent extends Component<Props> {
</Text>
</Container>
</Container>
<Container style = { styles.startRecordingText }>
<Text style = { styles.text }>
{ t('recording.startRecordingBody') }
</Text>
</Container>
</Container>
);
}
_signIn: () => {};
_onSignIn: () => {};
/**
* Sings in a user.
*
* @returns {void}
*/
_signIn() {
_onSignIn() {
sendAnalytics(
createRecordingDialogEvent('start', 'signIn.button')
);
this.props.dispatch(authorizeDropbox());
}
_signOut: () => {};
_onSignOut: () => {};
/**
* Sings out an user from dropbox.
*
* @returns {void}
*/
_signOut() {
_onSignOut() {
sendAnalytics(
createRecordingDialogEvent('start', 'signOut.button')
);

View File

@ -25,13 +25,23 @@ class StartRecordingDialog extends AbstractStartRecordingDialog {
*/
render() {
const { isTokenValid, isValidating, spaceLeft, userName } = this.state;
const { _isDropboxEnabled } = this.props;
const { _fileRecordingsServiceEnabled, _isDropboxEnabled } = this.props;
// disable ok button id recording service is shown only, when
// validating dropbox token, if that is not enabled we either always
// show the ok button or if just dropbox is enabled ok is available
// when there is token
const isOkDisabled
= _fileRecordingsServiceEnabled ? isValidating
: _isDropboxEnabled ? !isTokenValid : false;
return (
<CustomSubmitDialog
okDisabled = { _isDropboxEnabled && !isTokenValid }
okDisabled = { isOkDisabled }
onSubmit = { this._onSubmit } >
<StartRecordingDialogContent
fileRecordingsServiceEnabled
= { _fileRecordingsServiceEnabled }
integrationsEnabled = { _isDropboxEnabled }
isTokenValid = { isTokenValid }
isValidating = { isValidating }

View File

@ -6,6 +6,12 @@ import { BoxModel, createStyleSheet, ColorPalette } from '../../../base/styles';
// the special case(s) of the recording feature bellow.
const _PADDING = BoxModel.padding * 1.5;
export const DROPBOX_LOGO
= require('../../../../../images/dropboxLogo_square.png');
export const JITSI_LOGO
= require('../../../../../images/jitsiLogo_square.png');
/**
* The styles of the React {@code Components} of the feature recording.
*/
@ -28,18 +34,29 @@ export default createStyleSheet({
paddingBottom: _PADDING
},
startRecordingText: {
paddingBottom: _PADDING
recordingIcon: {
width: 24,
height: 24
},
signButton: {
backgroundColor: ColorPalette.blue,
color: ColorPalette.white,
fontSize: 16,
borderRadius: 5,
padding: BoxModel.padding * 0.5
},
switch: {
color: ColorPalette.white,
paddingRight: BoxModel.padding
color: ColorPalette.white
},
title: {
flex: 1,
fontSize: 16,
fontWeight: 'bold'
fontWeight: 'bold',
textAlign: 'left',
paddingLeft: BoxModel.padding
},
text: {

View File

@ -1,3 +1,7 @@
// 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 {};
export const DROPBOX_LOGO = 'images/dropboxLogo_square.png';
export const JITSI_LOGO = 'images/jitsiLogo_square.png';

View File

@ -25,16 +25,26 @@ class StartRecordingDialog extends AbstractStartRecordingDialog {
*/
render() {
const { isTokenValid, isValidating, spaceLeft, userName } = this.state;
const { _isDropboxEnabled } = this.props;
const { _fileRecordingsServiceEnabled, _isDropboxEnabled } = this.props;
// disable ok button id recording service is shown only, when
// validating dropbox token, if that is not enabled we either always
// show the ok button or if just dropbox is enabled ok is available
// when there is token
const isOkDisabled
= _fileRecordingsServiceEnabled ? isValidating
: _isDropboxEnabled ? !isTokenValid : false;
return (
<Dialog
okDisabled = { !isTokenValid && _isDropboxEnabled }
okKey = 'dialog.confirm'
okDisabled = { isOkDisabled }
okKey = 'dialog.startRecording'
onSubmit = { this._onSubmit }
titleKey = 'dialog.recording'
titleKey = 'dialog.startRecording'
width = 'small'>
<StartRecordingDialogContent
fileRecordingsServiceEnabled
= { _fileRecordingsServiceEnabled }
integrationsEnabled = { _isDropboxEnabled }
isTokenValid = { isTokenValid }
isValidating = { isValidating }

View File

@ -19,14 +19,13 @@ export const RECORDING_OFF_SOUND_ID = 'RECORDING_OFF_SOUND';
export const RECORDING_ON_SOUND_ID = 'RECORDING_ON_SOUND';
/**
* Expected supported recording types. JIBRI is known to support live streaming
* whereas JIRECON is for recording.
* Expected supported recording types.
*
* @type {Object}
* @enum {string}
*/
export const RECORDING_TYPES = {
JIBRI: 'jibri',
JIRECON: 'jirecon'
JITSI_REC_SERVICE: 'recording-service',
DROPBOX: 'dropbox'
};
/**