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);
+});