This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { Text, View, ViewStyle } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../../app/types';
|
||||
import { _abstractMapStateToProps } from '../../../../base/dialog/functions';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import { setGoogleAPIState } from '../../../../google-api/actions';
|
||||
import GoogleSignInButton from '../../../../google-api/components/GoogleSignInButton.native';
|
||||
import {
|
||||
GOOGLE_API_STATES,
|
||||
GOOGLE_SCOPE_YOUTUBE
|
||||
} from '../../../../google-api/constants';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import googleApi from '../../../../google-api/googleApi.native';
|
||||
import logger from '../../../logger';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* Prop type of the component {@code GoogleSigninForm}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Style of the dialogs feature.
|
||||
*/
|
||||
_dialogStyles: any;
|
||||
|
||||
/**
|
||||
* The Redux dispatch Function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* The current state of the Google api as defined in {@code constants.js}.
|
||||
*/
|
||||
googleAPIState: number;
|
||||
|
||||
/**
|
||||
* The recently received Google response.
|
||||
*/
|
||||
googleResponse: any;
|
||||
|
||||
/**
|
||||
* A callback to be invoked when an authenticated user changes, so
|
||||
* then we can get (or clear) the YouTube stream key.
|
||||
*/
|
||||
onUserChanged: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to render a google sign in form, or a google stream picker dialog.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class GoogleSigninForm extends Component<IProps> {
|
||||
/**
|
||||
* Instantiates a new {@code GoogleSigninForm} component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._logGoogleError = this._logGoogleError.bind(this);
|
||||
this._onGoogleButtonPress = this._onGoogleButtonPress.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's Component.componentDidMount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidMount() {
|
||||
googleApi.hasPlayServices()
|
||||
.then(() => {
|
||||
googleApi.configure({
|
||||
offlineAccess: false,
|
||||
scopes: [ GOOGLE_SCOPE_YOUTUBE ]
|
||||
});
|
||||
|
||||
googleApi.signInSilently().then((response: any) => {
|
||||
this._setApiState(response
|
||||
? GOOGLE_API_STATES.SIGNED_IN
|
||||
: GOOGLE_API_STATES.LOADED,
|
||||
response);
|
||||
}, () => {
|
||||
this._setApiState(GOOGLE_API_STATES.LOADED);
|
||||
});
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
this._logGoogleError(error);
|
||||
this._setApiState(GOOGLE_API_STATES.NOT_AVAILABLE);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { _dialogStyles, t } = this.props;
|
||||
const { googleAPIState, googleResponse } = this.props;
|
||||
const signedInUser = googleResponse?.user?.email;
|
||||
|
||||
if (googleAPIState === GOOGLE_API_STATES.NOT_AVAILABLE
|
||||
|| googleAPIState === GOOGLE_API_STATES.NEEDS_LOADING
|
||||
|| typeof googleAPIState === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userInfo = signedInUser
|
||||
? `${t('liveStreaming.signedInAs')} ${signedInUser}`
|
||||
: t('liveStreaming.signInCTA');
|
||||
|
||||
return (
|
||||
<View style = { styles.formWrapper as ViewStyle }>
|
||||
<View style = { styles.helpText as ViewStyle }>
|
||||
<Text
|
||||
style = { [
|
||||
_dialogStyles.text,
|
||||
styles.text
|
||||
] }>
|
||||
{ userInfo }
|
||||
</Text>
|
||||
</View>
|
||||
<GoogleSignInButton
|
||||
onClick = { this._onGoogleButtonPress }
|
||||
signedIn = {
|
||||
googleAPIState === GOOGLE_API_STATES.SIGNED_IN } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to log developer related errors.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} error - The error to be logged.
|
||||
* @returns {void}
|
||||
*/
|
||||
_logGoogleError(error: 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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?.user) {
|
||||
// the user is signed in
|
||||
this._onSignOut();
|
||||
} else {
|
||||
this._onSignIn();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a sign in if the user is not signed in yet.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSignIn() {
|
||||
googleApi.signIn().then((response: any) => {
|
||||
this._setApiState(GOOGLE_API_STATES.SIGNED_IN, response);
|
||||
}, this._logGoogleError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a sign out if the user is signed in.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSignOut() {
|
||||
googleApi.signOut().then((response: any) => {
|
||||
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: number, googleResponse?: Object) {
|
||||
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: IReduxState) {
|
||||
const { googleAPIState, googleResponse } = state['features/google-api'];
|
||||
|
||||
return {
|
||||
..._abstractMapStateToProps(state),
|
||||
googleAPIState,
|
||||
googleResponse
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(GoogleSigninForm));
|
||||
@@ -0,0 +1,63 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import { openDialog } from '../../../../base/dialog/actions';
|
||||
import { LIVE_STREAMING_ENABLED } from '../../../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../../../base/flags/functions';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import { navigate }
|
||||
from '../../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { screen } from '../../../../mobile/navigation/routes';
|
||||
import AbstractLiveStreamButton, {
|
||||
IProps as AbstractProps,
|
||||
_mapStateToProps as _abstractMapStateToProps
|
||||
} from '../AbstractLiveStreamButton';
|
||||
import { IProps } from '../AbstractStartLiveStreamDialog';
|
||||
|
||||
import StopLiveStreamDialog from './StopLiveStreamDialog';
|
||||
|
||||
type Props = IProps & AbstractProps;
|
||||
|
||||
/**
|
||||
* Button for opening the live stream settings screen.
|
||||
*/
|
||||
class LiveStreamButton extends AbstractLiveStreamButton<Props> {
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_onHandleClick() {
|
||||
const { _isLiveStreamRunning, dispatch } = this.props;
|
||||
|
||||
if (_isLiveStreamRunning) {
|
||||
dispatch(openDialog(StopLiveStreamDialog));
|
||||
} else {
|
||||
navigate(screen.conference.liveStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for this component.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} ownProps - The properties explicitly passed to the component
|
||||
* instance.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
export function mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const enabled = getFeatureFlag(state, LIVE_STREAMING_ENABLED, true);
|
||||
const abstractProps = _abstractMapStateToProps(state, ownProps);
|
||||
|
||||
return {
|
||||
...abstractProps,
|
||||
visible: enabled && abstractProps.visible
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(LiveStreamButton));
|
||||
@@ -0,0 +1,160 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
|
||||
import { StyleType } from '../../../../base/styles/functions.any';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import googleApi from '../../../../google-api/googleApi.native';
|
||||
import HeaderNavigationButton
|
||||
from '../../../../mobile/navigation/components/HeaderNavigationButton';
|
||||
import { goBack }
|
||||
from '../../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { setLiveStreamKey } from '../../../actions';
|
||||
import AbstractStartLiveStreamDialog, { IProps, _mapStateToProps } 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<IProps> {
|
||||
/**
|
||||
* Constructor of the component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onStartPress = this._onStartPress.bind(this);
|
||||
this._onStreamKeyChangeNative
|
||||
= this._onStreamKeyChangeNative.bind(this);
|
||||
this._onStreamKeyPick = this._onStreamKeyPick.bind(this);
|
||||
this._onUserChanged = this._onUserChanged.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}. Invoked
|
||||
* immediately after this component is mounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
const { navigation, t } = this.props;
|
||||
|
||||
navigation.setOptions({
|
||||
headerRight: () => (
|
||||
<HeaderNavigationButton
|
||||
label = { t('dialog.start') }
|
||||
onPress = { this._onStartPress }
|
||||
twoActions = { true } />
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts live stream session and goes back to the previous screen.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onStartPress() {
|
||||
this._onSubmit() && goBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component}'s render.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<JitsiScreen style = { styles.startLiveStreamContainer as StyleType }>
|
||||
<GoogleSigninForm
|
||||
onUserChanged = { this._onUserChanged } />
|
||||
<StreamKeyPicker
|
||||
broadcasts = { this.state.broadcasts }
|
||||
onChange = { this._onStreamKeyPick } />
|
||||
<StreamKeyForm
|
||||
onChange = { this._onStreamKeyChangeNative }
|
||||
value = {
|
||||
this.state.streamKey || this.props._streamKey || ''
|
||||
} />
|
||||
</JitsiScreen>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: string) {
|
||||
this.props.dispatch(setLiveStreamKey(streamKey));
|
||||
this._onStreamKeyChange(streamKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: string) {
|
||||
this.setState({
|
||||
streamKey
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 retrieved signin response.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onUserChanged(response: Object) {
|
||||
if (response) {
|
||||
googleApi.getTokens()
|
||||
.then((tokens: any) => {
|
||||
googleApi.getYouTubeLiveStreams(tokens.accessToken)
|
||||
.then((broadcasts: any) => {
|
||||
this.setState({
|
||||
broadcasts
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
broadcasts: undefined,
|
||||
streamKey: undefined
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
broadcasts: undefined,
|
||||
streamKey: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(StartLiveStreamDialog));
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import ConfirmDialog from '../../../../base/dialog/components/native/ConfirmDialog';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import AbstractStopLiveStreamDialog, {
|
||||
_mapStateToProps
|
||||
} from '../AbstractStopLiveStreamDialog';
|
||||
|
||||
/**
|
||||
* A React Component for confirming the participant wishes to stop the currently
|
||||
* active live stream of the conference.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class StopLiveStreamDialog extends AbstractStopLiveStreamDialog {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
descriptionKey = 'dialog.stopStreamingWarning'
|
||||
onSubmit = { this._onSubmit } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(StopLiveStreamDialog));
|
||||
@@ -0,0 +1,164 @@
|
||||
import React from 'react';
|
||||
import { Linking, Text, View, ViewStyle } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import { _abstractMapStateToProps } from '../../../../base/dialog/functions';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import Button from '../../../../base/ui/components/native/Button';
|
||||
import Input from '../../../../base/ui/components/native/Input';
|
||||
import { BUTTON_TYPES } from '../../../../base/ui/constants.native';
|
||||
import AbstractStreamKeyForm, {
|
||||
IProps as AbstractProps
|
||||
} from '../AbstractStreamKeyForm';
|
||||
import { getLiveStreaming } from '../functions';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
interface IProps extends AbstractProps {
|
||||
|
||||
/**
|
||||
* Style of the dialogs feature.
|
||||
*/
|
||||
_dialogStyles: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* A React Component for entering a key for starting a YouTube live stream.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class StreamKeyForm extends AbstractStreamKeyForm<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code StreamKeyForm} instance.
|
||||
*
|
||||
* @param {IProps} props - The React {@code Component} props to initialize
|
||||
* the new {@code StreamKeyForm} instance with.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onOpenGooglePrivacyPolicy = this._onOpenGooglePrivacyPolicy.bind(this);
|
||||
this._onOpenHelp = this._onOpenHelp.bind(this);
|
||||
this._onOpenYoutubeTerms = this._onOpenYoutubeTerms.bind(this);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { _dialogStyles, t } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style = { styles.formWrapper as ViewStyle }>
|
||||
<Input
|
||||
customStyles = {{
|
||||
input: styles.streamKeyInput,
|
||||
container: styles.streamKeyContainer }}
|
||||
onChange = { this._onInputChange }
|
||||
placeholder = { t('liveStreaming.enterStreamKey') }
|
||||
value = { this.props.value } />
|
||||
<View style = { styles.formValidationItem as ViewStyle }>
|
||||
{
|
||||
this.state.showValidationError && <Text
|
||||
style = { [
|
||||
_dialogStyles.text,
|
||||
styles.warningText
|
||||
] }>
|
||||
{ t('liveStreaming.invalidStreamKey') }
|
||||
</Text>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
<View style = { styles.formButtonsWrapper as ViewStyle }>
|
||||
<Button
|
||||
accessibilityLabel = 'liveStreaming.streamIdHelp'
|
||||
labelKey = 'liveStreaming.streamIdHelp'
|
||||
labelStyle = { styles.buttonLabelStyle }
|
||||
onClick = { this._onOpenHelp }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
<Button
|
||||
accessibilityLabel = 'liveStreaming.youtubeTerms'
|
||||
labelKey = 'liveStreaming.youtubeTerms'
|
||||
labelStyle = { styles.buttonLabelStyle }
|
||||
onClick = { this._onOpenYoutubeTerms }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
<Button
|
||||
accessibilityLabel = 'liveStreaming.googlePrivacyPolicy'
|
||||
labelKey = 'liveStreaming.googlePrivacyPolicy'
|
||||
labelStyle = { styles.buttonLabelStyle }
|
||||
onClick = { this._onOpenGooglePrivacyPolicy }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the Google Privacy Policy web page.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOpenGooglePrivacyPolicy() {
|
||||
const url = this.props._liveStreaming.dataPrivacyURL;
|
||||
|
||||
if (typeof url === 'string') {
|
||||
Linking.openURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the information link on how to manually locate a YouTube broadcast
|
||||
* stream key.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOpenHelp() {
|
||||
const url = this.props._liveStreaming.helpURL;
|
||||
|
||||
if (typeof url === 'string') {
|
||||
Linking.openURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the YouTube terms and conditions web page.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOpenYoutubeTerms() {
|
||||
const url = this.props._liveStreaming.termsURL;
|
||||
|
||||
if (typeof url === 'string') {
|
||||
Linking.openURL(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for the
|
||||
* {@code StreamKeyForm} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _liveStreaming: LiveStreamingProps
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
..._abstractMapStateToProps(state),
|
||||
_liveStreaming: getLiveStreaming(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(StreamKeyForm));
|
||||
@@ -0,0 +1,168 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import {
|
||||
Linking,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle
|
||||
} from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { _abstractMapStateToProps } from '../../../../base/dialog/functions';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import { YOUTUBE_LIVE_DASHBOARD_URL } from '../constants';
|
||||
|
||||
import styles, { ACTIVE_OPACITY, TOUCHABLE_UNDERLAY } from './styles';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Style of the dialogs feature.
|
||||
*/
|
||||
_dialogStyles: any;
|
||||
|
||||
/**
|
||||
* The list of broadcasts the user can pick from.
|
||||
*/
|
||||
broadcasts?: Array<{ key: string; title: string; }>;
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the user picked a broadcast. To be invoked
|
||||
* with a single key (string).
|
||||
*/
|
||||
onChange: Function;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* The key of the currently selected stream.
|
||||
*/
|
||||
streamKey?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<IProps, IState> {
|
||||
|
||||
/**
|
||||
* Instantiates a new instance of StreamKeyPicker.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
streamKey: null
|
||||
};
|
||||
|
||||
this._onOpenYoutubeDashboard = this._onOpenYoutubeDashboard.bind(this);
|
||||
this._onStreamPick = this._onStreamPick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { _dialogStyles, broadcasts } = this.props;
|
||||
|
||||
if (!broadcasts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!broadcasts.length) {
|
||||
return (
|
||||
<View style = { styles.formWrapper as ViewStyle }>
|
||||
<TouchableOpacity
|
||||
onPress = { this._onOpenYoutubeDashboard }>
|
||||
<Text
|
||||
style = { [
|
||||
_dialogStyles.text,
|
||||
styles.warningText
|
||||
] }>
|
||||
{ this.props.t(
|
||||
'liveStreaming.getStreamKeyManually') }
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style = { styles.formWrapper as ViewStyle }>
|
||||
<View style = { styles.streamKeyPickerCta as ViewStyle }>
|
||||
<Text
|
||||
style = { [
|
||||
_dialogStyles.text,
|
||||
styles.text
|
||||
] }>
|
||||
{ this.props.t('liveStreaming.choose') }
|
||||
</Text>
|
||||
</View>
|
||||
<View style = { styles.streamKeyPickerWrapper as ViewStyle } >
|
||||
{ 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
|
||||
] as ViewStyle[] }
|
||||
underlayColor = { TOUCHABLE_UNDERLAY }>
|
||||
<Text
|
||||
style = { [
|
||||
_dialogStyles.text,
|
||||
styles.text
|
||||
] }>
|
||||
{ broadcast.title }
|
||||
</Text>
|
||||
</TouchableHighlight>))
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the link which should display the YouTube broadcast live stream
|
||||
* key.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOpenYoutubeDashboard() {
|
||||
Linking.openURL(YOUTUBE_LIVE_DASHBOARD_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: string) {
|
||||
return () => {
|
||||
this.setState({
|
||||
streamKey
|
||||
});
|
||||
this.props.onChange(streamKey);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(
|
||||
connect(_abstractMapStateToProps)(StreamKeyPicker));
|
||||
146
react/features/recording/components/LiveStream/native/styles.ts
Normal file
146
react/features/recording/components/LiveStream/native/styles.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { createStyleSheet } from '../../../../base/styles/functions.native';
|
||||
import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
/**
|
||||
* Opacity of the TouchableHighlight.
|
||||
*/
|
||||
export const ACTIVE_OPACITY = 0.3;
|
||||
|
||||
/**
|
||||
* Underlay of the TouchableHighlight.
|
||||
*/
|
||||
export const TOUCHABLE_UNDERLAY = BaseTheme.palette.ui06;
|
||||
|
||||
/**
|
||||
* The styles of the React {@code Components} of LiveStream.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
|
||||
/**
|
||||
* Generic component to wrap form sections into achieving a unified look.
|
||||
*/
|
||||
formWrapper: {
|
||||
alignItems: 'stretch',
|
||||
flexDirection: 'column',
|
||||
paddingHorizontal: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
formValidationItem: {
|
||||
alignSelf: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
height: BaseTheme.spacing[4],
|
||||
marginTop: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
formButtonsWrapper: {
|
||||
alignSelf: 'center',
|
||||
display: 'flex',
|
||||
maxWidth: 200
|
||||
},
|
||||
|
||||
buttonLabelStyle: {
|
||||
color: BaseTheme.palette.link01
|
||||
},
|
||||
|
||||
/**
|
||||
* Explaining text on the top of the sign in form.
|
||||
*/
|
||||
helpText: {
|
||||
marginBottom: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
/**
|
||||
* Container for the live stream screen.
|
||||
*/
|
||||
startLiveStreamContainer: {
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: BaseTheme.spacing[2],
|
||||
paddingVertical: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper link text.
|
||||
*/
|
||||
streamKeyHelp: {
|
||||
alignSelf: 'flex-end'
|
||||
},
|
||||
|
||||
/**
|
||||
* Input field to manually enter stream key.
|
||||
*/
|
||||
streamKeyInput: {
|
||||
alignSelf: 'stretch',
|
||||
color: BaseTheme.palette.text01,
|
||||
textAlign: 'left'
|
||||
},
|
||||
|
||||
streamKeyContainer: {
|
||||
marginTop: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
/**
|
||||
* Custom component to pick a broadcast from the list fetched from Google.
|
||||
*/
|
||||
streamKeyPicker: {
|
||||
alignSelf: 'stretch',
|
||||
flex: 1,
|
||||
height: 40,
|
||||
marginHorizontal: BaseTheme.spacing[1],
|
||||
width: 300
|
||||
},
|
||||
|
||||
/**
|
||||
* CTA (label) of the picker.
|
||||
*/
|
||||
streamKeyPickerCta: {
|
||||
marginBottom: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
/**
|
||||
* Style of a single item in the list.
|
||||
*/
|
||||
streamKeyPickerItem: {
|
||||
padding: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
/**
|
||||
* Additional style for the selected item.
|
||||
*/
|
||||
streamKeyPickerItemHighlight: {
|
||||
backgroundColor: BaseTheme.palette.ui04
|
||||
},
|
||||
|
||||
/**
|
||||
* Overall wrapper for the picker.
|
||||
*/
|
||||
streamKeyPickerWrapper: {
|
||||
borderColor: BaseTheme.palette.ui07,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
borderWidth: 1,
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
/**
|
||||
* Terms and Conditions texts.
|
||||
*/
|
||||
tcText: {
|
||||
textAlign: 'right'
|
||||
},
|
||||
|
||||
text: {
|
||||
color: BaseTheme.palette.text01,
|
||||
fontSize: 14,
|
||||
textAlign: 'left'
|
||||
},
|
||||
|
||||
/**
|
||||
* A different colored text to indicate information needing attention.
|
||||
*/
|
||||
warningText: {
|
||||
color: BaseTheme.palette.warning02
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user