PLT-6215 Major post list refactor (#6501)
* Major post list refactor * Fix post and thread deletion * Fix preferences not selecting correctly * Fix military time displaying * Fix UP key for editing posts * Fix ESLint error * Various fixes and updates per feedback * Fix for permalink view * Revert to old scrolling method and various fixes * Add floating timestamp, new message indicator, scroll arrows * Update post loading for focus mode and add visibility limit * Fix pinning posts and a react warning * Add loading UI updates from Asaad * Fix refreshing loop * Temporarily bump post visibility limit * Update infinite scrolling * Remove infinite scrolling
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
0231e95f1c
Коммит
ab67f6e257
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
export default class CommentedOnFilesMessage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/*
|
||||
* The id of the post that was commented on
|
||||
*/
|
||||
parentPostId: React.PropTypes.string.isRequired,
|
||||
|
||||
/*
|
||||
* An array of file metadata for the parent post
|
||||
*/
|
||||
fileInfos: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
|
||||
actions: React.PropTypes.shape({
|
||||
|
||||
/*
|
||||
* Function to get file metadata for a post
|
||||
*/
|
||||
getFilesForPost: React.PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.fileInfos || this.props.fileInfos.length === 0) {
|
||||
this.props.actions.getFilesForPost(this.props.parentPostId);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let message = ' ';
|
||||
|
||||
if (this.props.fileInfos && this.props.fileInfos.length > 0) {
|
||||
message = this.props.fileInfos[0].name;
|
||||
|
||||
if (this.props.fileInfos.length === 2) {
|
||||
message += Utils.localizeMessage('post_body.plusOne', ' plus 1 other file');
|
||||
} else if (this.props.fileInfos.length > 2) {
|
||||
message += Utils.localizeMessage('post_body.plusMore', ' plus {count} other files').replace('{count}', (this.props.fileInfos.length - 1).toString());
|
||||
}
|
||||
}
|
||||
|
||||
return <span>{message}</span>;
|
||||
}
|
||||
}
|
||||
36
webapp/components/post_view/commented_on_files_message/index.js
Обычный файл
36
webapp/components/post_view/commented_on_files_message/index.js
Обычный файл
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// 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';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const selectFileInfosForPost = makeGetFilesForPost();
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
let fileInfos;
|
||||
if (ownProps.parentPostId) {
|
||||
fileInfos = selectFileInfosForPost(state, {id: ownProps.parentPostId});
|
||||
}
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
fileInfos
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getFilesForPost
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(CommentedOnFilesMessage);
|
||||
@@ -1,90 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import FileStore from 'stores/file_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
export default class CommentedOnFilesMessageContainer extends React.Component {
|
||||
static propTypes = {
|
||||
parentPostChannelId: PropTypes.string.isRequired,
|
||||
parentPostId: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleFileChange = this.handleFileChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
fileInfos: FileStore.getInfosForPost(this.props.parentPostId)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
FileStore.addChangeListener(this.handleFileChange);
|
||||
|
||||
if (!FileStore.hasInfosForPost(this.props.parentPostId)) {
|
||||
AsyncClient.getFileInfosForPost(this.props.parentPostChannelId, this.props.parentPostId);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.parentPostId !== this.props.parentPostId) {
|
||||
this.setState({
|
||||
fileInfos: FileStore.getInfosForPost(this.props.parentPostId)
|
||||
});
|
||||
|
||||
if (!FileStore.hasInfosForPost(this.props.parentPostId)) {
|
||||
AsyncClient.getFileInfosForPost(this.props.parentPostChannelId, this.props.parentPostId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (nextProps.parentPostId !== this.props.parentPostId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.parentPostChannelId !== this.props.parentPostChannelId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// fileInfos are treated as immutable by the FileStore
|
||||
if (nextState.fileInfos !== this.state.fileInfos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
handleFileChange() {
|
||||
this.setState({
|
||||
fileInfos: FileStore.getInfosForPost(this.props.parentPostId)
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
FileStore.removeChangeListener(this.handleFileChange);
|
||||
}
|
||||
|
||||
render() {
|
||||
let message = ' ';
|
||||
|
||||
if (this.state.fileInfos && this.state.fileInfos.length > 0) {
|
||||
message = this.state.fileInfos[0].name;
|
||||
|
||||
if (this.state.fileInfos.length === 2) {
|
||||
message += Utils.localizeMessage('post_body.plusOne', ' plus 1 other file');
|
||||
} else if (this.state.fileInfos.length > 2) {
|
||||
message += Utils.localizeMessage('post_body.plusMore', ' plus {count} other files').replace('{count}', (this.state.fileInfos.length - 1).toString());
|
||||
}
|
||||
}
|
||||
|
||||
return <span>{message}</span>;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {FormattedDate} from 'react-intl';
|
||||
|
||||
export default function DateSeparator(props) {
|
||||
return (
|
||||
<div
|
||||
className='date-separator'
|
||||
>
|
||||
<hr className='separator__hr'/>
|
||||
<div className='separator__text'>
|
||||
<FormattedDate
|
||||
value={props.date}
|
||||
weekday='short'
|
||||
month='short'
|
||||
day='2-digit'
|
||||
year='numeric'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DateSeparator.propTypes = {
|
||||
date: PropTypes.instanceOf(Date)
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostAttachment from './post_attachment.jsx';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default function PostAttachmentList(props) {
|
||||
const content = [];
|
||||
props.attachments.forEach((attachment, i) => {
|
||||
content.push(
|
||||
<PostAttachment
|
||||
attachment={attachment}
|
||||
key={'att_' + i}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='attachment_list'>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PostAttachmentList.propTypes = {
|
||||
attachments: PropTypes.array.isRequired
|
||||
};
|
||||
@@ -1,690 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
import $ from 'jquery';
|
||||
|
||||
import Post from './post.jsx';
|
||||
import FloatingTimestamp from './floating_timestamp.jsx';
|
||||
import ScrollToBottomArrows from './scroll_to_bottom_arrows.jsx';
|
||||
import NewMessageIndicator from './new_message_indicator.jsx';
|
||||
|
||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||
|
||||
import {createChannelIntroMessage} from 'utils/channel_intro_messages.jsx';
|
||||
|
||||
import * as UserAgent from 'utils/user_agent.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as PostUtils from 'utils/post_utils.jsx';
|
||||
import DelayedAction from 'utils/delayed_action.jsx';
|
||||
|
||||
import * as ChannelActions from 'actions/channel_actions.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const ScrollTypes = Constants.ScrollTypes;
|
||||
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import ScrollStore from 'stores/scroll_store.jsx';
|
||||
import {FormattedDate, FormattedMessage} from 'react-intl';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
const Preferences = Constants.Preferences;
|
||||
|
||||
export default class PostList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
this.handleScrollStop = this.handleScrollStop.bind(this);
|
||||
this.isAtBottom = this.isAtBottom.bind(this);
|
||||
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
|
||||
this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
|
||||
this.createPosts = this.createPosts.bind(this);
|
||||
this.updateScrolling = this.updateScrolling.bind(this);
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
this.scrollToBottom = this.scrollToBottom.bind(this);
|
||||
this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.childComponentDidUpdate = this.childComponentDidUpdate.bind(this);
|
||||
this.checkAndUpdateScrolling = this.checkAndUpdateScrolling.bind(this);
|
||||
|
||||
this.jumpToPostNode = null;
|
||||
this.wasAtBottom = true;
|
||||
this.scrollHeight = 0;
|
||||
this.animationFrameId = 0;
|
||||
|
||||
this.scrollStopAction = new DelayedAction(this.handleScrollStop);
|
||||
|
||||
this.state = {
|
||||
isScrolling: false,
|
||||
fullWidthIntro: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN,
|
||||
topPostId: null,
|
||||
unViewedCount: 0
|
||||
};
|
||||
|
||||
if (props.channel) {
|
||||
this.introText = createChannelIntroMessage(props.channel, this.state.fullWidthIntro);
|
||||
} else {
|
||||
this.introText = this.getArchivesIntroMessage();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.channel && this.props.channel.type === Constants.DM_CHANNEL) {
|
||||
const teammateId = Utils.getUserIdFromChannelName(this.props.channel);
|
||||
if (!this.props.profiles[teammateId] && nextProps.profiles[teammateId]) {
|
||||
this.introText = createChannelIntroMessage(this.props.channel, this.state.fullWidthIntro);
|
||||
}
|
||||
}
|
||||
|
||||
const posts = nextProps.postList.posts;
|
||||
const order = nextProps.postList.order;
|
||||
let unViewedCount = 0;
|
||||
|
||||
// Only count if we're not at the bottom, not in highlight view,
|
||||
// or anything else
|
||||
if (nextProps.scrollType === Constants.ScrollTypes.FREE) {
|
||||
unViewedCount = order.reduce((count, orderId) => {
|
||||
const post = posts[orderId];
|
||||
if (post.create_at > nextProps.lastViewedBottom &&
|
||||
post.user_id !== nextProps.currentUser.id &&
|
||||
post.state !== Constants.POST_DELETED) {
|
||||
return count + 1;
|
||||
}
|
||||
return count;
|
||||
}, 0);
|
||||
}
|
||||
this.setState({unViewedCount});
|
||||
|
||||
if (this.props.channelId !== nextProps.channelId) {
|
||||
PostStore.removePostDraftChangeListener(this.props.channelId, this.handlePostDraftChange);
|
||||
PostStore.addPostDraftChangeListener(nextProps.channelId, this.handlePostDraftChange);
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (e.which === Constants.KeyCodes.ESCAPE && $('.popover.in,.modal.in').length === 0) {
|
||||
e.preventDefault();
|
||||
ChannelActions.setChannelAsRead();
|
||||
}
|
||||
}
|
||||
|
||||
isAtBottom() {
|
||||
if (!this.refs.postlist) {
|
||||
return this.wasAtBottom;
|
||||
}
|
||||
|
||||
// consider the view to be at the bottom if it's within this many pixels of the bottom
|
||||
const atBottomMargin = 10;
|
||||
|
||||
return this.refs.postlist.clientHeight + this.refs.postlist.scrollTop >= this.refs.postlist.scrollHeight - atBottomMargin;
|
||||
}
|
||||
|
||||
handleScroll() {
|
||||
// HACK FOR RHS -- REMOVE WHEN RHS DIES
|
||||
const childNodes = this.refs.postlistcontent.childNodes;
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
// If the node is 1/3 down the page
|
||||
if (childNodes[i].offsetTop >= (this.refs.postlist.scrollTop + (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION))) {
|
||||
this.jumpToPostNode = childNodes[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this.jumpToPostNode && childNodes.length > 0) {
|
||||
this.jumpToPostNode = childNodes[childNodes.length - 1];
|
||||
}
|
||||
|
||||
this.updateFloatingTimestamp();
|
||||
|
||||
if (!this.state.isScrolling) {
|
||||
this.setState({
|
||||
isScrolling: true
|
||||
});
|
||||
}
|
||||
|
||||
// Postpone all DOM related calculations to next frame.
|
||||
// scrollHeight etc might return wrong data at this point
|
||||
setTimeout(() => {
|
||||
if (!this.refs.postlist) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.wasAtBottom = this.isAtBottom();
|
||||
this.props.postListScrolled(this.isAtBottom());
|
||||
this.prevScrollHeight = this.refs.postlist.scrollHeight;
|
||||
this.prevOffsetTop = this.jumpToPostNode.offsetTop;
|
||||
}, 0);
|
||||
|
||||
this.scrollStopAction.fireAfter(Constants.SCROLL_DELAY);
|
||||
}
|
||||
|
||||
handleScrollStop() {
|
||||
this.setState({
|
||||
isScrolling: false
|
||||
});
|
||||
}
|
||||
|
||||
updateFloatingTimestamp() {
|
||||
// skip this in non-mobile view since that's when the timestamp is visible
|
||||
if (!Utils.isMobile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.postList) {
|
||||
// iterate through posts starting at the bottom since users are more likely to be viewing newer posts
|
||||
for (let i = 0; i < this.props.postList.order.length; i++) {
|
||||
const id = this.props.postList.order[i];
|
||||
const element = this.refs[id];
|
||||
|
||||
if (!element || !element.domNode || element.domNode.offsetTop + element.domNode.clientHeight <= this.refs.postlist.scrollTop) {
|
||||
// this post is off the top of the screen so the last one is at the top of the screen
|
||||
let topPostId;
|
||||
|
||||
if (i > 0) {
|
||||
topPostId = this.props.postList.order[i - 1];
|
||||
} else {
|
||||
// the first post we look at should always be on the screen, but handle that case anyway
|
||||
topPostId = id;
|
||||
}
|
||||
|
||||
if (topPostId !== this.state.topPostId) {
|
||||
this.setState({
|
||||
topPostId
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadMorePostsTop(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.isFocusPost) {
|
||||
return GlobalActions.emitLoadMorePostsFocusedTopEvent();
|
||||
}
|
||||
return GlobalActions.emitLoadMorePostsEvent();
|
||||
}
|
||||
|
||||
loadMorePostsBottom() {
|
||||
GlobalActions.emitLoadMorePostsFocusedBottomEvent();
|
||||
}
|
||||
|
||||
createPosts(posts, order) {
|
||||
const postCtls = [];
|
||||
let previousPostDay = new Date(0);
|
||||
const userId = this.props.currentUser.id;
|
||||
const profiles = this.props.profiles || {};
|
||||
|
||||
let renderedLastViewed = false;
|
||||
|
||||
for (let i = order.length - 1; i >= 0; i--) {
|
||||
const post = posts[order[i]];
|
||||
const parentPost = posts[post.parent_id];
|
||||
const prevPost = posts[order[i + 1]];
|
||||
const postUserId = PostUtils.isSystemMessage(post) ? '' : post.user_id;
|
||||
|
||||
// If the post is a comment whose parent has been deleted, don't add it to the list.
|
||||
if (parentPost && parentPost.state === Constants.POST_DELETED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sameUser = false;
|
||||
let sameRoot = false;
|
||||
let hideProfilePic = false;
|
||||
|
||||
if (prevPost) {
|
||||
const postIsComment = PostUtils.isComment(post);
|
||||
const prevPostIsComment = PostUtils.isComment(prevPost);
|
||||
const postFromWebhook = Boolean(post.props && post.props.from_webhook);
|
||||
const prevPostFromWebhook = Boolean(prevPost.props && prevPost.props.from_webhook);
|
||||
const prevPostUserId = PostUtils.isSystemMessage(prevPost) ? '' : prevPost.user_id;
|
||||
|
||||
// consider posts from the same user if:
|
||||
// the previous post was made by the same user as the current post,
|
||||
// the previous post was made within 5 minutes of the current post,
|
||||
// the current post is not from a webhook
|
||||
// the previous post is not from a webhook
|
||||
if (prevPostUserId === postUserId &&
|
||||
post.create_at - prevPost.create_at <= Constants.POST_COLLAPSE_TIMEOUT &&
|
||||
!postFromWebhook && !prevPostFromWebhook) {
|
||||
sameUser = true;
|
||||
}
|
||||
|
||||
// consider posts from the same root if:
|
||||
// the current post is a comment,
|
||||
// the current post has the same root as the previous post
|
||||
if (postIsComment && (prevPost.id === post.root_id || prevPost.root_id === post.root_id)) {
|
||||
sameRoot = true;
|
||||
}
|
||||
|
||||
// consider posts from the same root if:
|
||||
// the current post is not a comment,
|
||||
// the previous post is not a comment,
|
||||
// the previous post is from the same user
|
||||
if (!postIsComment && !prevPostIsComment && sameUser) {
|
||||
sameRoot = true;
|
||||
}
|
||||
|
||||
// hide the profile pic if:
|
||||
// the previous post was made by the same user as the current post,
|
||||
// the previous post is not a comment,
|
||||
// the current post is not a comment,
|
||||
// the previous post is not from a webhook
|
||||
// the current post is not from a webhook
|
||||
if (prevPostUserId === postUserId &&
|
||||
!prevPostIsComment &&
|
||||
!postIsComment &&
|
||||
!prevPostFromWebhook &&
|
||||
!postFromWebhook) {
|
||||
hideProfilePic = true;
|
||||
}
|
||||
}
|
||||
|
||||
// check if it's the last comment in a consecutive string of comments on the same post
|
||||
// it is the last comment if it is last post in the channel or the next post has a different root post
|
||||
const isLastComment = PostUtils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
|
||||
|
||||
const keyPrefix = post.id ? post.id : i;
|
||||
|
||||
const shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id);
|
||||
|
||||
let profile;
|
||||
if (userId === post.user_id) {
|
||||
profile = this.props.currentUser;
|
||||
} else {
|
||||
profile = profiles[post.user_id];
|
||||
}
|
||||
|
||||
let commentCount = 0;
|
||||
let isCommentMention = false;
|
||||
let shouldHighlightThreads = false;
|
||||
let commentRootId;
|
||||
if (parentPost) {
|
||||
commentRootId = post.root_id;
|
||||
} else {
|
||||
commentRootId = post.id;
|
||||
}
|
||||
|
||||
if (commentRootId) {
|
||||
for (const postId in posts) {
|
||||
if (posts[postId].root_id === commentRootId && !PostUtils.isSystemMessage(posts[postId])) {
|
||||
commentCount += 1;
|
||||
if (posts[postId].user_id === userId) {
|
||||
shouldHighlightThreads = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parentPost && commentRootId) {
|
||||
const commentsNotifyLevel = this.props.currentUser.notify_props.comments || 'never';
|
||||
const notCurrentUser = post.user_id !== userId || (post.props && post.props.from_webhook);
|
||||
if (notCurrentUser) {
|
||||
if (commentsNotifyLevel === 'any' && (posts[commentRootId].user_id === userId || shouldHighlightThreads)) {
|
||||
isCommentMention = true;
|
||||
} else if (commentsNotifyLevel === 'root' && posts[commentRootId].user_id === userId) {
|
||||
isCommentMention = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let isFlagged = false;
|
||||
if (this.props.flaggedPosts) {
|
||||
isFlagged = this.props.flaggedPosts.get(post.id) === 'true';
|
||||
}
|
||||
|
||||
let status = '';
|
||||
if (this.props.statuses && profile) {
|
||||
status = this.props.statuses[profile.id] || 'offline';
|
||||
}
|
||||
|
||||
const postCtl = (
|
||||
<Post
|
||||
key={keyPrefix + 'postKey'}
|
||||
ref={post.id}
|
||||
lastPostCount={(i >= 0 && i < Constants.TEST_ID_COUNT) ? i : -1}
|
||||
sameUser={sameUser}
|
||||
sameRoot={sameRoot}
|
||||
post={post}
|
||||
parentPost={parentPost}
|
||||
hideProfilePic={hideProfilePic}
|
||||
isLastComment={isLastComment}
|
||||
shouldHighlight={shouldHighlight}
|
||||
displayNameType={this.props.displayNameType}
|
||||
user={profile}
|
||||
currentUser={this.props.currentUser}
|
||||
center={this.props.displayPostsInCenter}
|
||||
commentCount={commentCount}
|
||||
isCommentMention={isCommentMention}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
previewCollapsed={this.props.previewsCollapsed}
|
||||
useMilitaryTime={this.props.useMilitaryTime}
|
||||
isFlagged={isFlagged}
|
||||
status={status}
|
||||
isBusy={this.props.isBusy}
|
||||
childComponentDidUpdateFunction={this.childComponentDidUpdate}
|
||||
getPostList={this.getPostList}
|
||||
/>
|
||||
);
|
||||
|
||||
const currentPostDay = Utils.getDateForUnixTicks(post.create_at);
|
||||
if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
|
||||
postCtls.push(
|
||||
<div
|
||||
key={currentPostDay.toDateString()}
|
||||
className='date-separator'
|
||||
>
|
||||
<hr className='separator__hr'/>
|
||||
<div className='separator__text'>
|
||||
<FormattedDate
|
||||
value={currentPostDay}
|
||||
weekday='short'
|
||||
month='short'
|
||||
day='2-digit'
|
||||
year='numeric'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ((postUserId !== userId || this.props.ownNewMessage) &&
|
||||
this.props.lastViewed !== 0 &&
|
||||
post.create_at > this.props.lastViewed &&
|
||||
!Utils.isPostEphemeral(post) &&
|
||||
!renderedLastViewed) {
|
||||
renderedLastViewed = true;
|
||||
|
||||
// Temporary fix to solve ie11 rendering issue
|
||||
let newSeparatorId = '';
|
||||
if (!UserAgent.isInternetExplorer()) {
|
||||
newSeparatorId = 'new_message_' + post.id;
|
||||
}
|
||||
postCtls.push(
|
||||
<div
|
||||
id={newSeparatorId}
|
||||
key='unviewed'
|
||||
ref='newMessageSeparator'
|
||||
className='new-separator'
|
||||
>
|
||||
<hr
|
||||
className='separator__hr'
|
||||
/>
|
||||
<div className='separator__text'>
|
||||
<FormattedMessage
|
||||
id='posts_view.newMsg'
|
||||
defaultMessage='New Messages'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
postCtls.push(postCtl);
|
||||
previousPostDay = currentPostDay;
|
||||
}
|
||||
|
||||
return postCtls;
|
||||
}
|
||||
|
||||
updateScrolling() {
|
||||
if (this.props.scrollType === ScrollTypes.BOTTOM) {
|
||||
this.scrollToBottom();
|
||||
} else if (this.props.scrollType === ScrollTypes.NEW_MESSAGE) {
|
||||
window.requestAnimationFrame(() => {
|
||||
// If separator exists scroll to it. Otherwise scroll to bottom.
|
||||
if (this.refs.newMessageSeparator) {
|
||||
var objDiv = this.refs.postlist;
|
||||
objDiv.scrollTop = this.refs.newMessageSeparator.offsetTop; //scrolls node to top of Div
|
||||
} else if (this.refs.postlist) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
});
|
||||
|
||||
// This avoids the scroll jumping from top to bottom after the page has rendered (PLT-5025).
|
||||
if (!this.refs.newMessageSeparator) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
} else if (this.props.scrollType === ScrollTypes.POST && this.props.scrollPostId) {
|
||||
window.requestAnimationFrame(() => {
|
||||
const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]);
|
||||
if (postNode == null) {
|
||||
return;
|
||||
}
|
||||
postNode.scrollIntoView();
|
||||
if (this.refs.postlist.scrollTop === postNode.offsetTop) {
|
||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION);
|
||||
} else {
|
||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION) + (this.refs.postlist.scrollTop - postNode.offsetTop);
|
||||
}
|
||||
});
|
||||
} else if (this.props.scrollType === ScrollTypes.SIDEBAR_OPEN) {
|
||||
// If we are at the bottom then stay there
|
||||
if (this.wasAtBottom) {
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||
} else {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.jumpToPostNode.scrollIntoView();
|
||||
if (this.refs.postlist.scrollTop === this.jumpToPostNode.offsetTop) {
|
||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION);
|
||||
} else {
|
||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION) + (this.refs.postlist.scrollTop - this.jumpToPostNode.offsetTop);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (this.refs.postlist.scrollHeight !== this.prevScrollHeight) {
|
||||
window.requestAnimationFrame(() => {
|
||||
if (this.jumpToPostNode && this.refs.postlist) {
|
||||
this.refs.postlist.scrollTop += (this.jumpToPostNode.offsetTop - this.prevOffsetTop);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.updateScrolling();
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.animationFrameId = window.requestAnimationFrame(() => {
|
||||
if (this.refs.postlist) {
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scrollToBottomAnimated() {
|
||||
if (UserAgent.isIos()) {
|
||||
// JQuery animation doesn't work on iOS
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||
} else {
|
||||
var postList = $(this.refs.postlist);
|
||||
|
||||
postList.animate({scrollTop: this.refs.postlist.scrollHeight}, '500');
|
||||
}
|
||||
}
|
||||
|
||||
getArchivesIntroMessage() {
|
||||
return (
|
||||
<div className={'channel-intro'}>
|
||||
<h4 className='channel-intro__title'>
|
||||
<FormattedMessage
|
||||
id='post_focus_view.beginning'
|
||||
defaultMessage='Beginning of Channel Archives'
|
||||
/>
|
||||
</h4>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
checkAndUpdateScrolling() {
|
||||
if (this.props.postList != null && this.refs.postlist) {
|
||||
this.updateScrolling();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.postList != null) {
|
||||
this.updateScrolling();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
PostStore.addPostDraftChangeListener(this.props.channelId, this.handlePostDraftChange);
|
||||
ScrollStore.addPostScrollListener(this.checkAndUpdateScrolling);
|
||||
}
|
||||
|
||||
handlePostDraftChange = (draft) => {
|
||||
// this.state.draft isn't used anywhere, but this will cause an update to the scroll position
|
||||
// without causing two updates to trigger when something else changes
|
||||
this.setState({
|
||||
draft
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.cancelAnimationFrame(this.animationFrameId);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
ScrollStore.removePostScrollListener(this.checkAndUpdateScrolling);
|
||||
this.scrollStopAction.cancel();
|
||||
|
||||
PostStore.removePostDraftChangeListener(this.props.channelId, this.handlePostDraftChange);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.checkAndUpdateScrolling();
|
||||
}
|
||||
|
||||
childComponentDidUpdate() {
|
||||
this.checkAndUpdateScrolling();
|
||||
}
|
||||
|
||||
getPostList = () => {
|
||||
return this.refs.postlist;
|
||||
}
|
||||
|
||||
render() {
|
||||
// Create intro message or top loadmore link
|
||||
let moreMessagesTop;
|
||||
if (this.props.showMoreMessagesTop) {
|
||||
moreMessagesTop = (
|
||||
<a
|
||||
ref='loadmoretop'
|
||||
className='more-messages-text theme'
|
||||
href='#'
|
||||
onClick={this.loadMorePostsTop}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='posts_view.loadMore'
|
||||
defaultMessage='Load more messages'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
moreMessagesTop = this.introText;
|
||||
}
|
||||
|
||||
// Give option to load more posts at bottom if necessary
|
||||
let moreMessagesBottom;
|
||||
if (this.props.showMoreMessagesBottom) {
|
||||
moreMessagesBottom = (
|
||||
<a
|
||||
ref='loadmorebottom'
|
||||
className='more-messages-text theme'
|
||||
href='#'
|
||||
onClick={this.loadMorePostsBottom}
|
||||
>
|
||||
<FormattedMessage id='posts_view.loadMore'/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// Create post elements
|
||||
let postElements = null;
|
||||
let topPostCreateAt = 0;
|
||||
if (this.props.postList) {
|
||||
const posts = this.props.postList.posts;
|
||||
const order = this.props.postList.order;
|
||||
|
||||
postElements = this.createPosts(posts, order);
|
||||
|
||||
if (this.state.topPostId && this.props.postList.posts[this.state.topPostId]) {
|
||||
topPostCreateAt = this.props.postList.posts[this.state.topPostId].create_at;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FloatingTimestamp
|
||||
isScrolling={this.state.isScrolling}
|
||||
isMobile={Utils.isMobile()}
|
||||
createAt={topPostCreateAt}
|
||||
/>
|
||||
<ScrollToBottomArrows
|
||||
isScrolling={this.state.isScrolling}
|
||||
atBottom={this.wasAtBottom}
|
||||
onClick={this.scrollToBottomAnimated}
|
||||
/>
|
||||
<NewMessageIndicator
|
||||
newMessages={this.state.unViewedCount}
|
||||
onClick={this.scrollToBottomAnimated}
|
||||
/>
|
||||
<div
|
||||
ref='postlist'
|
||||
className='post-list-holder-by-time'
|
||||
onScroll={this.handleScroll}
|
||||
>
|
||||
<div className='post-list__table'>
|
||||
<div
|
||||
ref='postlistcontent'
|
||||
className='post-list__content'
|
||||
>
|
||||
{moreMessagesTop}
|
||||
{postElements}
|
||||
{moreMessagesBottom}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostList.defaultProps = {
|
||||
lastViewed: 0,
|
||||
lastViewedBottom: Number.MAX_VALUE,
|
||||
ownNewMessage: false
|
||||
};
|
||||
|
||||
PostList.propTypes = {
|
||||
postList: PropTypes.object,
|
||||
profiles: PropTypes.object,
|
||||
channel: PropTypes.object,
|
||||
channelId: PropTypes.string.isRequired,
|
||||
currentUser: PropTypes.object,
|
||||
scrollPostId: PropTypes.string,
|
||||
scrollType: PropTypes.number,
|
||||
postListScrolled: PropTypes.func.isRequired,
|
||||
showMoreMessagesTop: PropTypes.bool,
|
||||
showMoreMessagesBottom: PropTypes.bool,
|
||||
lastViewed: PropTypes.number,
|
||||
lastViewedBottom: PropTypes.number,
|
||||
ownNewMessage: PropTypes.bool,
|
||||
postsToHighlight: PropTypes.object,
|
||||
displayNameType: PropTypes.string,
|
||||
displayPostsInCenter: PropTypes.bool,
|
||||
compactDisplay: PropTypes.bool,
|
||||
previewsCollapsed: PropTypes.string,
|
||||
useMilitaryTime: PropTypes.bool.isRequired,
|
||||
isFocusPost: PropTypes.bool,
|
||||
flaggedPosts: PropTypes.object,
|
||||
statuses: PropTypes.object,
|
||||
isBusy: PropTypes.bool
|
||||
};
|
||||
@@ -1,106 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import EmojiStore from 'stores/emoji_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import {Preferences} from 'utils/constants.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import PostMessageView from './post_message_view.jsx';
|
||||
|
||||
export default class PostMessageContainer extends React.Component {
|
||||
static propTypes = {
|
||||
post: PropTypes.object.isRequired,
|
||||
options: PropTypes.object,
|
||||
lastPostCount: PropTypes.number
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
options: {}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onEmojiChange = this.onEmojiChange.bind(this);
|
||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||
this.onUserChange = this.onUserChange.bind(this);
|
||||
this.onChannelChange = this.onChannelChange.bind(this);
|
||||
|
||||
const mentionKeys = UserStore.getCurrentMentionKeys();
|
||||
mentionKeys.push('@here');
|
||||
|
||||
this.state = {
|
||||
emojis: EmojiStore.getEmojis(),
|
||||
enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
|
||||
mentionKeys,
|
||||
usernameMap: UserStore.getProfilesUsernameMap(),
|
||||
channelNamesMap: ChannelStore.getChannelNamesMap(),
|
||||
team: TeamStore.getCurrent()
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EmojiStore.addChangeListener(this.onEmojiChange);
|
||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
||||
UserStore.addChangeListener(this.onUserChange);
|
||||
ChannelStore.addChangeListener(this.onChannelChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EmojiStore.removeChangeListener(this.onEmojiChange);
|
||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
||||
UserStore.removeChangeListener(this.onUserChange);
|
||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
||||
}
|
||||
|
||||
onEmojiChange() {
|
||||
this.setState({
|
||||
emojis: EmojiStore.getEmojis()
|
||||
});
|
||||
}
|
||||
|
||||
onPreferenceChange() {
|
||||
this.setState({
|
||||
enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true)
|
||||
});
|
||||
}
|
||||
|
||||
onUserChange() {
|
||||
const mentionKeys = UserStore.getCurrentMentionKeys();
|
||||
mentionKeys.push('@here');
|
||||
|
||||
this.setState({
|
||||
mentionKeys,
|
||||
usernameMap: UserStore.getProfilesUsernameMap()
|
||||
});
|
||||
}
|
||||
|
||||
onChannelChange() {
|
||||
this.setState({
|
||||
channelNamesMap: ChannelStore.getChannelNamesMap()
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PostMessageView
|
||||
options={this.props.options}
|
||||
post={this.props.post}
|
||||
lastPostCount={this.props.lastPostCount}
|
||||
emojis={this.state.emojis}
|
||||
enableFormatting={this.state.enableFormatting}
|
||||
mentionKeys={this.state.mentionKeys}
|
||||
usernameMap={this.state.usernameMap}
|
||||
channelNamesMap={this.state.channelNamesMap}
|
||||
team={this.state.team}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {addReaction, removeReaction} from 'actions/post_actions.jsx';
|
||||
import * as UserActions from 'actions/user_actions.jsx';
|
||||
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import Reaction from './reaction.jsx';
|
||||
|
||||
export default class ReactionContainer extends React.Component {
|
||||
static propTypes = {
|
||||
post: PropTypes.object.isRequired,
|
||||
emojiName: PropTypes.string.isRequired,
|
||||
reactions: PropTypes.arrayOf(PropTypes.object),
|
||||
emojis: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleUsersChanged = this.handleUsersChanged.bind(this);
|
||||
|
||||
this.getStateFromStore = this.getStateFromStore.bind(this);
|
||||
|
||||
this.getProfilesForReactions = this.getProfilesForReactions.bind(this);
|
||||
this.getMissingProfiles = this.getMissingProfiles.bind(this);
|
||||
|
||||
this.state = this.getStateFromStore(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
UserStore.addChangeListener(this.handleUsersChanged);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.reactions !== this.props.reactions) {
|
||||
this.setState(this.getStateFromStore(nextProps));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
UserStore.removeChangeListener(this.handleUsersChanged);
|
||||
}
|
||||
|
||||
handleUsersChanged() {
|
||||
this.setState(this.getStateFromStore());
|
||||
}
|
||||
|
||||
getStateFromStore(props = this.props) {
|
||||
const profiles = this.getProfilesForReactions(props.reactions);
|
||||
const otherUsers = props.reactions.length - profiles.length;
|
||||
|
||||
return {
|
||||
profiles,
|
||||
otherUsers,
|
||||
currentUserId: UserStore.getCurrentId()
|
||||
};
|
||||
}
|
||||
|
||||
getProfilesForReactions(reactions) {
|
||||
return reactions.map((reaction) => {
|
||||
return UserStore.getProfile(reaction.user_id);
|
||||
}).filter((profile) => Boolean(profile));
|
||||
}
|
||||
|
||||
getMissingProfiles() {
|
||||
const ids = this.props.reactions.map((reaction) => reaction.user_id);
|
||||
|
||||
UserActions.getMissingProfiles(ids);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Reaction
|
||||
{...this.props}
|
||||
{...this.state}
|
||||
actions={{
|
||||
addReaction,
|
||||
getMissingProfiles: this.getMissingProfiles,
|
||||
removeReaction
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import EmojiStore from 'stores/emoji_store.jsx';
|
||||
import ReactionStore from 'stores/reaction_store.jsx';
|
||||
|
||||
import ReactionListView from './reaction_list_view.jsx';
|
||||
|
||||
export default class ReactionListContainer extends React.Component {
|
||||
static propTypes = {
|
||||
post: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleReactionsChanged = this.handleReactionsChanged.bind(this);
|
||||
this.handleEmojisChanged = this.handleEmojisChanged.bind(this);
|
||||
|
||||
this.state = {
|
||||
reactions: ReactionStore.getReactions(this.props.post.id),
|
||||
emojis: EmojiStore.getEmojis()
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ReactionStore.addChangeListener(this.props.post.id, this.handleReactionsChanged);
|
||||
EmojiStore.addChangeListener(this.handleEmojisChanged);
|
||||
|
||||
if (this.props.post.has_reactions) {
|
||||
AsyncClient.listReactions(this.props.post.channel_id, this.props.post.id);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.post.id !== this.props.post.id) {
|
||||
ReactionStore.removeChangeListener(this.props.post.id, this.handleReactionsChanged);
|
||||
ReactionStore.addChangeListener(nextProps.post.id, this.handleReactionsChanged);
|
||||
|
||||
this.setState({
|
||||
reactions: ReactionStore.getReactions(nextProps.post.id)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (nextProps.post.has_reactions !== this.props.post.has_reactions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.reactions !== this.state.reactions) {
|
||||
// this will only work so long as the entries in the ReactionStore are never mutated
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.emojis !== this.state.emojis) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ReactionStore.removeChangeListener(this.props.post.id, this.handleReactionsChanged);
|
||||
EmojiStore.removeChangeListener(this.handleEmojisChanged);
|
||||
}
|
||||
|
||||
handleReactionsChanged() {
|
||||
this.setState({
|
||||
reactions: ReactionStore.getReactions(this.props.post.id)
|
||||
});
|
||||
}
|
||||
|
||||
handleEmojisChanged() {
|
||||
this.setState({
|
||||
emojis: EmojiStore.getEmojis()
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ReactionListView
|
||||
post={this.props.post}
|
||||
reactions={this.state.reactions}
|
||||
emojis={this.state.emojis}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
32
webapp/components/post_view/date_separator.jsx
Обычный файл
32
webapp/components/post_view/date_separator.jsx
Обычный файл
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FormattedDate} from 'react-intl';
|
||||
|
||||
export default class DateSeparator extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/*
|
||||
* The date to display in the separator
|
||||
*/
|
||||
date: PropTypes.instanceOf(Date)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className='date-separator'
|
||||
>
|
||||
<hr className='separator__hr'/>
|
||||
<div className='separator__text'>
|
||||
<FormattedDate
|
||||
value={this.props.date}
|
||||
weekday='short'
|
||||
month='short'
|
||||
day='2-digit'
|
||||
year='numeric'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
|
||||
import {queuePost} from 'actions/post_actions.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import {createPost} from 'actions/post_actions.jsx';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
export default class FailedPostOptions extends React.Component {
|
||||
static propTypes = {
|
||||
|
||||
/*
|
||||
* The failed post
|
||||
*/
|
||||
post: PropTypes.object.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
|
||||
/**
|
||||
* The function to delete the post
|
||||
*/
|
||||
removePost: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}
|
||||
|
||||
export default class PendingPostOptions extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -24,6 +33,7 @@ export default class PendingPostOptions extends React.Component {
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
retryPost(e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -33,8 +43,12 @@ export default class PendingPostOptions extends React.Component {
|
||||
|
||||
this.submitting = true;
|
||||
|
||||
var post = this.props.post;
|
||||
queuePost(post, true, null,
|
||||
const post = {...this.props.post};
|
||||
Reflect.deleteProperty(post, 'id');
|
||||
createPost(post,
|
||||
() => {
|
||||
this.submitting = false;
|
||||
},
|
||||
(err) => {
|
||||
if (err.id === 'api.post.create_post.root_id.app_error') {
|
||||
this.showPostDeletedModal();
|
||||
@@ -45,18 +59,13 @@ export default class PendingPostOptions extends React.Component {
|
||||
this.submitting = false;
|
||||
}
|
||||
);
|
||||
|
||||
post.state = Constants.POST_LOADING;
|
||||
PostStore.updatePendingPost(post);
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
cancelPost(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var post = this.props.post;
|
||||
PostStore.removePendingPost(post.channel_id, post.pending_post_id);
|
||||
this.forceUpdate();
|
||||
this.props.actions.removePost(this.props.post);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<span className='pending-post-actions'>
|
||||
<a
|
||||
@@ -83,7 +92,3 @@ export default class PendingPostOptions extends React.Component {
|
||||
</span>);
|
||||
}
|
||||
}
|
||||
|
||||
PendingPostOptions.propTypes = {
|
||||
post: PropTypes.object
|
||||
};
|
||||
24
webapp/components/post_view/failed_post_options/index.js
Обычный файл
24
webapp/components/post_view/failed_post_options/index.js
Обычный файл
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {removePost} from 'mattermost-redux/actions/posts';
|
||||
|
||||
import FailedPostOptions from './failed_post_options.jsx';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
removePost
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FailedPostOptions);
|
||||
@@ -3,16 +3,15 @@
|
||||
|
||||
import {FormattedDate} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
|
||||
export default class FloatingTimestamp extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
export default class FloatingTimestamp extends React.PureComponent {
|
||||
static propTypes = {
|
||||
isScrolling: PropTypes.bool.isRequired,
|
||||
isMobile: PropTypes.bool,
|
||||
createAt: PropTypes.number,
|
||||
isRhsPost: PropTypes.bool
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -52,10 +51,3 @@ export default class FloatingTimestamp extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FloatingTimestamp.propTypes = {
|
||||
isScrolling: PropTypes.bool.isRequired,
|
||||
isMobile: PropTypes.bool,
|
||||
createAt: PropTypes.number,
|
||||
isRhsPost: PropTypes.bool
|
||||
};
|
||||
@@ -3,22 +3,52 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {viewChannel} from 'mattermost-redux/actions/channels';
|
||||
|
||||
import PostViewCache from './post_view_cache.jsx';
|
||||
import {makeGetPostsInChannel, makeGetPostsAroundPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getPosts, getPostsBefore, getPostsAfter, getPostThread} from 'mattermost-redux/actions/posts';
|
||||
import {increasePostVisibility} from 'actions/post_actions.jsx';
|
||||
import {Preferences} from 'utils/constants.jsx';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps
|
||||
import PostList from './post_list.jsx';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getPostsInChannel = makeGetPostsInChannel();
|
||||
const getPostsAroundPost = makeGetPostsAroundPost();
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
let posts;
|
||||
if (ownProps.focusedPostId) {
|
||||
posts = getPostsAroundPost(state, ownProps.focusedPostId, ownProps.channelId);
|
||||
} else {
|
||||
posts = getPostsInChannel(state, ownProps.channelId);
|
||||
}
|
||||
|
||||
return {
|
||||
channel: getChannel(state, ownProps.channelId),
|
||||
lastViewedAt: state.views.channel.lastChannelViewTime[ownProps.channelId],
|
||||
posts,
|
||||
postVisibility: state.views.channel.postVisibility[ownProps.channelId],
|
||||
loadingPosts: state.views.channel.loadingPosts[ownProps.channelId],
|
||||
focusedPostId: ownProps.focusedPostId,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
fullWidth: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
viewChannel
|
||||
getPosts,
|
||||
getPostsBefore,
|
||||
getPostsAfter,
|
||||
getPostThread,
|
||||
increasePostVisibility
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PostViewCache);
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(PostList);
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
export default class NewMessageIndicator extends React.Component {
|
||||
export default class NewMessageIndicator extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
newMessages: PropTypes.number
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -13,6 +18,7 @@ export default class NewMessageIndicator extends React.Component {
|
||||
rendered: false
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.newMessages > 0) {
|
||||
this.setState({rendered: true}, () => {
|
||||
@@ -22,6 +28,7 @@ export default class NewMessageIndicator extends React.Component {
|
||||
this.setState({visible: false});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let className = 'new-messages__button';
|
||||
if (this.state.visible > 0) {
|
||||
@@ -56,11 +63,7 @@ export default class NewMessageIndicator extends React.Component {
|
||||
this.setState({rendered: this.state.visible});
|
||||
}
|
||||
}
|
||||
|
||||
NewMessageIndicator.defaultProps = {
|
||||
newMessages: 0
|
||||
};
|
||||
|
||||
NewMessageIndicator.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
newMessages: PropTypes.number
|
||||
};
|
||||
33
webapp/components/post_view/post/index.js
Обычный файл
33
webapp/components/post_view/post/index.js
Обычный файл
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCurrentUser, getUser, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
|
||||
import {Preferences} from 'utils/constants.jsx';
|
||||
|
||||
import Post from './post.jsx';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const detailedPost = ownProps.post;
|
||||
return {
|
||||
post: getPost(state, detailedPost.id),
|
||||
lastPostCount: ownProps.lastPostCount,
|
||||
user: getUser(state, ownProps.post.user_id),
|
||||
status: getStatusForUserId(state, ownProps.post.user_id),
|
||||
currentUser: getCurrentUser(state),
|
||||
isFirstReply: Boolean(detailedPost.isFirstReply && detailedPost.commentedOnPost),
|
||||
highlight: detailedPost.highlight,
|
||||
consecutivePostByUser: detailedPost.consecutivePostByUser,
|
||||
previousPostIsComment: detailedPost.previousPostIsComment,
|
||||
replyCount: detailedPost.replyCount,
|
||||
isCommentMention: detailedPost.isCommentMention,
|
||||
center: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
||||
compactDisplay: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Post);
|
||||
@@ -1,46 +1,99 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import PostHeader from 'components/post_view/post_header';
|
||||
import PostBody from 'components/post_view/post_body';
|
||||
import ProfilePicture from 'components/profile_picture.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as PostUtils from 'utils/post_utils.jsx';
|
||||
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
||||
|
||||
import Constants, {ActionTypes} from 'utils/constants.jsx';
|
||||
import * as PostUtils from 'utils/post_utils.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import PostBody from './post_body.jsx';
|
||||
import PostHeader from './post_header.jsx';
|
||||
|
||||
export default class Post extends Component {
|
||||
export default class Post extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* The post to render
|
||||
*/
|
||||
post: PropTypes.object.isRequired,
|
||||
parentPost: PropTypes.object,
|
||||
|
||||
/**
|
||||
* The user who created the post
|
||||
*/
|
||||
user: PropTypes.object,
|
||||
sameUser: PropTypes.bool,
|
||||
sameRoot: PropTypes.bool,
|
||||
hideProfilePic: PropTypes.bool,
|
||||
lastPostCount: PropTypes.number,
|
||||
isLastComment: PropTypes.bool,
|
||||
shouldHighlight: PropTypes.bool,
|
||||
displayNameType: PropTypes.string,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
center: PropTypes.bool,
|
||||
compactDisplay: PropTypes.bool,
|
||||
previewCollapsed: PropTypes.string,
|
||||
commentCount: PropTypes.number,
|
||||
isCommentMention: PropTypes.bool,
|
||||
useMilitaryTime: PropTypes.bool.isRequired,
|
||||
isFlagged: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* The status of the poster
|
||||
*/
|
||||
status: PropTypes.string,
|
||||
|
||||
/**
|
||||
* The logged in user
|
||||
*/
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
|
||||
/**
|
||||
* Set to center the post
|
||||
*/
|
||||
center: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Set to render post compactly
|
||||
*/
|
||||
compactDisplay: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Set to render a preview of the parent post above this reply
|
||||
*/
|
||||
isFirstReply: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Set to highlight the background of the post
|
||||
*/
|
||||
highlight: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Set to render this post as if it was attached to the previous post
|
||||
*/
|
||||
consecutivePostByUser: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Set if the previous post is a comment
|
||||
*/
|
||||
previousPostIsComment: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Set to render this comment as a mention
|
||||
*/
|
||||
isCommentMention: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* The number of replies in the same thread as this post
|
||||
*/
|
||||
replyCount: PropTypes.number,
|
||||
|
||||
/**
|
||||
* Set to mark the poster as in a webrtc call
|
||||
*/
|
||||
isBusy: PropTypes.bool,
|
||||
childComponentDidUpdateFunction: PropTypes.func,
|
||||
|
||||
/**
|
||||
* The post count used for selenium tests
|
||||
*/
|
||||
lastPostCount: PropTypes.number,
|
||||
|
||||
/**
|
||||
* Function to get the post list HTML element
|
||||
*/
|
||||
getPostList: PropTypes.func.isRequired
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -75,91 +128,23 @@ export default class Post extends Component {
|
||||
this.refs.header.forceUpdate();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.sameRoot !== this.props.sameRoot) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.sameUser !== this.props.sameUser) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.displayNameType !== this.props.displayNameType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.commentCount !== this.props.commentCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.isCommentMention !== this.props.isCommentMention) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.shouldHighlight !== this.props.shouldHighlight) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.center !== this.props.center) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.compactDisplay !== this.props.compactDisplay) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.previewCollapsed !== this.props.previewCollapsed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.useMilitaryTime !== this.props.useMilitaryTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.isFlagged !== this.props.isFlagged) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.status !== this.props.status) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.dropdownOpened !== this.state.dropdownOpened) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.isBusy !== this.props.isBusy) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.lastPostCount !== this.props.lastPostCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getClassName = (post, isSystemMessage, fromWebhook) => {
|
||||
let className = 'post';
|
||||
|
||||
if (post.state === Constants.POST_DELETED || post.state === Constants.POST_FAILED) {
|
||||
if (post.failed || post.state === Posts.POST_DELETED) {
|
||||
className += ' post--hide-controls';
|
||||
}
|
||||
|
||||
if (this.props.shouldHighlight) {
|
||||
if (this.props.highlight) {
|
||||
className += ' post--highlight';
|
||||
}
|
||||
|
||||
let rootUser;
|
||||
if (this.props.sameRoot) {
|
||||
let rootUser = '';
|
||||
if (this.props.isFirstReply) {
|
||||
rootUser = 'other--root';
|
||||
} else if (!post.root_id && !this.props.previousPostIsComment && this.props.consecutivePostByUser) {
|
||||
rootUser = 'same--root';
|
||||
} else if (post.root_id) {
|
||||
rootUser = 'same--root';
|
||||
} else {
|
||||
rootUser = 'other--root';
|
||||
@@ -171,14 +156,14 @@ export default class Post extends Component {
|
||||
}
|
||||
|
||||
let sameUserClass = '';
|
||||
if (this.props.sameUser) {
|
||||
if (this.props.consecutivePostByUser) {
|
||||
sameUserClass = 'same--user';
|
||||
}
|
||||
|
||||
let postType = '';
|
||||
if (post.root_id && post.root_id.length > 0) {
|
||||
postType = 'post--comment';
|
||||
} else if (this.props.commentCount > 0) {
|
||||
} else if (this.props.replyCount > 0) {
|
||||
postType = 'post--root';
|
||||
sameUserClass = '';
|
||||
rootUser = '';
|
||||
@@ -209,7 +194,6 @@ export default class Post extends Component {
|
||||
|
||||
render() {
|
||||
const post = this.props.post;
|
||||
const parentPost = this.props.parentPost;
|
||||
const mattermostLogo = Constants.MATTERMOST_ICON_SVG;
|
||||
|
||||
const isSystemMessage = PostUtils.isSystemMessage(post);
|
||||
@@ -242,7 +226,7 @@ export default class Post extends Component {
|
||||
src={PostUtils.getProfilePicSrcForPost(post, timestamp)}
|
||||
/>
|
||||
);
|
||||
} else if (isSystemMessage) {
|
||||
} else if (PostUtils.isSystemMessage(post)) {
|
||||
profilePic = (
|
||||
<span
|
||||
className='icon'
|
||||
@@ -257,7 +241,7 @@ export default class Post extends Component {
|
||||
}
|
||||
|
||||
if (this.props.compactDisplay) {
|
||||
if (post.props && post.props.from_webhook) {
|
||||
if (fromWebhook) {
|
||||
profilePic = (
|
||||
<ProfilePicture
|
||||
src=''
|
||||
@@ -294,34 +278,24 @@ export default class Post extends Component {
|
||||
<PostHeader
|
||||
ref='header'
|
||||
post={post}
|
||||
sameRoot={this.props.sameRoot}
|
||||
lastPostCount={this.props.lastPostCount}
|
||||
commentCount={this.props.commentCount}
|
||||
handleCommentClick={this.handleCommentClick}
|
||||
handleDropdownOpened={this.handleDropdownOpened}
|
||||
isLastComment={this.props.isLastComment}
|
||||
sameUser={this.props.sameUser}
|
||||
user={this.props.user}
|
||||
currentUser={this.props.currentUser}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
displayNameType={this.props.displayNameType}
|
||||
useMilitaryTime={this.props.useMilitaryTime}
|
||||
isFlagged={this.props.isFlagged}
|
||||
status={this.props.status}
|
||||
isBusy={this.props.isBusy}
|
||||
lastPostCount={this.props.lastPostCount}
|
||||
replyCount={this.props.replyCount}
|
||||
consecutivePostByUser={this.props.consecutivePostByUser}
|
||||
getPostList={this.props.getPostList}
|
||||
/>
|
||||
<PostBody
|
||||
post={post}
|
||||
currentUser={this.props.currentUser}
|
||||
sameRoot={this.props.sameRoot}
|
||||
lastPostCount={this.props.lastPostCount}
|
||||
parentPost={parentPost}
|
||||
handleCommentClick={this.handleCommentClick}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
previewCollapsed={this.props.previewCollapsed}
|
||||
lastPostCount={this.props.lastPostCount}
|
||||
isCommentMention={this.props.isCommentMention}
|
||||
childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,27 +1,22 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import * as TextFormatting from 'utils/text_formatting.jsx';
|
||||
import {localizeMessage} from 'utils/utils.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
collapse: {
|
||||
id: 'post_attachment.collapse',
|
||||
defaultMessage: 'Show less...'
|
||||
},
|
||||
more: {
|
||||
id: 'post_attachment.more',
|
||||
defaultMessage: 'Show more...'
|
||||
}
|
||||
});
|
||||
|
||||
import $ from 'jquery';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React from 'react';
|
||||
export default class PostAttachment extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* The attachment to render
|
||||
*/
|
||||
attachment: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
class PostAttachment extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -46,7 +41,7 @@ class PostAttachment extends React.Component {
|
||||
getInitState() {
|
||||
const shouldCollapse = this.shouldCollapse();
|
||||
const text = TextFormatting.formatText(this.props.attachment.text || '');
|
||||
const uncollapsedText = text + (shouldCollapse ? `<div><a class="attachment-link-more" href="#">${this.props.intl.formatMessage(holders.collapse)}</a></div>` : '');
|
||||
const uncollapsedText = text + (shouldCollapse ? `<div><a class="attachment-link-more" href="#">${localizeMessage('post_attachment.collapse', 'Show less...')}</a></div>` : '');
|
||||
const collapsedText = shouldCollapse ? this.getCollapsedText() : text;
|
||||
|
||||
return {
|
||||
@@ -61,10 +56,10 @@ class PostAttachment extends React.Component {
|
||||
toggleCollapseState(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const state = this.state;
|
||||
state.text = state.collapsed ? state.uncollapsedText : state.collapsedText;
|
||||
state.collapsed = !state.collapsed;
|
||||
this.setState(state);
|
||||
this.setState({
|
||||
text: this.state.collapsed ? this.state.uncollapsedText : this.state.collapsedText,
|
||||
collapsed: !this.state.collapsed
|
||||
});
|
||||
}
|
||||
|
||||
shouldCollapse() {
|
||||
@@ -80,7 +75,7 @@ class PostAttachment extends React.Component {
|
||||
text = text.substr(0, 700);
|
||||
}
|
||||
|
||||
return TextFormatting.formatText(text) + `<div><a class="attachment-link-more" href="#">${this.props.intl.formatMessage(holders.more)}</a></div>`;
|
||||
return TextFormatting.formatText(text) + `<div><a class="attachment-link-more" href="#">${localizeMessage('post_attachment.more', 'Show more...')}</a></div>`;
|
||||
}
|
||||
|
||||
getFieldsTable() {
|
||||
@@ -314,10 +309,3 @@ class PostAttachment extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostAttachment.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
attachment: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(PostAttachment);
|
||||
35
webapp/components/post_view/post_attachment_list.jsx
Обычный файл
35
webapp/components/post_view/post_attachment_list.jsx
Обычный файл
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostAttachment from './post_attachment.jsx';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class PostAttachmentList extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* Array of attachments to render
|
||||
*/
|
||||
attachments: PropTypes.array.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = [];
|
||||
this.props.attachments.forEach((attachment, i) => {
|
||||
content.push(
|
||||
<PostAttachment
|
||||
attachment={attachment}
|
||||
key={'att_' + i}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='attachment_list'>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
26
webapp/components/post_view/post_attachment_opengraph/index.js
Обычный файл
26
webapp/components/post_view/post_attachment_opengraph/index.js
Обычный файл
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
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 PostAttachmentOpenGraph from './post_attachment_opengraph.jsx';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
openGraphData: getOpenGraphMetadataForUrl(state, ownProps.link)
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getOpenGraphMetadata
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PostAttachmentOpenGraph);
|
||||
@@ -1,16 +1,38 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import OpenGraphStore from 'stores/opengraph_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as CommonUtils from 'utils/commons.jsx';
|
||||
import {requestOpenGraphMetadata} from 'actions/global_actions.jsx';
|
||||
|
||||
export default class PostAttachmentOpenGraph extends React.Component {
|
||||
export default class PostAttachmentOpenGraph extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* The link to display the open graph data for
|
||||
*/
|
||||
link: PropTypes.string.isRequired,
|
||||
|
||||
/**
|
||||
* The open graph data to render
|
||||
*/
|
||||
openGraphData: PropTypes.object.isRequired,
|
||||
|
||||
/**
|
||||
* Set to collapse the preview
|
||||
*/
|
||||
previewCollapsed: PropTypes.string,
|
||||
actions: PropTypes.shape({
|
||||
|
||||
/**
|
||||
* The function to get open graph data for a link
|
||||
*/
|
||||
getOpenGraphMetadata: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.largeImageMinWidth = 150;
|
||||
@@ -29,7 +51,6 @@ export default class PostAttachmentOpenGraph extends React.Component {
|
||||
this.smallImageElement = null;
|
||||
|
||||
this.fetchData = this.fetchData.bind(this);
|
||||
this.onOpenGraphMetadataChange = this.onOpenGraphMetadataChange.bind(this);
|
||||
this.toggleImageVisibility = this.toggleImageVisibility.bind(this);
|
||||
this.onImageLoad = this.onImageLoad.bind(this);
|
||||
this.onImageError = this.onImageError.bind(this);
|
||||
@@ -44,7 +65,6 @@ export default class PostAttachmentOpenGraph extends React.Component {
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
data: {},
|
||||
imageLoaded: this.IMAGE_LOADED.LOADING,
|
||||
imageVisible: this.props.previewCollapsed.startsWith('false'),
|
||||
hasLargeImage: false
|
||||
@@ -53,61 +73,23 @@ export default class PostAttachmentOpenGraph extends React.Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!Utils.areObjectsEqual(nextProps.link, this.props.link)) {
|
||||
if (nextProps.link !== this.props.link) {
|
||||
this.fetchData(nextProps.link);
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (nextState.imageVisible !== this.state.imageVisible) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.hasLargeImage !== this.state.hasLargeImage) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.imageLoaded !== this.state.imageLoaded) {
|
||||
return true;
|
||||
}
|
||||
if (!Utils.areObjectsEqual(nextState.data, this.state.data)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
OpenGraphStore.addUrlDataChangeListener(this.onOpenGraphMetadataChange);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.childComponentDidUpdateFunction) {
|
||||
this.props.childComponentDidUpdateFunction();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
OpenGraphStore.removeUrlDataChangeListener(this.onOpenGraphMetadataChange);
|
||||
}
|
||||
|
||||
onOpenGraphMetadataChange(url) {
|
||||
if (url === this.props.link) {
|
||||
this.fetchData(url);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData(url) {
|
||||
const data = OpenGraphStore.getOgInfo(url);
|
||||
this.setState({data, imageLoaded: this.IMAGE_LOADED.LOADING});
|
||||
if (Utils.isEmptyObject(data)) {
|
||||
requestOpenGraphMetadata(url);
|
||||
if (!this.props.openGraphData) {
|
||||
this.props.actions.getOpenGraphMetadata(url);
|
||||
}
|
||||
}
|
||||
|
||||
getBestImageUrl() {
|
||||
if (Utils.isEmptyObject(this.state.data.images)) {
|
||||
if (Utils.isEmptyObject(this.props.openGraphData.images)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bestImage = CommonUtils.getNearestPoint(this.imageDimentions, this.state.data.images, 'width', 'height');
|
||||
const bestImage = CommonUtils.getNearestPoint(this.imageDimentions, this.props.openGraphData.images, 'width', 'height');
|
||||
return bestImage.secure_url || bestImage.url;
|
||||
}
|
||||
|
||||
@@ -217,11 +199,11 @@ export default class PostAttachmentOpenGraph extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (Utils.isEmptyObject(this.state.data) || Utils.isEmptyObject(this.state.data.description)) {
|
||||
if (!this.props.openGraphData || Utils.isEmptyObject(this.props.openGraphData.description)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = this.state.data;
|
||||
const data = this.props.openGraphData;
|
||||
const imageUrl = this.getBestImageUrl();
|
||||
|
||||
if (imageUrl) {
|
||||
@@ -275,13 +257,3 @@ export default class PostAttachmentOpenGraph extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostAttachmentOpenGraph.defaultProps = {
|
||||
previewCollapsed: 'false'
|
||||
};
|
||||
|
||||
PostAttachmentOpenGraph.propTypes = {
|
||||
link: PropTypes.string.isRequired,
|
||||
childComponentDidUpdateFunction: PropTypes.func,
|
||||
previewCollapsed: PropTypes.string
|
||||
};
|
||||
30
webapp/components/post_view/post_body/index.js
Обычный файл
30
webapp/components/post_view/post_body/index.js
Обычный файл
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
|
||||
import {Preferences} from 'utils/constants.jsx';
|
||||
|
||||
import PostBody from './post_body.jsx';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
let parentPost;
|
||||
let parentPostUser;
|
||||
if (ownProps.post.root_id) {
|
||||
parentPost = getPost(state, ownProps.post.root_id);
|
||||
parentPostUser = getUser(state, parentPost.user_id);
|
||||
}
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
parentPost,
|
||||
parentPostUser,
|
||||
previewCollapsed: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(PostBody);
|
||||
@@ -1,67 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||
import * as PostUtils from 'utils/post_utils.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import CommentedOnFilesMessageContainer from './commented_on_files_message_container.jsx';
|
||||
import FileAttachmentListContainer from 'components/file_attachment_list_container.jsx';
|
||||
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
|
||||
import PostMessageContainer from './post_message_container.jsx';
|
||||
import PendingPostOptions from './pending_post_options.jsx';
|
||||
import ReactionListContainer from './reaction_list_container.jsx';
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import loadingGif from 'images/load.gif';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import CommentedOnFilesMessage from 'components/post_view/commented_on_files_message';
|
||||
import FileAttachmentListContainer from 'components/file_attachment_list';
|
||||
import PostBodyAdditionalContent from 'components/post_view/post_body_additional_content.jsx';
|
||||
import PostMessageContainer from 'components/post_view/post_message_view';
|
||||
import ReactionListContainer from 'components/post_view/reaction_list';
|
||||
import FailedPostOptions from 'components/post_view/failed_post_options';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
export default class PostBody extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
export default class PostBody extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
this.removePost = this.removePost.bind(this);
|
||||
}
|
||||
/**
|
||||
* The post to render the body of
|
||||
*/
|
||||
post: PropTypes.object.isRequired,
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.isCommentMention !== this.props.isCommentMention) {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* The parent post of the thread this post is in
|
||||
*/
|
||||
parentPost: PropTypes.object,
|
||||
|
||||
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* The poster of the parent post, if exists
|
||||
*/
|
||||
parentPostUser: PropTypes.object,
|
||||
|
||||
if (!Utils.areObjectsEqual(nextProps.parentPost, this.props.parentPost)) {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* The function called when the comment icon is clicked
|
||||
*/
|
||||
handleCommentClick: PropTypes.func.isRequired,
|
||||
|
||||
if (nextProps.compactDisplay !== this.props.compactDisplay) {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Set to render post body compactly
|
||||
*/
|
||||
compactDisplay: PropTypes.bool,
|
||||
|
||||
if (nextProps.previewCollapsed !== this.props.previewCollapsed) {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Set to highlight comment as a mention
|
||||
*/
|
||||
isCommentMention: PropTypes.bool,
|
||||
|
||||
if (nextProps.handleCommentClick.toString() !== this.props.handleCommentClick.toString()) {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Set to collapse image and video previews
|
||||
*/
|
||||
previewCollapsed: PropTypes.string,
|
||||
|
||||
if (nextProps.lastPostCount !== this.props.lastPostCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
removePost() {
|
||||
GlobalActions.emitRemovePost(this.props.post);
|
||||
/**
|
||||
* Post identifiers for selenium tests
|
||||
*/
|
||||
lastPostCount: PropTypes.number
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -71,8 +67,8 @@ export default class PostBody extends React.Component {
|
||||
let comment = '';
|
||||
let postClass = '';
|
||||
|
||||
if (parentPost) {
|
||||
const profile = UserStore.getProfile(parentPost.user_id);
|
||||
if (parentPost && this.props.parentPostUser) {
|
||||
const profile = this.props.parentPostUser;
|
||||
|
||||
let apostrophe = '';
|
||||
let name = '...';
|
||||
@@ -105,8 +101,7 @@ export default class PostBody extends React.Component {
|
||||
message = Utils.replaceHtmlEntities(parentPost.message);
|
||||
} else if (parentPost.file_ids && parentPost.file_ids.length > 0) {
|
||||
message = (
|
||||
<CommentedOnFilesMessageContainer
|
||||
parentPostChannelId={parentPost.channel_id}
|
||||
<CommentedOnFilesMessage
|
||||
parentPostId={parentPost.id}
|
||||
/>
|
||||
);
|
||||
@@ -134,18 +129,10 @@ export default class PostBody extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
let loading;
|
||||
if (post.state === Constants.POST_FAILED) {
|
||||
let failedOptions;
|
||||
if (this.props.post.failed) {
|
||||
postClass += ' post--fail';
|
||||
loading = <PendingPostOptions post={this.props.post}/>;
|
||||
} else if (post.state === Constants.POST_LOADING) {
|
||||
postClass += ' post-waiting';
|
||||
loading = (
|
||||
<img
|
||||
className='post-loading-gif pull-right'
|
||||
src={loadingGif}
|
||||
/>
|
||||
);
|
||||
failedOptions = <FailedPostOptions post={this.props.post}/>;
|
||||
}
|
||||
|
||||
if (PostUtils.isEdited(this.props.post)) {
|
||||
@@ -153,7 +140,7 @@ export default class PostBody extends React.Component {
|
||||
}
|
||||
|
||||
let fileAttachmentHolder = null;
|
||||
if (((post.file_ids && post.file_ids.length > 0) || (post.filenames && post.filenames.length > 0)) && this.props.post.state !== Constants.POST_DELETED) {
|
||||
if (((post.file_ids && post.file_ids.length > 0) || (post.filenames && post.filenames.length > 0)) && this.props.post.state !== Posts.POST_DELETED) {
|
||||
fileAttachmentHolder = (
|
||||
<FileAttachmentListContainer
|
||||
post={post}
|
||||
@@ -168,7 +155,7 @@ export default class PostBody extends React.Component {
|
||||
id={`${post.id}_message`}
|
||||
className={postClass}
|
||||
>
|
||||
{loading}
|
||||
{failedOptions}
|
||||
<PostMessageContainer
|
||||
lastPostCount={this.props.lastPostCount}
|
||||
post={this.props.post}
|
||||
@@ -177,16 +164,14 @@ export default class PostBody extends React.Component {
|
||||
);
|
||||
|
||||
let messageWithAdditionalContent;
|
||||
if (this.props.post.state === Constants.POST_DELETED) {
|
||||
if (this.props.post.state === Posts.POST_DELETED) {
|
||||
messageWithAdditionalContent = messageWrapper;
|
||||
} else {
|
||||
messageWithAdditionalContent = (
|
||||
<PostBodyAdditionalContent
|
||||
post={this.props.post}
|
||||
message={messageWrapper}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
previewCollapsed={this.props.previewCollapsed}
|
||||
childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -208,16 +193,3 @@ export default class PostBody extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostBody.propTypes = {
|
||||
post: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
parentPost: PropTypes.object,
|
||||
retryPost: PropTypes.func,
|
||||
lastPostCount: PropTypes.number,
|
||||
handleCommentClick: PropTypes.func.isRequired,
|
||||
compactDisplay: PropTypes.bool,
|
||||
previewCollapsed: PropTypes.string,
|
||||
isCommentMention: PropTypes.bool,
|
||||
childComponentDidUpdateFunction: PropTypes.func
|
||||
};
|
||||
@@ -2,18 +2,39 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostAttachmentList from './post_attachment_list.jsx';
|
||||
import PostAttachmentOpenGraph from './post_attachment_opengraph.jsx';
|
||||
import PostAttachmentOpenGraph from './post_attachment_opengraph';
|
||||
import PostImage from './post_image.jsx';
|
||||
import YoutubeVideo from 'components/youtube_video.jsx';
|
||||
import YoutubeVideo from 'components/youtube_video';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React from 'react';
|
||||
export default class PostBodyAdditionalContent extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* The post to render the content of
|
||||
*/
|
||||
post: PropTypes.object.isRequired,
|
||||
|
||||
/**
|
||||
* The post's message
|
||||
*/
|
||||
message: PropTypes.element.isRequired,
|
||||
|
||||
/**
|
||||
* Set to collapse image and video previews
|
||||
*/
|
||||
previewCollapsed: PropTypes.string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
previewCollapsed: ''
|
||||
}
|
||||
|
||||
export default class PostBodyAdditionalContent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -40,25 +61,6 @@ export default class PostBodyAdditionalContent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
|
||||
return true;
|
||||
}
|
||||
if (!Utils.areObjectsEqual(nextProps.message, this.props.message)) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.embedVisible !== this.state.embedVisible) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.linkLoadError !== this.state.linkLoadError) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.linkLoaded !== this.state.linkLoaded) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
toggleEmbedVisibility() {
|
||||
this.setState({embedVisible: !this.state.embedVisible});
|
||||
}
|
||||
@@ -138,7 +140,6 @@ export default class PostBodyAdditionalContent extends React.Component {
|
||||
link={link}
|
||||
onLinkLoadError={this.handleLinkLoadError}
|
||||
onLinkLoaded={this.handleLinkLoaded}
|
||||
childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -156,7 +157,6 @@ export default class PostBodyAdditionalContent extends React.Component {
|
||||
return (
|
||||
<PostAttachmentOpenGraph
|
||||
link={link}
|
||||
childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
|
||||
previewCollapsed={this.props.previewCollapsed}
|
||||
/>
|
||||
);
|
||||
@@ -227,14 +227,3 @@ export default class PostBodyAdditionalContent extends React.Component {
|
||||
return this.props.message;
|
||||
}
|
||||
}
|
||||
|
||||
PostBodyAdditionalContent.defaultProps = {
|
||||
previewCollapsed: 'false'
|
||||
};
|
||||
PostBodyAdditionalContent.propTypes = {
|
||||
post: PropTypes.object.isRequired,
|
||||
message: PropTypes.element.isRequired,
|
||||
compactDisplay: PropTypes.bool,
|
||||
previewCollapsed: PropTypes.string,
|
||||
childComponentDidUpdateFunction: PropTypes.func
|
||||
};
|
||||
88
webapp/components/post_view/post_flag_icon.jsx
Обычный файл
88
webapp/components/post_view/post_flag_icon.jsx
Обычный файл
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
|
||||
|
||||
import {flagPost, unflagPost} from 'actions/post_actions.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
function flagToolTip(isFlagged) {
|
||||
return (
|
||||
<Tooltip id='flagTooltip'>
|
||||
<FormattedMessage
|
||||
id={isFlagged ? 'flag_post.unflag' : 'flag_post.flag'}
|
||||
defaultMessage={isFlagged ? 'Unflag' : 'Flag for follow up'}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function flagIcon() {
|
||||
return (
|
||||
<span
|
||||
className='icon'
|
||||
dangerouslySetInnerHTML={{__html: Constants.FLAG_ICON_SVG}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PostFlagIcon(props) {
|
||||
function onFlagPost(e) {
|
||||
e.preventDefault();
|
||||
flagPost(props.postId);
|
||||
}
|
||||
|
||||
function onUnflagPost(e) {
|
||||
e.preventDefault();
|
||||
unflagPost(props.postId);
|
||||
}
|
||||
|
||||
const flagFunc = props.isFlagged ? onUnflagPost : onFlagPost;
|
||||
const flagVisible = props.isFlagged ? 'visible' : '';
|
||||
|
||||
let flagIconId = null;
|
||||
if (props.idCount > -1) {
|
||||
flagIconId = Utils.createSafeId(props.idPrefix + props.idCount);
|
||||
}
|
||||
|
||||
if (!props.isEphemeral) {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
key={'flagtooltipkey' + flagVisible}
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='top'
|
||||
overlay={flagToolTip(props.isFlagged)}
|
||||
>
|
||||
<a
|
||||
id={flagIconId}
|
||||
href='#'
|
||||
className={'flag-icon__container ' + flagVisible}
|
||||
onClick={flagFunc}
|
||||
>
|
||||
{flagIcon()}
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
PostFlagIcon.propTypes = {
|
||||
idPrefix: PropTypes.string.isRequired,
|
||||
idCount: PropTypes.number,
|
||||
postId: PropTypes.string.isRequired,
|
||||
isFlagged: PropTypes.bool.isRequired,
|
||||
isEphemeral: PropTypes.bool
|
||||
};
|
||||
|
||||
PostFlagIcon.defaultProps = {
|
||||
idCount: -1,
|
||||
postId: '',
|
||||
isFlagged: false,
|
||||
isEphemeral: false
|
||||
};
|
||||
@@ -1,212 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostList from './components/post_list.jsx';
|
||||
import LoadingScreen from 'components/loading_screen.jsx';
|
||||
|
||||
import EmojiStore from 'stores/emoji_store.jsx';
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import WebrtcStore from 'stores/webrtc_store.jsx';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const Preferences = Constants.Preferences;
|
||||
const ScrollTypes = Constants.ScrollTypes;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostFocusView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChannelChange = this.onChannelChange.bind(this);
|
||||
this.onPostsChange = this.onPostsChange.bind(this);
|
||||
this.onUserChange = this.onUserChange.bind(this);
|
||||
this.onEmojiChange = this.onEmojiChange.bind(this);
|
||||
this.onStatusChange = this.onStatusChange.bind(this);
|
||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||
this.onPostListScroll = this.onPostListScroll.bind(this);
|
||||
this.onBusy = this.onBusy.bind(this);
|
||||
|
||||
const focusedPostId = PostStore.getFocusedPostId();
|
||||
|
||||
const channel = ChannelStore.getCurrent();
|
||||
const profiles = UserStore.getProfiles();
|
||||
|
||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
||||
|
||||
let statuses;
|
||||
if (channel) {
|
||||
statuses = Object.assign({}, UserStore.getStatuses());
|
||||
}
|
||||
|
||||
this.state = {
|
||||
postList: PostStore.filterPosts(focusedPostId, joinLeaveEnabled),
|
||||
currentUser: UserStore.getCurrentUser(),
|
||||
isBusy: WebrtcStore.isBusy(),
|
||||
profiles,
|
||||
statuses,
|
||||
scrollType: ScrollTypes.POST,
|
||||
currentChannel: ChannelStore.getCurrentId().slice(),
|
||||
scrollPostId: focusedPostId,
|
||||
atTop: PostStore.getVisibilityAtTop(focusedPostId),
|
||||
atBottom: PostStore.getVisibilityAtBottom(focusedPostId),
|
||||
emojis: EmojiStore.getEmojis(),
|
||||
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
||||
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
||||
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
|
||||
previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false'),
|
||||
useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
|
||||
flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onChannelChange);
|
||||
PostStore.addChangeListener(this.onPostsChange);
|
||||
UserStore.addChangeListener(this.onUserChange);
|
||||
UserStore.addStatusesChangeListener(this.onStatusChange);
|
||||
EmojiStore.addChangeListener(this.onEmojiChange);
|
||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
||||
WebrtcStore.addBusyListener(this.onBusy);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
||||
PostStore.removeChangeListener(this.onPostsChange);
|
||||
UserStore.removeChangeListener(this.onUserChange);
|
||||
UserStore.removeStatusesChangeListener(this.onStatusChange);
|
||||
EmojiStore.removeChangeListener(this.onEmojiChange);
|
||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
||||
WebrtcStore.removeBusyListener(this.onBusy);
|
||||
}
|
||||
|
||||
onChannelChange() {
|
||||
const currentChannel = ChannelStore.getCurrentId();
|
||||
if (this.state.currentChannel !== currentChannel) {
|
||||
this.setState({
|
||||
currentChannel: currentChannel.slice(),
|
||||
scrollType: ScrollTypes.POST
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onPostsChange() {
|
||||
const focusedPostId = PostStore.getFocusedPostId();
|
||||
if (focusedPostId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
||||
|
||||
this.setState({
|
||||
scrollPostId: focusedPostId,
|
||||
postList: PostStore.filterPosts(focusedPostId, joinLeaveEnabled),
|
||||
atTop: PostStore.getVisibilityAtTop(focusedPostId),
|
||||
atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
|
||||
});
|
||||
}
|
||||
|
||||
onUserChange() {
|
||||
this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))});
|
||||
}
|
||||
|
||||
onStatusChange() {
|
||||
const channel = ChannelStore.getCurrent();
|
||||
let statuses;
|
||||
if (channel) {
|
||||
statuses = Object.assign({}, UserStore.getStatuses());
|
||||
}
|
||||
|
||||
this.setState({statuses});
|
||||
}
|
||||
|
||||
onEmojiChange() {
|
||||
this.setState({
|
||||
emojis: EmojiStore.getEmojis()
|
||||
});
|
||||
}
|
||||
|
||||
onPreferenceChange(category) {
|
||||
// Bit of a hack to force render when this setting is updated
|
||||
// regardless of change
|
||||
let previewSuffix = '';
|
||||
if (category === Preferences.CATEGORY_DISPLAY_SETTINGS) {
|
||||
previewSuffix = '_' + Utils.generateId();
|
||||
}
|
||||
|
||||
const focusedPostId = PostStore.getFocusedPostId();
|
||||
if (focusedPostId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
||||
|
||||
this.setState({
|
||||
postList: PostStore.filterPosts(focusedPostId, joinLeaveEnabled),
|
||||
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
||||
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
||||
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
|
||||
previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false') + previewSuffix,
|
||||
useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
|
||||
flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST)
|
||||
});
|
||||
}
|
||||
|
||||
onPostListScroll() {
|
||||
this.setState({scrollType: ScrollTypes.FREE});
|
||||
}
|
||||
|
||||
onBusy(isBusy) {
|
||||
this.setState({isBusy});
|
||||
}
|
||||
|
||||
render() {
|
||||
const postsToHighlight = {};
|
||||
postsToHighlight[this.state.scrollPostId] = true;
|
||||
|
||||
let content;
|
||||
if (this.state.postList == null) {
|
||||
content = (
|
||||
<LoadingScreen
|
||||
position='absolute'
|
||||
key='loading'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<PostList
|
||||
postList={this.state.postList}
|
||||
currentUser={this.state.currentUser}
|
||||
profiles={this.state.profiles}
|
||||
scrollType={this.state.scrollType}
|
||||
scrollPostId={this.state.scrollPostId}
|
||||
postListScrolled={this.onPostListScroll}
|
||||
displayNameType={this.state.displayNameType}
|
||||
displayPostsInCenter={this.state.displayPostsInCenter}
|
||||
compactDisplay={this.state.compactDisplay}
|
||||
previewsCollapsed={this.state.previewsCollapsed}
|
||||
useMilitaryTime={this.state.useMilitaryTime}
|
||||
showMoreMessagesTop={!this.state.atTop}
|
||||
showMoreMessagesBottom={!this.state.atBottom}
|
||||
postsToHighlight={postsToHighlight}
|
||||
isFocusPost={true}
|
||||
emojis={this.state.emojis}
|
||||
flaggedPosts={this.state.flaggedPosts}
|
||||
statuses={this.state.statuses}
|
||||
isBusy={this.state.isBusy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='post-list'>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
18
webapp/components/post_view/post_header/index.js
Обычный файл
18
webapp/components/post_view/post_header/index.js
Обычный файл
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
|
||||
import PostHeader from './post_header.jsx';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
displayNameType: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(PostHeader);
|
||||
@@ -2,18 +2,80 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import UserProfile from 'components/user_profile.jsx';
|
||||
import PostInfo from './post_info.jsx';
|
||||
import PostInfo from 'components/post_view/post_info';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import * as PostUtils from 'utils/post_utils.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React from 'react';
|
||||
export default class PostHeader extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/*
|
||||
* The post to render the header for
|
||||
*/
|
||||
post: PropTypes.object.isRequired,
|
||||
|
||||
/*
|
||||
* The user who created the post
|
||||
*/
|
||||
user: PropTypes.object,
|
||||
|
||||
/*
|
||||
* Function called when the comment icon is clicked
|
||||
*/
|
||||
handleCommentClick: PropTypes.func.isRequired,
|
||||
|
||||
/*
|
||||
* Function called when the post options dropdown is opened
|
||||
*/
|
||||
handleDropdownOpened: PropTypes.func.isRequired,
|
||||
|
||||
/*
|
||||
* Set to render compactly
|
||||
*/
|
||||
compactDisplay: PropTypes.bool,
|
||||
|
||||
/*
|
||||
* Set to render the post as if it was part of the previous post
|
||||
*/
|
||||
consecutivePostByUser: PropTypes.bool,
|
||||
|
||||
/*
|
||||
* The method for displaying the post creator's name
|
||||
*/
|
||||
displayNameType: PropTypes.string,
|
||||
|
||||
/*
|
||||
* The status of the user who created the post
|
||||
*/
|
||||
status: PropTypes.string,
|
||||
|
||||
/*
|
||||
* Set if the post creator is currenlty in a WebRTC call
|
||||
*/
|
||||
isBusy: PropTypes.bool,
|
||||
|
||||
/*
|
||||
* The number of replies in the same thread as this post
|
||||
*/
|
||||
replyCount: PropTypes.number,
|
||||
|
||||
/*
|
||||
* Post identifiers for selenium tests
|
||||
*/
|
||||
lastPostCount: PropTypes.number,
|
||||
|
||||
/**
|
||||
* Function to get the post list HTML element
|
||||
*/
|
||||
getPostList: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default class PostHeader extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
@@ -81,16 +143,12 @@ export default class PostHeader extends React.Component {
|
||||
<div className='col'>
|
||||
<PostInfo
|
||||
post={post}
|
||||
lastPostCount={this.props.lastPostCount}
|
||||
commentCount={this.props.commentCount}
|
||||
handleCommentClick={this.props.handleCommentClick}
|
||||
handleDropdownOpened={this.props.handleDropdownOpened}
|
||||
isLastComment={this.props.isLastComment}
|
||||
sameUser={this.props.sameUser}
|
||||
currentUser={this.props.currentUser}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
useMilitaryTime={this.props.useMilitaryTime}
|
||||
isFlagged={this.props.isFlagged}
|
||||
lastPostCount={this.props.lastPostCount}
|
||||
replyCount={this.props.replyCount}
|
||||
consecutivePostByUser={this.props.consecutivePostByUser}
|
||||
getPostList={this.props.getPostList}
|
||||
/>
|
||||
</div>
|
||||
@@ -98,28 +156,3 @@ export default class PostHeader extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostHeader.defaultProps = {
|
||||
post: null,
|
||||
commentCount: 0,
|
||||
isLastComment: false,
|
||||
sameUser: false
|
||||
};
|
||||
PostHeader.propTypes = {
|
||||
post: PropTypes.object.isRequired,
|
||||
user: PropTypes.object,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
lastPostCount: PropTypes.number,
|
||||
commentCount: PropTypes.number.isRequired,
|
||||
isLastComment: PropTypes.bool.isRequired,
|
||||
handleCommentClick: PropTypes.func.isRequired,
|
||||
handleDropdownOpened: PropTypes.func.isRequired,
|
||||
sameUser: PropTypes.bool.isRequired,
|
||||
compactDisplay: PropTypes.bool,
|
||||
displayNameType: PropTypes.string,
|
||||
useMilitaryTime: PropTypes.bool.isRequired,
|
||||
isFlagged: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string,
|
||||
isBusy: PropTypes.bool,
|
||||
getPostList: PropTypes.func.isRequired
|
||||
};
|
||||
@@ -1,11 +1,28 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class PostImageEmbed extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* The link to load the image from
|
||||
*/
|
||||
link: PropTypes.string.isRequired,
|
||||
|
||||
/**
|
||||
* Function to call when image is loaded
|
||||
*/
|
||||
onLinkLoaded: PropTypes.func,
|
||||
|
||||
/**
|
||||
* The function to call if image load fails
|
||||
*/
|
||||
onLinkLoadError: PropTypes.func
|
||||
}
|
||||
|
||||
export default class PostImageEmbed extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -32,9 +49,6 @@ export default class PostImageEmbed extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.state.loaded && this.props.childComponentDidUpdateFunction) {
|
||||
this.props.childComponentDidUpdateFunction();
|
||||
}
|
||||
if (!this.state.loaded && prevProps.link !== this.props.link) {
|
||||
this.loadImg(this.props.link);
|
||||
}
|
||||
@@ -84,10 +98,3 @@ export default class PostImageEmbed extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostImageEmbed.propTypes = {
|
||||
link: PropTypes.string.isRequired,
|
||||
onLinkLoadError: PropTypes.func,
|
||||
onLinkLoaded: PropTypes.func,
|
||||
childComponentDidUpdateFunction: PropTypes.func
|
||||
};
|
||||
31
webapp/components/post_view/post_info/index.js
Обычный файл
31
webapp/components/post_view/post_info/index.js
Обычный файл
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {removePost, addReaction} from 'mattermost-redux/actions/posts';
|
||||
|
||||
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {Preferences} from 'utils/constants.jsx';
|
||||
|
||||
import PostInfo from './post_info.jsx';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
useMilitaryTime: getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
|
||||
isFlagged: getBool(state, Preferences.CATEGORY_FLAGGED_POST, ownProps.post.id)
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
removePost,
|
||||
addReaction
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PostInfo);
|
||||
@@ -1,26 +1,77 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostTime from './post_time.jsx';
|
||||
import PostFlagIcon from 'components/common/post_flag_icon.jsx';
|
||||
import DotMenu from 'components/dot_menu/dot_menu.jsx';
|
||||
|
||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||
import * as PostActions from 'actions/post_actions.jsx';
|
||||
import PostTime from 'components/post_view/post_time.jsx';
|
||||
import PostFlagIcon from 'components/post_view/post_flag_icon.jsx';
|
||||
import CommentIcon from 'components/common/comment_icon.jsx';
|
||||
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
|
||||
import DotMenu from 'components/dot_menu';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as PostUtils from 'utils/post_utils.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
export default class PostInfo extends React.Component {
|
||||
export default class PostInfo extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/*
|
||||
* The post to render the info for
|
||||
*/
|
||||
post: PropTypes.object.isRequired,
|
||||
|
||||
/*
|
||||
* Function called when the comment icon is clicked
|
||||
*/
|
||||
handleCommentClick: PropTypes.func.isRequired,
|
||||
|
||||
/*
|
||||
* Funciton called when the post options dropdown is opened
|
||||
*/
|
||||
handleDropdownOpened: PropTypes.func.isRequired,
|
||||
|
||||
/*
|
||||
* Set to display in 24 hour format
|
||||
*/
|
||||
useMilitaryTime: PropTypes.bool.isRequired,
|
||||
|
||||
/*
|
||||
* Set to mark the post as flagged
|
||||
*/
|
||||
isFlagged: PropTypes.bool,
|
||||
|
||||
/*
|
||||
* The number of replies in the same thread as this post
|
||||
*/
|
||||
replyCount: PropTypes.number,
|
||||
|
||||
/*
|
||||
* Post identifiers for selenium tests
|
||||
*/
|
||||
lastPostCount: PropTypes.number,
|
||||
|
||||
/**
|
||||
* Function to get the post list HTML element
|
||||
*/
|
||||
getPostList: PropTypes.func.isRequired,
|
||||
|
||||
actions: PropTypes.shape({
|
||||
|
||||
/*
|
||||
* Function to remove the post
|
||||
*/
|
||||
removePost: PropTypes.func.isRequired,
|
||||
|
||||
/*
|
||||
* Function to add a reaction to the post
|
||||
*/
|
||||
addReaction: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -29,7 +80,8 @@ export default class PostInfo extends React.Component {
|
||||
|
||||
this.state = {
|
||||
showEmojiPicker: false,
|
||||
reactionPickerOffset: 21
|
||||
reactionPickerOffset: 21,
|
||||
canEdit: PostUtils.canEditPost(props.post, this.editDisableAction)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,7 +98,7 @@ export default class PostInfo extends React.Component {
|
||||
}
|
||||
|
||||
removePost() {
|
||||
GlobalActions.emitRemovePost(this.props.post);
|
||||
this.props.actions.removePost(this.props.post);
|
||||
}
|
||||
|
||||
createRemovePostButton() {
|
||||
@@ -66,7 +118,7 @@ export default class PostInfo extends React.Component {
|
||||
const pickerOffset = 21;
|
||||
this.setState({showEmojiPicker: false, reactionPickerOffset: pickerOffset});
|
||||
const emojiName = emoji.name || emoji.aliases[0];
|
||||
PostActions.addReaction(this.props.post.channel_id, this.props.post.id, emojiName);
|
||||
this.props.actions.addReaction(this.props.post.id, emojiName);
|
||||
}
|
||||
|
||||
getDotMenu = () => {
|
||||
@@ -74,7 +126,7 @@ export default class PostInfo extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
var post = this.props.post;
|
||||
const post = this.props.post;
|
||||
|
||||
let idCount = -1;
|
||||
if (this.props.lastPostCount >= 0 && this.props.lastPostCount < Constants.TEST_ID_COUNT) {
|
||||
@@ -82,19 +134,18 @@ export default class PostInfo extends React.Component {
|
||||
}
|
||||
|
||||
const isEphemeral = Utils.isPostEphemeral(post);
|
||||
const isPending = post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING;
|
||||
const isSystemMessage = PostUtils.isSystemMessage(post);
|
||||
|
||||
let comments = null;
|
||||
let react = null;
|
||||
if (!isEphemeral && !isPending && !isSystemMessage) {
|
||||
if (!isEphemeral && !post.failed && !isSystemMessage) {
|
||||
comments = (
|
||||
<CommentIcon
|
||||
idPrefix={'commentIcon'}
|
||||
idPrefix='commentIcon'
|
||||
idCount={idCount}
|
||||
handleCommentClick={this.props.handleCommentClick}
|
||||
commentCount={this.props.commentCount}
|
||||
id={ChannelStore.getCurrentId() + '_' + post.id}
|
||||
commentCount={this.props.replyCount}
|
||||
id={post.channel_id + '_' + post.id}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -116,6 +167,7 @@ export default class PostInfo extends React.Component {
|
||||
<i className='fa fa-smile-o'/>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -127,13 +179,13 @@ export default class PostInfo extends React.Component {
|
||||
{this.createRemovePostButton()}
|
||||
</div>
|
||||
);
|
||||
} else if (!isPending) {
|
||||
} else if (!post.failed) {
|
||||
const dotMenu = (
|
||||
<DotMenu
|
||||
idPrefix={Constants.CENTER}
|
||||
idCount={idCount}
|
||||
post={this.props.post}
|
||||
commentCount={this.props.commentCount}
|
||||
commentCount={this.props.replyCount}
|
||||
isFlagged={this.props.isFlagged}
|
||||
handleCommentClick={this.props.handleCommentClick}
|
||||
handleDropdownOpened={this.props.handleDropdownOpened}
|
||||
@@ -171,8 +223,6 @@ export default class PostInfo extends React.Component {
|
||||
<div className='col'>
|
||||
<PostTime
|
||||
eventTime={post.create_at}
|
||||
sameUser={this.props.sameUser}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
useMilitaryTime={this.props.useMilitaryTime}
|
||||
postId={post.id}
|
||||
/>
|
||||
@@ -191,24 +241,3 @@ export default class PostInfo extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostInfo.defaultProps = {
|
||||
post: null,
|
||||
commentCount: 0,
|
||||
isLastComment: false,
|
||||
sameUser: false
|
||||
};
|
||||
PostInfo.propTypes = {
|
||||
post: PropTypes.object.isRequired,
|
||||
lastPostCount: PropTypes.number,
|
||||
commentCount: PropTypes.number.isRequired,
|
||||
isLastComment: PropTypes.bool.isRequired,
|
||||
handleCommentClick: PropTypes.func.isRequired,
|
||||
handleDropdownOpened: PropTypes.func.isRequired,
|
||||
sameUser: PropTypes.bool.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
compactDisplay: PropTypes.bool,
|
||||
useMilitaryTime: PropTypes.bool.isRequired,
|
||||
isFlagged: PropTypes.bool,
|
||||
getPostList: PropTypes.func.isRequired
|
||||
};
|
||||
523
webapp/components/post_view/post_list.jsx
Обычный файл
523
webapp/components/post_view/post_list.jsx
Обычный файл
@@ -0,0 +1,523 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import Post from './post';
|
||||
import LoadingScreen from 'components/loading_screen.jsx';
|
||||
import FloatingTimestamp from './floating_timestamp.jsx';
|
||||
import ScrollToBottomArrows from './scroll_to_bottom_arrows.jsx';
|
||||
import NewMessageIndicator from './new_message_indicator.jsx';
|
||||
|
||||
import * as UserAgent from 'utils/user_agent.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import {createChannelIntroMessage} from 'utils/channel_intro_messages.jsx';
|
||||
import DelayedAction from 'utils/delayed_action.jsx';
|
||||
|
||||
import {FormattedDate, FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const CLOSE_TO_BOTTOM_SCROLL_MARGIN = 10;
|
||||
const POSTS_PER_PAGE = Constants.POST_CHUNK_SIZE / 2;
|
||||
|
||||
export default class PostList extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* Array of posts in the channel, ordered from oldest to newest
|
||||
*/
|
||||
posts: PropTypes.array,
|
||||
|
||||
/**
|
||||
* The number of posts that should be rendered
|
||||
*/
|
||||
postVisibility: PropTypes.number,
|
||||
|
||||
/**
|
||||
* The channel the posts are in
|
||||
*/
|
||||
channel: PropTypes.object,
|
||||
|
||||
/**
|
||||
* The last time the channel was viewed, sets the new message separator
|
||||
*/
|
||||
lastViewedAt: PropTypes.number,
|
||||
|
||||
/**
|
||||
* Set if more posts are being loaded
|
||||
*/
|
||||
loadingPosts: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* The user id of the logged in user
|
||||
*/
|
||||
currentUserId: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Set to focus this post
|
||||
*/
|
||||
focusedPostId: PropTypes.array,
|
||||
|
||||
/**
|
||||
* Whether to display the channel intro at full width
|
||||
*/
|
||||
fullWidth: PropTypes.bool,
|
||||
|
||||
actions: PropTypes.shape({
|
||||
|
||||
/**
|
||||
* Function to get posts in the channel
|
||||
*/
|
||||
getPosts: PropTypes.func.isRequired,
|
||||
|
||||
/**
|
||||
* Function to get posts in the channel older than the focused post
|
||||
*/
|
||||
getPostsBefore: PropTypes.func.isRequired,
|
||||
|
||||
/**
|
||||
* Function to get posts in the channel newer than the focused post
|
||||
*/
|
||||
getPostsAfter: PropTypes.func.isRequired,
|
||||
|
||||
/**
|
||||
* Function to get the post thread for the focused post
|
||||
*/
|
||||
getPostThread: PropTypes.func.isRequired,
|
||||
|
||||
/**
|
||||
* Function to increase the number of posts being rendered
|
||||
*/
|
||||
increasePostVisibility: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.scrollStopAction = new DelayedAction(this.handleScrollStop);
|
||||
|
||||
this.previousScrollTop = Number.MAX_SAFE_INTEGER;
|
||||
this.previousScrollHeight = 0;
|
||||
this.previousClientHeight = 0;
|
||||
|
||||
this.state = {
|
||||
atEnd: false,
|
||||
unViewedCount: 0,
|
||||
isScrolling: false,
|
||||
lastViewed: Number.MAX_SAFE_INTEGER
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadPosts(this.props.channel.id, this.props.focusedPostId);
|
||||
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// Focusing on a new post so load posts around it
|
||||
if (nextProps.focusedPostId && this.props.focusedPostId !== nextProps.focusedPostId) {
|
||||
this.hasScrolledToFocusedPost = false;
|
||||
this.hasScrolledToNewMessageSeparator = false;
|
||||
this.setState({atEnd: false});
|
||||
this.loadPosts(nextProps.channel.id, nextProps.focusedPostId);
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = this.props.channel || {};
|
||||
const nextChannel = nextProps.channel || {};
|
||||
|
||||
if (nextProps.focusedPostId == null) {
|
||||
// Channel changed so load posts for new channel
|
||||
if (channel.id !== nextChannel.id) {
|
||||
this.hasScrolled = false;
|
||||
this.hasScrolledToFocusedPost = false;
|
||||
this.hasScrolledToNewMessageSeparator = false;
|
||||
this.setState({atEnd: false});
|
||||
|
||||
if (nextChannel.id) {
|
||||
this.loadPosts(nextChannel.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.wasAtBottom() && this.props.posts !== nextProps.posts) {
|
||||
const unViewedCount = nextProps.posts.reduce((count, post) => {
|
||||
if (post.create_at > this.state.lastViewed &&
|
||||
post.user_id !== nextProps.currentUserId &&
|
||||
post.state !== Constants.POST_DELETED) {
|
||||
return count + 1;
|
||||
}
|
||||
return count;
|
||||
}, 0);
|
||||
this.setState({unViewedCount});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUpdate() {
|
||||
if (this.refs.postlist) {
|
||||
this.previousScrollTop = this.refs.postlist.scrollTop;
|
||||
this.previousScrollHeight = this.refs.postlist.scrollHeight;
|
||||
this.previousClientHeight = this.refs.postlist.clientHeight;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// Scroll to focused post on first load
|
||||
const focusedPost = this.refs[this.props.focusedPostId];
|
||||
if (focusedPost) {
|
||||
if (!this.hasScrolledToFocusedPost && this.props.posts) {
|
||||
const element = ReactDOM.findDOMNode(focusedPost);
|
||||
const rect = element.getBoundingClientRect();
|
||||
const listHeight = this.refs.postlist.clientHeight / 2;
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollTop + (rect.top - listHeight);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Scroll to new message indicator or bottom on first load
|
||||
const messageSeparator = this.refs.newMessageSeparator;
|
||||
if (messageSeparator && !this.hasScrolledToNewMessageSeparator) {
|
||||
const element = ReactDOM.findDOMNode(messageSeparator);
|
||||
element.scrollIntoView();
|
||||
return;
|
||||
} else if (this.refs.postlist && !this.hasScrolledToNewMessageSeparator) {
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||
return;
|
||||
}
|
||||
|
||||
const prevPosts = prevProps.posts;
|
||||
const posts = this.props.posts;
|
||||
const postList = this.refs.postlist;
|
||||
|
||||
if (postList && prevPosts && posts && posts[0] && prevPosts[0]) {
|
||||
// A new message was posted, so scroll to bottom if it was from current user
|
||||
// or if user was already scrolled close to bottom
|
||||
let doScrollToBottom = false;
|
||||
if (posts[0].id !== prevPosts[0].id && posts[0].pending_post_id !== prevPosts[0].pending_post_id) {
|
||||
// If already scrolled to bottom
|
||||
if (this.wasAtBottom()) {
|
||||
doScrollToBottom = true;
|
||||
}
|
||||
|
||||
// If new post was by current user
|
||||
if (posts[0].user_id === this.props.currentUserId) {
|
||||
doScrollToBottom = true;
|
||||
}
|
||||
|
||||
// If new post was ephemeral
|
||||
if (Utils.isPostEphemeral(posts[0])) {
|
||||
doScrollToBottom = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (doScrollToBottom) {
|
||||
postList.scrollTop = postList.scrollHeight;
|
||||
return;
|
||||
}
|
||||
|
||||
// New posts added at the top, maintain scroll position
|
||||
if (this.previousScrollHeight !== this.refs.postlist.scrollHeight && posts[0].id === prevPosts[0].id) {
|
||||
this.refs.postlist.scrollTop = this.previousScrollTop + (this.refs.postlist.scrollHeight - this.previousScrollHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleScrollStop = () => {
|
||||
this.setState({
|
||||
isScrolling: false
|
||||
});
|
||||
}
|
||||
|
||||
wasAtBottom = () => {
|
||||
return this.previousClientHeight + this.previousScrollTop >= this.previousScrollHeight - CLOSE_TO_BOTTOM_SCROLL_MARGIN;
|
||||
}
|
||||
|
||||
handleResize = () => {
|
||||
const postList = this.refs.postlist;
|
||||
|
||||
if (postList && this.wasAtBottom()) {
|
||||
postList.scrollTop = postList.scrollHeight;
|
||||
|
||||
this.previousScrollHeight = postList.scrollHeight;
|
||||
this.previousScrollTop = postList.scrollTop;
|
||||
this.previousClientHeight = postList.clientHeight;
|
||||
}
|
||||
}
|
||||
|
||||
loadPosts = async (channelId, focusedPostId) => {
|
||||
let posts;
|
||||
if (focusedPostId) {
|
||||
const getPostThreadAsync = this.props.actions.getPostThread(focusedPostId);
|
||||
const getPostsBeforeAsync = this.props.actions.getPostsBefore(channelId, focusedPostId, 0, POSTS_PER_PAGE);
|
||||
const getPostsAfterAsync = this.props.actions.getPostsAfter(channelId, focusedPostId, 0, POSTS_PER_PAGE);
|
||||
|
||||
posts = await getPostsBeforeAsync;
|
||||
await getPostsAfterAsync;
|
||||
await getPostThreadAsync;
|
||||
|
||||
this.hasScrolledToFocusedPost = true;
|
||||
} else {
|
||||
posts = await this.props.actions.getPosts(channelId, 0, POSTS_PER_PAGE);
|
||||
this.hasScrolledToNewMessageSeparator = true;
|
||||
}
|
||||
|
||||
if (posts && posts.order.length < POSTS_PER_PAGE) {
|
||||
this.setState({atEnd: true});
|
||||
}
|
||||
}
|
||||
|
||||
loadMorePosts = (e) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
this.props.actions.increasePostVisibility(this.props.channel.id, this.props.focusedPostId).then((moreToLoad) => {
|
||||
this.setState({atEnd: !moreToLoad && this.props.posts.length < this.props.postVisibility});
|
||||
});
|
||||
}
|
||||
|
||||
handleScroll = () => {
|
||||
this.hasScrolledToFocusedPost = true;
|
||||
this.hasScrolled = true;
|
||||
this.previousScrollTop = this.refs.postlist.scrollTop;
|
||||
|
||||
this.updateFloatingTimestamp();
|
||||
|
||||
if (!this.state.isScrolling) {
|
||||
this.setState({
|
||||
isScrolling: true
|
||||
});
|
||||
}
|
||||
|
||||
if (this.wasAtBottom()) {
|
||||
this.setState({
|
||||
lastViewed: new Date().getTime(),
|
||||
unViewedCount: 0,
|
||||
isScrolling: false
|
||||
});
|
||||
}
|
||||
|
||||
this.scrollStopAction.fireAfter(Constants.SCROLL_DELAY);
|
||||
}
|
||||
|
||||
updateFloatingTimestamp = () => {
|
||||
// skip this in non-mobile view since that's when the timestamp is visible
|
||||
if (!Utils.isMobile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.posts) {
|
||||
// iterate through posts starting at the bottom since users are more likely to be viewing newer posts
|
||||
for (let i = 0; i < this.props.posts.length; i++) {
|
||||
const post = this.props.posts[i];
|
||||
const element = this.refs[post.id];
|
||||
|
||||
if (!element || !element.domNode || element.domNode.offsetTop + element.domNode.clientHeight <= this.refs.postlist.scrollTop) {
|
||||
// this post is off the top of the screen so the last one is at the top of the screen
|
||||
let topPost;
|
||||
|
||||
if (i > 0) {
|
||||
topPost = this.props.posts[i - 1];
|
||||
} else {
|
||||
// the first post we look at should always be on the screen, but handle that case anyway
|
||||
topPost = post;
|
||||
}
|
||||
|
||||
if (!this.state.topPost || topPost.id !== this.state.topPost.id) {
|
||||
this.setState({
|
||||
topPost
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrollToBottom = () => {
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||
}
|
||||
|
||||
createPosts = (posts) => {
|
||||
const postCtls = [];
|
||||
let previousPostDay = new Date(0);
|
||||
const currentUserId = this.props.currentUserId;
|
||||
const lastViewed = this.props.lastViewedAt || 0;
|
||||
|
||||
let renderedLastViewed = false;
|
||||
|
||||
for (let i = posts.length - 1; i >= 0; i--) {
|
||||
const post = posts[i];
|
||||
|
||||
const postCtl = (
|
||||
<Post
|
||||
ref={post.id}
|
||||
key={'post ' + (post.id || post.pending_post_id)}
|
||||
post={post}
|
||||
lastPostCount={(i >= 0 && i < Constants.TEST_ID_COUNT) ? i : -1}
|
||||
getPostList={this.getPostList}
|
||||
/>
|
||||
);
|
||||
|
||||
const currentPostDay = Utils.getDateForUnixTicks(post.create_at);
|
||||
if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
|
||||
postCtls.push(
|
||||
<div
|
||||
key={currentPostDay.toDateString()}
|
||||
className='date-separator'
|
||||
>
|
||||
<hr className='separator__hr'/>
|
||||
<div className='separator__text'>
|
||||
<FormattedDate
|
||||
value={currentPostDay}
|
||||
weekday='short'
|
||||
month='short'
|
||||
day='2-digit'
|
||||
year='numeric'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (post.user_id !== currentUserId &&
|
||||
lastViewed !== 0 &&
|
||||
post.create_at > lastViewed &&
|
||||
!Utils.isPostEphemeral(post) &&
|
||||
!renderedLastViewed) {
|
||||
renderedLastViewed = true;
|
||||
|
||||
// Temporary fix to solve ie11 rendering issue
|
||||
let newSeparatorId = '';
|
||||
if (!UserAgent.isInternetExplorer()) {
|
||||
newSeparatorId = 'new_message_' + post.id;
|
||||
}
|
||||
postCtls.push(
|
||||
<div
|
||||
id={newSeparatorId}
|
||||
key='unviewed'
|
||||
ref='newMessageSeparator'
|
||||
className='new-separator'
|
||||
>
|
||||
<hr
|
||||
className='separator__hr'
|
||||
/>
|
||||
<div className='separator__text'>
|
||||
<FormattedMessage
|
||||
id='posts_view.newMsg'
|
||||
defaultMessage='New Messages'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
postCtls.push(postCtl);
|
||||
previousPostDay = currentPostDay;
|
||||
}
|
||||
|
||||
return postCtls;
|
||||
}
|
||||
|
||||
getPostList = () => {
|
||||
return this.refs.postlist;
|
||||
}
|
||||
|
||||
render() {
|
||||
const posts = this.props.posts;
|
||||
const channel = this.props.channel;
|
||||
|
||||
if (posts == null || channel == null) {
|
||||
return (
|
||||
<div id='post-list'>
|
||||
<LoadingScreen
|
||||
position='absolute'
|
||||
key='loading'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let topRow;
|
||||
if (this.state.atEnd) {
|
||||
topRow = createChannelIntroMessage(channel, this.props.fullWidth);
|
||||
} else if (this.props.postVisibility >= Constants.MAX_POST_VISIBILITY) {
|
||||
topRow = (
|
||||
<div className='post-list__loading post-list__loading-search'>
|
||||
<FormattedMessage
|
||||
id='posts_view.maxLoaded'
|
||||
defaultMessage='Looking for a specific message? Try searching for it'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
topRow = (
|
||||
<a
|
||||
ref='loadmoretop'
|
||||
className='more-messages-text theme'
|
||||
href='#'
|
||||
onClick={this.loadMorePosts}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='posts_view.loadMore'
|
||||
defaultMessage='Load more messages'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const topPostCreateAt = this.state.topPost ? this.state.topPost.create_at : 0;
|
||||
|
||||
let postVisibility = this.props.postVisibility;
|
||||
|
||||
// In focus mode there's an extra (Constants.POST_CHUNK_SIZE / 2) posts to show
|
||||
if (this.props.focusedPostId) {
|
||||
postVisibility += Constants.POST_CHUNK_SIZE / 2;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='post-list'>
|
||||
<FloatingTimestamp
|
||||
isScrolling={this.state.isScrolling}
|
||||
isMobile={Utils.isMobile()}
|
||||
createAt={topPostCreateAt}
|
||||
/>
|
||||
<ScrollToBottomArrows
|
||||
isScrolling={this.state.isScrolling}
|
||||
atBottom={this.wasAtBottom()}
|
||||
onClick={this.scrollToBottom}
|
||||
/>
|
||||
<NewMessageIndicator
|
||||
newMessages={this.state.unViewedCount}
|
||||
onClick={this.scrollToBottom}
|
||||
/>
|
||||
<div
|
||||
ref='postlist'
|
||||
className='post-list-holder-by-time'
|
||||
key={'postlist-' + channel.id}
|
||||
onScroll={this.handleScroll}
|
||||
>
|
||||
<div className='post-list__table'>
|
||||
<div
|
||||
ref='postlistcontent'
|
||||
className='post-list__content'
|
||||
>
|
||||
{topRow}
|
||||
{this.createPosts(posts.slice(0, postVisibility))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
41
webapp/components/post_view/post_message_view/index.js
Обычный файл
41
webapp/components/post_view/post_message_view/index.js
Обычный файл
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {getCustomEmojisAsMap} from 'mattermost-redux/selectors/entities/emojis';
|
||||
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserMentionKeys, getUsersByUsername} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getSiteURL} from 'utils/url.jsx';
|
||||
|
||||
import {EmojiMap} from 'stores/emoji_store.jsx';
|
||||
|
||||
import PostMessageView from './post_message_view.jsx';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
let emojiMap;
|
||||
let oldCustomEmoji;
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const newCustomEmoji = getCustomEmojisAsMap(state);
|
||||
if (newCustomEmoji !== oldCustomEmoji) {
|
||||
emojiMap = new EmojiMap(newCustomEmoji);
|
||||
}
|
||||
oldCustomEmoji = newCustomEmoji;
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
emojis: emojiMap,
|
||||
enableFormatting: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
|
||||
mentionKeys: getCurrentUserMentionKeys(state),
|
||||
usernameMap: getUsersByUsername(state),
|
||||
team: getCurrentTeam(state),
|
||||
siteUrl: getSiteURL()
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(PostMessageView);
|
||||
@@ -1,72 +1,74 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import * as PostUtils from 'utils/post_utils.jsx';
|
||||
import * as TextFormatting from 'utils/text_formatting.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import {getSiteURL} from 'utils/url.jsx';
|
||||
|
||||
import {getChannelsNameMapInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import store from 'stores/redux_store.jsx';
|
||||
|
||||
import {renderSystemMessage} from './system_message_helpers.jsx';
|
||||
|
||||
export default class PostMessageView extends React.Component {
|
||||
export default class PostMessageView extends React.PureComponent {
|
||||
static propTypes = {
|
||||
options: PropTypes.object.isRequired,
|
||||
|
||||
/*
|
||||
* The post to render the message for
|
||||
*/
|
||||
post: PropTypes.object.isRequired,
|
||||
|
||||
/*
|
||||
* Object using emoji names as keys with custom emojis as the values
|
||||
*/
|
||||
emojis: PropTypes.object.isRequired,
|
||||
enableFormatting: PropTypes.bool.isRequired,
|
||||
mentionKeys: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
usernameMap: PropTypes.object.isRequired,
|
||||
channelNamesMap: PropTypes.object.isRequired,
|
||||
|
||||
/*
|
||||
* The team the post was made in
|
||||
*/
|
||||
team: PropTypes.object.isRequired,
|
||||
|
||||
/*
|
||||
* Set to enable Markdown formatting
|
||||
*/
|
||||
enableFormatting: PropTypes.bool,
|
||||
|
||||
/*
|
||||
* An array of words that can be used to mention a user
|
||||
*/
|
||||
mentionKeys: PropTypes.arrayOf(PropTypes.string),
|
||||
|
||||
/*
|
||||
* Object mapping usernames to users
|
||||
*/
|
||||
usernameMap: PropTypes.object,
|
||||
|
||||
/*
|
||||
* The URL that the app is hosted on
|
||||
*/
|
||||
siteUrl: PropTypes.string,
|
||||
|
||||
/*
|
||||
* Options specific to text formatting
|
||||
*/
|
||||
options: PropTypes.object,
|
||||
|
||||
/*
|
||||
* Post identifiers for selenium tests
|
||||
*/
|
||||
lastPostCount: PropTypes.number
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (!Utils.areObjectsEqual(nextProps.options, this.props.options)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.post.message !== this.props.post.message) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.post.state !== this.props.post.state) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.post.type !== this.props.post.type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// emojis are immutable
|
||||
if (nextProps.emojis !== this.props.emojis) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.enableFormatting !== this.props.enableFormatting) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextProps.mentionKeys, this.props.mentionKeys)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.lastPostCount !== this.props.lastPostCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Don't check if props.usernameMap changes since it is very large and inefficient to do so.
|
||||
// This mimics previous behaviour, but could be changed if we decide it's worth it.
|
||||
// The same choice (and reasoning) is also applied to the this.props.channelNamesMap.
|
||||
|
||||
return false;
|
||||
}
|
||||
static defaultProps = {
|
||||
options: {},
|
||||
mentionKeys: [],
|
||||
usernameMap: {}
|
||||
};
|
||||
|
||||
renderDeletedPost() {
|
||||
return (
|
||||
@@ -95,7 +97,7 @@ export default class PostMessageView extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.post.state === Constants.POST_DELETED) {
|
||||
if (this.props.post.state === Posts.POST_DELETED) {
|
||||
return this.renderDeletedPost();
|
||||
}
|
||||
|
||||
@@ -105,10 +107,10 @@ export default class PostMessageView extends React.Component {
|
||||
|
||||
const options = Object.assign({}, this.props.options, {
|
||||
emojis: this.props.emojis,
|
||||
siteURL: getSiteURL(),
|
||||
siteURL: this.props.siteUrl,
|
||||
mentionKeys: this.props.mentionKeys,
|
||||
usernameMap: this.props.usernameMap,
|
||||
channelNamesMap: this.props.channelNamesMap,
|
||||
channelNamesMap: getChannelsNameMapInCurrentTeam(store.getState()),
|
||||
team: this.props.team
|
||||
});
|
||||
|
||||
@@ -1,23 +1,41 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
|
||||
import {getDateForUnixTicks, isMobile, updateWindowDimensions} from 'utils/utils.jsx';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router/es6';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
|
||||
export default class PostTime extends React.Component {
|
||||
export default class PostTime extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/*
|
||||
* The time to display
|
||||
*/
|
||||
eventTime: PropTypes.number.isRequired,
|
||||
|
||||
/*
|
||||
* Set to display using 24 hour format
|
||||
*/
|
||||
useMilitaryTime: PropTypes.bool,
|
||||
|
||||
/*
|
||||
* The post id of posting being rendered
|
||||
*/
|
||||
postId: PropTypes.string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
eventTime: 0,
|
||||
useMilitaryTime: false
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
this.state = {
|
||||
currentTeamDisplayName: TeamStore.getCurrent().name,
|
||||
width: '',
|
||||
@@ -56,29 +74,18 @@ export default class PostTime extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return isMobile() ?
|
||||
this.renderTimeTag() :
|
||||
(
|
||||
<Link
|
||||
to={`/${this.state.currentTeamDisplayName}/pl/${this.props.postId}`}
|
||||
target='_blank'
|
||||
className='post__permalink'
|
||||
>
|
||||
{this.renderTimeTag()}
|
||||
</Link>
|
||||
);
|
||||
if (isMobile()) {
|
||||
return this.renderTimeTag();
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/${this.state.currentTeamDisplayName}/pl/${this.props.postId}`}
|
||||
target='_blank'
|
||||
className='post__permalink'
|
||||
>
|
||||
{this.renderTimeTag()}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostTime.defaultProps = {
|
||||
eventTime: 0,
|
||||
sameUser: false
|
||||
};
|
||||
|
||||
PostTime.propTypes = {
|
||||
eventTime: PropTypes.number.isRequired,
|
||||
sameUser: PropTypes.bool,
|
||||
compactDisplay: PropTypes.bool,
|
||||
useMilitaryTime: PropTypes.bool.isRequired,
|
||||
postId: PropTypes.string
|
||||
};
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information
|
||||
|
||||
import PostViewController from './post_view_controller.jsx';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const MAXIMUM_CACHED_VIEWS = 5;
|
||||
|
||||
export default class PostViewCache extends React.Component {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
viewChannel: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChannelChange = this.onChannelChange.bind(this);
|
||||
|
||||
const currentChannelId = ChannelStore.getCurrentId();
|
||||
const channel = ChannelStore.getCurrent();
|
||||
|
||||
this.state = {
|
||||
currentChannelId,
|
||||
channels: channel ? [channel] : []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onChannelChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (UserStore.getCurrentUser()) {
|
||||
this.props.actions.viewChannel('', this.state.currentChannelId || '');
|
||||
}
|
||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
||||
}
|
||||
|
||||
onChannelChange() {
|
||||
const channels = Object.assign([], this.state.channels);
|
||||
const currentChannel = ChannelStore.getCurrent();
|
||||
|
||||
if (!currentChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure current channel really changed
|
||||
if (currentChannel.id === this.state.currentChannelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (channels.length > MAXIMUM_CACHED_VIEWS) {
|
||||
channels.shift();
|
||||
}
|
||||
|
||||
const index = channels.map((c) => c.id).indexOf(currentChannel.id);
|
||||
if (index !== -1) {
|
||||
channels.splice(index, 1);
|
||||
}
|
||||
|
||||
channels.push(currentChannel);
|
||||
|
||||
this.setState({
|
||||
currentChannelId: currentChannel.id,
|
||||
channels
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const channels = this.state.channels;
|
||||
const currentChannelId = this.state.currentChannelId;
|
||||
|
||||
const postViews = [];
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
postViews.push(
|
||||
<PostViewController
|
||||
key={'postviewcontroller_' + channels[i].id}
|
||||
channel={channels[i]}
|
||||
active={channels[i].id === currentChannelId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='post-list'>
|
||||
{postViews}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,404 +0,0 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostList from './components/post_list.jsx';
|
||||
import LoadingScreen from 'components/loading_screen.jsx';
|
||||
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
import WebrtcStore from 'stores/webrtc_store.jsx';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const Preferences = Constants.Preferences;
|
||||
const ScrollTypes = Constants.ScrollTypes;
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostViewController extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||
this.onUserChange = this.onUserChange.bind(this);
|
||||
this.onPostsChange = this.onPostsChange.bind(this);
|
||||
this.onTeamChange = this.onTeamChange.bind(this);
|
||||
this.onStatusChange = this.onStatusChange.bind(this);
|
||||
this.onPostsViewJumpRequest = this.onPostsViewJumpRequest.bind(this);
|
||||
this.onSetNewMessageIndicator = this.onSetNewMessageIndicator.bind(this);
|
||||
this.onPostListScroll = this.onPostListScroll.bind(this);
|
||||
this.onActivate = this.onActivate.bind(this);
|
||||
this.onDeactivate = this.onDeactivate.bind(this);
|
||||
this.onBusy = this.onBusy.bind(this);
|
||||
|
||||
const channel = props.channel;
|
||||
const profiles = UserStore.getProfiles();
|
||||
|
||||
let lastViewed = Number.MAX_VALUE;
|
||||
let lastViewedBottom = Number.MAX_VALUE;
|
||||
const member = ChannelStore.getMyMember(channel.id);
|
||||
if (member != null) {
|
||||
lastViewed = member.last_viewed_at;
|
||||
lastViewedBottom = member.last_viewed_at;
|
||||
}
|
||||
|
||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
||||
|
||||
const statuses = Object.assign({}, UserStore.getStatuses());
|
||||
|
||||
// If we haven't received a page time then we aren't done loading the posts yet
|
||||
const loading = PostStore.getLatestPostFromPageTime(channel.id) === 0;
|
||||
|
||||
this.state = {
|
||||
channel,
|
||||
postList: PostStore.filterPosts(channel.id, joinLeaveEnabled),
|
||||
currentUser: UserStore.getCurrentUser(),
|
||||
currentTeamId: TeamStore.getCurrentId(),
|
||||
isBusy: WebrtcStore.isBusy(),
|
||||
profiles,
|
||||
statuses,
|
||||
atTop: PostStore.getVisibilityAtTop(channel.id),
|
||||
lastViewed,
|
||||
lastViewedBottom,
|
||||
ownNewMessage: false,
|
||||
loading,
|
||||
scrollType: ScrollTypes.NEW_MESSAGE,
|
||||
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
||||
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
||||
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
|
||||
previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false'),
|
||||
useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
|
||||
flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.active) {
|
||||
this.onActivate();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.active) {
|
||||
this.onDeactivate();
|
||||
}
|
||||
}
|
||||
|
||||
onPreferenceChange(category) {
|
||||
// Bit of a hack to force render when this setting is updated
|
||||
// regardless of change
|
||||
let previewSuffix = '';
|
||||
if (category === Preferences.CATEGORY_DISPLAY_SETTINGS) {
|
||||
previewSuffix = '_' + Utils.generateId();
|
||||
}
|
||||
|
||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
||||
|
||||
this.setState({
|
||||
postList: PostStore.filterPosts(this.state.channel.id, joinLeaveEnabled),
|
||||
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
||||
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
||||
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
|
||||
previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false') + previewSuffix,
|
||||
useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
|
||||
flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST)
|
||||
});
|
||||
}
|
||||
|
||||
onUserChange() {
|
||||
this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))});
|
||||
}
|
||||
|
||||
onPostsChange() {
|
||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
||||
const loading = PostStore.getLatestPostFromPageTime(this.state.channel.id) === 0;
|
||||
|
||||
const newState = {
|
||||
postList: PostStore.filterPosts(this.state.channel.id, joinLeaveEnabled),
|
||||
atTop: PostStore.getVisibilityAtTop(this.state.channel.id),
|
||||
loading
|
||||
};
|
||||
|
||||
if (this.state.loading && !loading) {
|
||||
newState.scrollType = ScrollTypes.NEW_MESSAGE;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
onStatusChange() {
|
||||
this.setState({statuses: Object.assign({}, UserStore.getStatuses())});
|
||||
}
|
||||
|
||||
onTeamChange() {
|
||||
const currentTeamId = TeamStore.getCurrentId();
|
||||
if ((this.state.channel.type === Constants.OPEN_CHANNEL || this.state.channel.type === Constants.PRIVATE_CHANNEL) && this.state.channel.team_id !== currentTeamId) {
|
||||
this.setState({
|
||||
currentTeamId,
|
||||
loading: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onActivate() {
|
||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
||||
UserStore.addChangeListener(this.onUserChange);
|
||||
TeamStore.addChangeListener(this.onTeamChange);
|
||||
UserStore.addStatusesChangeListener(this.onStatusChange);
|
||||
PostStore.addChangeListener(this.onPostsChange);
|
||||
PostStore.addPostsViewJumpListener(this.onPostsViewJumpRequest);
|
||||
ChannelStore.addLastViewedListener(this.onSetNewMessageIndicator);
|
||||
WebrtcStore.addBusyListener(this.onBusy);
|
||||
}
|
||||
|
||||
onDeactivate() {
|
||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
||||
UserStore.removeChangeListener(this.onUserChange);
|
||||
TeamStore.removeChangeListener(this.onTeamChange);
|
||||
UserStore.removeStatusesChangeListener(this.onStatusChange);
|
||||
PostStore.removeChangeListener(this.onPostsChange);
|
||||
PostStore.removePostsViewJumpListener(this.onPostsViewJumpRequest);
|
||||
ChannelStore.removeLastViewedListener(this.onSetNewMessageIndicator);
|
||||
WebrtcStore.removeBusyListener(this.onBusy);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.active && !nextProps.active) {
|
||||
this.onDeactivate();
|
||||
} else if (!this.props.active && nextProps.active) {
|
||||
this.onActivate();
|
||||
|
||||
const channel = nextProps.channel;
|
||||
|
||||
let lastViewed = Number.MAX_VALUE;
|
||||
const member = ChannelStore.getMyMember(channel.id);
|
||||
if (member != null) {
|
||||
lastViewed = member.last_viewed_at;
|
||||
}
|
||||
|
||||
const profiles = UserStore.getProfiles();
|
||||
|
||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
||||
|
||||
const statuses = Object.assign({}, UserStore.getStatuses());
|
||||
|
||||
this.setState({
|
||||
channel,
|
||||
lastViewed,
|
||||
ownNewMessage: false,
|
||||
profiles: JSON.parse(JSON.stringify(profiles)),
|
||||
statuses,
|
||||
postList: PostStore.filterPosts(channel.id, joinLeaveEnabled),
|
||||
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
||||
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
||||
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
|
||||
previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false'),
|
||||
useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
|
||||
scrollType: ScrollTypes.NEW_MESSAGE
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onPostsViewJumpRequest(type, postId) {
|
||||
switch (type) {
|
||||
case Constants.PostsViewJumpTypes.BOTTOM: {
|
||||
let lastViewedBottom;
|
||||
const lastPost = PostStore.getLatestPost(this.state.channel.id);
|
||||
|
||||
if (lastPost && lastPost.create_at) {
|
||||
lastViewedBottom = lastPost.create_at;
|
||||
} else {
|
||||
lastViewedBottom = new Date().getTime();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
scrollType: ScrollTypes.BOTTOM,
|
||||
lastViewedBottom
|
||||
});
|
||||
break;
|
||||
}
|
||||
case Constants.PostsViewJumpTypes.POST:
|
||||
this.setState({
|
||||
scrollType: ScrollTypes.POST,
|
||||
scrollPostId: postId
|
||||
});
|
||||
break;
|
||||
case Constants.PostsViewJumpTypes.SIDEBAR_OPEN:
|
||||
this.setState({scrollType: ScrollTypes.SIDEBAR_OPEN});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onSetNewMessageIndicator() {
|
||||
let lastViewed = Number.MAX_VALUE;
|
||||
const member = ChannelStore.getMyMember(this.props.channel.id);
|
||||
if (member != null) {
|
||||
lastViewed = member.last_viewed_at;
|
||||
}
|
||||
this.setState({lastViewed});
|
||||
}
|
||||
|
||||
onPostListScroll(atBottom) {
|
||||
if (atBottom) {
|
||||
let lastViewedBottom;
|
||||
const lastPost = PostStore.getLatestPost(this.state.channel.id);
|
||||
|
||||
if (lastPost && lastPost.create_at) {
|
||||
lastViewedBottom = lastPost.create_at;
|
||||
} else {
|
||||
lastViewedBottom = new Date().getTime();
|
||||
}
|
||||
|
||||
this.setState({scrollType: ScrollTypes.BOTTOM, lastViewedBottom});
|
||||
} else {
|
||||
this.setState({scrollType: ScrollTypes.FREE});
|
||||
}
|
||||
}
|
||||
|
||||
onBusy(isBusy) {
|
||||
this.setState({isBusy});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (nextProps.active !== this.props.active) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.loading !== this.state.loading) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.atTop !== this.state.atTop) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.displayNameType !== this.state.displayNameType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.displayPostsInCenter !== this.state.displayPostsInCenter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.compactDisplay !== this.state.compactDisplay) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.previewsCollapsed !== this.state.previewsCollapsed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.useMilitaryTime !== this.state.useMilitaryTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextState.flaggedPosts, this.state.flaggedPosts)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.lastViewed !== this.state.lastViewed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.ownNewMessage !== this.state.ownNewMessage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.showMoreMessagesTop !== this.state.showMoreMessagesTop) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.scrollType !== this.state.scrollType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.scrollPostId !== this.state.scrollPostId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.channel.id !== this.props.channel.id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextState.currentUser, this.state.currentUser)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextState.statuses, this.state.statuses)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextState.postList, this.state.postList)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextState.profiles, this.state.profiles)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.isBusy !== this.state.isBusy) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
let content;
|
||||
if (this.state.postList == null || this.state.loading) {
|
||||
content = (
|
||||
<LoadingScreen
|
||||
position='absolute'
|
||||
key='loading'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<PostList
|
||||
postList={this.state.postList}
|
||||
profiles={this.state.profiles}
|
||||
channelId={this.state.channel.id}
|
||||
channel={this.state.channel}
|
||||
currentUser={this.state.currentUser}
|
||||
showMoreMessagesTop={!this.state.atTop}
|
||||
scrollType={this.state.scrollType}
|
||||
scrollPostId={this.state.scrollPostId}
|
||||
postListScrolled={this.onPostListScroll}
|
||||
displayNameType={this.state.displayNameType}
|
||||
displayPostsInCenter={this.state.displayPostsInCenter}
|
||||
compactDisplay={this.state.compactDisplay}
|
||||
previewsCollapsed={this.state.previewsCollapsed}
|
||||
useMilitaryTime={this.state.useMilitaryTime}
|
||||
flaggedPosts={this.state.flaggedPosts}
|
||||
lastViewed={this.state.lastViewed}
|
||||
lastViewedBottom={this.state.lastViewedBottom}
|
||||
ownNewMessage={this.state.ownNewMessage}
|
||||
statuses={this.state.statuses}
|
||||
isBusy={this.state.isBusy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let activeClass = '';
|
||||
if (!this.props.active) {
|
||||
activeClass = 'inactive';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={activeClass}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostViewController.propTypes = {
|
||||
channel: PropTypes.object,
|
||||
active: PropTypes.bool
|
||||
};
|
||||
47
webapp/components/post_view/reaction/index.js
Обычный файл
47
webapp/components/post_view/reaction/index.js
Обычный файл
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {getCurrentUserId, makeGetProfilesForReactions} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getMissingProfilesByIds} from 'mattermost-redux/actions/users';
|
||||
import {addReaction, removeReaction} from 'mattermost-redux/actions/posts';
|
||||
import {getEmojiImageUrl} from 'mattermost-redux/utils/emoji_utils';
|
||||
import * as Emoji from 'utils/emoji.jsx';
|
||||
|
||||
import Reaction from './reaction.jsx';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getProfilesForReactions = makeGetProfilesForReactions();
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const profiles = getProfilesForReactions(state, ownProps.reactions);
|
||||
let emoji;
|
||||
if (Emoji.EmojiIndicesByAlias.has(ownProps.emojiName)) {
|
||||
emoji = Emoji.Emojis[Emoji.EmojiIndicesByAlias.get(ownProps.emojiName)];
|
||||
} else {
|
||||
emoji = ownProps.emojis[ownProps.emojiName];
|
||||
}
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
profiles,
|
||||
otherUsersCount: ownProps.reactions.length - profiles.length,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
reactionCount: ownProps.reactions.length,
|
||||
emojiImageUrl: getEmojiImageUrl(emoji)
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
addReaction,
|
||||
removeReaction,
|
||||
getMissingProfilesByIds
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(Reaction);
|
||||
@@ -1,28 +1,66 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import EmojiStore from 'stores/emoji_store.jsx';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
export default class Reaction extends React.Component {
|
||||
export default class Reaction extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/*
|
||||
* The post to render the reaction for
|
||||
*/
|
||||
post: PropTypes.object.isRequired,
|
||||
|
||||
/*
|
||||
* The user id of the logged in user
|
||||
*/
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
|
||||
/*
|
||||
* The name of the emoji for the reaction
|
||||
*/
|
||||
emojiName: PropTypes.string.isRequired,
|
||||
reactions: PropTypes.arrayOf(PropTypes.object),
|
||||
emojis: PropTypes.object.isRequired,
|
||||
|
||||
/*
|
||||
* The number of reactions to this post for this emoji
|
||||
*/
|
||||
reactionCount: PropTypes.number.isRequired,
|
||||
|
||||
/*
|
||||
* Array of users who reacted to this post
|
||||
*/
|
||||
profiles: PropTypes.array.isRequired,
|
||||
otherUsers: PropTypes.number.isRequired,
|
||||
|
||||
/*
|
||||
* The number of users not in the profile list who have reacted with this emoji
|
||||
*/
|
||||
otherUsersCount: PropTypes.number.isRequired,
|
||||
|
||||
/*
|
||||
* The URL of the emoji image
|
||||
*/
|
||||
emojiImageUrl: PropTypes.string.isRequired,
|
||||
|
||||
actions: PropTypes.shape({
|
||||
|
||||
/*
|
||||
* Function to add a reaction to a post
|
||||
*/
|
||||
addReaction: PropTypes.func.isRequired,
|
||||
getMissingProfiles: PropTypes.func.isRequired,
|
||||
|
||||
/*
|
||||
* Function to get non-loaded profiles by id
|
||||
*/
|
||||
getMissingProfilesByIds: PropTypes.func.isRequired,
|
||||
|
||||
/*
|
||||
* Function to remove a reaction from a post
|
||||
*/
|
||||
removeReaction: PropTypes.func.isRequired
|
||||
})
|
||||
}
|
||||
@@ -36,22 +74,18 @@ export default class Reaction extends React.Component {
|
||||
|
||||
addReaction(e) {
|
||||
e.preventDefault();
|
||||
this.props.actions.addReaction(this.props.post.channel_id, this.props.post.id, this.props.emojiName);
|
||||
this.props.actions.addReaction(this.props.post.id, this.props.emojiName);
|
||||
}
|
||||
|
||||
removeReaction(e) {
|
||||
e.preventDefault();
|
||||
this.props.actions.removeReaction(this.props.post.channel_id, this.props.post.id, this.props.emojiName);
|
||||
this.props.actions.removeReaction(this.props.post.id, this.props.emojiName);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.emojis.has(this.props.emojiName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let currentUserReacted = false;
|
||||
const users = [];
|
||||
const otherUsers = this.props.otherUsers;
|
||||
const otherUsersCount = this.props.otherUsersCount;
|
||||
for (const user of this.props.profiles) {
|
||||
if (user.id === this.props.currentUserId) {
|
||||
currentUserReacted = true;
|
||||
@@ -67,7 +101,7 @@ export default class Reaction extends React.Component {
|
||||
}
|
||||
|
||||
let names;
|
||||
if (otherUsers > 0) {
|
||||
if (otherUsersCount > 0) {
|
||||
if (users.length > 0) {
|
||||
names = (
|
||||
<FormattedMessage
|
||||
@@ -75,7 +109,7 @@ export default class Reaction extends React.Component {
|
||||
defaultMessage='{users} and {otherUsers, number} other {otherUsers, plural, one {user} other {users}}'
|
||||
values={{
|
||||
users: users.join(', '),
|
||||
otherUsers
|
||||
otherUsers: otherUsersCount
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -85,7 +119,7 @@ export default class Reaction extends React.Component {
|
||||
id='reaction.othersReacted'
|
||||
defaultMessage='{otherUsers, number} {otherUsers, plural, one {user} other {users}}'
|
||||
values={{
|
||||
otherUsers
|
||||
otherUsers: otherUsersCount
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -106,7 +140,7 @@ export default class Reaction extends React.Component {
|
||||
}
|
||||
|
||||
let reactionVerb;
|
||||
if (users.length + otherUsers > 1) {
|
||||
if (users.length + otherUsersCount > 1) {
|
||||
if (currentUserReacted) {
|
||||
reactionVerb = (
|
||||
<FormattedMessage
|
||||
@@ -185,7 +219,7 @@ export default class Reaction extends React.Component {
|
||||
{clickTooltip}
|
||||
</Tooltip>
|
||||
}
|
||||
onEnter={this.props.actions.getMissingProfiles}
|
||||
onEnter={this.props.actions.getMissingProfilesByIds}
|
||||
>
|
||||
<div
|
||||
className={className}
|
||||
@@ -193,10 +227,10 @@ export default class Reaction extends React.Component {
|
||||
>
|
||||
<span
|
||||
className='post-reaction__emoji emoticon'
|
||||
style={{backgroundImage: 'url(' + EmojiStore.getEmojiImageUrl(this.props.emojis.get(this.props.emojiName)) + ')'}}
|
||||
style={{backgroundImage: 'url(' + this.props.emojiImageUrl + ')'}}
|
||||
/>
|
||||
<span className='post-reaction__count'>
|
||||
{this.props.reactions.length}
|
||||
{this.props.reactionCount}
|
||||
</span>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
33
webapp/components/post_view/reaction_list/index.js
Обычный файл
33
webapp/components/post_view/reaction_list/index.js
Обычный файл
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCustomEmojisAsMap} from 'mattermost-redux/selectors/entities/emojis';
|
||||
|
||||
import * as Actions from 'mattermost-redux/actions/posts';
|
||||
|
||||
import ReactionList from './reaction_list.jsx';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getReactionsForPost = makeGetReactionsForPost();
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
reactions: getReactionsForPost(state, ownProps.post.id),
|
||||
emojis: getCustomEmojisAsMap(state)
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getReactionsForPost: Actions.getReactionsForPost
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(ReactionList);
|
||||
@@ -1,17 +1,41 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Reaction from './reaction_container.jsx';
|
||||
import Reaction from 'components/post_view/reaction';
|
||||
|
||||
export default class ReactionListView extends React.Component {
|
||||
export default class ReactionListView extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* The post to render reactions for
|
||||
*/
|
||||
post: PropTypes.object.isRequired,
|
||||
|
||||
/**
|
||||
* The reactions to render
|
||||
*/
|
||||
reactions: PropTypes.arrayOf(PropTypes.object),
|
||||
emojis: PropTypes.object.isRequired
|
||||
|
||||
/**
|
||||
* The emojis for the different reactions
|
||||
*/
|
||||
emojis: PropTypes.object.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
|
||||
/**
|
||||
* Function to get reactions for a post
|
||||
*/
|
||||
getReactionsForPost: PropTypes.func.isRequired
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.post.has_reactions) {
|
||||
this.props.actions.getReactionsForPost(this.props.post.id);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -41,7 +65,7 @@ export default class ReactionListView extends React.Component {
|
||||
key={emojiName}
|
||||
post={this.props.post}
|
||||
emojiName={emojiName}
|
||||
reactions={reactionsByName.get(emojiName)}
|
||||
reactions={reactionsByName.get(emojiName) || []}
|
||||
emojis={this.props.emojis}
|
||||
/>
|
||||
);
|
||||
Ссылка в новой задаче
Block a user