mattermost-mobile/app/components/markdown/markdown_image/markdown_image.js
Miguel Alatzar 1efb01deda
[MM-16593] Upgrade to RN 0.61.2 (#3455)
* [MM-16593] [MM-17175] [MM-17164] [MM-17189] [MM-17181] Android - Upgrade to RN 0.60 (#3145)

* Upgrade to react-native 0.60

* Use @sentry/react-native

* Manually link @sentry/react-native

* Address review comments

* Jetify after install

* Call jetify from node_modules

* [MM-17785] iOS - Upgrade to RN 0.60 (#3153)

* Upgrade to react-native 0.60

* Use @sentry/react-native

* Manually link @sentry/react-native

* Address review comments

* Jetify after install

* Call jetify from node_modules

* Get app building for iOS

* Revert react-native-image-picker upgrade

* Minor version upgrade of react-native-image-picker

* [MM-17142] Convert all string refs to callbacks (#3217)

* Replace string refs

* Fix tests

* Don't use inline functions

* Fix mattermost-redux reversion from master merge

* [MM-18336] Upload Sentry debug symbols only when SENTRY_ENABLED is true (#3227)

* Upgrade @sentry/react-native

* Run Sentry gradle tasks only when enabled

* Upgrade @sentry/react-native and remove extra Sentry build phase

* [MM-17144] Use Hermes (#3226)

* Replace string refs

* Fix tests

* Don't use inline functions

* Fix mattermost-redux reversion from master merge

* Use Hermes

* bundleCommand ram-bundle no longer needed

* Require harmony-reflect only for Android

* Fix failing test

* Path react-native's splitLayoutProps (#3337)

* [MM-18867] [MM-17186] [MM-18866] [MM-19447] [MM-18967] Upgrade to RN 0.61.2 (#3423)

* Upgrade to RN 0.61.2

* Update rn-fetch-blob commit hash

* Update react-native-keyboard-tracking-view commit hash

* Use react-native-youtube with AVPlayer fix

* Fix jest mocks

* Use updated document picker API

* Remove unnecessary linking

* Revert "MM-17759 Add code highlighting (#3072)"

This reverts commit 26b999e885.

* Fix share extension

* Revert "Revert "MM-17759 Add code highlighting (#3072)""

This reverts commit 52aca776b12dee3abe8646d0c90480f8528e86c1.

* Address PR reviews

* Rename patch to match version

* Update react-native-youtube patch

* Update dependencies

* Fix RNDocViewer reference

* Update tests and revert to redux-persist 4.10.2

* Revert "Revert "Revert "MM-17759 Add code highlighting (#3072)"""

This reverts commit 5ef383be2619b1be6167c23f128ecb4b4ad25df9.

* Android fixes after dep upgrades

* Use fresco 2.0.0

* Use mattermost forks

* Use React-Core in Mattermost.scheme instead of react

* Remove packager (#3452)

* Remove Pods from source control

* Fix unit tests

* Add new line before entering the keystore in gradle.properties

* set ios as working directory for cocoapods

* Cache cocoapods on circleCI builds

* set ios as working dir

* fix cocoapods cache key

* Unify fastlane and npm dependencies command

* Use package-lock.json checksum for npm cache

* Fix package.json and use the checksum as the cache key

* Fix package.json and use the checksum as the cache key

* changes to circleci.yaml fastlane and removing pods from git

* Fix Mattermost.xcodeproj

* Update coocoapods to 1.7.5 and fix xcode header search paths

* Update package-lock.json

* Remove unused tooltip component

* Fix incorrect ref

* Disable Hermes (#3460)

* Revert "Remove packager (#3452)"

This reverts commit b2a79e184b3242124dfdb91ae095f6ce3f4eb986.

* Disable Hermes :'(

* Update preloaded modules

* Fix packages moduleNames and modulePaths, update snapshots and update mm-redux (ts version)

* remove document picker from modulePaths

* Fix package-lock.json

* Add eslint disable rules
2019-10-28 15:04:24 -07:00

310 lines
9 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {intlShape} from 'react-intl';
import {
Clipboard,
Image,
Linking,
Platform,
StyleSheet,
Text,
View,
} from 'react-native';
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 ImageCacheManager from 'app/utils/image_cache_manager';
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.Component {
static propTypes = {
children: PropTypes.node,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
imagesMetadata: PropTypes.object,
linkDestination: PropTypes.string,
isReplyPost: PropTypes.bool,
source: PropTypes.string.isRequired,
errorTextStyle: CustomPropTypes.Style,
};
static contextTypes = {
intl: intlShape.isRequired,
};
constructor(props) {
super(props);
const dimensions = props.imagesMetadata?.[props.source];
this.state = {
originalHeight: dimensions?.height || 0,
originalWidth: dimensions?.width || 0,
failed: false,
uri: null,
};
this.mounted = false;
}
componentDidMount() {
this.mounted = true;
ImageCacheManager.cache(null, this.getSource(), this.setImageUrl);
}
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
ImageCacheManager.cache(null, this.getSource(), this.setImageUrl);
}
}
componentWillUnmount() {
this.mounted = false;
}
setImageRef = (ref) => {
this.imageRef = ref;
}
setItemRef = (ref) => {
this.itemRef = ref;
}
getSource = () => {
let source = this.props.source;
if (source.startsWith('/')) {
source = EphemeralStore.currentServerUrl + source;
}
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);
};
handleSizeReceived = (width, height) => {
if (!this.mounted) {
return;
}
if (!width || !height) {
this.setState({failed: true});
return;
}
this.setState({
failed: false,
originalHeight: height,
originalWidth: width,
});
};
handleSizeFailed = () => {
if (!this.mounted) {
return;
}
this.setState({
failed: true,
});
};
handleLinkPress = () => {
const url = normalizeProtocol(this.props.linkDestination);
Linking.canOpenURL(url).then((supported) => {
if (supported) {
Linking.openURL(url);
}
});
};
handleLinkLongPress = async () => {
const {formatMessage} = this.context.intl;
const config = mattermostManaged.getCachedConfig();
if (config?.copyAndPasteProtection !== 'true') {
const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'});
const actionText = formatMessage({id: 'mobile.markdown.link.copy_url', defaultMessage: 'Copy URL'});
BottomSheet.showBottomSheetWithOptions({
options: [actionText, cancelText],
cancelButtonIndex: 1,
}, (value) => {
if (value !== 1) {
this.handleLinkCopy();
}
});
}
};
handleLinkCopy = () => {
Clipboard.setString(this.props.linkDestination || this.props.source);
};
handlePreviewImage = () => {
const {
originalHeight,
originalWidth,
uri,
} = this.state;
const link = this.getSource();
let filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?'));
const extension = filename.split('.').pop();
if (extension === filename) {
const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
filename = `${filename}${ext}`;
}
const files = [{
caption: filename,
dimensions: {
width: originalWidth,
height: originalHeight,
},
source: {uri},
data: {
localPath: uri,
},
}];
previewImageAtIndex([this.itemRef], 0, files);
};
loadImageSize = (source) => {
if (!this.state.originalWidth) {
Image.getSize(source, this.handleSizeReceived, this.handleSizeFailed);
}
};
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());
if (width && height) {
if (Platform.OS === 'android' && (width > ANDROID_MAX_WIDTH || height > ANDROID_MAX_HEIGHT)) {
// Android has a cap on the max image size that can be displayed
image = (
<Text style={this.props.errorTextStyle}>
<FormattedText
id='mobile.markdown.image.too_large'
defaultMessage='Image exceeds max dimensions of {maxWidth} by {maxHeight}:'
values={{
maxWidth: ANDROID_MAX_WIDTH,
maxHeight: ANDROID_MAX_HEIGHT,
}}
/>
{' '}
{this.props.children}
</Text>
);
} else {
// React Native complains if we try to pass resizeMode as a style
let source = null;
if (uri) {
source = {uri};
}
image = (
<TouchableWithFeedback
onLongPress={this.handleLinkLongPress}
onPress={this.handlePreviewImage}
style={{width, height}}
>
<ProgressiveImage
ref={this.setImageRef}
defaultSource={source}
resizeMode='contain'
style={{width, height}}
/>
</TouchableWithFeedback>
);
}
} else if (this.state.failed) {
image = (
<Image
source={brokenImageIcon}
style={style.brokenImageIcon}
/>
);
}
if (image && this.props.linkDestination) {
image = (
<TouchableWithFeedback
onPress={this.handleLinkPress}
onLongPress={this.handleLinkLongPress}
>
{image}
</TouchableWithFeedback>
);
}
return (
<View
ref={this.setItemRef}
style={style.container}
>
{image}
</View>
);
}
}
const style = StyleSheet.create({
container: {
marginBottom: 5,
},
brokenImageIcon: {
width: 24,
height: 24,
},
});