diff --git a/android/sdk/build.gradle b/android/sdk/build.gradle index 050a044d1..02b3efd82 100644 --- a/android/sdk/build.gradle +++ b/android/sdk/build.gradle @@ -26,6 +26,7 @@ dependencies { compile 'com.facebook.react:react-native:+' compile project(':react-native-background-timer') + compile project(':react-native-fetch-blob') compile project(':react-native-immersive') compile project(':react-native-keep-awake') compile project(':react-native-vector-icons') diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java index 7fbfc0892..0939f4e35 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java @@ -84,10 +84,10 @@ public class JitsiMeetView extends FrameLayout { .addPackage(new com.oblador.vectoricons.VectorIconsPackage()) .addPackage(new com.ocetnik.timer.BackgroundTimerPackage()) .addPackage(new com.oney.WebRTCModule.WebRTCModulePackage()) + .addPackage(new com.RNFetchBlob.RNFetchBlobPackage()) .addPackage(new com.rnimmersive.RNImmersivePackage()) .addPackage(new org.jitsi.meet.sdk.audiomode.AudioModePackage()) - .addPackage( - new org.jitsi.meet.sdk.externalapi.ExternalAPIPackage()) + .addPackage(new org.jitsi.meet.sdk.externalapi.ExternalAPIPackage()) .addPackage(new org.jitsi.meet.sdk.proximity.ProximityPackage()) .setUseDeveloperSupport(BuildConfig.DEBUG) .setInitialLifecycleState(LifecycleState.RESUMED) diff --git a/android/settings.gradle b/android/settings.gradle index b062a0e0e..2684040b5 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -3,6 +3,8 @@ rootProject.name = 'jitsi-meet' include ':app', ':sdk' include ':react-native-background-timer' project(':react-native-background-timer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-timer/android') +include ':react-native-fetch-blob' +project(':react-native-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fetch-blob/android') include ':react-native-immersive' project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android') include ':react-native-keep-awake' diff --git a/ios/Podfile b/ios/Podfile index eea5b633c..ed8f1f3dc 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -17,8 +17,12 @@ target 'JitsiMeet' do ] pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga' - pod 'react-native-background-timer', :path => '../node_modules/react-native-background-timer' - pod 'react-native-keep-awake', :path => '../node_modules/react-native-keep-awake' + pod 'react-native-background-timer', + :path => '../node_modules/react-native-background-timer' + pod 'react-native-fetch-blob', + :path => '../node_modules/react-native-fetch-blob' + pod 'react-native-keep-awake', + :path => '../node_modules/react-native-keep-awake' pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc' pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons' end diff --git a/package.json b/package.json index eb3645aa0..7afe38bc1 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,8 @@ "react-i18next": "4.7.0", "react-native": "0.42.3", "react-native-background-timer": "1.1.0", + "react-native-fetch-blob": "0.10.6", + "react-native-img-cache": "1.4.0", "react-native-immersive": "0.0.5", "react-native-keep-awake": "2.0.4", "react-native-locale-detector": "1.0.1", diff --git a/react/features/app/components/App.native.js b/react/features/app/components/App.native.js index 3b2e9bc0b..dc5ec5ef5 100644 --- a/react/features/app/components/App.native.js +++ b/react/features/app/components/App.native.js @@ -8,6 +8,7 @@ import '../../mobile/audio-mode'; import '../../mobile/background'; import '../../mobile/external-api'; import '../../mobile/full-screen'; +import '../../mobile/image-cache'; import '../../mobile/proximity'; import '../../mobile/wake-lock'; diff --git a/react/features/base/participants/components/Avatar.native.js b/react/features/base/participants/components/Avatar.native.js index 3c10b9c54..b1861c7f6 100644 --- a/react/features/base/participants/components/Avatar.native.js +++ b/react/features/base/participants/components/Avatar.native.js @@ -1,5 +1,8 @@ import React, { Component } from 'react'; -import { Image } from 'react-native'; +import { CustomCachedImage } from 'react-native-img-cache'; + +import AvatarImage from './AvatarImage'; + /** * Implements an avatar as a React Native/mobile {@link Component}. @@ -52,21 +55,16 @@ export default class Avatar extends Component { * @returns {void} */ componentWillReceiveProps(nextProps) { - // uri const prevURI = this.props && this.props.uri; const nextURI = nextProps && nextProps.uri; - let nextState; if (prevURI !== nextURI || !this.state) { - nextState = { - ...nextState, - + const nextState = { /** * The source of the {@link Image} which is the actual - * representation of this {@link Avatar}. As {@code Avatar} - * accepts a single URI and {@code Image} deals with a set of - * possibly multiple URIs, the state {@code source} was - * explicitly introduced in order to reduce unnecessary renders. + * representation of this {@link Avatar}. The state + * {@code source} was explicitly introduced in order to reduce + * unnecessary renders. * * @type {{ * uri: string @@ -76,9 +74,7 @@ export default class Avatar extends Component { uri: nextURI } }; - } - if (nextState) { if (this.state) { this.setState(nextState); } else { @@ -100,10 +96,9 @@ export default class Avatar extends Component { const { uri, ...props } = this.props; return ( - ); diff --git a/react/features/base/participants/components/AvatarImage.native.js b/react/features/base/participants/components/AvatarImage.native.js new file mode 100644 index 000000000..f9193b5f2 --- /dev/null +++ b/react/features/base/participants/components/AvatarImage.native.js @@ -0,0 +1,200 @@ +import React, { Component } from 'react'; +import { Image, View } from 'react-native'; + +import { Platform } from '../../react'; + + +/** + * The default avatar to be used, in case the requested URI is not available + * or fails to be loaded. + * + * This is an inline version of images/avatar2.png. + * + * @type {string} + */ +const DEFAULT_AVATAR = require('./defaultAvatar.png'); + +/** + * The amount of time to wait when the avatar URI is undefined before we start + * showing a default locally generated one. Note that since we have no URI, we + * have nothing we can cache, so the color will be random. + * + * @type {number} + */ +const UNDEFINED_AVATAR_TIMEOUT = 1000; + + +/** + * Implements an Image component wrapper, which returns a default image if the + * requested one fails to load. The default image background is chosen by + * hashing the URL of the image. + */ +export default class AvatarImage extends Component { + /** + * AvatarImage component's property types. + * + * @static + */ + static propTypes = { + /** + * If set to true it will not load the URL, but will use the + * default instead. + */ + forceDefault: React.PropTypes.bool, + + /** + * The source the {@link Image}. + */ + source: React.PropTypes.object, + + /** + * The optional style to add to the {@link Image} in order to customize + * its base look (and feel). + */ + style: React.PropTypes.object + }; + + /** + * Initializes new AvatarImage component. + * + * @param {Object} props - Component props. + */ + constructor(props) { + super(props); + + this.state = { + failed: false, + showDefault: false + }; + + this.componentWillReceiveProps(props); + + this._onError = this._onError.bind(this); + } + + /** + * Notifies this mounted React Component that it will receive new props. + * If the URI is undefined, wait {@code UNDEFINED_AVATAR_TIMEOUT} ms and + * start showing a default locally generated avatar afterwards. + * + * Once a URI is passed, it will be rendered instead, except if loading it + * fails, in which case we fallback to a locally generated avatar again. + * + * @inheritdoc + * @param {Object} nextProps - The read-only React Component props that this + * instance will receive. + * @returns {void} + */ + componentWillReceiveProps(nextProps) { + const prevURI = this.props.source && this.props.source.uri; + const nextURI = nextProps.source && nextProps.source.uri; + + if (typeof prevURI === 'undefined') { + clearTimeout(this._timeout); + if (typeof nextURI === 'undefined') { + this._timeout = setTimeout(() => { + this.setState({ showDefault: true }); + }, UNDEFINED_AVATAR_TIMEOUT); + } else { + this.setState({ showDefault: nextProps.forceDefault }); + } + } + } + + /** + * Clear the timer just in case. See {@code componentWillReceiveProps} for + * details. + * + * @inheritdoc + */ + componentWillUnmount() { + clearTimeout(this._timeout); + } + + /** + * Computes a hash over the URI and returns a HSL background color. We use + * 75% as lightness, for nice pastel style colors. + * + * @returns {string} - The HSL CSS property. + * @private + */ + _getBackgroundColor() { + const uri = this.props.source.uri; + let hash = 0; + + // If we have no URI yet we have no data to hash from, so use a random + // value. + if (typeof uri === 'undefined') { + hash = Math.floor(Math.random() * 360); + } else { + /* eslint-disable no-bitwise */ + + for (let i = 0; i < uri.length; i++) { + hash = uri.charCodeAt(i) + ((hash << 5) - hash); + hash |= 0; // Convert to 32bit integer + } + + /* eslint-enable no-bitwise */ + } + + return `hsl(${hash % 360}, 100%, 75%)`; + } + + /** + * Error handler for image loading. When an image fails to load we'll mark + * it as failed and load the default URI instead. + * + * @private + * @returns {void} + */ + _onError() { + this.setState({ failed: true }); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + */ + render() { + // eslint-disable-next-line no-unused-vars + const { forceDefault, source, style, ...props } = this.props; + const { failed, showDefault } = this.state; + + if (failed || showDefault) { + const coloredBackground = { + ...style, + backgroundColor: this._getBackgroundColor(), + overflow: 'hidden' + }; + + let element = React.createElement(Image, { + ...props, + source: DEFAULT_AVATAR, + style: Platform.OS === 'android' ? style : coloredBackground + }); + + if (Platform.OS === 'android') { + // Here we need to wrap the Image in a View because of a bug in + // React Native for Android: + // https://github.com/facebook/react-native/issues/3198 + + element = React.createElement(View, + { style: coloredBackground }, element); + } + + return element; + } else if (typeof source.uri === 'undefined') { + return null; + } + + // We have a URI and it's time to render it. + return ( + + ); + } +} diff --git a/react/features/base/participants/components/defaultAvatar.png b/react/features/base/participants/components/defaultAvatar.png new file mode 100644 index 000000000..b5b04608a Binary files /dev/null and b/react/features/base/participants/components/defaultAvatar.png differ diff --git a/react/features/mobile/image-cache/index.js b/react/features/mobile/image-cache/index.js new file mode 100644 index 000000000..d43689289 --- /dev/null +++ b/react/features/mobile/image-cache/index.js @@ -0,0 +1 @@ +import './middleware'; diff --git a/react/features/mobile/image-cache/middleware.js b/react/features/mobile/image-cache/middleware.js new file mode 100644 index 000000000..432e3ca36 --- /dev/null +++ b/react/features/mobile/image-cache/middleware.js @@ -0,0 +1,28 @@ +/* @flow */ + +import { ImageCache } from 'react-native-img-cache'; + +import { APP_WILL_MOUNT } from '../../app'; +import { CONFERENCE_FAILED, CONFERENCE_LEFT } from '../../base/conference'; +import { MiddlewareRegistry } from '../../base/redux'; + +/** + * Middleware that captures conference actions and application startup in order + * cleans up the image cache. + * + * @param {Store} store - Redux store. + * @returns {Function} + */ +// eslint-disable-next-line no-unused-vars +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case APP_WILL_MOUNT: + case CONFERENCE_FAILED: + case CONFERENCE_LEFT: + ImageCache.get().clear(); + break; + + } + + return next(action); +});