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,243 @@
// @flow
import React, { Component } from 'react';
import { Platform, Text, View } from 'react-native';
import { connect } from 'react-redux';
import { translate } from '../../../../base/i18n';
import {
GOOGLE_API_STATES,
GOOGLE_SCOPE_YOUTUBE,
googleApi,
GoogleSignInButton,
setGoogleAPIState
} from '../../../../google-api';
import styles from './styles';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Prop type of the component {@code GoogleSigninForm}.
*/
type Props = {
/**
* The Redux dispatch Function.
*/
dispatch: Function,
/**
* The current state of the Google api as defined in {@code constants.js}.
*/
googleAPIState: number,
/**
* The recently received Google response.
*/
googleResponse: Object,
/**
* A callback to be invoked when an authenticated user changes, so
* then we can get (or clear) the YouTube stream key.
*/
onUserChanged: Function,
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
/**
* Class to render a google sign in form, or a google stream picker dialog.
*
* @extends Component
*/
class GoogleSigninForm extends Component<Props> {
/**
* Instantiates a new {@code GoogleSigninForm} component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._logGoogleError = this._logGoogleError.bind(this);
this._onGoogleButtonPress = this._onGoogleButtonPress.bind(this);
}
/**
* Implements React's Component.componentDidMount.
*
* @inheritdoc
*/
componentDidMount() {
if (Platform.OS === 'ios') {
const majorVersionIOS = parseInt(Platform.Version, 10);
if (majorVersionIOS <= 10) {
// Disable it on iOS 10 and earlier, since it doesn't work
// properly.
this._setApiState(GOOGLE_API_STATES.NOT_AVAILABLE);
return;
}
}
googleApi.hasPlayServices()
.then(() => {
googleApi.configure({
offlineAccess: false,
scopes: [ GOOGLE_SCOPE_YOUTUBE ]
});
googleApi.signInSilently().then(response => {
this._setApiState(response
? GOOGLE_API_STATES.SIGNED_IN
: GOOGLE_API_STATES.LOADED,
response);
}, () => {
this._setApiState(GOOGLE_API_STATES.LOADED);
});
})
.catch(error => {
this._logGoogleError(error);
this._setApiState(GOOGLE_API_STATES.NOT_AVAILABLE);
});
}
/**
* Renders the component.
*
* @inheritdoc
*/
render() {
const { t } = this.props;
const { googleAPIState, googleResponse } = this.props;
const signedInUser = googleResponse
&& googleResponse.user
&& googleResponse.user.email;
if (googleAPIState === GOOGLE_API_STATES.NOT_AVAILABLE
|| googleAPIState === GOOGLE_API_STATES.NEEDS_LOADING
|| typeof googleAPIState === 'undefined') {
return null;
}
return (
<View style = { styles.formWrapper }>
<View style = { styles.helpText }>
{ signedInUser ? <Text>
{ `${t('liveStreaming.signedInAs')} ${signedInUser}` }
</Text> : <Text>
{ t('liveStreaming.signInCTA') }
</Text> }
</View>
<GoogleSignInButton
onClick = { this._onGoogleButtonPress }
signedIn = {
googleAPIState === GOOGLE_API_STATES.SIGNED_IN } />
</View>
);
}
_logGoogleError: Object => void
/**
* A helper function to log developer related errors.
*
* @private
* @param {Object} error - The error to be logged.
* @returns {void}
*/
_logGoogleError(error) {
// NOTE: This is a developer error message, not intended for the
// user to see.
logger.error('Google API error. Possible cause: bad config.', error);
}
_onGoogleButtonPress: () => void
/**
* Callback to be invoked when the user presses the Google button,
* regardless of being logged in or out.
*
* @private
* @returns {void}
*/
_onGoogleButtonPress() {
const { googleResponse } = this.props;
if (googleResponse && googleResponse.user) {
// the user is signed in
this._onSignOut();
} else {
this._onSignIn();
}
}
_onSignIn: () => void
/**
* Initiates a sign in if the user is not signed in yet.
*
* @private
* @returns {void}
*/
_onSignIn() {
googleApi.signIn().then(response => {
this._setApiState(GOOGLE_API_STATES.SIGNED_IN, response);
}, this._logGoogleError);
}
_onSignOut: () => void
/**
* Initiates a sign out if the user is signed in.
*
* @private
* @returns {void}
*/
_onSignOut() {
googleApi.signOut().then(response => {
this._setApiState(GOOGLE_API_STATES.LOADED, response);
}, this._logGoogleError);
}
/**
* Updates the API (Google Auth) state.
*
* @private
* @param {number} apiState - The state of the API.
* @param {?Object} googleResponse - The response from the API.
* @returns {void}
*/
_setApiState(apiState, googleResponse) {
this.props.onUserChanged(googleResponse);
this.props.dispatch(setGoogleAPIState(apiState, googleResponse));
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code GoogleSigninForm} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* googleAPIState: number,
* googleResponse: Object
* }}
*/
function _mapStateToProps(state: Object) {
const { googleAPIState, googleResponse } = state['features/google-api'];
return {
googleAPIState,
googleResponse
};
}
export default translate(connect(_mapStateToProps)(GoogleSigninForm));

View File

@@ -0,0 +1,20 @@
// @flow
import { connect } from 'react-redux';
import { translate } from '../../../../base/i18n';
import AbstractLiveStreamButton, {
_mapStateToProps,
type Props
} from '../AbstractLiveStreamButton';
/**
* An implementation of a button for starting and stopping live streaming.
*/
class LiveStreamButton extends AbstractLiveStreamButton<Props> {
iconName = 'public';
toggledIconName = 'public';
}
export default translate(connect(_mapStateToProps)(LiveStreamButton));

View File

@@ -0,0 +1,130 @@
// @flow
import React from 'react';
import { View } from 'react-native';
import { connect } from 'react-redux';
import { translate } from '../../../../base/i18n';
import { googleApi } from '../../../../google-api';
import { setLiveStreamKey } from '../../../actions';
import AbstractStartLiveStreamDialog,
{ _mapStateToProps, type Props } from '../AbstractStartLiveStreamDialog';
import GoogleSigninForm from './GoogleSigninForm';
import StreamKeyForm from './StreamKeyForm';
import StreamKeyPicker from './StreamKeyPicker';
import styles from './styles';
/**
* A React Component for requesting a YouTube stream key to use for live
* streaming of the current conference.
*/
class StartLiveStreamDialog extends AbstractStartLiveStreamDialog<Props> {
/**
* Constructor of the component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onStreamKeyChangeNative
= this._onStreamKeyChangeNative.bind(this);
this._onStreamKeyPick = this._onStreamKeyPick.bind(this);
this._onUserChanged = this._onUserChanged.bind(this);
this._renderDialogContent = this._renderDialogContent.bind(this);
}
_onStreamKeyChange: string => void
_onStreamKeyChangeNative: string => void;
/**
* Callback to handle stream key changes.
*
* FIXME: This is a temporary method to store the streaming key on mobile
* for easier use, until the Google sign-in is implemented. We don't store
* the key on web for security reasons (e.g. We don't want to have the key
* stored if the used signed out).
*
* @private
* @param {string} streamKey - The new key value.
* @returns {void}
*/
_onStreamKeyChangeNative(streamKey) {
this.props.dispatch(setLiveStreamKey(streamKey));
this._onStreamKeyChange(streamKey);
}
_onStreamKeyPick: string => void
/**
* Callback to be invoked when the user selects a stream from the picker.
*
* @private
* @param {string} streamKey - The key of the selected stream.
* @returns {void}
*/
_onStreamKeyPick(streamKey) {
this.setState({
streamKey
});
}
_onUserChanged: Object => void
/**
* A callback to be invoked when an authenticated user changes, so
* then we can get (or clear) the YouTube stream key.
*
* TODO: Handle errors by showing some indication to the user.
*
* @private
* @param {Object} response - The retreived signin response.
* @returns {void}
*/
_onUserChanged(response) {
if (response && response.accessToken) {
googleApi.getYouTubeLiveStreams(response.accessToken)
.then(broadcasts => {
this.setState({
broadcasts
});
});
} else {
this.setState({
broadcasts: undefined,
streamKey: undefined
});
}
}
_renderDialogContent: () => React$Component<*>
/**
* Renders the platform specific dialog content.
*
* @returns {React$Component}
*/
_renderDialogContent() {
return (
<View style = { styles.startDialogWrapper }>
<GoogleSigninForm
onUserChanged = { this._onUserChanged } />
<StreamKeyPicker
broadcasts = { this.state.broadcasts }
onChange = { this._onStreamKeyPick } />
<StreamKeyForm
onChange = { this._onStreamKeyChangeNative }
value = { this.state.streamKey || this.props._streamKey } />
</View>
);
}
}
export default translate(connect(_mapStateToProps)(StartLiveStreamDialog));

View File

@@ -0,0 +1,37 @@
// @flow
import React from 'react';
import { connect } from 'react-redux';
import { DialogContent } from '../../../../base/dialog';
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 (
<DialogContent>
{
this.props.t('dialog.stopStreamingWarning')
}
</DialogContent>
);
}
}
export default translate(connect(_mapStateToProps)(StopLiveStreamDialog));

View File

@@ -0,0 +1,87 @@
// @flow
import React from 'react';
import { Linking, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { translate } from '../../../../base/i18n';
import AbstractStreamKeyForm, {
type Props
} from '../AbstractStreamKeyForm';
import styles from './styles';
/**
* 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 { t } = this.props;
return (
<View style = { styles.formWrapper }>
<Text style = { styles.streamKeyInputLabel }>
{
t('dialog.streamKey')
}
</Text>
<TextInput
onChangeText = { this._onInputChange }
placeholder = { t('liveStreaming.enterStreamKey') }
style = { styles.streamKeyInput }
value = { this.state.value } />
<TouchableOpacity
onPress = { this._onOpenHelp }
style = { styles.streamKeyHelp } >
<Text>
{
t('liveStreaming.streamIdHelp')
}
</Text>
</TouchableOpacity>
</View>
);
}
_onInputChange: Object => void
_onOpenHelp: () => void
/**
* Opens the information link on how to manually locate a YouTube broadcast
* stream key.
*
* @private
* @returns {void}
*/
_onOpenHelp() {
const { helpURL } = this;
if (typeof helpURL === 'string') {
Linking.openURL(helpURL);
}
}
}
export default translate(StreamKeyForm);

View File

@@ -0,0 +1,122 @@
// @flow
import React, { Component } from 'react';
import { Text, TouchableHighlight, View } from 'react-native';
import { translate } from '../../../../base/i18n';
import styles, { ACTIVE_OPACITY, TOUCHABLE_UNDERLAY } from './styles';
type Props = {
/**
* The list of broadcasts the user can pick from.
*/
broadcasts: ?Array<Object>,
/**
* Callback to be invoked when the user picked a broadcast. To be invoked
* with a single key (string).
*/
onChange: Function,
/**
* Function to be used to translate i18n labels.
*/
t: Function
}
type State = {
/**
* The key of the currently selected stream.
*/
streamKey: ?string
}
/**
* Class to implement a stream key picker (dropdown) component to allow the user
* to choose from the available Google Broadcasts/Streams.
*
* NOTE: This component is currently only used on mobile, but it is advised at
* a later point to unify mobile and web logic for this functionality. But it's
* out of the scope for now of the mobile live streaming functionality.
*/
class StreamKeyPicker extends Component<Props, State> {
/**
* Instantiates a new instance of StreamKeyPicker.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = {
streamKey: null
};
this._onStreamPick = this._onStreamPick.bind(this);
}
/**
* Renders the component.
*
* @inheritdoc
*/
render() {
const { broadcasts } = this.props;
if (!broadcasts || !broadcasts.length) {
return null;
}
return (
<View style = { styles.formWrapper }>
<View style = { styles.streamKeyPickerCta }>
<Text>
{ this.props.t('liveStreaming.choose') }
</Text>
</View>
<View style = { styles.streamKeyPickerWrapper } >
{ broadcasts.map((broadcast, index) =>
(<TouchableHighlight
activeOpacity = { ACTIVE_OPACITY }
key = { index }
onPress = { this._onStreamPick(broadcast.key) }
style = { [
styles.streamKeyPickerItem,
this.state.streamKey === broadcast.key
? styles.streamKeyPickerItemHighlight : null
] }
underlayColor = { TOUCHABLE_UNDERLAY }>
<Text style = { styles.streamKeyPickerItemText }>
{ broadcast.title }
</Text>
</TouchableHighlight>))
}
</View>
</View>
);
}
_onStreamPick: string => Function
/**
* Callback to be invoked when the user picks a stream from the list.
*
* @private
* @param {string} streamKey - The key of the stream selected.
* @returns {Function}
*/
_onStreamPick(streamKey) {
return () => {
this.setState({
streamKey
});
this.props.onChange(streamKey);
};
}
}
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';

View File

@@ -0,0 +1,124 @@
// @flow
import {
BoxModel,
ColorPalette,
createStyleSheet
} from '../../../../base/styles';
/**
* Opacity of the TouchableHighlight.
*/
export const ACTIVE_OPACITY = 0.3;
/**
* Underlay of the TouchableHighlight.
*/
export const TOUCHABLE_UNDERLAY = ColorPalette.lightGrey;
/**
* The styles of the React {@code Components} of LiveStream.
*/
export default createStyleSheet({
betaTag: {
backgroundColor: ColorPalette.darkGrey,
borderRadius: 2,
marginLeft: 16,
opacity: 0.90,
paddingLeft: 6,
paddingRight: 6
},
betaTagText: {
color: ColorPalette.white,
fontWeight: 'bold'
},
/**
* Generic component to wrap form sections into achieving a unified look.
*/
formWrapper: {
alignItems: 'stretch',
flexDirection: 'column',
padding: BoxModel.padding
},
/**
* Explaining text on the top of the sign in form.
*/
helpText: {
marginBottom: BoxModel.margin
},
/**
* Wrapper for the StartLiveStreamDialog form.
*/
startDialogWrapper: {
flexDirection: 'column'
},
/**
* Helper link text.
*/
streamKeyHelp: {
alignSelf: 'flex-end'
},
/**
* Input field to manually enter stream key.
*/
streamKeyInput: {
alignSelf: 'stretch',
height: 50
},
/**
* Label for the previous field.
*/
streamKeyInputLabel: {
alignSelf: 'flex-start'
},
/**
* Custom component to pick a broadcast from the list fetched from Google.
*/
streamKeyPicker: {
alignSelf: 'stretch',
flex: 1,
height: 40,
marginHorizontal: 4,
width: 300
},
/**
* CTA (label) of the picker.
*/
streamKeyPickerCta: {
marginBottom: 8
},
/**
* Style of a single item in the list.
*/
streamKeyPickerItem: {
padding: 4
},
/**
* Additional style for the selected item.
*/
streamKeyPickerItemHighlight: {
backgroundColor: ColorPalette.lighterGrey
},
/**
* Overall wrapper for the picker.
*/
streamKeyPickerWrapper: {
borderColor: ColorPalette.lightGrey,
borderRadius: 3,
borderWidth: 1,
flexDirection: 'column'
}
});