[React] Cross-platform Components

Introduce certain React Components which may be used to write
cross-platform source code such as Audio like Web's audio, Container
like Web's div, Text like Web's p, etc.
This commit is contained in:
Lyubo Marinov 2017-06-05 13:00:02 -05:00
parent d1ea29beeb
commit 409255f056
28 changed files with 314 additions and 97 deletions

View File

@ -0,0 +1,111 @@
import React, { Component } from 'react';
/**
* The React {@link Component} which is similar to Web's
* {@code HTMLAudioElement}.
*/
export default class AbstractAudio extends Component {
/**
* The (reference to the) {@link ReactElement} which actually implements
* this {@code AbstractAudio}.
*/
_ref: ?Object
/**
* {@code AbstractAudio} component's property types.
*
* @static
*/
static propTypes = {
/**
* The URL of a media resource to use in the element.
*
* @type {string}
*/
src: React.PropTypes.string,
stream: React.PropTypes.object
};
/**
* Initializes a new {@code AbstractAudio} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._setRef = this._setRef.bind(this);
}
/**
* Attempts to pause the playback of the media.
*
* @public
* @returns {void}
*/
pause() {
this._ref && typeof this._ref.pause === 'function' && this._ref.pause();
}
/**
* Attempts to being the playback of the media.
*
* @public
* @returns {void}
*/
play() {
this._ref && typeof this._ref.play === 'function' && this._ref.play();
}
/**
* Renders this {@code AbstractAudio} as a React {@link Component} of a
* specific type.
*
* @param {string|ReactClass} type - The type of the React {@code Component}
* which is to be rendered.
* @param {Object|undefined} props - The read-only React {@code Component}
* properties, if any, to render. If {@code undefined}, the props of this
* instance will be rendered.
* @protected
* @returns {ReactElement}
*/
_render(type, props) {
const {
children,
/* eslint-disable no-unused-vars */
// The following properties are consumed by React itself so they are
// to not be propagated.
ref,
/* eslint-enable no-unused-vars */
...filteredProps
} = props || this.props;
return (
React.createElement(
type,
{
...filteredProps,
ref: this._setRef
},
children));
}
/**
* Set the (reference to the) {@link ReactElement} which actually implements
* this {@code AbstractAudio}.
*
* @param {Object} ref - The (reference to the) {@code ReactElement} which
* actually implements this {@code AbstractAudio}.
* @private
* @returns {void}
*/
_setRef(ref) {
this._ref = ref;
}
}

View File

@ -6,11 +6,12 @@ import { shouldRenderVideoTrack } from '../functions';
import { Video } from './_'; import { Video } from './_';
/** /**
* Component that renders video element for a specified video track. * Implements a React {@link Component} that renders video element for a
* specific video track.
* *
* @abstract * @abstract
*/ */
export class AbstractVideoTrack extends Component { export default class AbstractVideoTrack extends Component {
/** /**
* AbstractVideoTrack component's property types. * AbstractVideoTrack component's property types.
* *

View File

@ -0,0 +1 @@
export * from './web';

View File

@ -1,19 +1,19 @@
import React, { Component } from 'react'; /* @flow */
import AbstractAudio from '../AbstractAudio';
/** /**
* The React Native component which is similar to Web's audio element and wraps * The React Native/mobile {@link Component} which is similar to Web's
* around react-native-webrtc's RTCView. * {@code HTMLAudioElement} and wraps around react-native-webrtc's
* {@link RTCView}.
*/ */
export class Audio extends Component { export default class Audio extends AbstractAudio {
/** /**
* Audio component's property types. * {@code Audio} component's property types.
* *
* @static * @static
*/ */
static propTypes = { static propTypes = AbstractAudio.propTypes;
muted: React.PropTypes.bool,
stream: React.PropTypes.object
};
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.

View File

@ -1,21 +1,23 @@
/* @flow */
import React, { Component } from 'react'; import React, { Component } from 'react';
import { RTCView } from 'react-native-webrtc'; import { RTCView } from 'react-native-webrtc';
import { styles } from './styles'; import { styles } from './styles';
/** /**
* The React Native component which is similar to Web's video element and wraps * The React Native {@link Component} which is similar to Web's
* around react-native-webrtc's RTCView. * {@code HTMLVideoElement} and wraps around react-native-webrtc's
* {@link RTCView}.
*/ */
export class Video extends Component { export default class Video extends Component {
/** /**
* Video component's property types. * {@code Video} component's property types.
* *
* @static * @static
*/ */
static propTypes = { static propTypes = {
mirror: React.PropTypes.bool, mirror: React.PropTypes.bool,
muted: React.PropTypes.bool,
onPlaying: React.PropTypes.func, onPlaying: React.PropTypes.func,
stream: React.PropTypes.object, stream: React.PropTypes.object,

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Animated } from 'react-native'; import { Animated } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { AbstractVideoTrack } from '../AbstractVideoTrack'; import AbstractVideoTrack from '../AbstractVideoTrack';
import { styles } from './styles'; import { styles } from './styles';
/** /**

View File

@ -1,3 +1,3 @@
export * from './Audio'; export { default as Audio } from './Audio';
export * from './Video'; export { default as Video } from './Video';
export { default as VideoTrack } from './VideoTrack'; export { default as VideoTrack } from './VideoTrack';

View File

@ -0,0 +1,26 @@
/* @flow */
import AbstractAudio from '../AbstractAudio';
/**
* The React/Web {@link Component} which is similar to and wraps around
* {@code HTMLAudioElement} in order to facilitate cross-platform source code.
*/
export default class Audio extends AbstractAudio {
/**
* {@code Audio} component's property types.
*
* @static
*/
static propTypes = AbstractAudio.propTypes;
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return super._render('audio');
}
}

View File

@ -0,0 +1 @@
export { default as Audio } from './Audio';

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { Image } from 'react-native'; import { Image } from 'react-native';
/** /**
* Display a participant avatar. * Implements an avatar as a React Native/mobile {@link Component}.
*/ */
export default class Avatar extends Component { export default class Avatar extends Component {
/** /**
@ -12,10 +12,16 @@ export default class Avatar extends Component {
*/ */
static propTypes = { static propTypes = {
/** /**
* The optional style to add to an Avatar in order to customize its base * The optional style to add to the {@link Avatar} in order to customize
* look (and feel). * its base look (and feel).
*/ */
style: React.PropTypes.object, style: React.PropTypes.object,
/**
* The URI of the {@link Avatar}.
*
* @type {string}
*/
uri: React.PropTypes.string uri: React.PropTypes.string
}; };
@ -87,13 +93,19 @@ export default class Avatar extends Component {
* @inheritdoc * @inheritdoc
*/ */
render() { render() {
// Propagate all props of this Avatar but the ones consumed by this
// Avatar to the Image it renders.
// eslint-disable-next-line no-unused-vars
const { uri, ...props } = this.props;
return ( return (
<Image <Image
{ ...props }
// XXX Avatar is expected to display the whole image. // XXX Avatar is expected to display the whole image.
resizeMode = 'contain' resizeMode = 'contain'
source = { this.state.source } source = { this.state.source } />
style = { this.props.style } />
); );
} }
} }

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
/** /**
* Display a participant avatar. * Implements an avatar as a React/Web {@link Component}.
*/ */
export default class Avatar extends Component { export default class Avatar extends Component {
/** /**
@ -11,7 +11,7 @@ export default class Avatar extends Component {
*/ */
static propTypes = { static propTypes = {
/** /**
* The URL for the avatar. * The URI of the {@link Avatar}.
* *
* @type {string} * @type {string}
*/ */
@ -24,6 +24,16 @@ export default class Avatar extends Component {
* @inheritdoc * @inheritdoc
*/ */
render() { render() {
return <img src = { this.props.uri } />; // Propagate all props of this Avatar but the ones consumed by this
// Avatar to the img it renders.
// eslint-disable-next-line no-unused-vars
const { uri, ...props } = this.props;
return (
<img
{ ...props }
src = { uri } />
);
} }
} }

View File

@ -69,6 +69,11 @@ export default class AbstractContainer extends Component {
...filteredProps ...filteredProps
} = props || this.props; } = props || this.props;
// visible
if (typeof visible !== 'undefined' && !visible) {
return null;
}
return React.createElement(type, filteredProps, children); return React.createElement(type, filteredProps, children);
} }
} }

View File

@ -1,58 +0,0 @@
import React from 'react';
import {
TouchableHighlight,
TouchableWithoutFeedback,
View
} from 'react-native';
import AbstractContainer from './AbstractContainer';
/**
* Represents a container of React Native Component children with a style.
*
* @extends AbstractContainer
*/
export class Container extends AbstractContainer {
/**
* Container component's property types.
*
* @static
*/
static propTypes = AbstractContainer.propTypes
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
// eslint-disable-next-line prefer-const
let { onClick, style, touchFeedback, visible, ...props } = this.props;
// visible
if (typeof visible !== 'undefined' && !visible) {
return null;
}
// onClick & touchFeedback
(typeof touchFeedback === 'undefined') && (touchFeedback = onClick);
const renderParent = touchFeedback || onClick;
// eslint-disable-next-line object-property-newline
let component = this._render(View, { ...props, style });
if (renderParent) {
const parentType
= touchFeedback ? TouchableHighlight : TouchableWithoutFeedback;
const parentProps = {};
onClick && (parentProps.onPress = onClick);
component = React.createElement(parentType, parentProps, component);
}
return component;
}
}

View File

@ -0,0 +1 @@
export * from './native';

View File

@ -0,0 +1 @@
export * from './web';

View File

@ -1,3 +1 @@
export * from './Container'; export * from './_';
export * from './Link';
export { default as Watermarks } from './Watermarks';

View File

@ -0,0 +1,55 @@
import React from 'react';
import {
TouchableHighlight,
TouchableWithoutFeedback,
View
} from 'react-native';
import AbstractContainer from '../AbstractContainer';
/**
* Represents a container of React Native/mobile {@link Component} children.
*
* @extends AbstractContainer
*/
export default class Container extends AbstractContainer {
/**
* {@code Container} component's property types.
*
* @static
*/
static propTypes = AbstractContainer.propTypes;
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
// eslint-disable-next-line prefer-const
let { onClick, style, touchFeedback, ...props } = this.props;
// eslint-disable-next-line object-property-newline
let component = this._render(View, { ...props, style });
if (component) {
// onClick & touchFeedback
(typeof touchFeedback === 'undefined') && (touchFeedback = onClick);
if (touchFeedback || onClick) {
const parentType
= touchFeedback
? TouchableHighlight
: TouchableWithoutFeedback;
const parentProps = {};
onClick && (parentProps.onPress = onClick);
component
= React.createElement(parentType, parentProps, component);
}
}
return component;
}
}

View File

@ -1,13 +1,15 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Linking, Text } from 'react-native'; import { Linking } from 'react-native';
import Text from './Text';
/** /**
* Implements a (hyper)link to a URL in the fashion of the HTML anchor element * Implements a (hyper)link to a URL in the fashion of the HTML anchor element
* and its href attribute. * and its href attribute.
*/ */
export class Link extends Component { export default class Link extends Component {
/** /**
* Link component's property types. * {@code Link} component's property types.
* *
* @static * @static
*/ */
@ -56,9 +58,7 @@ export class Link extends Component {
<Text <Text
onPress = { this._onPress } onPress = { this._onPress }
style = { this.props.style }> style = { this.props.style }>
{ { this.props.children }
this.props.children
}
</Text> </Text>
); );
} }

View File

@ -0,0 +1 @@
export { Text as default } from 'react-native';

View File

@ -0,0 +1,3 @@
export { default as Container } from './Container';
export { default as Link } from './Link';
export { default as Text } from './Text';

View File

@ -0,0 +1,25 @@
import AbstractContainer from '../AbstractContainer';
/**
* Represents a container of React/Web {@link Component} children with a style.
*
* @extends AbstractContainer
*/
export default class Container extends AbstractContainer {
/**
* {@code Container} component's property types.
*
* @static
*/
static propTypes = AbstractContainer.propTypes;
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return this._render('div');
}
}

View File

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

View File

@ -3,7 +3,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { translate } from '../../i18n'; import { translate } from '../../../i18n';
declare var interfaceConfig: Object; declare var interfaceConfig: Object;

View File

@ -0,0 +1,3 @@
export { default as Container } from './Container';
export { default as Text } from './Text';
export { default as Watermarks } from './Watermarks';

View File

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { Text, TextInput, TouchableHighlight, View } from 'react-native'; import { TextInput, TouchableHighlight, View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { Link } from '../../base/react'; import { Link, Text } from '../../base/react';
import { ColorPalette } from '../../base/styles'; import { ColorPalette } from '../../base/styles';
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage'; import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';