From b05b797b069eb6696ff8f3f9cce94dc49f753692 Mon Sep 17 00:00:00 2001 From: Bhargav Sangani Date: Sat, 7 Nov 2020 02:56:45 +0530 Subject: [PATCH] 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 --- android/app/src/main/AndroidManifest.xml | 1 + .../file_attachment_list.test.js.snap | 18 +++++ .../file_attachment_list/file_attachment.js | 3 + .../file_attachment_image.js | 8 +- .../file_attachment_list.js | 23 +++++- app/components/post_list/post_list.js | 10 ++- .../progressive_image.test.js.snap | 2 + .../progressive_image/progressive_image.js | 77 ++++++++++++++++++- .../progressive_image.test.js | 43 ++++++++++- 9 files changed, 176 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6b31fd313..7e71f36f4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ android:networkSecurityConfig="@xml/network_security_config" android:resizeableActivity="true" android:requestLegacyExternalStorage="true" + android:usesCleartextTraffic="true" > {this.renderMoreImagesOverlay(nonVisibleImagesCount, theme)} diff --git a/app/components/file_attachment_list/file_attachment_image.js b/app/components/file_attachment_list/file_attachment_image.js index 2258abfb8..482b9fb19 100644 --- a/app/components/file_attachment_list/file_attachment_image.js +++ b/app/components/file_attachment_list/file_attachment_image.js @@ -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; }; diff --git a/app/components/file_attachment_list/file_attachment_list.js b/app/components/file_attachment_list/file_attachment_list.js index 6ac9f90dc..d45471529 100644 --- a/app/components/file_attachment_list/file_attachment_list.js +++ b/app/components/file_attachment_list/file_attachment_list.js @@ -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} /> ); diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index eec8c8561..d9fb35b6b 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -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); } diff --git a/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap b/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap index 23f0b5da9..90687592a 100644 --- a/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap +++ b/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap @@ -102,6 +102,7 @@ exports[`ProgressiveImage should match snapshot for Image 1`] = ` }, ] } + testID="progressive_image.thumbnail" /> `; diff --git a/app/components/progressive_image/progressive_image.js b/app/components/progressive_image/progressive_image.js index c27c72ef2..0cab5fbb0 100644 --- a/app/components/progressive_image/progressive_image.js +++ b/app/components/progressive_image/progressive_image.js @@ -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 ( - + let thumbnail; + let image; + if (thumbnailUri) { + const ImageElement = thumbnailUri.startsWith('data:') ? Image : ImageComponent; + + thumbnail = ( + + {this.props.children} + + ); + + if (showHighResImage) { + image = ( + + {this.props.children} + + ); + } + } else { + thumbnail = ( + ); + + image = ( {this.props.children} + ); + } + + return ( + + {thumbnail} + {image} ); } diff --git a/app/components/progressive_image/progressive_image.test.js b/app/components/progressive_image/progressive_image.test.js index 5587485f2..9dae1408d 100644 --- a/app/components/progressive_image/progressive_image.test.js +++ b/app/components/progressive_image/progressive_image.test.js @@ -57,4 +57,45 @@ describe('ProgressiveImage', () => { const wrapper = shallow(); expect(wrapper.getElement()).toMatchSnapshot(); }); -}); \ No newline at end of file +}); + +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(); + 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(); + const selectHighResImage = () => wrapper.find({testID: 'progressive_image.highResImage'}); + expect(selectHighResImage().length).toEqual(0); + wrapper.setProps({ + inViewPort: true, + }); + expect(selectHighResImage().length).toEqual(1); + }); +});