From d3ed791ab52eaddb7690869bb1cd982453badd95 Mon Sep 17 00:00:00 2001 From: Florian Orben Date: Sat, 31 Oct 2015 03:22:02 +0100 Subject: attachments frontend --- web/react/components/create_comment.jsx | 1 + web/react/components/create_post.jsx | 1 + web/react/components/post_attachment.jsx | 240 ++++++++++++++++++++++++++ web/react/components/post_attachment_list.jsx | 32 ++++ web/react/components/post_body.jsx | 11 ++ web/react/utils/utils.jsx | 3 + web/sass-files/sass/partials/_post.scss | 71 ++++++++ 7 files changed, 359 insertions(+) create mode 100644 web/react/components/post_attachment.jsx create mode 100644 web/react/components/post_attachment_list.jsx diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 058594165..6fb473ae6 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -109,6 +109,7 @@ export default class CreateComment extends React.Component { post.pending_post_id = `${userId}:${time}`; post.user_id = userId; post.create_at = time; + post.attachments = []; PostStore.storePendingPost(post); PostStore.storeCommentDraft(this.props.rootId, null); diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 5a69c9bfb..90ba47718 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -173,6 +173,7 @@ export default class CreatePost extends React.Component { post.create_at = time; post.root_id = this.state.rootId; post.parent_id = this.state.parentId; + post.attachments = []; const channel = ChannelStore.get(this.state.channelId); diff --git a/web/react/components/post_attachment.jsx b/web/react/components/post_attachment.jsx new file mode 100644 index 000000000..6bc9ade47 --- /dev/null +++ b/web/react/components/post_attachment.jsx @@ -0,0 +1,240 @@ +// 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); + } + + 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( + + {field.title} + + ); + bodyCols.push( + + + ); + }); + + tHead = ( + + {headerCols} + + ); + tBody = ( + + {bodyCols} + + ); + } else { + tBody = []; + + fields.forEach((field, i) => { + tBody.push( + + + {field.title} + + + + + ); + }); + } + + return ( + + + {tHead} + + + {tBody} + +
+ ); + } + + render() { + const data = this.props.attachment; + + let preText; + if (data.pretext) { + preText = ( +
+
+ ); + } + + let author = []; + if (data.author_name || data.author_icon) { + if (data.author_icon) { + author.push( + + ); + } + if (data.author_name) { + author.push( + + {data.author_name} + + ); + } + } + if (data.author_link) { + author = ( + + {author} + + ); + } + + let title; + if (data.title) { + if (data.title_link) { + title = ( +

+ + {data.title} + +

+ ); + } else { + title = ( +

+ {data.title} +

+ ); + } + } + + let text; + if (data.text) { + text = ( +
+
+ ); + } + + let image; + if (data.image_url) { + image = ( + + ); + } + + let thumb; + if (data.thumb_url) { + thumb = ( +
+ +
+ ); + } + + const fields = this.getFieldsTable(); + + let useBorderStyle; + if (data.color && data.color[0] === '#') { + useBorderStyle = {borderLeftColor: data.color}; + } + + return ( +
+ {preText} +
+
+ {author} + {title} +
+
+ {text} + {image} + {fields} +
+ {thumb} +
+
+
+
+
+ ); + } +} + +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( + + ); + }); + + return ( +
+ {content} +
+ ); + } +} + +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..4da13dace 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 PostAttachmentList = require('./post_attachment_list.jsx'); export default class PostBody extends React.Component { constructor(props) { @@ -316,6 +317,15 @@ export default class PostBody extends React.Component { ); } + let postAttachments = ''; + if (post.attachments && post.attachments.length) { + postAttachments = ( + + ); + } + return (
{comment} @@ -331,6 +341,7 @@ export default class PostBody extends React.Component { dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}} />
+ {postAttachments} {fileAttachmentHolder} {embed} diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 9ce7ca1e8..f006956bc 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..dd88e6e16 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -610,3 +610,74 @@ 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__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%; + } + .attachment__thumb-container { + width: 20%; + float: right; + img { + height: 75px; + max-width: 100%; + } + } + .attachment___fields { + width: 100%; + .attachment___field-caption { + font-weight: 700; + } + } +} \ No newline at end of file -- cgit v1.2.3-1-g7c22 From 4b6eb56415c2085bc9078836b70b833b1e01a60d Mon Sep 17 00:00:00 2001 From: Florian Orben Date: Sat, 31 Oct 2015 04:46:35 +0100 Subject: collapse text after 700 chars or 5 line breaks --- web/react/components/post_attachment.jsx | 59 ++++++++++++++++++++++++++++++-- web/sass-files/sass/partials/_post.scss | 3 ++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/web/react/components/post_attachment.jsx b/web/react/components/post_attachment.jsx index 6bc9ade47..2d6b47f03 100644 --- a/web/react/components/post_attachment.jsx +++ b/web/react/components/post_attachment.jsx @@ -8,6 +8,60 @@ export default class PostAttachment extends React.Component { 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 ? '▲ collapse text' : ''); + 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) + '▼ read more'; } getFieldsTable() { @@ -169,7 +223,7 @@ export default class PostAttachment extends React.Component { text = (
); @@ -208,11 +262,12 @@ export default class PostAttachment extends React.Component { return (
{preText}
{author} diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index dd88e6e16..db03a1578 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -645,6 +645,9 @@ body.ios { width: 100%; } } + .attachment__text p:last-of-type { + display: inline-block; + } .attachment__thumb-pretext { margin-left: 5px; } -- cgit v1.2.3-1-g7c22 From b085bc2d56bdc98101b8cb50848aee248d42af28 Mon Sep 17 00:00:00 2001 From: Florian Orben Date: Thu, 5 Nov 2015 23:32:44 +0100 Subject: PLT-857: Support for Incoming Webhooks - Try #2 --- api/post.go | 14 ++++-- model/incoming_webhook.go | 11 +++-- model/post.go | 46 +++++++++--------- model/utils.go | 20 ++++++++ store/sql_post_store.go | 11 +++++ store/sql_store.go | 35 ++++++++++++++ web/react/components/post_body.jsx | 15 ++---- .../components/post_body_additional_content.jsx | 56 ++++++++++++++++++++++ web/sass-files/sass/partials/_post.scss | 4 ++ web/web.go | 11 ++++- 10 files changed, 182 insertions(+), 41 deletions(-) create mode 100644 web/react/components/post_body_additional_content.jsx diff --git a/api/post.go b/api/post.go index b52db8752..3892d4ee8 100644 --- a/api/post.go +++ b/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) } } diff --git a/model/incoming_webhook.go b/model/incoming_webhook.go index be1984244..8ead0da9f 100644 --- a/model/incoming_webhook.go +++ b/model/incoming_webhook.go @@ -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 { diff --git a/model/post.go b/model/post.go index e0074b348..248d40321 100644 --- a/model/post.go +++ b/model/post.go @@ -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() diff --git a/model/utils.go b/model/utils.go index 681ade870..1e71836c1 100644 --- a/model/utils.go +++ b/model/utils.go @@ -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 diff --git a/store/sql_post_store.go b/store/sql_post_store.go index fdae20f60..61cd109a1 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -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 { @@ -38,6 +40,15 @@ func NewSqlPostStore(sqlStore *SqlStore) PostStore { } func (s SqlPostStore) UpgradeSchemaIfNeeded() { + col := s.GetColumnInformation("Posts", "Props") + if col.Type != "text" { + _, err := s.GetMaster().Exec("ALTER TABLE Posts MODIFY COLUMN Props TEXT") + 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() { diff --git a/store/sql_store.go b/store/sql_store.go index e5c540e06..d2cca21d0 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -50,6 +50,15 @@ type SqlStore struct { preference PreferenceStore } +type Column struct { + Field string + Type string + Null string + Key interface{} + Default interface{} + Extra interface{} +} + func NewSqlStore() Store { sqlStore := &SqlStore{} @@ -455,6 +464,20 @@ func IsUniqueConstraintError(err string, mysql string, postgres string) bool { return unique && field } +func (ss SqlStore) GetColumnInformation(tableName, columnName string) Column { + var col Column + err := ss.GetMaster().SelectOne(&col, "SHOW COLUMNS FROM "+tableName+" WHERE Field = :Columnname", map[string]interface{}{ + "Columnname": columnName, + }) + if err != nil { + l4g.Critical("Failed to get information for column %s from table %s: %v", tableName, columnName, err.Error()) + time.Sleep(time.Second) + panic("Failed to get information for column " + tableName + " from table " + columnName + ": " + err.Error()) + } + + return col +} + func (ss SqlStore) GetMaster() *gorp.DbMap { return ss.master } @@ -529,6 +552,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 +597,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 diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 4da13dace..5a157b792 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -7,7 +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 PostAttachmentList = require('./post_attachment_list.jsx'); +const PostBodyAdditionalContent = require('./post_body_additional_content.jsx'); export default class PostBody extends React.Component { constructor(props) { @@ -317,15 +317,6 @@ export default class PostBody extends React.Component { ); } - let postAttachments = ''; - if (post.attachments && post.attachments.length) { - postAttachments = ( - - ); - } - return (
{comment} @@ -341,7 +332,9 @@ export default class PostBody extends React.Component { dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}} />
- {postAttachments} + {fileAttachmentHolder} {embed}
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 ( + + ); + } + + 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 ( +
+ {content} +
+ ); + } +} + +PostBodyAdditionalContent.propTypes = { + post: React.PropTypes.object.isRequired +}; \ No newline at end of file diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index db03a1578..b57c51242 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -668,6 +668,7 @@ body.ios { } .attachment__image { max-width: 100%; + margin-bottom: 1em; } .attachment__thumb-container { width: 20%; @@ -682,5 +683,8 @@ body.ios { .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 51f6664b6..bd0154542 100644 --- a/web/web.go +++ b/web/web.go @@ -990,6 +990,15 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { } channelName := parsedRequest.ChannelName + webhookType := parsedRequest.Type + + 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 { @@ -1039,7 +1048,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 } -- cgit v1.2.3-1-g7c22 From e00836f8df00670e8c02daad7e02a13283ddb92f Mon Sep 17 00:00:00 2001 From: Florian Orben Date: Thu, 5 Nov 2015 23:55:31 +0100 Subject: postgres needs another query to get table information... --- store/sql_post_store.go | 4 ++-- store/sql_store.go | 21 ++++++--------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/store/sql_post_store.go b/store/sql_post_store.go index 61cd109a1..7978408a2 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -40,8 +40,8 @@ func NewSqlPostStore(sqlStore *SqlStore) PostStore { } func (s SqlPostStore) UpgradeSchemaIfNeeded() { - col := s.GetColumnInformation("Posts", "Props") - if col.Type != "text" { + colType := s.GetColumnDataType("Posts", "Props") + if colType != "text" { _, err := s.GetMaster().Exec("ALTER TABLE Posts MODIFY COLUMN Props TEXT") if err != nil { l4g.Critical("Failed to alter column Posts.Props to TEXT: " + err.Error()) diff --git a/store/sql_store.go b/store/sql_store.go index d2cca21d0..697c89589 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -50,15 +50,6 @@ type SqlStore struct { preference PreferenceStore } -type Column struct { - Field string - Type string - Null string - Key interface{} - Default interface{} - Extra interface{} -} - func NewSqlStore() Store { sqlStore := &SqlStore{} @@ -464,18 +455,18 @@ func IsUniqueConstraintError(err string, mysql string, postgres string) bool { return unique && field } -func (ss SqlStore) GetColumnInformation(tableName, columnName string) Column { - var col Column - err := ss.GetMaster().SelectOne(&col, "SHOW COLUMNS FROM "+tableName+" WHERE Field = :Columnname", map[string]interface{}{ +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 information for column %s from table %s: %v", tableName, columnName, err.Error()) + l4g.Critical("Failed to get data type for column %s from table %s: %v", tableName, columnName, err.Error()) time.Sleep(time.Second) - panic("Failed to get information for column " + tableName + " from table " + columnName + ": " + err.Error()) + panic("Failed to get get data type for column " + tableName + " from table " + columnName + ": " + err.Error()) } - return col + return dataType } func (ss SqlStore) GetMaster() *gorp.DbMap { -- cgit v1.2.3-1-g7c22 From 4dedb14467ee79f21cdaa8db696c4cac0fb120de Mon Sep 17 00:00:00 2001 From: Florian Orben Date: Thu, 5 Nov 2015 23:57:01 +0100 Subject: remove two unused assignments --- web/react/components/create_comment.jsx | 1 - web/react/components/create_post.jsx | 1 - 2 files changed, 2 deletions(-) diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 6fb473ae6..058594165 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -109,7 +109,6 @@ export default class CreateComment extends React.Component { post.pending_post_id = `${userId}:${time}`; post.user_id = userId; post.create_at = time; - post.attachments = []; PostStore.storePendingPost(post); PostStore.storeCommentDraft(this.props.rootId, null); diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 90ba47718..5a69c9bfb 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -173,7 +173,6 @@ export default class CreatePost extends React.Component { post.create_at = time; post.root_id = this.state.rootId; post.parent_id = this.state.parentId; - post.attachments = []; const channel = ChannelStore.get(this.state.channelId); -- cgit v1.2.3-1-g7c22 From 17a17c336c6650148388f1d8430f97bd17098409 Mon Sep 17 00:00:00 2001 From: Florian Orben Date: Fri, 6 Nov 2015 00:00:11 +0100 Subject: fix logging statement not to post nonsense --- store/sql_store.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/store/sql_store.go b/store/sql_store.go index 697c89589..f348db10b 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -461,9 +461,9 @@ func (ss SqlStore) GetColumnDataType(tableName, columnName string) string { "Columnname": columnName, }) if err != nil { - l4g.Critical("Failed to get data type for column %s from table %s: %v", tableName, columnName, err.Error()) + 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 " + tableName + " from table " + columnName + ": " + err.Error()) + panic("Failed to get get data type for column " + columnName + " from table " + tableName + ": " + err.Error()) } return dataType -- cgit v1.2.3-1-g7c22 From 7e3b53c37be8d9959fbffb23b6c6eb965386c5fd Mon Sep 17 00:00:00 2001 From: Florian Orben Date: Fri, 6 Nov 2015 00:07:44 +0100 Subject: fix alter table command for postgres --- store/sql_post_store.go | 8 +++++++- web/web.go | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/store/sql_post_store.go b/store/sql_post_store.go index 7978408a2..a523b3320 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -42,7 +42,13 @@ func NewSqlPostStore(sqlStore *SqlStore) PostStore { func (s SqlPostStore) UpgradeSchemaIfNeeded() { colType := s.GetColumnDataType("Posts", "Props") if colType != "text" { - _, err := s.GetMaster().Exec("ALTER TABLE Posts MODIFY COLUMN Props 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) diff --git a/web/web.go b/web/web.go index bd0154542..1b2f5f742 100644 --- a/web/web.go +++ b/web/web.go @@ -992,6 +992,7 @@ 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) -- cgit v1.2.3-1-g7c22 From 195728b949a4f3aee75e01f4b0e4a0b2f67850da Mon Sep 17 00:00:00 2001 From: Florian Orben Date: Fri, 6 Nov 2015 00:37:21 +0100 Subject: remove max column size from 'props' --- store/sql_post_store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/sql_post_store.go b/store/sql_post_store.go index a523b3320..3460fca92 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -32,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) } -- cgit v1.2.3-1-g7c22