PLT-6403: Interactive messages (#7274)
* wip * finish first pass * requested changes * add DoPostAction to Client4
Этот коммит содержится в:
@@ -586,3 +586,14 @@ func (c *Context) RequireJobType() *Context {
|
|||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Context) RequireActionId() *Context {
|
||||||
|
if c.Err != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Params.ActionId) != 26 {
|
||||||
|
c.SetInvalidUrlParam("action_id")
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type ApiParams struct {
|
|||||||
Service string
|
Service string
|
||||||
JobId string
|
JobId string
|
||||||
JobType string
|
JobType string
|
||||||
|
ActionId string
|
||||||
Page int
|
Page int
|
||||||
PerPage int
|
PerPage int
|
||||||
Permanent bool
|
Permanent bool
|
||||||
@@ -137,6 +138,10 @@ func ApiParamsFromRequest(r *http.Request) *ApiParams {
|
|||||||
params.JobType = val
|
params.JobType = val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if val, ok := props["action_id"]; ok {
|
||||||
|
params.ActionId = val
|
||||||
|
}
|
||||||
|
|
||||||
if val, err := strconv.Atoi(r.URL.Query().Get("page")); err != nil || val < 0 {
|
if val, err := strconv.Atoi(r.URL.Query().Get("page")); err != nil || val < 0 {
|
||||||
params.Page = PAGE_DEFAULT
|
params.Page = PAGE_DEFAULT
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
20
api4/post.go
20
api4/post.go
@@ -27,6 +27,7 @@ func InitPost() {
|
|||||||
BaseRoutes.Team.Handle("/posts/search", ApiSessionRequired(searchPosts)).Methods("POST")
|
BaseRoutes.Team.Handle("/posts/search", ApiSessionRequired(searchPosts)).Methods("POST")
|
||||||
BaseRoutes.Post.Handle("", ApiSessionRequired(updatePost)).Methods("PUT")
|
BaseRoutes.Post.Handle("", ApiSessionRequired(updatePost)).Methods("PUT")
|
||||||
BaseRoutes.Post.Handle("/patch", ApiSessionRequired(patchPost)).Methods("PUT")
|
BaseRoutes.Post.Handle("/patch", ApiSessionRequired(patchPost)).Methods("PUT")
|
||||||
|
BaseRoutes.Post.Handle("/actions/{action_id:[A-Za-z0-9]+}", ApiSessionRequired(doPostAction)).Methods("POST")
|
||||||
BaseRoutes.Post.Handle("/pin", ApiSessionRequired(pinPost)).Methods("POST")
|
BaseRoutes.Post.Handle("/pin", ApiSessionRequired(pinPost)).Methods("POST")
|
||||||
BaseRoutes.Post.Handle("/unpin", ApiSessionRequired(unpinPost)).Methods("POST")
|
BaseRoutes.Post.Handle("/unpin", ApiSessionRequired(unpinPost)).Methods("POST")
|
||||||
}
|
}
|
||||||
@@ -428,3 +429,22 @@ func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(model.FileInfosToJson(infos)))
|
w.Write([]byte(model.FileInfosToJson(infos)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func doPostAction(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
c.RequirePostId().RequireActionId()
|
||||||
|
if c.Err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !app.SessionHasPermissionToChannelByPost(c.Session, c.Params.PostId, model.PERMISSION_READ_CHANNEL) {
|
||||||
|
c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.DoPostAction(c.Params.PostId, c.Params.ActionId, c.Session.UserId); err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ReturnStatusOK(w)
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func CreateCommandPost(post *model.Post, teamId string, response *model.CommandR
|
|||||||
}
|
}
|
||||||
|
|
||||||
post.ParentId = ""
|
post.ParentId = ""
|
||||||
SendEphemeralPost(teamId, post.UserId, post)
|
SendEphemeralPost(post.UserId, post)
|
||||||
}
|
}
|
||||||
|
|
||||||
return post, nil
|
return post, nil
|
||||||
|
|||||||
@@ -182,7 +182,6 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe
|
|||||||
if hereNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
|
if hereNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
|
||||||
hereNotification = false
|
hereNotification = false
|
||||||
SendEphemeralPost(
|
SendEphemeralPost(
|
||||||
team.Id,
|
|
||||||
post.UserId,
|
post.UserId,
|
||||||
&model.Post{
|
&model.Post{
|
||||||
ChannelId: post.ChannelId,
|
ChannelId: post.ChannelId,
|
||||||
@@ -195,7 +194,6 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe
|
|||||||
// If the channel has more than 1K users then @channel is disabled
|
// If the channel has more than 1K users then @channel is disabled
|
||||||
if channelNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
|
if channelNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
|
||||||
SendEphemeralPost(
|
SendEphemeralPost(
|
||||||
team.Id,
|
|
||||||
post.UserId,
|
post.UserId,
|
||||||
&model.Post{
|
&model.Post{
|
||||||
ChannelId: post.ChannelId,
|
ChannelId: post.ChannelId,
|
||||||
@@ -208,7 +206,6 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe
|
|||||||
// If the channel has more than 1K users then @all is disabled
|
// If the channel has more than 1K users then @all is disabled
|
||||||
if allNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
|
if allNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
|
||||||
SendEphemeralPost(
|
SendEphemeralPost(
|
||||||
team.Id,
|
|
||||||
post.UserId,
|
post.UserId,
|
||||||
&model.Post{
|
&model.Post{
|
||||||
ChannelId: post.ChannelId,
|
ChannelId: post.ChannelId,
|
||||||
@@ -734,7 +731,6 @@ func sendOutOfChannelMentions(sender *model.User, post *model.Post, teamId strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
SendEphemeralPost(
|
SendEphemeralPost(
|
||||||
teamId,
|
|
||||||
post.UserId,
|
post.UserId,
|
||||||
&model.Post{
|
&model.Post{
|
||||||
ChannelId: post.ChannelId,
|
ChannelId: post.ChannelId,
|
||||||
|
|||||||
67
app/post.go
67
app/post.go
@@ -4,8 +4,11 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
l4g "github.com/alecthomas/log4go"
|
l4g "github.com/alecthomas/log4go"
|
||||||
"github.com/dyatlov/go-opengraph/opengraph"
|
"github.com/dyatlov/go-opengraph/opengraph"
|
||||||
@@ -210,7 +213,7 @@ func parseSlackLinksToMarkdown(text string) string {
|
|||||||
return linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
|
return linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendEphemeralPost(teamId, userId string, post *model.Post) *model.Post {
|
func SendEphemeralPost(userId string, post *model.Post) *model.Post {
|
||||||
post.Type = model.POST_EPHEMERAL
|
post.Type = model.POST_EPHEMERAL
|
||||||
|
|
||||||
// fill in fields which haven't been specified which have sensible defaults
|
// fill in fields which haven't been specified which have sensible defaults
|
||||||
@@ -638,3 +641,65 @@ func GetOpenGraphMetadata(url string) *opengraph.OpenGraph {
|
|||||||
|
|
||||||
return og
|
return og
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DoPostAction(postId string, actionId string, userId string) *model.AppError {
|
||||||
|
pchan := Srv.Store.Post().GetSingle(postId)
|
||||||
|
|
||||||
|
var post *model.Post
|
||||||
|
if result := <-pchan; result.Err != nil {
|
||||||
|
return result.Err
|
||||||
|
} else {
|
||||||
|
post = result.Data.(*model.Post)
|
||||||
|
}
|
||||||
|
|
||||||
|
action := post.GetAction(actionId)
|
||||||
|
if action == nil || action.Integration == nil {
|
||||||
|
return model.NewAppError("DoPostAction", "api.post.do_action.action_id.app_error", nil, fmt.Sprintf("action=%v", action), http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
request := &model.PostActionIntegrationRequest{
|
||||||
|
UserId: userId,
|
||||||
|
Context: action.Integration.Context,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST", action.Integration.URL, strings.NewReader(request.ToJson()))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
resp, err := utils.HttpClient(false).Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, fmt.Sprintf("status=%v", resp.StatusCode), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response model.PostActionIntegrationResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
return model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Update != nil {
|
||||||
|
response.Update.Id = postId
|
||||||
|
response.Update.AddProp("from_webhook", "true")
|
||||||
|
if _, err := UpdatePost(response.Update, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.EphemeralText != "" {
|
||||||
|
ephemeralPost := &model.Post{}
|
||||||
|
ephemeralPost.Message = parseSlackLinksToMarkdown(response.EphemeralText)
|
||||||
|
ephemeralPost.ChannelId = post.ChannelId
|
||||||
|
ephemeralPost.RootId = post.RootId
|
||||||
|
if ephemeralPost.RootId == "" {
|
||||||
|
ephemeralPost.RootId = post.Id
|
||||||
|
}
|
||||||
|
ephemeralPost.UserId = userId
|
||||||
|
ephemeralPost.AddProp("from_webhook", "true")
|
||||||
|
SendEphemeralPost(userId, ephemeralPost)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,11 +4,18 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fmt"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/mattermost/platform/model"
|
"github.com/mattermost/platform/model"
|
||||||
|
"github.com/mattermost/platform/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUpdatePostEditAt(t *testing.T) {
|
func TestUpdatePostEditAt(t *testing.T) {
|
||||||
@@ -68,3 +75,66 @@ func TestPostReplyToPostWhereRootPosterLeftChannel(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPostAction(t *testing.T) {
|
||||||
|
th := Setup().InitBasic()
|
||||||
|
|
||||||
|
allowedInternalConnections := *utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections
|
||||||
|
defer func() {
|
||||||
|
utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
|
||||||
|
}()
|
||||||
|
*utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1"
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var request model.PostActionIntegrationRequest
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&request)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, request.UserId, th.BasicUser.Id)
|
||||||
|
assert.Equal(t, "foo", request.Context["s"])
|
||||||
|
assert.EqualValues(t, 3, request.Context["n"])
|
||||||
|
fmt.Fprintf(w, `{"update": {"message": "updated"}, "ephemeral_text": "foo"}`)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
interactivePost := model.Post{
|
||||||
|
Message: "Interactive post",
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
Props: model.StringInterface{
|
||||||
|
"attachments": []*model.SlackAttachment{
|
||||||
|
&model.SlackAttachment{
|
||||||
|
Text: "hello",
|
||||||
|
Actions: []*model.PostAction{
|
||||||
|
&model.PostAction{
|
||||||
|
Integration: &model.PostActionIntegration{
|
||||||
|
Context: model.StringInterface{
|
||||||
|
"s": "foo",
|
||||||
|
"n": 3,
|
||||||
|
},
|
||||||
|
URL: ts.URL,
|
||||||
|
},
|
||||||
|
Name: "action",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := CreatePostAsUser(&interactivePost)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
attachments, ok := post.Props["attachments"].([]*model.SlackAttachment)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
require.NotEmpty(t, attachments[0].Actions)
|
||||||
|
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||||
|
|
||||||
|
err = DoPostAction(post.Id, "notavalidid", th.BasicUser.Id)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||||
|
|
||||||
|
err = DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id)
|
||||||
|
require.Nil(t, err)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1759,6 +1759,14 @@
|
|||||||
"id": "api.post_get_post_by_id.get.app_error",
|
"id": "api.post_get_post_by_id.get.app_error",
|
||||||
"translation": "Unable to get post"
|
"translation": "Unable to get post"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.post.do_action.action_id.app_error",
|
||||||
|
"translation": "Invalid action id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.post.do_action.action_integration.app_error",
|
||||||
|
"translation": "Action integration error"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.preference.delete_preferences.decode.app_error",
|
"id": "api.preference.delete_preferences.decode.app_error",
|
||||||
"translation": "Unable to decode preferences from request"
|
"translation": "Unable to decode preferences from request"
|
||||||
|
|||||||
@@ -1803,6 +1803,16 @@ func (c *Client4) SearchPosts(teamId string, terms string, isOrSearch bool) (*Po
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DoPostAction performs a post action.
|
||||||
|
func (c *Client4) DoPostAction(postId, actionId string) (bool, *Response) {
|
||||||
|
if r, err := c.DoApiPost(c.GetPostRoute(postId)+"/actions/"+actionId, ""); err != nil {
|
||||||
|
return false, BuildErrorResponse(r, err)
|
||||||
|
} else {
|
||||||
|
defer closeBody(r)
|
||||||
|
return CheckStatusOK(r), BuildResponse(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// File Section
|
// File Section
|
||||||
|
|
||||||
// UploadFile will upload a file to a channel, to be later attached to a post.
|
// UploadFile will upload a file to a channel, to be later attached to a post.
|
||||||
|
|||||||
@@ -68,8 +68,31 @@ type PostForIndexing struct {
|
|||||||
ParentCreateAt *int64 `json:"parent_create_at"`
|
ParentCreateAt *int64 `json:"parent_create_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PostAction struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Integration *PostActionIntegration `json:"integration,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostActionIntegration struct {
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Context StringInterface `json:"context,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostActionIntegrationRequest struct {
|
||||||
|
UserId string `json:"user_id"`
|
||||||
|
Context StringInterface `json:"context,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostActionIntegrationResponse struct {
|
||||||
|
Update *Post `json:"update"`
|
||||||
|
EphemeralText string `json:"ephemeral_text"`
|
||||||
|
}
|
||||||
|
|
||||||
func (o *Post) ToJson() string {
|
func (o *Post) ToJson() string {
|
||||||
b, err := json.Marshal(o)
|
copy := *o
|
||||||
|
copy.StripActionIntegrations()
|
||||||
|
b, err := json.Marshal(©)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
} else {
|
} else {
|
||||||
@@ -179,6 +202,16 @@ func (o *Post) PreSave() {
|
|||||||
o.Props = make(map[string]interface{})
|
o.Props = make(map[string]interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok {
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
for _, action := range attachment.Actions {
|
||||||
|
if action.Id == "" {
|
||||||
|
action.Id = NewId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if o.Filenames == nil {
|
if o.Filenames == nil {
|
||||||
o.Filenames = []string{}
|
o.Filenames = []string{}
|
||||||
}
|
}
|
||||||
@@ -246,3 +279,53 @@ func PostPatchFromJson(data io.Reader) *PostPatch {
|
|||||||
|
|
||||||
return &post
|
return &post
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *PostActionIntegrationRequest) ToJson() string {
|
||||||
|
b, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Post) Attachments() []*SlackAttachment {
|
||||||
|
if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok {
|
||||||
|
return attachments
|
||||||
|
}
|
||||||
|
var ret []*SlackAttachment
|
||||||
|
if attachments, ok := o.Props["attachments"].([]interface{}); ok {
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
if enc, err := json.Marshal(attachment); err == nil {
|
||||||
|
var decoded SlackAttachment
|
||||||
|
if json.Unmarshal(enc, &decoded) == nil {
|
||||||
|
ret = append(ret, &decoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Post) StripActionIntegrations() {
|
||||||
|
attachments := o.Attachments()
|
||||||
|
if o.Props["attachments"] != nil {
|
||||||
|
o.Props["attachments"] = attachments
|
||||||
|
}
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
for _, action := range attachment.Actions {
|
||||||
|
action.Integration = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Post) GetAction(id string) *PostAction {
|
||||||
|
for _, attachment := range o.Attachments() {
|
||||||
|
for _, action := range attachment.Actions {
|
||||||
|
if action.Id == id {
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,8 +20,20 @@ func NewPostList() *PostList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *PostList) StripActionIntegrations() {
|
||||||
|
posts := o.Posts
|
||||||
|
o.Posts = make(map[string]*Post)
|
||||||
|
for id, post := range posts {
|
||||||
|
pcopy := *post
|
||||||
|
pcopy.StripActionIntegrations()
|
||||||
|
o.Posts[id] = &pcopy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (o *PostList) ToJson() string {
|
func (o *PostList) ToJson() string {
|
||||||
b, err := json.Marshal(o)
|
copy := *o
|
||||||
|
copy.StripActionIntegrations()
|
||||||
|
b, err := json.Marshal(©)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type SlackAttachment struct {
|
|||||||
Footer string `json:"footer"`
|
Footer string `json:"footer"`
|
||||||
FooterIcon string `json:"footer_icon"`
|
FooterIcon string `json:"footer_icon"`
|
||||||
Timestamp interface{} `json:"ts"` // This is either a string or an int64
|
Timestamp interface{} `json:"ts"` // This is either a string or an int64
|
||||||
|
Actions []*PostAction `json:"actions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SlackAttachmentField struct {
|
type SlackAttachmentField struct {
|
||||||
|
|||||||
@@ -351,3 +351,7 @@ export function unpinPost(postId) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doPostAction(postId, actionId) {
|
||||||
|
PostActions.doPostAction(postId, actionId)(dispatch, getState);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
import * as TextFormatting from 'utils/text_formatting.jsx';
|
import * as TextFormatting from 'utils/text_formatting.jsx';
|
||||||
import {localizeMessage} from 'utils/utils.jsx';
|
import {localizeMessage} from 'utils/utils.jsx';
|
||||||
|
|
||||||
|
import * as PostActions from 'actions/post_actions.jsx';
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@@ -11,6 +13,11 @@ import PropTypes from 'prop-types';
|
|||||||
export default class PostAttachment extends React.PureComponent {
|
export default class PostAttachment extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The post id
|
||||||
|
*/
|
||||||
|
postId: PropTypes.string.isRequired,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attachment to render
|
* The attachment to render
|
||||||
*/
|
*/
|
||||||
@@ -20,6 +27,8 @@ export default class PostAttachment extends React.PureComponent {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.handleActionButtonClick = this.handleActionButtonClick.bind(this);
|
||||||
|
this.getActionView = this.getActionView.bind(this);
|
||||||
this.getFieldsTable = this.getFieldsTable.bind(this);
|
this.getFieldsTable = this.getFieldsTable.bind(this);
|
||||||
this.getInitState = this.getInitState.bind(this);
|
this.getInitState = this.getInitState.bind(this);
|
||||||
this.shouldCollapse = this.shouldCollapse.bind(this);
|
this.shouldCollapse = this.shouldCollapse.bind(this);
|
||||||
@@ -80,6 +89,41 @@ export default class PostAttachment extends React.PureComponent {
|
|||||||
return TextFormatting.formatText(text) + `<div><a class="attachment-link-more" href="#">${localizeMessage('post_attachment.more', 'Show more...')}</a></div>`;
|
return TextFormatting.formatText(text) + `<div><a class="attachment-link-more" href="#">${localizeMessage('post_attachment.more', 'Show more...')}</a></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getActionView() {
|
||||||
|
const actions = this.props.attachment.actions;
|
||||||
|
if (!actions || !actions.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttons = [];
|
||||||
|
|
||||||
|
actions.forEach((action) => {
|
||||||
|
if (!action.id || !action.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buttons.push(
|
||||||
|
<button
|
||||||
|
key={action.id}
|
||||||
|
onClick={() => this.handleActionButtonClick(action.id)}
|
||||||
|
>
|
||||||
|
{action.name}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='attachment-actions'
|
||||||
|
>
|
||||||
|
{buttons}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleActionButtonClick(actionId) {
|
||||||
|
PostActions.doPostAction(this.props.postId, actionId);
|
||||||
|
}
|
||||||
|
|
||||||
getFieldsTable() {
|
getFieldsTable() {
|
||||||
const fields = this.props.attachment.fields;
|
const fields = this.props.attachment.fields;
|
||||||
if (!fields || !fields.length) {
|
if (!fields || !fields.length) {
|
||||||
@@ -275,6 +319,7 @@ export default class PostAttachment extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fields = this.getFieldsTable();
|
const fields = this.getFieldsTable();
|
||||||
|
const actions = this.getActionView();
|
||||||
|
|
||||||
let useBorderStyle;
|
let useBorderStyle;
|
||||||
if (data.color && data.color[0] === '#') {
|
if (data.color && data.color[0] === '#') {
|
||||||
@@ -301,6 +346,7 @@ export default class PostAttachment extends React.PureComponent {
|
|||||||
{text}
|
{text}
|
||||||
{image}
|
{image}
|
||||||
{fields}
|
{fields}
|
||||||
|
{actions}
|
||||||
</div>
|
</div>
|
||||||
{thumb}
|
{thumb}
|
||||||
<div style={{clear: 'both'}}/>
|
<div style={{clear: 'both'}}/>
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import PropTypes from 'prop-types';
|
|||||||
export default class PostAttachmentList extends React.PureComponent {
|
export default class PostAttachmentList extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The post id
|
||||||
|
*/
|
||||||
|
postId: PropTypes.string.isRequired,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Array of attachments to render
|
* Array of attachments to render
|
||||||
*/
|
*/
|
||||||
@@ -21,6 +26,7 @@ export default class PostAttachmentList extends React.PureComponent {
|
|||||||
content.push(
|
content.push(
|
||||||
<PostAttachment
|
<PostAttachment
|
||||||
attachment={attachment}
|
attachment={attachment}
|
||||||
|
postId={this.props.postId}
|
||||||
key={'att_' + i}
|
key={'att_' + i}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export default class PostBodyAdditionalContent extends React.PureComponent {
|
|||||||
return (
|
return (
|
||||||
<PostAttachmentList
|
<PostAttachmentList
|
||||||
attachments={attachments}
|
attachments={attachments}
|
||||||
|
postId={this.props.post.id}
|
||||||
key={this.props.post.id}
|
key={this.props.post.id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -284,5 +284,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attachment-actions {
|
||||||
|
margin-top: 9px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
@include border-radius(3px);
|
||||||
|
outline: 0;
|
||||||
|
margin: 8px 8px 0 0;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
height: 30px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -602,7 +602,7 @@ export function applyTheme(theme) {
|
|||||||
changeCss('.app__body .popover.top>.arrow:after, .app__body .tip-overlay.tip-overlay--chat .arrow', 'border-top-color:' + theme.centerChannelBg);
|
changeCss('.app__body .popover.top>.arrow:after, .app__body .tip-overlay.tip-overlay--chat .arrow', 'border-top-color:' + theme.centerChannelBg);
|
||||||
changeCss('@media(min-width: 768px){.app__body .form-control', 'background:' + theme.centerChannelBg);
|
changeCss('@media(min-width: 768px){.app__body .form-control', 'background:' + theme.centerChannelBg);
|
||||||
changeCss('@media(min-width: 768px){.app__body .sidebar--right.sidebar--right--expanded .sidebar-right-container', 'background:' + theme.centerChannelBg);
|
changeCss('@media(min-width: 768px){.app__body .sidebar--right.sidebar--right--expanded .sidebar-right-container', 'background:' + theme.centerChannelBg);
|
||||||
changeCss('.app__body .attachment__content', 'background:' + theme.centerChannelBg);
|
changeCss('.app__body .attachment__content, .app__body .attachment-actions button', 'background:' + theme.centerChannelBg);
|
||||||
changeCss('body.app__body', 'scrollbar-face-color:' + theme.centerChannelBg);
|
changeCss('body.app__body', 'scrollbar-face-color:' + theme.centerChannelBg);
|
||||||
changeCss('body.app__body', 'scrollbar-track-color:' + theme.centerChannelBg);
|
changeCss('body.app__body', 'scrollbar-track-color:' + theme.centerChannelBg);
|
||||||
changeCss('.app__body .shortcut-key, .app__body .post-list__new-messages-below', 'color:' + theme.centerChannelBg);
|
changeCss('.app__body .shortcut-key, .app__body .post-list__new-messages-below', 'color:' + theme.centerChannelBg);
|
||||||
@@ -653,7 +653,9 @@ export function applyTheme(theme) {
|
|||||||
changeCss('.app__body .input-group-addon, .app__body .form-control, .app__body .post-create__container .post-body__actions > span', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1));
|
changeCss('.app__body .input-group-addon, .app__body .form-control, .app__body .post-create__container .post-body__actions > span', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1));
|
||||||
changeCss('@media(min-width: 768px){.app__body .post-list__table .post-list__content .dropdown-menu a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.1));
|
changeCss('@media(min-width: 768px){.app__body .post-list__table .post-list__content .dropdown-menu a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.1));
|
||||||
changeCss('.app__body .form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
|
changeCss('.app__body .form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
|
||||||
changeCss('.app__body .attachment .attachment__content', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3));
|
changeCss('.app__body .attachment .attachment__content, .app__body .attachment-actions button', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3));
|
||||||
|
changeCss('.app__body .attachment-actions button:focus, .app__body .attachment-actions button:hover', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.5));
|
||||||
|
changeCss('.app__body .attachment-actions button:focus, .app__body .attachment-actions button:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.03));
|
||||||
changeCss('.app__body .input-group-addon, .app__body .channel-intro .channel-intro__content, .app__body .webhooks__container', 'background:' + changeOpacity(theme.centerChannelColor, 0.05));
|
changeCss('.app__body .input-group-addon, .app__body .channel-intro .channel-intro__content, .app__body .webhooks__container', 'background:' + changeOpacity(theme.centerChannelColor, 0.05));
|
||||||
changeCss('.app__body .date-separator .separator__text', 'color:' + theme.centerChannelColor);
|
changeCss('.app__body .date-separator .separator__text', 'color:' + theme.centerChannelColor);
|
||||||
changeCss('.app__body .date-separator .separator__hr, .app__body .modal-footer, .app__body .modal .custom-textarea', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
|
changeCss('.app__body .date-separator .separator__hr, .app__body .modal-footer, .app__body .modal .custom-textarea', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user