Commit b6196ad2 authored by Harrison Healey's avatar Harrison Healey
Browse files

Merge branch 'post-metadata'

parents 7dcafa9e fa4abebb
......@@ -175,18 +175,11 @@ export function increasePostVisibility(channelId, focusedPostId) {
return true;
}
dispatch(batchActions([
{
type: ActionTypes.LOADING_POSTS,
data: true,
channelId,
},
{
type: ActionTypes.INCREASE_POST_VISIBILITY,
data: channelId,
amount: POST_INCREASE_AMOUNT,
},
]));
dispatch({
type: ActionTypes.LOADING_POSTS,
data: true,
channelId,
});
const page = Math.floor(currentPostVisibility / POST_INCREASE_AMOUNT);
......@@ -198,13 +191,25 @@ export function increasePostVisibility(channelId, focusedPostId) {
}
const posts = result.data;
dispatch({
const actions = [{
type: ActionTypes.LOADING_POSTS,
data: false,
channelId,
});
}];
if (posts) {
actions.push({
type: ActionTypes.INCREASE_POST_VISIBILITY,
data: channelId,
amount: posts.order.length,
});
}
return posts ? posts.order.length >= POST_INCREASE_AMOUNT : false;
dispatch(batchActions(actions));
return {
moreToLoad: posts ? posts.order.length >= POST_INCREASE_AMOUNT : false,
error: result.error,
};
};
}
......
......@@ -216,43 +216,40 @@ describe('Actions.Posts', () => {
await testStore.dispatch(Actions.increasePostVisibility('current_channel_id'));
expect(testStore.getActions()).toEqual([
{channelId: 'current_channel_id', data: true, type: 'LOADING_POSTS'},
{args: ['current_channel_id', 2, 30], type: 'MOCK_GET_POSTS'},
{
meta: {batch: true},
payload: [
{channelId: 'current_channel_id', data: true, type: 'LOADING_POSTS'},
{amount: 30, data: 'current_channel_id', type: 'INCREASE_POST_VISIBILITY'},
{channelId: 'current_channel_id', data: false, type: 'LOADING_POSTS'},
],
type: 'BATCHING_REDUCER.BATCH',
},
{args: ['current_channel_id', 2, 30], type: 'MOCK_GET_POSTS'},
{channelId: 'current_channel_id', data: false, type: 'LOADING_POSTS'},
]);
await testStore.dispatch(Actions.increasePostVisibility('current_channel_id', 'latest_post_id'));
expect(testStore.getActions()).toEqual([
{channelId: 'current_channel_id', data: true, type: 'LOADING_POSTS'},
{args: ['current_channel_id', 2, 30], type: 'MOCK_GET_POSTS'},
{
meta: {batch: true},
payload: [
{channelId: 'current_channel_id', data: true, type: 'LOADING_POSTS'},
{amount: 30, data: 'current_channel_id', type: 'INCREASE_POST_VISIBILITY'},
{channelId: 'current_channel_id', data: false, type: 'LOADING_POSTS'},
],
type: 'BATCHING_REDUCER.BATCH',
},
{args: ['current_channel_id', 2, 30], type: 'MOCK_GET_POSTS'},
{channelId: 'current_channel_id', data: false, type: 'LOADING_POSTS'},
{channelId: 'current_channel_id', data: true, type: 'LOADING_POSTS'},
{
args: ['current_channel_id', 'latest_post_id', 2, 30],
type: 'MOCK_GET_POSTS_BEFORE',
},
{
meta: {batch: true},
payload: [
{channelId: 'current_channel_id', data: true, type: 'LOADING_POSTS'},
{amount: 30, data: 'current_channel_id', type: 'INCREASE_POST_VISIBILITY'},
{channelId: 'current_channel_id', data: false, type: 'LOADING_POSTS'},
],
type: 'BATCHING_REDUCER.BATCH',
},
{
args: ['current_channel_id', 'latest_post_id', 2, 30],
type: 'MOCK_GET_POSTS_BEFORE',
},
{channelId: 'current_channel_id', data: false, type: 'LOADING_POSTS'},
]);
});
......
// 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,28 @@ 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,38 @@ export default class PostAttachmentOpenGraph extends React.PureComponent {