From 0504a962a12292d3cdf481bf0485aee5cd5b7449 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Thu, 27 Feb 2020 16:29:51 -0300 Subject: [PATCH] MM-22379 & MM-22598 Boost perf by using FastImage cache in favor of ImageCacheManager (#3971) * MM-22379 & MM-22598 Boost perf by using FastImage cache in favor of ImageCacheManager * Code review --- app/components/emoji/emoji.js | 72 ++++------------- .../file_attachment_document.js | 16 +++- .../file_attachment_image.js | 10 ++- .../file_attachment_list.js | 6 +- .../markdown/markdown_image/markdown_image.js | 7 +- .../attachment_image/index.js | 5 +- .../attachment_image/index.test.js | 37 --------- .../post_attachment_opengraph.test.js.snap | 4 +- .../post_attachment_opengraph.js | 15 ++-- .../post_body_additional_content.js | 5 +- .../profile_picture/profile_picture.js | 8 +- .../progressive_image.test.js.snap | 47 ++++++++++- .../progressive_image/progressive_image.js | 65 +++++++++------- .../progressive_image.test.js | 8 +- app/components/team_icon/team_icon.js | 33 ++++---- app/init/global_event_handler.js | 2 +- app/utils/file.js | 78 +++++++++---------- package-lock.json | 41 +++++++--- 18 files changed, 230 insertions(+), 229 deletions(-) diff --git a/app/components/emoji/emoji.js b/app/components/emoji/emoji.js index 9ef918112..a21c39eac 100644 --- a/app/components/emoji/emoji.js +++ b/app/components/emoji/emoji.js @@ -4,14 +4,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - Image, Platform, StyleSheet, Text, } from 'react-native'; +import FastImage from 'react-native-fast-image'; import CustomPropTypes from 'app/constants/custom_prop_types'; -import ImageCacheManager from 'app/utils/image_cache_manager'; export default class Emoji extends React.PureComponent { static propTypes = { @@ -49,56 +48,15 @@ export default class Emoji extends React.PureComponent { isCustomEmoji: false, }; - constructor(props) { - super(props); - - this.state = { - imageUrl: null, - }; - } - - componentDidMount() { - const {displayTextOnly, emojiName, imageUrl} = this.props; - this.mounted = true; - if (!displayTextOnly && imageUrl) { - ImageCacheManager.cache(`emoji-${emojiName}`, imageUrl, this.setImageUrl); - } - } - - componentWillReceiveProps(nextProps) { - const {displayTextOnly, emojiName, imageUrl} = nextProps; - if (emojiName !== this.props.emojiName && this.mounted) { - this.setState({ - imageUrl: null, - }); - } - - if (!displayTextOnly && imageUrl && - imageUrl !== this.props.imageUrl) { - ImageCacheManager.cache(`emoji-${emojiName}`, imageUrl, this.setImageUrl); - } - } - - componentWillUnmount() { - this.mounted = false; - } - - setImageUrl = (imageUrl) => { - if (this.mounted) { - this.setState({ - imageUrl, - }); - } - }; - render() { const { - literal, - textStyle, - displayTextOnly, customEmojiStyle, + displayTextOnly, + imageUrl, + literal, + unicode, + textStyle, } = this.props; - const {imageUrl} = this.state; let size = this.props.size; let fontSize = size; @@ -117,36 +75,32 @@ export default class Emoji extends React.PureComponent { // Android can't change the size of an image after its first render, so // force a new image to be rendered when the size changes - const key = Platform.OS === 'android' ? (height + '-' + width) : null; + const key = Platform.OS === 'android' ? (`${imageUrl}-${height}-${width}`) : null; - if (this.props.unicode && !this.props.imageUrl) { - const codeArray = this.props.unicode.split('-'); + if (unicode && !imageUrl) { + const codeArray = unicode.split('-'); const code = codeArray.reduce((acc, c) => { return acc + String.fromCodePoint(parseInt(c, 16)); }, ''); return ( - + {code} ); } if (!imageUrl) { - return ( - - ); + return null; } return ( - ); } diff --git a/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js b/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js index 43e4de5dd..67bb75aa2 100644 --- a/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js +++ b/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js @@ -146,7 +146,7 @@ export default class FileAttachmentDocument extends PureComponent { this.setState({downloading: true}); this.downloadTask = RNFetchBlob.config(options).fetch('GET', getFileUrl(data.id)); this.downloadTask.progress((received, total) => { - const progress = (received / total) * 100; + const progress = Math.round((received / total) * 100); if (this.mounted) { this.setState({progress}); } @@ -216,7 +216,9 @@ export default class FileAttachmentDocument extends PureComponent { }; onDonePreviewingFile = () => { - this.setState({preview: false}); + if (this.mounted) { + this.setState({preview: false}); + } this.setStatusBarColor(); }; @@ -262,7 +264,9 @@ export default class FileAttachmentDocument extends PureComponent { RNFetchBlob.fs.unlink(path); } - this.setState({downloading: false, progress: 0}); + if (this.mounted) { + this.setState({downloading: false, progress: 0}); + } }); // Android does not trigger the event for DoneButtonEvent @@ -281,7 +285,11 @@ export default class FileAttachmentDocument extends PureComponent { didCancel: true, }, () => { // need to wait a bit for the progress circle UI to update to the give progress - setTimeout(() => this.setState({downloading: false}), 2000); + setTimeout(() => { + if (this.mounted) { + this.setState({downloading: false}); + } + }, 2000); }); } }; diff --git a/app/components/file_attachment_list/file_attachment_image.js b/app/components/file_attachment_list/file_attachment_image.js index 89c546459..0b2b6c488 100644 --- a/app/components/file_attachment_list/file_attachment_image.js +++ b/app/components/file_attachment_list/file_attachment_image.js @@ -8,13 +8,12 @@ import { View, StyleSheet, } from 'react-native'; +import FastImage from 'react-native-fast-image'; import {Client4} from 'mattermost-redux/client'; import ProgressiveImage from 'app/components/progressive_image'; import {isGif} from 'app/utils/file'; -import {emptyFunction} from 'app/utils/general'; -import ImageCacheManager from 'app/utils/image_cache_manager'; import {changeOpacity} from 'app/utils/theme'; import thumb from 'assets/images/thumb.png'; @@ -58,11 +57,14 @@ export default class FileAttachmentImage extends PureComponent { const {file} = props; if (file && file.id) { - ImageCacheManager.cache(file.name, Client4.getFileThumbnailUrl(file.id), emptyFunction); + const headers = {Authorization: `Bearer ${Client4.getToken()}`}; + const preloadImages = [{uri: Client4.getFileThumbnailUrl(file.id), headers}]; if (isGif(file)) { - ImageCacheManager.cache(file.name, Client4.getFileUrl(file.id), emptyFunction); + preloadImages.push({uri: Client4.getFileUrl(file.id), headers}); } + + FastImage.preload(preloadImages); } this.state = { diff --git a/app/components/file_attachment_list/file_attachment_list.js b/app/components/file_attachment_list/file_attachment_list.js index 11b38b939..e6bb105fd 100644 --- a/app/components/file_attachment_list/file_attachment_list.js +++ b/app/components/file_attachment_list/file_attachment_list.js @@ -13,10 +13,8 @@ import {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout'; import {DeviceTypes} from 'app/constants'; import mattermostManaged from 'app/mattermost_managed'; import {isDocument, isGif, isVideo} from 'app/utils/file'; -import ImageCacheManager from 'app/utils/image_cache_manager'; import {previewImageAtIndex} from 'app/utils/images'; import {preventDoubleTap} from 'app/utils/tap'; -import {emptyFunction} from 'app/utils/general'; import FileAttachment from './file_attachment'; @@ -123,9 +121,9 @@ export default class FileAttachmentList extends PureComponent { if (file.localPath) { uri = file.localPath; } else if (isGif(file)) { - uri = await ImageCacheManager.cache(file.name, Client4.getFileUrl(file.id), emptyFunction); // eslint-disable-line no-await-in-loop + uri = Client4.getFileUrl(file.id); } else { - uri = await ImageCacheManager.cache(file.name, Client4.getFilePreviewUrl(file.id), emptyFunction); // eslint-disable-line no-await-in-loop + uri = Client4.getFilePreviewUrl(file.id); } results.push({ diff --git a/app/components/markdown/markdown_image/markdown_image.js b/app/components/markdown/markdown_image/markdown_image.js index 61b235c61..7112a0dfa 100644 --- a/app/components/markdown/markdown_image/markdown_image.js +++ b/app/components/markdown/markdown_image/markdown_image.js @@ -21,7 +21,6 @@ import CustomPropTypes from 'app/constants/custom_prop_types'; import EphemeralStore from 'app/store/ephemeral_store'; import mattermostManaged from 'app/mattermost_managed'; import BottomSheet from 'app/utils/bottom_sheet'; -import ImageCacheManager from 'app/utils/image_cache_manager'; import {previewImageAtIndex, calculateDimensions, isGifTooLarge} from 'app/utils/images'; import {normalizeProtocol} from 'app/utils/url'; @@ -32,7 +31,7 @@ const ANDROID_MAX_WIDTH = 4096; const VIEWPORT_IMAGE_OFFSET = 66; const VIEWPORT_IMAGE_REPLY_OFFSET = 13; -export default class MarkdownImage extends React.Component { +export default class MarkdownImage extends React.PureComponent { static propTypes = { children: PropTypes.node, deviceHeight: PropTypes.number.isRequired, @@ -65,7 +64,7 @@ export default class MarkdownImage extends React.Component { componentDidMount() { this.mounted = true; - ImageCacheManager.cache(null, this.getSource(), this.setImageUrl); + this.setImageUrl(this.getSource()); } static getDerivedStateFromProps(props) { @@ -84,7 +83,7 @@ export default class MarkdownImage extends React.Component { componentDidUpdate(prevProps) { if (this.props.source !== prevProps.source) { // getSource also depends on serverURL, but that shouldn't change while this is mounted - ImageCacheManager.cache(null, this.getSource(), this.setImageUrl); + this.setImageUrl(this.getSource()); } } diff --git a/app/components/message_attachments/attachment_image/index.js b/app/components/message_attachments/attachment_image/index.js index 9f2a5f92e..b696adcdf 100644 --- a/app/components/message_attachments/attachment_image/index.js +++ b/app/components/message_attachments/attachment_image/index.js @@ -8,7 +8,6 @@ import {Image, View} from 'react-native'; import ProgressiveImage from 'app/components/progressive_image'; import TouchableWithFeedback from 'app/components/touchable_with_feedback'; import {isGifTooLarge, previewImageAtIndex, calculateDimensions} from 'app/utils/images'; -import ImageCacheManager from 'app/utils/image_cache_manager'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; const VIEWPORT_IMAGE_OFFSET = 100; @@ -42,13 +41,13 @@ export default class AttachmentImage extends PureComponent { } if (imageUrl) { - ImageCacheManager.cache(null, imageUrl, this.setImageUrl); + this.setImageUrl(imageUrl); } } componentDidUpdate(prevProps) { if (this.props.imageUrl && (prevProps.imageUrl !== this.props.imageUrl)) { - ImageCacheManager.cache(null, this.props.imageUrl, this.setImageUrl); + this.setImageUrl(this.props.imageUrl); } } diff --git a/app/components/message_attachments/attachment_image/index.test.js b/app/components/message_attachments/attachment_image/index.test.js index da1557e7f..27c16f9f9 100644 --- a/app/components/message_attachments/attachment_image/index.test.js +++ b/app/components/message_attachments/attachment_image/index.test.js @@ -1,12 +1,10 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import ImageCacheManager from 'app/utils/image_cache_manager'; import {Image} from 'react-native'; import {shallow} from 'enzyme'; import React from 'react'; -const originalCacheFn = ImageCacheManager.cache; const originalGetSizeFn = Image.getSize; import Preferences from 'mattermost-redux/constants/preferences'; @@ -24,57 +22,35 @@ describe('AttachmentImage', () => { afterEach(() => { Image.getSize = originalGetSizeFn; - ImageCacheManager.cache = originalCacheFn; }); test('it matches snapshot', () => { - const cacheFn = jest.fn((_, url, callback) => { - callback(url); - }); - ImageCacheManager.cache = cacheFn; - const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); test('it sets state based on props', () => { - const cacheFn = jest.fn((_, url, callback) => { - callback(url); - }); - ImageCacheManager.cache = cacheFn; - const wrapper = shallow(); const state = wrapper.state(); expect(state.hasImage).toBe(true); expect(state.imageUri).toBe('https://images.com/image.png'); expect(state.originalWidth).toBe(32); - expect(cacheFn).toHaveBeenCalled(); }); test('it does not render image if no imageUrl is provided', () => { - const cacheFn = jest.fn((_, url, callback) => { - callback(url); - }); - ImageCacheManager.cache = cacheFn; - const props = {...baseProps, imageUrl: null, imageMetadata: null}; const wrapper = shallow(); const state = wrapper.state(); expect(state.hasImage).toBe(false); expect(state.imageUri).toBe(null); - expect(cacheFn).not.toHaveBeenCalled(); }); test('it calls Image.getSize if metadata is not present', () => { - const cacheFn = jest.fn((_, url, callback) => { - callback(url); - }); const getSizeFn = jest.fn((_, callback) => { callback(64, 64); }); - ImageCacheManager.cache = cacheFn; Image.getSize = getSizeFn; const props = {...baseProps, imageMetadata: null}; @@ -84,16 +60,10 @@ describe('AttachmentImage', () => { expect(state.hasImage).toBe(true); expect(state.imageUri).toBe('https://images.com/image.png'); expect(state.originalWidth).toBe(64); - expect(cacheFn).toHaveBeenCalled(); expect(getSizeFn).toHaveBeenCalled(); }); test('it updates image when imageUrl prop changes', () => { - const cacheFn = jest.fn((_, url, callback) => { - callback(url); - }); - ImageCacheManager.cache = cacheFn; - const wrapper = shallow(); wrapper.setProps({ @@ -108,15 +78,9 @@ describe('AttachmentImage', () => { expect(state.hasImage).toBe(true); expect(state.imageUri).toBe('https://someothersite.com/picture.png'); expect(state.originalWidth).toBe(96); - expect(cacheFn).toHaveBeenCalledTimes(2); }); test('it does not update image when an unrelated prop changes', () => { - const cacheFn = jest.fn((_, url, callback) => { - callback(url); - }); - ImageCacheManager.cache = cacheFn; - const wrapper = shallow(); wrapper.setProps({ @@ -127,6 +91,5 @@ describe('AttachmentImage', () => { expect(state.hasImage).toBe(true); expect(state.imageUri).toBe('https://images.com/image.png'); expect(state.originalWidth).toBe(32); - expect(cacheFn).toHaveBeenCalledTimes(1); }); }); diff --git a/app/components/post_attachment_opengraph/__snapshots__/post_attachment_opengraph.test.js.snap b/app/components/post_attachment_opengraph/__snapshots__/post_attachment_opengraph.test.js.snap index 4da6f6ecb..b53f2a9e5 100644 --- a/app/components/post_attachment_opengraph/__snapshots__/post_attachment_opengraph.test.js.snap +++ b/app/components/post_attachment_opengraph/__snapshots__/post_attachment_opengraph.test.js.snap @@ -259,7 +259,7 @@ exports[`PostAttachmentOpenGraph should match state and snapshot, on renderImage "marginTop": 5, }, Object { - "height": 112.56666666666666, + "height": 69.83261802575107, "width": 307, }, ] @@ -277,7 +277,7 @@ exports[`PostAttachmentOpenGraph should match state and snapshot, on renderImage "borderRadius": 3, }, Object { - "height": 112.56666666666666, + "height": 69.83261802575107, "width": 307, }, ] diff --git a/app/components/post_attachment_opengraph/post_attachment_opengraph.js b/app/components/post_attachment_opengraph/post_attachment_opengraph.js index 07337c8f7..1710015da 100644 --- a/app/components/post_attachment_opengraph/post_attachment_opengraph.js +++ b/app/components/post_attachment_opengraph/post_attachment_opengraph.js @@ -14,7 +14,6 @@ import {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout'; import TouchableWithFeedback from 'app/components/touchable_with_feedback'; import {DeviceTypes} from 'app/constants'; -import ImageCacheManager from 'app/utils/image_cache_manager'; import {previewImageAtIndex, calculateDimensions} from 'app/utils/images'; import {getNearestPoint} from 'app/utils/opengraph'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; @@ -47,6 +46,10 @@ export default class PostAttachmentOpenGraph extends PureComponent { this.mounted = true; this.fetchData(this.props.link, this.props.openGraphData); + + if (this.state.openGraphImageUrl) { + this.getImageSize(this.state.openGraphImageUrl); + } } componentWillReceiveProps(nextProps) { @@ -56,7 +59,9 @@ export default class PostAttachmentOpenGraph extends PureComponent { } if (this.props.openGraphData !== nextProps.openGraphData) { - this.setState(this.getBestImageUrl(nextProps.openGraphData)); + this.setState(this.getBestImageUrl(nextProps.openGraphData), () => { + this.getImageSize(this.state.openGraphImageUrl); + }); } } @@ -110,10 +115,6 @@ export default class PostAttachmentOpenGraph extends PureComponent { dimensions = calculateDimensions(ogImage.height, ogImage.width, this.getViewPostWidth()); } - if (imageUrl) { - ImageCacheManager.cache(this.getFilename(imageUrl), imageUrl, this.getImageSize); - } - return { hasImage: true, ...dimensions, @@ -143,7 +144,7 @@ export default class PostAttachmentOpenGraph extends PureComponent { } if (!ogImage) { - ogImage = openGraphData.images.find((i) => i.url === openGraphImageUrl || i.secure_url === openGraphImageUrl); + ogImage = openGraphData?.images?.find((i) => i.url === openGraphImageUrl || i.secure_url === openGraphImageUrl); } // Fallback when the ogImage does not have dimensions but there is a metaImage defined diff --git a/app/components/post_body_additional_content/post_body_additional_content.js b/app/components/post_body_additional_content/post_body_additional_content.js index 714ce0b61..9cf156791 100644 --- a/app/components/post_body_additional_content/post_body_additional_content.js +++ b/app/components/post_body_additional_content/post_body_additional_content.js @@ -23,7 +23,6 @@ import TouchableWithFeedback from 'app/components/touchable_with_feedback'; import {DeviceTypes} from 'app/constants'; import CustomPropTypes from 'app/constants/custom_prop_types'; -import ImageCacheManager from 'app/utils/image_cache_manager'; import {previewImageAtIndex, calculateDimensions} from 'app/utils/images'; import {getYouTubeVideoId, isImageLink, isYoutubeLink} from 'app/utils/url'; @@ -126,7 +125,6 @@ export default class PostBodyAdditionalContent extends PureComponent { } else if (isYoutubeLink(link)) { const videoId = getYouTubeVideoId(link); imageUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`; - ImageCacheManager.cache(null, `https://i.ytimg.com/vi/${videoId}/default.jpg`, () => true); } else { const {data} = await this.props.actions.getRedirectLocation(link); @@ -141,7 +139,6 @@ export default class PostBodyAdditionalContent extends PureComponent { } else if (isYoutubeLink(shortenedLink)) { const videoId = getYouTubeVideoId(shortenedLink); imageUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`; - ImageCacheManager.cache(null, `https://i.ytimg.com/vi/${videoId}/default.jpg`, () => true); } if (this.mounted) { this.setState({shortenedLink}); @@ -150,7 +147,7 @@ export default class PostBodyAdditionalContent extends PureComponent { } if (imageUrl) { - ImageCacheManager.cache(null, imageUrl, this.getImageSize); + this.getImageSize(imageUrl); } } }; diff --git a/app/components/profile_picture/profile_picture.js b/app/components/profile_picture/profile_picture.js index 82f6b63d5..c5668b718 100644 --- a/app/components/profile_picture/profile_picture.js +++ b/app/components/profile_picture/profile_picture.js @@ -9,8 +9,6 @@ import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome'; import {Client4} from 'mattermost-redux/client'; import UserStatus from 'app/components/user_status'; -import ImageCacheManager from 'app/utils/image_cache_manager'; -import {emptyFunction} from 'app/utils/general'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import placeholder from 'assets/images/profile.jpg'; @@ -63,7 +61,8 @@ export default class ProfilePicture extends PureComponent { } else if (edit && imageUri) { this.setImageURL(imageUri); } else if (user) { - ImageCacheManager.cache('', Client4.getProfilePictureUrl(user.id, user.last_picture_update), this.setImageURL).then(this.clearProfileImageUri).catch(emptyFunction); + this.setImageURL(Client4.getProfilePictureUrl(user.id, user.last_picture_update)); + this.clearProfileImageUri(); } } @@ -101,7 +100,8 @@ export default class ProfilePicture extends PureComponent { if (nextUrl && url !== nextUrl) { // empty function is so that promise unhandled is not triggered in dev mode - ImageCacheManager.cache('', nextUrl, this.setImageURL).then(this.clearProfileImageUri).catch(emptyFunction); + this.setImageURL(nextUrl); + this.clearProfileImageUri(); } } } 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 31f7edf48..5935bb753 100644 --- a/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap +++ b/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap @@ -3,5 +3,50 @@ exports[`ProgressiveImage should match snapshot 1`] = ` +> + + + `; diff --git a/app/components/progressive_image/progressive_image.js b/app/components/progressive_image/progressive_image.js index 7dead8f8a..0a5403785 100644 --- a/app/components/progressive_image/progressive_image.js +++ b/app/components/progressive_image/progressive_image.js @@ -6,8 +6,9 @@ import PropTypes from 'prop-types'; import {Animated, Image, ImageBackground, Platform, View, StyleSheet} from 'react-native'; import FastImage from 'react-native-fast-image'; +import {Client4} from 'mattermost-redux/client'; + import CustomPropTypes from 'app/constants/custom_prop_types'; -import ImageCacheManager from 'app/utils/image_cache_manager'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; const AnimatedImageBackground = Animated.createAnimatedComponent(ImageBackground); @@ -73,15 +74,24 @@ export default class ProgressiveImage extends PureComponent { } load = () => { - const {filename, imageUri, thumbnailUri} = this.props; + const {imageUri, thumbnailUri} = this.props; + const headers = {Authorization: `Bearer ${Client4.getToken()}`}; - if (thumbnailUri) { - ImageCacheManager.cache(filename, thumbnailUri, this.setThumbnail); + if (thumbnailUri && imageUri) { + FastImage.preload([{uri: imageUri, headers}]); + this.setThumbnail(thumbnailUri); } else if (imageUri) { - ImageCacheManager.cache(filename, imageUri, this.setImage); + this.setImage(imageUri); } }; + loadFullImage = () => { + if (!this.state.uri) { + const {imageUri} = this.props; + this.setImage(imageUri); + } + } + setImage = (uri) => { if (this.subscribedToCache) { this.setState({uri}); @@ -94,12 +104,7 @@ export default class ProgressiveImage extends PureComponent { this.load(); this.setState({failedImageLoad: true}); } else { - const {filename, imageUri} = this.props; - this.setState({thumb}, () => { - setTimeout(() => { - ImageCacheManager.cache(filename, imageUri, this.setImage); - }, 300); - }); + this.setState({thumb, uri: null}); } } }; @@ -167,28 +172,36 @@ export default class ProgressiveImage extends PureComponent { ); } + if (hasDefaultSource && !hasPreview && !hasURI) { + return ( + + {defaultImage} + {hasPreview && + + } + + ); + } + + let source; + const headers = {Authorization: `Bearer ${Client4.getToken()}`}; + if (hasPreview && !isImageReady) { + source = {uri: thumb, headers}; + } else if (isImageReady) { + source = {uri, headers}; + } + return ( - {(hasDefaultSource && !hasPreview && !hasURI) && defaultImage} - {hasPreview && !isImageReady && + {source && - {this.props.children} - - } - {isImageReady && - {this.props.children} diff --git a/app/components/progressive_image/progressive_image.test.js b/app/components/progressive_image/progressive_image.test.js index b453f6740..7f4e1977c 100644 --- a/app/components/progressive_image/progressive_image.test.js +++ b/app/components/progressive_image/progressive_image.test.js @@ -8,6 +8,10 @@ import Preferences from 'mattermost-redux/constants/preferences'; import ProgressiveImage from './progressive_image'; +jest.mock('react-native-fast-image', () => ({ + preload: jest.fn(), +})); + jest.useFakeTimers(); describe('ProgressiveImage', () => { @@ -68,15 +72,15 @@ describe('ProgressiveImage', () => { expect(instance.setImage).toHaveBeenCalledTimes(1); }); - test('should set thumbnail when thumbnailUri is set', () => { + test('should set thumbnail when thumbnailUri and imageUri are set', () => { const wrapper = shallow( , ); const instance = wrapper.instance(); jest.spyOn(instance, 'setThumbnail'); + instance.load(); jest.runAllTimers(); expect(instance.setThumbnail).toHaveBeenCalledTimes(1); diff --git a/app/components/team_icon/team_icon.js b/app/components/team_icon/team_icon.js index 75fed29f8..090fd4dd3 100644 --- a/app/components/team_icon/team_icon.js +++ b/app/components/team_icon/team_icon.js @@ -12,7 +12,6 @@ import { import {Client4} from 'mattermost-redux/client'; -import ImageCacheManager from 'app/utils/image_cache_manager'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; export default class TeamIcon extends React.PureComponent { @@ -29,34 +28,38 @@ export default class TeamIcon extends React.PureComponent { constructor(props) { super(props); - const {lastIconUpdate, teamId} = props; - if (lastIconUpdate) { - ImageCacheManager.cache('', Client4.getTeamIconUrl(teamId, lastIconUpdate), this.setImageURL); - } - this.state = { teamIcon: null, imageError: false, }; } - componentWillReceiveProps(nextProps) { - const newState = {imageError: false}; + componentDidMount() { + const {lastIconUpdate, teamId} = this.props; + this.mounted = true; - if (this.props.teamId !== nextProps.teamId) { - newState.teamIcon = null; + if (lastIconUpdate) { + this.setImageURL(Client4.getTeamIconUrl(teamId, lastIconUpdate)); } + } - if (nextProps.lastIconUpdate && this.props.lastIconUpdate !== nextProps.lastIconUpdate) { - const {lastIconUpdate, teamId} = nextProps; - ImageCacheManager.cache('', Client4.getTeamIconUrl(teamId, lastIconUpdate), this.setImageURL); + componentDidUpdate(prevProps) { + if (this.props.teamId !== prevProps.teamId) { + this.setImageURL(null); + } else if (this.props.lastIconUpdate && this.props.lastIconUpdate !== prevProps.lastIconUpdate) { + const {lastIconUpdate, teamId} = this.props; + this.setImageURL(Client4.getTeamIconUrl(teamId, lastIconUpdate)); } + } - this.setState(newState); + componentWillUnmount() { + this.mounted = false; } setImageURL = (teamIcon) => { - this.setState({teamIcon}); + if (this.mounted) { + this.setState({imageError: false, teamIcon}); + } }; render() { diff --git a/app/init/global_event_handler.js b/app/init/global_event_handler.js index 27fb747c8..7ce8e9ad5 100644 --- a/app/init/global_event_handler.js +++ b/app/init/global_event_handler.js @@ -8,7 +8,6 @@ import RNFetchBlob from 'rn-fetch-blob'; import semver from 'semver/preload'; import {setAppState, setServerVersion} from 'mattermost-redux/actions/general'; -import {loadMe, logout} from 'mattermost-redux/actions/users'; import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone'; import {close as closeWebSocket} from 'mattermost-redux/actions/websocket'; import {Client4} from 'mattermost-redux/client'; @@ -22,6 +21,7 @@ import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusB import {selectDefaultChannel} from 'app/actions/views/channel'; import {showOverlay} from 'app/actions/navigation'; import {loadConfigAndLicense, setDeepLinkURL, startDataCleanup} from 'app/actions/views/root'; +import {loadMe, logout} from 'app/actions/views/user'; import {NavigationTypes, ViewTypes} from 'app/constants'; import {getTranslations, resetMomentLocale} from 'app/i18n'; import mattermostBucket from 'app/mattermost_bucket'; diff --git a/app/utils/file.js b/app/utils/file.js index 4a77576bd..5e2db1692 100644 --- a/app/utils/file.js +++ b/app/utils/file.js @@ -7,11 +7,8 @@ import mimeDB from 'mime-db'; import {lookupMimeType} from 'mattermost-redux/utils/file_utils'; -import {DeviceTypes} from 'app/constants/'; - const EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/; const CONTENT_DISPOSITION_REGEXP = /inline;filename=".*\.([a-z]+)";/i; -const {DOCUMENTS_PATH, IMAGES_PATH, VIDEOS_PATH} = DeviceTypes; const DEFAULT_SERVER_MAX_FILE_SIZE = 50 * 1024 * 1024;// 50 Mb export const SUPPORTED_DOCS_FORMAT = [ @@ -58,50 +55,49 @@ export function generateId() { return 'uid' + id; } +const dirsToExclude = ['Cache.db', 'WebKit', 'WebView']; +async function getDirectorySize(fileStats) { + if (fileStats?.length) { + const total = await fileStats.reduce(async (previousPromise, stat) => { + const value = await previousPromise; + + const exclude = dirsToExclude.find((f) => stat.path.includes(f)); + if (exclude) { + return value; + } + + if (stat.type === 'directory') { + const stats = await RNFetchBlob.fs.lstat(stat.path); + const dirSize = await getDirectorySize(stats, 0); + return value + dirSize; + } + + return value + parseInt(stat.size, 10); + }, Promise.resolve(0)); + + return total; + } + + return 0; +} + export async function getFileCacheSize() { - const isDocsDir = await RNFetchBlob.fs.isDir(DOCUMENTS_PATH); - const isImagesDir = await RNFetchBlob.fs.isDir(IMAGES_PATH); - const isVideosDir = await RNFetchBlob.fs.isDir(VIDEOS_PATH); - let size = 0; - - if (isDocsDir) { - const docsStats = await RNFetchBlob.fs.lstat(DOCUMENTS_PATH); - size = docsStats.reduce((accumulator, stat) => { - return accumulator + parseInt(stat.size, 10); - }, size); - } - - if (isImagesDir) { - const imagesStats = await RNFetchBlob.fs.lstat(IMAGES_PATH); - size = imagesStats.reduce((accumulator, stat) => { - return accumulator + parseInt(stat.size, 10); - }, size); - } - - if (isVideosDir) { - const videoStats = await RNFetchBlob.fs.lstat(VIDEOS_PATH); - size = videoStats.reduce((accumulator, stat) => { - return accumulator + parseInt(stat.size, 10); - }, size); - } + const cacheStats = await RNFetchBlob.fs.lstat(RNFetchBlob.fs.dirs.CacheDir); + const size = await getDirectorySize(cacheStats); return size; } export async function deleteFileCache() { - const isDocsDir = await RNFetchBlob.fs.isDir(DOCUMENTS_PATH); - const isImagesDir = await RNFetchBlob.fs.isDir(IMAGES_PATH); - const isVideosDir = await RNFetchBlob.fs.isDir(VIDEOS_PATH); - if (isDocsDir) { - await RNFetchBlob.fs.unlink(DOCUMENTS_PATH); - } - - if (isImagesDir) { - await RNFetchBlob.fs.unlink(IMAGES_PATH); - } - - if (isVideosDir) { - await RNFetchBlob.fs.unlink(VIDEOS_PATH); + const cacheDir = RNFetchBlob.fs.dirs.CacheDir; + const isCacheDir = await RNFetchBlob.fs.isDir(cacheDir); + if (isCacheDir) { + try { + await RNFetchBlob.fs.unlink(cacheDir); + await RNFetchBlob.fs.mkdir(cacheDir); + } catch (e) { + // do nothing + } } return true; diff --git a/package-lock.json b/package-lock.json index 955746ac0..dccb52cf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7065,7 +7065,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -7083,11 +7084,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7100,15 +7103,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -7211,7 +7217,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -7221,6 +7228,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7233,17 +7241,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7260,6 +7271,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -7332,7 +7344,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -7342,6 +7355,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -7417,7 +7431,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -7447,6 +7462,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7464,6 +7480,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -7502,11 +7519,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } },