[RN] Replace cached image implementation

Use react-native-fastimage, which uses 2 full-native image impleentations using
well known and mature (native) libraries.

This gets us rid of 2 libraries which were observerd as a source of bugs and
created trouble with dependencies: react-native-fetch-blob and
react-native-img-cache. They are also no longer well maintained.
This commit is contained in:
Saúl Ibarra Corretgé 2018-07-31 11:44:48 +02:00 committed by Paweł Domas
parent f5a667ad9e
commit 27021ea271
17 changed files with 160 additions and 323 deletions

View File

@ -68,3 +68,9 @@
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
# FastImage
-keep public class com.dylanvann.fastimage.* {*;}
-keep public class com.dylanvann.fastimage.** {*;}

View File

@ -25,7 +25,7 @@ dependencies {
compile 'com.facebook.react:react-native:+'
compile project(':react-native-background-timer')
compile project(':react-native-fetch-blob')
compile project(':react-native-fast-image')
compile project(':react-native-immersive')
compile project(':react-native-keep-awake')
compile project(':react-native-linear-gradient')

View File

@ -122,12 +122,12 @@ class ReactInstanceManagerHolder {
.addPackage(new com.BV.LinearGradient.LinearGradientPackage())
.addPackage(new com.calendarevents.CalendarEventsPackage())
.addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
.addPackage(new com.dylanvann.fastimage.FastImageViewPackage())
.addPackage(new com.facebook.react.shell.MainReactPackage())
.addPackage(new com.i18n.reactnativei18n.ReactNativeI18n())
.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 com.zmxv.RNSound.RNSoundPackage())
.addPackage(new ReactPackageAdapter() {

View File

@ -3,8 +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-fast-image'
project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fast-image/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'

View File

@ -28,8 +28,8 @@ target 'JitsiMeet' do
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-fast-image',
:path => '../node_modules/react-native-fast-image'
pod 'react-native-keep-awake',
:path => '../node_modules/react-native-keep-awake'
pod 'react-native-locale-detector',

View File

@ -1,6 +1,7 @@
PODS:
- boost-for-react-native (1.63.0)
- DoubleConversion (1.1.5)
- FLAnimatedImage (1.0.12)
- Folly (2016.09.26.00):
- boost-for-react-native
- DoubleConversion
@ -12,8 +13,11 @@ PODS:
- React
- react-native-calendar-events (1.6.0):
- React
- react-native-fetch-blob (0.10.6):
- React/Core
- react-native-fast-image (4.0.14):
- FLAnimatedImage
- React
- SDWebImage/Core
- SDWebImage/GIF
- react-native-keep-awake (2.0.6):
- React
- react-native-locale-detector (1.0.0):
@ -68,6 +72,10 @@ PODS:
- React/Core
- RNVectorIcons (4.4.2):
- React
- SDWebImage/Core (4.4.1)
- SDWebImage/GIF (4.4.1):
- FLAnimatedImage (~> 1.0)
- SDWebImage/Core
- yoga (0.55.4.React)
DEPENDENCIES:
@ -76,7 +84,7 @@ DEPENDENCIES:
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- react-native-background-timer (from `../node_modules/react-native-background-timer`)
- react-native-calendar-events (from `../node_modules/react-native-calendar-events`)
- react-native-fetch-blob (from `../node_modules/react-native-fetch-blob`)
- react-native-fast-image (from `../node_modules/react-native-fast-image`)
- react-native-keep-awake (from `../node_modules/react-native-keep-awake`)
- react-native-locale-detector (from `../node_modules/react-native-locale-detector`)
- react-native-webrtc (from `../node_modules/react-native-webrtc`)
@ -98,6 +106,8 @@ DEPENDENCIES:
SPEC REPOS:
https://github.com/cocoapods/specs.git:
- boost-for-react-native
- FLAnimatedImage
- SDWebImage
EXTERNAL SOURCES:
DoubleConversion:
@ -112,8 +122,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-background-timer"
react-native-calendar-events:
:path: "../node_modules/react-native-calendar-events"
react-native-fetch-blob:
:path: "../node_modules/react-native-fetch-blob"
react-native-fast-image:
:path: "../node_modules/react-native-fast-image"
react-native-keep-awake:
:path: "../node_modules/react-native-keep-awake"
react-native-locale-detector:
@ -132,20 +142,22 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
DoubleConversion: e22e0762848812a87afd67ffda3998d9ef29170c
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
Folly: 211775e49d8da0ca658aebc8eab89d642935755c
glog: 1de0bb937dccdc981596d3b5825ebfb765017ded
React: aa2040dbb6f317b95314968021bd2888816e03d5
react-native-background-timer: 63dcbf37dbcf294b5c6c071afcdc661fa06a7594
react-native-calendar-events: fe6fbc8ed337a7423c98f2c9012b25f20444de09
react-native-fetch-blob: 63394b1d7b0781547b3e4463b3195790177b1222
react-native-fast-image: cba3d9bf9c2cf8ddb643d887a686c53a5dd90a2c
react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94
react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1
react-native-webrtc: 31b6d3f1e3e2ce373aa43fd682b04367250f807d
ReactNativePermissions: 9f2d9c45c98800795e6c2ed330e25d11a66a8169
RNSound: b360b3862d3118ed1c74bb9825696b5957686ac4
RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
SDWebImage: 47e9b5b925cbce75946c23f0c42dd19464189af4
yoga: a23273df0088bf7f2bb7e5d7b00044ea57a2a54a
PODFILE CHECKSUM: e24d0131e937934fbe4d1f0b7ad5947ee0192f58
PODFILE CHECKSUM: 1d5c8382f73d9540fac68d93b32e1d3b58d069ee
COCOAPODS: 1.5.3

38
package-lock.json generated
View File

@ -5573,11 +5573,6 @@
"randomfill": "^1.0.3"
}
},
"crypto-js": {
"version": "3.1.9-1",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz",
"integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg="
},
"css-color-list": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/css-color-list/-/css-color-list-0.0.1.tgz",
@ -12715,35 +12710,12 @@
"jssha": "^2.2.0"
}
},
"react-native-fetch-blob": {
"version": "github:joltup/react-native-fetch-blob#1f9a1761aea4e37bd672bd0d233f3adf0e113a11",
"from": "github:joltup/react-native-fetch-blob#1f9a1761aea4e37bd672bd0d233f3adf0e113a11",
"react-native-fast-image": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-4.0.14.tgz",
"integrity": "sha512-MeRgL70JxoY/hn8ZRGBsDED9SGvTEeznneL//fWZyLaG0CM+w2CH4QXAMvADnIvu2RFd8WQWNii6c6VOpVe4Tg==",
"requires": {
"base-64": "0.1.0",
"glob": "7.0.6"
},
"dependencies": {
"glob": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz",
"integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.2",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"react-native-img-cache": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/react-native-img-cache/-/react-native-img-cache-1.5.2.tgz",
"integrity": "sha1-6HG4MJk3t/mSbgmwFuTk5nWKOfo=",
"requires": {
"crypto-js": "^3.1.9-1"
"prop-types": "^15.5.10"
}
},
"react-native-immersive": {

View File

@ -60,8 +60,7 @@
"react-native-background-timer": "2.0.0",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#cad37355f36d17587d84af72b0095e8cc5fd3df9",
"react-native-callstats": "3.52.0",
"react-native-fetch-blob": "github:joltup/react-native-fetch-blob#1f9a1761aea4e37bd672bd0d233f3adf0e113a11",
"react-native-img-cache": "1.5.2",
"react-native-fast-image": "4.0.14",
"react-native-immersive": "1.1.0",
"react-native-keep-awake": "2.0.6",
"react-native-linear-gradient": "2.4.0",

View File

@ -1,10 +1,9 @@
// @flow
import React, { Component } from 'react';
import React, { Component, Fragment } from 'react';
import { Image, View } from 'react-native';
import FastImage from 'react-native-fast-image';
import { CachedImage, ImageCache } from '../../../mobile/image-cache';
import { Platform } from '../../react';
import { ColorPalette } from '../../styles';
import styles from './styles';
@ -46,7 +45,8 @@ type Props = {
*/
type State = {
backgroundColor: string,
source: number | { uri: string }
source: ?{ uri: string },
useDefaultAvatar: boolean
};
/**
@ -68,6 +68,9 @@ export default class Avatar extends Component<Props, State> {
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onAvatarLoaded = this._onAvatarLoaded.bind(this);
// Fork (in Facebook/React speak) the prop uri because Image will
// receive it through a source object. Additionally, other props may be
// forked as well.
@ -94,18 +97,8 @@ export default class Avatar extends Component<Props, State> {
if (prevURI !== nextURI || assignState) {
const nextState = {
backgroundColor: this._getBackgroundColor(nextProps),
/**
* The source of the {@link Image} which is the actual
* representation of this {@link Avatar}. The state
* {@code source} was explicitly introduced in order to reduce
* unnecessary renders.
*
* @type {{
* uri: string
* }}
*/
source: _DEFAULT_SOURCE
source: undefined,
useDefaultAvatar: true
};
if (assignState) {
@ -130,7 +123,14 @@ export default class Avatar extends Component<Props, State> {
// an image retrieval action.
if (nextURI && !nextURI.startsWith('#')) {
const nextSource = { uri: nextURI };
const observer = () => {
if (assignState) {
// eslint-disable-next-line react/no-direct-mutation-state
this.state = {
...this.state,
source: nextSource
};
} else {
this._unmounted || this.setState((prevState, props) => {
if (props.uri === nextURI
&& (!prevState.source
@ -140,22 +140,6 @@ export default class Avatar extends Component<Props, State> {
return {};
});
};
// Wait for the source/URI to load.
if (ImageCache) {
ImageCache.get().on(
nextSource,
observer,
/* immutable */ true);
} else if (assignState) {
// eslint-disable-next-line react/no-direct-mutation-state
this.state = {
...this.state,
source: nextSource
};
} else {
observer();
}
}
}
@ -203,107 +187,115 @@ export default class Avatar extends Component<Props, State> {
return `hsl(${hash % 360}, 100%, 75%)`;
}
/**
* Helper which computes the style for the {@code Image} / {@code FastImage}
* component.
*
* @private
* @returns {Object}
*/
_getImageStyle() {
const { size } = this.props;
return {
...styles.avatar,
borderRadius: size / 2,
height: size,
width: size
};
}
_onAvatarLoaded: () => void;
/**
* Handler called when the remote image was loaded. When this happens we
* show that instead of the default locally generated one.
*
* @private
* @returns {void}
*/
_onAvatarLoaded() {
this._unmounted || this.setState({ useDefaultAvatar: false });
}
/**
* Renders a default, locally generated avatar image.
*
* @private
* @returns {ReactElement}
*/
_renderDefaultAvatar() {
// When using a local image, react-native-fastimage falls back to a
// regular Image, so we need to wrap it in a view to make it round.
// https://github.com/facebook/react-native/issues/3198
const { backgroundColor, useDefaultAvatar } = this.state;
const imageStyle = this._getImageStyle();
const viewStyle = {
...imageStyle,
backgroundColor,
display: useDefaultAvatar ? 'flex' : 'none',
// FIXME @lyubomir: Without the opacity bellow I feel like the
// avatar colors are too strong. Besides, we use opacity for the
// ToolbarButtons. That's where I copied the value from and we
// may want to think about "standardizing" the opacity in the
// app in a way similar to ColorPalette.
opacity: 0.1,
overflow: 'hidden'
};
return (
<View style = { viewStyle }>
<Image
// The Image adds a fade effect without asking, so lets
// explicitly disable it. More info here:
// https://github.com/facebook/react-native/issues/10194
fadeDuration = { 0 }
resizeMode = 'contain'
source = { _DEFAULT_SOURCE }
style = { imageStyle } />
</View>
);
}
/**
* Renders an avatar using a remote image.
*
* @private
* @returns {ReactElement}
*/
_renderAvatar() {
const { source, useDefaultAvatar } = this.state;
const style = {
...this._getImageStyle(),
display: useDefaultAvatar ? 'none' : 'flex'
};
return (
<FastImage
onLoad = { this._onAvatarLoaded }
resizeMode = 'contain'
source = { source }
style = { style } />
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
// Propagate all props of this Avatar but the ones consumed by this
// Avatar to the Image it renders.
const {
/* eslint-disable no-unused-vars */
const { source, useDefaultAvatar } = this.state;
// The following are forked in state:
uri: forked0,
/* eslint-enable no-unused-vars */
size,
...props
} = this.props;
const {
backgroundColor,
source
} = this.state;
// Compute the base style
const borderRadius = size / 2;
const style = {
...styles.avatar,
// XXX Workaround for Android: for radii < 80 the border radius
// doesn't work properly, but applying a radius twice as big seems
// to do the trick.
borderRadius:
Platform.OS === 'android' && borderRadius < 80
? size * 2
: borderRadius,
height: size,
width: size
};
// If we're rendering the _DEFAULT_SOURCE, then we want to do some
// additional fu like having automagical colors generated per
// participant, transparency to make the intermediate state while
// downloading the remote image a little less "in your face", etc.
let styleWithBackgroundColor;
if (source === _DEFAULT_SOURCE && backgroundColor) {
styleWithBackgroundColor = {
...style,
backgroundColor,
// FIXME @lyubomir: Without the opacity bellow I feel like the
// avatar colors are too strong. Besides, we use opacity for the
// ToolbarButtons. That's where I copied the value from and we
// may want to think about "standardizing" the opacity in the
// app in a way similar to ColorPalette.
opacity: 0.1,
overflow: 'hidden'
};
}
// If we're styling with backgroundColor, 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
let imageStyle;
let viewStyle;
if (styleWithBackgroundColor) {
if (Platform.OS === 'android') {
imageStyle = style;
viewStyle = styleWithBackgroundColor;
} else {
imageStyle = styleWithBackgroundColor;
}
} else {
imageStyle = style;
}
let element
= React.createElement(
// XXX CachedImage removed support for images which clearly do
// not need caching.
typeof source === 'number' ? Image : CachedImage,
{
...props,
// The Image adds a fade effect without asking, so lets
// explicitly disable it. More info here:
// https://github.com/facebook/react-native/issues/10194
fadeDuration: 0,
resizeMode: 'contain',
source,
style: imageStyle
});
if (viewStyle) {
element = React.createElement(View, { style: viewStyle }, element);
}
return element;
return (
<Fragment>
{ source && this._renderAvatar() }
{ useDefaultAvatar && this._renderDefaultAvatar() }
</Fragment>
);
}
}

View File

@ -2,6 +2,7 @@
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import FastImage from 'react-native-fast-image';
import { connect } from 'react-redux';
import { translate } from '../../i18n';
@ -11,7 +12,6 @@ import {
shouldRenderVideoTrack,
VideoTrack
} from '../../media';
import { prefetch } from '../../../mobile/image-cache';
import { Container, TintedView } from '../../react';
import { TestHint } from '../../testing/components';
import { getTrackByMediaTypeAndParticipant } from '../../tracks';
@ -303,7 +303,8 @@ function _mapStateToProps(state, ownProps) {
// ParticipantView knows before Avatar that an avatar URL will be used
// so it's advisable to prefetch here.
avatar && prefetch({ uri: avatar });
avatar && !avatar.startsWith('#')
&& FastImage.preload([ { uri: avatar } ]);
}
return {

View File

@ -1,31 +0,0 @@
import { ImageCache } from './';
/**
* Notifies about the successful download of an {@code Image} source. The name
* is inspired by {@code Image}. The downloaded {@code Image} source is not
* available because (1) I do not know how to get it from {@link ImageCache} and
* (2) we do not need it bellow. The function was explicitly introduced to cut
* down on unnecessary {@code ImageCache} {@code observer} instances.
*
* @private
* @returns {void}
*/
function _onLoad() {
// ImageCache requires an observer; otherwise, we do not need it because we
// merely want to initiate the download and do not care what happens with it
// afterwards.
}
/**
* Initiates the retrieval of a specific {@code Image} source (if it has not
* been initiated already). Due to limitations of {@link ImageCache}, the source
* may have at most one {@code uri}. The name is inspired by {@code Image}.
*
* @param {Object} source - The {@code Image} source with preferably exactly
* one {@code uri}.
* @public
* @returns {void}
*/
export function prefetch(source) {
ImageCache && ImageCache.get().on(source, _onLoad, /* immutable */ true);
}

View File

@ -1,4 +0,0 @@
export * from './functions';
export * from './react-native-img-cache';
import './middleware';

View File

@ -1,91 +0,0 @@
/* @flow */
import { APP_WILL_MOUNT } from '../../base/app';
import {
getAvatarURL,
getLocalParticipant,
getParticipantById,
PARTICIPANT_ID_CHANGED,
PARTICIPANT_JOINED,
PARTICIPANT_UPDATED
} from '../../base/participants';
import { MiddlewareRegistry } from '../../base/redux';
import { ImageCache, prefetch } from './';
/**
* The indicator which determines whether avatar URLs are to be prefetched in
* the middleware here. Unless/until the implementation starts observing the
* redux store instead of the respective redux actions, the value should very
* likely be {@code false} because the middleware here is pretty much the last
* to get a chance to figure out that an avatar URL may be used. Besides, it is
* somewhat uninformed to download just about anything that may eventually be
* used or not.
*
* @private
* @type {boolean}
*/
const _PREFETCH_AVATAR_URLS = false;
/**
* Middleware which captures app startup and conference actions in order to
* clear the image cache.
*
* @returns {Function}
*/
MiddlewareRegistry.register(({ getState }) => next => action => {
switch (action.type) {
case APP_WILL_MOUNT:
// XXX CONFERENCE_FAILED/LEFT are no longer used here because they
// are tricky to get right as detectors of the moments in time at which
// CachedImage is not used. Anyway, if ImageCache is to be cleared from
// time to time, SET_LOCATION_URL is a much easier detector of such
// opportune times. Fixes at least one 100%-reproducible case of
// "TypeError: Cannot read property handlers of undefined." Anyway, in
// order to reduce the re-downloading of the same avatars, eventually we
// decided to not clear during the runtime of the app (other that at the
// beginning that is).
ImageCache && ImageCache.get().clear();
break;
case PARTICIPANT_ID_CHANGED:
case PARTICIPANT_JOINED:
case PARTICIPANT_UPDATED: {
if (!_PREFETCH_AVATAR_URLS) {
break;
}
const result = next(action);
// Initiate the downloads of participants' avatars as soon as possible.
// 1. Figure out the participant (instance).
let { participant } = action;
if (participant) {
if (participant.id) {
participant = getParticipantById(getState, participant.id);
} else if (participant.local) {
participant = getLocalParticipant(getState);
} else {
participant = undefined;
}
} else if (action.oldValue && action.newValue) {
participant = getParticipantById(getState, action.newValue);
}
if (participant) {
// 2. Get the participant's avatar URL.
const uri = getAvatarURL(participant);
if (uri) {
// 3. Initiate the download of the participant's avatar.
prefetch({ uri });
}
}
return result;
}
}
return next(action);
});

View File

@ -1 +0,0 @@
export * from './react-native-img-cache.yes';

View File

@ -1 +0,0 @@
export * from './react-native-img-cache.yes';

View File

@ -1,16 +0,0 @@
// XXX The third-party react-native modules react-native-fetch-blob utilizes the
// same HTTP library as react-native i.e. okhttp. Unfortunately, that means that
// the versions of okhttp on which react-native and react-native-fetch-blob
// depend may have incompatible APIs. Such an incompatibility will be made
// apparent at compile time and the developer doing the compilation may choose
// to not compile react-native-fetch-blob's source code.
// XXX The choice between the use of react-native-img-cache could've been done
// at runtime based on whether NativeModules.RNFetchBlob is defined if only
// react-native-fetch-blob would've completely protected itself. At the time of
// this writing its source code appears to be attempting to protect itself from
// missing native binaries but that protection is incomplete and there's a
// TypeError.
import { Image } from 'react-native';
export { Image as CachedImage, undefined as ImageCache };

View File

@ -1 +0,0 @@
export { CachedImage, ImageCache } from 'react-native-img-cache';