MM-27354 Progressive image loading (#4945)

* init

* Pick one from mini preview and thumnail URL in file attachment image

* Blur thumbnail mini preview & update testID's

* Set blurRadious based on Platform OS

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Bhargav Sangani 2020-11-07 02:56:45 +05:30 committed by GitHub
parent c70381baf2
commit b05b797b06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 176 additions and 9 deletions

View file

@ -21,6 +21,7 @@
android:networkSecurityConfig="@xml/network_security_config"
android:resizeableActivity="true"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true"
>
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
<meta-data android:name="android.content.APP_RESTRICTIONS"

View file

@ -52,6 +52,7 @@ exports[`FileAttachmentList should match snapshot with a single image file 1`] =
}
}
id="fileId"
inViewPort={false}
index={0}
isSingleImage={true}
onCaptureRef={[Function]}
@ -144,6 +145,7 @@ exports[`FileAttachmentList should match snapshot with combination of image and
}
}
id="fileId"
inViewPort={false}
index={0}
isSingleImage={false}
onCaptureRef={[Function]}
@ -215,6 +217,7 @@ exports[`FileAttachmentList should match snapshot with combination of image and
}
}
id="otherFileId"
inViewPort={false}
index={1}
isSingleImage={false}
onCaptureRef={[Function]}
@ -276,6 +279,7 @@ exports[`FileAttachmentList should match snapshot with combination of image and
}
}
id="fileId"
inViewPort={false}
index={0}
isSingleImage={false}
onCaptureRef={[Function]}
@ -367,6 +371,7 @@ exports[`FileAttachmentList should match snapshot with four image files 1`] = `
}
}
id="fileId"
inViewPort={false}
index={0}
isSingleImage={false}
onCaptureRef={[Function]}
@ -438,6 +443,7 @@ exports[`FileAttachmentList should match snapshot with four image files 1`] = `
}
}
id="otherFileId"
inViewPort={false}
index={1}
isSingleImage={false}
onCaptureRef={[Function]}
@ -509,6 +515,7 @@ exports[`FileAttachmentList should match snapshot with four image files 1`] = `
}
}
id="thirdFileId"
inViewPort={false}
index={2}
isSingleImage={false}
onCaptureRef={[Function]}
@ -580,6 +587,7 @@ exports[`FileAttachmentList should match snapshot with four image files 1`] = `
}
}
id="fourthFileId"
inViewPort={false}
index={3}
isSingleImage={false}
onCaptureRef={[Function]}
@ -672,6 +680,7 @@ exports[`FileAttachmentList should match snapshot with more than four image file
}
}
id="fileId"
inViewPort={false}
index={0}
isSingleImage={false}
onCaptureRef={[Function]}
@ -743,6 +752,7 @@ exports[`FileAttachmentList should match snapshot with more than four image file
}
}
id="otherFileId"
inViewPort={false}
index={1}
isSingleImage={false}
onCaptureRef={[Function]}
@ -814,6 +824,7 @@ exports[`FileAttachmentList should match snapshot with more than four image file
}
}
id="thirdFileId"
inViewPort={false}
index={2}
isSingleImage={false}
onCaptureRef={[Function]}
@ -885,6 +896,7 @@ exports[`FileAttachmentList should match snapshot with more than four image file
}
}
id="fourthFileId"
inViewPort={false}
index={3}
isSingleImage={false}
nonVisibleImagesCount={2}
@ -958,6 +970,7 @@ exports[`FileAttachmentList should match snapshot with non-image attachment 1`]
}
}
id="fileId"
inViewPort={false}
index={0}
isSingleImage={false}
onCaptureRef={[Function]}
@ -1049,6 +1062,7 @@ exports[`FileAttachmentList should match snapshot with three image files 1`] = `
}
}
id="fileId"
inViewPort={false}
index={0}
isSingleImage={false}
onCaptureRef={[Function]}
@ -1120,6 +1134,7 @@ exports[`FileAttachmentList should match snapshot with three image files 1`] = `
}
}
id="otherFileId"
inViewPort={false}
index={1}
isSingleImage={false}
onCaptureRef={[Function]}
@ -1191,6 +1206,7 @@ exports[`FileAttachmentList should match snapshot with three image files 1`] = `
}
}
id="thirdFileId"
inViewPort={false}
index={2}
isSingleImage={false}
onCaptureRef={[Function]}
@ -1283,6 +1299,7 @@ exports[`FileAttachmentList should match snapshot with two image files 1`] = `
}
}
id="fileId"
inViewPort={false}
index={0}
isSingleImage={false}
onCaptureRef={[Function]}
@ -1354,6 +1371,7 @@ exports[`FileAttachmentList should match snapshot with two image files 1`] = `
}
}
id="otherFileId"
inViewPort={false}
index={1}
isSingleImage={false}
onCaptureRef={[Function]}

View file

@ -35,6 +35,7 @@ export default class FileAttachment extends PureComponent {
wrapperWidth: PropTypes.number,
isSingleImage: PropTypes.bool,
nonVisibleImagesCount: PropTypes.number,
inViewPort: PropTypes.bool,
};
static defaultProps = {
@ -143,6 +144,7 @@ export default class FileAttachment extends PureComponent {
onLongPress,
isSingleImage,
nonVisibleImagesCount,
inViewPort,
} = this.props;
const {data} = file;
const style = getStyleSheet(theme);
@ -165,6 +167,7 @@ export default class FileAttachment extends PureComponent {
theme={theme}
isSingleImage={isSingleImage}
imageDimensions={imageDimensions}
inViewPort={inViewPort}
/>
{this.renderMoreImagesOverlay(nonVisibleImagesCount, theme)}
</TouchableWithFeedback>

View file

@ -40,6 +40,7 @@ export default class FileAttachmentImage extends PureComponent {
isSingleImage: PropTypes.bool,
imageDimensions: PropTypes.object,
backgroundColor: PropTypes.string,
inViewPort: PropTypes.bool,
};
static defaultProps = {
@ -76,8 +77,13 @@ export default class FileAttachmentImage extends PureComponent {
if (file.localPath) {
imageProps.defaultSource = {uri: file.localPath};
} else if (file.id) {
imageProps.thumbnailUri = Client4.getFileThumbnailUrl(file.id);
if (file.mini_preview && file.mime_type) {
imageProps.thumbnailUri = `data:${file.mime_type};base64,${file.mini_preview}`;
} else {
imageProps.thumbnailUri = Client4.getFileThumbnailUrl(file.id);
}
imageProps.imageUri = Client4.getFilePreviewUrl(file.id);
imageProps.inViewPort = this.props.inViewPort;
}
return imageProps;
};

View file

@ -3,7 +3,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {StyleSheet, View} from 'react-native';
import {StyleSheet, View, DeviceEventEmitter} from 'react-native';
import ImageViewPort from '@components/image_viewport';
import {Client4} from '@mm-redux/client';
@ -33,7 +33,9 @@ export default class FileAttachmentList extends ImageViewPort {
constructor(props) {
super(props);
this.state = {
inViewPort: false,
};
this.items = [];
this.filesForGallery = this.getFilesForGallery(props);
@ -42,6 +44,16 @@ export default class FileAttachmentList extends ImageViewPort {
});
}
componentDidMount() {
this.onScrollEnd = DeviceEventEmitter.addListener('scrolled', (viewableItems) => {
if (this.props.postId in viewableItems) {
this.setState({
inViewPort: true,
});
}
});
}
componentDidUpdate(prevProps) {
if (prevProps.files.length !== this.props.files.length) {
this.filesForGallery = this.getFilesForGallery(this.props);
@ -51,6 +63,12 @@ export default class FileAttachmentList extends ImageViewPort {
}
}
componentWillUnmount() {
if (this.onScrollEnd && this.onScrollEnd.remove) {
this.onScrollEnd.remove();
}
}
attachmentIndex = (fileId) => {
return this.filesForGallery.findIndex((file) => file.id === fileId) || 0;
};
@ -165,6 +183,7 @@ export default class FileAttachmentList extends ImageViewPort {
isSingleImage={isSingleImage}
nonVisibleImagesCount={nonVisibleImagesCount}
wrapperWidth={getViewPortWidth(isReplyPost, this.hasPermanentSidebar())}
inViewPort={this.state.inViewPort}
/>
</View>
);

View file

@ -3,7 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Alert, FlatList, Platform, RefreshControl, StyleSheet} from 'react-native';
import {Alert, DeviceEventEmitter, FlatList, Platform, RefreshControl, StyleSheet} from 'react-native';
import {intlShape} from 'react-intl';
import {Posts} from '@mm-redux/constants';
@ -454,7 +454,13 @@ export default class PostList extends PureComponent {
if (!this.onViewableItemsChangedListener || !viewableItems.length || this.props.deepLinkURL) {
return;
}
const viewableItemsMap = viewableItems.reduce((acc, {item, isViewable}) => {
if (isViewable) {
acc[item] = true;
}
return acc;
}, {});
DeviceEventEmitter.emit('scrolled', viewableItemsMap);
this.onViewableItemsChangedListener(viewableItems);
}

View file

@ -102,6 +102,7 @@ exports[`ProgressiveImage should match snapshot for Image 1`] = `
},
]
}
testID="progressive_image.thumbnail"
/>
<ForwardRef(AnimatedComponentWrapper)
onError={[MockFunction]}
@ -128,6 +129,7 @@ exports[`ProgressiveImage should match snapshot for Image 1`] = `
},
]
}
testID="progressive_image.highResImage"
/>
</ForwardRef(AnimatedComponentWrapper)>
`;

View file

@ -3,7 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Animated, ImageBackground, View, StyleSheet} from 'react-native';
import {Animated, ImageBackground, Image, Platform, View, StyleSheet} from 'react-native';
import FastImage from 'react-native-fast-image';
import thumb from '@assets/images/thumb.png';
@ -20,11 +20,13 @@ export default class ProgressiveImage extends PureComponent {
defaultSource: PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.number]), // this should be provided by the component
imageUri: PropTypes.string,
imageStyle: CustomPropTypes.Style,
inViewPort: PropTypes.bool,
onError: PropTypes.func,
resizeMethod: PropTypes.string,
resizeMode: PropTypes.string,
style: CustomPropTypes.Style,
theme: PropTypes.object.isRequired,
thumbnailUri: PropTypes.string,
tintDefaultSource: PropTypes.bool,
};
@ -39,9 +41,22 @@ export default class ProgressiveImage extends PureComponent {
this.state = {
intensity: new Animated.Value(0),
showHighResImage: false,
};
}
componentDidUpdate(prevProps) {
if (prevProps.inViewPort !== this.props.inViewPort && this.props.inViewPort) {
this.startLoadingOriginalImage();
}
}
startLoadingOriginalImage = () => {
this.setState({
showHighResImage: true,
});
}
onLoadImageEnd = () => {
Animated.timing(this.state.intensity, {
duration: 300,
@ -61,9 +76,12 @@ export default class ProgressiveImage extends PureComponent {
resizeMethod,
style,
theme,
thumbnailUri,
tintDefaultSource,
} = this.props;
const {showHighResImage} = this.state;
let DefaultComponent;
let ImageComponent;
if (isBackgroundImage) {
@ -119,15 +137,60 @@ export default class ProgressiveImage extends PureComponent {
backgroundColor: changeOpacity(theme.centerChannelColor, defaultOpacity),
};
return (
<Animated.View style={[styles.defaultImageContainer, style, containerStyle]}>
let thumbnail;
let image;
if (thumbnailUri) {
const ImageElement = thumbnailUri.startsWith('data:') ? Image : ImageComponent;
thumbnail = (
<ImageElement
resizeMode={resizeMode}
resizeMethod={resizeMethod}
onError={onError}
source={{uri: thumbnailUri}}
style={[
StyleSheet.absoluteFill,
imageStyle,
]}
blurRadius={Platform.OS === 'android' ? 0.4 : 1}
testID='progressive_image.miniPreview'
>
{this.props.children}
</ImageElement>
);
if (showHighResImage) {
image = (
<ImageComponent
resizeMode={resizeMode}
resizeMethod={resizeMethod}
onError={onError}
source={{uri: imageUri}}
style={[
StyleSheet.absoluteFill,
imageStyle,
{opacity}]
}
testID='progressive_image.highResImage'
onLoadEnd={this.onLoadImageEnd}
>
{this.props.children}
</ImageComponent>
);
}
} else {
thumbnail = (
<DefaultComponent
resizeMode={resizeMode}
resizeMethod={resizeMethod}
onError={onError}
source={thumb}
style={[imageStyle, {tintColor: theme.centerChannelColor, opacity: defaultOpacity}]}
testID='progressive_image.thumbnail'
/>
);
image = (
<ImageComponent
resizeMode={resizeMode}
resizeMethod={resizeMethod}
@ -135,9 +198,17 @@ export default class ProgressiveImage extends PureComponent {
source={{uri: imageUri}}
style={[StyleSheet.absoluteFill, imageStyle, {opacity}]}
onLoadEnd={this.onLoadImageEnd}
testID='progressive_image.highResImage'
>
{this.props.children}
</ImageComponent>
);
}
return (
<Animated.View style={[styles.defaultImageContainer, style, containerStyle]}>
{thumbnail}
{image}
</Animated.View>
);
}

View file

@ -57,4 +57,45 @@ describe('ProgressiveImage', () => {
const wrapper = shallow(<ProgressiveImage {...baseProps}/>);
expect(wrapper.getElement()).toMatchSnapshot();
});
});
});
describe('MiniPreview', () => {
test('should show mini preview when supported & image not in viewport', () => {
const baseProps = {
isBackgroundImage: false,
imageUri: 'https://images.com/image.png',
onError: jest.fn(),
resizeMethod: 'auto',
resizeMode: 'contain',
theme: Preferences.THEMES.default,
tintDefaultSource: false,
defaultSource: null,
inViewPort: false,
thumbnailUri: 'somebase64data',
};
const wrapper = shallow(<ProgressiveImage {...baseProps}/>);
expect(wrapper.find({testID: 'progressive_image.miniPreview'}).length).toEqual(1);
});
test('should load and show high res image with animation when component comes into viewport', () => {
const baseProps = {
isBackgroundImage: false,
imageUri: 'https://images.com/image.png',
onError: jest.fn(),
resizeMethod: 'auto',
resizeMode: 'contain',
theme: Preferences.THEMES.default,
tintDefaultSource: false,
defaultSource: null,
inViewPort: false,
thumbnailUri: 'somebase64data',
};
const wrapper = shallow(<ProgressiveImage {...baseProps}/>);
const selectHighResImage = () => wrapper.find({testID: 'progressive_image.highResImage'});
expect(selectHighResImage().length).toEqual(0);
wrapper.setProps({
inViewPort: true,
});
expect(selectHighResImage().length).toEqual(1);
});
});