diff options
author | Joram Wilander <jwawilander@gmail.com> | 2015-11-06 09:05:32 -0500 |
---|---|---|
committer | Joram Wilander <jwawilander@gmail.com> | 2015-11-06 09:05:32 -0500 |
commit | 1ecad4301e6e511a426da5884a54111c5bb7a4fd (patch) | |
tree | 114dea884f0a08729f5b6b5f9e45d2e470bedc71 /web | |
parent | 6417d4728dc9351d5bf3180e458be8ce6e1e642f (diff) | |
parent | 195728b949a4f3aee75e01f4b0e4a0b2f67850da (diff) | |
download | chat-1ecad4301e6e511a426da5884a54111c5bb7a4fd.tar.gz chat-1ecad4301e6e511a426da5884a54111c5bb7a4fd.tar.bz2 chat-1ecad4301e6e511a426da5884a54111c5bb7a4fd.zip |
Merge pull request #1327 from florianorben/PLT-857-2
PLT-857: Support `attachments` for Incoming Webhooks
Diffstat (limited to 'web')
-rw-r--r-- | web/react/components/post_attachment.jsx | 295 | ||||
-rw-r--r-- | web/react/components/post_attachment_list.jsx | 32 | ||||
-rw-r--r-- | web/react/components/post_body.jsx | 4 | ||||
-rw-r--r-- | web/react/components/post_body_additional_content.jsx | 56 | ||||
-rw-r--r-- | web/react/utils/utils.jsx | 3 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_post.scss | 78 | ||||
-rw-r--r-- | web/web.go | 12 |
7 files changed, 479 insertions, 1 deletions
diff --git a/web/react/components/post_attachment.jsx b/web/react/components/post_attachment.jsx new file mode 100644 index 000000000..2d6b47f03 --- /dev/null +++ b/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 +};
\ No newline at end of file diff --git a/web/react/components/post_attachment_list.jsx b/web/react/components/post_attachment_list.jsx new file mode 100644 index 000000000..03b866656 --- /dev/null +++ b/web/react/components/post_attachment_list.jsx @@ -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 +}; diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index e4094daf3..5a157b792 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -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> diff --git a/web/react/components/post_body_additional_content.jsx b/web/react/components/post_body_additional_content.jsx new file mode 100644 index 000000000..8189ba2d3 --- /dev/null +++ b/web/react/components/post_body_additional_content.jsx @@ -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 +};
\ No newline at end of file diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 5a3000dff..8052c000c 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -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); diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index ef19ac601..b57c51242 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -610,3 +610,81 @@ body.ios { font-weight: 600; margin: 0 0 0 -4px; } + +.attachment { + .attachment__content { + border-width: 1px; + border-style: solid; + border-radius: 4px; + padding: 2px 5px; + margin: 0 0 5px 0; + } + .attachment__thumb-pretext { + border: 0 none; + background: transparent; + } + .attachment__container { + border-left-width: 4px; + border-left-style: solid; + padding: 2px 0 2px 10px; + &.attachment__container--good { + border-left-color: #00C100; + } + &.attachment__container--warning { + border-left-color: #DEDE01; + } + &.attachment__container--danger { + border-left-color: #E40303; + } + } + .attachment__body { + float: left; + width: 80%; + padding-right: 5px; + &.attachment__body--no_thumb { + width: 100%; + } + } + .attachment__text p:last-of-type { + display: inline-block; + } + .attachment__thumb-pretext { + margin-left: 5px; + } + .attachment__title { + margin: 5px 0; + padding: 0; + line-height: 16px; + font-size: 16px; + a { + font-size: 16px; + } + } + .attachment__author-icon { + @include border-radius(50px); + margin-right: 5px; + width: 14px; + height: 14px; + } + .attachment__image { + max-width: 100%; + margin-bottom: 1em; + } + .attachment__thumb-container { + width: 20%; + float: right; + img { + height: 75px; + max-width: 100%; + } + } + .attachment___fields { + width: 100%; + .attachment___field-caption { + font-weight: 700; + } + .attachment___field p { + margin: 0; + } + } +}
\ No newline at end of file diff --git a/web/web.go b/web/web.go index 96f826359..1cae604ae 100644 --- a/web/web.go +++ b/web/web.go @@ -995,6 +995,16 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { } channelName := parsedRequest.ChannelName + webhookType := parsedRequest.Type + + //attachments is in here for slack compatibility + if parsedRequest.Attachments != nil { + if len(parsedRequest.Props) == 0 { + parsedRequest.Props = make(model.StringInterface) + } + parsedRequest.Props["attachments"] = parsedRequest.Attachments + webhookType = model.POST_SLACK_ATTACHMENT + } var hook *model.IncomingWebhook if result := <-hchan; result.Err != nil { @@ -1044,7 +1054,7 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { return } - if _, err := api.CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl); err != nil { + if _, err := api.CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl, parsedRequest.Props, webhookType); err != nil { c.Err = err return } |