diff --git a/app/components/file_attachment_list/file_attachment.js b/app/components/file_attachment_list/file_attachment.js
index 8957acc88..4fe3e5b8c 100644
--- a/app/components/file_attachment_list/file_attachment.js
+++ b/app/components/file_attachment_list/file_attachment.js
@@ -24,7 +24,6 @@ export default class FileAttachment extends PureComponent {
file: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
onCaptureRef: PropTypes.func,
- onCapturePreviewRef: PropTypes.func,
onInfoPress: PropTypes.func,
onPreviewPress: PropTypes.func,
theme: PropTypes.object.isRequired,
@@ -44,14 +43,6 @@ export default class FileAttachment extends PureComponent {
}
};
- handleCapturePreviewRef = (ref) => {
- const {onCapturePreviewRef, index} = this.props;
-
- if (onCapturePreviewRef) {
- onCapturePreviewRef(ref, index);
- }
- };
-
handlePreviewPress = () => {
this.props.onPreviewPress(this.props.index);
};
@@ -105,7 +96,6 @@ export default class FileAttachment extends PureComponent {
@@ -124,7 +114,6 @@ export default class FileAttachment extends PureComponent {
diff --git a/app/components/file_attachment_list/file_attachment_icon.js b/app/components/file_attachment_list/file_attachment_icon.js
index fc9c9dfdf..95e6a7791 100644
--- a/app/components/file_attachment_list/file_attachment_icon.js
+++ b/app/components/file_attachment_list/file_attachment_icon.js
@@ -42,7 +42,6 @@ export default class FileAttachmentIcon extends PureComponent {
iconHeight: PropTypes.number,
iconWidth: PropTypes.number,
onCaptureRef: PropTypes.func,
- onCapturePreviewRef: PropTypes.func,
wrapperHeight: PropTypes.number,
wrapperWidth: PropTypes.number,
};
@@ -67,14 +66,6 @@ export default class FileAttachmentIcon extends PureComponent {
}
};
- handleCapturePreviewRef = (ref) => {
- const {onCapturePreviewRef} = this.props;
-
- if (onCapturePreviewRef) {
- onCapturePreviewRef(ref);
- }
- };
-
render() {
const {file, iconHeight, iconWidth, wrapperHeight, wrapperWidth} = this.props;
const source = this.getFileIconPath(file);
@@ -85,7 +76,6 @@ export default class FileAttachmentIcon extends PureComponent {
style={[styles.fileIconWrapper, {height: wrapperHeight, width: wrapperWidth}]}
>
diff --git a/app/components/file_attachment_list/file_attachment_image.js b/app/components/file_attachment_list/file_attachment_image.js
index 204e3f019..b365d9bc6 100644
--- a/app/components/file_attachment_list/file_attachment_image.js
+++ b/app/components/file_attachment_list/file_attachment_image.js
@@ -35,7 +35,6 @@ export default class FileAttachmentImage extends PureComponent {
]),
imageWidth: PropTypes.number,
onCaptureRef: PropTypes.func,
- onCapturePreviewRef: PropTypes.func,
resizeMode: PropTypes.string,
resizeMethod: PropTypes.string,
wrapperHeight: PropTypes.number,
@@ -92,14 +91,6 @@ export default class FileAttachmentImage extends PureComponent {
}
};
- handleCapturePreviewRef = (ref) => {
- const {onCapturePreviewRef} = this.props;
-
- if (onCapturePreviewRef) {
- onCapturePreviewRef(ref);
- }
- };
-
render() {
const {
file,
@@ -135,7 +126,6 @@ export default class FileAttachmentImage extends PureComponent {
style={[style.fileImageWrapper, {height: wrapperHeight, width: wrapperWidth, overflow: 'hidden'}]}
>
{
- const activeComponent = this.items[index];
-
- if (!activeComponent) {
- cb(null);
- return;
- }
-
- activeComponent.measure((rx, ry, width, height, x, y) => {
- cb({
- origin: {x, y, width, height},
- });
- });
- };
-
- getPreviewProps = (index) => {
- const previewComponent = this.previewItems[index];
- return previewComponent ? {...previewComponent.props} : {};
- };
-
- goToImagePreview = (passProps) => {
- this.props.navigator.showModal({
- screen: 'ImagePreview',
- title: '',
- animationType: 'none',
- passProps,
- navigatorStyle: {
- navBarHidden: true,
- statusBarHidden: false,
- statusBarHideWithNavBar: false,
- screenBackgroundColor: 'transparent',
- modalPresentationStyle: 'overCurrentContext',
- },
- });
- };
-
handleCaptureRef = (ref, idx) => {
this.items[idx] = ref;
};
- handleCapturePreviewRef = (ref, idx) => {
- this.previewItems[idx] = ref;
- };
-
handleInfoPress = () => {
this.props.hideOptionsContext();
this.props.onPress();
@@ -171,22 +132,7 @@ export default class FileAttachmentList extends Component {
handlePreviewPress = preventDoubleTap((idx) => {
this.props.hideOptionsContext();
Keyboard.dismiss();
- const component = this.items[idx];
-
- if (!component) {
- return;
- }
-
- component.measure((rx, ry, width, height, x, y) => {
- this.goToImagePreview({
- index: idx,
- origin: {x, y, width, height},
- target: {x: 0, y: 0, opacity: 1},
- files: this.galleryFiles,
- getItemMeasures: this.getItemMeasures,
- getPreviewProps: this.getPreviewProps,
- });
- });
+ previewImageAtIndex(this.props.navigator, this.items, idx, this.galleryFiles);
});
handlePressIn = () => {
@@ -231,7 +177,6 @@ export default class FileAttachmentList extends Component {
index={idx}
navigator={navigator}
onCaptureRef={this.handleCaptureRef}
- onCapturePreviewRef={this.handleCapturePreviewRef}
onInfoPress={this.handleInfoPress}
onPreviewPress={this.handlePreviewPress}
theme={this.props.theme}
diff --git a/app/components/markdown/markdown.js b/app/components/markdown/markdown.js
index 204e6ab4f..c638a79e6 100644
--- a/app/components/markdown/markdown.js
+++ b/app/components/markdown/markdown.js
@@ -37,6 +37,7 @@ export default class Markdown extends PureComponent {
baseTextStyle: CustomPropTypes.Style,
blockStyles: PropTypes.object,
isEdited: PropTypes.bool,
+ isReplyPost: PropTypes.bool,
isSearchResult: PropTypes.bool,
navigator: PropTypes.object.isRequired,
onChannelLinkPress: PropTypes.func,
@@ -166,6 +167,7 @@ export default class Markdown extends PureComponent {
return (
{
- const activeComponent = this.refs.item;
-
- if (!activeComponent) {
- cb(null);
- return;
- }
-
- activeComponent.measure((rx, ry, width, height, x, y) => {
- cb({
- origin: {x, y, width, height},
- });
- });
- };
-
- getPreviewProps = () => {
- const previewComponent = this.refs.image;
- return previewComponent ? {...previewComponent.props} : {};
- };
-
getSource = (props = this.props) => {
let source = props.source;
@@ -112,30 +95,20 @@ export default class MarkdownImage extends React.Component {
return source;
};
- goToImagePreview = (passProps) => {
- this.props.navigator.showModal({
- screen: 'ImagePreview',
- title: '',
- animationType: 'none',
- passProps,
- navigatorStyle: {
- navBarHidden: true,
- statusBarHidden: false,
- statusBarHideWithNavBar: false,
- screenBackgroundColor: 'transparent',
- modalPresentationStyle: 'overCurrentContext',
- },
- });
- };
-
handleSizeReceived = (width, height) => {
if (!this.mounted) {
return;
}
+ const {deviceHeight, deviceWidth, isReplyPost} = this.props;
+ const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth;
+ const viewPortWidth = deviceSize - VIEWPORT_IMAGE_OFFSET - (isReplyPost ? VIEWPORT_IMAGE_REPLY_OFFSET : 0);
+ const dimensions = calculateDimensions(height, width, viewPortWidth);
+
this.setState({
- width,
- height,
+ ...dimensions,
+ originalHeight: height,
+ originalWidth: width,
});
};
@@ -149,16 +122,6 @@ export default class MarkdownImage extends React.Component {
});
};
- handleLayout = (event) => {
- if (!this.mounted) {
- return;
- }
-
- this.setState({
- maxWidth: event.nativeEvent.layout.width,
- });
- };
-
handleLinkPress = () => {
const url = normalizeProtocol(this.props.linkDestination);
@@ -190,40 +153,32 @@ export default class MarkdownImage extends React.Component {
};
handlePreviewImage = () => {
- const component = this.refs.item;
+ 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 (!component) {
- return;
+ if (extension === filename) {
+ const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
+ filename = `${filename}${ext}`;
}
- component.measure((rx, ry, width, height, x, y) => {
- const {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,
- source: {uri},
- data: {
- localPath: uri,
- },
- }];
-
- this.goToImagePreview({
- index: 0,
- origin: {x, y, width, height},
- target: {x: 0, y: 0, opacity: 1},
- files,
- getItemMeasures: this.getItemMeasures,
- getPreviewProps: this.getPreviewProps,
- });
- });
+ const files = [{
+ caption: filename,
+ dimensions: {
+ width: originalWidth,
+ height: originalHeight,
+ },
+ source: {uri},
+ data: {
+ localPath: uri,
+ },
+ }];
+ previewImageAtIndex(this.props.navigator, [this.refs.item], 0, files);
};
loadImageSize = (source) => {
@@ -243,11 +198,9 @@ export default class MarkdownImage extends React.Component {
render() {
let image = null;
- const {uri} = this.state;
-
- if (this.state.width && this.state.height && this.state.maxWidth) {
- let {width, height} = this.state;
+ const {height, uri, width} = this.state;
+ 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
@@ -266,18 +219,6 @@ export default class MarkdownImage extends React.Component {
);
} else {
- const maxWidth = this.state.maxWidth;
- if (width > maxWidth) {
- height = height * (maxWidth / width);
- width = maxWidth;
- }
-
- const maxHeight = MAX_IMAGE_HEIGHT;
- if (height > maxHeight) {
- width = width * (maxHeight / height);
- height = maxHeight;
- }
-
// React Native complains if we try to pass resizeMode as a style
let source = null;
if (uri) {
@@ -326,8 +267,7 @@ export default class MarkdownImage extends React.Component {
return (
{image}
diff --git a/app/components/message_attachments/message_attachment.js b/app/components/message_attachments/message_attachment.js
index 4ad1fdc9f..3f1fb07a8 100644
--- a/app/components/message_attachments/message_attachment.js
+++ b/app/components/message_attachments/message_attachment.js
@@ -6,17 +6,24 @@ import PropTypes from 'prop-types';
import {
Image,
Linking,
+ Platform,
Text,
+ TouchableWithoutFeedback,
View,
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import Markdown from 'app/components/markdown';
+import ProgressiveImage from 'app/components/progressive_image';
import CustomPropTypes from 'app/constants/custom_prop_types';
+import ImageCacheManager from 'app/utils/image_cache_manager';
+import {previewImageAtIndex, calculateDimensions} from 'app/utils/images';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import InteractiveAction from './interactive_action';
+const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10;
+const VIEWPORT_IMAGE_OFFSET = 32;
const STATUS_COLORS = {
good: '#00c100',
warning: '#dede01',
@@ -42,6 +49,20 @@ export default class MessageAttachment extends PureComponent {
this.state = this.getInitState();
}
+ componentWillMount() {
+ if (this.props.attachment.image_url) {
+ ImageCacheManager.cache(null, this.props.attachment.image_url, this.setImageUrl);
+ }
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
getActionView = (style) => {
const {attachment, postId} = this.props;
const {actions} = attachment;
@@ -95,6 +116,7 @@ export default class MessageAttachment extends PureComponent {
uncollapsedText,
text: shouldCollapse ? collapsedText : uncollapsedText,
collapsed: shouldCollapse,
+ imageUri: null,
};
};
@@ -189,12 +211,70 @@ export default class MessageAttachment extends PureComponent {
);
};
+ handleLayout = (event) => {
+ if (!this.maxImageWidth) {
+ const {height, width} = event.nativeEvent.layout;
+ const viewPortWidth = width > height ? height : width;
+ this.maxImageWidth = viewPortWidth - VIEWPORT_IMAGE_OFFSET;
+ }
+ };
+
+ handlePreviewImage = () => {
+ const {attachment, navigator} = this.props;
+ const {
+ imageUri: uri,
+ originalHeight,
+ originalWidth,
+ } = this.state;
+ const link = attachment.image_url;
+ 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: {
+ height: originalHeight,
+ width: originalWidth,
+ },
+ source: {uri},
+ data: {
+ localPath: uri,
+ },
+ }];
+ previewImageAtIndex(navigator, [this.refs.item], 0, files);
+ };
+
openLink = (link) => {
if (Linking.canOpenURL(link)) {
Linking.openURL(link);
}
};
+ setImageUrl = (imageURL) => {
+ let imageUri = imageURL;
+
+ if (Platform.OS === 'android') {
+ imageUri = `file://${imageURL}`;
+ }
+
+ Image.getSize(imageUri, (width, height) => {
+ const dimensions = calculateDimensions(height, width, this.maxImageWidth);
+ if (this.mounted) {
+ this.setState({
+ ...dimensions,
+ originalWidth: width,
+ originalHeight: height,
+ imageUri,
+ });
+ }
+ }, () => null);
+ };
+
shouldCollapse = () => {
const text = this.props.attachment.text || '';
return (text.match(/\n/g) || []).length >= 5 || text.length > 400;
@@ -219,6 +299,12 @@ export default class MessageAttachment extends PureComponent {
theme,
} = this.props;
+ const {
+ height,
+ imageUri,
+ width,
+ } = this.state;
+
const style = getStyleSheet(theme);
let preText;
@@ -357,13 +443,23 @@ export default class MessageAttachment extends PureComponent {
const actions = this.getActionView(style);
let image;
- if (attachment.image_url) {
+ if (imageUri) {
image = (
-
-
+
+
+
+
);
}
@@ -371,7 +467,10 @@ export default class MessageAttachment extends PureComponent {
return (
{preText}
-
+
{author}
@@ -459,10 +558,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderWidth: 1,
borderRadius: 2,
marginTop: 5,
- },
- image: {
- flex: 1,
- height: 50,
+ padding: 5,
},
actionsContainer: {
flex: 1,
diff --git a/app/components/post_attachment_opengraph/index.js b/app/components/post_attachment_opengraph/index.js
index 3c003ff32..f422ddf5e 100644
--- a/app/components/post_attachment_opengraph/index.js
+++ b/app/components/post_attachment_opengraph/index.js
@@ -6,8 +6,16 @@ import {bindActionCreators} from 'redux';
import {getOpenGraphMetadata} from 'mattermost-redux/actions/posts';
+import {getDimensions} from 'app/selectors/device';
+
import PostAttachmentOpenGraph from './post_attachment_opengraph';
+function mapStateToProps(state) {
+ return {
+ ...getDimensions(state),
+ };
+}
+
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
@@ -16,4 +24,4 @@ function mapDispatchToProps(dispatch) {
};
}
-export default connect(null, mapDispatchToProps)(PostAttachmentOpenGraph);
+export default connect(mapStateToProps, mapDispatchToProps)(PostAttachmentOpenGraph);
diff --git a/app/components/post_attachment_opengraph/post_attachment_opengraph.js b/app/components/post_attachment_opengraph/post_attachment_opengraph.js
index 15da1affc..ef0717080 100644
--- a/app/components/post_attachment_opengraph/post_attachment_opengraph.js
+++ b/app/components/post_attachment_opengraph/post_attachment_opengraph.js
@@ -4,7 +4,6 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
- Dimensions,
Image,
Linking,
Platform,
@@ -16,16 +15,21 @@ import {
import ProgressiveImage from 'app/components/progressive_image';
import ImageCacheManager from 'app/utils/image_cache_manager';
+import {previewImageAtIndex, calculateDimensions} from 'app/utils/images';
import {getNearestPoint} from 'app/utils/opengraph';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const MAX_IMAGE_HEIGHT = 150;
+const VIEWPORT_IMAGE_OFFSET = 88;
+const VIEWPORT_IMAGE_REPLY_OFFSET = 13;
export default class PostAttachmentOpenGraph extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
getOpenGraphMetadata: PropTypes.func.isRequired,
}).isRequired,
+ deviceHeight: PropTypes.number.isRequired,
+ deviceWidth: PropTypes.number.isRequired,
isReplyPost: PropTypes.bool,
link: PropTypes.string.isRequired,
navigator: PropTypes.object.isRequired,
@@ -66,27 +70,6 @@ export default class PostAttachmentOpenGraph extends PureComponent {
this.mounted = false;
}
- calculateLargeImageDimensions = (width, height) => {
- const {width: deviceWidth} = Dimensions.get('window');
- let maxHeight = MAX_IMAGE_HEIGHT;
- let maxWidth = deviceWidth - 88 - (this.props.isReplyPost ? 15 : 0);
-
- if (height <= MAX_IMAGE_HEIGHT) {
- maxHeight = height;
- } else {
- maxHeight = (height / width) * maxWidth;
- if (maxHeight > MAX_IMAGE_HEIGHT) {
- maxHeight = MAX_IMAGE_HEIGHT;
- }
- }
-
- if (height > width) {
- maxWidth = (width / height) * maxHeight;
- }
-
- return {width: maxWidth, height: maxHeight};
- };
-
fetchData(url, openGraphData) {
if (!openGraphData) {
this.props.actions.getOpenGraphMetadata(url);
@@ -99,7 +82,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
}
const bestDimensions = {
- width: Dimensions.get('window').width - 88,
+ width: this.getViewPostWidth(),
height: MAX_IMAGE_HEIGHT,
};
@@ -126,51 +109,25 @@ export default class PostAttachmentOpenGraph extends PureComponent {
const uri = `${prefix}${imageUrl}`;
Image.getSize(uri, (width, height) => {
- const dimensions = this.calculateLargeImageDimensions(width, height);
+ const dimensions = calculateDimensions(height, width, this.getViewPostWidth());
if (this.mounted) {
this.setState({
...dimensions,
+ originalHeight: height,
+ originalWidth: width,
imageUrl: uri,
});
}
}, () => null);
};
- getItemMeasures = (index, cb) => {
- const activeComponent = this.refs.item;
+ getViewPostWidth = () => {
+ const {deviceHeight, deviceWidth, isReplyPost} = this.props;
+ const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth;
+ const viewPortWidth = deviceSize - VIEWPORT_IMAGE_OFFSET - (isReplyPost ? VIEWPORT_IMAGE_REPLY_OFFSET : 0);
- if (!activeComponent) {
- cb(null);
- return;
- }
-
- activeComponent.measure((rx, ry, width, height, x, y) => {
- cb({
- origin: {x, y, width, height},
- });
- });
- };
-
- getPreviewProps = () => {
- const previewComponent = this.refs.image;
- return previewComponent ? {...previewComponent.props} : {};
- };
-
- goToImagePreview = (passProps) => {
- this.props.navigator.showModal({
- screen: 'ImagePreview',
- title: '',
- animationType: 'none',
- passProps,
- navigatorStyle: {
- navBarHidden: true,
- statusBarHidden: false,
- statusBarHideWithNavBar: false,
- screenBackgroundColor: 'transparent',
- modalPresentationStyle: 'overCurrentContext',
- },
- });
+ return viewPortWidth;
};
goToLink = () => {
@@ -178,39 +135,33 @@ export default class PostAttachmentOpenGraph extends PureComponent {
};
handlePreviewImage = () => {
- const component = this.refs.item;
+ const {
+ imageUrl: uri,
+ openGraphImageUrl: link,
+ originalWidth,
+ originalHeight,
+ } = this.state;
+ let filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?'));
+ const extension = filename.split('.').pop();
- if (!component) {
- return;
+ if (extension === filename) {
+ const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
+ filename = `${filename}${ext}`;
}
- component.measure((rx, ry, width, height, x, y) => {
- const {imageUrl: uri, openGraphImageUrl: link} = this.state;
- let filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?'));
- const extension = filename.split('.').pop();
+ const files = [{
+ caption: filename,
+ dimensions: {
+ width: originalWidth,
+ height: originalHeight,
+ },
+ source: {uri},
+ data: {
+ localPath: uri,
+ },
+ }];
- if (extension === filename) {
- const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
- filename = `${filename}${ext}`;
- }
-
- const files = [{
- caption: filename,
- source: {uri},
- data: {
- localPath: uri,
- },
- }];
-
- this.goToImagePreview({
- index: 0,
- origin: {x, y, width, height},
- target: {x: 0, y: 0, opacity: 1},
- files,
- getItemMeasures: this.getItemMeasures,
- getPreviewProps: this.getPreviewProps,
- });
- });
+ previewImageAtIndex(this.props.navigator, [this.refs.item], 0, files);
};
render() {
@@ -274,7 +225,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
ref='image'
style={[style.image, {width, height}]}
imageUri={imageUrl}
- resizeMode='cover'
+ resizeMode='contain'
/>
diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js
index 35894f98a..65b5be1cf 100644
--- a/app/components/post_body/post_body.js
+++ b/app/components/post_body/post_body.js
@@ -398,6 +398,7 @@ export default class PostBody extends PureComponent {
isFailed,
isPending,
isPostAddChannelMember,
+ isReplyPost,
isSearchResult,
isSystemMessage,
message,
@@ -470,6 +471,7 @@ export default class PostBody extends PureComponent {
baseTextStyle={messageStyle}
blockStyles={blockStyles}
isEdited={hasBeenEdited}
+ isReplyPost={isReplyPost}
isSearchResult={isSearchResult}
navigator={navigator}
onLongPress={this.showOptionsContext}
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 6bd72d063..3e4d95343 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
@@ -19,10 +19,12 @@ import ProgressiveImage from 'app/components/progressive_image';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {emptyFunction} from 'app/utils/general';
import ImageCacheManager from 'app/utils/image_cache_manager';
+import {previewImageAtIndex, calculateDimensions} from 'app/utils/images';
import {getYouTubeVideoId, isImageLink, isYoutubeLink} from 'app/utils/url';
-const MAX_IMAGE_HEIGHT = 150;
-
+const VIEWPORT_IMAGE_OFFSET = 66;
+const VIEWPORT_IMAGE_REPLY_OFFSET = 13;
+const MAX_YOUTUBE_IMAGE_HEIGHT = 150;
let MessageAttachments;
let PostAttachmentOpenGraph;
@@ -97,18 +99,18 @@ export default class PostBodyAdditionalContent extends PureComponent {
}
};
- calculateDimensions = (width, height) => {
+ calculateYouTubeImageDimensions = (width, height) => {
const {deviceHeight, deviceWidth} = this.props;
- let maxHeight = MAX_IMAGE_HEIGHT;
+ let maxHeight = MAX_YOUTUBE_IMAGE_HEIGHT;
const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth;
let maxWidth = deviceSize - 78;
- if (height <= MAX_IMAGE_HEIGHT) {
+ if (height <= MAX_YOUTUBE_IMAGE_HEIGHT) {
maxHeight = height;
} else {
maxHeight = (height / width) * maxWidth;
- if (maxHeight > MAX_IMAGE_HEIGHT) {
- maxHeight = MAX_IMAGE_HEIGHT;
+ if (maxHeight > MAX_YOUTUBE_IMAGE_HEIGHT) {
+ maxHeight = MAX_YOUTUBE_IMAGE_HEIGHT;
}
}
@@ -151,7 +153,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
generateToggleableEmbed = (isImage, isYouTube) => {
const {link} = this.props;
const {width, height, uri} = this.state;
- const imgHeight = height || MAX_IMAGE_HEIGHT;
+ const imgHeight = height;
if (link) {
if (isYouTube) {
@@ -196,7 +198,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
ref='image'
style={[styles.image, {width, height: imgHeight}]}
defaultSource={{uri}}
- resizeMode='cover'
+ resizeMode='contain'
onError={this.handleLinkLoadError}
/>
@@ -209,7 +211,9 @@ export default class PostBodyAdditionalContent extends PureComponent {
};
getImageSize = (path) => {
- const {link} = this.props;
+ const {deviceHeight, deviceWidth, link, isReplyPost} = this.props;
+ const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth;
+ const viewPortWidth = deviceSize - VIEWPORT_IMAGE_OFFSET - (isReplyPost ? VIEWPORT_IMAGE_REPLY_OFFSET : 0);
if (link && path) {
let prefix = '';
@@ -228,27 +232,24 @@ export default class PostBodyAdditionalContent extends PureComponent {
return;
}
- const dimensions = this.calculateDimensions(width, height);
- this.setState({...dimensions, linkLoaded: true, uri});
+ let dimensions;
+ if (isYoutubeLink(link)) {
+ dimensions = this.calculateYouTubeImageDimensions(width, height);
+ } else {
+ dimensions = calculateDimensions(height, width, viewPortWidth);
+ }
+
+ this.setState({
+ ...dimensions,
+ originalHeight: height,
+ originalWidth: width,
+ linkLoaded: true,
+ uri,
+ });
}, () => this.setState({linkLoadError: true}));
}
};
- getItemMeasures = (index, cb) => {
- const activeComponent = this.refs.item;
-
- if (!activeComponent) {
- cb(null);
- return;
- }
-
- activeComponent.measure((rx, ry, width, height, x, y) => {
- cb({
- origin: {x, y, width, height},
- });
- });
- };
-
getMessageAttachment = () => {
const {
postId,
@@ -285,11 +286,6 @@ export default class PostBodyAdditionalContent extends PureComponent {
return null;
};
- getPreviewProps = () => {
- const previewComponent = this.refs.image;
- return previewComponent ? {...previewComponent.props} : {};
- };
-
getYouTubeTime = (link) => {
const timeRegex = /[\\?&](t|start|time_continue)=([0-9]+h)?([0-9]+m)?([0-9]+s?)/;
@@ -319,54 +315,31 @@ export default class PostBodyAdditionalContent extends PureComponent {
return ticks;
};
- goToImagePreview = (passProps) => {
- this.props.navigator.showModal({
- screen: 'ImagePreview',
- title: '',
- animationType: 'none',
- passProps,
- navigatorStyle: {
- navBarHidden: true,
- statusBarHidden: false,
- statusBarHideWithNavBar: false,
- screenBackgroundColor: 'transparent',
- modalPresentationStyle: 'overCurrentContext',
- },
- });
- };
-
handleLinkLoadError = () => {
this.setState({linkLoadError: true});
};
handlePreviewImage = () => {
- const component = this.refs.item;
+ const {link, navigator} = this.props;
+ const {
+ originalHeight,
+ originalWidth,
+ uri,
+ } = this.state;
+ const filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?'));
+ const files = [{
+ caption: filename,
+ source: {uri},
+ dimensions: {
+ width: originalWidth,
+ height: originalHeight,
+ },
+ data: {
+ localPath: uri,
+ },
+ }];
- if (!component) {
- return;
- }
-
- component.measure((rx, ry, width, height, x, y) => {
- const {link} = this.props;
- const {uri} = this.state;
- const filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?'));
- const files = [{
- caption: filename,
- source: {uri},
- data: {
- localPath: uri,
- },
- }];
-
- this.goToImagePreview({
- index: 0,
- origin: {x, y, width, height},
- target: {x: 0, y: 0, opacity: 1},
- files,
- getItemMeasures: this.getItemMeasures,
- getPreviewProps: this.getPreviewProps,
- });
- });
+ previewImageAtIndex(navigator, [this.refs.item], 0, files);
};
playYouTubeVideo = () => {
diff --git a/app/screens/image_preview/image_preview.js b/app/screens/image_preview/image_preview.js
index be92d21b5..5bd403d2d 100644
--- a/app/screens/image_preview/image_preview.js
+++ b/app/screens/image_preview/image_preview.js
@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
import {
Alert,
Animated,
+ Image,
Platform,
SafeAreaView,
ScrollView,
@@ -30,6 +31,7 @@ import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachm
import {NavigationTypes, PermissionTypes} from 'app/constants';
import {isDocument, isVideo} from 'app/utils/file';
import {emptyFunction} from 'app/utils/general';
+import {calculateDimensions} from 'app/utils/images';
import Downloader from './downloader';
import VideoPreview from './video_preview';
@@ -49,7 +51,6 @@ export default class ImagePreview extends PureComponent {
deviceWidth: PropTypes.number.isRequired,
files: PropTypes.array,
getItemMeasures: PropTypes.func.isRequired,
- getPreviewProps: PropTypes.func.isRequired,
index: PropTypes.number.isRequired,
navigator: PropTypes.object,
origin: PropTypes.object,
@@ -179,10 +180,23 @@ export default class ImagePreview extends PureComponent {
};
getSwipeableStyle = () => {
- const {deviceHeight, deviceWidth} = this.props;
- const {origin, target} = this.state;
+ const {deviceHeight, deviceWidth, files} = this.props;
+ const {origin, target, index} = this.state;
const inputRange = [0, 1];
+ let toHeight = deviceHeight;
+ let toWidth = deviceWidth;
+ if (files[index].dimensions) {
+ const {height, width} = files[index].dimensions;
+ if (width < deviceWidth) {
+ toWidth = width;
+ }
+
+ if (height < deviceWidth) {
+ toHeight = height;
+ }
+ }
+
return {
left: this.openAnim.interpolate({
inputRange,
@@ -194,11 +208,11 @@ export default class ImagePreview extends PureComponent {
}),
width: this.openAnim.interpolate({
inputRange,
- outputRange: [origin.width, deviceWidth],
+ outputRange: [origin.width, toWidth],
}),
height: this.openAnim.interpolate({
inputRange,
- outputRange: [origin.height, deviceHeight],
+ outputRange: [origin.height, toHeight],
}),
};
};
@@ -323,6 +337,7 @@ export default class ImagePreview extends PureComponent {
return (
{
+ if (imageDimensions) {
+ const {height, width} = imageDimensions;
+ const {style, ...otherProps} = imageProps;
+ const flattenStyle = StyleSheet.flatten(style);
+ const calculatedDimensions = calculateDimensions(height, width, flattenStyle.width, flattenStyle.height);
+ const imageStyle = {...flattenStyle, ...calculatedDimensions};
+
+ return (
+
+
+
+ );
+ }
+
+ return null;
+ }
+
renderOtherItems = (index) => {
const {files} = this.props;
const file = files[index];
@@ -382,23 +418,19 @@ export default class ImagePreview extends PureComponent {
};
renderSelectedItem = () => {
- const {hide, index} = this.state;
+ const {hide} = this.state;
const file = this.getCurrentFile();
- if (hide || isDocument(file.data) || isVideo(file.data)) {
+ if (hide || !file || !file.source || !file.source.uri || isDocument(file.data) || isVideo(file.data)) {
return null;
}
- const {getPreviewProps} = this.props;
const containerStyle = this.getSwipeableStyle();
- const previewProps = getPreviewProps(index);
- Reflect.deleteProperty(previewProps, 'thumbnailUri');
-
return (
diff --git a/app/utils/images.js b/app/utils/images.js
new file mode 100644
index 000000000..1b7f0419d
--- /dev/null
+++ b/app/utils/images.js
@@ -0,0 +1,90 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+let previewComponents;
+const IMAGE_MAX_HEIGHT = 350;
+const IMAGE_MIN_DIMENSION = 50;
+
+export const calculateDimensions = (height, width, viewPortWidth = 0, viewPortHeight = 0) => {
+ const ratio = height / width;
+ const heightRatio = width / height;
+
+ let imageWidth = width;
+ let imageHeight = height;
+
+ if (width > viewPortWidth) {
+ imageWidth = viewPortWidth;
+ imageHeight = imageWidth * ratio;
+ } else if (width < IMAGE_MIN_DIMENSION) {
+ imageWidth = IMAGE_MIN_DIMENSION;
+ imageHeight = imageWidth * ratio;
+ }
+
+ if (
+ (
+ imageHeight > IMAGE_MAX_HEIGHT ||
+ (viewPortHeight && imageHeight > viewPortHeight)
+ ) && viewPortHeight <= IMAGE_MAX_HEIGHT
+ ) {
+ imageHeight = viewPortHeight || IMAGE_MAX_HEIGHT;
+ imageWidth = imageHeight * heightRatio;
+ } else if (imageHeight < IMAGE_MIN_DIMENSION) {
+ imageHeight = IMAGE_MIN_DIMENSION;
+ imageWidth = imageHeight * heightRatio;
+ }
+
+ return {
+ height: imageHeight,
+ width: imageWidth,
+ };
+};
+
+export function previewImageAtIndex(navigator, components, index, files) {
+ previewComponents = components;
+ const component = components[index];
+ if (component) {
+ component.measure((rx, ry, width, height, x, y) => {
+ goToImagePreview(
+ navigator,
+ {
+ index,
+ origin: {x, y, width, height},
+ target: {x: 0, y: 0, opacity: 1},
+ files,
+ getItemMeasures,
+ }
+ );
+ });
+ }
+}
+
+function goToImagePreview(navigator, passProps) {
+ navigator.showModal({
+ screen: 'ImagePreview',
+ title: '',
+ animationType: 'none',
+ passProps,
+ navigatorStyle: {
+ navBarHidden: true,
+ statusBarHidden: false,
+ statusBarHideWithNavBar: false,
+ screenBackgroundColor: 'transparent',
+ modalPresentationStyle: 'overCurrentContext',
+ },
+ });
+}
+
+function getItemMeasures(index, cb) {
+ const activeComponent = previewComponents[index];
+
+ if (!activeComponent) {
+ cb(null);
+ return;
+ }
+
+ activeComponent.measure((rx, ry, width, height, x, y) => {
+ cb({
+ origin: {x, y, width, height},
+ });
+ });
+}
diff --git a/app/utils/images.test.js b/app/utils/images.test.js
new file mode 100644
index 000000000..9f1a57f6c
--- /dev/null
+++ b/app/utils/images.test.js
@@ -0,0 +1,59 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {calculateDimensions} from 'app/utils/images';
+
+const PORTRAIT_VIEWPORT = 315;
+const IMAGE_MAX_HEIGHT = 350;
+const IMAGE_MIN_DIMENSION = 50;
+
+describe('Images calculateDimensions', () => {
+ it('image smaller than 50x50 should return 50x50', () => {
+ const {height, width} = calculateDimensions(20, 20, PORTRAIT_VIEWPORT);
+ expect(height).toEqual(IMAGE_MIN_DIMENSION);
+ expect(width).toEqual(IMAGE_MIN_DIMENSION);
+ });
+
+ it('images with height below 50 should return height of 50', () => {
+ const {height} = calculateDimensions(45, 100, PORTRAIT_VIEWPORT);
+ expect(height).toEqual(IMAGE_MIN_DIMENSION);
+ });
+
+ it('images with width below 50 should return width of 50', () => {
+ const {width} = calculateDimensions(100, 45, PORTRAIT_VIEWPORT);
+ expect(width).toEqual(IMAGE_MIN_DIMENSION);
+ });
+
+ it('images with that are 50x50 should return the same size', () => {
+ const {height, width} = calculateDimensions(50, 50, PORTRAIT_VIEWPORT);
+ expect(height).toEqual(IMAGE_MIN_DIMENSION);
+ expect(width).toEqual(IMAGE_MIN_DIMENSION);
+ });
+
+ it('images with smaller sizes than the max allowed should return the same size', () => {
+ const {height, width} = calculateDimensions(75, 150, PORTRAIT_VIEWPORT);
+ expect(height).toEqual(75);
+ expect(width).toEqual(150);
+ });
+
+ it('images with that have a width of the same size as the viewPort return the same size', () => {
+ const {height, width} = calculateDimensions(75, PORTRAIT_VIEWPORT, PORTRAIT_VIEWPORT);
+ expect(height).toEqual(75);
+ expect(width).toEqual(PORTRAIT_VIEWPORT);
+ });
+
+ it('large images with more height than width should return a MAX height of 350', () => {
+ const {height} = calculateDimensions(1920, 1080, PORTRAIT_VIEWPORT);
+ expect(height).toEqual(IMAGE_MAX_HEIGHT);
+ });
+
+ it('large images with more width than height should return the MAX width equal to the viewPort', () => {
+ const {width} = calculateDimensions(1080, 1920, PORTRAIT_VIEWPORT);
+ expect(width).toEqual(PORTRAIT_VIEWPORT);
+ });
+
+ it('large images with the viewPort height defined should return the MAX height equal to the viewPort Height', () => {
+ const {height} = calculateDimensions(1920, 1080, PORTRAIT_VIEWPORT, 340);
+ expect(height).toEqual(340);
+ });
+});