MM-24285 Use FastImage instead of Image (#4218)
* Enable ESLint no-unused-vars * Use FastImage instead of Image * Update fast-image patch to support multiple cookies * Fix ESLint errors * Have jest run timers for post_textbox tests * Feedback review * Update snapshots
This commit is contained in:
parent
8226b4730c
commit
9af108026e
67 changed files with 739 additions and 813 deletions
|
|
@ -30,7 +30,7 @@
|
|||
"@typescript-eslint/camelcase": 0,
|
||||
"@typescript-eslint/no-undefined": 0,
|
||||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/no-unused-vars": 0,
|
||||
"@typescript-eslint/no-unused-vars": 2,
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-use-before-define": 0,
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import configureStore from 'redux-mock-store';
|
|||
import thunk from 'redux-thunk';
|
||||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import * as GeneralActions from '@mm-redux/actions/general';
|
||||
|
||||
import {handleSuccessfulLogin} from 'app/actions/views/login';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {batchActions} from 'redux-batched-actions';
|
|||
|
||||
import {ChannelTypes, GeneralTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {fetchMyChannelsAndMembers} from '@mm-redux/actions/channels';
|
||||
import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from '@mm-redux/actions/general';
|
||||
import {getDataRetentionPolicy} from '@mm-redux/actions/general';
|
||||
import {receivedNewPost} from '@mm-redux/actions/posts';
|
||||
import {getMyTeams, getMyTeamMembers} from '@mm-redux/actions/teams';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
|
|
|||
|
|
@ -212,10 +212,7 @@ export function logout(skipServerLogout = false) {
|
|||
}
|
||||
|
||||
export function forceLogoutIfNecessary(error) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
|
||||
return async (dispatch) => {
|
||||
if (error.status_code === HTTP_UNAUTHORIZED && error.url && !error.url.includes('/login')) {
|
||||
dispatch(logout(true));
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ import configureMockStore from 'redux-mock-store';
|
|||
import {ChannelTypes, GeneralTypes, RoleTypes, TeamTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import * as ChannelActions from '@mm-redux/actions/channels';
|
||||
import * as PostActions from '@mm-redux/actions/posts';
|
||||
import * as PreferenceActions from '@mm-redux/actions/preferences';
|
||||
import * as TeamActions from '@mm-redux/actions/teams';
|
||||
import * as UserActions from '@mm-redux/actions/users';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Posts, RequestStatus} from '@mm-redux/constants';
|
||||
import * as PostSelectors from '@mm-redux/selectors/entities/posts';
|
||||
|
|
@ -901,7 +899,7 @@ describe('Actions.Websocket doReconnect', () => {
|
|||
});
|
||||
|
||||
describe('Actions.Websocket notVisibleUsersActions', () => {
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
configureMockStore([thunk]);
|
||||
|
||||
const channel1 = TestHelper.fakeChannelWithId('');
|
||||
const channel2 = TestHelper.fakeChannelWithId('');
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ exports[`FileAttachmentList should match snapshot with a single image file 1`] =
|
|||
"marginTop": 5,
|
||||
},
|
||||
Object {
|
||||
"width": undefined,
|
||||
"width": 680,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
@ -109,7 +109,7 @@ exports[`FileAttachmentList should match snapshot with combination of image and
|
|||
"marginTop": 5,
|
||||
},
|
||||
Object {
|
||||
"width": undefined,
|
||||
"width": 680,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
@ -332,7 +332,7 @@ exports[`FileAttachmentList should match snapshot with four image files 1`] = `
|
|||
"marginTop": 5,
|
||||
},
|
||||
Object {
|
||||
"width": undefined,
|
||||
"width": 680,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
@ -637,7 +637,7 @@ exports[`FileAttachmentList should match snapshot with more than four image file
|
|||
"marginTop": 5,
|
||||
},
|
||||
Object {
|
||||
"width": undefined,
|
||||
"width": 680,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
@ -1014,7 +1014,7 @@ exports[`FileAttachmentList should match snapshot with three image files 1`] = `
|
|||
"marginTop": 5,
|
||||
},
|
||||
Object {
|
||||
"width": undefined,
|
||||
"width": 680,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
@ -1248,7 +1248,7 @@ exports[`FileAttachmentList should match snapshot with two image files 1`] = `
|
|||
"marginTop": 5,
|
||||
},
|
||||
Object {
|
||||
"width": undefined,
|
||||
"width": 680,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Animated,
|
||||
View,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
|
|
@ -12,11 +11,11 @@ import FastImage from 'react-native-fast-image';
|
|||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
import {isGif} from 'app/utils/file';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import ProgressiveImage from '@components/progressive_image';
|
||||
import {isGif} from '@utils/file';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import thumb from 'assets/images/thumb.png';
|
||||
import brokenImageIcon from 'assets/images/icons/brokenimage.png';
|
||||
|
||||
const SMALL_IMAGE_MAX_HEIGHT = 48;
|
||||
const SMALL_IMAGE_MAX_WIDTH = 48;
|
||||
|
|
@ -57,11 +56,7 @@ export default class FileAttachmentImage extends PureComponent {
|
|||
|
||||
const {file} = props;
|
||||
if (file && file.id && !file.localPath) {
|
||||
const headers = {
|
||||
Authorization: `Bearer ${Client4.getToken()}`,
|
||||
'X-CSRF-Token': Client4.csrf,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
};
|
||||
const headers = Client4.getOptions({}).headers;
|
||||
|
||||
const preloadImages = [
|
||||
{uri: Client4.getFileThumbnailUrl(file.id), headers},
|
||||
|
|
@ -76,9 +71,7 @@ export default class FileAttachmentImage extends PureComponent {
|
|||
}
|
||||
|
||||
this.state = {
|
||||
opacity: new Animated.Value(0),
|
||||
requesting: true,
|
||||
retry: 0,
|
||||
failed: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -97,9 +90,17 @@ export default class FileAttachmentImage extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
handleError = () => {
|
||||
this.setState({failed: true});
|
||||
}
|
||||
|
||||
imageProps = (file) => {
|
||||
const imageProps = {};
|
||||
if (file.localPath) {
|
||||
const {failed} = this.state;
|
||||
|
||||
if (failed) {
|
||||
imageProps.defaultSource = brokenImageIcon;
|
||||
} else if (file.localPath) {
|
||||
imageProps.defaultSource = {uri: file.localPath};
|
||||
} else if (file.id) {
|
||||
imageProps.thumbnailUri = Client4.getFileThumbnailUrl(file.id);
|
||||
|
|
@ -134,9 +135,9 @@ export default class FileAttachmentImage extends PureComponent {
|
|||
<View style={style.smallImageOverlay}>
|
||||
<ProgressiveImage
|
||||
style={{height: file.height, width: file.width}}
|
||||
defaultSource={thumb}
|
||||
tintDefaultSource={!file.localPath}
|
||||
tintDefaultSource={!file.localPath && !this.state.failed}
|
||||
filename={file.name}
|
||||
onError={this.handleError}
|
||||
resizeMode={'contain'}
|
||||
resizeMethod={resizeMethod}
|
||||
{...this.imageProps(file)}
|
||||
|
|
@ -168,9 +169,9 @@ export default class FileAttachmentImage extends PureComponent {
|
|||
{this.boxPlaceholder()}
|
||||
<ProgressiveImage
|
||||
style={[this.props.isSingleImage ? null : style.imagePreview, imageDimensions]}
|
||||
defaultSource={thumb}
|
||||
tintDefaultSource={!file.localPath}
|
||||
tintDefaultSource={!file.localPath && !this.state.failed}
|
||||
filename={file.name}
|
||||
onError={this.handleError}
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
{...imageProps}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,21 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Dimensions, StyleSheet, View} from 'react-native';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import ImageViewPort from '@components/image_viewport';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
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 {previewImageAtIndex} from 'app/utils/images';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {isDocument, isGif, isVideo} from '@utils/file';
|
||||
import {getViewPortWidth, previewImageAtIndex} from '@utils/images';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
import FileAttachment from './file_attachment';
|
||||
|
||||
const MAX_VISIBLE_ROW_IMAGES = 4;
|
||||
const VIEWPORT_IMAGE_OFFSET = 70;
|
||||
const VIEWPORT_IMAGE_REPLY_OFFSET = 11;
|
||||
|
||||
export default class FileAttachmentList extends PureComponent {
|
||||
export default class FileAttachmentList extends ImageViewPort {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
loadFilesForPostIfNecessary: PropTypes.func.isRequired,
|
||||
|
|
@ -41,8 +34,6 @@ export default class FileAttachmentList extends PureComponent {
|
|||
files: [],
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
|
@ -55,14 +46,9 @@ export default class FileAttachmentList extends PureComponent {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
const {files} = this.props;
|
||||
|
||||
this.mounted = true;
|
||||
this.handlePermanentSidebar();
|
||||
this.handleDimensions();
|
||||
EventEmitter.on(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
Dimensions.addEventListener('change', this.handleDimensions);
|
||||
|
||||
if (files.length === 0) {
|
||||
this.loadFilesForPost();
|
||||
}
|
||||
|
|
@ -78,12 +64,6 @@ export default class FileAttachmentList extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
EventEmitter.off(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
Dimensions.removeEventListener('change', this.handleDimensions);
|
||||
}
|
||||
|
||||
attachmentIndex = (fileId) => {
|
||||
return this.filesForGallery.findIndex((file) => file.id === fileId) || 0;
|
||||
};
|
||||
|
|
@ -147,45 +127,10 @@ export default class FileAttachmentList extends PureComponent {
|
|||
return results;
|
||||
};
|
||||
|
||||
getPortraitPostWidth = () => {
|
||||
const {isReplyPost} = this.props;
|
||||
const {width, height} = Dimensions.get('window');
|
||||
const permanentSidebar = DeviceTypes.IS_TABLET && !this.state?.isSplitView && this.state?.permanentSidebar;
|
||||
let portraitPostWidth = Math.min(width, height) - VIEWPORT_IMAGE_OFFSET;
|
||||
|
||||
if (permanentSidebar) {
|
||||
portraitPostWidth -= TABLET_WIDTH;
|
||||
}
|
||||
|
||||
if (isReplyPost) {
|
||||
portraitPostWidth -= VIEWPORT_IMAGE_REPLY_OFFSET;
|
||||
}
|
||||
|
||||
return portraitPostWidth;
|
||||
};
|
||||
|
||||
handleCaptureRef = (ref, idx) => {
|
||||
this.items[idx] = ref;
|
||||
};
|
||||
|
||||
handleDimensions = () => {
|
||||
if (this.mounted) {
|
||||
if (DeviceTypes.IS_TABLET) {
|
||||
mattermostManaged.isRunningInSplitView().then((result) => {
|
||||
const isSplitView = Boolean(result.isSplitView);
|
||||
this.setState({isSplitView});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handlePermanentSidebar = async () => {
|
||||
if (DeviceTypes.IS_TABLET && this.mounted) {
|
||||
const enabled = await AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS);
|
||||
this.setState({permanentSidebar: enabled === 'true'});
|
||||
}
|
||||
};
|
||||
|
||||
handlePreviewPress = preventDoubleTap((idx) => {
|
||||
previewImageAtIndex(this.items, idx, this.galleryFiles);
|
||||
});
|
||||
|
|
@ -199,7 +144,7 @@ export default class FileAttachmentList extends PureComponent {
|
|||
}
|
||||
|
||||
renderItems = (items, moreImagesCount, includeGutter = false) => {
|
||||
const {canDownloadFiles, onLongPress, theme} = this.props;
|
||||
const {canDownloadFiles, isReplyPost, onLongPress, theme} = this.props;
|
||||
const isSingleImage = this.isSingleImage(items);
|
||||
let nonVisibleImagesCount;
|
||||
let container = styles.container;
|
||||
|
|
@ -236,7 +181,7 @@ export default class FileAttachmentList extends PureComponent {
|
|||
theme={theme}
|
||||
isSingleImage={isSingleImage}
|
||||
nonVisibleImagesCount={nonVisibleImagesCount}
|
||||
wrapperWidth={this.getPortraitPostWidth()}
|
||||
wrapperWidth={getViewPortWidth(isReplyPost, this.hasPermanentSidebar())}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -248,8 +193,9 @@ export default class FileAttachmentList extends PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const {isReplyPost} = this.props;
|
||||
const visibleImages = images.slice(0, MAX_VISIBLE_ROW_IMAGES);
|
||||
const {portraitPostWidth} = this.state;
|
||||
const portraitPostWidth = getViewPortWidth(isReplyPost, this.hasPermanentSidebar());
|
||||
|
||||
let nonVisibleImagesCount;
|
||||
if (images.length > MAX_VISIBLE_ROW_IMAGES) {
|
||||
|
|
|
|||
|
|
@ -3,21 +3,21 @@
|
|||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text, View} from 'react-native';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import {AnimatedCircularProgress} from 'react-native-circular-progress';
|
||||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import FileAttachmentImage from 'app/components/file_attachment_list/file_attachment_image';
|
||||
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
|
||||
import FileUploadRetry from 'app/components/file_upload_preview/file_upload_retry';
|
||||
import FileUploadRemove from 'app/components/file_upload_preview/file_upload_remove';
|
||||
import FileAttachmentImage from '@components/file_attachment_list/file_attachment_image';
|
||||
import FileAttachmentIcon from '@components/file_attachment_list/file_attachment_icon';
|
||||
import FileUploadRetry from '@components/file_upload_preview/file_upload_retry';
|
||||
import FileUploadRemove from '@components/file_upload_preview/file_upload_remove';
|
||||
import {buildFileUploadData, encodeHeaderURIStringToUTF8} from '@utils/file';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import ImageCacheManager from '@utils/image_cache_manager';
|
||||
|
||||
import mattermostBucket from 'app/mattermost_bucket';
|
||||
import {buildFileUploadData, encodeHeaderURIStringToUTF8} from 'app/utils/file';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class FileUploadItem extends PureComponent {
|
||||
static propTypes = {
|
||||
|
|
@ -132,10 +132,8 @@ export default class FileUploadItem extends PureComponent {
|
|||
const fileData = buildFileUploadData(file);
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${Client4.getToken()}`,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
...Client4.getOptions({method: 'post'}).headers,
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'X-CSRF-Token': Client4.csrf,
|
||||
};
|
||||
|
||||
const fileInfo = {
|
||||
|
|
@ -164,7 +162,6 @@ export default class FileUploadItem extends PureComponent {
|
|||
};
|
||||
|
||||
renderProgress = (fill) => {
|
||||
const styles = getStyleSheet(this.props.theme);
|
||||
const realFill = Number(fill.toFixed(0));
|
||||
|
||||
return (
|
||||
|
|
@ -184,7 +181,6 @@ export default class FileUploadItem extends PureComponent {
|
|||
theme,
|
||||
} = this.props;
|
||||
const {progress} = this.state;
|
||||
const styles = getStyleSheet(theme);
|
||||
let filePreviewComponent;
|
||||
|
||||
if (this.isImageType()) {
|
||||
|
|
@ -193,6 +189,7 @@ export default class FileUploadItem extends PureComponent {
|
|||
<FileAttachmentImage
|
||||
file={file}
|
||||
theme={theme}
|
||||
resizeMode='center'
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -249,7 +246,7 @@ export default class FileUploadItem extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
const styles = StyleSheet.create({
|
||||
preview: {
|
||||
paddingTop: 5,
|
||||
marginLeft: 12,
|
||||
|
|
@ -274,10 +271,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
|||
fontWeight: 'bold',
|
||||
},
|
||||
filePreview: {
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
width: 56,
|
||||
height: 56,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
|
|
|||
55
app/components/image_viewport.js
Normal file
55
app/components/image_viewport.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import {PureComponent} from 'react';
|
||||
import {Dimensions} from 'react-native';
|
||||
|
||||
import {DeviceTypes} from '@constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
|
||||
export default class ImageViewPort extends PureComponent {
|
||||
state = {};
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.handlePermanentSidebar();
|
||||
this.handleDimensions();
|
||||
EventEmitter.on(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
Dimensions.addEventListener('change', this.handleDimensions);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
EventEmitter.off(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
Dimensions.removeEventListener('change', this.handleDimensions);
|
||||
}
|
||||
|
||||
handleDimensions = () => {
|
||||
if (this.mounted) {
|
||||
if (DeviceTypes.IS_TABLET) {
|
||||
mattermostManaged.isRunningInSplitView().then((result) => {
|
||||
const isSplitView = Boolean(result.isSplitView);
|
||||
this.setState({isSplitView});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handlePermanentSidebar = async () => {
|
||||
if (DeviceTypes.IS_TABLET && this.mounted) {
|
||||
const enabled = await AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS);
|
||||
this.setState({permanentSidebar: enabled === 'true'});
|
||||
}
|
||||
};
|
||||
|
||||
hasPermanentSidebar = () => {
|
||||
return DeviceTypes.IS_TABLET && !this.state?.isSplitView && this.state?.permanentSidebar;
|
||||
};
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,13 +5,10 @@ import {connect} from 'react-redux';
|
|||
|
||||
import {getCurrentUrl} from '@mm-redux/selectors/entities/general';
|
||||
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
|
||||
import MarkdownImage from './markdown_image';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
...getDimensions(state),
|
||||
serverURL: getCurrentUrl(state),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,29 +13,27 @@ import {
|
|||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import ImageViewPort from '@components/image_viewport';
|
||||
import ProgressiveImage from '@components/progressive_image';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {CustomPropTypes} from '@constants';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import BottomSheet from '@utils/bottom_sheet';
|
||||
import {calculateDimensions, getViewPortWidth, isGifTooLarge, previewImageAtIndex} from '@utils/images';
|
||||
import {normalizeProtocol} from '@utils/url';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
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 {previewImageAtIndex, calculateDimensions, isGifTooLarge} from 'app/utils/images';
|
||||
import {normalizeProtocol} from 'app/utils/url';
|
||||
|
||||
import brokenImageIcon from 'assets/images/icons/brokenimage.png';
|
||||
|
||||
const ANDROID_MAX_HEIGHT = 4096;
|
||||
const ANDROID_MAX_WIDTH = 4096;
|
||||
const VIEWPORT_IMAGE_OFFSET = 66;
|
||||
const VIEWPORT_IMAGE_REPLY_OFFSET = 13;
|
||||
|
||||
export default class MarkdownImage extends React.PureComponent {
|
||||
export default class MarkdownImage extends ImageViewPort {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
imagesMetadata: PropTypes.object,
|
||||
linkDestination: PropTypes.string,
|
||||
isReplyPost: PropTypes.bool,
|
||||
|
|
@ -57,38 +55,11 @@ export default class MarkdownImage extends React.PureComponent {
|
|||
failed: false,
|
||||
uri: null,
|
||||
};
|
||||
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
|
||||
this.setImageUrl(this.getSource());
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props) {
|
||||
const imageMetadata = props.imagesMetadata?.[props.source];
|
||||
|
||||
if (imageMetadata) {
|
||||
return {
|
||||
originalHeight: imageMetadata.height,
|
||||
originalWidth: imageMetadata.width,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.source !== prevProps.source) {
|
||||
// getSource also depends on serverURL, but that shouldn't change while this is mounted
|
||||
this.setImageUrl(this.getSource());
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
super.componentDidMount();
|
||||
this.loadImageSize(this.getSource());
|
||||
}
|
||||
|
||||
setImageRef = (ref) => {
|
||||
|
|
@ -100,19 +71,14 @@ export default class MarkdownImage extends React.PureComponent {
|
|||
}
|
||||
|
||||
getSource = () => {
|
||||
let source = this.props.source;
|
||||
let uri = this.props.source;
|
||||
|
||||
if (source.startsWith('/')) {
|
||||
source = EphemeralStore.currentServerUrl + source;
|
||||
if (uri.startsWith('/')) {
|
||||
uri = EphemeralStore.currentServerUrl + uri;
|
||||
}
|
||||
|
||||
return source;
|
||||
};
|
||||
|
||||
getViewPortWidth = () => {
|
||||
const {deviceHeight, deviceWidth, isReplyPost} = this.props;
|
||||
const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth;
|
||||
return deviceSize - VIEWPORT_IMAGE_OFFSET - (isReplyPost ? VIEWPORT_IMAGE_REPLY_OFFSET : 0);
|
||||
FastImage.preload([{uri}]);
|
||||
return uri;
|
||||
};
|
||||
|
||||
handleSizeReceived = (width, height) => {
|
||||
|
|
@ -179,7 +145,6 @@ export default class MarkdownImage extends React.PureComponent {
|
|||
const {
|
||||
originalHeight,
|
||||
originalWidth,
|
||||
uri,
|
||||
} = this.state;
|
||||
const link = this.getSource();
|
||||
let filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?'));
|
||||
|
|
@ -196,9 +161,9 @@ export default class MarkdownImage extends React.PureComponent {
|
|||
width: originalWidth,
|
||||
height: originalHeight,
|
||||
},
|
||||
source: {uri},
|
||||
source: {link},
|
||||
data: {
|
||||
localPath: uri,
|
||||
localPath: link,
|
||||
},
|
||||
}];
|
||||
|
||||
|
|
@ -211,21 +176,15 @@ export default class MarkdownImage extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
setImageUrl = (imageURL) => {
|
||||
const uri = imageURL;
|
||||
|
||||
this.setState({uri});
|
||||
this.loadImageSize(uri);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (isGifTooLarge(this.props.imagesMetadata?.[this.props.source])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let image = null;
|
||||
const {originalHeight, originalWidth, uri} = this.state;
|
||||
const {height, width} = calculateDimensions(originalHeight, originalWidth, this.getViewPortWidth());
|
||||
const {originalHeight, originalWidth} = this.state;
|
||||
const uri = this.getSource();
|
||||
const {height, width} = calculateDimensions(originalHeight, originalWidth, getViewPortWidth(this.props.isReplyPost, this.hasPermanentSidebar()));
|
||||
|
||||
if (width && height) {
|
||||
if (Platform.OS === 'android' && (width > ANDROID_MAX_WIDTH || height > ANDROID_MAX_HEIGHT)) {
|
||||
|
|
@ -269,7 +228,7 @@ export default class MarkdownImage extends React.PureComponent {
|
|||
}
|
||||
} else if (this.state.failed) {
|
||||
image = (
|
||||
<Image
|
||||
<FastImage
|
||||
source={brokenImageIcon}
|
||||
style={style.brokenImageIcon}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3055,7 +3055,7 @@ function nodeToString(node) {
|
|||
}
|
||||
|
||||
const ignoredKeys = {_sourcepos: true, _lastLineBlank: true, _open: true, _string_content: true, _info: true, _isFenced: true, _fenceChar: true, _fenceLength: true, _fenceOffset: true, _onEnter: true, _onExit: true};
|
||||
function astToJson(node, visited = [], indent = '') { // eslint-disable-line no-unused-vars
|
||||
function astToJson(node, visited = [], indent = '') { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
let out = '{';
|
||||
|
||||
const myVisited = [...visited];
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ exports[`AttachmentFooter it matches snapshot when both footer and footer_icon a
|
|||
}
|
||||
}
|
||||
>
|
||||
<Image
|
||||
<FastImage
|
||||
key="footer_icon"
|
||||
source={
|
||||
Object {
|
||||
|
|
@ -78,7 +78,7 @@ exports[`AttachmentFooter it matches snapshot when the footer is longer than the
|
|||
}
|
||||
}
|
||||
>
|
||||
<Image
|
||||
<FastImage
|
||||
key="footer_icon"
|
||||
source={
|
||||
Object {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {Image, Linking, Text, View} from 'react-native';
|
||||
import {Linking, Text, View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
|
@ -15,6 +16,14 @@ export default class AttachmentAuthor extends PureComponent {
|
|||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props.icon) {
|
||||
FastImage.preload([{uri: props.icon}]);
|
||||
}
|
||||
}
|
||||
|
||||
openLink = () => {
|
||||
const {link} = this.props;
|
||||
if (link && Linking.canOpenURL(link)) {
|
||||
|
|
@ -39,7 +48,7 @@ export default class AttachmentAuthor extends PureComponent {
|
|||
return (
|
||||
<View style={style.container}>
|
||||
{Boolean(icon) &&
|
||||
<Image
|
||||
<FastImage
|
||||
source={{uri: icon}}
|
||||
key='author_icon'
|
||||
style={style.icon}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {Image, Text, View, Platform} from 'react-native';
|
||||
import {Text, View, Platform} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import PropTypes from 'prop-types';
|
||||
import truncate from 'lodash/truncate';
|
||||
|
||||
|
|
@ -16,6 +17,14 @@ export default class AttachmentFooter extends PureComponent {
|
|||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props.icon) {
|
||||
FastImage.preload([{uri: props.icon}]);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
text,
|
||||
|
|
@ -32,7 +41,7 @@ export default class AttachmentFooter extends PureComponent {
|
|||
return (
|
||||
<View style={style.container}>
|
||||
{Boolean(icon) &&
|
||||
<Image
|
||||
<FastImage
|
||||
source={{uri: icon}}
|
||||
key='footer_icon'
|
||||
style={style.icon}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ exports[`PostAttachmentOpenGraph should match snapshot, without image and descri
|
|||
onPress={[Function]}
|
||||
type="none"
|
||||
>
|
||||
<Image
|
||||
<FastImage
|
||||
resizeMode="contain"
|
||||
style={
|
||||
Array [
|
||||
|
|
@ -269,7 +269,7 @@ exports[`PostAttachmentOpenGraph should match state and snapshot, on renderImage
|
|||
onPress={[Function]}
|
||||
type="none"
|
||||
>
|
||||
<Image
|
||||
<FastImage
|
||||
resizeMode="contain"
|
||||
style={
|
||||
Array [
|
||||
|
|
|
|||
|
|
@ -3,20 +3,15 @@
|
|||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Image,
|
||||
Linking,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {Image, Linking, Text, View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout';
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
|
||||
import {previewImageAtIndex, calculateDimensions} from 'app/utils/images';
|
||||
import {getNearestPoint} from 'app/utils/opengraph';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {TABLET_WIDTH} from '@components/sidebars/drawer_layout';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {DeviceTypes} from '@constants';
|
||||
import {previewImageAtIndex, calculateDimensions} from '@utils/images';
|
||||
import {getNearestPoint} from '@utils/opengraph';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const MAX_IMAGE_HEIGHT = 150;
|
||||
const VIEWPORT_IMAGE_OFFSET = 93;
|
||||
|
|
@ -115,6 +110,8 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
|||
dimensions = calculateDimensions(ogImage.height, ogImage.width, this.getViewPostWidth());
|
||||
}
|
||||
|
||||
FastImage.preload([{uri: imageUrl}]);
|
||||
|
||||
return {
|
||||
hasImage: true,
|
||||
...dimensions,
|
||||
|
|
@ -259,7 +256,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
|||
onPress={this.handlePreviewImage}
|
||||
type={'none'}
|
||||
>
|
||||
<Image
|
||||
<FastImage
|
||||
style={[style.image, {width, height}]}
|
||||
source={source}
|
||||
resizeMode='contain'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {Image} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
|
@ -81,7 +81,7 @@ describe('PostAttachmentOpenGraph', () => {
|
|||
// should return null
|
||||
expect(wrapper.instance().renderImage()).toMatchSnapshot();
|
||||
expect(wrapper.state('hasImage')).toEqual(false);
|
||||
expect(wrapper.find(Image).exists()).toEqual(false);
|
||||
expect(wrapper.find(FastImage).exists()).toEqual(false);
|
||||
expect(wrapper.find(TouchableWithFeedback).exists()).toEqual(false);
|
||||
|
||||
const images = [{height: 440, width: 1200, url: 'https://mattermost.com/logo.png'}];
|
||||
|
|
@ -90,7 +90,7 @@ describe('PostAttachmentOpenGraph', () => {
|
|||
|
||||
expect(wrapper.instance().renderImage()).toMatchSnapshot();
|
||||
expect(wrapper.state('hasImage')).toEqual(true);
|
||||
expect(wrapper.find(Image).exists()).toEqual(true);
|
||||
expect(wrapper.find(FastImage).exists()).toEqual(true);
|
||||
expect(wrapper.find(TouchableWithFeedback).exists()).toEqual(true);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Alert,
|
||||
|
|
@ -14,26 +14,22 @@ import {
|
|||
import {YouTubeStandaloneAndroid, YouTubeStandaloneIOS} from 'react-native-youtube';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import ImageViewPort from '@components/image_viewport';
|
||||
import PostAttachmentImage from '@components/post_attachment_image';
|
||||
import ProgressiveImage from '@components/progressive_image';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import CustomPropTypes from '@constants/custom_prop_types';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {calculateDimensions, getViewPortWidth, previewImageAtIndex} from '@utils/images';
|
||||
import {getYouTubeVideoId, isImageLink, isYoutubeLink} from '@utils/url';
|
||||
|
||||
import {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout';
|
||||
import PostAttachmentImage from 'app/components/post_attachment_image';
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {previewImageAtIndex, calculateDimensions} from 'app/utils/images';
|
||||
import {getYouTubeVideoId, isImageLink, isYoutubeLink} from 'app/utils/url';
|
||||
|
||||
const VIEWPORT_IMAGE_OFFSET = 66;
|
||||
const VIEWPORT_IMAGE_REPLY_OFFSET = 13;
|
||||
const MAX_YOUTUBE_IMAGE_HEIGHT = 150;
|
||||
const MAX_YOUTUBE_IMAGE_WIDTH = 297;
|
||||
const MAX_YOUTUBE_IMAGE_HEIGHT = 202;
|
||||
const MAX_YOUTUBE_IMAGE_WIDTH = 360;
|
||||
const MAX_IMAGE_HEIGHT = 150;
|
||||
let MessageAttachments;
|
||||
let PostAttachmentOpenGraph;
|
||||
|
||||
export default class PostBodyAdditionalContent extends PureComponent {
|
||||
export default class PostBodyAdditionalContent extends ImageViewPort {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
getRedirectLocation: PropTypes.func.isRequired,
|
||||
|
|
@ -73,7 +69,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
|||
if (this.isImage() && props.metadata && props.metadata.images) {
|
||||
const img = props.metadata.images[props.link];
|
||||
if (img && img.height && img.width) {
|
||||
dimensions = calculateDimensions(img.height, img.width, this.getViewPortWidth(props));
|
||||
dimensions = calculateDimensions(img.height, img.width, getViewPortWidth(props.isReplyPost, this.hasPermanentSidebar()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,13 +78,10 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
|||
linkLoaded: false,
|
||||
...dimensions,
|
||||
};
|
||||
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
|
||||
super.componentDidMount();
|
||||
this.load();
|
||||
}
|
||||
|
||||
|
|
@ -98,174 +91,20 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
isImage = (specificLink) => {
|
||||
const {metadata, link} = this.props;
|
||||
|
||||
if (isImageLink(specificLink || link)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (metadata && metadata.images) {
|
||||
return Boolean(metadata.images[specificLink] || metadata.images[link]);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
getImageUrl = (link) => {
|
||||
let imageUrl;
|
||||
|
||||
if (this.isImage()) {
|
||||
imageUrl = link;
|
||||
} else if (isYoutubeLink(link)) {
|
||||
const videoId = getYouTubeVideoId(link);
|
||||
imageUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
load = (linkChanged = false) => {
|
||||
const {link, expandedLink, actions} = this.props;
|
||||
|
||||
if (link) {
|
||||
let imageUrl = this.getImageUrl(link);
|
||||
|
||||
if (!imageUrl) {
|
||||
if (!expandedLink || linkChanged) {
|
||||
actions.getRedirectLocation(link);
|
||||
} else {
|
||||
imageUrl = this.getImageUrl(expandedLink);
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
this.getImageSize(imageUrl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
calculateYouTubeImageDimensions = (width, height) => {
|
||||
const {deviceHeight, deviceWidth} = this.props;
|
||||
let maxHeight = MAX_YOUTUBE_IMAGE_HEIGHT;
|
||||
const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth;
|
||||
let maxWidth = deviceSize - 78;
|
||||
|
||||
if (maxWidth > MAX_YOUTUBE_IMAGE_WIDTH) {
|
||||
maxWidth = MAX_YOUTUBE_IMAGE_WIDTH;
|
||||
}
|
||||
|
||||
if (height <= MAX_YOUTUBE_IMAGE_HEIGHT) {
|
||||
maxHeight = height;
|
||||
} else {
|
||||
maxHeight = (height / width) * maxWidth;
|
||||
if (maxHeight > MAX_YOUTUBE_IMAGE_HEIGHT) {
|
||||
maxHeight = MAX_YOUTUBE_IMAGE_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
if (height > width) {
|
||||
maxWidth = (width / height) * maxHeight;
|
||||
}
|
||||
|
||||
return {width: maxWidth, height: maxHeight};
|
||||
};
|
||||
|
||||
generateStaticEmbed = (isYouTube, isImage) => {
|
||||
const {isReplyPost, link, metadata, openGraphData, showLinkPreviews, theme} = this.props;
|
||||
|
||||
if (isYouTube || (isImage && !openGraphData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attachments = this.getMessageAttachment();
|
||||
if (attachments) {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
if (!openGraphData && metadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (link && showLinkPreviews) {
|
||||
if (!PostAttachmentOpenGraph) {
|
||||
PostAttachmentOpenGraph = require('app/components/post_attachment_opengraph').default;
|
||||
}
|
||||
|
||||
return (
|
||||
<PostAttachmentOpenGraph
|
||||
isReplyPost={isReplyPost}
|
||||
link={link}
|
||||
openGraphData={openGraphData}
|
||||
imagesMetadata={metadata && metadata.images}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
generateToggleableEmbed = (isImage, isYouTube) => {
|
||||
let {link} = this.props;
|
||||
const {expandedLink} = this.props;
|
||||
if (expandedLink) {
|
||||
link = expandedLink;
|
||||
}
|
||||
const {width, height, uri} = this.state;
|
||||
|
||||
if (link) {
|
||||
if (isYouTube) {
|
||||
const videoId = getYouTubeVideoId(link);
|
||||
const imgUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
|
||||
const thumbUrl = `https://i.ytimg.com/vi/${videoId}/default.jpg`;
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
style={[styles.imageContainer, {height: height || MAX_YOUTUBE_IMAGE_HEIGHT}]}
|
||||
onPress={this.playYouTubeVideo}
|
||||
type={'opacity'}
|
||||
>
|
||||
<ProgressiveImage
|
||||
isBackgroundImage={true}
|
||||
imageUri={imgUrl}
|
||||
style={[styles.image, {width: width || MAX_YOUTUBE_IMAGE_WIDTH, height: height || MAX_YOUTUBE_IMAGE_HEIGHT}]}
|
||||
thumbnailUri={thumbUrl}
|
||||
resizeMode='cover'
|
||||
onError={this.handleLinkLoadError}
|
||||
>
|
||||
<TouchableWithFeedback
|
||||
style={styles.playButton}
|
||||
onPress={this.playYouTubeVideo}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Image
|
||||
source={require('assets/images/icons/youtube-play-icon.png')}
|
||||
onPress={this.playYouTubeVideo}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
</ProgressiveImage>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
return this.renderYouTubeVideo(link);
|
||||
}
|
||||
|
||||
if (isImage) {
|
||||
const imageMetadata = this.props.metadata?.images?.[link];
|
||||
|
||||
return (
|
||||
<PostAttachmentImage
|
||||
height={height || MAX_YOUTUBE_IMAGE_HEIGHT}
|
||||
imageMetadata={imageMetadata}
|
||||
onImagePress={this.handlePreviewImage}
|
||||
onError={this.handleLinkLoadError}
|
||||
uri={uri}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
return this.renderImage(link);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -291,83 +130,17 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
getViewPortWidth = (props) => {
|
||||
const {deviceHeight, deviceWidth, isReplyPost} = props;
|
||||
const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth;
|
||||
const viewPortWidth = deviceSize - VIEWPORT_IMAGE_OFFSET - (isReplyPost ? VIEWPORT_IMAGE_REPLY_OFFSET : 0);
|
||||
const tabletOffset = DeviceTypes.IS_TABLET ? TABLET_WIDTH : 0;
|
||||
getImageUrl = (link) => {
|
||||
let imageUrl;
|
||||
|
||||
return viewPortWidth - tabletOffset;
|
||||
};
|
||||
|
||||
setImageSize = (uri, originalWidth, originalHeight) => {
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
if (this.isImage()) {
|
||||
imageUrl = link;
|
||||
} else if (isYoutubeLink(link)) {
|
||||
const videoId = getYouTubeVideoId(link);
|
||||
imageUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
|
||||
}
|
||||
|
||||
if (!originalWidth && !originalHeight) {
|
||||
this.setState({linkLoadError: true});
|
||||
return;
|
||||
}
|
||||
|
||||
const {link} = this.props;
|
||||
const viewPortWidth = this.getViewPortWidth(this.props);
|
||||
|
||||
let dimensions;
|
||||
if (isYoutubeLink(link)) {
|
||||
dimensions = this.calculateYouTubeImageDimensions(originalWidth, originalHeight);
|
||||
} else {
|
||||
dimensions = calculateDimensions(originalHeight, originalWidth, viewPortWidth);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
...dimensions,
|
||||
originalHeight,
|
||||
originalWidth,
|
||||
linkLoaded: true,
|
||||
uri,
|
||||
});
|
||||
};
|
||||
|
||||
getMessageAttachment = () => {
|
||||
const {
|
||||
postId,
|
||||
postProps,
|
||||
baseTextStyle,
|
||||
blockStyles,
|
||||
deviceHeight,
|
||||
deviceWidth,
|
||||
metadata,
|
||||
onHashtagPress,
|
||||
onPermalinkPress,
|
||||
textStyles,
|
||||
theme,
|
||||
} = this.props;
|
||||
const {attachments} = postProps;
|
||||
|
||||
if (attachments && attachments.length) {
|
||||
if (!MessageAttachments) {
|
||||
MessageAttachments = require('app/components/message_attachments').default;
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageAttachments
|
||||
attachments={attachments}
|
||||
baseTextStyle={baseTextStyle}
|
||||
blockStyles={blockStyles}
|
||||
deviceHeight={deviceHeight}
|
||||
deviceWidth={deviceWidth}
|
||||
metadata={metadata}
|
||||
postId={postId}
|
||||
textStyles={textStyles}
|
||||
theme={theme}
|
||||
onHashtagPress={onHashtagPress}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
getYouTubeTime = (link) => {
|
||||
|
|
@ -430,6 +203,44 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
|||
previewImageAtIndex([imageRef], 0, files);
|
||||
};
|
||||
|
||||
isImage = (specificLink) => {
|
||||
const {metadata, link} = this.props;
|
||||
|
||||
if (isImageLink(specificLink || link)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (metadata && metadata.images) {
|
||||
return Boolean(metadata.images[specificLink] || metadata.images[link]);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
load = (linkChanged = false) => {
|
||||
const {link, expandedLink, actions} = this.props;
|
||||
|
||||
if (link) {
|
||||
let imageUrl = this.getImageUrl(link);
|
||||
|
||||
if (isYoutubeLink(link)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!imageUrl) {
|
||||
if (!expandedLink || linkChanged) {
|
||||
actions.getRedirectLocation(link);
|
||||
} else {
|
||||
imageUrl = this.getImageUrl(expandedLink);
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
this.getImageSize(imageUrl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
playYouTubeVideo = () => {
|
||||
const {link} = this.props;
|
||||
const videoId = getYouTubeVideoId(link);
|
||||
|
|
@ -480,6 +291,164 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
|||
);
|
||||
};
|
||||
|
||||
renderImage = (link) => {
|
||||
const imageMetadata = this.props.metadata?.images?.[link];
|
||||
const {width, height, uri} = this.state;
|
||||
|
||||
return (
|
||||
<PostAttachmentImage
|
||||
height={height || MAX_IMAGE_HEIGHT}
|
||||
imageMetadata={imageMetadata}
|
||||
onImagePress={this.handlePreviewImage}
|
||||
onError={this.handleLinkLoadError}
|
||||
uri={uri}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderMessageAttachment = () => {
|
||||
const {
|
||||
postId,
|
||||
postProps,
|
||||
baseTextStyle,
|
||||
blockStyles,
|
||||
deviceHeight,
|
||||
deviceWidth,
|
||||
metadata,
|
||||
onHashtagPress,
|
||||
onPermalinkPress,
|
||||
textStyles,
|
||||
theme,
|
||||
} = this.props;
|
||||
const {attachments} = postProps;
|
||||
|
||||
if (attachments && attachments.length) {
|
||||
if (!MessageAttachments) {
|
||||
MessageAttachments = require('app/components/message_attachments').default;
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageAttachments
|
||||
attachments={attachments}
|
||||
baseTextStyle={baseTextStyle}
|
||||
blockStyles={blockStyles}
|
||||
deviceHeight={deviceHeight}
|
||||
deviceWidth={deviceWidth}
|
||||
metadata={metadata}
|
||||
postId={postId}
|
||||
textStyles={textStyles}
|
||||
theme={theme}
|
||||
onHashtagPress={onHashtagPress}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
renderOpenGraph = (isYouTube, isImage) => {
|
||||
const {isReplyPost, link, metadata, openGraphData, showLinkPreviews, theme} = this.props;
|
||||
|
||||
if (isYouTube || (isImage && !openGraphData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attachments = this.renderMessageAttachment();
|
||||
if (attachments) {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
if (!openGraphData && metadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (link && showLinkPreviews) {
|
||||
if (!PostAttachmentOpenGraph) {
|
||||
PostAttachmentOpenGraph = require('app/components/post_attachment_opengraph').default;
|
||||
}
|
||||
|
||||
return (
|
||||
<PostAttachmentOpenGraph
|
||||
isReplyPost={isReplyPost}
|
||||
link={link}
|
||||
openGraphData={openGraphData}
|
||||
imagesMetadata={metadata && metadata.images}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
renderYouTubeVideo = (link) => {
|
||||
const videoId = getYouTubeVideoId(link);
|
||||
const dimensions = calculateDimensions(
|
||||
MAX_YOUTUBE_IMAGE_HEIGHT,
|
||||
MAX_YOUTUBE_IMAGE_WIDTH,
|
||||
getViewPortWidth(this.props.isReplyPost, this.hasPermanentSidebar()),
|
||||
);
|
||||
const imgUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
style={[styles.imageContainer, {height: dimensions.height}]}
|
||||
onPress={this.playYouTubeVideo}
|
||||
type={'opacity'}
|
||||
>
|
||||
<ProgressiveImage
|
||||
isBackgroundImage={true}
|
||||
imageUri={imgUrl}
|
||||
style={[styles.image, dimensions]}
|
||||
resizeMode='cover'
|
||||
onError={this.handleLinkLoadError}
|
||||
>
|
||||
<TouchableWithFeedback
|
||||
style={styles.playButton}
|
||||
onPress={this.playYouTubeVideo}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Image
|
||||
source={require('assets/images/icons/youtube-play-icon.png')}
|
||||
onPress={this.playYouTubeVideo}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
</ProgressiveImage>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
setImageSize = (uri, originalWidth, originalHeight) => {
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!originalWidth && !originalHeight) {
|
||||
this.setState({linkLoadError: true});
|
||||
return;
|
||||
}
|
||||
|
||||
const {isReplyPost, link} = this.props;
|
||||
const viewPortWidth = getViewPortWidth(isReplyPost, this.hasPermanentSidebar());
|
||||
|
||||
let dimensions;
|
||||
if (isYoutubeLink(link)) {
|
||||
dimensions = calculateDimensions(MAX_YOUTUBE_IMAGE_HEIGHT, MAX_YOUTUBE_IMAGE_WIDTH, viewPortWidth);
|
||||
} else {
|
||||
dimensions = calculateDimensions(originalHeight, originalWidth, viewPortWidth);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
...dimensions,
|
||||
originalHeight,
|
||||
originalWidth,
|
||||
linkLoaded: true,
|
||||
uri,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
let {link} = this.props;
|
||||
const {openGraphData, postProps, expandedLink} = this.props;
|
||||
|
|
@ -504,7 +473,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
return this.generateStaticEmbed(isYouTube, isImage && !linkLoadError);
|
||||
return this.renderOpenGraph(isYouTube, isImage && !linkLoadError);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Image, Platform, StyleSheet, View} from 'react-native';
|
||||
import {Platform, StyleSheet, View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import AppIcon from 'app/components/app_icon';
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
|
|
@ -72,7 +73,7 @@ export default class PostProfilePicture extends PureComponent {
|
|||
width: frameSize,
|
||||
}, style.buffer]}
|
||||
>
|
||||
<Image
|
||||
<FastImage
|
||||
source={icon}
|
||||
style={{
|
||||
height: pictureSize,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import CameraButton from './components/camera_button';
|
|||
|
||||
import PostTextbox from './post_textbox.ios';
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
jest.mock('react-native-image-picker', () => ({
|
||||
launchCamera: jest.fn(),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Image, Platform, View} from 'react-native';
|
||||
import {Platform, View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
|
@ -61,7 +62,10 @@ export default class ProfilePicture extends PureComponent {
|
|||
} else if (edit && imageUri) {
|
||||
this.setImageURL(imageUri);
|
||||
} else if (user) {
|
||||
this.setImageURL(Client4.getProfilePictureUrl(user.id, user.last_picture_update));
|
||||
const uri = Client4.getProfilePictureUrl(user.id, user.last_picture_update);
|
||||
FastImage.preload([{uri, headers: Client4.getOptions({}).headers}]);
|
||||
|
||||
this.setImageURL(uri);
|
||||
this.clearProfileImageUri();
|
||||
}
|
||||
}
|
||||
|
|
@ -100,6 +104,7 @@ export default class ProfilePicture extends PureComponent {
|
|||
|
||||
if (nextUrl && url !== nextUrl) {
|
||||
// empty function is so that promise unhandled is not triggered in dev mode
|
||||
FastImage.preload([{uri: nextUrl, headers: Client4.getOptions({}).headers}]);
|
||||
this.setImageURL(nextUrl);
|
||||
this.clearProfileImageUri();
|
||||
}
|
||||
|
|
@ -150,7 +155,7 @@ export default class ProfilePicture extends PureComponent {
|
|||
};
|
||||
|
||||
image = (
|
||||
<Image
|
||||
<FastImage
|
||||
key={pictureUrl}
|
||||
style={{width: this.props.size, height: this.props.size, borderRadius: this.props.size / 2}}
|
||||
source={source}
|
||||
|
|
@ -158,7 +163,7 @@ export default class ProfilePicture extends PureComponent {
|
|||
);
|
||||
} else {
|
||||
image = (
|
||||
<Image
|
||||
<FastImage
|
||||
style={{width: this.props.size, height: this.props.size, borderRadius: this.props.size / 2}}
|
||||
source={placeholder}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,109 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ProgressiveImage should match snapshot 1`] = `
|
||||
exports[`ProgressiveImage should match snapshot for BackgroundImage 1`] = `
|
||||
<View
|
||||
style={Object {}}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
},
|
||||
Object {},
|
||||
]
|
||||
}
|
||||
>
|
||||
<ImageBackground
|
||||
resizeMode="cover"
|
||||
source={
|
||||
Object {
|
||||
"uri": "https://images.com/image.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`ProgressiveImage should match snapshot for Default Image 1`] = `
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
},
|
||||
Object {},
|
||||
]
|
||||
}
|
||||
>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
blurRadius={5}
|
||||
onError={[MockFunction]}
|
||||
resizeMethod="auto"
|
||||
resizeMode="contain"
|
||||
source="https://images.com/image.png"
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
},
|
||||
undefined,
|
||||
null,
|
||||
]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`ProgressiveImage should match snapshot for Image 1`] = `
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
},
|
||||
Object {},
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,NaN)",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
onError={[MockFunction]}
|
||||
resizeMethod="auto"
|
||||
resizeMode="contain"
|
||||
source={
|
||||
Object {
|
||||
"testUri": "../../../dist/assets/images/thumb.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Array [
|
||||
undefined,
|
||||
Object {
|
||||
"opacity": 0.5,
|
||||
"tintColor": "#3d3c40",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
onError={[MockFunction]}
|
||||
onLoadEnd={[Function]}
|
||||
resizeMethod="auto"
|
||||
|
|
@ -25,25 +123,11 @@ exports[`ProgressiveImage should match snapshot 1`] = `
|
|||
"top": 0,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
/>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"opacity": 0.8,
|
||||
"opacity": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</ForwardRef(AnimatedComponentWrapper)>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -3,23 +3,22 @@
|
|||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Animated, Image, ImageBackground, Platform, View, StyleSheet} from 'react-native';
|
||||
import {Animated, ImageBackground, View, StyleSheet} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import thumb from 'assets/images/thumb.png';
|
||||
|
||||
const AnimatedImageBackground = Animated.createAnimatedComponent(ImageBackground);
|
||||
const AnimatedImage = Animated.createAnimatedComponent(FastImage);
|
||||
const AnimatedFastImage = Animated.createAnimatedComponent(FastImage);
|
||||
|
||||
export default class ProgressiveImage extends PureComponent {
|
||||
static propTypes = {
|
||||
isBackgroundImage: PropTypes.bool,
|
||||
children: CustomPropTypes.Children,
|
||||
defaultSource: PropTypes.oneOfType([PropTypes.object, PropTypes.number]), // this should be provided by the component
|
||||
filename: PropTypes.string,
|
||||
imageUri: PropTypes.string,
|
||||
imageStyle: CustomPropTypes.Style,
|
||||
onError: PropTypes.func,
|
||||
|
|
@ -27,90 +26,35 @@ export default class ProgressiveImage extends PureComponent {
|
|||
resizeMode: PropTypes.string,
|
||||
style: CustomPropTypes.Style,
|
||||
theme: PropTypes.object.isRequired,
|
||||
thumbnailUri: PropTypes.string,
|
||||
tintDefaultSource: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
style: {},
|
||||
resizeMode: 'contain',
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.subscribedToCache = true;
|
||||
|
||||
this.state = {
|
||||
intensity: new Animated.Value(80),
|
||||
thumb: null,
|
||||
uri: null,
|
||||
failedImageLoad: false,
|
||||
intensity: new Animated.Value(0),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.load();
|
||||
onLoadImageEnd = () => {
|
||||
Animated.timing(this.state.intensity, {
|
||||
duration: 300,
|
||||
toValue: 100,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.props.filename !== prevProps.filename ||
|
||||
this.props.imageUri !== prevProps.imageUri ||
|
||||
this.props.thumbnailUri !== prevProps.thumbnailUri) {
|
||||
this.load();
|
||||
}
|
||||
|
||||
const {intensity, thumb, uri} = this.state;
|
||||
if (uri && thumb && uri !== thumb && prevState.uri !== uri) {
|
||||
Animated.timing(intensity, {
|
||||
duration: 300,
|
||||
toValue: 0,
|
||||
useNativeDriver: Platform.OS === 'android',
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscribedToCache = false;
|
||||
}
|
||||
|
||||
load = () => {
|
||||
const {imageUri, thumbnailUri} = this.props;
|
||||
|
||||
if (thumbnailUri) {
|
||||
this.setThumbnail(thumbnailUri);
|
||||
} else if (imageUri) {
|
||||
this.setImage(imageUri);
|
||||
}
|
||||
};
|
||||
|
||||
loadFullImage = () => {
|
||||
if (!this.state.uri) {
|
||||
const {imageUri} = this.props;
|
||||
this.setImage(imageUri);
|
||||
}
|
||||
}
|
||||
|
||||
setImage = (uri) => {
|
||||
if (this.subscribedToCache) {
|
||||
this.setState({uri});
|
||||
}
|
||||
};
|
||||
|
||||
setThumbnail = (thumb) => {
|
||||
if (this.subscribedToCache) {
|
||||
if (!thumb && !this.state.failedImageLoad) {
|
||||
this.load();
|
||||
this.setState({failedImageLoad: true});
|
||||
} else {
|
||||
this.setState({thumb, uri: null});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
defaultSource,
|
||||
imageStyle,
|
||||
imageUri,
|
||||
isBackgroundImage,
|
||||
onError,
|
||||
resizeMode,
|
||||
|
|
@ -119,15 +63,6 @@ export default class ProgressiveImage extends PureComponent {
|
|||
theme,
|
||||
tintDefaultSource,
|
||||
} = this.props;
|
||||
const {uri, intensity, thumb} = this.state;
|
||||
const hasDefaultSource = Boolean(defaultSource);
|
||||
const hasPreview = Boolean(thumb);
|
||||
const hasURI = Boolean(uri);
|
||||
const isImageReady = uri && uri !== thumb;
|
||||
const opacity = intensity.interpolate({
|
||||
inputRange: [50, 100],
|
||||
outputRange: [0.5, 1],
|
||||
});
|
||||
|
||||
let DefaultComponent;
|
||||
let ImageComponent;
|
||||
|
|
@ -135,20 +70,33 @@ export default class ProgressiveImage extends PureComponent {
|
|||
DefaultComponent = ImageBackground;
|
||||
ImageComponent = AnimatedImageBackground;
|
||||
} else {
|
||||
DefaultComponent = Image;
|
||||
ImageComponent = AnimatedImage;
|
||||
DefaultComponent = Animated.Image;
|
||||
ImageComponent = AnimatedFastImage;
|
||||
}
|
||||
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
let defaultImage;
|
||||
if (hasDefaultSource && tintDefaultSource) {
|
||||
defaultImage = (
|
||||
<View style={styles.defaultImageContainer}>
|
||||
if (isBackgroundImage) {
|
||||
return (
|
||||
<View style={[styles.defaultImageContainer, style]}>
|
||||
<DefaultComponent
|
||||
source={{uri: imageUri}}
|
||||
resizeMode={'cover'}
|
||||
style={[StyleSheet.absoluteFill, imageStyle]}
|
||||
>
|
||||
{this.props.children}
|
||||
</DefaultComponent>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (defaultSource) {
|
||||
return (
|
||||
<View style={[styles.defaultImageContainer, style]}>
|
||||
<DefaultComponent
|
||||
source={defaultSource}
|
||||
style={styles.defaultImageTint}
|
||||
resizeMode='center'
|
||||
style={[StyleSheet.absoluteFill, imageStyle, tintDefaultSource ? styles.defaultImageTint : null]}
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
onError={onError}
|
||||
>
|
||||
|
|
@ -156,57 +104,41 @@ export default class ProgressiveImage extends PureComponent {
|
|||
</DefaultComponent>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
defaultImage = (
|
||||
}
|
||||
|
||||
const opacity = this.state.intensity.interpolate({
|
||||
inputRange: [20, 100],
|
||||
outputRange: [0.2, 1],
|
||||
});
|
||||
const defaultOpacity = this.state.intensity.interpolate({
|
||||
inputRange: [0, 100],
|
||||
outputRange: [0.5, 0],
|
||||
});
|
||||
|
||||
const containerStyle = {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, defaultOpacity),
|
||||
};
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.defaultImageContainer, style, containerStyle]}>
|
||||
<DefaultComponent
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
onError={onError}
|
||||
source={defaultSource}
|
||||
style={[StyleSheet.absoluteFill, imageStyle]}
|
||||
>
|
||||
{this.props.children}
|
||||
</DefaultComponent>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasDefaultSource && !hasPreview && !hasURI) {
|
||||
return (
|
||||
<View style={style}>
|
||||
{defaultImage}
|
||||
{hasPreview &&
|
||||
<Animated.View style={[StyleSheet.absoluteFill, {backgroundColor: theme.centerChannelBg, opacity}]}/>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let source;
|
||||
if (hasPreview && !isImageReady) {
|
||||
source = {uri: thumb};
|
||||
} else if (isImageReady) {
|
||||
source = {uri};
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
{source &&
|
||||
source={thumb}
|
||||
style={[imageStyle, {tintColor: theme.centerChannelColor, opacity: defaultOpacity}]}
|
||||
/>
|
||||
<ImageComponent
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
onError={onError}
|
||||
source={source}
|
||||
style={[StyleSheet.absoluteFill, imageStyle]}
|
||||
blurRadius={isImageReady ? null : 5}
|
||||
onLoadEnd={this.loadFullImage}
|
||||
source={{uri: imageUri}}
|
||||
style={[StyleSheet.absoluteFill, imageStyle, {opacity}]}
|
||||
onLoadEnd={this.onLoadImageEnd}
|
||||
>
|
||||
{this.props.children}
|
||||
</ImageComponent>
|
||||
}
|
||||
{hasPreview &&
|
||||
<Animated.View style={[StyleSheet.absoluteFill, {backgroundColor: theme.centerChannelBg, opacity}]}/>
|
||||
}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -214,10 +146,6 @@ export default class ProgressiveImage extends PureComponent {
|
|||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
defaultImageContainer: {
|
||||
flex: 1,
|
||||
position: 'absolute',
|
||||
height: 80,
|
||||
width: 80,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,74 +15,48 @@ jest.mock('react-native-fast-image', () => ({
|
|||
jest.useFakeTimers();
|
||||
|
||||
describe('ProgressiveImage', () => {
|
||||
const baseProps = {
|
||||
isBackgroundImage: false,
|
||||
defaultSource: 0,
|
||||
filename: 'defaultImage',
|
||||
imageUri: 'https://images.com/image.png',
|
||||
onError: jest.fn(),
|
||||
resizeMethod: 'auto',
|
||||
resizeMode: 'contain',
|
||||
theme: Preferences.THEMES.default,
|
||||
thumbnailUri: 'https://images.com/image.png',
|
||||
tintDefaultSource: false,
|
||||
};
|
||||
test('should match snapshot for Image', () => {
|
||||
const baseProps = {
|
||||
isBackgroundImage: false,
|
||||
imageUri: 'https://images.com/image.png',
|
||||
onError: jest.fn(),
|
||||
resizeMethod: 'auto',
|
||||
resizeMode: 'contain',
|
||||
theme: Preferences.THEMES.default,
|
||||
tintDefaultSource: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<ProgressiveImage {...baseProps}/>);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should load image', () => {
|
||||
test('should match snapshot for Default Image', () => {
|
||||
const baseProps = {
|
||||
isBackgroundImage: false,
|
||||
defaultSource: 'https://images.com/image.png',
|
||||
onError: jest.fn(),
|
||||
resizeMethod: 'auto',
|
||||
resizeMode: 'contain',
|
||||
theme: Preferences.THEMES.default,
|
||||
tintDefaultSource: false,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<ProgressiveImage {...baseProps}/>);
|
||||
const instance = wrapper.instance();
|
||||
jest.spyOn(instance, 'load');
|
||||
|
||||
// should load image on componentDidMount
|
||||
instance.componentDidMount();
|
||||
expect(instance.load).toHaveBeenCalledTimes(1);
|
||||
|
||||
// should not re-load image on componentDidUpdate with same props
|
||||
instance.componentDidUpdate(baseProps);
|
||||
expect(instance.load).toHaveBeenCalledTimes(1);
|
||||
|
||||
// should re-load image on componentDidUpdate when props changed
|
||||
wrapper.setProps({filename: 'newImage'});
|
||||
expect(instance.load).toHaveBeenCalledTimes(2);
|
||||
|
||||
wrapper.setProps({imageUri: 'https://images.com/new_image.png'});
|
||||
expect(instance.load).toHaveBeenCalledTimes(3);
|
||||
|
||||
wrapper.setProps({thumbnailUri: 'https://images.com/new_image.png'});
|
||||
expect(instance.load).toHaveBeenCalledTimes(4);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should set image when imageUri is set but not the thumbnailUri', () => {
|
||||
const wrapper = shallow(
|
||||
<ProgressiveImage
|
||||
{...baseProps}
|
||||
thumbnailUri={null}
|
||||
/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
jest.spyOn(instance, 'setImage');
|
||||
test('should match snapshot for BackgroundImage', () => {
|
||||
const baseProps = {
|
||||
isBackgroundImage: true,
|
||||
imageUri: 'https://images.com/image.png',
|
||||
onError: jest.fn(),
|
||||
resizeMethod: 'auto',
|
||||
resizeMode: 'contain',
|
||||
theme: Preferences.THEMES.default,
|
||||
tintDefaultSource: false,
|
||||
};
|
||||
|
||||
instance.componentDidMount();
|
||||
jest.runAllTimers();
|
||||
expect(instance.setImage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should set thumbnail when thumbnailUri and imageUri are set', () => {
|
||||
const wrapper = shallow(
|
||||
<ProgressiveImage
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
jest.spyOn(instance, 'setThumbnail');
|
||||
instance.load();
|
||||
|
||||
jest.runAllTimers();
|
||||
expect(instance.setThumbnail).toHaveBeenCalledTimes(1);
|
||||
const wrapper = shallow(<ProgressiveImage {...baseProps}/>);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -3,12 +3,8 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Image,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {Text, View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
|
|
@ -39,7 +35,7 @@ export default class TeamIcon extends React.PureComponent {
|
|||
this.mounted = true;
|
||||
|
||||
if (lastIconUpdate) {
|
||||
this.setImageURL(Client4.getTeamIconUrl(teamId, lastIconUpdate));
|
||||
this.preloadTeamIcon(teamId, lastIconUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +44,7 @@ export default class TeamIcon extends React.PureComponent {
|
|||
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.preloadTeamIcon(teamId, lastIconUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +52,12 @@ export default class TeamIcon extends React.PureComponent {
|
|||
this.mounted = false;
|
||||
}
|
||||
|
||||
preloadTeamIcon = (teamId, lastIconUpdate) => {
|
||||
const uri = Client4.getTeamIconUrl(teamId, lastIconUpdate);
|
||||
FastImage.preload([{uri, headers: Client4.getOptions({}).headers}]);
|
||||
this.setImageURL(uri);
|
||||
};
|
||||
|
||||
setImageURL = (teamIcon) => {
|
||||
if (this.mounted) {
|
||||
this.setState({imageError: false, teamIcon});
|
||||
|
|
@ -76,7 +78,7 @@ export default class TeamIcon extends React.PureComponent {
|
|||
let teamIconContent;
|
||||
if (this.state.teamIcon) {
|
||||
teamIconContent = (
|
||||
<Image
|
||||
<FastImage
|
||||
style={[styles.image, styleImage]}
|
||||
source={{uri: this.state.teamIcon}}
|
||||
onError={() => this.setState({imageError: true})}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ import {
|
|||
ActivityIndicator,
|
||||
Animated,
|
||||
AppState,
|
||||
Image,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import Slider from 'react-native-slider';
|
||||
|
||||
import fullscreenImage from 'assets/images/video_player/fullscreen.png';
|
||||
|
|
@ -150,8 +150,7 @@ export default class VideoControls extends PureComponent {
|
|||
renderControls() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.controlsRow}/>
|
||||
<View style={[styles.controlsRow]}>
|
||||
<View style={[styles.controlsRow, StyleSheet.absoluteFill]}>
|
||||
{
|
||||
this.props.isLoading ? this.setLoadingView() : this.setPlayerControls(this.props.playerState)
|
||||
}
|
||||
|
|
@ -182,7 +181,10 @@ export default class VideoControls extends PureComponent {
|
|||
style={styles.fullScreenContainer}
|
||||
onPress={this.props.onFullScreen}
|
||||
>
|
||||
<Image source={fullscreenImage}/>
|
||||
<FastImage
|
||||
source={fullscreenImage}
|
||||
style={{width: 20, height: 20}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -221,7 +223,7 @@ export default class VideoControls extends PureComponent {
|
|||
style={[styles.playButton, {backgroundColor: this.props.mainColor}]}
|
||||
onPress={pressAction}
|
||||
>
|
||||
<Image
|
||||
<FastImage
|
||||
source={icon}
|
||||
style={styles.playIcon}
|
||||
/>
|
||||
|
|
@ -279,7 +281,6 @@ const styles = StyleSheet.create({
|
|||
right: 0,
|
||||
},
|
||||
controlsRow: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
alignSelf: 'stretch',
|
||||
|
|
@ -307,9 +308,11 @@ const styles = StyleSheet.create({
|
|||
resizeMode: 'stretch',
|
||||
},
|
||||
progressContainer: {
|
||||
position: 'absolute',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
marginBottom: -25,
|
||||
bottom: 25,
|
||||
marginLeft: 16,
|
||||
},
|
||||
progressColumnContainer: {
|
||||
flex: 1,
|
||||
|
|
@ -318,7 +321,8 @@ const styles = StyleSheet.create({
|
|||
alignSelf: 'stretch',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingLeft: 20,
|
||||
paddingLeft: 10,
|
||||
paddingTop: 8,
|
||||
},
|
||||
progressSlider: {
|
||||
alignSelf: 'stretch',
|
||||
|
|
|
|||
|
|
@ -21,4 +21,5 @@ export default {
|
|||
IS_TABLET: DeviceInfo.isTablet(),
|
||||
VIDEOS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Videos`,
|
||||
PERMANENT_SIDEBAR_SETTINGS: '@PERMANENT_SIDEBAR_SETTINGS',
|
||||
TABLET_WIDTH: 250,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,3 +3,6 @@
|
|||
|
||||
export const IMAGE_MAX_HEIGHT = 350;
|
||||
export const IMAGE_MIN_DIMENSION = 50;
|
||||
export const MAX_GIF_SIZE = 100 * 1024 * 1024;
|
||||
export const VIEWPORT_IMAGE_OFFSET = 70;
|
||||
export const VIEWPORT_IMAGE_REPLY_OFFSET = 11;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import CustomPropTypes from './custom_prop_types';
|
||||
import DeepLinkTypes from './deep_linking';
|
||||
import DeviceTypes from './device';
|
||||
import ListTypes from './list';
|
||||
|
|
@ -10,6 +11,7 @@ import ViewTypes, {UpgradeTypes} from './view';
|
|||
import WebsocketEvents from './websocket';
|
||||
|
||||
export {
|
||||
CustomPropTypes,
|
||||
DeepLinkTypes,
|
||||
DeviceTypes,
|
||||
ListTypes,
|
||||
|
|
|
|||
|
|
@ -6,13 +6,11 @@ import AsyncStorage from '@react-native-community/async-storage';
|
|||
import CookieManager from 'react-native-cookies';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
import semver from 'semver/preload';
|
||||
|
||||
import {setAppState, setServerVersion} from '@mm-redux/actions/general';
|
||||
import {autoUpdateTimezone} from '@mm-redux/actions/timezone';
|
||||
import {close as closeWebSocket} from '@actions/websocket';
|
||||
import {GeneralTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import {captureJSException} from '@utils/sentry';
|
|||
|
||||
const init = async () => {
|
||||
const credentials = await getAppCredentials();
|
||||
const dt = Date.now();
|
||||
const MMKVStorage = await getStorage();
|
||||
|
||||
const {store} = configureStore(MMKVStorage);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import assert from 'assert';
|
|||
import nock from 'nock';
|
||||
|
||||
import * as BotActions from '@mm-redux/actions/bots';
|
||||
import * as UserActions from '@mm-redux/actions/users';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
|
|
|
|||
|
|
@ -1644,7 +1644,7 @@ describe('Actions.Channels', () => {
|
|||
post('/channels/search').
|
||||
reply(200, [TestHelper.basicChannel, userChannel]);
|
||||
|
||||
const {data} = await store.dispatch(Actions.searchAllChannels('test', 0));
|
||||
await store.dispatch(Actions.searchAllChannels('test', 0));
|
||||
|
||||
const moreRequest = store.getState().requests.channels.getAllChannels;
|
||||
if (moreRequest.status === RequestStatus.FAILURE) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import nock from 'nock';
|
|||
|
||||
import * as Actions from '@mm-redux/actions/files';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {RequestStatus} from '../constants';
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import nock from 'nock';
|
|||
import * as Actions from '@mm-redux/actions/groups';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import {RequestStatus, Groups} from '../constants';
|
||||
import {Groups} from '../constants';
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import assert from 'assert';
|
||||
|
||||
import {UserTypes} from '@mm-redux/action_types';
|
||||
import {forceLogoutIfNecessary} from '@mm-redux/actions/helpers';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {ClientError} from '@mm-redux/client/client4';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import nock from 'nock';
|
|||
import * as Actions from '@mm-redux/actions/preferences';
|
||||
import {login} from '@mm-redux/actions/users';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {Preferences, RequestStatus} from '../constants';
|
||||
import {Preferences} from '../constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
|
|
|||
|
|
@ -747,7 +747,7 @@ describe('Actions.Teams', () => {
|
|||
post('/teams/search').
|
||||
reply(200, [TestHelper.basicTeam, userTeam]);
|
||||
|
||||
const {data} = await store.dispatch(Actions.searchTeams('test', 0));
|
||||
await store.dispatch(Actions.searchTeams('test', 0));
|
||||
|
||||
const moreRequest = store.getState().requests.teams.getTeams;
|
||||
if (moreRequest.status === RequestStatus.FAILURE) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import {IncomingWebhook, OutgoingWebhook, Command, OAuthApp, DialogSubmission} f
|
|||
import {CustomEmoji} from '@mm-redux/types/emojis';
|
||||
import {Config} from '@mm-redux/types/config';
|
||||
import {Bot, BotPatch} from '@mm-redux/types/bots';
|
||||
import {Dictionary} from '@mm-redux/types/utilities';
|
||||
import {SyncablePatch} from '@mm-redux/types/groups';
|
||||
|
||||
const FormData = require('form-data');
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import {CategoryTypes} from '../../constants/channel_categories';
|
||||
|
||||
import {ChannelCategoryTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {TeamTypes} from '@mm-redux/action_types';
|
||||
|
||||
import * as Reducers from './channel_categories';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ChannelTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {ChannelTypes} from '@mm-redux/action_types';
|
||||
import deepFreeze from '@mm-redux/utils/deep_freeze';
|
||||
|
||||
import channelsReducer, * as Reducers from './channels';
|
||||
|
|
|
|||
|
|
@ -3,11 +3,7 @@
|
|||
|
||||
import assert from 'assert';
|
||||
|
||||
import {
|
||||
PostTypes,
|
||||
SearchTypes,
|
||||
UserTypes,
|
||||
} from '@mm-redux/action_types';
|
||||
import {PostTypes, SearchTypes} from '@mm-redux/action_types';
|
||||
import reducer from '@mm-redux/reducers/entities/search';
|
||||
|
||||
describe('reducers.entities.search', () => {
|
||||
|
|
|
|||
|
|
@ -680,7 +680,6 @@ describe('makeFilterManuallyClosedDMs', () => {
|
|||
const currentUser = {id: 'currentUser'};
|
||||
const otherUser1 = {id: 'otherUser1'};
|
||||
const otherUser2 = {id: 'otherUser2'};
|
||||
const otherUser3 = {id: 'otherUser3'};
|
||||
|
||||
test('should filter DMs based on preferences', () => {
|
||||
const filterManuallyClosedDMs = Selectors.makeFilterManuallyClosedDMs();
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze';
|
|||
import TestHelper from 'test/test_helper';
|
||||
import {sortChannelsByDisplayName, getDirectChannelName} from '@mm-redux/utils/channel_utils';
|
||||
import * as Selectors from '@mm-redux/selectors/entities/channels';
|
||||
import * as TeamSelectors from '@mm-redux/selectors/entities/teams';
|
||||
import * as PreferencesSelectors from '@mm-redux/selectors/entities/preferences';
|
||||
import {General, Preferences, Permissions} from '../../constants';
|
||||
|
||||
const sortUsernames = (a, b) => a.localeCompare(b, General.DEFAULT_LOCALE, {numeric: true});
|
||||
|
|
|
|||
|
|
@ -20,19 +20,6 @@ describe('user utils', () => {
|
|||
first_name: 'test',
|
||||
last_name: 'user',
|
||||
};
|
||||
const testUser1 = {
|
||||
id: 'test_user_id',
|
||||
username: 'username',
|
||||
first_name: 'First',
|
||||
last_name: 'Last',
|
||||
};
|
||||
const testUser2 = {
|
||||
id: 'test_user_id_2',
|
||||
username: 'username2',
|
||||
first_name: 'First2',
|
||||
last_name: 'Last2',
|
||||
nickname: 'nick2',
|
||||
};
|
||||
it('should return username', () => {
|
||||
assert.equal(displayUsername(userObj, 'UNKNOWN_PREFERENCE'), 'testUser');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {combineReducers} from 'redux';
|
||||
import {UserTypes} from '@mm-redux/action_types';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ import {
|
|||
import {Navigation} from 'react-native-navigation';
|
||||
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
|
||||
|
||||
import {RequestStatus} from '@mm-redux/constants';
|
||||
|
||||
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
|
||||
import ErrorText from 'app/components/error_text';
|
||||
import Loading from 'app/components/loading';
|
||||
|
|
|
|||
|
|
@ -154,8 +154,9 @@ exports[`ImagePreview should match snapshot 1`] = `
|
|||
"opacity": 1,
|
||||
},
|
||||
Object {
|
||||
"height": 64,
|
||||
"justifyContent": "center",
|
||||
"maxHeight": 64,
|
||||
"minHeight": 32,
|
||||
"overflow": "hidden",
|
||||
"position": "absolute",
|
||||
"width": "100%",
|
||||
|
|
@ -186,10 +187,10 @@ exports[`ImagePreview should match snapshot 1`] = `
|
|||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"height": 64,
|
||||
"justifyContent": "flex-end",
|
||||
"left": 0,
|
||||
"paddingBottom": 5,
|
||||
"maxHeight": 64,
|
||||
"paddingBottom": 0,
|
||||
"paddingHorizontal": 24,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const {VIDEOS_PATH} = DeviceTypes;
|
|||
const {View: AnimatedView} = Animated;
|
||||
const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView);
|
||||
const HEADER_HEIGHT = 48;
|
||||
const ANIM_CONFIG = {duration: 300};
|
||||
const ANIM_CONFIG = {duration: 300, userNativeDriver: true};
|
||||
|
||||
export default class ImagePreview extends PureComponent {
|
||||
static propTypes = {
|
||||
|
|
@ -375,11 +375,12 @@ export default class ImagePreview extends PureComponent {
|
|||
const flattenStyle = StyleSheet.flatten(style);
|
||||
const calculatedDimensions = calculateDimensions(height, width, deviceWidth, deviceHeight - statusBar);
|
||||
const imageStyle = {...flattenStyle, ...calculatedDimensions};
|
||||
const src = {...source, cache: FastImage.cacheControl.cacheOnly};
|
||||
|
||||
return (
|
||||
<View style={[style, {justifyContent: 'center', alignItems: 'center'}]}>
|
||||
<FastImage
|
||||
source={source}
|
||||
source={src}
|
||||
style={imageStyle}
|
||||
/>
|
||||
</View>
|
||||
|
|
@ -637,7 +638,8 @@ const style = StyleSheet.create({
|
|||
textAlign: 'center',
|
||||
},
|
||||
footerContainer: {
|
||||
height: 64,
|
||||
minHeight: 32,
|
||||
maxHeight: 64,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
|
|
@ -648,10 +650,10 @@ const style = StyleSheet.create({
|
|||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 64,
|
||||
maxHeight: 64,
|
||||
justifyContent: 'flex-end',
|
||||
paddingHorizontal: 24,
|
||||
paddingBottom: 5,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
filename: {
|
||||
color: 'white',
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ import {bindActionCreators} from 'redux';
|
|||
import {connect} from 'react-redux';
|
||||
|
||||
import {loadFromPushNotification} from 'app/actions/views/root';
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
|
||||
import {getChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getTeammateNameDisplaySetting} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getUser} from '@mm-redux/selectors/entities/users';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
|
||||
|
|
@ -16,7 +15,6 @@ import Notification from './notification';
|
|||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {data} = ownProps.notification;
|
||||
const {deviceWidth} = getDimensions(state);
|
||||
|
||||
let user;
|
||||
if (data.sender_id) {
|
||||
|
|
@ -31,10 +29,8 @@ function mapStateToProps(state, ownProps) {
|
|||
return {
|
||||
config,
|
||||
channel,
|
||||
deviceWidth,
|
||||
user,
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Image,
|
||||
InteractionManager,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
|
|
@ -12,6 +11,7 @@ import {
|
|||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import {PanGestureHandler} from 'react-native-gesture-handler';
|
||||
|
|
@ -41,10 +41,8 @@ export default class Notification extends PureComponent {
|
|||
componentId: PropTypes.string.isRequired,
|
||||
channel: PropTypes.object,
|
||||
config: PropTypes.object,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
notification: PropTypes.object.isRequired,
|
||||
teammateNameDisplay: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
user: PropTypes.object,
|
||||
};
|
||||
|
||||
|
|
@ -158,7 +156,7 @@ export default class Notification extends PureComponent {
|
|||
const {data} = notification;
|
||||
|
||||
let icon = (
|
||||
<Image
|
||||
<FastImage
|
||||
source={logo}
|
||||
style={style.icon}
|
||||
/>
|
||||
|
|
@ -168,7 +166,7 @@ export default class Notification extends PureComponent {
|
|||
const overrideIconURL = Client4.getAbsoluteUrl(data.override_icon_url); // eslint-disable-line camelcase
|
||||
const wsIcon = data.override_icon_url ? {uri: overrideIconURL} : webhookIcon;
|
||||
icon = (
|
||||
<Image
|
||||
<FastImage
|
||||
source={wsIcon}
|
||||
style={style.icon}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
export default class TableImage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
|
@ -81,7 +81,7 @@ export default class TableImage extends React.PureComponent {
|
|||
style={style.scrollContainer}
|
||||
contentContainerStyle={style.container}
|
||||
>
|
||||
<Image
|
||||
<FastImage
|
||||
style={[style.image, {width, height}]}
|
||||
source={{uri: this.props.imageSource}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
|
||||
export function getLastChannelForTeam(payload) {
|
||||
if (payload?.views?.team?.lastChannelForTeam) {
|
||||
const lastChannelForTeam = {...payload.views.team.lastChannelForTeam};
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ export default function messageRetention(store) {
|
|||
const build = DeviceInfo.getBuildNumber();
|
||||
const version = DeviceInfo.getVersion();
|
||||
const previousVersion = app?.version;
|
||||
const previousBuild = app?.build;
|
||||
|
||||
action.payload = {
|
||||
...action.payload,
|
||||
|
|
|
|||
|
|
@ -11,14 +11,10 @@ export default function createSentryMiddleware() {
|
|||
Sentry = require('@sentry/react-native');
|
||||
}
|
||||
|
||||
return (store) => { // eslint-disable-line no-unused-vars
|
||||
return (next) => {
|
||||
return (action) => {
|
||||
Sentry.addBreadcrumb(makeBreadcrumbFromAction(action));
|
||||
return () => (next) => (action) => {
|
||||
Sentry.addBreadcrumb(makeBreadcrumbFromAction(action));
|
||||
|
||||
return next(action);
|
||||
};
|
||||
};
|
||||
return next(action);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ export function waitForHydration(store, callback) {
|
|||
let executed = false; // this is to prevent a race condition when subcription runs before unsubscribed
|
||||
let state = store.getState();
|
||||
let root = state.views?.root;
|
||||
let persist = state._persist; //eslint-disable-line no-underscore-dangle
|
||||
|
||||
if (root?.hydrationComplete && !executed) {
|
||||
if (callback && typeof callback === 'function') {
|
||||
|
|
@ -59,7 +58,6 @@ export function waitForHydration(store, callback) {
|
|||
const subscription = () => {
|
||||
state = store.getState();
|
||||
root = state.views?.root;
|
||||
persist = state._persist; //eslint-disable-line no-underscore-dangle
|
||||
if (root?.hydrationComplete && !executed) {
|
||||
unsubscribeFromStore();
|
||||
if (callback && typeof callback === 'function') {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Keyboard} from 'react-native';
|
||||
import {Dimensions, Keyboard} from 'react-native';
|
||||
|
||||
import {showModalOverCurrentContext} from '@actions/navigation';
|
||||
import {DeviceTypes} from '@constants';
|
||||
import {
|
||||
IMAGE_MAX_HEIGHT,
|
||||
IMAGE_MIN_DIMENSION,
|
||||
} from 'app/constants/image';
|
||||
import {showModalOverCurrentContext} from 'app/actions/navigation';
|
||||
MAX_GIF_SIZE,
|
||||
VIEWPORT_IMAGE_OFFSET,
|
||||
VIEWPORT_IMAGE_REPLY_OFFSET,
|
||||
} from '@constants/image';
|
||||
|
||||
let previewComponents;
|
||||
|
||||
|
|
@ -58,6 +62,21 @@ export const calculateDimensions = (height, width, viewPortWidth = 0, viewPortHe
|
|||
};
|
||||
};
|
||||
|
||||
export function getViewPortWidth(isReplyPost, permanentSidebar = false) {
|
||||
const {width, height} = Dimensions.get('window');
|
||||
let portraitPostWidth = Math.min(width, height) - VIEWPORT_IMAGE_OFFSET;
|
||||
|
||||
if (permanentSidebar) {
|
||||
portraitPostWidth -= DeviceTypes.TABLET_WIDTH;
|
||||
}
|
||||
|
||||
if (isReplyPost) {
|
||||
portraitPostWidth -= VIEWPORT_IMAGE_REPLY_OFFSET;
|
||||
}
|
||||
|
||||
return portraitPostWidth;
|
||||
}
|
||||
|
||||
export function previewImageAtIndex(components, index, files) {
|
||||
previewComponents = components;
|
||||
const component = components[index];
|
||||
|
|
@ -94,8 +113,6 @@ function getItemMeasures(index, cb) {
|
|||
});
|
||||
}
|
||||
|
||||
const MAX_GIF_SIZE = 100 * 1024 * 1024;
|
||||
|
||||
// isGifTooLarge returns true if we think that the GIF may cause the device to run out of memory when rendered
|
||||
// based on the image's dimensions and frame count.
|
||||
export function isGifTooLarge(imageMetadata) {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class PushNotificationUtils {
|
|||
};
|
||||
|
||||
onPushNotification = async (deviceNotification) => {
|
||||
const {dispatch, getState} = Store.redux;
|
||||
const {dispatch} = Store.redux;
|
||||
const {data, foreground, message, userInteraction} = deviceNotification;
|
||||
const notification = {
|
||||
data,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ module.exports = {
|
|||
alias: {
|
||||
assets: './dist/assets',
|
||||
'@actions': './app/actions',
|
||||
'@components': './app/components',
|
||||
'@constants': './app/constants',
|
||||
'@i18n': './app/i18n',
|
||||
'@init': './app/init',
|
||||
|
|
|
|||
1
index.js
1
index.js
|
|
@ -21,6 +21,7 @@ if (__DEV__) {
|
|||
'Warning: componentWillReceiveProps',
|
||||
'Warning: StatusBarIOS',
|
||||
'`-[RCTRootView cancelTouches]`',
|
||||
'Animated',
|
||||
|
||||
// Hide warnings caused by React Native (https://github.com/facebook/react-native/issues/20841)
|
||||
'Require cycle: node_modules/react-native/Libraries/Network/fetch.js',
|
||||
|
|
|
|||
|
|
@ -550,7 +550,7 @@ SPEC CHECKSUMS:
|
|||
jail-monkey: d7c5048b2336f22ee9c9e0efa145f1f917338ea9
|
||||
libwebp: 946cb3063cea9236285f7e9a8505d806d30e07f3
|
||||
MMKV: 7bb6c30f9ff2ea45bc2398c86e66dd1cb63cfe20
|
||||
MMKVCore: f1e0aad4fc330e7cbfbdff16720017bf0973db5a
|
||||
MMKVCore: f455d4bf01bb5257511c1062fdbc3d7ebdec216b
|
||||
Permission-Camera: 8f0e5decca5f28f70f28a8dc31f012c9bad40ad8
|
||||
Permission-PhotoLibrary: b209bf23b784c9e1409a57d81c6d11ab1d3079c1
|
||||
RCTRequired: cec6a34b3ac8a9915c37e7e4ad3aa74726ce4035
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageCookieJar.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageCookieJar.java
|
||||
new file mode 100644
|
||||
index 0000000..f2306f6
|
||||
index 0000000..a302394
|
||||
--- /dev/null
|
||||
+++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageCookieJar.java
|
||||
@@ -0,0 +1,38 @@
|
||||
@@ -0,0 +1,45 @@
|
||||
+package com.dylanvann.fastimage;
|
||||
+
|
||||
+import android.webkit.CookieManager;
|
||||
+
|
||||
+import java.util.Collections;
|
||||
+import java.util.LinkedList;
|
||||
+import java.util.List;
|
||||
+
|
||||
+import okhttp3.Cookie;
|
||||
|
|
@ -39,7 +40,13 @@ index 0000000..f2306f6
|
|||
+ return Collections.emptyList();
|
||||
+ }
|
||||
+
|
||||
+ return Collections.singletonList(Cookie.parse(url, cookie));
|
||||
+ String[] pairs = cookie.split(";");
|
||||
+ List<Cookie> cookies = new LinkedList<Cookie>();
|
||||
+ for (String pair : pairs) {
|
||||
+ cookies.add(Cookie.parse(url, pair));
|
||||
+ }
|
||||
+
|
||||
+ return cookies;
|
||||
+ }
|
||||
+}
|
||||
diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchMyChannelsAndMembers} from '@mm-redux/actions/channels';
|
||||
import {getRedirectChannelNameForTeam, getChannelsNameMapInTeam} from '@mm-redux/selectors/entities/channels';
|
||||
import {getChannelByName} from '@mm-redux/utils/channel_utils';
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {intlShape} from 'react-intl';
|
|||
|
||||
import {
|
||||
Alert,
|
||||
Image,
|
||||
NativeModules,
|
||||
PermissionsAndroid,
|
||||
ScrollView,
|
||||
|
|
@ -17,6 +16,7 @@ import {
|
|||
TextInput,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
import Video from 'react-native-video';
|
||||
import LocalAuth from 'react-native-local-auth';
|
||||
|
|
@ -505,7 +505,7 @@ export default class ExtensionPost extends PureComponent {
|
|||
key={`item-${index}`}
|
||||
style={styles.imageContainer}
|
||||
>
|
||||
<Image
|
||||
<FastImage
|
||||
source={{uri: file.fullPath, isStatic: true}}
|
||||
resizeMode='cover'
|
||||
style={styles.image}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,15 @@ jest.doMock('react-native', () => {
|
|||
jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper');
|
||||
jest.mock('../node_modules/react-native/Libraries/EventEmitter/NativeEventEmitter');
|
||||
|
||||
jest.mock('react-native-cookies', () => ({
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
openURL: jest.fn(),
|
||||
canOpenURL: jest.fn(),
|
||||
getInitialURL: jest.fn(),
|
||||
clearAll: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-native-device-info', () => {
|
||||
return {
|
||||
getVersion: () => '0.0.0',
|
||||
|
|
@ -120,13 +129,37 @@ jest.mock('react-native-device-info', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('react-native-cookies', () => ({
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
openURL: jest.fn(),
|
||||
canOpenURL: jest.fn(),
|
||||
getInitialURL: jest.fn(),
|
||||
clearAll: jest.fn(),
|
||||
jest.mock('react-native-fast-image', () => {
|
||||
const FastImage = require.requireActual('react-native-fast-image').default;
|
||||
FastImage.preload = jest.fn();
|
||||
|
||||
return FastImage;
|
||||
});
|
||||
|
||||
jest.mock('rn-fetch-blob', () => ({
|
||||
fs: {
|
||||
dirs: {
|
||||
DocumentDir: () => jest.fn(),
|
||||
CacheDir: '/data/com.mattermost.beta/cache',
|
||||
},
|
||||
exists: jest.fn(),
|
||||
existsWithDiffExt: jest.fn(),
|
||||
unlink: jest.fn(),
|
||||
mv: jest.fn(),
|
||||
},
|
||||
fetch: jest.fn(),
|
||||
config: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('rn-fetch-blob/fs', () => ({
|
||||
dirs: {
|
||||
DocumentDir: () => jest.fn(),
|
||||
CacheDir: '/data/com.mattermost.beta/cache',
|
||||
},
|
||||
exists: jest.fn(),
|
||||
existsWithDiffExt: jest.fn(),
|
||||
unlink: jest.fn(),
|
||||
mv: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-native-navigation', () => {
|
||||
|
|
@ -200,32 +233,6 @@ beforeEach(() => {
|
|||
errors = [];
|
||||
});
|
||||
|
||||
jest.mock('rn-fetch-blob', () => ({
|
||||
fs: {
|
||||
dirs: {
|
||||
DocumentDir: () => jest.fn(),
|
||||
CacheDir: '/data/com.mattermost.beta/cache',
|
||||
},
|
||||
exists: jest.fn(),
|
||||
existsWithDiffExt: jest.fn(),
|
||||
unlink: jest.fn(),
|
||||
mv: jest.fn(),
|
||||
},
|
||||
fetch: jest.fn(),
|
||||
config: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('rn-fetch-blob/fs', () => ({
|
||||
dirs: {
|
||||
DocumentDir: () => jest.fn(),
|
||||
CacheDir: '/data/com.mattermost.beta/cache',
|
||||
},
|
||||
exists: jest.fn(),
|
||||
existsWithDiffExt: jest.fn(),
|
||||
unlink: jest.fn(),
|
||||
mv: jest.fn(),
|
||||
}));
|
||||
|
||||
global.requestAnimationFrame = (callback) => {
|
||||
setTimeout(callback, 0);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue