summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/react/components/post_attachment.jsx295
-rw-r--r--web/react/components/post_attachment_list.jsx32
-rw-r--r--web/react/components/post_body.jsx4
-rw-r--r--web/react/components/post_body_additional_content.jsx56
-rw-r--r--web/react/stores/user_store.jsx9
-rw-r--r--web/react/utils/utils.jsx3
-rw-r--r--web/sass-files/sass/partials/_post.scss78
-rw-r--r--web/web.go12
8 files changed, 488 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/stores/user_store.jsx b/web/react/stores/user_store.jsx
index 4fa7224b7..6b7d671fc 100644
--- a/web/react/stores/user_store.jsx
+++ b/web/react/stores/user_store.jsx
@@ -58,6 +58,8 @@ class UserStoreClass extends EventEmitter {
this.setStatus = this.setStatus.bind(this);
this.getStatuses = this.getStatuses.bind(this);
this.getStatus = this.getStatus.bind(this);
+
+ this.profileCache = null;
}
emitChange(userId) {
@@ -184,6 +186,10 @@ class UserStoreClass extends EventEmitter {
}
getProfiles() {
+ if (this.profileCache !== null) {
+ return this.profileCache;
+ }
+
return BrowserStore.getItem('profiles', {});
}
@@ -218,6 +224,7 @@ class UserStoreClass extends EventEmitter {
saveProfile(profile) {
var ps = this.getProfiles();
ps[profile.id] = profile;
+ this.profileCache = ps;
BrowserStore.setItem('profiles', ps);
}
@@ -226,6 +233,8 @@ class UserStoreClass extends EventEmitter {
if (currentId in profiles) {
delete profiles[currentId];
}
+
+ this.profileCache = profiles;
BrowserStore.setItem('profiles', profiles);
}
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
}