diff --git a/.eslintrc.json b/.eslintrc.json
index 9c0bc2547..4dc53db69 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -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,
diff --git a/app/actions/views/login.test.js b/app/actions/views/login.test.js
index dd2a6028f..e8bcf278c 100644
--- a/app/actions/views/login.test.js
+++ b/app/actions/views/login.test.js
@@ -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';
diff --git a/app/actions/views/root.js b/app/actions/views/root.js
index 188469b61..8825fdb33 100644
--- a/app/actions/views/root.js
+++ b/app/actions/views/root.js
@@ -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';
diff --git a/app/actions/views/user.js b/app/actions/views/user.js
index 3067977b0..233ca69a2 100644
--- a/app/actions/views/user.js
+++ b/app/actions/views/user.js
@@ -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;
diff --git a/app/actions/websocket.test.js b/app/actions/websocket.test.js
index 55f331bff..01ee420a2 100644
--- a/app/actions/websocket.test.js
+++ b/app/actions/websocket.test.js
@@ -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('');
diff --git a/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap b/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap
index 92df63048..d31af611a 100644
--- a/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap
+++ b/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap
@@ -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,
},
]
}
diff --git a/app/components/file_attachment_list/file_attachment_image.js b/app/components/file_attachment_list/file_attachment_image.js
index 1a5d2625d..9deccbbed 100644
--- a/app/components/file_attachment_list/file_attachment_image.js
+++ b/app/components/file_attachment_list/file_attachment_image.js
@@ -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 {
{
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())}
/>
);
@@ -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) {
diff --git a/app/components/file_upload_preview/file_upload_item/file_upload_item.js b/app/components/file_upload_preview/file_upload_item/file_upload_item.js
index d957cb818..2000149f0 100644
--- a/app/components/file_upload_preview/file_upload_item/file_upload_item.js
+++ b/app/components/file_upload_preview/file_upload_item/file_upload_item.js
@@ -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 {
);
@@ -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,
},
-}));
+});
diff --git a/app/components/image_viewport.js b/app/components/image_viewport.js
new file mode 100644
index 000000000..7a251780e
--- /dev/null
+++ b/app/components/image_viewport.js
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/app/components/markdown/markdown_image/index.js b/app/components/markdown/markdown_image/index.js
index 41dc8e0e0..bb8fa550e 100644
--- a/app/components/markdown/markdown_image/index.js
+++ b/app/components/markdown/markdown_image/index.js
@@ -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),
};
}
diff --git a/app/components/markdown/markdown_image/markdown_image.js b/app/components/markdown/markdown_image/markdown_image.js
index 7112a0dfa..71859a21f 100644
--- a/app/components/markdown/markdown_image/markdown_image.js
+++ b/app/components/markdown/markdown_image/markdown_image.js
@@ -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 = (
-
diff --git a/app/components/markdown/transform.test.js b/app/components/markdown/transform.test.js
index 1e83e9ed6..9797a3af8 100644
--- a/app/components/markdown/transform.test.js
+++ b/app/components/markdown/transform.test.js
@@ -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];
diff --git a/app/components/message_attachments/__snapshots__/attachment_footer.test.js.snap b/app/components/message_attachments/__snapshots__/attachment_footer.test.js.snap
index 5dc1bfdb1..1e202e71a 100644
--- a/app/components/message_attachments/__snapshots__/attachment_footer.test.js.snap
+++ b/app/components/message_attachments/__snapshots__/attachment_footer.test.js.snap
@@ -10,7 +10,7 @@ exports[`AttachmentFooter it matches snapshot when both footer and footer_icon a
}
}
>
-
- {
const {link} = this.props;
if (link && Linking.canOpenURL(link)) {
@@ -39,7 +48,7 @@ export default class AttachmentAuthor extends PureComponent {
return (
{Boolean(icon) &&
-
{Boolean(icon) &&
-
-
-
- {
// 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);
});
diff --git a/app/components/post_body_additional_content/post_body_additional_content.js b/app/components/post_body_additional_content/post_body_additional_content.js
index 3c291ae64..fd25bc920 100644
--- a/app/components/post_body_additional_content/post_body_additional_content.js
+++ b/app/components/post_body_additional_content/post_body_additional_content.js
@@ -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 (
-
- );
- }
-
- 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 (
-
-
-
-
-
-
-
- );
+ return this.renderYouTubeVideo(link);
}
if (isImage) {
- const imageMetadata = this.props.metadata?.images?.[link];
-
- return (
-
- );
+ 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 (
-
- );
- }
-
- 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 (
+
+ );
+ };
+
+ 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 (
+
+ );
+ }
+
+ 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 (
+
+ );
+ }
+
+ 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 (
+
+
+
+
+
+
+
+ );
+ }
+
+ 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);
}
}
diff --git a/app/components/post_profile_picture/post_profile_picture.js b/app/components/post_profile_picture/post_profile_picture.js
index 699979dac..7f7e95b08 100644
--- a/app/components/post_profile_picture/post_profile_picture.js
+++ b/app/components/post_profile_picture/post_profile_picture.js
@@ -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]}
>
- ({
launchCamera: jest.fn(),
}));
diff --git a/app/components/profile_picture/profile_picture.js b/app/components/profile_picture/profile_picture.js
index 7d0dc3995..883e6ef01 100644
--- a/app/components/profile_picture/profile_picture.js
+++ b/app/components/profile_picture/profile_picture.js
@@ -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 = (
-
diff --git a/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap b/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap
index cd2c28710..23f0b5da9 100644
--- a/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap
+++ b/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap
@@ -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`] = `
+
+
+`;
+
+exports[`ProgressiveImage should match snapshot for Default Image 1`] = `
+
+
+`;
+
+exports[`ProgressiveImage should match snapshot for Image 1`] = `
+
+
+
-
-
+
`;
diff --git a/app/components/progressive_image/progressive_image.js b/app/components/progressive_image/progressive_image.js
index 7aaecdae1..d74385c05 100644
--- a/app/components/progressive_image/progressive_image.js
+++ b/app/components/progressive_image/progressive_image.js
@@ -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 = (
-
+ if (isBackgroundImage) {
+ return (
+
+
+ {this.props.children}
+
+
+ );
+ }
+
+ if (defaultSource) {
+ return (
+
@@ -156,57 +104,41 @@ export default class ProgressiveImage extends PureComponent {
);
- } 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 (
+
- {this.props.children}
-
- );
- }
-
- if (hasDefaultSource && !hasPreview && !hasURI) {
- return (
-
- {defaultImage}
- {hasPreview &&
-
- }
-
- );
- }
-
- let source;
- if (hasPreview && !isImageReady) {
- source = {uri: thumb};
- } else if (isImageReady) {
- source = {uri};
- }
-
- return (
-
- {source &&
+ source={thumb}
+ style={[imageStyle, {tintColor: theme.centerChannelColor, opacity: defaultOpacity}]}
+ />
{this.props.children}
- }
- {hasPreview &&
-
- }
-
+
);
}
}
@@ -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',
},
diff --git a/app/components/progressive_image/progressive_image.test.js b/app/components/progressive_image/progressive_image.test.js
index dd82b2911..ead3f23ec 100644
--- a/app/components/progressive_image/progressive_image.test.js
+++ b/app/components/progressive_image/progressive_image.test.js
@@ -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();
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();
- 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(
- ,
- );
- 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(
- ,
- );
- const instance = wrapper.instance();
- jest.spyOn(instance, 'setThumbnail');
- instance.load();
-
- jest.runAllTimers();
- expect(instance.setThumbnail).toHaveBeenCalledTimes(1);
+ const wrapper = shallow();
+ expect(wrapper.getElement()).toMatchSnapshot();
});
});
\ No newline at end of file
diff --git a/app/components/team_icon/team_icon.js b/app/components/team_icon/team_icon.js
index f4e6122cc..7cdd5ad91 100644
--- a/app/components/team_icon/team_icon.js
+++ b/app/components/team_icon/team_icon.js
@@ -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 = (
- this.setState({imageError: true})}
diff --git a/app/components/video_controls.js b/app/components/video_controls.js
index abf35da5b..61ed42017 100644
--- a/app/components/video_controls.js
+++ b/app/components/video_controls.js
@@ -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 (
-
-
+
{
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}
>
-
+
@@ -221,7 +223,7 @@ export default class VideoControls extends PureComponent {
style={[styles.playButton, {backgroundColor: this.props.mainColor}]}
onPress={pressAction}
>
-
@@ -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',
diff --git a/app/constants/device.js b/app/constants/device.js
index a46bbc035..7cfd41a9b 100644
--- a/app/constants/device.js
+++ b/app/constants/device.js
@@ -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,
};
diff --git a/app/constants/image.js b/app/constants/image.js
index e5e61c557..448a80912 100644
--- a/app/constants/image.js
+++ b/app/constants/image.js
@@ -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;
diff --git a/app/constants/index.js b/app/constants/index.js
index 6678203b0..cdd661b49 100644
--- a/app/constants/index.js
+++ b/app/constants/index.js
@@ -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,
diff --git a/app/init/global_event_handler.js b/app/init/global_event_handler.js
index 429ed41a5..2ca97e36b 100644
--- a/app/init/global_event_handler.js
+++ b/app/init/global_event_handler.js
@@ -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';
diff --git a/app/mattermost.js b/app/mattermost.js
index 4c2d4f34d..1eee8ab33 100644
--- a/app/mattermost.js
+++ b/app/mattermost.js
@@ -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);
diff --git a/app/mm-redux/actions/bots.test.js b/app/mm-redux/actions/bots.test.js
index 7ff9b9a17..18e3010e8 100644
--- a/app/mm-redux/actions/bots.test.js
+++ b/app/mm-redux/actions/bots.test.js
@@ -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';
diff --git a/app/mm-redux/actions/channels.test.js b/app/mm-redux/actions/channels.test.js
index 4c339c354..a7e0f9675 100644
--- a/app/mm-redux/actions/channels.test.js
+++ b/app/mm-redux/actions/channels.test.js
@@ -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) {
diff --git a/app/mm-redux/actions/files.test.js b/app/mm-redux/actions/files.test.js
index 9f939071c..8c6188f55 100644
--- a/app/mm-redux/actions/files.test.js
+++ b/app/mm-redux/actions/files.test.js
@@ -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';
diff --git a/app/mm-redux/actions/groups.test.js b/app/mm-redux/actions/groups.test.js
index 185763c25..c36656fbf 100644
--- a/app/mm-redux/actions/groups.test.js
+++ b/app/mm-redux/actions/groups.test.js
@@ -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';
diff --git a/app/mm-redux/actions/helpers.test.js b/app/mm-redux/actions/helpers.test.js
index fa7b44160..9832c6b60 100644
--- a/app/mm-redux/actions/helpers.test.js
+++ b/app/mm-redux/actions/helpers.test.js
@@ -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';
diff --git a/app/mm-redux/actions/preferences.test.js b/app/mm-redux/actions/preferences.test.js
index a24dd3de1..5e5c92f19 100644
--- a/app/mm-redux/actions/preferences.test.js
+++ b/app/mm-redux/actions/preferences.test.js
@@ -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';
diff --git a/app/mm-redux/actions/teams.test.js b/app/mm-redux/actions/teams.test.js
index e631d4466..d6fa96720 100644
--- a/app/mm-redux/actions/teams.test.js
+++ b/app/mm-redux/actions/teams.test.js
@@ -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) {
diff --git a/app/mm-redux/client/client4.ts b/app/mm-redux/client/client4.ts
index 72550d020..0df5e0ba6 100644
--- a/app/mm-redux/client/client4.ts
+++ b/app/mm-redux/client/client4.ts
@@ -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');
diff --git a/app/mm-redux/reducers/entities/channel_categories.test.js b/app/mm-redux/reducers/entities/channel_categories.test.js
index 75c7eeb5d..a1d894d2c 100644
--- a/app/mm-redux/reducers/entities/channel_categories.test.js
+++ b/app/mm-redux/reducers/entities/channel_categories.test.js
@@ -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';
diff --git a/app/mm-redux/reducers/entities/channels.test.js b/app/mm-redux/reducers/entities/channels.test.js
index 21a710fec..e6d945fd4 100644
--- a/app/mm-redux/reducers/entities/channels.test.js
+++ b/app/mm-redux/reducers/entities/channels.test.js
@@ -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';
diff --git a/app/mm-redux/reducers/entities/search.test.js b/app/mm-redux/reducers/entities/search.test.js
index 509a52965..f0e474095 100644
--- a/app/mm-redux/reducers/entities/search.test.js
+++ b/app/mm-redux/reducers/entities/search.test.js
@@ -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', () => {
diff --git a/app/mm-redux/selectors/entities/channel_categories.test.js b/app/mm-redux/selectors/entities/channel_categories.test.js
index c70e5c295..3e7845700 100644
--- a/app/mm-redux/selectors/entities/channel_categories.test.js
+++ b/app/mm-redux/selectors/entities/channel_categories.test.js
@@ -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();
diff --git a/app/mm-redux/selectors/entities/channels.test.js b/app/mm-redux/selectors/entities/channels.test.js
index adaa2ef81..3fbf35318 100644
--- a/app/mm-redux/selectors/entities/channels.test.js
+++ b/app/mm-redux/selectors/entities/channels.test.js
@@ -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});
diff --git a/app/mm-redux/utils/user_utils.test.js b/app/mm-redux/utils/user_utils.test.js
index d6862a328..a07c4e118 100644
--- a/app/mm-redux/utils/user_utils.test.js
+++ b/app/mm-redux/utils/user_utils.test.js
@@ -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');
});
diff --git a/app/reducers/views/post.js b/app/reducers/views/post.js
index e805928b7..ade8b8212 100644
--- a/app/reducers/views/post.js
+++ b/app/reducers/views/post.js
@@ -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';
diff --git a/app/screens/edit_post/edit_post.js b/app/screens/edit_post/edit_post.js
index 7fc9e7a12..e2bf78617 100644
--- a/app/screens/edit_post/edit_post.js
+++ b/app/screens/edit_post/edit_post.js
@@ -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';
diff --git a/app/screens/image_preview/__snapshots__/image_preview.test.js.snap b/app/screens/image_preview/__snapshots__/image_preview.test.js.snap
index d53e2f2d4..2d36508bc 100644
--- a/app/screens/image_preview/__snapshots__/image_preview.test.js.snap
+++ b/app/screens/image_preview/__snapshots__/image_preview.test.js.snap
@@ -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,
diff --git a/app/screens/image_preview/image_preview.js b/app/screens/image_preview/image_preview.js
index 01cdd8181..629ea3a0d 100644
--- a/app/screens/image_preview/image_preview.js
+++ b/app/screens/image_preview/image_preview.js
@@ -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 (
@@ -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',
diff --git a/app/screens/notification/index.js b/app/screens/notification/index.js
index f651bbc3c..46f22496e 100644
--- a/app/screens/notification/index.js
+++ b/app/screens/notification/index.js
@@ -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),
};
}
diff --git a/app/screens/notification/notification.js b/app/screens/notification/notification.js
index 3338e7594..73f129ce1 100644
--- a/app/screens/notification/notification.js
+++ b/app/screens/notification/notification.js
@@ -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 = (
-
@@ -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 = (
-
diff --git a/app/screens/table_image/table_image.js b/app/screens/table_image/table_image.js
index bc8f64ea3..24429c25f 100644
--- a/app/screens/table_image/table_image.js
+++ b/app/screens/table_image/table_image.js
@@ -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}
>
-
diff --git a/app/store/middlewares/helpers.js b/app/store/middlewares/helpers.js
index 238a15f68..ca227731f 100644
--- a/app/store/middlewares/helpers.js
+++ b/app/store/middlewares/helpers.js
@@ -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};
diff --git a/app/store/middlewares/message_retention.js b/app/store/middlewares/message_retention.js
index 33b648667..bb4f61d0d 100644
--- a/app/store/middlewares/message_retention.js
+++ b/app/store/middlewares/message_retention.js
@@ -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,
diff --git a/app/store/middlewares/sentry.js b/app/store/middlewares/sentry.js
index d4f21efdb..c092494f9 100644
--- a/app/store/middlewares/sentry.js
+++ b/app/store/middlewares/sentry.js
@@ -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);
};
}
diff --git a/app/store/utils.js b/app/store/utils.js
index 231444568..b9ce94470 100644
--- a/app/store/utils.js
+++ b/app/store/utils.js
@@ -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') {
diff --git a/app/utils/images.js b/app/utils/images.js
index 104779914..493c1c23c 100644
--- a/app/utils/images.js
+++ b/app/utils/images.js
@@ -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) {
diff --git a/app/utils/push_notifications.js b/app/utils/push_notifications.js
index 87a465480..5a04ec772 100644
--- a/app/utils/push_notifications.js
+++ b/app/utils/push_notifications.js
@@ -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,
diff --git a/babel.config.js b/babel.config.js
index a045ed23a..3ae18fa24 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -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',
diff --git a/index.js b/index.js
index e56dba743..89b2d352a 100644
--- a/index.js
+++ b/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',
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 358d34fbd..d6da1aa67 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -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
diff --git a/patches/react-native-fast-image+8.1.5.patch b/patches/react-native-fast-image+8.1.5.patch
index ba6b38a40..4ddd9e165 100644
--- a/patches/react-native-fast-image+8.1.5.patch
+++ b/patches/react-native-fast-image+8.1.5.patch
@@ -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 cookies = new LinkedList();
++ 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
diff --git a/share_extension/android/actions/index.js b/share_extension/android/actions/index.js
index a3f9748d7..13a57f995 100644
--- a/share_extension/android/actions/index.js
+++ b/share_extension/android/actions/index.js
@@ -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';
diff --git a/share_extension/android/extension_post/extension_post.js b/share_extension/android/extension_post/extension_post.js
index 9caf85230..8df471b3d 100644
--- a/share_extension/android/extension_post/extension_post.js
+++ b/share_extension/android/extension_post/extension_post.js
@@ -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}
>
- {
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);
};