Merge pull request #1327 from florianorben/PLT-857-2

PLT-857: Support `attachments` for Incoming Webhooks
Этот коммит содержится в:
Joram Wilander
2015-11-06 09:05:32 -05:00
родитель 6417d4728d 195728b949
Коммит 1ecad4301e
13 изменённых файлов: 585 добавлений и 31 удалений

295
web/react/components/post_attachment.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,295 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
const TextFormatting = require('../utils/text_formatting.jsx');
export default 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="#">▲ collapse text</a>' : '');
const collapsedText = shouldCollapse ? this.getCollapsedText() : text;
return {
shouldCollapse,
collapsedText,
uncollapsedText,
text: shouldCollapse ? collapsedText : uncollapsedText,
collapsed: shouldCollapse
};
}
toggleCollapseState(e) {
e.preventDefault();
let state = this.state;
state.text = state.collapsed ? state.uncollapsedText : state.collapsedText;
state.collapsed = !state.collapsed;
this.setState(state);
}
shouldCollapse() {
return (this.props.attachment.text.match(/\n/g) || []).length >= 5 || this.props.attachment.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="#">▼ read more</a>';
}
getFieldsTable() {
const fields = this.props.attachment.fields;
if (!fields || !fields.length) {
return '';
}
const compactTable = fields.filter((field) => field.short).length > 0;
let tHead;
let tBody;
if (compactTable) {
let headerCols = [];
let bodyCols = [];
fields.forEach((field, i) => {
headerCols.push(
<th
className='attachment___field-caption'
key={'attachment__field-caption-' + i}
>
{field.title}
</th>
);
bodyCols.push(
<td
className='attachment___field'
key={'attachment__field-' + i}
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(field.value || '')}}
>
</td>
);
});
tHead = (
<tr>
{headerCols}
</tr>
);
tBody = (
<tr>
{bodyCols}
</tr>
);
} else {
tBody = [];
fields.forEach((field, i) => {
tBody.push(
<tr key={'attachment__field-' + i}>
<td
className='attachment___field-caption'
>
{field.title}
</td>
<td
className='attachment___field'
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(field.value || '')}}
>
</td>
</tr>
);
});
}
return (
<table
className='attachment___fields'
>
<thead>
{tHead}
</thead>
<tbody>
{tBody}
</tbody>
</table>
);
}
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'
>
{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'
>
{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 = {
attachment: React.PropTypes.object.isRequired
};

Просмотреть файл

@@ -0,0 +1,32 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
const PostAttachment = require('./post_attachment.jsx');
export default class PostAttachmentList extends React.Component {
constructor(props) {
super(props);
}
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
};

Просмотреть файл

@@ -7,6 +7,7 @@ const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
const TextFormatting = require('../utils/text_formatting.jsx');
const twemoji = require('twemoji');
const PostBodyAdditionalContent = require('./post_body_additional_content.jsx');
export default class PostBody extends React.Component {
constructor(props) {
@@ -331,6 +332,9 @@ export default class PostBody extends React.Component {
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
/>
</div>
<PostBodyAdditionalContent
post={post}
/>
{fileAttachmentHolder}
{embed}
</div>

Просмотреть файл

@@ -0,0 +1,56 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
const PostAttachmentList = require('./post_attachment_list.jsx');
export default class PostBodyAdditionalContent extends React.Component {
constructor(props) {
super(props);
this.getSlackAttachment = this.getSlackAttachment.bind(this);
this.getComponent = this.getComponent.bind(this);
}
componentWillMount() {
this.setState({type: this.props.post.type, shouldRender: Boolean(this.props.post.type)});
}
getSlackAttachment() {
const attachments = this.props.post.props && this.props.post.props.attachments || [];
return (
<PostAttachmentList
key={'post_body_additional_content' + this.props.post.id}
attachments={attachments}
/>
);
}
getComponent() {
switch (this.state.type) {
case 'slack_attachment':
return this.getSlackAttachment();
}
}
render() {
let content = [];
if (this.state.shouldRender) {
const component = this.getComponent();
if (component) {
content = component;
}
}
return (
<div>
{content}
</div>
);
}
}
PostBodyAdditionalContent.propTypes = {
post: React.PropTypes.object.isRequired
};

Просмотреть файл

@@ -481,6 +481,7 @@ export function applyTheme(theme) {
changeCss('.modal .modal-header', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('#navbar .navbar-default', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('@media(max-width: 768px){.search-bar__container', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('.attachment .attachment__container', 'border-left-color:' + theme.sidebarHeaderBg, 1);
}
if (theme.sidebarHeaderTextColor) {
@@ -519,6 +520,7 @@ export function applyTheme(theme) {
changeCss('.popover.left>.arrow:after', 'border-left-color:' + theme.centerChannelBg, 1);
changeCss('.popover.top>.arrow:after, .tip-overlay.tip-overlay--chat .arrow', 'border-top-color:' + theme.centerChannelBg, 1);
changeCss('.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1);
changeCss('.attachment__content', 'background:' + theme.centerChannelBg, 1);
}
if (theme.centerChannelColor) {
@@ -552,6 +554,7 @@ export function applyTheme(theme) {
changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1);
changeCss('.input-group-addon, .search-bar__container .search__form, .form-control', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.attachment .attachment__content', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.channel-intro .channel-intro__content, .webhooks__container', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);