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:
Elias Nahum 2020-04-28 11:23:39 -04:00 committed by GitHub
parent 8226b4730c
commit 9af108026e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 739 additions and 813 deletions

View file

@ -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,

View file

@ -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';

View file

@ -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';

View file

@ -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;

View file

@ -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('');

View file

@ -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,
},
]
}

View file

@ -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}

View file

@ -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) {

View file

@ -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,
},
}));
});

View 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;
}
}

View file

@ -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),
};
}

View file

@ -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}
/>

View file

@ -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];

View file

@ -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 {

View file

@ -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}

View file

@ -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}

View file

@ -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 [

View file

@ -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'

View file

@ -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);
});

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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(),
}));

View file

@ -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}
/>

View file

@ -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)>
`;

View file

@ -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',
},

View file

@ -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();
});
});

View file

@ -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})}

View file

@ -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',

View file

@ -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,
};

View file

@ -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;

View file

@ -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,

View file

@ -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';

View file

@ -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);

View file

@ -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';

View file

@ -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) {

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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) {

View file

@ -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');

View file

@ -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';

View file

@ -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';

View file

@ -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', () => {

View file

@ -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();

View file

@ -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});

View file

@ -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');
});

View file

@ -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';

View file

@ -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';

View file

@ -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,

View file

@ -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',

View file

@ -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),
};
}

View file

@ -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}
/>

View file

@ -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}}
/>

View file

@ -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};

View file

@ -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,

View file

@ -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);
};
}

View file

@ -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') {

View file

@ -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) {

View file

@ -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,

View file

@ -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',

View file

@ -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',

View file

@ -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

View file

@ -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

View file

@ -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';

View file

@ -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}

View file

@ -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);
};