Commit aea28b08 authored by Sudheer's avatar Sudheer Committed by Elias Nahum
Browse files

[MM-11509] Use post metadata (#1686)

* MM-11509 Changes for consuming post metadata on webapp

   * Remove delayed api call for attachment info
   * Remove delayed api call for reactions on post
   * Pass down images prop from metadata to markdown image for dimentions

* Use metadata for webapp

  * Use Dimentions of images for
    1. Markdown inline images
    2. Message attachments
    3. Opengraph images
    4. Image embeds

  * Remove api calls for fetching post data
  * Remove api calls for fetching reactions
  * Remove image onload corrections for message attachments

  * Add new utility func for calculating dimentions of images
    based on metadata and maxDimentions for the component

* * Use opengraph data from store as before
* Fix review comments

* * Revert pack-lock.json
* Add constants for dimentions

* * Fix spelling for dimensions
* Added couple of checks for local temp vs postmeta data check

* Revert package-lock changes

* Fix faling tests and checks

* Update MM-redux hash to match post-metadata branch

* Change tests folder

* Fix spelling for dimentsion
parent b25d10b3
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/MarkdownImage should match snapshot 1`] = `
<img
onLoad={[Function]}
src="https://something.com/image.png"
style={
Object {
"height": "200",
}
}
/>
`;
......@@ -39,14 +39,6 @@ export default class FileAttachmentList extends React.Component {
isEmbedVisible: PropTypes.bool,
locale: PropTypes.string.isRequired,
actions: PropTypes.shape({
/*
* Function to get file metadata for a post
*/
getMissingFilesForPost: PropTypes.func.isRequired,
}).isRequired,
}
constructor(props) {
......@@ -57,12 +49,6 @@ export default class FileAttachmentList extends React.Component {
this.state = {showPreviewModal: false, startImgIndex: 0};
}
componentDidMount() {
if (this.props.post.file_ids || this.props.post.filenames) {
this.props.actions.getMissingFilesForPost(this.props.post.id);
}
}
handleImageClick(indexClicked) {
this.setState({showPreviewModal: true, startImgIndex: indexClicked});
}
......
......@@ -2,8 +2,6 @@
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {getMissingFilesForPost} from 'mattermost-redux/actions/files';
import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files';
import {getCurrentLocale} from 'selectors/i18n';
......@@ -34,12 +32,4 @@ function makeMapStateToProps() {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getMissingFilesForPost,
}, dispatch),
};
}
export default connect(makeMapStateToProps, mapDispatchToProps)(FileAttachmentList);
export default connect(makeMapStateToProps)(FileAttachmentList);
......@@ -70,12 +70,18 @@ export default class Markdown extends React.PureComponent {
* Any extra props that should be passed into the MarkdownImage component
*/
imageProps: PropTypes.object,
/**
* prop for passed down to MarkdownImage component for dimensions
*/
imagesMetadata: PropTypes.object,
};
static defaultProps = {
options: {},
isRHS: false,
proxyImages: true,
imagesMetadata: {},
};
render() {
......@@ -96,6 +102,7 @@ export default class Markdown extends React.PureComponent {
const htmlFormattedText = TextFormatting.formatText(this.props.message, options);
return messageHtmlToComponent(htmlFormattedText, this.props.isRHS, {
imageProps: this.props.imageProps,
imagesMetadata: this.props.imagesMetadata,
});
}
}
......@@ -15,6 +15,7 @@ describe('components/Markdown', () => {
siteURL: 'https://markdown.example.com',
team: {name: 'yourteamhere'},
hasImageProxy: false,
metadata: {},
};
test('should render properly', () => {
......
......@@ -4,15 +4,13 @@
import PropTypes from 'prop-types';
import React from 'react';
const WAIT_FOR_HEIGHT_TIMEOUT = 100;
export default class MarkdownImage extends React.PureComponent {
static propTypes = {
/*
* The href of the image to be loaded
* dimensions object to create empty space required to prevent scroll pop
*/
href: PropTypes.string,
dimensions: PropTypes.object,
/*
* A callback that is called as soon as the image component has a height value
......@@ -20,72 +18,27 @@ export default class MarkdownImage extends React.PureComponent {
onHeightReceived: PropTypes.func,
}
constructor(props) {
super(props);
this.heightTimeout = 0;
}
componentDidMount() {
this.waitForHeight();
}
componentDidUpdate(prevProps) {
if (this.props.href !== prevProps.href) {
this.waitForHeight();
}
}
componentWillUnmount() {
this.stopWaitingForHeight();
}
waitForHeight = () => {
if (this.refs.image.height) {
if (this.props.onHeightReceived) {
this.props.onHeightReceived(this.refs.image.height);
}
this.heightTimeout = 0;
} else {
this.heightTimeout = setTimeout(this.waitForHeight, WAIT_FOR_HEIGHT_TIMEOUT);
}
}
stopWaitingForHeight = () => {
if (this.heightTimeout !== 0) {
clearTimeout(this.heightTimeout);
this.heightTimeout = 0;
return true;
}
return false;
}
handleLoad = () => {
const wasWaiting = this.stopWaitingForHeight();
// image is loaded but still havent recived new post webscoket event for metadata
// so meanwhile correct manually
// The image loaded before we caught its layout event, so we still need to notify that its height changed
if (wasWaiting && this.props.onHeightReceived) {
if (!this.props.dimensions && this.props.onHeightReceived) {
this.props.onHeightReceived(this.refs.image.height);
}
};
handleError = () => {
this.stopWaitingForHeight();
};
render() {
const props = {...this.props};
Reflect.deleteProperty(props, 'onHeightReceived');
Reflect.deleteProperty(props, 'dimensions');
return (
<img
ref='image'
{...props}
onLoad={this.handleLoad}
onError={this.handleError}
style={{
height: this.props.dimensions ? this.props.dimensions.height : 'initial',
}}
/>
);
}
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import MarkdownImage from 'components/markdown_image';
describe('components/MarkdownImage', () => {
test('should match snapshot', () => {
const wrapper = shallow(
<MarkdownImage
src={'https://something.com/image.png'}
dimensions={{
width: '100',
height: '200',
}}
onHeightReceived={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
});
test('should call onHeightReceived', () => {
const onHeightReceived = jest.fn();
const wrapper = shallow(
<MarkdownImage
src={'https://something.com/image.png'}
onHeightReceived={onHeightReceived}
/>
);
const instance = wrapper.instance();
instance.refs = {
image: {
height: 100,
},
};
wrapper.find('img').prop('onLoad')();
expect(onHeightReceived).toHaveBeenCalledWith(100);
});
});
......@@ -71,6 +71,7 @@ export default class PostMarkdown extends React.PureComponent {
proxyImages={proxyImages}
options={this.props.options}
channelNamesMap={channelNamesMap}
imagesMetadata={this.props.post.metadata && this.props.post.metadata.images}
/>
);
}
......
......@@ -17,20 +17,6 @@ export default class CommentedOnFilesMessage extends React.PureComponent {
* An array of file metadata for the parent post
*/
fileInfos: PropTypes.arrayOf(PropTypes.object),
actions: PropTypes.shape({
/*
* Function to get file metadata for a post
*/
getFilesForPost: PropTypes.func.isRequired,
}).isRequired,
}
componentDidMount() {
if (!this.props.fileInfos || this.props.fileInfos.length === 0) {
this.props.actions.getFilesForPost(this.props.parentPostId);
}
}
render() {
......
......@@ -2,8 +2,6 @@
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {getFilesForPost} from 'mattermost-redux/actions/files';
import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files';
import CommentedOnFilesMessage from './commented_on_files_message.jsx';
......@@ -23,12 +21,4 @@ function makeMapStateToProps() {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getFilesForPost,
}, dispatch),
};
}
export default connect(makeMapStateToProps, mapDispatchToProps)(CommentedOnFilesMessage);
export default connect(makeMapStateToProps)(CommentedOnFilesMessage);
......@@ -75,7 +75,9 @@ exports[`components/post_view/MessageAttachment should call actions.doPostAction
</Connect(ShowMore)>
<img
className="attachment__image"
height={200}
src="image_url"
width={200}
/>
<div
className="attachment-actions"
......@@ -96,7 +98,9 @@ exports[`components/post_view/MessageAttachment should call actions.doPostAction
className="attachment__thumb-container"
>
<img
height={75}
src="thumb_url"
width={75}
/>
</div>
<div
......@@ -187,14 +191,18 @@ exports[`components/post_view/MessageAttachment should match snapshot 1`] = `
</Connect(ShowMore)>
<img
className="attachment__image"
height={200}
src="image_url"
width={200}
/>
</div>
<div
className="attachment__thumb-container"
>
<img
height={75}
src="thumb_url"
width={75}
/>
</div>
<div
......
......@@ -8,6 +8,7 @@ import {postListScrollChange} from 'actions/global_actions';
import {isUrlSafe} from 'utils/url.jsx';
import {handleFormattedTextClick} from 'utils/utils';
import {getFileDimensionsForDisplay} from 'utils/file_utils';
import Markdown from 'components/markdown';
import ShowMore from 'components/post_view/show_more';
......@@ -17,6 +18,15 @@ import ActionMenu from '../action_menu';
const MAX_ATTACHMENT_TEXT_HEIGHT = 200;
const MAX_DIMENSIONS_IMAGE_URL = {
maxHeight: 300,
maxWidth: 500,
};
const MAX_DIMENSIONS_THUMB_URL = {
maxHeight: 75,
maxWidth: 80,
};
export default class MessageAttachment extends React.PureComponent {
static propTypes = {
......@@ -35,6 +45,11 @@ export default class MessageAttachment extends React.PureComponent {
*/
options: PropTypes.object,
/**
* images object for dimensions
*/
imagesMetadata: PropTypes.object,
actions: PropTypes.shape({
doPostAction: PropTypes.func.isRequired,
}).isRequired,
......@@ -296,22 +311,26 @@ export default class MessageAttachment extends React.PureComponent {
let image;
if (attachment.image_url) {
const imageDimensions = getFileDimensionsForDisplay(this.props.imagesMetadata[attachment.image_url], MAX_DIMENSIONS_IMAGE_URL);
image = (
<img
className='attachment__image'
src={attachment.image_url}
{...imageDimensions}
/>
);
}
let thumb;
if (attachment.thumb_url) {
const imageDimensions = getFileDimensionsForDisplay(this.props.imagesMetadata[attachment.thumb_url], MAX_DIMENSIONS_THUMB_URL);
thumb = (
<div
className='attachment__thumb-container'
>
<img
src={attachment.thumb_url}
{...imageDimensions}
/>
</div>
);
......
......@@ -29,6 +29,16 @@ describe('components/post_view/MessageAttachment', () => {
postId: 'post_id',
attachment,
actions: {doPostAction: jest.fn()},
imagesMetadata: {
image_url: {
height: 200,
width: 200,
},
thumb_url: {
height: 200,
width: 200,
},
},
};
test('should match snapshot', () => {
......
......@@ -23,6 +23,15 @@ export default class MessageAttachmentList extends React.PureComponent {
* Options specific to text formatting
*/
options: PropTypes.object,
/**
* Images object used for creating placeholders to prevent scroll popup
*/
imagesMetadata: PropTypes.object,
}
static defaultProps = {
imagesMetadata: {},
}
render() {
......@@ -34,6 +43,7 @@ export default class MessageAttachmentList extends React.PureComponent {
postId={this.props.postId}
key={'att_' + i}
options={this.props.options}
imagesMetadata={this.props.imagesMetadata}
/>
);
});
......
......@@ -3,8 +3,6 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {getOpenGraphMetadata} from 'mattermost-redux/actions/posts';
import {getOpenGraphMetadataForUrl} from 'mattermost-redux/selectors/entities/posts';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
......@@ -14,8 +12,8 @@ import PostAttachmentOpenGraph from './post_attachment_opengraph.jsx';
function mapStateToProps(state, ownProps) {
return {
openGraphData: getOpenGraphMetadataForUrl(state, ownProps.link),
currentUser: getCurrentUser(state),
openGraphData: getOpenGraphMetadataForUrl(state, ownProps.link),
};
}
......@@ -23,7 +21,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
editPost,
getOpenGraphMetadata,
}, dispatch),
};
}
......
......@@ -10,6 +10,22 @@ import {PostTypes} from 'utils/constants.jsx';
import {useSafeUrl} from 'utils/url';
import * as Utils from 'utils/utils.jsx';
import {isSystemMessage} from 'utils/post_utils.jsx';
import {getFileDimensionsForDisplay} from 'utils/file_utils';
const MAX_DIMENSIONS_LARGE_IMAGE = {
maxHeight: 200,
maxWidth: 400,
};
const MAX_DIMENSIONS_SMALL_IMAGE = {
maxHeight: 80,
maxWidth: 95,
};
const DIMENSIONS_NEAREST_POINT_IMAGE = {
height: 80,
width: 80,
};
export default class PostAttachmentOpenGraph extends React.PureComponent {
static propTypes = {
......@@ -38,61 +54,36 @@ export default class PostAttachmentOpenGraph extends React.PureComponent {
toggleEmbedVisibility: PropTypes.func.isRequired,
actions: PropTypes.shape({
editPost: PropTypes.func.isRequired,
/**
* The function to get open graph data for a link
*/
getOpenGraphMetadata: PropTypes.func.isRequired,
}).isRequired,
}
constructor(props) {
super(props);
this.largeImageMinWidth = 150;
this.imageDimentions = { // Image dimentions in pixels.
height: 80,
width: 80,
};
this.textMaxLength = 300;
this.textEllipsis = '...';
this.largeImageMinRatio = 16 / 9;
this.smallImageContainerLeftPadding = 15;
this.imageRatio = null;
this.smallImageContainer = null;
this.smallImageElement = null;
this.handleRemovePreview = this.handleRemovePreview.bind(this);
this.IMAGE_LOADED = {
LOADING: 'loading',
YES: 'yes',
ERROR: 'error',
};
}
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
const removePreview = this.isRemovePreview(this.props.post, this.props.currentUser);
this.setState({
imageLoaded: this.IMAGE_LOADED.LOADING,
hasLargeImage: false,
const removePreview = this.isRemovePreview(props.post, props.currentUser);
const imageUrl = this.getBestImageUrl(props.openGraphData);
const hasLargeImage = props.post.metadata && imageUrl ? this.hasLargeImage(props.post.metadata.images[imageUrl]) : false;
this.state = {
hasLargeImage,
removePreview,
});
this.fetchData(this.props.link);
imageUrl,
};
}
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
const removePreview = this.isRemovePreview(nextProps.post, nextProps.currentUser);
const imageUrl = this.getBestImageUrl(nextProps.openGraphData);
const hasLargeImage = nextProps.post.metadata && imageUrl ? this.hasLargeImage(nextProps.post.metadata.images[imageUrl]) : false;
this.setState({
hasLargeImage,
removePreview,
imageUrl,
});
}
if (nextProps.link !== this.props.link) {
this.fetchData(nextProps.link);
}
}
componentDidUpdate() {
......@@ -106,38 +97,31 @@ export default class PostAttachmentOpenGraph extends React.PureComponent {
}
getBestImageUrl(data) {
if (Utils.isEmptyObject(data.images)) {
if (!data || Utils.isEmptyObject(data.images)) {
return null;
}
const bestImage = CommonUtils.getNearestPoint(this.imageDimentions, data.images, 'width', 'height');
const bestImage = CommonUtils.getNearestPoint(DIMENSIONS_NEAREST_POINT_IMAGE, data.images, 'width', 'height');
return bestImage.secure_url || bestImage.url;
}
onImageLoad = (image) => {
this.imageRatio = image.target.naturalWidth / image.target.naturalHeight;
if (
image.target.naturalWidth >= this.largeImageMinWidth &&
this.imageRatio >= this.largeImageMinRatio &&
!this.state.hasLargeImage
) {
this.setState({
hasLargeImage: true,
});
hasLargeImage({height, width}) {
let hasLargeImage;
const largeImageMinRatio = 16 / 9;
const largeImageMinWidth = 150;