diff options
Diffstat (limited to 'webapp/components/post_view')
33 files changed, 0 insertions, 4332 deletions
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 deleted file mode 100644 index 1755d68e4..000000000 --- a/webapp/components/post_view/commented_on_files_message/commented_on_files_message.jsx +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import PropTypes from 'prop-types'; -import React from 'react'; -import {FormattedMessage} from 'react-intl'; - -export default class CommentedOnFilesMessage extends React.PureComponent { - static propTypes = { - - /* - * The id of the post that was commented on - */ - parentPostId: PropTypes.string.isRequired, - - /* - * An array of file metadata for the parent post - */ - fileInfos: PropTypes.arrayOf(PropTypes.object), - - actions: PropTypes.shape({ - - /* - * Function to get file metadata for a post - */ - getFilesForPost: PropTypes.func.isRequired - }).isRequired - } - - componentDidMount() { - if (!this.props.fileInfos || this.props.fileInfos.length === 0) { - this.props.actions.getFilesForPost(this.props.parentPostId); - } - } - - render() { - if (!this.props.fileInfos || this.props.fileInfos.length === 0) { - return null; - } - - let plusMore = null; - if (this.props.fileInfos.length > 1) { - plusMore = ( - <FormattedMessage - id='post_body.plusMore' - defaultMessage=' plus {count, number} other {count, plural, one {file} other {files}}' - values={{ - count: this.props.fileInfos.length - 1 - }} - /> - ); - } - - return ( - <span> - {this.props.fileInfos[0].name} - {plusMore} - </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 deleted file mode 100644 index d55cff16b..000000000 --- a/webapp/components/post_view/commented_on_files_message/index.js +++ /dev/null @@ -1,36 +0,0 @@ -// 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, 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/date_separator.jsx b/webapp/components/post_view/date_separator.jsx deleted file mode 100644 index 3f5184dbf..000000000 --- a/webapp/components/post_view/date_separator.jsx +++ /dev/null @@ -1,32 +0,0 @@ -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/failed_post_options/failed_post_options.jsx b/webapp/components/post_view/failed_post_options/failed_post_options.jsx deleted file mode 100644 index f28de343b..000000000 --- a/webapp/components/post_view/failed_post_options/failed_post_options.jsx +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {createPost} from 'actions/post_actions.jsx'; - -import React from 'react'; -import PropTypes from 'prop-types'; -import {FormattedMessage} from 'react-intl'; - -export default class FailedPostOptions extends React.Component { - static propTypes = { - - /* - * The failed post - */ - post: PropTypes.object.isRequired, - actions: PropTypes.shape({ - - /** - * The function to delete the post - */ - removePost: PropTypes.func.isRequired - }).isRequired - } - - constructor(props) { - super(props); - - this.retryPost = this.retryPost.bind(this); - this.cancelPost = this.cancelPost.bind(this); - - this.submitting = false; - - this.state = {}; - } - - retryPost(e) { - e.preventDefault(); - - if (this.submitting) { - return; - } - - this.submitting = true; - - 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(); - } else { - this.forceUpdate(); - } - - this.submitting = false; - } - ); - } - - cancelPost(e) { - e.preventDefault(); - this.props.actions.removePost(this.props.post); - } - - render() { - return (<span className='pending-post-actions'> - <a - className='post-retry' - href='#' - onClick={this.retryPost} - > - <FormattedMessage - id='pending_post_actions.retry' - defaultMessage='Retry' - /> - </a> - {' - '} - <a - className='post-cancel' - href='#' - onClick={this.cancelPost} - > - <FormattedMessage - id='pending_post_actions.cancel' - defaultMessage='Cancel' - /> - </a> - </span>); - } -} diff --git a/webapp/components/post_view/failed_post_options/index.js b/webapp/components/post_view/failed_post_options/index.js deleted file mode 100644 index bb8dde893..000000000 --- a/webapp/components/post_view/failed_post_options/index.js +++ /dev/null @@ -1,24 +0,0 @@ -// 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/floating_timestamp.jsx b/webapp/components/post_view/floating_timestamp.jsx deleted file mode 100644 index f0f6af60e..000000000 --- a/webapp/components/post_view/floating_timestamp.jsx +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {FormattedDate} from 'react-intl'; - -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class FloatingTimestamp extends React.PureComponent { - static propTypes = { - isScrolling: PropTypes.bool.isRequired, - isMobile: PropTypes.bool, - createAt: PropTypes.number, - isRhsPost: PropTypes.bool - } - - render() { - if (!this.props.isMobile) { - return <noscript/>; - } - - if (this.props.createAt === 0) { - return <noscript/>; - } - - const dateString = ( - <FormattedDate - value={this.props.createAt} - weekday='short' - day='2-digit' - month='short' - year='numeric' - /> - ); - - let className = 'post-list__timestamp'; - if (this.props.isScrolling) { - className += ' scrolling'; - } - - if (this.props.isRhsPost) { - className += ' rhs'; - } - - return ( - <div className={className}> - <div> - <span>{dateString}</span> - </div> - </div> - ); - } -} diff --git a/webapp/components/post_view/index.js b/webapp/components/post_view/index.js deleted file mode 100644 index ad0270cdd..000000000 --- a/webapp/components/post_view/index.js +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; - -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'; - -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({ - getPosts, - getPostsBefore, - getPostsAfter, - getPostThread, - increasePostVisibility - }, dispatch) - }; -} - -export default connect(makeMapStateToProps, mapDispatchToProps)(PostList); diff --git a/webapp/components/post_view/new_message_indicator.jsx b/webapp/components/post_view/new_message_indicator.jsx deleted file mode 100644 index 537520c29..000000000 --- a/webapp/components/post_view/new_message_indicator.jsx +++ /dev/null @@ -1,72 +0,0 @@ -// 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 Constants from 'utils/constants.jsx'; -import {FormattedMessage} from 'react-intl'; - -export default class NewMessageIndicator extends React.PureComponent { - static propTypes = { - onClick: PropTypes.func.isRequired, - newMessages: PropTypes.number - } - - constructor(props) { - super(props); - this.state = { - visible: false, - rendered: false - }; - } - - componentWillReceiveProps(nextProps) { - if (nextProps.newMessages > 0) { - this.setState({rendered: true}, () => { - this.setState({visible: true}); - }); - } else { - this.setState({visible: false}); - } - } - - render() { - const unreadIcon = Constants.UNREAD_ICON_SVG; - let className = 'new-messages__button'; - if (this.state.visible > 0) { - className += ' visible'; - } - if (!this.state.rendered) { - className += ' disabled'; - } - return ( - <div - className={className} - onTransitionEnd={this.setRendered.bind(this)} - ref='indicator' - > - <div onClick={this.props.onClick}> - <FormattedMessage - id='posts_view.newMsgBelow' - defaultMessage='New {count, plural, one {message} other {messages}}' - values={{count: this.props.newMessages}} - /> - <span - className='icon icon__unread' - dangerouslySetInnerHTML={{__html: unreadIcon}} - /> - </div> - </div> - ); - } - - // Sync 'rendered' state with visibility param, only after transitions - // have ended - setRendered() { - this.setState({rendered: this.state.visible}); - } -} - -NewMessageIndicator.defaultProps = { - newMessages: 0 -}; diff --git a/webapp/components/post_view/post/index.js b/webapp/components/post_view/post/index.js deleted file mode 100644 index 2e7125f34..000000000 --- a/webapp/components/post_view/post/index.js +++ /dev/null @@ -1,34 +0,0 @@ -// 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, detailedPost.user_id), - status: getStatusForUserId(state, detailedPost.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/post/post.jsx b/webapp/components/post_view/post/post.jsx deleted file mode 100644 index 25d23c690..000000000 --- a/webapp/components/post_view/post/post.jsx +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import PostHeader from 'components/post_view/post_header'; -import PostBody from 'components/post_view/post_body'; -import ProfilePicture from 'components/profile_picture.jsx'; - -import Constants from 'utils/constants.jsx'; -const ActionTypes = Constants.ActionTypes; -import {Posts} from 'mattermost-redux/constants'; - -import * as Utils from 'utils/utils.jsx'; -import * as PostUtils from 'utils/post_utils.jsx'; -import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; - -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class Post extends React.PureComponent { - static propTypes = { - - /** - * The post to render - */ - post: PropTypes.object.isRequired, - - /** - * The user who created the post - */ - user: PropTypes.object, - - /** - * The status of the poster - */ - status: PropTypes.string, - - /** - * The logged in user - */ - currentUser: PropTypes.object.isRequired, - - /** - * Set to center the post - */ - center: PropTypes.bool, - - /** - * Set to render post compactly - */ - compactDisplay: PropTypes.bool, - - /** - * Set to render a preview of the parent post above this reply - */ - isFirstReply: PropTypes.bool, - - /** - * Set to highlight the background of the post - */ - highlight: PropTypes.bool, - - /** - * Set to render this post as if it was attached to the previous post - */ - consecutivePostByUser: PropTypes.bool, - - /** - * Set if the previous post is a comment - */ - previousPostIsComment: PropTypes.bool, - - /** - * Set to render this comment as a mention - */ - isCommentMention: PropTypes.bool, - - /** - * The number of replies in the same thread as this post - */ - replyCount: PropTypes.number, - - /** - * Set to mark the poster as in a webrtc call - */ - isBusy: PropTypes.bool, - - /** - * 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); - - this.state = { - dropdownOpened: false - }; - } - - handleCommentClick = (e) => { - e.preventDefault(); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POST_SELECTED, - postId: Utils.getRootId(this.props.post) - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_SEARCH, - results: null - }); - } - - handleDropdownOpened = (opened) => { - this.setState({ - dropdownOpened: opened - }); - } - - forceUpdateInfo = () => { - this.refs.info.forceUpdate(); - this.refs.header.forceUpdate(); - } - - getClassName = (post, isSystemMessage, fromWebhook) => { - let className = 'post'; - - if (post.failed || post.state === Posts.POST_DELETED) { - className += ' post--hide-controls'; - } - - if (this.props.highlight) { - className += ' post--highlight'; - } - - 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'; - } - - let currentUserCss = ''; - if (this.props.currentUser.id === post.user_id && !fromWebhook && !isSystemMessage) { - currentUserCss = 'current--user'; - } - - let sameUserClass = ''; - 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.replyCount > 0) { - postType = 'post--root'; - sameUserClass = ''; - rootUser = ''; - } - - if (isSystemMessage) { - className += ' post--system'; - sameUserClass = ''; - currentUserCss = ''; - postType = ''; - rootUser = ''; - } - - if (this.props.compactDisplay) { - className += ' post--compact'; - } - - if (this.state.dropdownOpened) { - className += ' post--hovered'; - } - - if (post.is_pinned) { - className += ' post--pinned'; - } - - return className + ' ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss; - } - - render() { - const post = this.props.post; - const mattermostLogo = Constants.MATTERMOST_ICON_SVG; - - const isSystemMessage = PostUtils.isSystemMessage(post); - const fromWebhook = post.props && post.props.from_webhook === 'true'; - - let status = this.props.status; - if (fromWebhook) { - status = null; - } - - let profilePic = ( - <ProfilePicture - src={PostUtils.getProfilePicSrcForPost(post, this.props.user)} - status={status} - user={this.props.user} - isBusy={this.props.isBusy} - hasMention={true} - /> - ); - - if (fromWebhook) { - profilePic = ( - <ProfilePicture - src={PostUtils.getProfilePicSrcForPost(post, this.props.user)} - /> - ); - } else if (PostUtils.isSystemMessage(post)) { - profilePic = ( - <span - className='icon' - dangerouslySetInnerHTML={{__html: mattermostLogo}} - /> - ); - } - - let centerClass = ''; - if (this.props.center) { - centerClass = 'center'; - } - - if (this.props.compactDisplay) { - if (fromWebhook) { - profilePic = ( - <ProfilePicture - src='' - status={status} - isBusy={this.props.isBusy} - user={this.props.user} - /> - ); - } else { - profilePic = ( - <ProfilePicture - src='' - status={status} - /> - ); - } - } - - const profilePicContainer = (<div className='post__img'>{profilePic}</div>); - - return ( - <div - ref={(div) => { - this.domNode = div; - }} - > - <div - id={'post_' + post.id} - className={this.getClassName(this.props.post, isSystemMessage, fromWebhook)} - > - <div className={'post__content ' + centerClass}> - {profilePicContainer} - <div> - <PostHeader - ref='header' - post={post} - handleCommentClick={this.handleCommentClick} - handleDropdownOpened={this.handleDropdownOpened} - user={this.props.user} - currentUser={this.props.currentUser} - compactDisplay={this.props.compactDisplay} - 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} - handleCommentClick={this.handleCommentClick} - compactDisplay={this.props.compactDisplay} - lastPostCount={this.props.lastPostCount} - isCommentMention={this.props.isCommentMention} - /> - </div> - </div> - </div> - </div> - ); - } -} diff --git a/webapp/components/post_view/post_attachment.jsx b/webapp/components/post_view/post_attachment.jsx deleted file mode 100644 index cc7aa509c..000000000 --- a/webapp/components/post_view/post_attachment.jsx +++ /dev/null @@ -1,359 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as TextFormatting from 'utils/text_formatting.jsx'; -import {localizeMessage} from 'utils/utils.jsx'; - -import * as PostActions from 'actions/post_actions.jsx'; - -import $ from 'jquery'; -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class PostAttachment extends React.PureComponent { - static propTypes = { - - /** - * The post id - */ - postId: PropTypes.string.isRequired, - - /** - * The attachment to render - */ - attachment: PropTypes.object.isRequired - } - - constructor(props) { - super(props); - - this.handleActionButtonClick = this.handleActionButtonClick.bind(this); - this.getActionView = this.getActionView.bind(this); - this.getFieldsTable = this.getFieldsTable.bind(this); - this.getInitState = this.getInitState.bind(this); - this.shouldCollapse = this.shouldCollapse.bind(this); - this.toggleCollapseState = this.toggleCollapseState.bind(this); - } - - componentDidMount() { - $(this.refs.attachment).on('click', '.attachment-link-more', this.toggleCollapseState); - } - - componentWillUnmount() { - $(this.refs.attachment).off('click', '.attachment-link-more', this.toggleCollapseState); - } - - componentWillMount() { - this.setState(this.getInitState()); - } - - getInitState() { - const shouldCollapse = this.shouldCollapse(); - const text = TextFormatting.formatText(this.props.attachment.text || ''); - 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 { - shouldCollapse, - collapsedText, - uncollapsedText, - text: shouldCollapse ? collapsedText : uncollapsedText, - collapsed: shouldCollapse - }; - } - - toggleCollapseState(e) { - e.preventDefault(); - this.setState((prevState) => { - return { - text: prevState.collapsed ? prevState.uncollapsedText : prevState.collapsedText, - collapsed: !prevState.collapsed - }; - }); - } - - shouldCollapse() { - const text = this.props.attachment.text || ''; - return (text.match(/\n/g) || []).length >= 5 || text.length > 700; - } - - getCollapsedText() { - let text = this.props.attachment.text || ''; - if ((text.match(/\n/g) || []).length >= 5) { - text = text.split('\n').splice(0, 5).join('\n'); - } - if (text.length > 300) { - text = text.substr(0, 300); - } - - return TextFormatting.formatText(text) + `<div><a class="attachment-link-more" href="#">${localizeMessage('post_attachment.more', 'Show more...')}</a></div>`; - } - - getActionView() { - const actions = this.props.attachment.actions; - if (!actions || !actions.length) { - return ''; - } - - const buttons = []; - - actions.forEach((action) => { - if (!action.id || !action.name) { - return; - } - buttons.push( - <button - key={action.id} - onClick={() => this.handleActionButtonClick(action.id)} - > - {action.name} - </button> - ); - }); - - return ( - <div - className='attachment-actions' - > - {buttons} - </div> - ); - } - - handleActionButtonClick(actionId) { - PostActions.doPostAction(this.props.postId, actionId); - } - - getFieldsTable() { - const fields = this.props.attachment.fields; - if (!fields || !fields.length) { - return ''; - } - - const fieldTables = []; - - let headerCols = []; - let bodyCols = []; - let rowPos = 0; - let lastWasLong = false; - let nrTables = 0; - - fields.forEach((field, i) => { - if (rowPos === 2 || !(field.short === true) || lastWasLong) { - fieldTables.push( - <table - className='attachment-fields' - key={'attachment__table__' + nrTables} - > - <thead> - <tr> - {headerCols} - </tr> - </thead> - <tbody> - <tr> - {bodyCols} - </tr> - </tbody> - </table> - ); - headerCols = []; - bodyCols = []; - rowPos = 0; - nrTables += 1; - lastWasLong = false; - } - headerCols.push( - <th - className='attachment-field__caption' - key={'attachment__field-caption-' + i + '__' + nrTables} - width='50%' - > - {field.title} - </th> - ); - bodyCols.push( - <td - className='attachment-field' - key={'attachment__field-' + i + '__' + nrTables} - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(field.value || '')}} - /> - ); - rowPos += 1; - lastWasLong = !(field.short === true); - }); - if (headerCols.length > 0) { // Flush last fields - fieldTables.push( - <table - className='attachment-fields' - key={'attachment__table__' + nrTables} - > - <thead> - <tr> - {headerCols} - </tr> - </thead> - <tbody> - <tr> - {bodyCols} - </tr> - </tbody> - </table> - ); - } - return ( - <div> - {fieldTables} - </div> - ); - } - - render() { - const data = this.props.attachment; - - let preText; - if (data.pretext) { - preText = ( - <div - className='attachment__thumb-pretext' - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(data.pretext)}} - /> - ); - } - - let author = []; - if (data.author_name || data.author_icon) { - if (data.author_icon) { - author.push( - <img - className='attachment__author-icon' - src={data.author_icon} - key={'attachment__author-icon'} - height='14' - width='14' - /> - ); - } - if (data.author_name) { - author.push( - <span - className='attachment__author-name' - key={'attachment__author-name'} - > - {data.author_name} - </span> - ); - } - } - if (data.author_link) { - author = ( - <a - href={data.author_link} - target='_blank' - rel='noopener noreferrer' - > - {author} - </a> - ); - } - - let title; - if (data.title) { - if (data.title_link) { - title = ( - <h1 - className='attachment__title' - > - <a - className='attachment__title-link' - href={data.title_link} - target='_blank' - rel='noopener noreferrer' - > - {data.title} - </a> - </h1> - ); - } else { - title = ( - <h1 - className='attachment__title' - > - {data.title} - </h1> - ); - } - } - - let text; - if (data.text) { - text = ( - <div - className='attachment__text' - dangerouslySetInnerHTML={{__html: this.state.text}} - /> - ); - } - - let image; - if (data.image_url) { - image = ( - <img - className='attachment__image' - src={data.image_url} - /> - ); - } - - let thumb; - if (data.thumb_url) { - thumb = ( - <div - className='attachment__thumb-container' - > - <img - src={data.thumb_url} - /> - </div> - ); - } - - const fields = this.getFieldsTable(); - const actions = this.getActionView(); - - let useBorderStyle; - if (data.color && data.color[0] === '#') { - useBorderStyle = {borderLeftColor: data.color}; - } - - return ( - <div - className='attachment' - ref='attachment' - > - {preText} - <div className='attachment__content'> - <div - className={useBorderStyle ? 'clearfix attachment__container' : 'clearfix attachment__container attachment__container--' + data.color} - style={useBorderStyle} - > - {author} - {title} - <div> - <div - className={thumb ? 'attachment__body' : 'attachment__body attachment__body--no_thumb'} - > - {text} - {image} - {fields} - {actions} - </div> - {thumb} - <div style={{clear: 'both'}}/> - </div> - </div> - </div> - </div> - ); - } -} diff --git a/webapp/components/post_view/post_attachment_list.jsx b/webapp/components/post_view/post_attachment_list.jsx deleted file mode 100644 index ce60a0155..000000000 --- a/webapp/components/post_view/post_attachment_list.jsx +++ /dev/null @@ -1,41 +0,0 @@ -// 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 = { - - /** - * The post id - */ - postId: PropTypes.string.isRequired, - - /** - * Array of attachments to render - */ - attachments: PropTypes.array.isRequired - } - - render() { - const content = []; - this.props.attachments.forEach((attachment, i) => { - content.push( - <PostAttachment - attachment={attachment} - postId={this.props.postId} - 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 deleted file mode 100644 index 1f889f1d6..000000000 --- a/webapp/components/post_view/post_attachment_opengraph/index.js +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {connect} from 'react-redux'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; -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), - currentUser: getCurrentUser(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - getOpenGraphMetadata - }, dispatch) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(PostAttachmentOpenGraph); diff --git a/webapp/components/post_view/post_attachment_opengraph/post_attachment_opengraph.jsx b/webapp/components/post_view/post_attachment_opengraph/post_attachment_opengraph.jsx deleted file mode 100644 index 729084637..000000000 --- a/webapp/components/post_view/post_attachment_opengraph/post_attachment_opengraph.jsx +++ /dev/null @@ -1,328 +0,0 @@ -// 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 {postListScrollChange} from 'actions/global_actions.jsx'; -import {updatePost} from 'actions/post_actions.jsx'; - -import * as Utils from 'utils/utils.jsx'; -import * as CommonUtils from 'utils/commons.jsx'; -import {PostTypes} from 'utils/constants.jsx'; - -export default class PostAttachmentOpenGraph extends React.PureComponent { - static propTypes = { - - /** - * The link to display the open graph data for - */ - link: PropTypes.string.isRequired, - - /** - * The current user viewing the post - */ - currentUser: PropTypes.object, - - /** - * The post where this link is included - */ - post: PropTypes.object, - - /** - * The open graph data to render - */ - openGraphData: PropTypes.object, - - /** - * 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; - this.imageDimentions = { // Image dimentions in pixels. - height: 80, - width: 80 - }; - this.textMaxLenght = 300; - this.textEllipsis = '...'; - this.largeImageMinRatio = 16 / 9; - this.smallImageContainerLeftPadding = 15; - - this.imageRatio = null; - - this.smallImageContainer = null; - this.smallImageElement = null; - - this.IMAGE_LOADED = { - LOADING: 'loading', - YES: 'yes', - ERROR: 'error' - }; - - this.fetchData = this.fetchData.bind(this); - this.toggleImageVisibility = this.toggleImageVisibility.bind(this); - this.onImageLoad = this.onImageLoad.bind(this); - this.onImageError = this.onImageError.bind(this); - this.handleRemovePreview = this.handleRemovePreview.bind(this); - } - - componentWillMount() { - const removePreview = this.isRemovePreview(this.props.post, this.props.currentUser); - - this.setState({ - imageLoaded: this.IMAGE_LOADED.LOADING, - imageVisible: this.props.previewCollapsed.startsWith('false'), - hasLargeImage: false, - removePreview - }); - this.fetchData(this.props.link); - } - - componentWillReceiveProps(nextProps) { - if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) { - const removePreview = this.isRemovePreview(nextProps.post, nextProps.currentUser); - this.setState({ - removePreview - }); - } - if (nextProps.link !== this.props.link) { - this.fetchData(nextProps.link); - } - if (nextProps.previewCollapsed !== this.props.previewCollapsed) { - this.setState({ - imageVisible: nextProps.previewCollapsed.startsWith('false') - }); - } - } - - componentDidUpdate() { - setTimeout(postListScrollChange, 0); - } - - fetchData(url) { - if (!this.props.openGraphData) { - this.props.actions.getOpenGraphMetadata(url); - } - } - - getBestImageUrl(data) { - if (Utils.isEmptyObject(data.images)) { - return null; - } - - const bestImage = CommonUtils.getNearestPoint(this.imageDimentions, data.images, 'width', 'height'); - return bestImage.secure_url || bestImage.url; - } - - toggleImageVisibility() { - this.setState({imageVisible: !this.state.imageVisible}); - } - - onImageLoad(image) { - this.imageRatio = image.target.naturalWidth / image.target.naturalHeight; - if ( - image.target.naturalWidth >= this.largeImageMinWidth && - this.imageRatio >= this.largeImageMinRatio && - !this.state.hasLargeImage - ) { - this.setState({ - hasLargeImage: true - }); - } - this.setState({ - imageLoaded: this.IMAGE_LOADED.YES - }); - } - - onImageError() { - this.setState({imageLoaded: this.IMAGE_LOADED.ERROR}); - } - - loadImage(src) { - const img = new Image(); - img.onload = this.onImageLoad; - img.onerror = this.onImageError; - img.src = src; - } - - imageToggleAnchoreTag(imageUrl) { - if (imageUrl && this.state.hasLargeImage) { - return ( - <a - className={'post__embed-visibility'} - data-expanded={this.state.imageVisible} - aria-label='Toggle Embed Visibility' - onClick={this.toggleImageVisibility} - /> - ); - } - return null; - } - - wrapInSmallImageContainer(imageElement) { - return ( - <div - className='attachment__image__container--openraph' - ref={(div) => { - this.smallImageContainer = div; - }} - > - {imageElement} - </div> - ); - } - - imageTag(imageUrl, renderingForLargeImage = false) { - var element = null; - if ( - imageUrl && renderingForLargeImage === this.state.hasLargeImage && - (!renderingForLargeImage || (renderingForLargeImage && this.state.imageVisible)) - ) { - if (this.state.imageLoaded === this.IMAGE_LOADED.LOADING) { - if (renderingForLargeImage) { - element = <img className={'attachment__image attachment__image--openraph loading large_image'}/>; - } else { - element = this.wrapInSmallImageContainer( - <img className={'attachment__image attachment__image--openraph loading '}/> - ); - } - } else if (this.state.imageLoaded === this.IMAGE_LOADED.YES) { - if (renderingForLargeImage) { - element = ( - <img - className={'attachment__image attachment__image--openraph large_image'} - src={imageUrl} - /> - ); - } else { - element = this.wrapInSmallImageContainer( - <img - className={'attachment__image attachment__image--openraph'} - src={imageUrl} - ref={(img) => { - this.smallImageElement = img; - }} - /> - ); - } - } else if (this.state.imageLoaded === this.IMAGE_LOADED.ERROR) { - return null; - } - } - return element; - } - - truncateText(text, maxLength = this.textMaxLenght, ellipsis = this.textEllipsis) { - if (text.length > maxLength) { - return text.substring(0, maxLength - ellipsis.length) + ellipsis; - } - return text; - } - - handleRemovePreview() { - const props = Object.assign({}, this.props.post.props); - props[PostTypes.REMOVE_LINK_PREVIEW] = 'true'; - - const patchedPost = ({ - id: this.props.post.id, - props - }); - - updatePost(patchedPost, () => { - this.setState({removePreview: true}); - }); - } - - isRemovePreview(post, currentUser) { - if (post && post.props && currentUser.id === post.user_id) { - return post.props[PostTypes.REMOVE_LINK_PREVIEW] && post.props[PostTypes.REMOVE_LINK_PREVIEW] === 'true'; - } - - return false; - } - - render() { - const data = this.props.openGraphData; - if (!data || Utils.isEmptyObject(data.description) || this.state.removePreview) { - return null; - } - - let removePreviewButton; - if (this.props.currentUser.id === this.props.post.user_id) { - removePreviewButton = ( - <button - id='removePreviewButton' - type='button' - className='btn-close' - aria-label='Close' - onClick={this.handleRemovePreview} - > - <span aria-hidden='true'>{'×'}</span> - </button> - ); - } - - const imageUrl = this.getBestImageUrl(data); - if (imageUrl) { - this.loadImage(imageUrl); - } - - return ( - <div - className='attachment attachment--opengraph' - ref='attachment' - > - <div className='attachment__content'> - <div - className={'clearfix attachment__container attachment__container--opengraph'} - > - <div - className={'attachment__body__wrap attachment__body__wrap--opengraph'} - > - <span className='sitename'>{this.truncateText(data.site_name)}</span> - {removePreviewButton} - <h1 - className={'attachment__title attachment__title--opengraph' + (data.title ? '' : ' is-url')} - > - <a - className='attachment__title-link attachment__title-link--opengraph' - href={data.url || this.props.link} - target='_blank' - rel='noopener noreferrer' - title={data.title || data.url || this.props.link} - > - {this.truncateText(data.title || data.url || this.props.link)} - </a> - </h1> - <div > - <div - className={'attachment__body attachment__body--opengraph'} - > - <div> - <div> - {this.truncateText(data.description)} - {this.imageToggleAnchoreTag(imageUrl)} - </div> - {this.imageTag(imageUrl, true)} - </div> - </div> - </div> - </div> - {this.imageTag(imageUrl, false)} - </div> - </div> - </div> - ); - } -} diff --git a/webapp/components/post_view/post_body/index.js b/webapp/components/post_view/post_body/index.js deleted file mode 100644 index 90f04e0f9..000000000 --- a/webapp/components/post_view/post_body/index.js +++ /dev/null @@ -1,30 +0,0 @@ -// 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 = parentPost ? getUser(state, parentPost.user_id) : null; - } - - 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/post_body/post_body.jsx b/webapp/components/post_view/post_body/post_body.jsx deleted file mode 100644 index 1eab74cf2..000000000 --- a/webapp/components/post_view/post_body/post_body.jsx +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import PropTypes from 'prop-types'; -import React from 'react'; -import {FormattedMessage} from 'react-intl'; - -import * as PostActions from 'actions/post_actions.jsx'; - -import FileAttachmentListContainer from 'components/file_attachment_list'; -import CommentedOnFilesMessage from 'components/post_view/commented_on_files_message'; -import PostBodyAdditionalContent from 'components/post_view/post_body_additional_content.jsx'; -import FailedPostOptions from 'components/post_view/failed_post_options'; -import PostMessageView from 'components/post_view/post_message_view'; -import ReactionListContainer from 'components/post_view/reaction_list'; - -import * as Utils from 'utils/utils.jsx'; -import * as PostUtils from 'utils/post_utils.jsx'; - -import {Posts} from 'mattermost-redux/constants'; - -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() { - const post = this.props.post; - const parentPost = this.props.parentPost; - - let comment = ''; - let postClass = ''; - - if (parentPost && !Utils.isPostEphemeral(post)) { - const profile = this.props.parentPostUser; - - let apostrophe = ''; - let name = '...'; - if (profile != null) { - let username = Utils.displayUsernameForUser(profile); - if (parentPost.props && - parentPost.props.from_webhook && - parentPost.props.override_username && - global.window.mm_config.EnablePostUsernameOverride === 'true') { - username = parentPost.props.override_username; - } - - if (username.slice(-1) === 's') { - apostrophe = '\''; - } else { - apostrophe = '\'s'; - } - name = ( - <a - className='theme' - onClick={PostActions.searchForTerm.bind(null, username)} - > - {username} - </a> - ); - } - - let message = ''; - if (parentPost.message) { - message = Utils.replaceHtmlEntities(parentPost.message); - } else if (parentPost.file_ids && parentPost.file_ids.length > 0) { - message = ( - <CommentedOnFilesMessage - parentPostId={parentPost.id} - /> - ); - } - - comment = ( - <div className='post__link'> - <span> - <FormattedMessage - id='post_body.commentedOn' - defaultMessage='Commented on {name}{apostrophe} message: ' - values={{ - name, - apostrophe - }} - /> - <a - className='theme' - onClick={this.props.handleCommentClick} - > - {message} - </a> - </span> - </div> - ); - } - - let failedOptions; - if (this.props.post.failed) { - postClass += ' post--fail'; - failedOptions = <FailedPostOptions post={this.props.post}/>; - } - - if (PostUtils.isEdited(this.props.post)) { - postClass += ' post--edited'; - } - - let fileAttachmentHolder = null; - 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} - compactDisplay={this.props.compactDisplay} - /> - ); - } - - const messageWrapper = ( - <div - key={`${post.id}_message`} - id={`${post.id}_message`} - className={postClass} - > - {failedOptions} - <PostMessageView - lastPostCount={this.props.lastPostCount} - post={this.props.post} - compactDisplay={this.props.compactDisplay} - hasMention={true} - /> - </div> - ); - - let messageWithAdditionalContent; - if (this.props.post.state === Posts.POST_DELETED) { - messageWithAdditionalContent = messageWrapper; - } else { - messageWithAdditionalContent = ( - <PostBodyAdditionalContent - post={this.props.post} - message={messageWrapper} - previewCollapsed={this.props.previewCollapsed} - /> - ); - } - - let mentionHighlightClass = ''; - if (this.props.isCommentMention) { - mentionHighlightClass = 'mention-comment'; - } - - return ( - <div> - {comment} - <div className={'post__body ' + mentionHighlightClass}> - {messageWithAdditionalContent} - {fileAttachmentHolder} - <ReactionListContainer post={post}/> - </div> - </div> - ); - } -} diff --git a/webapp/components/post_view/post_body_additional_content.jsx b/webapp/components/post_view/post_body_additional_content.jsx deleted file mode 100644 index 88e8f2ba8..000000000 --- a/webapp/components/post_view/post_body_additional_content.jsx +++ /dev/null @@ -1,273 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import PostAttachmentList from './post_attachment_list.jsx'; -import PostAttachmentOpenGraph from './post_attachment_opengraph'; -import PostImage from './post_image.jsx'; -import YoutubeVideo from 'components/youtube_video'; - -import Constants from 'utils/constants.jsx'; -import * as Utils from 'utils/utils.jsx'; -import BrowserStore from 'stores/browser_store.jsx'; - -import React from 'react'; -import PropTypes from 'prop-types'; - -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: '' - } - - constructor(props) { - super(props); - - this.getSlackAttachment = this.getSlackAttachment.bind(this); - this.generateToggleableEmbed = this.generateToggleableEmbed.bind(this); - this.generateStaticEmbed = this.generateStaticEmbed.bind(this); - this.toggleEmbedVisibility = this.toggleEmbedVisibility.bind(this); - this.isLinkToggleable = this.isLinkToggleable.bind(this); - this.handleLinkLoadError = this.handleLinkLoadError.bind(this); - this.handleLinkLoaded = this.handleLinkLoaded.bind(this); - - this.state = { - embedVisible: PostBodyAdditionalContent.isEmbedVisible(props), - link: Utils.extractFirstLink(props.post.message), - linkLoadError: false, - linkLoaded: false - }; - } - - componentDidMount() { - // check the availability of the image rendered(if any) in the first render. - this.preCheckImageLink(); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.previewCollapsed !== this.props.previewCollapsed || nextProps.post.message !== this.props.post.message) { - this.setState({ - embedVisible: PostBodyAdditionalContent.isEmbedVisible(nextProps), - link: Utils.extractFirstLink(nextProps.post.message) - }, () => { - // check the availability of the image link - this.preCheckImageLink(); - }); - } - } - - toggleEmbedVisibility() { - // save the taggle info in the localstorage - BrowserStore.setItem(`isVisible-${this.props.post.id}`, !this.state.embedVisible); - - this.setState((prevState) => { - return {embedVisible: !prevState.embedVisible}; - }); - } - - getSlackAttachment() { - let attachments = []; - if (this.props.post.props && this.props.post.props.attachments) { - attachments = this.props.post.props.attachments; - } - - return ( - <PostAttachmentList - attachments={attachments} - postId={this.props.post.id} - key={this.props.post.id} - /> - ); - } - - // when image links are collapsed, check if the link is a valid image url and it is available - preCheckImageLink() { - // check only if embedVisible is false i.e the image are by default hidden/collapsed - // if embedVisible is true, the image is rendered, during which image load error is captured - if (!this.state.embedVisible && this.isLinkImage(this.state.link)) { - const image = new Image(); - image.src = this.state.link; - - image.onload = () => { - this.handleLinkLoaded(); - }; - - image.onerror = () => { - this.handleLinkLoadError(); - }; - } - } - - isLinkImage(link) { - const regex = /.+\/(.+\.(?:jpg|gif|bmp|png|jpeg))(?:\?.*)?$/i; - const match = link.match(regex); - if (match && match[1]) { - return true; - } - - return false; - } - - isLinkToggleable() { - const link = this.state.link; - if (!link) { - return false; - } - - if (YoutubeVideo.isYoutubeLink(link)) { - return true; - } - - if (this.isLinkImage(link)) { - return true; - } - - return false; - } - - handleLinkLoadError() { - this.setState({ - linkLoadError: true - }); - } - - handleLinkLoaded() { - this.setState({ - linkLoaded: true - }); - } - - generateToggleableEmbed() { - const link = this.state.link; - if (!link) { - return null; - } - - if (YoutubeVideo.isYoutubeLink(link)) { - return ( - <YoutubeVideo - channelId={this.props.post.channel_id} - link={link} - show={this.state.embedVisible} - onLinkLoaded={this.handleLinkLoaded} - /> - ); - } - - if (this.isLinkImage(link)) { - return ( - <PostImage - channelId={this.props.post.channel_id} - link={link} - onLinkLoadError={this.handleLinkLoadError} - onLinkLoaded={this.handleLinkLoaded} - /> - ); - } - - return null; - } - - generateStaticEmbed() { - if (this.props.post.props && this.props.post.props.attachments) { - return this.getSlackAttachment(); - } - - const link = Utils.extractFirstLink(this.props.post.message); - if (link && Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_PREVIEW) && global.window.mm_config.EnableLinkPreviews === 'true') { - return ( - <PostAttachmentOpenGraph - link={link} - previewCollapsed={this.props.previewCollapsed} - post={this.props.post} - /> - ); - } - - return null; - } - - render() { - if (this.isLinkToggleable() && !this.state.linkLoadError) { - // if message has only one line and starts with a link place toggle in this only line - // else - place it in new line between message and embed - const prependToggle = (/^\s*https?:\/\/.*$/).test(this.props.post.message); - - const toggle = ( - <a - key='toggle' - className={`post__embed-visibility ${prependToggle ? 'pull-left' : ''}`} - data-expanded={this.state.embedVisible} - aria-label='Toggle Embed Visibility' - onClick={this.toggleEmbedVisibility} - /> - ); - const message = ( - <div key='message'> - {this.props.message} - </div> - ); - - const contents = [message]; - - if (this.state.linkLoaded || YoutubeVideo.isYoutubeLink(this.state.link)) { - if (prependToggle) { - contents.unshift(toggle); - } else { - contents.push(toggle); - } - } - - if (this.state.embedVisible) { - contents.push( - <div - key='embed' - className='post__embed-container' - > - {this.generateToggleableEmbed()} - </div> - ); - } - - return ( - <div> - {contents} - </div> - ); - } - - const staticEmbed = this.generateStaticEmbed(); - - if (staticEmbed) { - return ( - <div> - {this.props.message} - {staticEmbed} - </div> - ); - } - - return this.props.message; - } - - static isEmbedVisible(props) { - // check first in localstorage, if not present, consider previewCollapsed from the parent component - return BrowserStore.getItem(`isVisible-${props.post.id}`, props.previewCollapsed.startsWith('false')); - } -} diff --git a/webapp/components/post_view/post_flag_icon.jsx b/webapp/components/post_view/post_flag_icon.jsx deleted file mode 100644 index 02f8feb53..000000000 --- a/webapp/components/post_view/post_flag_icon.jsx +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; -import PropTypes from 'prop-types'; -import {FormattedMessage} from 'react-intl'; -import {Tooltip, OverlayTrigger} from 'react-bootstrap'; - -import {flagPost, unflagPost} from 'actions/post_actions.jsx'; -import Constants from 'utils/constants.jsx'; -import * as Utils from 'utils/utils.jsx'; - -function flagToolTip(isFlagged) { - return ( - <Tooltip id='flagTooltip'> - <FormattedMessage - id={isFlagged ? 'flag_post.unflag' : 'flag_post.flag'} - defaultMessage={isFlagged ? 'Unflag' : 'Flag for follow up'} - /> - </Tooltip> - ); -} - -function flagIcon(isFlagged) { - let flagIconSvg = Constants.FLAG_ICON_SVG; - - if (isFlagged) { - flagIconSvg = Constants.FLAG_FILLED_ICON_SVG; - } - - return ( - <span - className='icon' - dangerouslySetInnerHTML={{__html: flagIconSvg}} - /> - ); -} - -export default function PostFlagIcon(props) { - function onFlagPost(e) { - e.preventDefault(); - flagPost(props.postId); - } - - function onUnflagPost(e) { - e.preventDefault(); - unflagPost(props.postId); - } - - const flagFunc = props.isFlagged ? onUnflagPost : onFlagPost; - const flagVisible = props.isFlagged ? 'visible' : ''; - - let flagIconId = null; - if (props.idCount > -1) { - flagIconId = Utils.createSafeId(props.idPrefix + props.idCount); - } - - if (!props.isEphemeral) { - return ( - <OverlayTrigger - trigger={['hover', 'focus']} - key={'flagtooltipkey' + flagVisible} - delayShow={Constants.OVERLAY_TIME_DELAY} - placement='top' - overlay={flagToolTip(props.isFlagged)} - > - <a - id={flagIconId} - href='#' - className={'flag-icon__container ' + flagVisible} - onClick={flagFunc} - > - {flagIcon(props.isFlagged)} - </a> - </OverlayTrigger> - ); - } - - return null; -} - -PostFlagIcon.propTypes = { - idPrefix: PropTypes.string.isRequired, - idCount: PropTypes.number, - postId: PropTypes.string.isRequired, - isFlagged: PropTypes.bool.isRequired, - isEphemeral: PropTypes.bool -}; - -PostFlagIcon.defaultProps = { - idCount: -1, - postId: '', - isFlagged: false, - isEphemeral: false -}; diff --git a/webapp/components/post_view/post_header/index.js b/webapp/components/post_view/post_header/index.js deleted file mode 100644 index d7aaef1d5..000000000 --- a/webapp/components/post_view/post_header/index.js +++ /dev/null @@ -1,18 +0,0 @@ -// 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/post_header/post_header.jsx b/webapp/components/post_view/post_header/post_header.jsx deleted file mode 100644 index 0715f047c..000000000 --- a/webapp/components/post_view/post_header/post_header.jsx +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import UserProfile from 'components/user_profile.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'; - -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 - } - - constructor(props) { - super(props); - this.state = {}; - } - - render() { - const post = this.props.post; - const isSystemMessage = PostUtils.isSystemMessage(post); - - let userProfile = ( - <UserProfile - user={this.props.user} - displayNameType={this.props.displayNameType} - status={this.props.status} - isBusy={this.props.isBusy} - hasMention={true} - /> - ); - let botIndicator; - let colon; - - if (post.props && post.props.from_webhook) { - if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { - userProfile = ( - <UserProfile - user={this.props.user} - overwriteName={post.props.override_username} - disablePopover={true} - /> - ); - } else { - userProfile = ( - <UserProfile - user={this.props.user} - displayNameType={this.props.displayNameType} - disablePopover={true} - /> - ); - } - - botIndicator = <div className='bot-indicator'>{Constants.BOT_NAME}</div>; - } else if (isSystemMessage) { - userProfile = ( - <UserProfile - user={{}} - overwriteName={ - <FormattedMessage - id='post_info.system' - defaultMessage='System' - /> - } - overwriteImage={Constants.SYSTEM_MESSAGE_PROFILE_IMAGE} - disablePopover={true} - /> - ); - } - - if (this.props.compactDisplay) { - colon = (<strong className='colon'>{':'}</strong>); - } - - return ( - <div className='post__header'> - <div className='col col__name'>{userProfile}{colon}</div> - {botIndicator} - <div className='col'> - <PostInfo - post={post} - handleCommentClick={this.props.handleCommentClick} - handleDropdownOpened={this.props.handleDropdownOpened} - compactDisplay={this.props.compactDisplay} - lastPostCount={this.props.lastPostCount} - replyCount={this.props.replyCount} - consecutivePostByUser={this.props.consecutivePostByUser} - getPostList={this.props.getPostList} - /> - </div> - </div> - ); - } -} diff --git a/webapp/components/post_view/post_image.jsx b/webapp/components/post_view/post_image.jsx deleted file mode 100644 index 322742305..000000000 --- a/webapp/components/post_view/post_image.jsx +++ /dev/null @@ -1,105 +0,0 @@ -// 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 {postListScrollChange} from 'actions/global_actions.jsx'; - -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 - } - - constructor(props) { - super(props); - - this.handleLoadComplete = this.handleLoadComplete.bind(this); - this.handleLoadError = this.handleLoadError.bind(this); - - this.state = { - loaded: false, - errored: false - }; - } - - componentWillMount() { - this.loadImg(this.props.link); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.link !== this.props.link) { - this.setState({ - loaded: false, - errored: false - }); - } - } - - componentDidUpdate(prevProps) { - if (!this.state.loaded && prevProps.link !== this.props.link) { - this.loadImg(this.props.link); - } - } - - loadImg(src) { - const img = new Image(); - img.onload = this.handleLoadComplete; - img.onerror = this.handleLoadError; - img.src = src; - } - - handleLoadComplete() { - this.setState({ - loaded: true, - errored: false - }); - - postListScrollChange(); - - if (this.props.onLinkLoaded) { - this.props.onLinkLoaded(); - } - } - - handleLoadError() { - this.setState({ - errored: true, - loaded: true - }); - if (this.props.onLinkLoadError) { - this.props.onLinkLoadError(); - } - } - - render() { - if (this.state.errored || !this.state.loaded) { - return null; - } - - return ( - <div - className='post__embed-container' - > - <img - className='img-div' - src={this.props.link} - /> - </div> - ); - } -} diff --git a/webapp/components/post_view/post_info/index.js b/webapp/components/post_view/post_info/index.js deleted file mode 100644 index 041080da8..000000000 --- a/webapp/components/post_view/post_info/index.js +++ /dev/null @@ -1,31 +0,0 @@ -// 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 {get, 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: get(state, Preferences.CATEGORY_FLAGGED_POST, ownProps.post.id, null) != null - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - removePost, - addReaction - }, dispatch) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(PostInfo); diff --git a/webapp/components/post_view/post_info/post_info.jsx b/webapp/components/post_view/post_info/post_info.jsx deleted file mode 100644 index cc3133764..000000000 --- a/webapp/components/post_view/post_info/post_info.jsx +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -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 * as ReduxPostUtils from 'mattermost-redux/utils/post_utils'; -import {emitEmojiPosted} from 'actions/post_actions.jsx'; -import Constants from 'utils/constants.jsx'; -import {Posts} from 'mattermost-redux/constants'; - -import React from 'react'; -import PropTypes from 'prop-types'; -import {FormattedMessage} from 'react-intl'; - -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, - - /** - * Set to render in compact view - */ - compactDisplay: PropTypes.bool, - - /** - * 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); - - this.removePost = this.removePost.bind(this); - this.reactEmojiClick = this.reactEmojiClick.bind(this); - - this.state = { - showEmojiPicker: false, - reactionPickerOffset: 21 - }; - } - - toggleEmojiPicker = () => { - const showEmojiPicker = !this.state.showEmojiPicker; - - this.setState({showEmojiPicker}); - this.props.handleDropdownOpened(showEmojiPicker); - }; - - hideEmojiPicker = () => { - this.setState({showEmojiPicker: false}); - this.props.handleDropdownOpened(false); - }; - - removePost() { - this.props.actions.removePost(this.props.post); - } - - createRemovePostButton() { - return ( - <a - href='#' - className='post__remove theme' - type='button' - onClick={this.removePost} - > - {'×'} - </a> - ); - } - - reactEmojiClick(emoji) { - const pickerOffset = 21; - this.setState({showEmojiPicker: false, reactionPickerOffset: pickerOffset}); - const emojiName = emoji.name || emoji.aliases[0]; - this.props.actions.addReaction(this.props.post.id, emojiName); - emitEmojiPosted(emojiName); - this.props.handleDropdownOpened(false); - } - - getDotMenu = () => { - return this.refs.dotMenu; - }; - - render() { - const post = this.props.post; - - let idCount = -1; - if (this.props.lastPostCount >= 0 && this.props.lastPostCount < Constants.TEST_ID_COUNT) { - idCount = this.props.lastPostCount; - } - - const isEphemeral = Utils.isPostEphemeral(post); - const isSystemMessage = PostUtils.isSystemMessage(post); - - let comments = null; - let react = null; - if (!isEphemeral && !post.failed && !isSystemMessage) { - comments = ( - <CommentIcon - idPrefix='commentIcon' - idCount={idCount} - handleCommentClick={this.props.handleCommentClick} - commentCount={this.props.replyCount} - id={post.channel_id + '_' + post.id} - /> - ); - - if (window.mm_config.EnableEmojiPicker === 'true') { - react = ( - <span> - <EmojiPickerOverlay - show={this.state.showEmojiPicker} - container={this.props.getPostList} - target={this.getDotMenu} - onHide={this.hideEmojiPicker} - onEmojiClick={this.reactEmojiClick} - rightOffset={7} - /> - <a - href='#' - className='reacticon__container' - onClick={this.toggleEmojiPicker} - > - <span - className='icon icon--emoji' - dangerouslySetInnerHTML={{__html: Constants.EMOJI_ICON_SVG}} - /> - </a> - </span> - - ); - } - } - - let options; - if (isEphemeral) { - options = ( - <div className='col col__remove'> - {this.createRemovePostButton()} - </div> - ); - } else if (!post.failed) { - const dotMenu = ( - <DotMenu - idPrefix={Constants.CENTER} - idCount={idCount} - post={this.props.post} - commentCount={this.props.replyCount} - isFlagged={this.props.isFlagged} - handleCommentClick={this.props.handleCommentClick} - handleDropdownOpened={this.props.handleDropdownOpened} - /> - ); - - if (PostUtils.shouldShowDotMenu(this.props.post)) { - options = ( - <div - ref='dotMenu' - className='col col__reply' - > - {dotMenu} - {react} - {comments} - </div> - ); - } - } - - let visibleMessage; - if (isEphemeral && !this.props.compactDisplay && post.state !== Posts.POST_DELETED) { - visibleMessage = ( - <span className='post__visibility'> - <FormattedMessage - id='post_info.message.visible' - defaultMessage='(Only visible to you)' - /> - </span> - ); - } - - let pinnedBadge; - if (post.is_pinned) { - pinnedBadge = ( - <span className='post__pinned-badge'> - <FormattedMessage - id='post_info.pinned' - defaultMessage='Pinned' - /> - </span> - ); - } - - // timestamp should not be a permalink if the post has been deleted, is ephemeral message, or is pending - const isPermalink = !(isEphemeral || - Posts.POST_DELETED === this.props.post.state || - ReduxPostUtils.isPostPendingOrFailed(this.props.post)); - - return ( - <div className='post__header--info'> - <div className='col'> - <PostTime - isPermalink={isPermalink} - eventTime={post.create_at} - useMilitaryTime={this.props.useMilitaryTime} - postId={post.id} - /> - {pinnedBadge} - {this.state.showEmojiPicker} - <PostFlagIcon - idPrefix={'centerPostFlag'} - idCount={idCount} - postId={post.id} - isFlagged={this.props.isFlagged} - isEphemeral={isEphemeral} - /> - {visibleMessage} - </div> - {options} - </div> - ); - } -} diff --git a/webapp/components/post_view/post_list.jsx b/webapp/components/post_view/post_list.jsx deleted file mode 100644 index c13de7096..000000000 --- a/webapp/components/post_view/post_list.jsx +++ /dev/null @@ -1,582 +0,0 @@ -// 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 EventTypes from 'utils/event_types.jsx'; -import GlobalEventEmitter from 'utils/global_event_emitter.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.isRequired, - - /** - * 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.atBottom = false; - - this.state = { - atEnd: false, - unViewedCount: 0, - isScrolling: false, - lastViewed: props.lastViewedAt - }; - } - - componentDidMount() { - this.loadPosts(this.props.channel.id, this.props.focusedPostId); - GlobalEventEmitter.addListener(EventTypes.POST_LIST_SCROLL_CHANGE, this.handleResize); - - window.addEventListener('resize', () => this.handleResize()); - } - - componentWillUnmount() { - GlobalEventEmitter.removeListener(EventTypes.POST_LIST_SCROLL_CHANGE, this.handleResize); - 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.atBottom = false; - this.setState({atEnd: false, lastViewed: nextProps.lastViewedAt}); - - if (nextChannel.id) { - this.loadPosts(nextChannel.id); - } - } - - const nextPosts = nextProps.posts || []; - const posts = this.props.posts || []; - const hasNewPosts = (posts.length === 0 && nextPosts.length > 0) || (posts.length > 0 && nextPosts.length > 0 && posts[0].id !== nextPosts[0].id); - - if (!this.checkBottom() && hasNewPosts) { - this.setUnreadsBelow(nextPosts, nextProps.currentUserId); - } - } - } - - 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, prevState) { - // Do not update scrolling unless posts, visibility or intro message change - if (this.props.posts === prevProps.posts && this.props.postVisibility === prevProps.postVisibility && this.state.atEnd === prevState.atEnd) { - return; - } - - const prevPosts = prevProps.posts; - const posts = this.props.posts; - const postList = this.refs.postlist; - - if (!postList) { - return; - } - - // Scroll to focused post on first load - const focusedPost = this.refs[this.props.focusedPostId]; - if (focusedPost && this.props.posts) { - if (!this.hasScrolledToFocusedPost) { - const element = ReactDOM.findDOMNode(focusedPost); - const rect = element.getBoundingClientRect(); - const listHeight = postList.clientHeight / 2; - postList.scrollTop += rect.top - listHeight; - } else if (this.previousScrollHeight !== postList.scrollHeight && posts[0].id === prevPosts[0].id) { - postList.scrollTop = this.previousScrollTop + (postList.scrollHeight - this.previousScrollHeight); - } - 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(); - if (!this.checkBottom()) { - this.setUnreadsBelow(posts, this.props.currentUserId); - } - return; - } else if (postList && !this.hasScrolledToNewMessageSeparator) { - postList.scrollTop = postList.scrollHeight; - this.atBottom = true; - return; - } - - if (postList && prevPosts && posts && posts[0] && prevPosts[0]) { - // A new message was posted, so scroll to bottom if user - // was already scrolled close to bottom - let doScrollToBottom = false; - const postId = posts[0].id; - const prevPostId = prevPosts[0].id; - const pendingPostId = posts[0].pending_post_id; - if (postId !== prevPostId && pendingPostId !== prevPostId) { - // If already scrolled to bottom - if (this.atBottom) { - doScrollToBottom = true; - } - - // If new post was ephemeral - if (Utils.isPostEphemeral(posts[0])) { - doScrollToBottom = true; - } - } - - if (doScrollToBottom) { - this.atBottom = true; - postList.scrollTop = postList.scrollHeight; - return; - } - - // New posts added at the top, maintain scroll position - if (this.previousScrollHeight !== postList.scrollHeight && posts[0].id === prevPosts[0].id) { - postList.scrollTop = this.previousScrollTop + (postList.scrollHeight - this.previousScrollHeight); - } - } - } - - setUnreadsBelow = (posts, currentUserId) => { - const unViewedCount = posts.reduce((count, post) => { - if (post.create_at > this.state.lastViewed && - post.user_id !== currentUserId && - post.state !== Constants.POST_DELETED) { - return count + 1; - } - return count; - }, 0); - this.setState({unViewedCount}); - } - - handleScrollStop = () => { - this.setState({ - isScrolling: false - }); - } - - checkBottom = () => { - if (!this.refs.postlist) { - return true; - } - - // No scroll bar so we're at the bottom - if (this.refs.postlist.scrollHeight <= this.refs.postlist.clientHeight) { - return true; - } - - return this.refs.postlist.clientHeight + this.refs.postlist.scrollTop >= this.refs.postlist.scrollHeight - CLOSE_TO_BOTTOM_SCROLL_MARGIN; - } - - handleResize = (forceScrollToBottom) => { - const postList = this.refs.postlist; - const messageSeparator = this.refs.newMessageSeparator; - const doScrollToBottom = this.atBottom || forceScrollToBottom; - - if (postList) { - if (doScrollToBottom) { - postList.scrollTop = postList.scrollHeight; - } else if (!this.hasScrolled && messageSeparator) { - const element = ReactDOM.findDOMNode(messageSeparator); - element.scrollIntoView(); - } - - this.previousScrollHeight = postList.scrollHeight; - this.previousScrollTop = postList.scrollTop; - this.previousClientHeight = postList.clientHeight; - - this.atBottom = this.checkBottom(); - } - } - - loadPosts = async (channelId, focusedPostId) => { - let posts; - if (focusedPostId) { - const getPostThreadAsync = this.props.actions.getPostThread(focusedPostId, false); - 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 = () => { - // Only count as user scroll if we've already performed our first load scroll - this.hasScrolled = this.hasScrolledToNewMessageSeparator || this.hasScrolledToFocusedPost; - if (!this.refs.postlist) { - return; - } - - this.previousScrollTop = this.refs.postlist.scrollTop; - - if (this.refs.postlist.scrollHeight === this.previousScrollHeight) { - this.atBottom = this.checkBottom(); - } - - this.updateFloatingTimestamp(); - - if (!this.state.isScrolling) { - this.setState({ - isScrolling: true - }); - } - - if (this.checkBottom()) { - 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 = () => { - if (this.refs.postlist) { - 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]; - - if (post == null) { - continue; - } - - 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.atBottom} - 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 deleted file mode 100644 index 84682eb89..000000000 --- a/webapp/components/post_view/post_message_view/index.js +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {connect} from 'react-redux'; -import {getCustomEmojisByName} 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 = getCustomEmojisByName(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/post_message_view/post_message_view.jsx b/webapp/components/post_view/post_message_view/post_message_view.jsx deleted file mode 100644 index 348748450..000000000 --- a/webapp/components/post_view/post_message_view/post_message_view.jsx +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {Parser, ProcessNodeDefinitions} from 'html-to-react'; -import PropTypes from 'prop-types'; -import React from 'react'; -import {FormattedMessage} from 'react-intl'; - -import AtMention from 'components/at_mention'; -import MarkdownImage from 'components/markdown_image'; - -import store from 'stores/redux_store.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 {getChannelsNameMapInCurrentTeam} from 'mattermost-redux/selectors/entities/channels'; -import {Posts} from 'mattermost-redux/constants'; - -import {renderSystemMessage} from './system_message_helpers.jsx'; - -export default class PostMessageView extends React.PureComponent { - static propTypes = { - - /* - * 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, - - /* - * 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), - - /* - * 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, - - /** - * Set to render post body compactly - */ - compactDisplay: PropTypes.bool, - - /** - * Flags if the post_message_view is for the RHS (Reply). - */ - isRHS: PropTypes.bool, - - /** - * Flags if the post_message_view is for the RHS (Reply). - */ - hasMention: PropTypes.bool - }; - - static defaultProps = { - options: {}, - mentionKeys: [], - isRHS: false, - hasMention: false - }; - - renderDeletedPost() { - return ( - <p> - <FormattedMessage - id='post_body.deleted' - defaultMessage='(message deleted)' - /> - </p> - ); - } - - renderEditedIndicator() { - if (!PostUtils.isEdited(this.props.post)) { - return null; - } - - return ( - <span className='post-edited-indicator'> - <FormattedMessage - id='post_message_view.edited' - defaultMessage='(edited)' - /> - </span> - ); - } - - postMessageHtmlToComponent(html) { - const parser = new Parser(); - const attrib = 'data-mention'; - const processNodeDefinitions = new ProcessNodeDefinitions(React); - - function isValidNode() { - return true; - } - - const processingInstructions = [ - { - replaceChildren: true, - shouldProcessNode: (node) => node.attribs && node.attribs[attrib], - processNode: (node) => { - const mentionName = node.attribs[attrib]; - - return ( - <AtMention - mentionName={mentionName} - isRHS={this.props.isRHS} - hasMention={this.props.hasMention} - /> - ); - } - }, - { - shouldProcessNode: (node) => node.type === 'tag' && node.name === 'img', - processNode: (node) => { - const { - class: className, - ...attribs - } = node.attribs; - - return ( - <MarkdownImage - className={className} - {...attribs} - /> - ); - } - }, - { - shouldProcessNode: () => true, - processNode: processNodeDefinitions.processDefaultNode - } - ]; - - return parser.parseWithInstructions(html, isValidNode, processingInstructions); - } - - render() { - if (this.props.post.state === Posts.POST_DELETED) { - return this.renderDeletedPost(); - } - - if (!this.props.enableFormatting) { - return <span>{this.props.post.message}</span>; - } - - const options = Object.assign({}, this.props.options, { - emojis: this.props.emojis, - siteURL: this.props.siteUrl, - mentionKeys: this.props.mentionKeys, - atMentions: true, - channelNamesMap: getChannelsNameMapInCurrentTeam(store.getState()), - team: this.props.team - }); - - const renderedSystemMessage = renderSystemMessage(this.props.post, options); - if (renderedSystemMessage) { - return <div>{renderedSystemMessage}</div>; - } - - let postId = null; - if (this.props.lastPostCount >= 0) { - postId = Utils.createSafeId('lastPostMessageText' + this.props.lastPostCount); - } - - let message = this.props.post.message; - const isEphemeral = Utils.isPostEphemeral(this.props.post); - if (this.props.compactDisplay && isEphemeral) { - const visibleMessage = Utils.localizeMessage('post_info.message.visible.compact', ' (Only visible to you)'); - message = message.concat(visibleMessage); - } - const htmlFormattedText = TextFormatting.formatText(message, options); - const postMessageComponent = this.postMessageHtmlToComponent(htmlFormattedText); - - return ( - <div> - <span - id={postId} - className='post-message__text' - onClick={Utils.handleFormattedTextClick} - > - {postMessageComponent} - </span> - {this.renderEditedIndicator()} - </div> - ); - } -} diff --git a/webapp/components/post_view/post_message_view/system_message_helpers.jsx b/webapp/components/post_view/post_message_view/system_message_helpers.jsx deleted file mode 100644 index c134e1a7a..000000000 --- a/webapp/components/post_view/post_message_view/system_message_helpers.jsx +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; -import {FormattedMessage} from 'react-intl'; - -import {PostTypes} from 'utils/constants.jsx'; -import {formatText} from 'utils/text_formatting.jsx'; - -function renderUsername(value, options) { - return renderFormattedText(value, {...options, markdown: false}); -} - -function renderFormattedText(value, options) { - return <span dangerouslySetInnerHTML={{__html: formatText(value, options)}}/>; -} - -function renderJoinChannelMessage(post, options) { - const username = renderUsername(post.props.username, options); - - return ( - <FormattedMessage - id='api.channel.join_channel.post_and_forget' - defaultMessage='{username} has joined the channel.' - values={{username}} - /> - ); -} - -function renderLeaveChannelMessage(post, options) { - const username = renderUsername(post.props.username, options); - - return ( - <FormattedMessage - id='api.channel.leave.left' - defaultMessage='{username} has left the channel.' - values={{username}} - /> - ); -} - -function renderAddToChannelMessage(post, options) { - const username = renderUsername(post.props.username, options); - const addedUsername = renderUsername(post.props.addedUsername, options); - - return ( - <FormattedMessage - id='api.channel.add_member.added' - defaultMessage='{addedUsername} added to the channel by {username}' - values={{ - username, - addedUsername - }} - /> - ); -} - -function renderRemoveFromChannelMessage(post, options) { - const removedUsername = renderUsername(post.props.removedUsername, options); - - return ( - <FormattedMessage - id='api.channel.remove_member.removed' - defaultMessage='{removedUsername} was removed from the channel' - values={{ - removedUsername - }} - /> - ); -} - -function renderHeaderChangeMessage(post, options) { - if (!post.props.username) { - return null; - } - - const headerOptions = { - ...options, - singleline: true - }; - - const username = renderUsername(post.props.username, options); - const oldHeader = post.props.old_header ? renderFormattedText(post.props.old_header, headerOptions) : null; - const newHeader = post.props.new_header ? renderFormattedText(post.props.new_header, headerOptions) : null; - - if (post.props.new_header) { - if (post.props.old_header) { - return ( - <FormattedMessage - id='api.channel.post_update_channel_header_message_and_forget.updated_from' - defaultMessage='{username} updated the channel header from: {old} to: {new}' - values={{ - username, - old: oldHeader, - new: newHeader - }} - /> - ); - } - - return ( - <FormattedMessage - id='api.channel.post_update_channel_header_message_and_forget.updated_to' - defaultMessage='{username} updated the channel header to: {new}' - values={{ - username, - new: newHeader - }} - /> - ); - } else if (post.props.old_header) { - return ( - <FormattedMessage - id='api.channel.post_update_channel_header_message_and_forget.removed' - defaultMessage='{username} removed the channel header (was: {old})' - values={{ - username, - old: oldHeader - }} - /> - ); - } - - return null; -} - -function renderDisplayNameChangeMessage(post, options) { - if (!(post.props.username && post.props.old_displayname && post.props.new_displayname)) { - return null; - } - - const username = renderUsername(post.props.username, options); - const oldDisplayName = post.props.old_displayname; - const newDisplayName = post.props.new_displayname; - - return ( - <FormattedMessage - id='api.channel.post_update_channel_displayname_message_and_forget.updated_from' - defaultMessage='{username} updated the channel display name from: {old} to: {new}' - values={{ - username, - old: oldDisplayName, - new: newDisplayName - }} - /> - ); -} - -function renderPurposeChangeMessage(post, options) { - if (!post.props.username) { - return null; - } - - const username = renderUsername(post.props.username, options); - const oldPurpose = post.props.old_purpose; - const newPurpose = post.props.new_purpose; - - if (post.props.new_purpose) { - if (post.props.old_purpose) { - return ( - <FormattedMessage - id='app.channel.post_update_channel_purpose_message.updated_from' - defaultMessage='{username} updated the channel purpose from: {old} to: {new}' - values={{ - username, - old: oldPurpose, - new: newPurpose - }} - /> - ); - } - - return ( - <FormattedMessage - id='app.channel.post_update_channel_purpose_message.updated_to' - defaultMessage='{username} updated the channel purpose to: {new}' - values={{ - username, - new: newPurpose - }} - /> - ); - } else if (post.props.old_purpose) { - return ( - <FormattedMessage - id='app.channel.post_update_channel_purpose_message.removed' - defaultMessage='{username} removed the channel purpose (was: {old})' - values={{ - username, - old: oldPurpose - }} - /> - ); - } - - return null; -} - -function renderChannelDeletedMessage(post, options) { - if (!post.props.username) { - return null; - } - - const username = renderUsername(post.props.username, options); - - return ( - <FormattedMessage - id='api.channel.delete_channel.archived' - defaultMessage='{username} has archived the channel.' - values={{username}} - /> - ); -} - -const systemMessageRenderers = { - [PostTypes.JOIN_CHANNEL]: renderJoinChannelMessage, - [PostTypes.LEAVE_CHANNEL]: renderLeaveChannelMessage, - [PostTypes.ADD_TO_CHANNEL]: renderAddToChannelMessage, - [PostTypes.REMOVE_FROM_CHANNEL]: renderRemoveFromChannelMessage, - [PostTypes.HEADER_CHANGE]: renderHeaderChangeMessage, - [PostTypes.DISPLAYNAME_CHANGE]: renderDisplayNameChangeMessage, - [PostTypes.PURPOSE_CHANGE]: renderPurposeChangeMessage, - [PostTypes.CHANNEL_DELETED]: renderChannelDeletedMessage -}; - -export function renderSystemMessage(post, options) { - if (!systemMessageRenderers[post.type]) { - return null; - } - - return systemMessageRenderers[post.type](post, options); -} diff --git a/webapp/components/post_view/post_time.jsx b/webapp/components/post_view/post_time.jsx deleted file mode 100644 index 2619c6807..000000000 --- a/webapp/components/post_view/post_time.jsx +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import Constants from 'utils/constants.jsx'; -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.PureComponent { - static propTypes = { - - /* - * If true, time will be rendered as a permalink to the post - */ - isPermalink: PropTypes.bool.isRequired, - - /* - * 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.state = { - currentTeamDisplayName: TeamStore.getCurrent().name, - width: '', - height: '' - }; - } - - componentDidMount() { - this.intervalId = setInterval(() => { - this.forceUpdate(); - }, Constants.TIME_SINCE_UPDATE_INTERVAL); - window.addEventListener('resize', () => { - updateWindowDimensions(this); - }); - } - - componentWillUnmount() { - clearInterval(this.intervalId); - window.removeEventListener('resize', () => { - updateWindowDimensions(this); - }); - } - - renderTimeTag() { - const date = getDateForUnixTicks(this.props.eventTime); - - return ( - <time - className='post__time' - dateTime={date.toISOString()} - title={date} - > - {date.toLocaleString('en', {hour: '2-digit', minute: '2-digit', hour12: !this.props.useMilitaryTime})} - </time> - ); - } - - render() { - if (isMobile() || !this.props.isPermalink) { - return this.renderTimeTag(); - } - - return ( - <Link - to={`/${this.state.currentTeamDisplayName}/pl/${this.props.postId}`} - target='_blank' - className='post__permalink' - > - {this.renderTimeTag()} - </Link> - ); - } -} diff --git a/webapp/components/post_view/reaction/index.js b/webapp/components/post_view/reaction/index.js deleted file mode 100644 index 74edd1324..000000000 --- a/webapp/components/post_view/reaction/index.js +++ /dev/null @@ -1,52 +0,0 @@ -// 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.get(ownProps.emojiName); - } - - let emojiImageUrl = ''; - if (emoji) { - emojiImageUrl = getEmojiImageUrl(emoji); - } - - return { - ...ownProps, - profiles, - otherUsersCount: ownProps.reactions.length - profiles.length, - currentUserId: getCurrentUserId(state), - reactionCount: ownProps.reactions.length, - emojiImageUrl - }; - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - addReaction, - removeReaction, - getMissingProfilesByIds - }, dispatch) - }; -} - -export default connect(makeMapStateToProps, mapDispatchToProps)(Reaction); diff --git a/webapp/components/post_view/reaction/reaction.jsx b/webapp/components/post_view/reaction/reaction.jsx deleted file mode 100644 index 673f8fd7f..000000000 --- a/webapp/components/post_view/reaction/reaction.jsx +++ /dev/null @@ -1,254 +0,0 @@ -// 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 * as Utils from 'utils/utils.jsx'; - -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, - - /* - * 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, - - /* - * The number of users not in the profile list who have reacted with this emoji - */ - otherUsersCount: PropTypes.number.isRequired, - - /* - * Array of reactions by user - */ - reactions: PropTypes.arrayOf(PropTypes.object).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, - - /* - * Function to get non-loaded profiles by id - */ - getMissingProfilesByIds: PropTypes.func.isRequired, - - /* - * Function to remove a reaction from a post - */ - removeReaction: PropTypes.func.isRequired - }) - } - - constructor(props) { - super(props); - - this.addReaction = this.addReaction.bind(this); - this.removeReaction = this.removeReaction.bind(this); - } - - addReaction(e) { - e.preventDefault(); - this.props.actions.addReaction(this.props.post.id, this.props.emojiName); - } - - removeReaction(e) { - e.preventDefault(); - this.props.actions.removeReaction(this.props.post.id, this.props.emojiName); - } - - loadMissingProfiles = () => { - const ids = this.props.reactions.map((reaction) => reaction.user_id); - this.props.actions.getMissingProfilesByIds(ids); - } - - render() { - if (!this.props.emojiImageUrl) { - return null; - } - - let currentUserReacted = false; - const users = []; - const otherUsersCount = this.props.otherUsersCount; - for (const user of this.props.profiles) { - if (user.id === this.props.currentUserId) { - currentUserReacted = true; - } else { - users.push(Utils.displayUsernameForUser(user)); - } - } - - // Sort users in alphabetical order with "you" being first if the current user reacted - users.sort(); - if (currentUserReacted) { - users.unshift(Utils.localizeMessage('reaction.you', 'You')); - } - - let names; - if (otherUsersCount > 0) { - if (users.length > 0) { - names = ( - <FormattedMessage - id='reaction.usersAndOthersReacted' - defaultMessage='{users} and {otherUsers, number} other {otherUsers, plural, one {user} other {users}}' - values={{ - users: users.join(', '), - otherUsers: otherUsersCount - }} - /> - ); - } else { - names = ( - <FormattedMessage - id='reaction.othersReacted' - defaultMessage='{otherUsers, number} {otherUsers, plural, one {user} other {users}}' - values={{ - otherUsers: otherUsersCount - }} - /> - ); - } - } else if (users.length > 1) { - names = ( - <FormattedMessage - id='reaction.usersReacted' - defaultMessage='{users} and {lastUser}' - values={{ - users: users.slice(0, -1).join(', '), - lastUser: users[users.length - 1] - }} - /> - ); - } else { - names = users[0]; - } - - let reactionVerb; - if (users.length + otherUsersCount > 1) { - if (currentUserReacted) { - reactionVerb = ( - <FormattedMessage - id='reaction.reactionVerb.youAndUsers' - defaultMessage='reacted' - /> - ); - } else { - reactionVerb = ( - <FormattedMessage - id='reaction.reactionVerb.users' - defaultMessage='reacted' - /> - ); - } - } else if (currentUserReacted) { - reactionVerb = ( - <FormattedMessage - id='reaction.reactionVerb.you' - defaultMessage='reacted' - /> - ); - } else { - reactionVerb = ( - <FormattedMessage - id='reaction.reactionVerb.user' - defaultMessage='reacted' - /> - ); - } - - const tooltip = ( - <FormattedMessage - id='reaction.reacted' - defaultMessage='{users} {reactionVerb} with {emoji}' - values={{ - users: <b>{names}</b>, - reactionVerb, - emoji: <b>{':' + this.props.emojiName + ':'}</b> - }} - /> - ); - - let handleClick; - let clickTooltip; - let className = 'post-reaction'; - if (currentUserReacted) { - handleClick = this.removeReaction; - clickTooltip = ( - <FormattedMessage - id='reaction.clickToRemove' - defaultMessage='(click to remove)' - /> - ); - - className += ' post-reaction--current-user'; - } else { - handleClick = this.addReaction; - clickTooltip = ( - <FormattedMessage - id='reaction.clickToAdd' - defaultMessage='(click to add)' - /> - ); - } - - return ( - <OverlayTrigger - trigger={['hover', 'focus']} - delayShow={1000} - placement='top' - shouldUpdatePosition={true} - overlay={ - <Tooltip id={`${this.props.post.id}-${this.props.emojiName}-reaction`}> - {tooltip} - <br/> - {clickTooltip} - </Tooltip> - } - onEnter={this.loadMissingProfiles} - > - <div - className={className} - onClick={handleClick} - > - <span - className='post-reaction__emoji emoticon' - style={{backgroundImage: 'url(' + this.props.emojiImageUrl + ')'}} - /> - <span className='post-reaction__count'> - {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 deleted file mode 100644 index ee807ca88..000000000 --- a/webapp/components/post_view/reaction_list/index.js +++ /dev/null @@ -1,33 +0,0 @@ -// 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 {getCustomEmojisByName} 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: getCustomEmojisByName(state) - }; - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - getReactionsForPost: Actions.getReactionsForPost - }, dispatch) - }; -} - -export default connect(makeMapStateToProps, mapDispatchToProps)(ReactionList); diff --git a/webapp/components/post_view/reaction_list/reaction_list.jsx b/webapp/components/post_view/reaction_list/reaction_list.jsx deleted file mode 100644 index 4d2f3a5fc..000000000 --- a/webapp/components/post_view/reaction_list/reaction_list.jsx +++ /dev/null @@ -1,88 +0,0 @@ -// 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 {postListScrollChange} from 'actions/global_actions.jsx'; - -import Reaction from 'components/post_view/reaction'; - -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), - - /** - * 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); - } - } - - componentDidUpdate(prevProps) { - if (this.props.reactions !== prevProps.reactions) { - postListScrollChange(); - } - } - - render() { - if (!this.props.post.has_reactions || (this.props.reactions && this.props.reactions.length === 0)) { - return null; - } - - const reactionsByName = new Map(); - const emojiNames = []; - - if (this.props.reactions) { - for (const reaction of this.props.reactions) { - const emojiName = reaction.emoji_name; - - if (reactionsByName.has(emojiName)) { - reactionsByName.get(emojiName).push(reaction); - } else { - emojiNames.push(emojiName); - reactionsByName.set(emojiName, [reaction]); - } - } - } - - const children = emojiNames.map((emojiName) => { - return ( - <Reaction - key={emojiName} - post={this.props.post} - emojiName={emojiName} - reactions={reactionsByName.get(emojiName) || []} - emojis={this.props.emojis} - /> - ); - }); - - return ( - <div className='post-reaction-list'> - {children} - </div> - ); - } -} diff --git a/webapp/components/post_view/scroll_to_bottom_arrows.jsx b/webapp/components/post_view/scroll_to_bottom_arrows.jsx deleted file mode 100644 index 73f8e6527..000000000 --- a/webapp/components/post_view/scroll_to_bottom_arrows.jsx +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import $ from 'jquery'; - -import Constants from 'utils/constants.jsx'; - -import PropTypes from 'prop-types'; - -import React from 'react'; - -export default function ScrollToBottomArrows(props) { - // only show on mobile - if ($(window).width() > 768) { - return <noscript/>; - } - - let className = 'post-list__arrows'; - if (props.isScrolling && !props.atBottom) { - className += ' scrolling'; - } - - return ( - <div - className={className} - onClick={props.onClick} - > - <span dangerouslySetInnerHTML={{__html: Constants.SCROLL_BOTTOM_ICON}}/> - </div> - ); -} - -ScrollToBottomArrows.propTypes = { - isScrolling: PropTypes.bool.isRequired, - atBottom: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired -}; |