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
This commit is contained in:
parent
ff4a771a92
commit
0504a962a1
18 changed files with 230 additions and 229 deletions
|
|
@ -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 (
|
||||
<Text style={[this.props.textStyle, {fontSize: size}]}>
|
||||
<Text style={[textStyle, {fontSize: size}]}>
|
||||
{code}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (!imageUrl) {
|
||||
return (
|
||||
<Image
|
||||
key={key}
|
||||
style={{width, height}}
|
||||
/>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
<FastImage
|
||||
key={key}
|
||||
style={[customEmojiStyle, {width, height}]}
|
||||
source={{uri: imageUrl}}
|
||||
onError={this.onError}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(<AttachmentImage {...baseProps}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it sets state based on props', () => {
|
||||
const cacheFn = jest.fn((_, url, callback) => {
|
||||
callback(url);
|
||||
});
|
||||
ImageCacheManager.cache = cacheFn;
|
||||
|
||||
const wrapper = shallow(<AttachmentImage {...baseProps}/>);
|
||||
|
||||
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(<AttachmentImage {...props}/>);
|
||||
|
||||
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(<AttachmentImage {...baseProps}/>);
|
||||
|
||||
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(<AttachmentImage {...baseProps}/>);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,5 +3,50 @@
|
|||
exports[`ProgressiveImage should match snapshot 1`] = `
|
||||
<View
|
||||
style={Object {}}
|
||||
/>
|
||||
>
|
||||
<AnimatedComponent
|
||||
blurRadius={5}
|
||||
onError={[MockFunction]}
|
||||
onLoadEnd={[Function]}
|
||||
resizeMethod="auto"
|
||||
resizeMode="contain"
|
||||
source={
|
||||
Object {
|
||||
"headers": Object {
|
||||
"Authorization": "Bearer ",
|
||||
},
|
||||
"uri": "https://images.com/image.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
/>
|
||||
<AnimatedComponent
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"opacity": 0.8,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<View style={style}>
|
||||
{defaultImage}
|
||||
{hasPreview &&
|
||||
<Animated.View style={[StyleSheet.absoluteFill, {backgroundColor: theme.centerChannelBg, opacity}]}/>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let source;
|
||||
const headers = {Authorization: `Bearer ${Client4.getToken()}`};
|
||||
if (hasPreview && !isImageReady) {
|
||||
source = {uri: thumb, headers};
|
||||
} else if (isImageReady) {
|
||||
source = {uri, headers};
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
{(hasDefaultSource && !hasPreview && !hasURI) && defaultImage}
|
||||
{hasPreview && !isImageReady &&
|
||||
{source &&
|
||||
<ImageComponent
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
onError={onError}
|
||||
source={{uri: thumb}}
|
||||
style={[StyleSheet.absoluteFill, imageStyle]}
|
||||
blurRadius={5}
|
||||
>
|
||||
{this.props.children}
|
||||
</ImageComponent>
|
||||
}
|
||||
{isImageReady &&
|
||||
<ImageComponent
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
onError={onError}
|
||||
source={{uri}}
|
||||
source={source}
|
||||
style={[StyleSheet.absoluteFill, imageStyle]}
|
||||
blurRadius={isImageReady ? null : 5}
|
||||
onLoadEnd={this.loadFullImage}
|
||||
>
|
||||
{this.props.children}
|
||||
</ImageComponent>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<ProgressiveImage
|
||||
{...baseProps}
|
||||
imageUri={null}
|
||||
/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
jest.spyOn(instance, 'setThumbnail');
|
||||
instance.load();
|
||||
|
||||
jest.runAllTimers();
|
||||
expect(instance.setThumbnail).toHaveBeenCalledTimes(1);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
41
package-lock.json
generated
41
package-lock.json
generated
|
|
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue