summaryrefslogtreecommitdiffstats
path: root/webapp/components
diff options
context:
space:
mode:
authorJoram Wilander <jwawilander@gmail.com>2017-06-18 14:42:32 -0400
committerGitHub <noreply@github.com>2017-06-18 14:42:32 -0400
commitab67f6e257f6e8f08145a02a7b93550f99641be4 (patch)
treed33d1c58a3d229f7e37db58bc2c397ac3806c503 /webapp/components
parent0231e95f1c5a8c42ba97875f0d2301815f552974 (diff)
downloadchat-ab67f6e257f6e8f08145a02a7b93550f99641be4.tar.gz
chat-ab67f6e257f6e8f08145a02a7b93550f99641be4.tar.bz2
chat-ab67f6e257f6e8f08145a02a7b93550f99641be4.zip
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
Diffstat (limited to 'webapp/components')
-rw-r--r--webapp/components/channel_view.jsx6
-rw-r--r--webapp/components/create_comment.jsx3
-rw-r--r--webapp/components/create_post.jsx35
-rw-r--r--webapp/components/dot_menu/dot_menu.jsx33
-rw-r--r--webapp/components/dot_menu/dot_menu_flag.jsx21
-rw-r--r--webapp/components/dot_menu/dot_menu_item.jsx20
-rw-r--r--webapp/components/dot_menu/index.js26
-rw-r--r--webapp/components/edit_post_modal.jsx12
-rw-r--r--webapp/components/file_attachment.jsx8
-rw-r--r--webapp/components/file_attachment_list/file_attachment_list.jsx (renamed from webapp/components/file_attachment_list.jsx)47
-rw-r--r--webapp/components/file_attachment_list/index.js39
-rw-r--r--webapp/components/file_attachment_list_container.jsx92
-rw-r--r--webapp/components/file_preview.jsx4
-rw-r--r--webapp/components/needs_team/needs_team.jsx11
-rw-r--r--webapp/components/permalink_view.jsx24
-rw-r--r--webapp/components/post_view/commented_on_files_message/commented_on_files_message.jsx51
-rw-r--r--webapp/components/post_view/commented_on_files_message/index.js36
-rw-r--r--webapp/components/post_view/components/commented_on_files_message_container.jsx90
-rw-r--r--webapp/components/post_view/components/date_separator.jsx26
-rw-r--r--webapp/components/post_view/components/post_attachment_list.jsx30
-rw-r--r--webapp/components/post_view/components/post_list.jsx690
-rw-r--r--webapp/components/post_view/components/post_message_container.jsx106
-rw-r--r--webapp/components/post_view/components/reaction_container.jsx90
-rw-r--r--webapp/components/post_view/components/reaction_list_container.jsx94
-rw-r--r--webapp/components/post_view/date_separator.jsx32
-rw-r--r--webapp/components/post_view/failed_post_options/failed_post_options.jsx (renamed from webapp/components/post_view/components/pending_post_options.jsx)49
-rw-r--r--webapp/components/post_view/failed_post_options/index.js24
-rw-r--r--webapp/components/post_view/floating_timestamp.jsx (renamed from webapp/components/post_view/components/floating_timestamp.jsx)22
-rw-r--r--webapp/components/post_view/index.js44
-rw-r--r--webapp/components/post_view/new_message_indicator.jsx (renamed from webapp/components/post_view/components/new_message_indicator.jsx)19
-rw-r--r--webapp/components/post_view/post/index.js33
-rw-r--r--webapp/components/post_view/post/post.jsx (renamed from webapp/components/post_view/components/post.jsx)214
-rw-r--r--webapp/components/post_view/post_attachment.jsx (renamed from webapp/components/post_view/components/post_attachment.jsx)46
-rw-r--r--webapp/components/post_view/post_attachment_list.jsx35
-rw-r--r--webapp/components/post_view/post_attachment_opengraph/index.js26
-rw-r--r--webapp/components/post_view/post_attachment_opengraph/post_attachment_opengraph.jsx (renamed from webapp/components/post_view/components/post_attachment_opengraph.jsx)96
-rw-r--r--webapp/components/post_view/post_body/index.js30
-rw-r--r--webapp/components/post_view/post_body/post_body.jsx (renamed from webapp/components/post_view/components/post_body.jsx)148
-rw-r--r--webapp/components/post_view/post_body_additional_content.jsx (renamed from webapp/components/post_view/components/post_body_additional_content.jsx)61
-rw-r--r--webapp/components/post_view/post_flag_icon.jsx (renamed from webapp/components/common/post_flag_icon.jsx)3
-rw-r--r--webapp/components/post_view/post_focus_view_controller.jsx212
-rw-r--r--webapp/components/post_view/post_header/index.js18
-rw-r--r--webapp/components/post_view/post_header/post_header.jsx (renamed from webapp/components/post_view/components/post_header.jsx)103
-rw-r--r--webapp/components/post_view/post_image.jsx (renamed from webapp/components/post_view/components/post_image.jsx)33
-rw-r--r--webapp/components/post_view/post_info/index.js31
-rw-r--r--webapp/components/post_view/post_info/post_info.jsx (renamed from webapp/components/post_view/components/post_info.jsx)119
-rw-r--r--webapp/components/post_view/post_list.jsx523
-rw-r--r--webapp/components/post_view/post_message_view/index.js41
-rw-r--r--webapp/components/post_view/post_message_view/post_message_view.jsx (renamed from webapp/components/post_view/components/post_message_view.jsx)108
-rw-r--r--webapp/components/post_view/post_message_view/system_message_helpers.jsx (renamed from webapp/components/post_view/components/system_message_helpers.jsx)0
-rw-r--r--webapp/components/post_view/post_time.jsx (renamed from webapp/components/post_view/components/post_time.jsx)71
-rw-r--r--webapp/components/post_view/post_view_cache.jsx98
-rw-r--r--webapp/components/post_view/post_view_controller.jsx404
-rw-r--r--webapp/components/post_view/reaction/index.js47
-rw-r--r--webapp/components/post_view/reaction/reaction.jsx (renamed from webapp/components/post_view/components/reaction.jsx)80
-rw-r--r--webapp/components/post_view/reaction_list/index.js33
-rw-r--r--webapp/components/post_view/reaction_list/reaction_list.jsx (renamed from webapp/components/post_view/components/reaction_list_view.jsx)36
-rw-r--r--webapp/components/post_view/scroll_to_bottom_arrows.jsx (renamed from webapp/components/post_view/components/scroll_to_bottom_arrows.jsx)0
-rw-r--r--webapp/components/rhs_comment.jsx71
-rw-r--r--webapp/components/rhs_root_post.jsx63
-rw-r--r--webapp/components/rhs_thread/index.js27
-rw-r--r--webapp/components/rhs_thread/rhs_thread.jsx (renamed from webapp/components/rhs_thread.jsx)145
-rw-r--r--webapp/components/search_results_item.jsx12
-rw-r--r--webapp/components/sidebar_right/index.js17
-rw-r--r--webapp/components/sidebar_right/sidebar_right.jsx (renamed from webapp/components/sidebar_right.jsx)56
-rw-r--r--webapp/components/view_image.jsx11
-rw-r--r--webapp/components/youtube_video/index.js16
-rw-r--r--webapp/components/youtube_video/youtube_video.jsx (renamed from webapp/components/youtube_video.jsx)32
68 files changed, 2028 insertions, 2855 deletions
diff --git a/webapp/components/channel_view.jsx b/webapp/components/channel_view.jsx
index 97275d37d..3f6edbd2e 100644
--- a/webapp/components/channel_view.jsx
+++ b/webapp/components/channel_view.jsx
@@ -9,7 +9,7 @@ import * as UserAgent from 'utils/user_agent.jsx';
import ChannelHeader from 'components/channel_header.jsx';
import FileUploadOverlay from 'components/file_upload_overlay.jsx';
import CreatePost from 'components/create_post.jsx';
-import PostViewCache from 'components/post_view';
+import PostView from 'components/post_view';
import ChannelStore from 'stores/channel_store.jsx';
@@ -77,7 +77,9 @@ export default class ChannelView extends React.Component {
<ChannelHeader
channelId={this.state.channelId}
/>
- <PostViewCache/>
+ <PostView
+ channelId={this.state.channelId}
+ />
<div
className='post-create__container'
id='post-create'
diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx
index 175eb03be..56e9eb88f 100644
--- a/webapp/components/create_comment.jsx
+++ b/webapp/components/create_comment.jsx
@@ -229,7 +229,6 @@ export default class CreateComment extends React.Component {
post.channel_id = this.props.channelId;
post.root_id = this.props.rootId;
post.parent_id = this.props.rootId;
- post.file_ids = this.state.fileInfos.map((info) => info.id);
post.pending_post_id = `${userId}:${time}`;
post.user_id = userId;
post.create_at = time;
@@ -244,7 +243,7 @@ export default class CreateComment extends React.Component {
});
}
- PostActions.queuePost(post, false, null,
+ PostActions.createPost(post, this.state.fileInfos, null,
(err) => {
if (err.id === 'api.post.create_post.root_id.app_error') {
this.showPostDeletedModal();
diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx
index d2f64a266..124728c3d 100644
--- a/webapp/components/create_post.jsx
+++ b/webapp/components/create_post.jsx
@@ -77,7 +77,7 @@ export default class CreatePost extends React.Component {
PostStore.clearDraftUploads();
const channelId = ChannelStore.getCurrentId();
- const draft = PostStore.getPostDraft(channelId);
+ const draft = PostStore.getDraft(channelId);
const stats = ChannelStore.getCurrentStats();
const members = stats.member_count - 1;
@@ -141,7 +141,7 @@ export default class CreatePost extends React.Component {
const isReaction = REACTION_PATTERN.exec(post.message);
if (post.message.indexOf('/') === 0) {
- PostActions.storePostDraft(this.state.channelId, null);
+ PostStore.storeDraft(this.state.channelId, null);
this.setState({message: '', postError: null, fileInfos: [], enableSendButton: false});
const args = {};
@@ -228,7 +228,6 @@ export default class CreatePost extends React.Component {
sendMessage(post) {
post.channel_id = this.state.channelId;
- post.file_ids = this.state.fileInfos.map((info) => info.id);
const time = Utils.getTimestamp();
const userId = UserStore.getCurrentId();
@@ -247,7 +246,7 @@ export default class CreatePost extends React.Component {
});
}
- PostActions.queuePost(post, false, null,
+ PostActions.createPost(post, this.state.fileInfos, null,
(err) => {
if (err.id === 'api.post.create_post.root_id.app_error') {
// this should never actually happen since you can't reply from this textbox
@@ -267,7 +266,7 @@ export default class CreatePost extends React.Component {
const action = isReaction[1];
const emojiName = isReaction[2];
- const postId = PostStore.getLatestNonEphemeralPost(this.state.channelId).id;
+ const postId = PostStore.getLatestPostId(this.state.channelId);
if (postId && action === '+') {
PostActions.addReaction(this.state.channelId, postId, emojiName);
@@ -275,7 +274,7 @@ export default class CreatePost extends React.Component {
PostActions.removeReaction(this.state.channelId, postId, emojiName);
}
- PostActions.storePostDraft(this.state.channelId, null);
+ PostStore.storeDraft(this.state.channelId, null);
}
focusTextbox(keepFocus = false) {
@@ -305,9 +304,9 @@ export default class CreatePost extends React.Component {
enableSendButton
});
- const draft = PostStore.getPostDraft(this.state.channelId);
+ const draft = PostStore.getDraft(this.state.channelId);
draft.message = message;
- PostActions.storePostDraft(this.state.channelId, draft);
+ PostStore.storeDraft(this.state.channelId, draft);
}
handleFileUploadChange() {
@@ -315,10 +314,10 @@ export default class CreatePost extends React.Component {
}
handleUploadStart(clientIds, channelId) {
- const draft = PostStore.getPostDraft(channelId);
+ const draft = PostStore.getDraft(channelId);
draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds);
- PostActions.storePostDraft(channelId, draft);
+ PostStore.storeDraft(channelId, draft);
this.setState({uploadsInProgress: draft.uploadsInProgress});
@@ -328,7 +327,7 @@ export default class CreatePost extends React.Component {
}
handleFileUploadComplete(fileInfos, clientIds, channelId) {
- const draft = PostStore.getPostDraft(channelId);
+ const draft = PostStore.getDraft(channelId);
// remove each finished file from uploads
for (let i = 0; i < clientIds.length; i++) {
@@ -340,7 +339,7 @@ export default class CreatePost extends React.Component {
}
draft.fileInfos = draft.fileInfos.concat(fileInfos);
- PostActions.storePostDraft(channelId, draft);
+ PostStore.storeDraft(channelId, draft);
if (channelId === this.state.channelId) {
this.setState({
@@ -359,14 +358,14 @@ export default class CreatePost extends React.Component {
}
if (clientId !== -1) {
- const draft = PostStore.getPostDraft(channelId);
+ const draft = PostStore.getDraft(channelId);
const index = draft.uploadsInProgress.indexOf(clientId);
if (index !== -1) {
draft.uploadsInProgress.splice(index, 1);
}
- PostActions.storePostDraft(channelId, draft);
+ PostStore.storeDraft(channelId, draft);
if (channelId === this.state.channelId) {
this.setState({uploadsInProgress: draft.uploadsInProgress});
@@ -396,10 +395,10 @@ export default class CreatePost extends React.Component {
fileInfos.splice(index, 1);
}
- const draft = PostStore.getPostDraft(this.state.channelId);
+ const draft = PostStore.getDraft(this.state.channelId);
draft.fileInfos = fileInfos;
draft.uploadsInProgress = uploadsInProgress;
- PostActions.storePostDraft(this.state.channelId, draft);
+ PostStore.storeDraft(this.state.channelId, draft);
const enableSendButton = this.handleEnableSendButton(this.state.message, fileInfos);
this.setState({fileInfos, uploadsInProgress, enableSendButton});
@@ -462,7 +461,7 @@ export default class CreatePost extends React.Component {
onChange() {
const channelId = ChannelStore.getCurrentId();
if (this.state.channelId !== channelId) {
- const draft = PostStore.getPostDraft(channelId);
+ const draft = PostStore.getDraft(channelId);
this.setState({channelId, message: draft.message, submitting: false, serverError: null, postError: null, fileInfos: draft.fileInfos, uploadsInProgress: draft.uploadsInProgress});
}
@@ -483,7 +482,7 @@ export default class CreatePost extends React.Component {
return this.state.fileInfos.length + this.state.uploadsInProgress.length;
}
- const draft = PostStore.getPostDraft(channelId);
+ const draft = PostStore.getDraft(channelId);
return draft.fileInfos.length + draft.uploadsInProgress.length;
}
diff --git a/webapp/components/dot_menu/dot_menu.jsx b/webapp/components/dot_menu/dot_menu.jsx
index b5f9fde45..eb6a6c005 100644
--- a/webapp/components/dot_menu/dot_menu.jsx
+++ b/webapp/components/dot_menu/dot_menu.jsx
@@ -22,7 +22,30 @@ export default class DotMenu extends Component {
commentCount: PropTypes.number,
isFlagged: PropTypes.bool,
handleCommentClick: PropTypes.func,
- handleDropdownOpened: PropTypes.func
+ handleDropdownOpened: PropTypes.func,
+
+ actions: PropTypes.shape({
+
+ /*
+ * Function flag the post
+ */
+ flagPost: PropTypes.func.isRequired,
+
+ /*
+ * Function to unflag the post
+ */
+ unflagPost: PropTypes.func.isRequired,
+
+ /*
+ * Function to pin the post
+ */
+ pinPost: PropTypes.func.isRequired,
+
+ /*
+ * Function to unpin the post
+ */
+ unpinPost: PropTypes.func.isRequired
+ }).isRequired
}
static defaultProps = {
@@ -90,6 +113,10 @@ export default class DotMenu extends Component {
idCount={this.props.idCount}
postId={this.props.post.id}
isFlagged={this.props.isFlagged}
+ actions={{
+ flagPost: this.props.actions.flagPost,
+ unflagPost: this.props.actions.unflagPost
+ }}
/>
);
}
@@ -121,6 +148,10 @@ export default class DotMenu extends Component {
idPrefix={idPrefix + 'Pin'}
idCount={this.props.idCount}
post={this.props.post}
+ actions={{
+ pinPost: this.props.actions.pinPost,
+ unpinPost: this.props.actions.unpinPost
+ }}
/>
);
}
diff --git a/webapp/components/dot_menu/dot_menu_flag.jsx b/webapp/components/dot_menu/dot_menu_flag.jsx
index 105363211..11546ee79 100644
--- a/webapp/components/dot_menu/dot_menu_flag.jsx
+++ b/webapp/components/dot_menu/dot_menu_flag.jsx
@@ -5,7 +5,6 @@ import React from 'react';
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
-import {flagPost, unflagPost} from 'actions/post_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
@@ -21,12 +20,12 @@ function formatMessage(isFlagged) {
export default function DotMenuFlag(props) {
function onFlagPost(e) {
e.preventDefault();
- flagPost(props.postId);
+ props.actions.flagPost(props.postId);
}
function onUnflagPost(e) {
e.preventDefault();
- unflagPost(props.postId);
+ props.actions.unflagPost(props.postId);
}
const flagFunc = props.isFlagged ? onUnflagPost : onFlagPost;
@@ -60,7 +59,21 @@ DotMenuFlag.propTypes = {
idCount: PropTypes.number,
idPrefix: PropTypes.string.isRequired,
postId: PropTypes.string.isRequired,
- isFlagged: PropTypes.bool.isRequired
+ isFlagged: PropTypes.bool.isRequired,
+
+ actions: PropTypes.shape({
+
+ /*
+ * Function flag the post
+ */
+ flagPost: PropTypes.func.isRequired,
+
+ /*
+ * Function to unflag the post
+ */
+ unflagPost: PropTypes.func.isRequired
+
+ }).isRequired
};
DotMenuFlag.defaultProps = {
diff --git a/webapp/components/dot_menu/dot_menu_item.jsx b/webapp/components/dot_menu/dot_menu_item.jsx
index ceda0a1a4..6411beafb 100644
--- a/webapp/components/dot_menu/dot_menu_item.jsx
+++ b/webapp/components/dot_menu/dot_menu_item.jsx
@@ -5,7 +5,6 @@ import React from 'react';
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
-import {unpinPost, pinPost} from 'actions/post_actions.jsx';
import {showGetPostLinkModal, showDeletePostModal} from 'actions/global_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
@@ -18,12 +17,12 @@ export default function DotMenuItem(props) {
function handleUnpinPost(e) {
e.preventDefault();
- unpinPost(props.post.channel_id, props.post.id);
+ props.actions.unpinPost(props.post.id);
}
function handlePinPost(e) {
e.preventDefault();
- pinPost(props.post.channel_id, props.post.id);
+ props.actions.pinPost(props.post.id);
}
function handleDeletePost(e) {
@@ -98,7 +97,20 @@ DotMenuItem.propTypes = {
post: PropTypes.object,
handleOnClick: PropTypes.func,
type: PropTypes.string,
- commentCount: PropTypes.number
+ commentCount: PropTypes.number,
+
+ actions: PropTypes.shape({
+
+ /*
+ * Function to pin the post
+ */
+ pinPost: PropTypes.func,
+
+ /*
+ * Function to unpin the post
+ */
+ unpinPost: PropTypes.func
+ })
};
DotMenuItem.defaultProps = {
diff --git a/webapp/components/dot_menu/index.js b/webapp/components/dot_menu/index.js
new file mode 100644
index 000000000..eaa1e8d2c
--- /dev/null
+++ b/webapp/components/dot_menu/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 {flagPost, unflagPost, pinPost, unpinPost} from 'mattermost-redux/actions/posts';
+
+import DotMenu from './dot_menu.jsx';
+
+function mapStateToProps(state, ownProps) {
+ return ownProps;
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ flagPost,
+ unflagPost,
+ pinPost,
+ unpinPost
+ }, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(DotMenu);
+
diff --git a/webapp/components/edit_post_modal.jsx b/webapp/components/edit_post_modal.jsx
index 3ec7fedcc..683371d23 100644
--- a/webapp/components/edit_post_modal.jsx
+++ b/webapp/components/edit_post_modal.jsx
@@ -21,6 +21,11 @@ import React from 'react';
import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl';
+import store from 'stores/redux_store.jsx';
+const getState = store.getState;
+
+import * as Selectors from 'mattermost-redux/selectors/entities/posts';
+
export default class EditPostModal extends React.Component {
constructor(props) {
super(props);
@@ -85,7 +90,7 @@ export default class EditPostModal extends React.Component {
Reflect.deleteProperty(tempState, 'editText');
BrowserStore.setItem('edit_state_transfer', tempState);
$('#edit_post').modal('hide');
- GlobalActions.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
+ GlobalActions.showDeletePostModal(Selectors.getPost(getState(), this.state.post_id), this.state.comments);
return;
}
@@ -93,8 +98,7 @@ export default class EditPostModal extends React.Component {
updatedPost,
() => {
window.scrollTo(0, 0);
- },
- Boolean(PostStore.getFocusedPostId()) // If there is focused post we need to update that post's store too.
+ }
);
$('#edit_post').modal('hide');
@@ -120,7 +124,7 @@ export default class EditPostModal extends React.Component {
}
handleEditPostEvent(options) {
- var post = PostStore.getPost(options.channelId, options.postId);
+ const post = Selectors.getPost(getState(), options.postId);
if (global.window.mm_license.IsLicensed === 'true') {
if (global.window.mm_config.AllowEditPost === Constants.ALLOW_EDIT_POST_NEVER) {
return;
diff --git a/webapp/components/file_attachment.jsx b/webapp/components/file_attachment.jsx
index 0b8bd1042..f14718e64 100644
--- a/webapp/components/file_attachment.jsx
+++ b/webapp/components/file_attachment.jsx
@@ -2,7 +2,7 @@
// See License.txt for license information.
import Constants from 'utils/constants.jsx';
-import FileStore from 'stores/file_store.jsx';
+import {getFileUrl, getFileThumbnailUrl} from 'mattermost-redux/utils/file_utils';
import * as Utils from 'utils/utils.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
@@ -46,7 +46,7 @@ export default class FileAttachment extends React.Component {
const fileType = Utils.getFileType(fileInfo.extension);
if (fileType === 'image') {
- const thumbnailUrl = FileStore.getFileThumbnailUrl(fileInfo.id);
+ const thumbnailUrl = getFileThumbnailUrl(fileInfo.id);
const img = new Image();
img.onload = () => {
@@ -64,7 +64,7 @@ export default class FileAttachment extends React.Component {
render() {
const fileInfo = this.props.fileInfo;
const fileName = fileInfo.name;
- const fileUrl = FileStore.getFileUrl(fileInfo.id);
+ const fileUrl = getFileUrl(fileInfo.id);
let thumbnail;
if (this.state.loaded) {
@@ -83,7 +83,7 @@ export default class FileAttachment extends React.Component {
<div
className={className}
style={{
- backgroundImage: `url(${FileStore.getFileThumbnailUrl(fileInfo.id)})`
+ backgroundImage: `url(${getFileThumbnailUrl(fileInfo.id)})`
}}
/>
);
diff --git a/webapp/components/file_attachment_list.jsx b/webapp/components/file_attachment_list/file_attachment_list.jsx
index 9beacf94c..31b1ac424 100644
--- a/webapp/components/file_attachment_list.jsx
+++ b/webapp/components/file_attachment_list/file_attachment_list.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import ViewImageModal from './view_image.jsx';
-import FileAttachment from './file_attachment.jsx';
+import ViewImageModal from 'components/view_image.jsx';
+import FileAttachment from 'components/file_attachment.jsx';
import Constants from 'utils/constants.jsx';
import PropTypes from 'prop-types';
@@ -10,6 +10,37 @@ import PropTypes from 'prop-types';
import React from 'react';
export default class FileAttachmentList extends React.Component {
+ static propTypes = {
+
+ /*
+ * The post the files are attached to
+ */
+ post: PropTypes.object.isRequired,
+
+ /*
+ * The number of files attached to the post
+ */
+ fileCount: PropTypes.number.isRequired,
+
+ /*
+ * Array of metadata for each file attached to the post
+ */
+ fileInfos: PropTypes.arrayOf(PropTypes.object),
+
+ /*
+ * Set to render compactly
+ */
+ compactDisplay: PropTypes.bool,
+
+ actions: PropTypes.shape({
+
+ /*
+ * Function to get file metadata for a post
+ */
+ getMissingFilesForPost: PropTypes.func.isRequired
+ }).isRequired
+ }
+
constructor(props) {
super(props);
@@ -18,6 +49,12 @@ export default class FileAttachmentList extends React.Component {
this.state = {showPreviewModal: false, startImgIndex: 0};
}
+ componentDidMount() {
+ if (this.props.post.file_ids || this.props.post.filenames) {
+ this.props.actions.getMissingFilesForPost(this.props.post.id);
+ }
+ }
+
handleImageClick(indexClicked) {
this.setState({showPreviewModal: true, startImgIndex: indexClicked});
}
@@ -65,9 +102,3 @@ export default class FileAttachmentList extends React.Component {
);
}
}
-
-FileAttachmentList.propTypes = {
- fileCount: PropTypes.number.isRequired,
- fileInfos: PropTypes.arrayOf(PropTypes.object),
- compactDisplay: PropTypes.bool
-};
diff --git a/webapp/components/file_attachment_list/index.js b/webapp/components/file_attachment_list/index.js
new file mode 100644
index 000000000..4081e4220
--- /dev/null
+++ b/webapp/components/file_attachment_list/index.js
@@ -0,0 +1,39 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import {getMissingFilesForPost} from 'mattermost-redux/actions/files';
+import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files';
+
+import FileAttachmentList from './file_attachment_list.jsx';
+
+function makeMapStateToProps() {
+ const selectFilesForPost = makeGetFilesForPost();
+ return function mapStateToProps(state, ownProps) {
+ const fileInfos = selectFilesForPost(state, ownProps.post);
+
+ let fileCount = 0;
+ if (ownProps.post.file_ids) {
+ fileCount = ownProps.post.file_ids.length;
+ } else if (this.props.post.filenames) {
+ fileCount = ownProps.post.filenames.length;
+ }
+
+ return {
+ ...ownProps,
+ fileInfos,
+ fileCount
+ };
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ getMissingFilesForPost
+ }, dispatch)
+ };
+}
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(FileAttachmentList);
diff --git a/webapp/components/file_attachment_list_container.jsx b/webapp/components/file_attachment_list_container.jsx
deleted file mode 100644
index 4b05e392c..000000000
--- a/webapp/components/file_attachment_list_container.jsx
+++ /dev/null
@@ -1,92 +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 FileAttachmentList from './file_attachment_list.jsx';
-
-export default class FileAttachmentListContainer extends React.Component {
- static propTypes = {
- post: PropTypes.object.isRequired,
- compactDisplay: PropTypes.bool.isRequired
- }
-
- constructor(props) {
- super(props);
-
- this.handleFileChange = this.handleFileChange.bind(this);
-
- this.state = {
- fileInfos: FileStore.getInfosForPost(props.post.id)
- };
- }
-
- componentDidMount() {
- FileStore.addChangeListener(this.handleFileChange);
-
- if (this.props.post.id && !FileStore.hasInfosForPost(this.props.post.id)) {
- AsyncClient.getFileInfosForPost(this.props.post.channel_id, this.props.post.id);
- }
- }
-
- componentWillReceiveProps(nextProps) {
- if (nextProps.post.id !== this.props.post.id) {
- this.setState({
- fileInfos: FileStore.getInfosForPost(nextProps.post.id)
- });
-
- if (nextProps.post.id && !FileStore.hasInfosForPost(nextProps.post.id)) {
- AsyncClient.getFileInfosForPost(nextProps.post.channel_id, nextProps.post.id);
- }
- }
- }
-
- shouldComponentUpdate(nextProps, nextState) {
- if (this.props.post.id !== nextProps.post.id) {
- return true;
- }
-
- if (this.props.compactDisplay !== nextProps.compactDisplay) {
- 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.post.id)
- });
- }
-
- componentWillUnmount() {
- FileStore.removeChangeListener(this.handleFileChange);
- }
-
- render() {
- let fileCount = 0;
- if (this.props.post.file_ids) {
- fileCount = this.props.post.file_ids.length;
- } else if (this.props.post.filenames) {
- fileCount = this.props.post.filenames.length;
- }
-
- return (
- <FileAttachmentList
- fileCount={fileCount}
- fileInfos={this.state.fileInfos}
- compactDisplay={this.props.compactDisplay}
- />
- );
- }
-}
diff --git a/webapp/components/file_preview.jsx b/webapp/components/file_preview.jsx
index 3bf05744f..65a71c047 100644
--- a/webapp/components/file_preview.jsx
+++ b/webapp/components/file_preview.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import FileStore from 'stores/file_store.jsx';
import ReactDOM from 'react-dom';
import * as Utils from 'utils/utils.jsx';
+import {getFileUrl} from 'mattermost-redux/utils/file_utils';
import PropTypes from 'prop-types';
@@ -39,7 +39,7 @@ export default class FilePreview extends React.Component {
previewImage = (
<img
className='file-preview__image'
- src={FileStore.getFileUrl(info.id)}
+ src={getFileUrl(info.id)}
/>
);
} else {
diff --git a/webapp/components/needs_team/needs_team.jsx b/webapp/components/needs_team/needs_team.jsx
index 4f5188a47..6fd2d3208 100644
--- a/webapp/components/needs_team/needs_team.jsx
+++ b/webapp/components/needs_team/needs_team.jsx
@@ -13,7 +13,6 @@ import UserStore from 'stores/user_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
-import * as GlobalActions from 'actions/global_actions.jsx';
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
import {startPeriodicSync, stopPeriodicSync} from 'actions/websocket_actions.jsx';
import {loadProfilesForSidebar} from 'actions/user_actions.jsx';
@@ -23,13 +22,16 @@ const TutorialSteps = Constants.TutorialSteps;
const Preferences = Constants.Preferences;
import AnnouncementBar from 'components/announcement_bar';
-import SidebarRight from 'components/sidebar_right.jsx';
+import SidebarRight from 'components/sidebar_right';
import SidebarRightMenu from 'components/sidebar_right_menu.jsx';
import Navbar from 'components/navbar.jsx';
import WebrtcSidebar from 'components/webrtc/components/webrtc_sidebar.jsx';
import WebrtcNotification from 'components/webrtc/components/webrtc_notification.jsx';
+import store from 'stores/redux_store.jsx';
+import {getPost} from 'mattermost-redux/selectors/entities/posts';
+
// Modals
import GetPostLinkModal from 'components/get_post_link_modal.jsx';
import GetPublicLinkModal from 'components/get_public_link_modal.jsx';
@@ -111,9 +113,6 @@ export default class NeedsTeam extends React.Component {
TeamStore.addChangeListener(this.onTeamChanged);
PreferenceStore.addChangeListener(this.onPreferencesChanged);
- // Emit view action
- GlobalActions.viewLoggedIn();
-
startPeriodicStatusUpdates();
startPeriodicSync();
@@ -201,7 +200,7 @@ export default class NeedsTeam extends React.Component {
if (channel == null) {
// the permalink view is not really tied to a particular channel but still needs it
const postId = PostStore.getFocusedPostId();
- const post = PostStore.getEarliestPostFromPage(postId);
+ const post = getPost(store.getState(), postId);
// the post take some time before being available on page load
if (post != null) {
diff --git a/webapp/components/permalink_view.jsx b/webapp/components/permalink_view.jsx
index ebcd83916..237ad8f44 100644
--- a/webapp/components/permalink_view.jsx
+++ b/webapp/components/permalink_view.jsx
@@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import ChannelHeader from 'components/channel_header.jsx';
-import PostFocusViewController from 'components/post_view/post_focus_view_controller.jsx';
+import PostView from 'components/post_view';
import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
@@ -14,7 +14,11 @@ import TeamStore from 'stores/team_store.jsx';
import {Link} from 'react-router/es6';
import {FormattedMessage} from 'react-intl';
-export default class PermalinkView extends React.Component {
+export default class PermalinkView extends React.PureComponent {
+ static propTypes = {
+ params: PropTypes.object.isRequired
+ }
+
constructor(props) {
super(props);
@@ -24,6 +28,7 @@ export default class PermalinkView extends React.Component {
this.state = this.getStateFromStores(props);
}
+
getStateFromStores(props) {
const postId = props.params.postid;
const channel = ChannelStore.getCurrent();
@@ -38,27 +43,33 @@ export default class PermalinkView extends React.Component {
postId
};
}
+
isStateValid() {
return this.state.channelId !== '' && this.state.teamName;
}
+
updateState() {
this.setState(this.getStateFromStores(this.props));
}
+
componentDidMount() {
ChannelStore.addChangeListener(this.updateState);
TeamStore.addChangeListener(this.updateState);
$('body').addClass('app__body');
}
+
componentWillUnmount() {
ChannelStore.removeChangeListener(this.updateState);
TeamStore.removeChangeListener(this.updateState);
$('body').removeClass('app__body');
}
+
componentWillReceiveProps(nextProps) {
this.setState(this.getStateFromStores(nextProps));
}
+
render() {
if (!this.isStateValid()) {
return null;
@@ -71,7 +82,10 @@ export default class PermalinkView extends React.Component {
<ChannelHeader
channelId={this.state.channelId}
/>
- <PostFocusViewController/>
+ <PostView
+ channelId={this.state.channelId}
+ focusedPostId={this.state.postId}
+ />
<div
id='archive-link-home'
>
@@ -89,7 +103,3 @@ export default class PermalinkView extends React.Component {
);
}
}
-
-PermalinkView.propTypes = {
- params: PropTypes.object.isRequired
-};
diff --git a/webapp/components/post_view/commented_on_files_message/commented_on_files_message.jsx b/webapp/components/post_view/commented_on_files_message/commented_on_files_message.jsx
new file mode 100644
index 000000000..a09b2b156
--- /dev/null
+++ b/webapp/components/post_view/commented_on_files_message/commented_on_files_message.jsx
@@ -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>;
+ }
+}
diff --git a/webapp/components/post_view/commented_on_files_message/index.js b/webapp/components/post_view/commented_on_files_message/index.js
new file mode 100644
index 000000000..fd6aa7193
--- /dev/null
+++ b/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);
diff --git a/webapp/components/post_view/components/commented_on_files_message_container.jsx b/webapp/components/post_view/components/commented_on_files_message_container.jsx
deleted file mode 100644
index 6ba1de3de..000000000
--- a/webapp/components/post_view/components/commented_on_files_message_container.jsx
+++ /dev/null
@@ -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>;
- }
-}
diff --git a/webapp/components/post_view/components/date_separator.jsx b/webapp/components/post_view/components/date_separator.jsx
deleted file mode 100644
index 4648f456c..000000000
--- a/webapp/components/post_view/components/date_separator.jsx
+++ /dev/null
@@ -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)
-};
diff --git a/webapp/components/post_view/components/post_attachment_list.jsx b/webapp/components/post_view/components/post_attachment_list.jsx
deleted file mode 100644
index 3d7c0e4cd..000000000
--- a/webapp/components/post_view/components/post_attachment_list.jsx
+++ /dev/null
@@ -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
-};
diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx
deleted file mode 100644
index ec3c8dc6a..000000000
--- a/webapp/components/post_view/components/post_list.jsx
+++ /dev/null
@@ -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
-};
diff --git a/webapp/components/post_view/components/post_message_container.jsx b/webapp/components/post_view/components/post_message_container.jsx
deleted file mode 100644
index 91ca03828..000000000
--- a/webapp/components/post_view/components/post_message_container.jsx
+++ /dev/null
@@ -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}
- />
- );
- }
-}
diff --git a/webapp/components/post_view/components/reaction_container.jsx b/webapp/components/post_view/components/reaction_container.jsx
deleted file mode 100644
index 29936c60a..000000000
--- a/webapp/components/post_view/components/reaction_container.jsx
+++ /dev/null
@@ -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
- }}
- />
- );
- }
-}
diff --git a/webapp/components/post_view/components/reaction_list_container.jsx b/webapp/components/post_view/components/reaction_list_container.jsx
deleted file mode 100644
index fbc5f683c..000000000
--- a/webapp/components/post_view/components/reaction_list_container.jsx
+++ /dev/null
@@ -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}
- />
- );
- }
-}
diff --git a/webapp/components/post_view/date_separator.jsx b/webapp/components/post_view/date_separator.jsx
new file mode 100644
index 000000000..3f5184dbf
--- /dev/null
+++ b/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>
+ );
+ }
+}
diff --git a/webapp/components/post_view/components/pending_post_options.jsx b/webapp/components/post_view/failed_post_options/failed_post_options.jsx
index 9742a74bf..f28de343b 100644
--- a/webapp/components/post_view/components/pending_post_options.jsx
+++ b/webapp/components/post_view/failed_post_options/failed_post_options.jsx
@@ -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 {createPost} from 'actions/post_actions.jsx';
+import React from 'react';
+import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
-import PropTypes from 'prop-types';
+export default class FailedPostOptions extends React.Component {
+ static propTypes = {
-import React from 'react';
+ /*
+ * 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
-};
diff --git a/webapp/components/post_view/failed_post_options/index.js b/webapp/components/post_view/failed_post_options/index.js
new file mode 100644
index 000000000..bb8dde893
--- /dev/null
+++ b/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);
diff --git a/webapp/components/post_view/components/floating_timestamp.jsx b/webapp/components/post_view/floating_timestamp.jsx
index 34e6ce006..f0f6af60e 100644
--- a/webapp/components/post_view/components/floating_timestamp.jsx
+++ b/webapp/components/post_view/floating_timestamp.jsx
@@ -3,16 +3,15 @@
import {FormattedDate} from 'react-intl';
-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);
+import PropTypes from 'prop-types';
- 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
-};
diff --git a/webapp/components/post_view/index.js b/webapp/components/post_view/index.js
index b42b486ab..ad0270cdd 100644
--- a/webapp/components/post_view/index.js
+++ b/webapp/components/post_view/index.js
@@ -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);
diff --git a/webapp/components/post_view/components/new_message_indicator.jsx b/webapp/components/post_view/new_message_indicator.jsx
index cafdc128a..d5fb6c1d3 100644
--- a/webapp/components/post_view/components/new_message_indicator.jsx
+++ b/webapp/components/post_view/new_message_indicator.jsx
@@ -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
-};
diff --git a/webapp/components/post_view/post/index.js b/webapp/components/post_view/post/index.js
new file mode 100644
index 000000000..1e195f920
--- /dev/null
+++ b/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);
diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/post/post.jsx
index b9823396d..eda4405bb 100644
--- a/webapp/components/post_view/components/post.jsx
+++ b/webapp/components/post_view/post/post.jsx
@@ -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 AppDispatcher from 'dispatcher/app_dispatcher.jsx';
+import Constants from 'utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
+import {Posts} from 'mattermost-redux/constants';
-import Constants, {ActionTypes} from 'utils/constants.jsx';
-import * as PostUtils from 'utils/post_utils.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as PostUtils from 'utils/post_utils.jsx';
+import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
-import PostBody from './post_body.jsx';
-import PostHeader from './post_header.jsx';
+import React from 'react';
+import PropTypes from 'prop-types';
-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,
+
+ /**
+ * 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,
- previewCollapsed: PropTypes.string,
- commentCount: PropTypes.number,
+
+ /**
+ * 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,
- useMilitaryTime: PropTypes.bool.isRequired,
- isFlagged: PropTypes.bool,
- status: PropTypes.string,
+
+ /**
+ * 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>
diff --git a/webapp/components/post_view/components/post_attachment.jsx b/webapp/components/post_view/post_attachment.jsx
index e873ef9c7..b7bd1ade9 100644
--- a/webapp/components/post_view/components/post_attachment.jsx
+++ b/webapp/components/post_view/post_attachment.jsx
@@ -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);
diff --git a/webapp/components/post_view/post_attachment_list.jsx b/webapp/components/post_view/post_attachment_list.jsx
new file mode 100644
index 000000000..cfd2f81f8
--- /dev/null
+++ b/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>
+ );
+ }
+}
diff --git a/webapp/components/post_view/post_attachment_opengraph/index.js b/webapp/components/post_view/post_attachment_opengraph/index.js
new file mode 100644
index 000000000..e0bec8f36
--- /dev/null
+++ b/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);
diff --git a/webapp/components/post_view/components/post_attachment_opengraph.jsx b/webapp/components/post_view/post_attachment_opengraph/post_attachment_opengraph.jsx
index 129111800..dbf8f6049 100644
--- a/webapp/components/post_view/components/post_attachment_opengraph.jsx
+++ b/webapp/components/post_view/post_attachment_opengraph/post_attachment_opengraph.jsx
@@ -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
-};
diff --git a/webapp/components/post_view/post_body/index.js b/webapp/components/post_view/post_body/index.js
new file mode 100644
index 000000000..37cf114b0
--- /dev/null
+++ b/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);
diff --git a/webapp/components/post_view/components/post_body.jsx b/webapp/components/post_view/post_body/post_body.jsx
index 0f481ec02..a60d25760 100644
--- a/webapp/components/post_view/components/post_body.jsx
+++ b/webapp/components/post_view/post_body/post_body.jsx
@@ -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);
-
- this.removePost = this.removePost.bind(this);
- }
-
- shouldComponentUpdate(nextProps) {
- if (nextProps.isCommentMention !== this.props.isCommentMention) {
- return true;
- }
-
- if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
- return true;
- }
-
- if (!Utils.areObjectsEqual(nextProps.parentPost, this.props.parentPost)) {
- return true;
- }
-
- if (nextProps.compactDisplay !== this.props.compactDisplay) {
- return true;
- }
-
- if (nextProps.previewCollapsed !== this.props.previewCollapsed) {
- return true;
- }
-
- if (nextProps.handleCommentClick.toString() !== this.props.handleCommentClick.toString()) {
- return true;
- }
-
- if (nextProps.lastPostCount !== this.props.lastPostCount) {
- return true;
- }
-
- return false;
- }
-
- removePost() {
- GlobalActions.emitRemovePost(this.props.post);
+export default class PostBody extends React.PureComponent {
+ static propTypes = {
+
+ /**
+ * The post to render the body of
+ */
+ post: PropTypes.object.isRequired,
+
+ /**
+ * The parent post of the thread this post is in
+ */
+ parentPost: PropTypes.object,
+
+ /**
+ * The poster of the parent post, if exists
+ */
+ parentPostUser: PropTypes.object,
+
+ /**
+ * The function called when the comment icon is clicked
+ */
+ handleCommentClick: PropTypes.func.isRequired,
+
+ /**
+ * Set to render post body compactly
+ */
+ compactDisplay: PropTypes.bool,
+
+ /**
+ * Set to highlight comment as a mention
+ */
+ isCommentMention: PropTypes.bool,
+
+ /**
+ * Set to collapse image and video previews
+ */
+ previewCollapsed: PropTypes.string,
+
+ /**
+ * 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
-};
diff --git a/webapp/components/post_view/components/post_body_additional_content.jsx b/webapp/components/post_view/post_body_additional_content.jsx
index 180681100..bf8380912 100644
--- a/webapp/components/post_view/components/post_body_additional_content.jsx
+++ b/webapp/components/post_view/post_body_additional_content.jsx
@@ -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
-};
diff --git a/webapp/components/common/post_flag_icon.jsx b/webapp/components/post_view/post_flag_icon.jsx
index 533b38bff..295bdd116 100644
--- a/webapp/components/common/post_flag_icon.jsx
+++ b/webapp/components/post_view/post_flag_icon.jsx
@@ -1,9 +1,8 @@
-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 {FormattedMessage} from 'react-intl';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
diff --git a/webapp/components/post_view/post_focus_view_controller.jsx b/webapp/components/post_view/post_focus_view_controller.jsx
deleted file mode 100644
index dadc6b80e..000000000
--- a/webapp/components/post_view/post_focus_view_controller.jsx
+++ /dev/null
@@ -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>
- );
- }
-}
diff --git a/webapp/components/post_view/post_header/index.js b/webapp/components/post_view/post_header/index.js
new file mode 100644
index 000000000..d7aaef1d5
--- /dev/null
+++ b/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);
diff --git a/webapp/components/post_view/components/post_header.jsx b/webapp/components/post_view/post_header/post_header.jsx
index eab2d4629..562bd2b82 100644
--- a/webapp/components/post_view/components/post_header.jsx
+++ b/webapp/components/post_view/post_header/post_header.jsx
@@ -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
-};
diff --git a/webapp/components/post_view/components/post_image.jsx b/webapp/components/post_view/post_image.jsx
index 1268c9df2..5feb01db4 100644
--- a/webapp/components/post_view/components/post_image.jsx
+++ b/webapp/components/post_view/post_image.jsx
@@ -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
-};
diff --git a/webapp/components/post_view/post_info/index.js b/webapp/components/post_view/post_info/index.js
new file mode 100644
index 000000000..749ec5aba
--- /dev/null
+++ b/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);
diff --git a/webapp/components/post_view/components/post_info.jsx b/webapp/components/post_view/post_info/post_info.jsx
index 502565752..f037bf03b 100644
--- a/webapp/components/post_view/components/post_info.jsx
+++ b/webapp/components/post_view/post_info/post_info.jsx
@@ -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
-};
diff --git a/webapp/components/post_view/post_list.jsx b/webapp/components/post_view/post_list.jsx
new file mode 100644
index 000000000..bf0ee079d
--- /dev/null
+++ b/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>
+ );
+ }
+}
diff --git a/webapp/components/post_view/post_message_view/index.js b/webapp/components/post_view/post_message_view/index.js
new file mode 100644
index 000000000..cf457a508
--- /dev/null
+++ b/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);
diff --git a/webapp/components/post_view/components/post_message_view.jsx b/webapp/components/post_view/post_message_view/post_message_view.jsx
index 938b5a8db..66a8d01f8 100644
--- a/webapp/components/post_view/components/post_message_view.jsx
+++ b/webapp/components/post_view/post_message_view/post_message_view.jsx
@@ -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
});
diff --git a/webapp/components/post_view/components/system_message_helpers.jsx b/webapp/components/post_view/post_message_view/system_message_helpers.jsx
index c134e1a7a..c134e1a7a 100644
--- a/webapp/components/post_view/components/system_message_helpers.jsx
+++ b/webapp/components/post_view/post_message_view/system_message_helpers.jsx
diff --git a/webapp/components/post_view/components/post_time.jsx b/webapp/components/post_view/post_time.jsx
index 9f6ef51cc..133b6b5a3 100644
--- a/webapp/components/post_view/components/post_time.jsx
+++ b/webapp/components/post_view/post_time.jsx
@@ -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
-};
diff --git a/webapp/components/post_view/post_view_cache.jsx b/webapp/components/post_view/post_view_cache.jsx
deleted file mode 100644
index b8ae39e4a..000000000
--- a/webapp/components/post_view/post_view_cache.jsx
+++ /dev/null
@@ -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>
- );
- }
-}
diff --git a/webapp/components/post_view/post_view_controller.jsx b/webapp/components/post_view/post_view_controller.jsx
deleted file mode 100644
index 12112ac10..000000000
--- a/webapp/components/post_view/post_view_controller.jsx
+++ /dev/null
@@ -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
-};
diff --git a/webapp/components/post_view/reaction/index.js b/webapp/components/post_view/reaction/index.js
new file mode 100644
index 000000000..9bb2524a1
--- /dev/null
+++ b/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);
diff --git a/webapp/components/post_view/components/reaction.jsx b/webapp/components/post_view/reaction/reaction.jsx
index d79e9e092..5b65e604f 100644
--- a/webapp/components/post_view/components/reaction.jsx
+++ b/webapp/components/post_view/reaction/reaction.jsx
@@ -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>
diff --git a/webapp/components/post_view/reaction_list/index.js b/webapp/components/post_view/reaction_list/index.js
new file mode 100644
index 000000000..4fc9355d9
--- /dev/null
+++ b/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);
diff --git a/webapp/components/post_view/components/reaction_list_view.jsx b/webapp/components/post_view/reaction_list/reaction_list.jsx
index 4379453a3..516f5332f 100644
--- a/webapp/components/post_view/components/reaction_list_view.jsx
+++ b/webapp/components/post_view/reaction_list/reaction_list.jsx
@@ -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}
/>
);
diff --git a/webapp/components/post_view/components/scroll_to_bottom_arrows.jsx b/webapp/components/post_view/scroll_to_bottom_arrows.jsx
index 73f8e6527..73f8e6527 100644
--- a/webapp/components/post_view/components/scroll_to_bottom_arrows.jsx
+++ b/webapp/components/post_view/scroll_to_bottom_arrows.jsx
diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx
index 73ae70598..11d64f871 100644
--- a/webapp/components/rhs_comment.jsx
+++ b/webapp/components/rhs_comment.jsx
@@ -2,41 +2,35 @@
// See License.txt for license information.
import UserProfile from './user_profile.jsx';
-import FileAttachmentListContainer from './file_attachment_list_container.jsx';
-import PendingPostOptions from 'components/post_view/components/pending_post_options.jsx';
-import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
+import FileAttachmentListContainer from 'components/file_attachment_list';
+import PostMessageContainer from 'components/post_view/post_message_view';
import ProfilePicture from 'components/profile_picture.jsx';
-import ReactionListContainer from 'components/post_view/components/reaction_list_container.jsx';
-import PostFlagIcon from 'components/common/post_flag_icon.jsx';
-import DotMenu from 'components/dot_menu/dot_menu.jsx';
+import ReactionListContainer from 'components/post_view/reaction_list';
+import PostFlagIcon from 'components/post_view/post_flag_icon.jsx';
+import FailedPostOptions from 'components/post_view/failed_post_options';
+import DotMenu from 'components/dot_menu';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
-import {flagPost, unflagPost, pinPost, unpinPost, addReaction} from 'actions/post_actions.jsx';
+import {addReaction} from 'actions/post_actions.jsx';
import TeamStore from 'stores/team_store.jsx';
import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
-import Constants from 'utils/constants.jsx';
-import loadingGif from 'images/load.gif';
+import Constants from 'utils/constants.jsx';
import React from 'react';
import PropTypes from 'prop-types';
-import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router/es6';
+import {FormattedMessage} from 'react-intl';
export default class RhsComment extends React.Component {
constructor(props) {
super(props);
- this.handlePermalink = this.handlePermalink.bind(this);
this.removePost = this.removePost.bind(this);
- this.flagPost = this.flagPost.bind(this);
- this.unflagPost = this.unflagPost.bind(this);
- this.pinPost = this.pinPost.bind(this);
- this.unpinPost = this.unpinPost.bind(this);
this.reactEmojiClick = this.reactEmojiClick.bind(this);
this.handleDropdownOpened = this.handleDropdownOpened.bind(this);
@@ -61,11 +55,6 @@ export default class RhsComment extends React.Component {
});
}
- handlePermalink(e) {
- e.preventDefault();
- GlobalActions.showGetPostLinkModal(this.props.post);
- }
-
removePost() {
GlobalActions.emitRemovePost(this.props.post);
}
@@ -127,26 +116,6 @@ export default class RhsComment extends React.Component {
return false;
}
- flagPost(e) {
- e.preventDefault();
- flagPost(this.props.post.id);
- }
-
- unflagPost(e) {
- e.preventDefault();
- unflagPost(this.props.post.id);
- }
-
- pinPost(e) {
- e.preventDefault();
- pinPost(this.props.post.channel_id, this.props.post.id);
- }
-
- unpinPost(e) {
- e.preventDefault();
- unpinPost(this.props.post.channel_id, this.props.post.id);
- }
-
timeTag(post, timeOptions) {
return (
<time
@@ -229,7 +198,6 @@ export default class RhsComment 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);
var timestamp = this.props.currentUser.last_picture_update;
@@ -283,20 +251,12 @@ export default class RhsComment extends React.Component {
);
}
- let loading;
+ let failedPostOptions;
let postClass = '';
- if (post.state === Constants.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}
- />
- );
+ if (post.failed) {
+ postClass += ' post-failed';
+ failedPostOptions = <FailedPostOptions post={this.props.post}/>;
}
if (PostUtils.isEdited(this.props.post)) {
@@ -365,7 +325,8 @@ export default class RhsComment extends React.Component {
}
let react;
- if (!isEphemeral && !isPending && !isSystemMessage && Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMOJI_PICKER_PREVIEW)) {
+
+ if (!isEphemeral && !post.failed && !isSystemMessage && Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMOJI_PICKER_PREVIEW)) {
react = (
<span>
<EmojiPickerOverlay
@@ -462,7 +423,7 @@ export default class RhsComment extends React.Component {
</div>
<div className='post__body' >
<div className={postClass}>
- {loading}
+ {failedPostOptions}
<PostMessageContainer post={post}/>
</div>
{fileAttachment}
diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx
index c617477af..8f464056b 100644
--- a/webapp/components/rhs_root_post.jsx
+++ b/webapp/components/rhs_root_post.jsx
@@ -2,50 +2,38 @@
// See License.txt for license information.
import UserProfile from './user_profile.jsx';
-import PostBodyAdditionalContent from 'components/post_view/components/post_body_additional_content.jsx';
-import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
-import FileAttachmentListContainer from './file_attachment_list_container.jsx';
+import PostBodyAdditionalContent from 'components/post_view/post_body_additional_content.jsx';
+import PostMessageContainer from 'components/post_view/post_message_view';
+import FileAttachmentListContainer from 'components/file_attachment_list';
import ProfilePicture from 'components/profile_picture.jsx';
-import ReactionListContainer from 'components/post_view/components/reaction_list_container.jsx';
-import PostFlagIcon from 'components/common/post_flag_icon.jsx';
-import DotMenu from 'components/dot_menu/dot_menu.jsx';
+import ReactionListContainer from 'components/post_view/reaction_list';
+import PostFlagIcon from 'components/post_view/post_flag_icon.jsx';
+import DotMenu from 'components/dot_menu';
+import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
-import * as GlobalActions from 'actions/global_actions.jsx';
-import {flagPost, unflagPost, pinPost, unpinPost, addReaction} from 'actions/post_actions.jsx';
+import {addReaction} from 'actions/post_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
-import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
-
import Constants from 'utils/constants.jsx';
-import DelayedAction from 'utils/delayed_action.jsx';
-
-import {FormattedMessage} from 'react-intl';
-
-import PropTypes from 'prop-types';
import React from 'react';
+import PropTypes from 'prop-types';
import {Link} from 'react-router/es6';
+import {FormattedMessage} from 'react-intl';
export default class RhsRootPost extends React.Component {
constructor(props) {
super(props);
- this.handlePermalink = this.handlePermalink.bind(this);
- this.flagPost = this.flagPost.bind(this);
- this.unflagPost = this.unflagPost.bind(this);
- this.pinPost = this.pinPost.bind(this);
- this.unpinPost = this.unpinPost.bind(this);
this.reactEmojiClick = this.reactEmojiClick.bind(this);
this.handleDropdownOpened = this.handleDropdownOpened.bind(this);
- this.editDisableAction = new DelayedAction(this.handleEditDisable);
-
this.state = {
currentTeamDisplayName: TeamStore.getCurrent().name,
width: '',
@@ -68,11 +56,6 @@ export default class RhsRootPost extends React.Component {
});
}
- handlePermalink(e) {
- e.preventDefault();
- GlobalActions.showGetPostLinkModal(this.props.post);
- }
-
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.status !== this.props.status) {
return true;
@@ -121,16 +104,6 @@ export default class RhsRootPost extends React.Component {
return false;
}
- flagPost(e) {
- e.preventDefault();
- flagPost(this.props.post.id);
- }
-
- unflagPost(e) {
- e.preventDefault();
- unflagPost(this.props.post.id);
- }
-
timeTag(post, timeOptions) {
return (
<time
@@ -156,16 +129,6 @@ export default class RhsRootPost extends React.Component {
);
}
- pinPost(e) {
- e.preventDefault();
- pinPost(this.props.post.channel_id, this.props.post.id);
- }
-
- unpinPost(e) {
- e.preventDefault();
- unpinPost(this.props.post.channel_id, this.props.post.id);
- }
-
toggleEmojiPicker = () => {
const showEmojiPicker = !this.state.showEmojiPicker;
@@ -220,7 +183,6 @@ export default class RhsRootPost extends React.Component {
var channel = ChannelStore.get(post.channel_id);
const isEphemeral = Utils.isPostEphemeral(post);
- const isPending = post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING;
const isSystemMessage = PostUtils.isSystemMessage(post);
var channelName;
@@ -238,7 +200,8 @@ export default class RhsRootPost extends React.Component {
}
let react;
- if (!isEphemeral && !isPending && !isSystemMessage && Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMOJI_PICKER_PREVIEW)) {
+
+ if (!isEphemeral && !post.failed && !isSystemMessage && Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMOJI_PICKER_PREVIEW)) {
react = (
<span>
<EmojiPickerOverlay
@@ -454,10 +417,8 @@ RhsRootPost.defaultProps = {
};
RhsRootPost.propTypes = {
post: PropTypes.object.isRequired,
- lastPostCount: PropTypes.number,
user: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
- commentCount: PropTypes.number,
compactDisplay: PropTypes.bool,
useMilitaryTime: PropTypes.bool.isRequired,
isFlagged: PropTypes.bool,
diff --git a/webapp/components/rhs_thread/index.js b/webapp/components/rhs_thread/index.js
new file mode 100644
index 000000000..c4465cafd
--- /dev/null
+++ b/webapp/components/rhs_thread/index.js
@@ -0,0 +1,27 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+import {getPost, makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts';
+
+import RhsThread from './rhs_thread.jsx';
+
+function makeMapStateToProps() {
+ const getPostsForThread = makeGetPostsForThread();
+
+ return function mapStateToProps(state, ownProps) {
+ const selected = getPost(state, state.views.rhs.selectedPostId);
+ let posts = [];
+ if (selected) {
+ posts = getPostsForThread(state, {rootId: selected.id, channelId: selected.channel_id});
+ }
+
+ return {
+ ...ownProps,
+ selected,
+ posts
+ };
+ };
+}
+
+export default connect(makeMapStateToProps)(RhsThread);
diff --git a/webapp/components/rhs_thread.jsx b/webapp/components/rhs_thread/rhs_thread.jsx
index a532119b8..f4e7b33fa 100644
--- a/webapp/components/rhs_thread.jsx
+++ b/webapp/components/rhs_thread/rhs_thread.jsx
@@ -1,14 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import CreateComment from './create_comment.jsx';
-import RhsHeaderPost from './rhs_header_post.jsx';
-import RootPost from './rhs_root_post.jsx';
-import Comment from './rhs_comment.jsx';
-import FloatingTimestamp from './post_view/components/floating_timestamp.jsx';
-import DateSeparator from './post_view/components/date_separator.jsx';
-
-import PostStore from 'stores/post_store.jsx';
+import CreateComment from 'components/create_comment.jsx';
+import RhsHeaderPost from 'components/rhs_header_post.jsx';
+import RootPost from 'components/rhs_root_post.jsx';
+import Comment from 'components/rhs_comment.jsx';
+import FloatingTimestamp from 'components/post_view/floating_timestamp.jsx';
+import DateSeparator from 'components/post_view/date_separator.jsx';
+
import UserStore from 'stores/user_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import WebrtcStore from 'stores/webrtc_store.jsx';
@@ -49,14 +48,31 @@ export function renderThumbVertical(props) {
}
export default class RhsThread extends React.Component {
+ static propTypes = {
+ posts: PropTypes.arrayOf(PropTypes.object).isRequired,
+ selected: PropTypes.object.isRequired,
+ fromSearch: PropTypes.string,
+ fromFlaggedPosts: PropTypes.bool,
+ fromPinnedPosts: PropTypes.bool,
+ isWebrtc: PropTypes.bool,
+ isMentionSearch: PropTypes.bool,
+ currentUser: PropTypes.object.isRequired,
+ useMilitaryTime: PropTypes.bool.isRequired,
+ toggleSize: PropTypes.func,
+ shrink: PropTypes.func
+ }
+
+ static defaultProps = {
+ fromSearch: '',
+ isMentionSearch: false
+ }
+
constructor(props) {
super(props);
this.mounted = false;
- this.onPostChange = this.onPostChange.bind(this);
this.onUserChange = this.onUserChange.bind(this);
- this.onSelectedChange = this.onSelectedChange.bind(this);
this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.onStatusChange = this.onStatusChange.bind(this);
@@ -67,7 +83,7 @@ export default class RhsThread extends React.Component {
this.scrollStopAction = new DelayedAction(this.handleScrollStop);
const openTime = (new Date()).getTime();
- const state = this.getPosts(openTime);
+ const state = {};
state.windowWidth = Utils.windowWidth();
state.windowHeight = Utils.windowHeight();
state.profiles = JSON.parse(JSON.stringify(UserStore.getProfiles()));
@@ -86,9 +102,6 @@ export default class RhsThread extends React.Component {
}
componentDidMount() {
- PostStore.addSelectedPostChangeListener(this.onSelectedChange);
- PostStore.addSelectedPostChangeListener(this.onPostChange);
- PostStore.addChangeListener(this.onPostChange);
PreferenceStore.addChangeListener(this.onPreferenceChange);
UserStore.addChangeListener(this.onUserChange);
UserStore.addStatusesChangeListener(this.onStatusChange);
@@ -101,9 +114,6 @@ export default class RhsThread extends React.Component {
}
componentWillUnmount() {
- PostStore.addSelectedPostChangeListener(this.onSelectedChange);
- PostStore.removeSelectedPostChangeListener(this.onPostChange);
- PostStore.removeChangeListener(this.onPostChange);
PreferenceStore.removeChangeListener(this.onPreferenceChange);
UserStore.removeChangeListener(this.onUserChange);
UserStore.removeStatusesChangeListener(this.onStatusChange);
@@ -116,7 +126,7 @@ export default class RhsThread extends React.Component {
componentDidUpdate(prevProps, prevState) {
const prevPostsArray = prevState.postsArray || [];
- const curPostsArray = this.state.postsArray || [];
+ const curPostsArray = this.props.posts || [];
if (prevPostsArray.length >= curPostsArray.length) {
return;
@@ -134,11 +144,11 @@ export default class RhsThread extends React.Component {
return true;
}
- if (!Utils.areObjectsEqual(nextState.postsArray, this.state.postsArray)) {
+ if (!Utils.areObjectsEqual(nextState.postsArray, this.props.posts)) {
return true;
}
- if (!Utils.areObjectsEqual(nextState.selected, this.state.selected)) {
+ if (!Utils.areObjectsEqual(nextState.selected, this.props.selected)) {
return true;
}
@@ -198,10 +208,16 @@ export default class RhsThread extends React.Component {
});
}
- onSelectedChange() {
- this.setState({
- openTime: (new Date()).getTime()
- });
+ componentWillReceiveProps(nextProps) {
+ if (!this.props.selected || !nextProps.selected) {
+ return;
+ }
+
+ if (this.props.selected.id !== nextProps.selected.id) {
+ this.setState({
+ openTime: (new Date()).getTime()
+ });
+ }
}
onPreferenceChange(category) {
@@ -218,12 +234,6 @@ export default class RhsThread extends React.Component {
this.forceUpdateInfo();
}
- onPostChange() {
- if (this.mounted) {
- this.setState(this.getPosts(this.state.openTime));
- }
- }
-
onStatusChange() {
this.setState({statuses: Object.assign({}, UserStore.getStatuses())});
}
@@ -232,53 +242,21 @@ export default class RhsThread extends React.Component {
this.setState({isBusy});
}
- getPosts(openTime) {
- const selected = PostStore.getSelectedPost();
- const posts = PostStore.getSelectedPostThread();
-
+ filterPosts(posts, selected, openTime) {
const postsArray = [];
- for (const id in posts) {
- if (posts.hasOwnProperty(id)) {
- const cpost = posts[id];
-
- // Do not show empherals created before sidebar has been opened
- if (cpost.type === 'system_ephemeral' && cpost.create_at < openTime) {
- continue;
- }
-
- if (cpost.root_id === selected.id) {
- postsArray.push(cpost);
- }
+ posts.forEach((cpost) => {
+ // Do not show empherals created before sidebar has been opened
+ if (cpost.type === 'system_ephemeral' && cpost.create_at < openTime) {
+ return;
}
- }
- // sort failed posts to bottom, followed by pending, and then regular posts
- postsArray.sort((a, b) => {
- if ((a.state === Constants.POST_LOADING || a.state === Constants.POST_FAILED) && (b.state !== Constants.POST_LOADING && b.state !== Constants.POST_FAILED)) {
- return 1;
- }
- if ((a.state !== Constants.POST_LOADING && a.state !== Constants.POST_FAILED) && (b.state === Constants.POST_LOADING || b.state === Constants.POST_FAILED)) {
- return -1;
+ if (cpost.root_id === selected.id) {
+ postsArray.unshift(cpost);
}
-
- if (a.state === Constants.POST_LOADING && b.state === Constants.POST_FAILED) {
- return -1;
- }
- if (a.state === Constants.POST_FAILED && b.state === Constants.POST_LOADING) {
- return 1;
- }
-
- if (a.create_at < b.create_at) {
- return -1;
- }
- if (a.create_at > b.create_at) {
- return 1;
- }
- return 0;
});
- return {postsArray, selected};
+ return postsArray;
}
onUserChange() {
@@ -298,7 +276,7 @@ export default class RhsThread extends React.Component {
return;
}
- if (this.state.postsArray) {
+ if (this.props.posts) {
const childNodes = this.refs.rhspostlist.childNodes;
const viewPort = this.refs.rhspostlist.getBoundingClientRect();
let topRhsPostCreateAt = 0;
@@ -307,7 +285,7 @@ export default class RhsThread extends React.Component {
// determine the top rhs comment assuming that childNodes and postsArray are of same length
for (let i = 0; i < childNodes.length; i++) {
if ((childNodes[i].offsetTop + viewPort.top) - offset > 0) {
- topRhsPostCreateAt = this.state.postsArray[i].create_at;
+ topRhsPostCreateAt = this.props.posts[i].create_at;
break;
}
}
@@ -343,14 +321,14 @@ export default class RhsThread extends React.Component {
}
render() {
- if (this.state.postsArray == null || this.state.selected == null) {
+ if (this.props.posts == null || this.props.selected == null) {
return (
<div/>
);
}
- const postsArray = this.state.postsArray;
- const selected = this.state.selected;
+ const postsArray = this.filterPosts(this.props.posts, this.props.selected, this.state.openTime);
+ const selected = this.props.selected;
const profiles = this.state.profiles || {};
let profile;
@@ -490,20 +468,3 @@ export default class RhsThread extends React.Component {
);
}
}
-
-RhsThread.defaultProps = {
- fromSearch: '',
- isMentionSearch: false
-};
-
-RhsThread.propTypes = {
- fromSearch: PropTypes.string,
- fromFlaggedPosts: PropTypes.bool,
- fromPinnedPosts: PropTypes.bool,
- isWebrtc: PropTypes.bool,
- isMentionSearch: PropTypes.bool,
- currentUser: PropTypes.object.isRequired,
- useMilitaryTime: PropTypes.bool.isRequired,
- toggleSize: PropTypes.func,
- shrink: PropTypes.func
-};
diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx
index 4eb738065..eae384f0d 100644
--- a/webapp/components/search_results_item.jsx
+++ b/webapp/components/search_results_item.jsx
@@ -2,9 +2,9 @@
// See License.txt for license information.
import $ from 'jquery';
-import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
+import PostMessageContainer from 'components/post_view/post_message_view';
import UserProfile from './user_profile.jsx';
-import FileAttachmentListContainer from './file_attachment_list_container.jsx';
+import FileAttachmentListContainer from 'components/file_attachment_list';
import ProfilePicture from './profile_picture.jsx';
import CommentIcon from 'components/common/comment_icon.jsx';
@@ -14,17 +14,17 @@ import UserStore from 'stores/user_store.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {flagPost, unflagPost} from 'actions/post_actions.jsx';
-import PostFlagIcon from 'components/common/post_flag_icon.jsx';
+import PostFlagIcon from 'components/post_view/post_flag_icon.jsx';
import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
-
-import PropTypes from 'prop-types';
+import {Posts} from 'mattermost-redux/constants';
import React from 'react';
+import PropTypes from 'prop-types';
import {FormattedMessage, FormattedDate} from 'react-intl';
import {browserHistory, Link} from 'react-router/es6';
@@ -187,7 +187,7 @@ export default class SearchResultsItem extends React.Component {
let message;
let flagContent;
let rhsControls;
- if (post.state === Constants.POST_DELETED) {
+ if (post.state === Posts.POST_DELETED) {
message = (
<p>
<FormattedMessage
diff --git a/webapp/components/sidebar_right/index.js b/webapp/components/sidebar_right/index.js
new file mode 100644
index 000000000..126ffc776
--- /dev/null
+++ b/webapp/components/sidebar_right/index.js
@@ -0,0 +1,17 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+import SidebarRight from './sidebar_right.jsx';
+
+function mapStateToProps(state, ownProps) {
+ return {
+ ...ownProps,
+ postRightVisible: Boolean(state.views.rhs.selectedPostId),
+ fromSearch: state.views.rhs.fromSearch,
+ fromFlaggedPosts: state.views.rhs.fromFlaggedPosts,
+ fromPinnedPosts: state.views.rhs.fromPinnedPosts
+ };
+}
+
+export default connect(mapStateToProps)(SidebarRight);
diff --git a/webapp/components/sidebar_right.jsx b/webapp/components/sidebar_right/sidebar_right.jsx
index 3ceab3a18..00a7d2d25 100644
--- a/webapp/components/sidebar_right.jsx
+++ b/webapp/components/sidebar_right/sidebar_right.jsx
@@ -3,10 +3,10 @@
import $ from 'jquery';
-import SearchResults from './search_results.jsx';
-import RhsThread from './rhs_thread.jsx';
-import SearchBox from './search_bar.jsx';
-import FileUploadOverlay from './file_upload_overlay.jsx';
+import SearchResults from 'components/search_results.jsx';
+import RhsThread from 'components/rhs_thread';
+import SearchBox from 'components/search_bar.jsx';
+import FileUploadOverlay from 'components/file_upload_overlay.jsx';
import SearchStore from 'stores/search_store.jsx';
import PostStore from 'stores/post_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -19,18 +19,24 @@ import {trackEvent} from 'actions/diagnostics_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
-import PropTypes from 'prop-types';
-
import React from 'react';
+import PropTypes from 'prop-types';
export default class SidebarRight extends React.Component {
+ static propTypes = {
+ channel: PropTypes.object,
+ postRightVisible: PropTypes.bool,
+ fromSearch: PropTypes.string,
+ fromFlaggedPosts: PropTypes.bool,
+ fromPinnedPosts: PropTypes.bool
+ }
+
constructor(props) {
super(props);
this.plScrolledToBottom = true;
this.onPreferenceChange = this.onPreferenceChange.bind(this);
- this.onSelectedChange = this.onSelectedChange.bind(this);
this.onPostPinnedChange = this.onPostPinnedChange.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
this.onUserChange = this.onUserChange.bind(this);
@@ -45,7 +51,6 @@ export default class SidebarRight extends React.Component {
isMentionSearch: SearchStore.getIsMentionSearch(),
isFlaggedPosts: SearchStore.getIsFlaggedPosts(),
isPinnedPosts: SearchStore.getIsPinnedPosts(),
- postRightVisible: Boolean(PostStore.getSelectedPost()),
expanded: false,
fromSearch: false,
currentUser: UserStore.getCurrentUser(),
@@ -55,7 +60,6 @@ export default class SidebarRight extends React.Component {
componentDidMount() {
SearchStore.addSearchChangeListener(this.onSearchChange);
- PostStore.addSelectedPostChangeListener(this.onSelectedChange);
PostStore.addPostPinnedChangeListener(this.onPostPinnedChange);
SearchStore.addShowSearchListener(this.onShowSearch);
UserStore.addChangeListener(this.onUserChange);
@@ -65,7 +69,6 @@ export default class SidebarRight extends React.Component {
componentWillUnmount() {
SearchStore.removeSearchChangeListener(this.onSearchChange);
- PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
PostStore.removePostPinnedChangeListener(this.onPostPinnedChange);
SearchStore.removeShowSearchListener(this.onShowSearch);
UserStore.removeChangeListener(this.onUserChange);
@@ -73,21 +76,17 @@ export default class SidebarRight extends React.Component {
}
shouldComponentUpdate(nextProps, nextState) {
- return !Utils.areObjectsEqual(nextState, this.state);
+ return !Utils.areObjectsEqual(nextState, this.state) || !Utils.areObjectsEqual(nextProps, this.props);
}
componentWillUpdate(nextProps, nextState) {
- const isOpen = this.state.searchVisible || this.state.postRightVisible;
+ const isOpen = this.state.searchVisible || this.props.postRightVisible;
const willOpen = nextState.searchVisible || nextState.postRightVisible;
if (!isOpen && willOpen) {
trackEvent('ui', 'ui_rhs_opened');
}
- if (isOpen !== willOpen) {
- PostStore.jumpPostsViewSidebarOpen();
- }
-
if (!isOpen && willOpen) {
this.setState({
expanded: false
@@ -105,7 +104,7 @@ export default class SidebarRight extends React.Component {
$('.app__body .sidebar--right').addClass('move--left');
//$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
- if (!this.state.searchVisible && !this.state.postRightVisible) {
+ if (!this.state.searchVisible && !this.props.postRightVisible) {
$('.app__body .inner-wrap').removeClass('move--left').removeClass('move--right');
$('.app__body .sidebar--right').removeClass('move--left');
return (
@@ -122,7 +121,7 @@ export default class SidebarRight extends React.Component {
}
componentDidUpdate() {
- const isOpen = this.state.searchVisible || this.state.postRightVisible;
+ const isOpen = this.state.searchVisible || this.props.postRightVisible;
WebrtcStore.emitRhsChanged(isOpen);
this.doStrangeThings();
}
@@ -137,15 +136,6 @@ export default class SidebarRight extends React.Component {
});
}
- onSelectedChange(fromSearch, fromFlaggedPosts, fromPinnedPosts) {
- this.setState({
- postRightVisible: Boolean(PostStore.getSelectedPost()),
- fromSearch,
- fromFlaggedPosts,
- fromPinnedPosts
- });
- }
-
onPostPinnedChange() {
if (this.props.channel && this.state.isPinnedPosts) {
getPinnedPosts(this.props.channel.id);
@@ -225,15 +215,15 @@ export default class SidebarRight extends React.Component {
/>
</div>
);
- } else if (this.state.postRightVisible) {
+ } else if (this.props.postRightVisible) {
content = (
<div className='post-right__container'>
<FileUploadOverlay overlayType='right'/>
<div className='search-bar__container sidebar--right__search-header'>{searchForm}</div>
<RhsThread
- fromFlaggedPosts={this.state.fromFlaggedPosts}
- fromSearch={this.state.fromSearch}
- fromPinnedPosts={this.state.fromPinnedPosts}
+ fromFlaggedPosts={this.props.fromFlaggedPosts}
+ fromSearch={this.props.fromSearch}
+ fromPinnedPosts={this.props.fromPinnedPosts}
isWebrtc={WebrtcStore.isBusy()}
isMentionSearch={this.state.isMentionSearch}
currentUser={this.state.currentUser}
@@ -265,7 +255,3 @@ export default class SidebarRight extends React.Component {
);
}
}
-
-SidebarRight.propTypes = {
- channel: PropTypes.object
-};
diff --git a/webapp/components/view_image.jsx b/webapp/components/view_image.jsx
index b2cd41810..04ace6c55 100644
--- a/webapp/components/view_image.jsx
+++ b/webapp/components/view_image.jsx
@@ -9,9 +9,8 @@ import ViewImagePopoverBar from './view_image_popover_bar.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
-import FileStore from 'stores/file_store.jsx';
-
import * as Utils from 'utils/utils.jsx';
+import {getFileUrl, getFilePreviewUrl} from 'mattermost-redux/utils/file_utils';
import Constants from 'utils/constants.jsx';
const KeyCodes = Constants.KeyCodes;
@@ -127,10 +126,10 @@ export default class ViewImageModal extends React.Component {
if (fileType === 'image') {
let previewUrl;
if (fileInfo.has_image_preview) {
- previewUrl = FileStore.getFilePreviewUrl(fileInfo.id);
+ previewUrl = getFilePreviewUrl(fileInfo.id);
} else {
// some images (eg animated gifs) just show the file itself and not a preview
- previewUrl = FileStore.getFileUrl(fileInfo.id);
+ previewUrl = getFileUrl(fileInfo.id);
}
const img = new Image();
@@ -175,7 +174,7 @@ export default class ViewImageModal extends React.Component {
}
const fileInfo = this.props.fileInfos[this.state.imgId];
- const fileUrl = FileStore.getFileUrl(fileInfo.id);
+ const fileUrl = getFileUrl(fileInfo.id);
let content;
if (this.state.loaded[this.state.imgId]) {
@@ -346,7 +345,7 @@ LoadingImagePreview.propTypes = {
function ImagePreview({fileInfo, fileUrl}) {
let previewUrl;
if (fileInfo.has_preview_image) {
- previewUrl = FileStore.getFilePreviewUrl(fileInfo.id);
+ previewUrl = getFilePreviewUrl(fileInfo.id);
} else {
previewUrl = fileUrl;
}
diff --git a/webapp/components/youtube_video/index.js b/webapp/components/youtube_video/index.js
new file mode 100644
index 000000000..592e52240
--- /dev/null
+++ b/webapp/components/youtube_video/index.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
+
+import YoutubeVideo from './youtube_video.jsx';
+
+function mapStateToProps(state, ownProps) {
+ return {
+ ...ownProps,
+ currentChannelId: getCurrentChannelId(state)
+ };
+}
+
+export default connect(mapStateToProps)(YoutubeVideo);
diff --git a/webapp/components/youtube_video.jsx b/webapp/components/youtube_video/youtube_video.jsx
index 49f490bda..5151e6576 100644
--- a/webapp/components/youtube_video.jsx
+++ b/webapp/components/youtube_video/youtube_video.jsx
@@ -1,17 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import ChannelStore from 'stores/channel_store.jsx';
import WebClient from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
const ytRegex = /(?:http|https):\/\/(?:www\.|m\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#&?]*)/;
+import React from 'react';
import PropTypes from 'prop-types';
-import React from 'react';
+export default class YoutubeVideo extends React.PureComponent {
+ static propTypes = {
+ channelId: PropTypes.string.isRequired,
+ currentChannelId: PropTypes.string.isRequired,
+ link: PropTypes.string.isRequired,
+ show: PropTypes.bool.isRequired
+ }
-export default class YoutubeVideo extends React.Component {
constructor(props) {
super(props);
@@ -22,7 +27,6 @@ export default class YoutubeVideo extends React.Component {
this.play = this.play.bind(this);
this.stop = this.stop.bind(this);
- this.stopOnChannelChange = this.stopOnChannelChange.bind(this);
this.state = {
loaded: false,
@@ -52,6 +56,10 @@ export default class YoutubeVideo extends React.Component {
this.stop();
}
+ if (props.channelId !== props.currentChannelId) {
+ this.stop();
+ }
+
this.setState({
videoId: match[1],
time: this.handleYoutubeTime(link)
@@ -138,22 +146,12 @@ export default class YoutubeVideo extends React.Component {
play() {
this.setState({playing: true});
-
- if (ChannelStore.getCurrentId() === this.props.channelId) {
- ChannelStore.addChangeListener(this.stopOnChannelChange);
- }
}
stop() {
this.setState({playing: false});
}
- stopOnChannelChange() {
- if (ChannelStore.getCurrentId() !== this.props.channelId) {
- this.stop();
- }
- }
-
render() {
if (!this.state.loaded) {
return (
@@ -243,9 +241,3 @@ export default class YoutubeVideo extends React.Component {
return link.trim().match(ytRegex);
}
}
-
-YoutubeVideo.propTypes = {
- channelId: PropTypes.string.isRequired,
- link: PropTypes.string.isRequired,
- show: PropTypes.bool.isRequired
-};