PLT-2672 Refactored posts view with caching (#3054)
* Refactored posts view to use view controller design * Add post view caching * Required updates after rebase * Fixed bug where current channel not set yet was causing breakage
Этот коммит содержится в:
коммит произвёл
Christopher Speller
родитель
ca9f348be6
Коммит
6399a94ce2
54
webapp/components/post_view/components/floating_timestamp.jsx
Обычный файл
54
webapp/components/post_view/components/floating_timestamp.jsx
Обычный файл
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {FormattedDate} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
|
||||
export default class FloatingTimestamp extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div>
|
||||
<span>{dateString}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FloatingTimestamp.propTypes = {
|
||||
isScrolling: React.PropTypes.bool.isRequired,
|
||||
isMobile: React.PropTypes.bool,
|
||||
createAt: React.PropTypes.number
|
||||
};
|
||||
92
webapp/components/post_view/components/pending_post_options.jsx
Обычный файл
92
webapp/components/post_view/components/pending_post_options.jsx
Обычный файл
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
|
||||
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
||||
|
||||
import Client from 'utils/web_client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PendingPostOptions extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.retryPost = this.retryPost.bind(this);
|
||||
this.cancelPost = this.cancelPost.bind(this);
|
||||
this.state = {};
|
||||
}
|
||||
retryPost(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var post = this.props.post;
|
||||
Client.createPost(post,
|
||||
(data) => {
|
||||
AsyncClient.getPosts(post.channel_id);
|
||||
|
||||
var channel = ChannelStore.get(post.channel_id);
|
||||
var member = ChannelStore.getMember(post.channel_id);
|
||||
member.msg_count = channel.total_msg_count;
|
||||
member.last_viewed_at = (new Date()).getTime();
|
||||
ChannelStore.setChannelMember(member);
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_POST,
|
||||
post: data
|
||||
});
|
||||
},
|
||||
() => {
|
||||
post.state = Constants.POST_FAILED;
|
||||
PostStore.updatePendingPost(post);
|
||||
this.forceUpdate();
|
||||
}
|
||||
);
|
||||
|
||||
post.state = Constants.POST_LOADING;
|
||||
PostStore.updatePendingPost(post);
|
||||
this.forceUpdate();
|
||||
}
|
||||
cancelPost(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var post = this.props.post;
|
||||
PostStore.removePendingPost(post.channel_id, post.pending_post_id);
|
||||
this.forceUpdate();
|
||||
}
|
||||
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>);
|
||||
}
|
||||
}
|
||||
|
||||
PendingPostOptions.propTypes = {
|
||||
post: React.PropTypes.object
|
||||
};
|
||||
216
webapp/components/post_view/components/post.jsx
Обычный файл
216
webapp/components/post_view/components/post.jsx
Обычный файл
@@ -0,0 +1,216 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostHeader from './post_header.jsx';
|
||||
import PostBody from './post_body.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
|
||||
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';
|
||||
|
||||
export default class Post extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleCommentClick = this.handleCommentClick.bind(this);
|
||||
this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
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
|
||||
});
|
||||
}
|
||||
forceUpdateInfo() {
|
||||
this.refs.info.forceUpdate();
|
||||
this.refs.header.forceUpdate();
|
||||
}
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.sameRoot !== this.props.sameRoot) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.sameUser !== this.props.sameUser) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.displayNameType !== this.props.displayNameType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.commentCount !== this.props.commentCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.shouldHighlight !== this.props.shouldHighlight) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.center !== this.props.center) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.compactDisplay !== this.props.compactDisplay) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
render() {
|
||||
const post = this.props.post;
|
||||
const parentPost = this.props.parentPost;
|
||||
const mattermostLogo = Constants.MATTERMOST_ICON_SVG;
|
||||
|
||||
if (!post.props) {
|
||||
post.props = {};
|
||||
}
|
||||
|
||||
let type = 'Post';
|
||||
if (post.root_id && post.root_id.length > 0) {
|
||||
type = 'Comment';
|
||||
}
|
||||
|
||||
const commentCount = this.props.commentCount;
|
||||
|
||||
let rootUser;
|
||||
if (this.props.sameRoot) {
|
||||
rootUser = 'same--root';
|
||||
} else {
|
||||
rootUser = 'other--root';
|
||||
}
|
||||
|
||||
let postType = '';
|
||||
if (type !== 'Post') {
|
||||
postType = 'post--comment';
|
||||
} else if (commentCount > 0) {
|
||||
postType = 'post--root';
|
||||
}
|
||||
|
||||
let currentUserCss = '';
|
||||
if (this.props.currentUser.id === post.user_id && !post.props.from_webhook && !PostUtils.isSystemMessage(post)) {
|
||||
currentUserCss = 'current--user';
|
||||
}
|
||||
|
||||
let timestamp = 0;
|
||||
if (!this.props.user || this.props.user.update_at == null) {
|
||||
timestamp = this.props.currentUser.update_at;
|
||||
} else {
|
||||
timestamp = this.props.user.update_at;
|
||||
}
|
||||
|
||||
let sameUserClass = '';
|
||||
if (this.props.sameUser) {
|
||||
sameUserClass = 'same--user';
|
||||
}
|
||||
|
||||
let shouldHighlightClass = '';
|
||||
if (this.props.shouldHighlight) {
|
||||
shouldHighlightClass = 'post--highlight';
|
||||
}
|
||||
|
||||
let systemMessageClass = '';
|
||||
if (PostUtils.isSystemMessage(post)) {
|
||||
systemMessageClass = 'post--system';
|
||||
}
|
||||
|
||||
let profilePic = (
|
||||
<img
|
||||
src={PostUtils.getProfilePicSrcForPost(post, timestamp)}
|
||||
height='36'
|
||||
width='36'
|
||||
/>
|
||||
);
|
||||
|
||||
if (PostUtils.isSystemMessage(post)) {
|
||||
profilePic = (
|
||||
<span
|
||||
className='icon'
|
||||
dangerouslySetInnerHTML={{__html: mattermostLogo}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let centerClass = '';
|
||||
if (this.props.center) {
|
||||
centerClass = 'center';
|
||||
}
|
||||
|
||||
let compactClass = '';
|
||||
if (this.props.compactDisplay) {
|
||||
compactClass = 'post--compact';
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
id={'post_' + post.id}
|
||||
className={'post ' + sameUserClass + ' ' + compactClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass + ' ' + systemMessageClass}
|
||||
>
|
||||
<div className={'post__content ' + centerClass}>
|
||||
<div className='post__img'>{profilePic}</div>
|
||||
<div>
|
||||
<PostHeader
|
||||
ref='header'
|
||||
post={post}
|
||||
sameRoot={this.props.sameRoot}
|
||||
commentCount={commentCount}
|
||||
handleCommentClick={this.handleCommentClick}
|
||||
isLastComment={this.props.isLastComment}
|
||||
sameUser={this.props.sameUser}
|
||||
user={this.props.user}
|
||||
currentUser={this.props.currentUser}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
/>
|
||||
<PostBody
|
||||
post={post}
|
||||
sameRoot={this.props.sameRoot}
|
||||
parentPost={parentPost}
|
||||
handleCommentClick={this.handleCommentClick}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Post.propTypes = {
|
||||
post: React.PropTypes.object.isRequired,
|
||||
parentPost: React.PropTypes.object,
|
||||
user: React.PropTypes.object,
|
||||
sameUser: React.PropTypes.bool,
|
||||
sameRoot: React.PropTypes.bool,
|
||||
hideProfilePic: React.PropTypes.bool,
|
||||
isLastComment: React.PropTypes.bool,
|
||||
shouldHighlight: React.PropTypes.bool,
|
||||
displayNameType: React.PropTypes.string,
|
||||
hasProfiles: React.PropTypes.bool,
|
||||
currentUser: React.PropTypes.object.isRequired,
|
||||
center: React.PropTypes.bool,
|
||||
compactDisplay: React.PropTypes.bool,
|
||||
commentCount: React.PropTypes.number
|
||||
};
|
||||
324
webapp/components/post_view/components/post_attachment.jsx
Обычный файл
324
webapp/components/post_view/components/post_attachment.jsx
Обычный файл
@@ -0,0 +1,324 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import * as TextFormatting from 'utils/text_formatting.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
collapse: {
|
||||
id: 'post_attachment.collapse',
|
||||
defaultMessage: '▲ collapse text'
|
||||
},
|
||||
more: {
|
||||
id: 'post_attachment.more',
|
||||
defaultMessage: '▼ read more'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class PostAttachment extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
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 ? `<a class="attachment-link-more" href="#">${this.props.intl.formatMessage(holders.collapse)}</a>` : '');
|
||||
const collapsedText = shouldCollapse ? this.getCollapsedText() : text;
|
||||
|
||||
return {
|
||||
shouldCollapse,
|
||||
collapsedText,
|
||||
uncollapsedText,
|
||||
text: shouldCollapse ? collapsedText : uncollapsedText,
|
||||
collapsed: shouldCollapse
|
||||
};
|
||||
}
|
||||
|
||||
toggleCollapseState(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const state = this.state;
|
||||
state.text = state.collapsed ? state.uncollapsedText : state.collapsedText;
|
||||
state.collapsed = !state.collapsed;
|
||||
this.setState(state);
|
||||
}
|
||||
|
||||
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');
|
||||
} else if (text.length > 700) {
|
||||
text = text.substr(0, 700);
|
||||
}
|
||||
|
||||
return TextFormatting.formatText(text) + `<a class="attachment-link-more" href="#">${this.props.intl.formatMessage(holders.more)}</a>`;
|
||||
}
|
||||
|
||||
getFieldsTable() {
|
||||
const fields = this.props.attachment.fields;
|
||||
if (!fields || !fields.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let 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 || '')}}
|
||||
>
|
||||
</td>
|
||||
);
|
||||
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)}}
|
||||
>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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}}
|
||||
>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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}
|
||||
</div>
|
||||
{thumb}
|
||||
<div style={{clear: 'both'}}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostAttachment.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
attachment: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(PostAttachment);
|
||||
30
webapp/components/post_view/components/post_attachment_list.jsx
Обычный файл
30
webapp/components/post_view/components/post_attachment_list.jsx
Обычный файл
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostAttachment from './post_attachment.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostAttachmentList extends React.Component {
|
||||
render() {
|
||||
let content = [];
|
||||
this.props.attachments.forEach((attachment, i) => {
|
||||
content.push(
|
||||
<PostAttachment
|
||||
attachment={attachment}
|
||||
key={'att_' + i}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='attachment_list'>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostAttachmentList.propTypes = {
|
||||
attachments: React.PropTypes.array.isRequired
|
||||
};
|
||||
108
webapp/components/post_view/components/post_attachment_oembed.jsx
Обычный файл
108
webapp/components/post_view/components/post_attachment_oembed.jsx
Обычный файл
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import React from 'react';
|
||||
|
||||
export default class PostAttachmentOEmbed extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.fetchData = this.fetchData.bind(this);
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({data: {}});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.link !== this.props.link) {
|
||||
this.isLoading = false;
|
||||
this.fetchData(nextProps.link);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchData(this.props.link);
|
||||
}
|
||||
|
||||
fetchData(link) {
|
||||
if (!this.isLoading) {
|
||||
this.isLoading = true;
|
||||
let url = 'https://noembed.com/embed?nowrap=on';
|
||||
url += '&url=' + encodeURIComponent(link);
|
||||
url += '&maxheight=' + this.props.provider.height;
|
||||
return $.ajax({
|
||||
url,
|
||||
dataType: 'jsonp',
|
||||
success: (result) => {
|
||||
this.isLoading = false;
|
||||
if (result.error) {
|
||||
this.setState({data: {}});
|
||||
} else {
|
||||
this.setState({data: result});
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.setState({data: {}});
|
||||
}
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
let data = {};
|
||||
let content;
|
||||
if ($.isEmptyObject(this.state.data)) {
|
||||
content = <div style={{height: this.props.provider.height}}/>;
|
||||
} else {
|
||||
data = this.state.data;
|
||||
content = (
|
||||
<div
|
||||
style={{height: this.props.provider.height}}
|
||||
dangerouslySetInnerHTML={{__html: data.html}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='attachment attachment--oembed'
|
||||
ref='attachment'
|
||||
>
|
||||
<div className='attachment__content'>
|
||||
<div
|
||||
className={'clearfix attachment__container'}
|
||||
>
|
||||
<h1
|
||||
className='attachment__title'
|
||||
>
|
||||
<a
|
||||
className='attachment__title-link'
|
||||
href={data.url}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
{data.title}
|
||||
</a>
|
||||
</h1>
|
||||
<div >
|
||||
<div
|
||||
className={'attachment__body attachment__body--no_thumb'}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostAttachmentOEmbed.propTypes = {
|
||||
link: React.PropTypes.string.isRequired,
|
||||
provider: React.PropTypes.object.isRequired
|
||||
};
|
||||
186
webapp/components/post_view/components/post_body.jsx
Обычный файл
186
webapp/components/post_view/components/post_body.jsx
Обычный файл
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import FileAttachmentList from 'components/file_attachment_list.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import * as TextFormatting from 'utils/text_formatting.jsx';
|
||||
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
|
||||
import PendingPostOptions from './pending_post_options.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import loadingGif from 'images/load.gif';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostBody extends React.Component {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextProps.parentPost, this.props.parentPost)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextProps.compactDisplay, this.props.compactDisplay)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.handleCommentClick.toString() !== this.props.handleCommentClick.toString()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const post = this.props.post;
|
||||
const filenames = this.props.post.filenames;
|
||||
const parentPost = this.props.parentPost;
|
||||
|
||||
let comment = '';
|
||||
let postClass = '';
|
||||
|
||||
if (parentPost) {
|
||||
const profile = UserStore.getProfile(parentPost.user_id);
|
||||
|
||||
let apostrophe = '';
|
||||
let name = '...';
|
||||
if (profile != null) {
|
||||
let username = profile.username;
|
||||
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={Utils.searchForTerm.bind(null, username)}
|
||||
>
|
||||
{username}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
let message = '';
|
||||
if (parentPost.message) {
|
||||
message = Utils.replaceHtmlEntities(parentPost.message);
|
||||
} else if (parentPost.filenames.length) {
|
||||
message = parentPost.filenames[0].split('/').pop();
|
||||
|
||||
if (parentPost.filenames.length === 2) {
|
||||
message += Utils.localizeMessage('post_body.plusOne', ' plus 1 other file');
|
||||
} else if (parentPost.filenames.length > 2) {
|
||||
message += Utils.localizeMessage('post_body.plusMore', ' plus {count} other files').replace('{count}', (parentPost.filenames.length - 1).toString());
|
||||
}
|
||||
}
|
||||
|
||||
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 loading;
|
||||
if (post.state === Constants.POST_FAILED) {
|
||||
postClass += ' post--fail';
|
||||
loading = <PendingPostOptions post={this.props.post}/>;
|
||||
} else if (post.state === Constants.POST_LOADING) {
|
||||
postClass += ' post-waiting';
|
||||
loading = (
|
||||
<img
|
||||
className='post-loading-gif pull-right'
|
||||
src={loadingGif}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let fileAttachmentHolder = '';
|
||||
if (filenames && filenames.length > 0) {
|
||||
fileAttachmentHolder = (
|
||||
<FileAttachmentList
|
||||
|
||||
filenames={filenames}
|
||||
channelId={post.channel_id}
|
||||
userId={post.user_id}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let message;
|
||||
let additionalContent = null;
|
||||
if (this.props.post.state === Constants.POST_DELETED) {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='post_body.deleted'
|
||||
defaultMessage='(message deleted)'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
message = (
|
||||
<span
|
||||
onClick={TextFormatting.handleClick}
|
||||
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message)}}
|
||||
/>
|
||||
);
|
||||
|
||||
additionalContent = (
|
||||
<PostBodyAdditionalContent post={this.props.post}/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{comment}
|
||||
<div className='post__body'>
|
||||
<div
|
||||
key={`${post.id}_message`}
|
||||
id={`${post.id}_message`}
|
||||
className={postClass}
|
||||
>
|
||||
{loading}
|
||||
{message}
|
||||
</div>
|
||||
{fileAttachmentHolder}
|
||||
{additionalContent}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostBody.propTypes = {
|
||||
post: React.PropTypes.object.isRequired,
|
||||
parentPost: React.PropTypes.object,
|
||||
retryPost: React.PropTypes.func.isRequired,
|
||||
handleCommentClick: React.PropTypes.func.isRequired,
|
||||
compactDisplay: React.PropTypes.bool
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostAttachmentList from './post_attachment_list.jsx';
|
||||
import PostAttachmentOEmbed from './post_attachment_oembed.jsx';
|
||||
import PostImage from './post_image.jsx';
|
||||
import YoutubeVideo from 'components/youtube_video.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import OEmbedProviders from './providers.json';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostBodyAdditionalContent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getSlackAttachment = this.getSlackAttachment.bind(this);
|
||||
this.getOEmbedProvider = this.getOEmbedProvider.bind(this);
|
||||
this.generateEmbed = this.generateEmbed.bind(this);
|
||||
this.toggleEmbedVisibility = this.toggleEmbedVisibility.bind(this);
|
||||
|
||||
this.state = {
|
||||
embedVisible: true
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.embedVisible !== this.state.embedVisible) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
toggleEmbedVisibility() {
|
||||
this.setState({embedVisible: !this.state.embedVisible});
|
||||
}
|
||||
|
||||
getSlackAttachment() {
|
||||
let attachments = [];
|
||||
if (this.props.post.props && this.props.post.props.attachments) {
|
||||
attachments = this.props.post.props.attachments;
|
||||
}
|
||||
|
||||
return (
|
||||
<PostAttachmentList
|
||||
attachments={attachments}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getOEmbedProvider(link) {
|
||||
for (let i = 0; i < OEmbedProviders.length; i++) {
|
||||
for (let j = 0; j < OEmbedProviders[i].patterns.length; j++) {
|
||||
if (link.match(OEmbedProviders[i].patterns[j])) {
|
||||
return OEmbedProviders[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
generateEmbed() {
|
||||
if (this.props.post.type === 'slack_attachment') {
|
||||
return this.getSlackAttachment();
|
||||
}
|
||||
|
||||
const link = Utils.extractFirstLink(this.props.post.message);
|
||||
if (!link) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_PREVIEW)) {
|
||||
const provider = this.getOEmbedProvider(link);
|
||||
|
||||
if (provider) {
|
||||
return (
|
||||
<PostAttachmentOEmbed
|
||||
provider={provider}
|
||||
link={link}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (YoutubeVideo.isYoutubeLink(link)) {
|
||||
return (
|
||||
<YoutubeVideo
|
||||
channelId={this.props.post.channel_id}
|
||||
link={link}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < Constants.IMAGE_TYPES.length; i++) {
|
||||
const imageType = Constants.IMAGE_TYPES[i];
|
||||
const suffix = link.substring(link.length - (imageType.length + 1));
|
||||
if (suffix === '.' + imageType || suffix === '=' + imageType) {
|
||||
return (
|
||||
<PostImage
|
||||
channelId={this.props.post.channel_id}
|
||||
link={link}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const generateEmbed = this.generateEmbed();
|
||||
|
||||
if (generateEmbed) {
|
||||
let toggle;
|
||||
if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_TOGGLE)) {
|
||||
toggle = (
|
||||
<a
|
||||
className='post__embed-visibility'
|
||||
data-expanded={this.state.embedVisible}
|
||||
aria-label='Toggle Embed Visibility'
|
||||
onClick={this.toggleEmbedVisibility}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{toggle}
|
||||
<div
|
||||
className='post__embed-container'
|
||||
hidden={!this.state.embedVisible}
|
||||
>
|
||||
{generateEmbed}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
PostBodyAdditionalContent.propTypes = {
|
||||
post: React.PropTypes.object.isRequired
|
||||
};
|
||||
84
webapp/components/post_view/components/post_header.jsx
Обычный файл
84
webapp/components/post_view/components/post_header.jsx
Обычный файл
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import UserProfile from 'components/user_profile.jsx';
|
||||
import PostInfo from './post_info.jsx';
|
||||
|
||||
import * as PostUtils from 'utils/post_utils.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostHeader extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
const post = this.props.post;
|
||||
|
||||
let userProfile = <UserProfile user={this.props.user}/>;
|
||||
let botIndicator;
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
botIndicator = <li className='col col__name bot-indicator'>{Constants.BOT_NAME}</li>;
|
||||
} else if (PostUtils.isSystemMessage(post)) {
|
||||
userProfile = (
|
||||
<UserProfile
|
||||
user={{}}
|
||||
overwriteName={Constants.SYSTEM_MESSAGE_PROFILE_NAME}
|
||||
overwriteImage={Constants.SYSTEM_MESSAGE_PROFILE_IMAGE}
|
||||
disablePopover={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className='post__header'>
|
||||
<li className='col col__name'>{userProfile}</li>
|
||||
{botIndicator}
|
||||
<li className='col'>
|
||||
<PostInfo
|
||||
post={post}
|
||||
commentCount={this.props.commentCount}
|
||||
handleCommentClick={this.props.handleCommentClick}
|
||||
allowReply='true'
|
||||
isLastComment={this.props.isLastComment}
|
||||
sameUser={this.props.sameUser}
|
||||
currentUser={this.props.currentUser}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostHeader.defaultProps = {
|
||||
post: null,
|
||||
commentCount: 0,
|
||||
isLastComment: false,
|
||||
sameUser: false
|
||||
};
|
||||
PostHeader.propTypes = {
|
||||
post: React.PropTypes.object.isRequired,
|
||||
user: React.PropTypes.object,
|
||||
currentUser: React.PropTypes.object.isRequired,
|
||||
commentCount: React.PropTypes.number.isRequired,
|
||||
isLastComment: React.PropTypes.bool.isRequired,
|
||||
handleCommentClick: React.PropTypes.func.isRequired,
|
||||
sameUser: React.PropTypes.bool.isRequired,
|
||||
compactDisplay: React.PropTypes.bool
|
||||
};
|
||||
83
webapp/components/post_view/components/post_image.jsx
Обычный файл
83
webapp/components/post_view/components/post_image.jsx
Обычный файл
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostImageEmbed extends React.Component {
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
handleLoadError() {
|
||||
this.setState({
|
||||
errored: true,
|
||||
loaded: true
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.errored) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.state.loaded) {
|
||||
return (
|
||||
<img
|
||||
className='img-div placeholder'
|
||||
height='500px'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className='img-div'
|
||||
src={this.props.link}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostImageEmbed.propTypes = {
|
||||
link: React.PropTypes.string.isRequired
|
||||
};
|
||||
257
webapp/components/post_view/components/post_info.jsx
Обычный файл
257
webapp/components/post_view/components/post_info.jsx
Обычный файл
@@ -0,0 +1,257 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import TimeSince from 'components/time_since.jsx';
|
||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostInfo extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.dropdownPosition = this.dropdownPosition.bind(this);
|
||||
this.handlePermalink = this.handlePermalink.bind(this);
|
||||
this.removePost = this.removePost.bind(this);
|
||||
}
|
||||
dropdownPosition(e) {
|
||||
var position = $('#post-list').height() - $(e.target).offset().top;
|
||||
var dropdown = $(e.target).closest('.col__reply').find('.dropdown-menu');
|
||||
if (position < dropdown.height()) {
|
||||
dropdown.addClass('bottom');
|
||||
}
|
||||
}
|
||||
createDropdown() {
|
||||
var post = this.props.post;
|
||||
var isOwner = this.props.currentUser.id === post.user_id;
|
||||
var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
|
||||
const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
|
||||
|
||||
if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || Utils.isPostEphemeral(post)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var type = 'Post';
|
||||
if (post.root_id && post.root_id.length > 0) {
|
||||
type = 'Comment';
|
||||
}
|
||||
|
||||
var dropdownContents = [];
|
||||
var dataComments = 0;
|
||||
if (type === 'Post') {
|
||||
dataComments = this.props.commentCount;
|
||||
}
|
||||
|
||||
if (this.props.allowReply === 'true') {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='replyLink'
|
||||
role='presentation'
|
||||
>
|
||||
<a
|
||||
className='link__reply theme'
|
||||
href='#'
|
||||
onClick={this.props.handleCommentClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='post_info.reply'
|
||||
defaultMessage='Reply'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
if (!Utils.isMobile()) {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='copyLink'
|
||||
role='presentation'
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.handlePermalink}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='post_info.permalink'
|
||||
defaultMessage='Permalink'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
if (isOwner || isAdmin) {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='deletePost'
|
||||
role='presentation'
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
role='menuitem'
|
||||
onClick={() => GlobalActions.showDeletePostModal(post, dataComments)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='post_info.del'
|
||||
defaultMessage='Delete'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
if (isOwner && !isSystemMessage) {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='editPost'
|
||||
role='presentation'
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
role='menuitem'
|
||||
data-toggle='modal'
|
||||
data-target='#edit_post'
|
||||
data-refocusid='#post_textbox'
|
||||
data-title={type}
|
||||
data-message={post.message}
|
||||
data-postid={post.id}
|
||||
data-channelid={post.channel_id}
|
||||
data-comments={dataComments}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='post_info.edit'
|
||||
defaultMessage='Edit'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
if (dropdownContents.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<a
|
||||
href='#'
|
||||
className='dropdown-toggle post__dropdown theme'
|
||||
type='button'
|
||||
data-toggle='dropdown'
|
||||
aria-expanded='false'
|
||||
onClick={this.dropdownPosition}
|
||||
/>
|
||||
<ul
|
||||
className='dropdown-menu'
|
||||
role='menu'
|
||||
>
|
||||
{dropdownContents}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handlePermalink(e) {
|
||||
e.preventDefault();
|
||||
GlobalActions.showGetPostLinkModal(this.props.post);
|
||||
}
|
||||
|
||||
removePost() {
|
||||
GlobalActions.emitRemovePost(this.props.post);
|
||||
}
|
||||
createRemovePostButton(post) {
|
||||
if (!Utils.isPostEphemeral(post)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href='#'
|
||||
className='post__remove theme'
|
||||
type='button'
|
||||
onClick={this.removePost}
|
||||
>
|
||||
{'×'}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
var post = this.props.post;
|
||||
var comments = '';
|
||||
var showCommentClass = '';
|
||||
var commentCountText = this.props.commentCount;
|
||||
|
||||
if (this.props.commentCount >= 1) {
|
||||
showCommentClass = ' icon--show';
|
||||
} else {
|
||||
commentCountText = '';
|
||||
}
|
||||
|
||||
if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && !Utils.isPostEphemeral(post)) {
|
||||
comments = (
|
||||
<a
|
||||
href='#'
|
||||
className={'comment-icon__container' + showCommentClass}
|
||||
onClick={this.props.handleCommentClick}
|
||||
>
|
||||
<span
|
||||
className='comment-icon'
|
||||
dangerouslySetInnerHTML={{__html: Constants.REPLY_ICON}}
|
||||
/>
|
||||
{commentCountText}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
var dropdown = this.createDropdown();
|
||||
|
||||
return (
|
||||
<ul className='post__header--info'>
|
||||
<li className='col'>
|
||||
<TimeSince
|
||||
eventTime={post.create_at}
|
||||
sameUser={this.props.sameUser}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
/>
|
||||
</li>
|
||||
<li className='col col__reply'>
|
||||
<div
|
||||
className='dropdown'
|
||||
ref='dotMenu'
|
||||
>
|
||||
{dropdown}
|
||||
</div>
|
||||
{comments}
|
||||
{this.createRemovePostButton(post)}
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostInfo.defaultProps = {
|
||||
post: null,
|
||||
commentCount: 0,
|
||||
isLastComment: false,
|
||||
allowReply: false,
|
||||
sameUser: false
|
||||
};
|
||||
PostInfo.propTypes = {
|
||||
post: React.PropTypes.object.isRequired,
|
||||
commentCount: React.PropTypes.number.isRequired,
|
||||
isLastComment: React.PropTypes.bool.isRequired,
|
||||
allowReply: React.PropTypes.string.isRequired,
|
||||
handleCommentClick: React.PropTypes.func.isRequired,
|
||||
sameUser: React.PropTypes.bool.isRequired,
|
||||
currentUser: React.PropTypes.object.isRequired,
|
||||
compactDisplay: React.PropTypes.bool
|
||||
};
|
||||
524
webapp/components/post_view/components/post_list.jsx
Обычный файл
524
webapp/components/post_view/components/post_list.jsx
Обычный файл
@@ -0,0 +1,524 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
|
||||
import Post from './post.jsx';
|
||||
import FloatingTimestamp from './floating_timestamp.jsx';
|
||||
import ScrollToBottomArrows from './scroll_to_bottom_arrows.jsx';
|
||||
|
||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||
|
||||
import {createChannelIntroMessage} from 'utils/channel_intro_messages.jsx';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as PostUtils from 'utils/post_utils.jsx';
|
||||
import DelayedAction from 'utils/delayed_action.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const ScrollTypes = Constants.ScrollTypes;
|
||||
|
||||
import {FormattedDate, FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
export default class PostList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
this.handleScrollStop = this.handleScrollStop.bind(this);
|
||||
this.isAtBottom = this.isAtBottom.bind(this);
|
||||
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
|
||||
this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
|
||||
this.createPosts = this.createPosts.bind(this);
|
||||
this.updateScrolling = this.updateScrolling.bind(this);
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
this.scrollToBottom = this.scrollToBottom.bind(this);
|
||||
this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
|
||||
|
||||
this.jumpToPostNode = null;
|
||||
this.wasAtBottom = true;
|
||||
this.scrollHeight = 0;
|
||||
|
||||
this.scrollStopAction = new DelayedAction(this.handleScrollStop);
|
||||
|
||||
if (props.channel) {
|
||||
this.introText = createChannelIntroMessage(props.channel);
|
||||
} else {
|
||||
this.introText = this.getArchivesIntroMessage();
|
||||
}
|
||||
|
||||
this.state = {
|
||||
isScrolling: false,
|
||||
topPostId: null
|
||||
};
|
||||
}
|
||||
|
||||
isAtBottom() {
|
||||
// consider the view to be at the bottom if it's within this many pixels of the bottom
|
||||
const atBottomMargin = 10;
|
||||
|
||||
return this.refs.postlist.clientHeight + this.refs.postlist.scrollTop >= this.refs.postlist.scrollHeight - atBottomMargin;
|
||||
}
|
||||
|
||||
handleScroll() {
|
||||
// HACK FOR RHS -- REMOVE WHEN RHS DIES
|
||||
const childNodes = this.refs.postlistcontent.childNodes;
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
// If the node is 1/3 down the page
|
||||
if (childNodes[i].offsetTop > (this.refs.postlist.scrollTop + (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION))) {
|
||||
this.jumpToPostNode = childNodes[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.wasAtBottom = this.isAtBottom();
|
||||
if (!this.jumpToPostNode && childNodes.length > 0) {
|
||||
this.jumpToPostNode = childNodes[childNodes.length - 1];
|
||||
}
|
||||
|
||||
// --- --------
|
||||
|
||||
this.props.postListScrolled(this.isAtBottom());
|
||||
this.prevScrollHeight = this.refs.postlist.scrollHeight;
|
||||
this.prevOffsetTop = this.jumpToPostNode.offsetTop;
|
||||
|
||||
this.updateFloatingTimestamp();
|
||||
|
||||
if (!this.state.isScrolling) {
|
||||
this.setState({
|
||||
isScrolling: true
|
||||
});
|
||||
}
|
||||
|
||||
this.scrollStopAction.fireAfter(Constants.SCROLL_DELAY);
|
||||
}
|
||||
|
||||
handleScrollStop() {
|
||||
this.setState({
|
||||
isScrolling: false
|
||||
});
|
||||
}
|
||||
|
||||
updateFloatingTimestamp() {
|
||||
// skip this in non-mobile view since that's when the timestamp is visible
|
||||
if (!Utils.isMobile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.postList) {
|
||||
// iterate through posts starting at the bottom since users are more likely to be viewing newer posts
|
||||
for (let i = 0; i < this.props.postList.order.length; i++) {
|
||||
const id = this.props.postList.order[i];
|
||||
const element = this.refs[id];
|
||||
|
||||
if (!element || element.offsetTop + element.clientHeight <= this.refs.postlist.scrollTop) {
|
||||
// this post is off the top of the screen so the last one is at the top of the screen
|
||||
let topPostId;
|
||||
|
||||
if (i > 0) {
|
||||
topPostId = this.props.postList.order[i - 1];
|
||||
} else {
|
||||
// the first post we look at should always be on the screen, but handle that case anyway
|
||||
topPostId = id;
|
||||
}
|
||||
|
||||
if (topPostId !== this.state.topPostId) {
|
||||
this.setState({
|
||||
topPostId
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadMorePostsTop() {
|
||||
GlobalActions.emitLoadMorePostsEvent();
|
||||
}
|
||||
|
||||
loadMorePostsBottom() {
|
||||
GlobalActions.emitLoadMorePostsFocusedBottomEvent();
|
||||
}
|
||||
|
||||
createPosts(posts, order) {
|
||||
const postCtls = [];
|
||||
let previousPostDay = new Date(0);
|
||||
const userId = this.props.currentUser.id;
|
||||
const profiles = this.props.profiles || {};
|
||||
|
||||
let renderedLastViewed = false;
|
||||
|
||||
for (let i = order.length - 1; i >= 0; i--) {
|
||||
const post = posts[order[i]];
|
||||
const parentPost = posts[post.parent_id];
|
||||
const prevPost = posts[order[i + 1]];
|
||||
const postUserId = PostUtils.isSystemMessage(post) ? '' : post.user_id;
|
||||
|
||||
// If the post is a comment whose parent has been deleted, don't add it to the list.
|
||||
if (parentPost && parentPost.state === Constants.POST_DELETED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sameUser = false;
|
||||
let sameRoot = false;
|
||||
let hideProfilePic = false;
|
||||
|
||||
if (prevPost) {
|
||||
const postIsComment = PostUtils.isComment(post);
|
||||
const prevPostIsComment = PostUtils.isComment(prevPost);
|
||||
const postFromWebhook = Boolean(post.props && post.props.from_webhook);
|
||||
const prevPostFromWebhook = Boolean(prevPost.props && prevPost.props.from_webhook);
|
||||
const prevPostUserId = PostUtils.isSystemMessage(prevPost) ? '' : prevPost.user_id;
|
||||
|
||||
// consider posts from the same user if:
|
||||
// the previous post was made by the same user as the current post,
|
||||
// the previous post was made within 5 minutes of the current post,
|
||||
// the current post is not from a webhook
|
||||
// the previous post is not from a webhook
|
||||
if (prevPostUserId === postUserId &&
|
||||
post.create_at - prevPost.create_at <= Constants.POST_COLLAPSE_TIMEOUT &&
|
||||
!postFromWebhook && !prevPostFromWebhook) {
|
||||
sameUser = true;
|
||||
}
|
||||
|
||||
// consider posts from the same root if:
|
||||
// the current post is a comment,
|
||||
// the current post has the same root as the previous post
|
||||
if (postIsComment && (prevPost.id === post.root_id || prevPost.root_id === post.root_id)) {
|
||||
sameRoot = true;
|
||||
}
|
||||
|
||||
// consider posts from the same root if:
|
||||
// the current post is not a comment,
|
||||
// the previous post is not a comment,
|
||||
// the previous post is from the same user
|
||||
if (!postIsComment && !prevPostIsComment && sameUser) {
|
||||
sameRoot = true;
|
||||
}
|
||||
|
||||
// hide the profile pic if:
|
||||
// the previous post was made by the same user as the current post,
|
||||
// the previous post is not a comment,
|
||||
// the current post is not a comment,
|
||||
// the previous post is not from a webhook
|
||||
// the current post is not from a webhook
|
||||
if (prevPostUserId === postUserId &&
|
||||
!prevPostIsComment &&
|
||||
!postIsComment &&
|
||||
!prevPostFromWebhook &&
|
||||
!postFromWebhook) {
|
||||
hideProfilePic = true;
|
||||
}
|
||||
}
|
||||
|
||||
// check if it's the last comment in a consecutive string of comments on the same post
|
||||
// it is the last comment if it is last post in the channel or the next post has a different root post
|
||||
const isLastComment = PostUtils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
|
||||
|
||||
const keyPrefix = post.id ? post.id : i;
|
||||
|
||||
const shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id);
|
||||
|
||||
let profile;
|
||||
if (userId === post.user_id) {
|
||||
profile = this.props.currentUser;
|
||||
} else {
|
||||
profile = profiles[post.user_id];
|
||||
}
|
||||
|
||||
let commentCount = 0;
|
||||
let commentRootId;
|
||||
if (parentPost) {
|
||||
commentRootId = post.root_id;
|
||||
} else {
|
||||
commentRootId = post.id;
|
||||
}
|
||||
for (const postId in posts) {
|
||||
if (posts[postId].root_id === commentRootId) {
|
||||
commentCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const postCtl = (
|
||||
<Post
|
||||
key={keyPrefix + 'postKey'}
|
||||
ref={post.id}
|
||||
sameUser={sameUser}
|
||||
sameRoot={sameRoot}
|
||||
post={post}
|
||||
parentPost={parentPost}
|
||||
hideProfilePic={hideProfilePic}
|
||||
isLastComment={isLastComment}
|
||||
shouldHighlight={shouldHighlight}
|
||||
displayNameType={this.props.displayNameType}
|
||||
user={profile}
|
||||
currentUser={this.props.currentUser}
|
||||
center={this.props.displayPostsInCenter}
|
||||
commentCount={commentCount}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
/>
|
||||
);
|
||||
|
||||
const currentPostDay = Utils.getDateForUnixTicks(post.create_at);
|
||||
if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
|
||||
postCtls.push(
|
||||
<div
|
||||
key={currentPostDay.toDateString()}
|
||||
className='date-separator'
|
||||
>
|
||||
<hr className='separator__hr'/>
|
||||
<div className='separator__text'>
|
||||
<FormattedDate
|
||||
value={currentPostDay}
|
||||
weekday='short'
|
||||
month='short'
|
||||
day='2-digit'
|
||||
year='numeric'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (postUserId !== userId &&
|
||||
this.props.lastViewed !== 0 &&
|
||||
post.create_at > this.props.lastViewed &&
|
||||
!renderedLastViewed) {
|
||||
renderedLastViewed = true;
|
||||
|
||||
// Temporary fix to solve ie11 rendering issue
|
||||
let newSeparatorId = '';
|
||||
if (!Utils.isBrowserIE()) {
|
||||
newSeparatorId = 'new_message_' + post.id;
|
||||
}
|
||||
postCtls.push(
|
||||
<div
|
||||
id={newSeparatorId}
|
||||
key='unviewed'
|
||||
ref='newMessageSeparator'
|
||||
className='new-separator'
|
||||
>
|
||||
<hr
|
||||
className='separator__hr'
|
||||
/>
|
||||
<div className='separator__text'>
|
||||
<FormattedMessage
|
||||
id='posts_view.newMsg'
|
||||
defaultMessage='New Messages'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
postCtls.push(postCtl);
|
||||
previousPostDay = currentPostDay;
|
||||
}
|
||||
|
||||
return postCtls;
|
||||
}
|
||||
|
||||
updateScrolling() {
|
||||
if (this.props.scrollType === ScrollTypes.BOTTOM) {
|
||||
this.scrollToBottom();
|
||||
} else if (this.props.scrollType === ScrollTypes.NEW_MESSAGE) {
|
||||
window.setTimeout(window.requestAnimationFrame(() => {
|
||||
// If separator exists scroll to it. Otherwise scroll to bottom.
|
||||
if (this.refs.newMessageSeparator) {
|
||||
var objDiv = this.refs.postlist;
|
||||
objDiv.scrollTop = this.refs.newMessageSeparator.offsetTop; //scrolls node to top of Div
|
||||
} else if (this.refs.postlist) {
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||
}
|
||||
}), 0);
|
||||
} else if (this.props.scrollType === ScrollTypes.POST && this.props.scrollPostId) {
|
||||
window.requestAnimationFrame(() => {
|
||||
const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]);
|
||||
if (postNode == null) {
|
||||
return;
|
||||
}
|
||||
postNode.scrollIntoView();
|
||||
if (this.refs.postlist.scrollTop === postNode.offsetTop) {
|
||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION);
|
||||
} else {
|
||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION) + (this.refs.postlist.scrollTop - postNode.offsetTop);
|
||||
}
|
||||
});
|
||||
} else if (this.props.scrollType === ScrollTypes.SIDEBAR_OPEN) {
|
||||
// If we are at the bottom then stay there
|
||||
if (this.wasAtBottom) {
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||
} else {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.jumpToPostNode.scrollIntoView();
|
||||
if (this.refs.postlist.scrollTop === this.jumpToPostNode.offsetTop) {
|
||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION);
|
||||
} else {
|
||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION) + (this.refs.postlist.scrollTop - this.jumpToPostNode.offsetTop);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (this.refs.postlist.scrollHeight !== this.prevScrollHeight) {
|
||||
window.requestAnimationFrame(() => {
|
||||
// Only need to jump if we added posts to the top.
|
||||
if (this.jumpToPostNode && (this.jumpToPostNode.offsetTop !== this.prevOffsetTop)) {
|
||||
this.refs.postlist.scrollTop += (this.refs.postlist.scrollHeight - this.prevScrollHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.updateScrolling();
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||
});
|
||||
}
|
||||
|
||||
scrollToBottomAnimated() {
|
||||
var postList = $(this.refs.postlist);
|
||||
postList.animate({scrollTop: this.refs.postlist.scrollHeight}, '500');
|
||||
}
|
||||
|
||||
getArchivesIntroMessage() {
|
||||
return (
|
||||
<div className='channel-intro'>
|
||||
<h4 className='channel-intro__title'>
|
||||
<FormattedMessage
|
||||
id='post_focus_view.beginning'
|
||||
defaultMessage='Beginning of Channel Archives'
|
||||
/>
|
||||
</h4>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.postList != null) {
|
||||
this.updateScrolling();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
this.scrollStopAction.cancel();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.postList != null) {
|
||||
this.updateScrolling();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.postList == null) {
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
const posts = this.props.postList.posts;
|
||||
const order = this.props.postList.order;
|
||||
|
||||
// Create intro message or top loadmore link
|
||||
let moreMessagesTop;
|
||||
if (this.props.showMoreMessagesTop) {
|
||||
moreMessagesTop = (
|
||||
<a
|
||||
ref='loadmoretop'
|
||||
className='more-messages-text theme'
|
||||
href='#'
|
||||
onClick={this.loadMorePostsTop}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='posts_view.loadMore'
|
||||
defaultMessage='Load more messages'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
moreMessagesTop = this.introText;
|
||||
}
|
||||
|
||||
// Give option to load more posts at bottom if necessary
|
||||
let moreMessagesBottom;
|
||||
if (this.props.showMoreMessagesBottom) {
|
||||
moreMessagesBottom = (
|
||||
<a
|
||||
ref='loadmorebottom'
|
||||
className='more-messages-text theme'
|
||||
href='#'
|
||||
onClick={this.loadMorePostsBottom}
|
||||
>
|
||||
<FormattedMessage id='posts_view.loadMore'/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// Create post elements
|
||||
const postElements = this.createPosts(posts, order);
|
||||
|
||||
let topPostCreateAt = 0;
|
||||
if (this.state.topPostId && this.props.postList.posts[this.state.topPostId]) {
|
||||
topPostCreateAt = this.props.postList.posts[this.state.topPostId].create_at;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FloatingTimestamp
|
||||
isScrolling={this.state.isScrolling}
|
||||
isMobile={Utils.isMobile()}
|
||||
createAt={topPostCreateAt}
|
||||
/>
|
||||
<ScrollToBottomArrows
|
||||
isScrolling={this.state.isScrolling}
|
||||
atBottom={this.wasAtBottom}
|
||||
onClick={this.scrollToBottomAnimated}
|
||||
/>
|
||||
<div
|
||||
ref='postlist'
|
||||
className='post-list-holder-by-time'
|
||||
onScroll={this.handleScroll}
|
||||
>
|
||||
<div className='post-list__table'>
|
||||
<div
|
||||
ref='postlistcontent'
|
||||
className='post-list__content'
|
||||
>
|
||||
{moreMessagesTop}
|
||||
{postElements}
|
||||
{moreMessagesBottom}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostList.defaultProps = {
|
||||
lastViewed: 0
|
||||
};
|
||||
|
||||
PostList.propTypes = {
|
||||
postList: React.PropTypes.object,
|
||||
profiles: React.PropTypes.object,
|
||||
channel: React.PropTypes.object,
|
||||
currentUser: React.PropTypes.object,
|
||||
scrollPostId: React.PropTypes.string,
|
||||
scrollType: React.PropTypes.number,
|
||||
postListScrolled: React.PropTypes.func.isRequired,
|
||||
showMoreMessagesTop: React.PropTypes.bool,
|
||||
showMoreMessagesBottom: React.PropTypes.bool,
|
||||
lastViewed: React.PropTypes.number,
|
||||
postsToHighlight: React.PropTypes.object,
|
||||
displayNameType: React.PropTypes.string,
|
||||
displayPostsInCenter: React.PropTypes.bool,
|
||||
compactDisplay: React.PropTypes.bool
|
||||
};
|
||||
376
webapp/components/post_view/components/providers.json
Обычный файл
376
webapp/components/post_view/components/providers.json
Обычный файл
@@ -0,0 +1,376 @@
|
||||
[
|
||||
{
|
||||
"patterns": [
|
||||
"http://(?:www\\.)?xkcd\\.com/\\d+/?"
|
||||
],
|
||||
"name": "XKCD",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://soundcloud.com/.*/.*"
|
||||
],
|
||||
"name": "SoundCloud",
|
||||
"height": 140
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://(?:www\\.)?flickr\\.com/.*",
|
||||
"https?://flic\\.kr/p/[a-zA-Z0-9]+"
|
||||
],
|
||||
"name": "Flickr",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://www\\.ted\\.com/talks/.+\\.html"
|
||||
],
|
||||
"name": "TED",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://(?:www\\.)?theverge\\.com/\\d{4}/\\d{1,2}/\\d{1,2}/\\d+/[^/]+/?$"
|
||||
],
|
||||
"name": "The Verge",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://.*\\.viddler\\.com/.*"
|
||||
],
|
||||
"name": "Viddler",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://(?:www\\.)?avclub\\.com/article/[^/]+/?$"
|
||||
],
|
||||
"name": "The AV Club",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://(?:www\\.)?wired\\.com/([^/]+/)?\\d+/\\d+/[^/]+/?$"
|
||||
],
|
||||
"name": "Wired",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://www\\.theonion\\.com/articles/[^/]+/?"
|
||||
],
|
||||
"name": "The Onion",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://yfrog\\.com/[0-9a-zA-Z]+/?$"
|
||||
],
|
||||
"name": "YFrog",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://www\\.duffelblog\\.com/\\d{4}/\\d{1,2}/[^/]+/?$"
|
||||
],
|
||||
"name": "The Duffel Blog",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://www\\.clickhole\\.com/article/[^/]+/?"
|
||||
],
|
||||
"name": "Clickhole",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://(?:www.)?skitch.com/([^/]+)/[^/]+/.+",
|
||||
"http://skit.ch/[^/]+"
|
||||
],
|
||||
"name": "Skitch",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://(alpha|posts|photos)\\.app\\.net/.*"
|
||||
],
|
||||
"name": "ADN",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://gist\\.github\\.com/(?:[-0-9a-zA-Z]+/)?([0-9a-fA-f]+)"
|
||||
],
|
||||
"name": "Gist",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://www\\.(dropbox\\.com/s/.+\\.(?:jpg|png|gif))",
|
||||
"https?://db\\.tt/[a-zA-Z0-9]+"
|
||||
],
|
||||
"name": "Dropbox",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://[^\\.]+\\.wikipedia\\.org/wiki/(?!Talk:)[^#]+(?:#(.+))?"
|
||||
],
|
||||
"name": "Wikipedia",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://www.traileraddict.com/trailer/[^/]+/trailer"
|
||||
],
|
||||
"name": "TrailerAddict",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://lockerz\\.com/[sd]/\\d+"
|
||||
],
|
||||
"name": "Lockerz",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://gifuk\\.com/s/[0-9a-f]{16}"
|
||||
],
|
||||
"name": "GIFUK",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://trailers\\.apple\\.com/trailers/[^/]+/[^/]+"
|
||||
],
|
||||
"name": "iTunes Movie Trailers",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://gfycat\\.com/([a-zA-Z]+)"
|
||||
],
|
||||
"name": "Gfycat",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://bash\\.org/\\?(\\d+)"
|
||||
],
|
||||
"name": "Bash.org",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://arstechnica\\.com/[^/]+/\\d+/\\d+/[^/]+/?$"
|
||||
],
|
||||
"name": "Ars Technica",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://imgur\\.com/gallery/[0-9a-zA-Z]+"
|
||||
],
|
||||
"name": "Imgur",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://www\\.asciiartfarts\\.com/[0-9]+\\.html"
|
||||
],
|
||||
"name": "ASCII Art Farts",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://www\\.monoprice\\.com/products/product\\.asp\\?.*p_id=\\d+"
|
||||
],
|
||||
"name": "Monoprice",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://boingboing\\.net/\\d{4}/\\d{2}/\\d{2}/[^/]+\\.html"
|
||||
],
|
||||
"name": "Boing Boing",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://github\\.com/([^/]+)/([^/]+)/commit/(.+)",
|
||||
"http://git\\.io/[_0-9a-zA-Z]+"
|
||||
],
|
||||
"name": "Github Commit",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://open\\.spotify\\.com/(track|album)/([0-9a-zA-Z]{22})"
|
||||
],
|
||||
"name": "Spotify",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://path\\.com/p/([0-9a-zA-Z]+)$"
|
||||
],
|
||||
"name": "Path",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://www.funnyordie.com/videos/[^/]+/.+"
|
||||
],
|
||||
"name": "Funny or Die",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://(?:www\\.)?twitpic\\.com/([^/]+)"
|
||||
],
|
||||
"name": "Twitpic",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://www\\.giantbomb\\.com/videos/[^/]+/\\d+-\\d+/?"
|
||||
],
|
||||
"name": "GiantBomb",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://(?:www\\.)?beeradvocate\\.com/beer/profile/\\d+/\\d+"
|
||||
],
|
||||
"name": "Beer Advocate",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://(?:www\\.)?imdb.com/title/(tt\\d+)"
|
||||
],
|
||||
"name": "IMDB",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://cl\\.ly/(?:image/)?[0-9a-zA-Z]+/?$"
|
||||
],
|
||||
"name": "CloudApp",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://clyp\\.it/.*"
|
||||
],
|
||||
"name": "Clyp",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://www\\.hulu\\.com/watch/.*"
|
||||
],
|
||||
"name": "Hulu",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://(?:www|mobile\\.)?twitter\\.com/(?:#!/)?[^/]+/status(?:es)?/(\\d+)/?$",
|
||||
"https?://t\\.co/[a-zA-Z0-9]+"
|
||||
],
|
||||
"name": "Twitter",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://(?:www\\.)?vimeo\\.com/.+"
|
||||
],
|
||||
"name": "Vimeo",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://www\\.amazon\\.com/(?:.+/)?[gd]p/(?:product/)?(?:tags-on-product/)?([a-zA-Z0-9]+)",
|
||||
"http://amzn\\.com/([^/]+)"
|
||||
],
|
||||
"name": "Amazon",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://qik\\.com/video/.*"
|
||||
],
|
||||
"name": "Qik",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://www\\.rdio\\.com/artist/[^/]+/album/[^/]+/?",
|
||||
"http://www\\.rdio\\.com/artist/[^/]+/album/[^/]+/track/[^/]+/?",
|
||||
"http://www\\.rdio\\.com/people/[^/]+/playlists/\\d+/[^/]+"
|
||||
],
|
||||
"name": "Rdio",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://www\\.slideshare\\.net/.*/.*"
|
||||
],
|
||||
"name": "SlideShare",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://imgur\\.com/([0-9a-zA-Z]+)$"
|
||||
],
|
||||
"name": "Imgur",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://instagr(?:\\.am|am\\.com)/p/.+"
|
||||
],
|
||||
"name": "Instagram",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://www\\.twitlonger\\.com/show/[a-zA-Z0-9]+",
|
||||
"http://tl\\.gd/[^/]+"
|
||||
],
|
||||
"name": "Twitlonger",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://vine.co/v/[a-zA-Z0-9]+"
|
||||
],
|
||||
"name": "Vine",
|
||||
"height": 490
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://www\\.urbandictionary\\.com/define\\.php\\?term=.+"
|
||||
],
|
||||
"name": "Urban Dictionary",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"http://picplz\\.com/user/[^/]+/pic/[^/]+"
|
||||
],
|
||||
"name": "Picplz",
|
||||
"height": 110
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"https?://(?:www\\.)?twitter\\.com/(?:#!/)?[^/]+/status(?:es)?/(\\d+)/photo/\\d+(?:/large|/)?$",
|
||||
"https?://pic\\.twitter\\.com/.+"
|
||||
],
|
||||
"name": "Twitter",
|
||||
"height": 110
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class ScrollToBottomArrows extends React.Component {
|
||||
render() {
|
||||
// only show on mobile
|
||||
if ($(window).width() > 768) {
|
||||
return <noscript/>;
|
||||
}
|
||||
|
||||
let className = 'post-list__arrows';
|
||||
if (this.props.isScrolling && !this.props.atBottom) {
|
||||
className += ' scrolling';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{__html: Constants.SCROLL_BOTTOM_ICON}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ScrollToBottomArrows.propTypes = {
|
||||
isScrolling: React.PropTypes.bool.isRequired,
|
||||
atBottom: React.PropTypes.bool.isRequired,
|
||||
onClick: React.PropTypes.func.isRequired
|
||||
};
|
||||
128
webapp/components/post_view/post_focus_view_controller.jsx
Обычный файл
128
webapp/components/post_view/post_focus_view_controller.jsx
Обычный файл
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostList from './components/post_list.jsx';
|
||||
import LoadingScreen from 'components/loading_screen.jsx';
|
||||
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const ScrollTypes = Constants.ScrollTypes;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostFocusView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChannelChange = this.onChannelChange.bind(this);
|
||||
this.onPostsChange = this.onPostsChange.bind(this);
|
||||
this.onUserChange = this.onUserChange.bind(this);
|
||||
this.onPostListScroll = this.onPostListScroll.bind(this);
|
||||
|
||||
const focusedPostId = PostStore.getFocusedPostId();
|
||||
|
||||
const channel = ChannelStore.getCurrent();
|
||||
let profiles = UserStore.getProfiles();
|
||||
if (channel && channel.type === Constants.DM_CHANNEL) {
|
||||
profiles = Object.assign({}, profiles, UserStore.getDirectProfiles());
|
||||
}
|
||||
|
||||
this.state = {
|
||||
postList: PostStore.getVisiblePosts(focusedPostId),
|
||||
currentUser: UserStore.getCurrentUser(),
|
||||
profiles,
|
||||
scrollType: ScrollTypes.POST,
|
||||
currentChannel: ChannelStore.getCurrentId().slice(),
|
||||
scrollPostId: focusedPostId,
|
||||
atTop: PostStore.getVisibilityAtTop(focusedPostId),
|
||||
atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onChannelChange);
|
||||
PostStore.addChangeListener(this.onPostsChange);
|
||||
UserStore.addChangeListener(this.onUserChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
||||
PostStore.removeChangeListener(this.onPostsChange);
|
||||
UserStore.removeChangeListener(this.onUserChange);
|
||||
}
|
||||
|
||||
onChannelChange() {
|
||||
const currentChannel = ChannelStore.getCurrentId();
|
||||
if (this.state.currentChannel !== currentChannel) {
|
||||
this.setState({
|
||||
currentChannel: currentChannel.slice(),
|
||||
scrollType: ScrollTypes.POST
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onPostsChange() {
|
||||
const focusedPostId = PostStore.getFocusedPostId();
|
||||
if (focusedPostId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
scrollPostId: focusedPostId,
|
||||
postList: PostStore.getVisiblePosts(focusedPostId),
|
||||
atTop: PostStore.getVisibilityAtTop(focusedPostId),
|
||||
atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
|
||||
});
|
||||
}
|
||||
|
||||
onUserChange() {
|
||||
const channel = ChannelStore.getCurrent();
|
||||
let profiles = UserStore.getProfiles();
|
||||
if (channel && channel.type === Constants.DM_CHANNEL) {
|
||||
profiles = Object.assign({}, profiles, UserStore.getDirectProfiles());
|
||||
}
|
||||
this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(profiles))});
|
||||
}
|
||||
|
||||
onPostListScroll() {
|
||||
this.setState({scrollType: ScrollTypes.FREE});
|
||||
}
|
||||
|
||||
render() {
|
||||
const postsToHighlight = {};
|
||||
postsToHighlight[this.state.scrollPostId] = true;
|
||||
|
||||
let content;
|
||||
if (this.state.postList == null) {
|
||||
content = (
|
||||
<LoadingScreen
|
||||
position='absolute'
|
||||
key='loading'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<PostList
|
||||
postList={this.state.postList}
|
||||
currentUser={this.state.currentUser}
|
||||
profiles={this.state.profiles}
|
||||
scrollType={this.state.scrollType}
|
||||
scrollPostId={this.state.scrollPostId}
|
||||
postListScrolled={this.onPostListScroll}
|
||||
showMoreMessagesTop={!this.state.atTop}
|
||||
showMoreMessagesBottom={!this.state.atBottom}
|
||||
postsToHighlight={postsToHighlight}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='post-list'>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
85
webapp/components/post_view/post_view_cache.jsx
Обычный файл
85
webapp/components/post_view/post_view_cache.jsx
Обычный файл
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information
|
||||
|
||||
import PostViewController from './post_view_controller.jsx';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const MAXIMUM_CACHED_VIEWS = 5;
|
||||
|
||||
export default class PostViewCache extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChannelChange = this.onChannelChange.bind(this);
|
||||
|
||||
const channel = ChannelStore.getCurrent();
|
||||
|
||||
this.state = {
|
||||
currentChannelId: channel.id,
|
||||
channels: [channel]
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onChannelChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
||||
}
|
||||
|
||||
onChannelChange() {
|
||||
const channels = Object.assign([], this.state.channels);
|
||||
const currentChannel = ChannelStore.getCurrent();
|
||||
|
||||
if (currentChannel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure current channel really changed
|
||||
if (currentChannel.id === this.state.currentChannelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (channels.length > MAXIMUM_CACHED_VIEWS) {
|
||||
channels.shift();
|
||||
}
|
||||
|
||||
const index = channels.map((c) => c.id).indexOf(currentChannel.id);
|
||||
if (index !== -1) {
|
||||
channels.splice(index, 1);
|
||||
}
|
||||
|
||||
channels.push(currentChannel);
|
||||
|
||||
this.setState({
|
||||
currentChannelId: currentChannel.id,
|
||||
channels
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const channels = this.state.channels;
|
||||
const currentChannelId = this.state.currentChannelId;
|
||||
|
||||
let postViews = [];
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
postViews.push(
|
||||
<PostViewController
|
||||
key={'postviewcontroller_' + channels[i].id}
|
||||
channel={channels[i]}
|
||||
active={channels[i].id === currentChannelId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='post-list'>
|
||||
{postViews}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
264
webapp/components/post_view/post_view_controller.jsx
Обычный файл
264
webapp/components/post_view/post_view_controller.jsx
Обычный файл
@@ -0,0 +1,264 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostList from './components/post_list.jsx';
|
||||
import LoadingScreen from 'components/loading_screen.jsx';
|
||||
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const Preferences = Constants.Preferences;
|
||||
const ScrollTypes = Constants.ScrollTypes;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostViewController extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||
this.onUserChange = this.onUserChange.bind(this);
|
||||
this.onPostsChange = this.onPostsChange.bind(this);
|
||||
this.onPostsViewJumpRequest = this.onPostsViewJumpRequest.bind(this);
|
||||
this.onPostListScroll = this.onPostListScroll.bind(this);
|
||||
this.onActivate = this.onActivate.bind(this);
|
||||
this.onDeactivate = this.onDeactivate.bind(this);
|
||||
|
||||
const channel = props.channel;
|
||||
let profiles = UserStore.getProfiles();
|
||||
if (channel && channel.type === Constants.DM_CHANNEL) {
|
||||
profiles = Object.assign({}, profiles, UserStore.getDirectProfiles());
|
||||
}
|
||||
|
||||
let lastViewed = Number.MAX_VALUE;
|
||||
const member = ChannelStore.getMember(channel.id);
|
||||
if (member != null) {
|
||||
lastViewed = member.last_viewed_at;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
channel,
|
||||
postList: PostStore.getVisiblePosts(channel.id),
|
||||
currentUser: UserStore.getCurrentUser(),
|
||||
profiles,
|
||||
atTop: PostStore.getVisibilityAtTop(channel.id),
|
||||
lastViewed,
|
||||
scrollType: ScrollTypes.BOTTOM,
|
||||
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
||||
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
||||
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.active) {
|
||||
this.onActivate();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.active) {
|
||||
this.onDeactivate();
|
||||
}
|
||||
}
|
||||
|
||||
onPreferenceChange() {
|
||||
this.setState({
|
||||
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
||||
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
||||
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT
|
||||
});
|
||||
}
|
||||
|
||||
onUserChange() {
|
||||
const channel = this.state.channel;
|
||||
let profiles = UserStore.getProfiles();
|
||||
if (channel && channel.type === Constants.DM_CHANNEL) {
|
||||
profiles = Object.assign({}, profiles, UserStore.getDirectProfiles());
|
||||
}
|
||||
this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(profiles))});
|
||||
}
|
||||
|
||||
onPostsChange() {
|
||||
this.setState({
|
||||
postList: JSON.parse(JSON.stringify(PostStore.getVisiblePosts(this.state.channel.id))),
|
||||
atTop: PostStore.getVisibilityAtTop(this.state.channel.id)
|
||||
});
|
||||
}
|
||||
|
||||
onActivate() {
|
||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
||||
UserStore.addChangeListener(this.onUserChange);
|
||||
PostStore.addChangeListener(this.onPostsChange);
|
||||
PostStore.addPostsViewJumpListener(this.onPostsViewJumpRequest);
|
||||
}
|
||||
|
||||
onDeactivate() {
|
||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
||||
UserStore.removeChangeListener(this.onUserChange);
|
||||
PostStore.removeChangeListener(this.onPostsChange);
|
||||
PostStore.removePostsViewJumpListener(this.onPostsViewJumpRequest);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.active && !nextProps.active) {
|
||||
this.onDeactivate();
|
||||
} else if (!this.props.active && nextProps.active) {
|
||||
this.onActivate();
|
||||
|
||||
const channel = nextProps.channel;
|
||||
|
||||
let lastViewed = Number.MAX_VALUE;
|
||||
const member = ChannelStore.getMember(channel.id);
|
||||
if (member != null) {
|
||||
lastViewed = member.last_viewed_at;
|
||||
}
|
||||
|
||||
let profiles = UserStore.getProfiles();
|
||||
if (channel && channel.type === Constants.DM_CHANNEL) {
|
||||
profiles = Object.assign({}, profiles, UserStore.getDirectProfiles());
|
||||
}
|
||||
|
||||
this.setState({
|
||||
channel,
|
||||
lastViewed,
|
||||
profiles: JSON.parse(JSON.stringify(profiles)),
|
||||
postList: JSON.parse(JSON.stringify(PostStore.getVisiblePosts(channel.id))),
|
||||
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
||||
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
||||
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
|
||||
scrollType: ScrollTypes.BOTTOM
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onPostsViewJumpRequest(type, postId) {
|
||||
switch (type) {
|
||||
case Constants.PostsViewJumpTypes.BOTTOM:
|
||||
this.setState({scrollType: ScrollTypes.BOTTOM});
|
||||
break;
|
||||
case Constants.PostsViewJumpTypes.POST:
|
||||
this.setState({
|
||||
scrollType: ScrollTypes.POST,
|
||||
scrollPostId: postId
|
||||
});
|
||||
break;
|
||||
case Constants.PostsViewJumpTypes.SIDEBAR_OPEN:
|
||||
this.setState({scrollType: ScrollTypes.SIDEBAR_OPEN});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onPostListScroll(atBottom) {
|
||||
if (atBottom) {
|
||||
this.setState({scrollType: ScrollTypes.BOTTOM});
|
||||
} else {
|
||||
this.setState({scrollType: ScrollTypes.FREE});
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (nextProps.active !== this.props.active) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.atTop !== this.state.atTop) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.displayNameType !== this.state.displayNameType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.displayPostsInCenter !== this.state.displayPostsInCenter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.compactDisplay !== this.state.compactDisplay) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.lastViewed !== this.state.lastViewed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.showMoreMessagesTop !== this.state.showMoreMessagesTop) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.scrollType !== this.state.scrollType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.scrollPostId !== this.state.scrollPostId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.channel.id !== this.props.channel.id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextState.currentUser, this.state.currentUser)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextState.postList, this.state.postList)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextState.profiles, this.state.profiles)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
let content;
|
||||
if (this.state.postList == null) {
|
||||
content = (
|
||||
<LoadingScreen
|
||||
position='absolute'
|
||||
key='loading'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<PostList
|
||||
postList={this.state.postList}
|
||||
profiles={this.state.profiles}
|
||||
channel={this.state.channel}
|
||||
currentUser={this.state.currentUser}
|
||||
showMoreMessagesTop={!this.state.atTop}
|
||||
scrollType={this.state.scrollType}
|
||||
scrollPostId={this.state.scrollPostId}
|
||||
postListScrolled={this.onPostListScroll}
|
||||
displayNameType={this.state.displayNameType}
|
||||
displayPostsInCenter={this.state.displayPostsInCenter}
|
||||
compactDisplay={this.state.compactDisplay}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let activeClass = '';
|
||||
if (!this.props.active) {
|
||||
activeClass = 'inactive';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={activeClass}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostViewController.propTypes = {
|
||||
channel: React.PropTypes.object,
|
||||
active: React.PropTypes.bool
|
||||
};
|
||||
Ссылка в новой задаче
Block a user