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:
parent
c70381baf2
commit
b05b797b06
9 changed files with 176 additions and 9 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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]}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue