Rearrange recording feature files

This commit is contained in:
Bettenbuk Zoltan
2018-11-19 15:09:09 +01:00
committed by Zoltan Bettenbuk
parent 2b1cb75e40
commit 71edea8aac
40 changed files with 115 additions and 69 deletions

View File

@@ -0,0 +1,142 @@
// @flow
import React from 'react';
import { connect } from 'react-redux';
import { translate } from '../../../../base/i18n';
import { Container, Text } from '../../../../base/react';
import AbstractLiveStreamButton, {
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
} from '../AbstractLiveStreamButton';
declare var interfaceConfig: Object;
type Props = AbstractProps & {
/**
* True if the button should be disabled, false otherwise.
*
* NOTE: On web, if the feature is not disabled on purpose, then we still
* show the button but disabled and with a tooltip rendered on it,
* explaining why it's not available.
*/
_disabled: boolean,
/**
* Tooltip for the button when it's disabled in a certain way.
*/
_liveStreamDisabledTooltipKey: ?string
}
/**
* An implementation of a button for starting and stopping live streaming.
*/
class LiveStreamButton extends AbstractLiveStreamButton<Props> {
iconName = 'icon-public';
toggledIconName = 'icon-public';
/**
* Constructor of the component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.tooltip = props._liveStreamDisabledTooltipKey;
}
/**
* Implements {@code Component}'s componentWillReceiveProps.
*
* @inheritdoc
*/
componentWillReceiveProps(newProps: Props) {
this.tooltip = newProps._liveStreamDisabledTooltipKey;
}
/**
* Helper function to be implemented by subclasses, which returns
* a React Element to display (a beta tag) at the end of the button.
*
* @override
* @protected
* @returns {ReactElement}
*/
_getElementAfter() {
return (
<Container
className = { 'beta-tag' }>
<Text>
{ this.props.t('recording.beta') }
</Text>
</Container>
);
}
/**
* Helper function to be implemented by subclasses, which must return a
* boolean value indicating if this button is disabled or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._disabled;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code LiveStreamButton} component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the Component.
* @private
* @returns {{
* _conference: Object,
* _isLiveStreamRunning: boolean,
* _disabled: boolean,
* visible: boolean
* }}
*/
function _mapStateToProps(state: Object, ownProps: Props) {
const abstractProps = _abstractMapStateToProps(state, ownProps);
let { visible } = ownProps;
const _disabledByFeatures = abstractProps.disabledByFeatures;
let _disabled = false;
let _liveStreamDisabledTooltipKey;
if (!abstractProps.visible
&& _disabledByFeatures !== undefined && !_disabledByFeatures) {
_disabled = true;
// button and tooltip
if (state['features/base/jwt'].isGuest) {
_liveStreamDisabledTooltipKey
= 'dialog.liveStreamingDisabledForGuestTooltip';
} else {
_liveStreamDisabledTooltipKey
= 'dialog.liveStreamingDisabledTooltip';
}
}
if (typeof visible === 'undefined') {
visible = interfaceConfig.TOOLBAR_BUTTONS.includes('livestreaming')
&& (abstractProps.visible
|| Boolean(_liveStreamDisabledTooltipKey));
}
return {
...abstractProps,
_disabled,
_liveStreamDisabledTooltipKey,
visible
};
}
export default translate(connect(_mapStateToProps)(LiveStreamButton));

View File

@@ -0,0 +1,369 @@
// @flow
import Spinner from '@atlaskit/spinner';
import React from 'react';
import { connect } from 'react-redux';
import { translate } from '../../../../base/i18n';
import {
GOOGLE_API_STATES,
GoogleSignInButton,
loadGoogleAPI,
requestAvailableYouTubeBroadcasts,
requestLiveStreamsForYouTubeBroadcast,
showAccountSelection,
signIn,
updateProfile
} from '../../../../google-api';
import AbstractStartLiveStreamDialog, {
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
} from '../AbstractStartLiveStreamDialog';
import StreamKeyPicker from './StreamKeyPicker';
import StreamKeyForm from './StreamKeyForm';
type Props = AbstractProps & {
/**
* The ID for the Google client application used for making stream key
* related requests.
*/
_googleApiApplicationClientID: string
}
/**
* A React Component for requesting a YouTube stream key to use for live
* streaming of the current conference.
*
* @extends Component
*/
class StartLiveStreamDialog
extends AbstractStartLiveStreamDialog<Props> {
/**
* Initializes a new {@code StartLiveStreamDialog} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code StartLiveStreamDialog} instance with.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onGetYouTubeBroadcasts = this._onGetYouTubeBroadcasts.bind(this);
this._onInitializeGoogleApi = this._onInitializeGoogleApi.bind(this);
this._onGoogleSignIn = this._onGoogleSignIn.bind(this);
this._onRequestGoogleSignIn = this._onRequestGoogleSignIn.bind(this);
this._onYouTubeBroadcastIDSelected
= this._onYouTubeBroadcastIDSelected.bind(this);
this._renderDialogContent = this._renderDialogContent.bind(this);
}
/**
* Implements {@link Component#componentDidMount()}. Invoked immediately
* after this component is mounted.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
super.componentDidMount();
if (this.props._googleApiApplicationClientID) {
this._onInitializeGoogleApi();
}
}
_onInitializeGoogleApi: () => Promise<*>;
/**
* Loads the Google web client application used for fetching stream keys.
* If the user is already logged in, then a request for available YouTube
* broadcasts is also made.
*
* @private
* @returns {Promise}
*/
_onInitializeGoogleApi() {
this.props.dispatch(
loadGoogleAPI(this.props._googleApiApplicationClientID))
.catch(response => this._parseErrorFromResponse(response));
}
/**
* Automatically selects the input field's value after starting to edit the
* display name.
*
* @inheritdoc
* @returns {void}
*/
componentDidUpdate(previousProps) {
if (previousProps._googleAPIState === GOOGLE_API_STATES.LOADED
&& this.props._googleAPIState === GOOGLE_API_STATES.SIGNED_IN) {
this._onGetYouTubeBroadcasts();
}
}
_onGetYouTubeBroadcasts: () => Promise<*>;
/**
* Asks the user to sign in, if not already signed in, and then requests a
* list of the user's YouTube broadcasts.
*
* @private
* @returns {void}
*/
_onGetYouTubeBroadcasts() {
this.props.dispatch(updateProfile())
.catch(response => this._parseErrorFromResponse(response));
this.props.dispatch(requestAvailableYouTubeBroadcasts())
.then(broadcasts => {
this._setStateIfMounted({
broadcasts
});
if (broadcasts.length === 1) {
const broadcast = broadcasts[0];
this._onYouTubeBroadcastIDSelected(broadcast.boundStreamID);
}
})
.catch(response => this._parseErrorFromResponse(response));
}
_onGoogleSignIn: () => Object;
/**
* Forces the Google web client application to prompt for a sign in, such as
* when changing account, and will then fetch available YouTube broadcasts.
*
* @private
* @returns {Promise}
*/
_onGoogleSignIn() {
this.props.dispatch(signIn())
.catch(response => this._parseErrorFromResponse(response));
}
_onRequestGoogleSignIn: () => Object;
/**
* Forces the Google web client application to prompt for a sign in, such as
* when changing account, and will then fetch available YouTube broadcasts.
*
* @private
* @returns {Promise}
*/
_onRequestGoogleSignIn() {
// when there is an error we show the google sign-in button.
// once we click it we want to clear the error from the state
this.props.dispatch(showAccountSelection())
.then(() =>
this._setStateIfMounted({
broadcasts: undefined,
errorType: undefined
}))
.then(() => this._onGetYouTubeBroadcasts());
}
_onStreamKeyChange: string => void;
_onYouTubeBroadcastIDSelected: (string) => Object;
/**
* Fetches the stream key for a YouTube broadcast and updates the internal
* state to display the associated stream key as being entered.
*
* @param {string} boundStreamID - The bound stream ID associated with the
* broadcast from which to get the stream key.
* @private
* @returns {Promise}
*/
_onYouTubeBroadcastIDSelected(boundStreamID) {
this.props.dispatch(
requestLiveStreamsForYouTubeBroadcast(boundStreamID))
.then(({ streamKey, selectedBoundStreamID }) =>
this._setStateIfMounted({
streamKey,
selectedBoundStreamID
}));
}
/**
* Only show an error if an external request was made with the Google api.
* Do not error if the login in canceled.
* And searches in a Google API error response for the error type.
*
* @param {Object} response - The Google API response that may contain an
* error.
* @private
* @returns {string|null}
*/
_parseErrorFromResponse(response) {
if (!response || !response.result) {
return;
}
const result = response.result;
const error = result.error;
const errors = error && error.errors;
const firstError = errors && errors[0];
this._setStateIfMounted({
errorType: (firstError && firstError.reason) || null
});
}
_renderDialogContent: () => React$Component<*>
/**
* Renders the platform specific dialog content.
*
* @returns {React$Component}
*/
_renderDialogContent() {
const { _googleApiApplicationClientID } = this.props;
return (
<div className = 'live-stream-dialog'>
{ _googleApiApplicationClientID
? this._renderYouTubePanel() : null }
<StreamKeyForm
onChange = { this._onStreamKeyChange }
value = { this.state.streamKey || this.props._streamKey } />
</div>
);
}
/**
* Renders a React Element for authenticating with the Google web client.
*
* @private
* @returns {ReactElement}
*/
_renderYouTubePanel() {
const {
t,
_googleProfileEmail
} = this.props;
const {
broadcasts,
selectedBoundStreamID
} = this.state;
let googleContent, helpText;
switch (this.props._googleAPIState) {
case GOOGLE_API_STATES.LOADED:
googleContent
= <GoogleSignInButton onClick = { this._onGoogleSignIn } />;
helpText = t('liveStreaming.signInCTA');
break;
case GOOGLE_API_STATES.SIGNED_IN:
googleContent = (
<StreamKeyPicker
broadcasts = { broadcasts }
onBroadcastSelected = { this._onYouTubeBroadcastIDSelected }
selectedBoundStreamID = { selectedBoundStreamID } />
);
/**
* FIXME: Ideally this help text would be one translation string
* that also accepts the anchor. This can be done using the Trans
* component of react-i18next but I couldn't get it working...
*/
helpText = (
<div>
{ `${t('liveStreaming.chooseCTA',
{ email: _googleProfileEmail })} ` }
<a onClick = { this._onRequestGoogleSignIn }>
{ t('liveStreaming.changeSignIn') }
</a>
</div>
);
break;
case GOOGLE_API_STATES.NEEDS_LOADING:
default:
googleContent = (
<Spinner
isCompleting = { false }
size = 'medium' />
);
break;
}
if (this.state.errorType !== undefined) {
googleContent = (
<GoogleSignInButton
onClick = { this._onRequestGoogleSignIn } />
);
helpText = this._getGoogleErrorMessageToDisplay();
}
return (
<div className = 'google-panel'>
<div className = 'live-stream-cta'>
{ helpText }
</div>
<div className = 'google-api'>
{ googleContent }
</div>
</div>
);
}
_setStateIfMounted: Object => void
/**
* Returns the error message to display for the current error state.
*
* @private
* @returns {string} The error message to display.
*/
_getGoogleErrorMessageToDisplay() {
let text;
switch (this.state.errorType) {
case 'liveStreamingNotEnabled':
text = this.props.t(
'liveStreaming.errorLiveStreamNotEnabled',
{ email: this.props._googleProfileEmail });
break;
default:
text = this.props.t('liveStreaming.errorAPI');
break;
}
return <div className = 'google-error'>{ text }</div>;
}
}
/**
* Maps part of the Redux state to the component's props.
*
* @param {Object} state - The Redux state.
* @returns {{
* _googleApiApplicationClientID: string
* }}
*/
function _mapStateToProps(state: Object) {
return {
..._abstractMapStateToProps(state),
_googleApiApplicationClientID:
state['features/base/config'].googleApiApplicationClientID
};
}
export default translate(connect(_mapStateToProps)(StartLiveStreamDialog));

View File

@@ -0,0 +1,29 @@
// @flow
import { connect } from 'react-redux';
import { translate } from '../../../../base/i18n';
import AbstractStopLiveStreamDialog, {
_mapStateToProps
} from '../AbstractStopLiveStreamDialog';
/**
* A React Component for confirming the participant wishes to stop the currently
* active live stream of the conference.
*
* @extends Component
*/
class StopLiveStreamDialog extends AbstractStopLiveStreamDialog {
/**
* Renders the platform specific {@code Dialog} content.
*
* @inheritdoc
*/
_renderDialogContent() {
return this.props.t('dialog.stopStreamingWarning');
}
}
export default translate(connect(_mapStateToProps)(StopLiveStreamDialog));

View File

@@ -0,0 +1,85 @@
// @flow
import { FieldTextStateless } from '@atlaskit/field-text';
import React from 'react';
import { translate } from '../../../../base/i18n';
import AbstractStreamKeyForm, {
type Props
} from '../AbstractStreamKeyForm';
/**
* A React Component for entering a key for starting a YouTube live stream.
*
* @extends Component
*/
class StreamKeyForm extends AbstractStreamKeyForm {
/**
* Initializes a new {@code StreamKeyForm} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code StreamKeyForm} instance with.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onOpenHelp = this._onOpenHelp.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { value, t } = this.props;
return (
<div className = 'stream-key-form'>
<FieldTextStateless
autoFocus = { true }
compact = { true }
isSpellCheckEnabled = { false }
label = { t('dialog.streamKey') }
name = 'streamId'
okDisabled = { !value }
onChange = { this._onInputChange }
placeholder = { t('liveStreaming.enterStreamKey') }
shouldFitContainer = { true }
type = 'text'
value = { this.state.value } />
{ this.helpURL
? <div className = 'form-footer'>
<a
className = 'helper-link'
onClick = { this._onOpenHelp }>
{ t('liveStreaming.streamIdHelp') }
</a>
</div>
: null
}
</div>
);
}
_onInputChange: Object => void
_onOpenHelp: () => void
/**
* Opens a new tab with information on how to manually locate a YouTube
* broadcast stream key.
*
* @private
* @returns {void}
*/
_onOpenHelp() {
window.open(this.helpURL, 'noopener');
}
}
export default translate(StreamKeyForm);

View File

@@ -0,0 +1,182 @@
/* @flow */
import {
DropdownItem,
DropdownItemGroup,
DropdownMenuStateless
} from '@atlaskit/dropdown-menu';
import React, { PureComponent } from 'react';
import { translate } from '../../../../base/i18n';
/**
* The type of the React {@code Component} props of {@link StreamKeyPicker}.
*/
type Props = {
/**
* Broadcasts available for selection. Each broadcast item should be an
* object with a title for display in the dropdown and a boundStreamID to
* return in the {@link onBroadcastSelected} callback.
*/
broadcasts: Array<Object>,
/**
* Callback invoked when an item in the dropdown is selected. The selected
* broadcast's boundStreamID will be passed back.
*/
onBroadcastSelected: Function,
/**
* The boundStreamID of the broadcast that should display as selected in the
* dropdown.
*/
selectedBoundStreamID: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* The type of the React {@code Component} state of {@link StreamKeyPicker}.
*/
type State = {
/**
* Whether or not to display the dropdown menu to pick a YouTube broadcast.
*/
isDropdownOpen: boolean
};
/**
* A dropdown to select a YouTube broadcast.
*
* @extends Component
*/
class StreamKeyPicker extends PureComponent<Props, State> {
/**
* Default values for {@code StreamKeyForm} component's properties.
*
* @static
*/
static defaultProps = {
broadcasts: []
};
/**
* The initial state of a {@code StreamKeyForm} instance.
*/
state = {
isDropdownOpen: false
};
/**
* Initializes a new {@code StreamKeyPicker} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code StreamKeyPicker} instance with.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onDropdownOpenChange = this._onDropdownOpenChange.bind(this);
this._onSelect = this._onSelect.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { broadcasts, selectedBoundStreamID, t } = this.props;
const dropdownItems
= broadcasts.map(broadcast => (
<DropdownItem
key = { broadcast.boundStreamID }
// eslint-disable-next-line react/jsx-no-bind
onClick = { () => this._onSelect(broadcast.boundStreamID) }>
{ broadcast.title }
</DropdownItem>));
const selected
= this.props.broadcasts.find(
broadcast => broadcast.boundStreamID === selectedBoundStreamID);
const triggerText
= (selected && selected.title) || t('liveStreaming.choose');
return (
<div className = 'broadcast-dropdown'>
<DropdownMenuStateless
isOpen = { this.state.isDropdownOpen }
onItemActivated = { this._onSelect }
onOpenChange = { this._onDropdownOpenChange }
shouldFitContainer = { true }
trigger = { triggerText }
triggerButtonProps = {{
className: 'broadcast-dropdown-trigger',
shouldFitContainer: true
}}
triggerType = 'button'>
<DropdownItemGroup>
{ dropdownItems }
</DropdownItemGroup>
</DropdownMenuStateless>
</div>
);
}
/**
* Transforms the passed in broadcasts into an array of objects that can
* be parsed by {@code DropdownMenuStateless}.
*
* @param {Array<Object>} broadcasts - The YouTube broadcasts to display.
* @private
* @returns {Array<Object>}
*/
_formatBroadcasts(broadcasts) {
return broadcasts.map(broadcast => {
return {
content: broadcast.title,
value: broadcast
};
});
}
_onDropdownOpenChange: (Object) => void;
/**
* Sets the dropdown to be displayed or not based on the passed in event.
*
* @param {Object} dropdownEvent - The event passed from
* {@code DropdownMenuStateless} indicating if the dropdown should be open
* or closed.
* @private
* @returns {void}
*/
_onDropdownOpenChange(dropdownEvent) {
this.setState({
isDropdownOpen: dropdownEvent.isOpen
});
}
_onSelect: (string) => void;
/**
* Callback invoked when an item has been clicked in the dropdown menu.
*
* @param {Object} boundStreamID - The bound stream ID for the selected
* broadcast.
* @returns {void}
*/
_onSelect(boundStreamID) {
this.props.onBroadcastSelected(boundStreamID);
}
}
export default translate(StreamKeyPicker);

View File

@@ -0,0 +1,5 @@
// @flow
export { default as LiveStreamButton } from './LiveStreamButton';
export { default as StartLiveStreamDialog } from './StartLiveStreamDialog';
export { default as StopLiveStreamDialog } from './StopLiveStreamDialog';