Merge pull request #1327 from florianorben/PLT-857-2
PLT-857: Support `attachments` for Incoming Webhooks
Этот коммит содержится в:
14
api/post.go
14
api/post.go
@@ -147,7 +147,7 @@ func CreatePost(c *Context, post *model.Post, triggerWebhooks bool) (*model.Post
|
||||
return rpost, nil
|
||||
}
|
||||
|
||||
func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIconUrl string) (*model.Post, *model.AppError) {
|
||||
func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIconUrl string, props model.StringInterface, postType string) (*model.Post, *model.AppError) {
|
||||
// parse links into Markdown format
|
||||
linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
|
||||
text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
|
||||
@@ -155,7 +155,7 @@ func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIc
|
||||
linkRegex := regexp.MustCompile(`<\s*(\S*)\s*>`)
|
||||
text = linkRegex.ReplaceAllString(text, "${1}")
|
||||
|
||||
post := &model.Post{UserId: c.Session.UserId, ChannelId: channelId, Message: text}
|
||||
post := &model.Post{UserId: c.Session.UserId, ChannelId: channelId, Message: text, Type: postType}
|
||||
post.AddProp("from_webhook", "true")
|
||||
|
||||
if utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
|
||||
@@ -174,6 +174,14 @@ func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIc
|
||||
}
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
for key, val := range props {
|
||||
if key != "override_icon_url" && key != "override_username" && key != "from_webhook" {
|
||||
post.AddProp(key, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := CreatePost(c, post, false); err != nil {
|
||||
return nil, model.NewAppError("CreateWebhookPost", "Error creating post", "err="+err.Message)
|
||||
}
|
||||
@@ -286,7 +294,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team
|
||||
newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, 0}
|
||||
|
||||
if text, ok := respProps["text"]; ok {
|
||||
if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"]); err != nil {
|
||||
if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil {
|
||||
l4g.Error("Failed to create response post, err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,13 @@ type IncomingWebhook struct {
|
||||
}
|
||||
|
||||
type IncomingWebhookRequest struct {
|
||||
Text string `json:"text"`
|
||||
Username string `json:"username"`
|
||||
IconURL string `json:"icon_url"`
|
||||
ChannelName string `json:"channel"`
|
||||
Text string `json:"text"`
|
||||
Username string `json:"username"`
|
||||
IconURL string `json:"icon_url"`
|
||||
ChannelName string `json:"channel"`
|
||||
Props StringInterface `json:"props"`
|
||||
Attachments interface{} `json:"attachments"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (o *IncomingWebhook) ToJson() string {
|
||||
|
||||
@@ -10,27 +10,28 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
POST_DEFAULT = ""
|
||||
POST_JOIN_LEAVE = "join_leave"
|
||||
POST_DEFAULT = ""
|
||||
POST_SLACK_ATTACHMENT = "slack_attachment"
|
||||
POST_JOIN_LEAVE = "join_leave"
|
||||
)
|
||||
|
||||
type Post struct {
|
||||
Id string `json:"id"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
DeleteAt int64 `json:"delete_at"`
|
||||
UserId string `json:"user_id"`
|
||||
ChannelId string `json:"channel_id"`
|
||||
RootId string `json:"root_id"`
|
||||
ParentId string `json:"parent_id"`
|
||||
OriginalId string `json:"original_id"`
|
||||
Message string `json:"message"`
|
||||
ImgCount int64 `json:"img_count"`
|
||||
Type string `json:"type"`
|
||||
Props StringMap `json:"props"`
|
||||
Hashtags string `json:"hashtags"`
|
||||
Filenames StringArray `json:"filenames"`
|
||||
PendingPostId string `json:"pending_post_id" db:"-"`
|
||||
Id string `json:"id"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
DeleteAt int64 `json:"delete_at"`
|
||||
UserId string `json:"user_id"`
|
||||
ChannelId string `json:"channel_id"`
|
||||
RootId string `json:"root_id"`
|
||||
ParentId string `json:"parent_id"`
|
||||
OriginalId string `json:"original_id"`
|
||||
Message string `json:"message"`
|
||||
ImgCount int64 `json:"img_count"`
|
||||
Type string `json:"type"`
|
||||
Props StringInterface `json:"props"`
|
||||
Hashtags string `json:"hashtags"`
|
||||
Filenames StringArray `json:"filenames"`
|
||||
PendingPostId string `json:"pending_post_id" db:"-"`
|
||||
}
|
||||
|
||||
func (o *Post) ToJson() string {
|
||||
@@ -103,7 +104,8 @@ func (o *Post) IsValid() *AppError {
|
||||
return NewAppError("Post.IsValid", "Invalid hashtags", "id="+o.Id)
|
||||
}
|
||||
|
||||
if !(o.Type == POST_DEFAULT || o.Type == POST_JOIN_LEAVE) {
|
||||
// should be removed once more message types are supported
|
||||
if !(o.Type == POST_DEFAULT || o.Type == POST_JOIN_LEAVE || o.Type == POST_SLACK_ATTACHMENT) {
|
||||
return NewAppError("Post.IsValid", "Invalid type", "id="+o.Type)
|
||||
}
|
||||
|
||||
@@ -128,7 +130,7 @@ func (o *Post) PreSave() {
|
||||
o.UpdateAt = o.CreateAt
|
||||
|
||||
if o.Props == nil {
|
||||
o.Props = make(map[string]string)
|
||||
o.Props = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if o.Filenames == nil {
|
||||
@@ -138,14 +140,14 @@ func (o *Post) PreSave() {
|
||||
|
||||
func (o *Post) MakeNonNil() {
|
||||
if o.Props == nil {
|
||||
o.Props = make(map[string]string)
|
||||
o.Props = make(map[string]interface{})
|
||||
}
|
||||
if o.Filenames == nil {
|
||||
o.Filenames = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Post) AddProp(key string, value string) {
|
||||
func (o *Post) AddProp(key string, value interface{}) {
|
||||
|
||||
o.MakeNonNil()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type StringInterface map[string]interface{}
|
||||
type StringMap map[string]string
|
||||
type StringArray []string
|
||||
type EncryptStringMap map[string]string
|
||||
@@ -125,6 +126,25 @@ func ArrayFromJson(data io.Reader) []string {
|
||||
}
|
||||
}
|
||||
|
||||
func StringInterfaceToJson(objmap map[string]interface{}) string {
|
||||
if b, err := json.Marshal(objmap); err != nil {
|
||||
return ""
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func StringInterfaceFromJson(data io.Reader) map[string]interface{} {
|
||||
decoder := json.NewDecoder(data)
|
||||
|
||||
var objmap map[string]interface{}
|
||||
if err := decoder.Decode(&objmap); err != nil {
|
||||
return make(map[string]interface{})
|
||||
} else {
|
||||
return objmap
|
||||
}
|
||||
}
|
||||
|
||||
func IsLower(s string) bool {
|
||||
if strings.ToLower(s) == s {
|
||||
return true
|
||||
|
||||
@@ -9,8 +9,10 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
l4g "code.google.com/p/log4go"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SqlPostStore struct {
|
||||
@@ -30,7 +32,7 @@ func NewSqlPostStore(sqlStore *SqlStore) PostStore {
|
||||
table.ColMap("Message").SetMaxSize(4000)
|
||||
table.ColMap("Type").SetMaxSize(26)
|
||||
table.ColMap("Hashtags").SetMaxSize(1000)
|
||||
table.ColMap("Props").SetMaxSize(4000)
|
||||
table.ColMap("Props")
|
||||
table.ColMap("Filenames").SetMaxSize(4000)
|
||||
}
|
||||
|
||||
@@ -38,6 +40,21 @@ func NewSqlPostStore(sqlStore *SqlStore) PostStore {
|
||||
}
|
||||
|
||||
func (s SqlPostStore) UpgradeSchemaIfNeeded() {
|
||||
colType := s.GetColumnDataType("Posts", "Props")
|
||||
if colType != "text" {
|
||||
|
||||
query := "ALTER TABLE Posts MODIFY COLUMN Props TEXT"
|
||||
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
|
||||
query = "ALTER TABLE Posts ALTER COLUMN Props TYPE text"
|
||||
}
|
||||
|
||||
_, err := s.GetMaster().Exec(query)
|
||||
if err != nil {
|
||||
l4g.Critical("Failed to alter column Posts.Props to TEXT: " + err.Error())
|
||||
time.Sleep(time.Second)
|
||||
panic("Failed to alter column Posts.Props to TEXT: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s SqlPostStore) CreateIndexesIfNotExists() {
|
||||
|
||||
@@ -455,6 +455,20 @@ func IsUniqueConstraintError(err string, mysql string, postgres string) bool {
|
||||
return unique && field
|
||||
}
|
||||
|
||||
func (ss SqlStore) GetColumnDataType(tableName, columnName string) string {
|
||||
dataType, err := ss.GetMaster().SelectStr("SELECT data_type FROM INFORMATION_SCHEMA.COLUMNS where table_name = :Tablename AND column_name = :Columnname", map[string]interface{}{
|
||||
"Tablename": tableName,
|
||||
"Columnname": columnName,
|
||||
})
|
||||
if err != nil {
|
||||
l4g.Critical("Failed to get data type for column %s from table %s: %v", columnName, tableName, err.Error())
|
||||
time.Sleep(time.Second)
|
||||
panic("Failed to get get data type for column " + columnName + " from table " + tableName + ": " + err.Error())
|
||||
}
|
||||
|
||||
return dataType
|
||||
}
|
||||
|
||||
func (ss SqlStore) GetMaster() *gorp.DbMap {
|
||||
return ss.master
|
||||
}
|
||||
@@ -529,6 +543,8 @@ func (me mattermConverter) ToDb(val interface{}) (interface{}, error) {
|
||||
return model.ArrayToJson(t), nil
|
||||
case model.EncryptStringMap:
|
||||
return encrypt([]byte(utils.Cfg.SqlSettings.AtRestEncryptKey), model.MapToJson(t))
|
||||
case model.StringInterface:
|
||||
return model.StringInterfaceToJson(t), nil
|
||||
}
|
||||
|
||||
return val, nil
|
||||
@@ -572,6 +588,16 @@ func (me mattermConverter) FromDb(target interface{}) (gorp.CustomScanner, bool)
|
||||
return json.Unmarshal(b, target)
|
||||
}
|
||||
return gorp.CustomScanner{new(string), target, binder}, true
|
||||
case *model.StringInterface:
|
||||
binder := func(holder, target interface{}) error {
|
||||
s, ok := holder.(*string)
|
||||
if !ok {
|
||||
return errors.New("FromDb: Unable to convert StringInterface to *string")
|
||||
}
|
||||
b := []byte(*s)
|
||||
return json.Unmarshal(b, target)
|
||||
}
|
||||
return gorp.CustomScanner{new(string), target, binder}, true
|
||||
}
|
||||
|
||||
return gorp.CustomScanner{}, false
|
||||
|
||||
295
web/react/components/post_attachment.jsx
Обычный файл
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
|
||||
};
|
||||
32
web/react/components/post_attachment_list.jsx
Обычный файл
32
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
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
56
web/react/components/post_body_additional_content.jsx
Обычный файл
56
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
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
web/web.go
12
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
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user