Add pinned posts (#4217)
Этот коммит содержится в:
коммит произвёл
George Goldberg
родитель
482a0fb5fc
Коммит
fe38d6d5bb
@@ -225,7 +225,7 @@ func TestGetTeamAnalyticsStandard(t *testing.T) {
|
|||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
|
|
||||||
if rows[0].Value != 3 {
|
if rows[0].Value != 4 {
|
||||||
t.Log(rows.ToJson())
|
t.Log(rows.ToJson())
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
@@ -245,7 +245,7 @@ func TestGetTeamAnalyticsStandard(t *testing.T) {
|
|||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
|
|
||||||
if rows[2].Value != 5 {
|
if rows[2].Value != 6 {
|
||||||
t.Log(rows.ToJson())
|
t.Log(rows.ToJson())
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type TestHelper struct {
|
|||||||
BasicUser2 *model.User
|
BasicUser2 *model.User
|
||||||
BasicChannel *model.Channel
|
BasicChannel *model.Channel
|
||||||
BasicPost *model.Post
|
BasicPost *model.Post
|
||||||
|
PinnedPost *model.Post
|
||||||
|
|
||||||
SystemAdminClient *model.Client
|
SystemAdminClient *model.Client
|
||||||
SystemAdminTeam *model.Team
|
SystemAdminTeam *model.Team
|
||||||
@@ -91,6 +92,9 @@ func (me *TestHelper) InitBasic() *TestHelper {
|
|||||||
me.BasicChannel = me.CreateChannel(me.BasicClient, me.BasicTeam)
|
me.BasicChannel = me.CreateChannel(me.BasicClient, me.BasicTeam)
|
||||||
me.BasicPost = me.CreatePost(me.BasicClient, me.BasicChannel)
|
me.BasicPost = me.CreatePost(me.BasicClient, me.BasicChannel)
|
||||||
|
|
||||||
|
pinnedPostChannel := me.CreateChannel(me.BasicClient, me.BasicTeam)
|
||||||
|
me.PinnedPost = me.CreatePinnedPost(me.BasicClient, pinnedPostChannel)
|
||||||
|
|
||||||
return me
|
return me
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +269,21 @@ func (me *TestHelper) CreatePost(client *model.Client, channel *model.Channel) *
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (me *TestHelper) CreatePinnedPost(client *model.Client, channel *model.Channel) *model.Post {
|
||||||
|
id := model.NewId()
|
||||||
|
|
||||||
|
post := &model.Post{
|
||||||
|
ChannelId: channel.Id,
|
||||||
|
Message: "message_" + id,
|
||||||
|
IsPinned: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.DisableDebugLogForTest()
|
||||||
|
r := client.Must(client.CreatePost(post)).Data.(*model.Post)
|
||||||
|
utils.EnableDebugLogForTest()
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func (me *TestHelper) LoginBasic() {
|
func (me *TestHelper) LoginBasic() {
|
||||||
utils.DisableDebugLogForTest()
|
utils.DisableDebugLogForTest()
|
||||||
me.BasicClient.Must(me.BasicClient.Login(me.BasicUser.Email, me.BasicUser.Password))
|
me.BasicClient.Must(me.BasicClient.Login(me.BasicUser.Email, me.BasicUser.Password))
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ func InitChannel() {
|
|||||||
BaseRoutes.NeedChannel.Handle("/stats", ApiUserRequired(getChannelStats)).Methods("GET")
|
BaseRoutes.NeedChannel.Handle("/stats", ApiUserRequired(getChannelStats)).Methods("GET")
|
||||||
BaseRoutes.NeedChannel.Handle("/members/{user_id:[A-Za-z0-9]+}", ApiUserRequired(getChannelMember)).Methods("GET")
|
BaseRoutes.NeedChannel.Handle("/members/{user_id:[A-Za-z0-9]+}", ApiUserRequired(getChannelMember)).Methods("GET")
|
||||||
BaseRoutes.NeedChannel.Handle("/members/ids", ApiUserRequired(getChannelMembersByIds)).Methods("POST")
|
BaseRoutes.NeedChannel.Handle("/members/ids", ApiUserRequired(getChannelMembersByIds)).Methods("POST")
|
||||||
|
BaseRoutes.NeedChannel.Handle("/pinned", ApiUserRequired(getPinnedPosts)).Methods("GET")
|
||||||
BaseRoutes.NeedChannel.Handle("/join", ApiUserRequired(join)).Methods("POST")
|
BaseRoutes.NeedChannel.Handle("/join", ApiUserRequired(join)).Methods("POST")
|
||||||
BaseRoutes.NeedChannel.Handle("/leave", ApiUserRequired(leave)).Methods("POST")
|
BaseRoutes.NeedChannel.Handle("/leave", ApiUserRequired(leave)).Methods("POST")
|
||||||
BaseRoutes.NeedChannel.Handle("/delete", ApiUserRequired(deleteChannel)).Methods("POST")
|
BaseRoutes.NeedChannel.Handle("/delete", ApiUserRequired(deleteChannel)).Methods("POST")
|
||||||
@@ -598,6 +599,21 @@ func getMyChannelMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPinnedPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
params := mux.Vars(r)
|
||||||
|
channelId := params["channel_id"]
|
||||||
|
posts := &model.PostList{}
|
||||||
|
|
||||||
|
if result := <-app.Srv.Store.Channel().GetPinnedPosts(channelId); result.Err != nil {
|
||||||
|
c.Err = result.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
posts = result.Data.(*model.PostList)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(posts.ToJson()))
|
||||||
|
}
|
||||||
|
|
||||||
func addMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
func addMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
params := mux.Vars(r)
|
params := mux.Vars(r)
|
||||||
id := params["channel_id"]
|
id := params["channel_id"]
|
||||||
|
|||||||
@@ -926,8 +926,8 @@ func TestGetMoreChannelsPage(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
channels := r.Data.(*model.ChannelList)
|
channels := r.Data.(*model.ChannelList)
|
||||||
|
|
||||||
// 1 for BasicChannel, 2 for open channels created above
|
// 1 for BasicChannel, 1 for PinnedPostChannel, 2 for open channels created above
|
||||||
if len(*channels) != 3 {
|
if len(*channels) != 4 {
|
||||||
t.Fatal("wrong length")
|
t.Fatal("wrong length")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -990,11 +990,11 @@ func TestGetChannelCounts(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
counts := result.Data.(*model.ChannelCounts)
|
counts := result.Data.(*model.ChannelCounts)
|
||||||
|
|
||||||
if len(counts.Counts) != 5 {
|
if len(counts.Counts) != 6 {
|
||||||
t.Fatal("wrong number of channel counts")
|
t.Fatal("wrong number of channel counts")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(counts.UpdateTimes) != 5 {
|
if len(counts.UpdateTimes) != 6 {
|
||||||
t.Fatal("wrong number of channel update times")
|
t.Fatal("wrong number of channel update times")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1024,8 +1024,8 @@ func TestGetMyChannelMembers(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
members := result.Data.(*model.ChannelMembers)
|
members := result.Data.(*model.ChannelMembers)
|
||||||
|
|
||||||
// town-square, off-topic, basic test channel, channel1, channel2
|
// town-square, off-topic, basic test channel, pinned post channel, channel1, channel2
|
||||||
if len(*members) != 5 {
|
if len(*members) != 6 {
|
||||||
t.Fatal("wrong number of members", len(*members))
|
t.Fatal("wrong number of members", len(*members))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2117,3 +2117,24 @@ func TestUpdateChannelRoles(t *testing.T) {
|
|||||||
t.Fatal("Channel member should not be able to promote itself to channel admin:", meta)
|
t.Fatal("Channel member should not be able to promote itself to channel admin:", meta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetPinnedPosts(t *testing.T) {
|
||||||
|
th := Setup().InitBasic()
|
||||||
|
Client := th.BasicClient
|
||||||
|
|
||||||
|
post1 := th.BasicPost
|
||||||
|
r1 := Client.Must(Client.GetPinnedPosts(post1.ChannelId)).Data.(*model.PostList)
|
||||||
|
if len(r1.Order) != 0 {
|
||||||
|
t.Fatal("should not have gotten a pinned post")
|
||||||
|
}
|
||||||
|
|
||||||
|
post2 := th.PinnedPost
|
||||||
|
r2 := Client.Must(Client.GetPinnedPosts(post2.ChannelId)).Data.(*model.PostList)
|
||||||
|
if len(r2.Order) == 0 {
|
||||||
|
t.Fatal("should have gotten a pinned post")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := r2.Posts[post2.Id]; !ok {
|
||||||
|
t.Fatal("missing pinned post")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
55
api/post.go
55
api/post.go
@@ -38,6 +38,8 @@ func InitPost() {
|
|||||||
BaseRoutes.NeedPost.Handle("/before/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsBefore)).Methods("GET")
|
BaseRoutes.NeedPost.Handle("/before/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsBefore)).Methods("GET")
|
||||||
BaseRoutes.NeedPost.Handle("/after/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsAfter)).Methods("GET")
|
BaseRoutes.NeedPost.Handle("/after/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsAfter)).Methods("GET")
|
||||||
BaseRoutes.NeedPost.Handle("/get_file_infos", ApiUserRequired(getFileInfosForPost)).Methods("GET")
|
BaseRoutes.NeedPost.Handle("/get_file_infos", ApiUserRequired(getFileInfosForPost)).Methods("GET")
|
||||||
|
BaseRoutes.NeedPost.Handle("/pin", ApiUserRequired(pinPost)).Methods("POST")
|
||||||
|
BaseRoutes.NeedPost.Handle("/unpin", ApiUserRequired(unpinPost)).Methods("POST")
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -91,6 +93,59 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(rpost.ToJson()))
|
w.Write([]byte(rpost.ToJson()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func saveIsPinnedPost(c *Context, w http.ResponseWriter, r *http.Request, isPinned bool) {
|
||||||
|
params := mux.Vars(r)
|
||||||
|
|
||||||
|
channelId := params["channel_id"]
|
||||||
|
if len(channelId) != 26 {
|
||||||
|
c.SetInvalidParam("savedIsPinnedPost", "channelId")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
postId := params["post_id"]
|
||||||
|
if len(postId) != 26 {
|
||||||
|
c.SetInvalidParam("savedIsPinnedPost", "postId")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pchan := app.Srv.Store.Post().Get(postId)
|
||||||
|
|
||||||
|
var oldPost *model.Post
|
||||||
|
if result := <-pchan; result.Err != nil {
|
||||||
|
c.Err = result.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
oldPost = result.Data.(*model.PostList).Posts[postId]
|
||||||
|
newPost := &model.Post{}
|
||||||
|
*newPost = *oldPost
|
||||||
|
newPost.IsPinned = isPinned
|
||||||
|
|
||||||
|
if result := <-app.Srv.Store.Post().Update(newPost, oldPost); result.Err != nil {
|
||||||
|
c.Err = result.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rpost := result.Data.(*model.Post)
|
||||||
|
|
||||||
|
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", rpost.ChannelId, "", nil)
|
||||||
|
message.Add("post", rpost.ToJson())
|
||||||
|
|
||||||
|
go app.Publish(message)
|
||||||
|
|
||||||
|
app.InvalidateCacheForChannelPosts(rpost.ChannelId)
|
||||||
|
|
||||||
|
w.Write([]byte(rpost.ToJson()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pinPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
saveIsPinnedPost(c, w, r, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpinPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
saveIsPinnedPost(c, w, r, false)
|
||||||
|
}
|
||||||
|
|
||||||
func getFlaggedPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
func getFlaggedPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
params := mux.Vars(r)
|
params := mux.Vars(r)
|
||||||
|
|
||||||
|
|||||||
@@ -1378,3 +1378,49 @@ func TestGetOpenGraphMetadata(t *testing.T) {
|
|||||||
t.Fatal("should have failed with 501 - disabled link previews")
|
t.Fatal("should have failed with 501 - disabled link previews")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPinPost(t *testing.T) {
|
||||||
|
th := Setup().InitBasic()
|
||||||
|
Client := th.BasicClient
|
||||||
|
|
||||||
|
post := th.BasicPost
|
||||||
|
if rupost1, err := Client.PinPost(post.ChannelId, post.Id); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if rupost1.Data.(*model.Post).IsPinned != true {
|
||||||
|
t.Fatal("failed to pin post")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pinnedPost := th.PinnedPost
|
||||||
|
if rupost2, err := Client.PinPost(pinnedPost.ChannelId, pinnedPost.Id); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if rupost2.Data.(*model.Post).IsPinned != true {
|
||||||
|
t.Fatal("pinning a post should be idempotent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnpinPost(t *testing.T) {
|
||||||
|
th := Setup().InitBasic()
|
||||||
|
Client := th.BasicClient
|
||||||
|
|
||||||
|
pinnedPost := th.PinnedPost
|
||||||
|
if rupost1, err := Client.UnpinPost(pinnedPost.ChannelId, pinnedPost.Id); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if rupost1.Data.(*model.Post).IsPinned != false {
|
||||||
|
t.Fatal("failed to unpin post")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post := th.BasicPost
|
||||||
|
if rupost2, err := Client.UnpinPost(post.ChannelId, post.Id); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if rupost2.Data.(*model.Post).IsPinned != false {
|
||||||
|
t.Fatal("unpinning a post should be idempotent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4623,6 +4623,10 @@
|
|||||||
"id": "store.sql_channel.extra_updated.app_error",
|
"id": "store.sql_channel.extra_updated.app_error",
|
||||||
"translation": "Problem updating members last updated time"
|
"translation": "Problem updating members last updated time"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_channel.pinned_posts.app_error",
|
||||||
|
"translation": "We couldn't find the pinned posts"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "store.sql_channel.get.existing.app_error",
|
"id": "store.sql_channel.get.existing.app_error",
|
||||||
"translation": "We couldn't find the existing channel"
|
"translation": "We couldn't find the existing channel"
|
||||||
|
|||||||
@@ -1533,6 +1533,16 @@ func (c *Client) GetFlaggedPosts(offset int, limit int) (*Result, *AppError) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetPinnedPosts(channelId string) (*Result, *AppError) {
|
||||||
|
if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/pinned", "", ""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
defer closeBody(r)
|
||||||
|
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||||
|
r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) UploadProfileFile(data []byte, contentType string) (*Result, *AppError) {
|
func (c *Client) UploadProfileFile(data []byte, contentType string) (*Result, *AppError) {
|
||||||
return c.uploadFile(c.ApiUrl+"/users/newimage", data, contentType)
|
return c.uploadFile(c.ApiUrl+"/users/newimage", data, contentType)
|
||||||
}
|
}
|
||||||
@@ -2389,3 +2399,23 @@ func (c *Client) UpdateChannelRoles(channelId string, userId string, roles strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) PinPost(channelId string, postId string) (*Result, *AppError) {
|
||||||
|
if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/posts/"+postId+"/pin", ""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
defer closeBody(r)
|
||||||
|
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||||
|
r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) UnpinPost(channelId string, postId string) (*Result, *AppError) {
|
||||||
|
if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/posts/"+postId+"/unpin", ""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
defer closeBody(r)
|
||||||
|
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||||
|
r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type Post struct {
|
|||||||
UpdateAt int64 `json:"update_at"`
|
UpdateAt int64 `json:"update_at"`
|
||||||
EditAt int64 `json:"edit_at"`
|
EditAt int64 `json:"edit_at"`
|
||||||
DeleteAt int64 `json:"delete_at"`
|
DeleteAt int64 `json:"delete_at"`
|
||||||
|
IsPinned bool `json:"is_pinned"`
|
||||||
UserId string `json:"user_id"`
|
UserId string `json:"user_id"`
|
||||||
ChannelId string `json:"channel_id"`
|
ChannelId string `json:"channel_id"`
|
||||||
RootId string `json:"root_id"`
|
RootId string `json:"root_id"`
|
||||||
|
|||||||
@@ -321,6 +321,32 @@ func (s SqlChannelStore) Get(id string, allowFromCache bool) StoreChannel {
|
|||||||
return s.get(id, false, allowFromCache)
|
return s.get(id, false, allowFromCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlChannelStore) GetPinnedPosts(channelId string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
pl := &model.PostList{}
|
||||||
|
|
||||||
|
var posts []*model.Post
|
||||||
|
if _, err := s.GetReplica().Select(&posts, "SELECT * FROM Posts WHERE IsPinned = true AND ChannelId = :ChannelId AND DeleteAt = 0 ORDER BY CreateAt ASC", map[string]interface{}{"ChannelId": channelId}); err != nil {
|
||||||
|
result.Err = model.NewLocAppError("SqlPostStore.GetPinnedPosts", "store.sql_channel.pinned_posts.app_error", nil, err.Error())
|
||||||
|
} else {
|
||||||
|
for _, post := range posts {
|
||||||
|
pl.AddPost(post)
|
||||||
|
pl.AddOrder(post.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Data = pl
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|
||||||
func (s SqlChannelStore) GetFromMaster(id string) StoreChannel {
|
func (s SqlChannelStore) GetFromMaster(id string) StoreChannel {
|
||||||
return s.get(id, true, false)
|
return s.get(id, true, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1493,3 +1493,46 @@ func TestChannelStoreAnalyticsDeletedTypeCount(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestChannelStoreGetPinnedPosts(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
o1 := Must(store.Channel().Save(&model.Channel{
|
||||||
|
TeamId: model.NewId(),
|
||||||
|
DisplayName: "Name",
|
||||||
|
Name: "a" + model.NewId() + "b",
|
||||||
|
Type: model.CHANNEL_OPEN,
|
||||||
|
})).(*model.Channel)
|
||||||
|
|
||||||
|
p1 := Must(store.Post().Save(&model.Post{
|
||||||
|
UserId: model.NewId(),
|
||||||
|
ChannelId: o1.Id,
|
||||||
|
Message: "test",
|
||||||
|
IsPinned: true,
|
||||||
|
})).(*model.Post)
|
||||||
|
|
||||||
|
if r1 := <-store.Channel().GetPinnedPosts(o1.Id); r1.Err != nil {
|
||||||
|
t.Fatal(r1.Err)
|
||||||
|
} else if r1.Data.(*model.PostList).Posts[p1.Id] == nil {
|
||||||
|
t.Fatal("didn't return relevant pinned posts")
|
||||||
|
}
|
||||||
|
|
||||||
|
o2 := Must(store.Channel().Save(&model.Channel{
|
||||||
|
TeamId: model.NewId(),
|
||||||
|
DisplayName: "Name",
|
||||||
|
Name: "a" + model.NewId() + "b",
|
||||||
|
Type: model.CHANNEL_OPEN,
|
||||||
|
})).(*model.Channel)
|
||||||
|
|
||||||
|
Must(store.Post().Save(&model.Post{
|
||||||
|
UserId: model.NewId(),
|
||||||
|
ChannelId: o2.Id,
|
||||||
|
Message: "test",
|
||||||
|
}))
|
||||||
|
|
||||||
|
if r2 := <-store.Channel().GetPinnedPosts(o2.Id); r2.Err != nil {
|
||||||
|
t.Fatal(r2.Err)
|
||||||
|
} else if len(r2.Data.(*model.PostList).Posts) != 0 {
|
||||||
|
t.Fatal("wasn't supposed to return posts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ func (s SqlPostStore) CreateIndexesIfNotExists() {
|
|||||||
s.CreateIndexIfNotExists("idx_posts_channel_id", "Posts", "ChannelId")
|
s.CreateIndexIfNotExists("idx_posts_channel_id", "Posts", "ChannelId")
|
||||||
s.CreateIndexIfNotExists("idx_posts_root_id", "Posts", "RootId")
|
s.CreateIndexIfNotExists("idx_posts_root_id", "Posts", "RootId")
|
||||||
s.CreateIndexIfNotExists("idx_posts_user_id", "Posts", "UserId")
|
s.CreateIndexIfNotExists("idx_posts_user_id", "Posts", "UserId")
|
||||||
|
s.CreateIndexIfNotExists("idx_posts_is_pinned", "Posts", "IsPinned")
|
||||||
|
|
||||||
s.CreateFullTextIndexIfNotExists("idx_posts_message_txt", "Posts", "Message")
|
s.CreateFullTextIndexIfNotExists("idx_posts_message_txt", "Posts", "Message")
|
||||||
s.CreateFullTextIndexIfNotExists("idx_posts_hashtags_txt", "Posts", "Hashtags")
|
s.CreateFullTextIndexIfNotExists("idx_posts_hashtags_txt", "Posts", "Hashtags")
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ type ChannelStore interface {
|
|||||||
InvalidateMemberCount(channelId string)
|
InvalidateMemberCount(channelId string)
|
||||||
GetMemberCountFromCache(channelId string) int64
|
GetMemberCountFromCache(channelId string) int64
|
||||||
GetMemberCount(channelId string, allowFromCache bool) StoreChannel
|
GetMemberCount(channelId string, allowFromCache bool) StoreChannel
|
||||||
|
GetPinnedPosts(channelId string) StoreChannel
|
||||||
RemoveMember(channelId string, userId string) StoreChannel
|
RemoveMember(channelId string, userId string) StoreChannel
|
||||||
PermanentDeleteMembersByUser(userId string) StoreChannel
|
PermanentDeleteMembersByUser(userId string) StoreChannel
|
||||||
PermanentDeleteMembersByChannel(channelId string) StoreChannel
|
PermanentDeleteMembersByChannel(channelId string) StoreChannel
|
||||||
|
|||||||
@@ -68,6 +68,14 @@ export function handleNewPost(post, msg) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pinPost(channelId, postId) {
|
||||||
|
AsyncClient.pinPost(channelId, postId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unpinPost(channelId, postId) {
|
||||||
|
AsyncClient.unpinPost(channelId, postId);
|
||||||
|
}
|
||||||
|
|
||||||
export function flagPost(postId) {
|
export function flagPost(postId) {
|
||||||
trackEvent('api', 'api_posts_flagged');
|
trackEvent('api', 'api_posts_flagged');
|
||||||
AsyncClient.savePreference(Preferences.CATEGORY_FLAGGED_POST, postId, 'true');
|
AsyncClient.savePreference(Preferences.CATEGORY_FLAGGED_POST, postId, 'true');
|
||||||
@@ -96,7 +104,8 @@ export function getFlaggedPosts() {
|
|||||||
AppDispatcher.handleServerAction({
|
AppDispatcher.handleServerAction({
|
||||||
type: ActionTypes.RECEIVED_SEARCH,
|
type: ActionTypes.RECEIVED_SEARCH,
|
||||||
results: data,
|
results: data,
|
||||||
is_flagged_posts: true
|
is_flagged_posts: true,
|
||||||
|
is_pinned_posts: false
|
||||||
});
|
});
|
||||||
|
|
||||||
loadProfilesForPosts(data.posts);
|
loadProfilesForPosts(data.posts);
|
||||||
@@ -107,6 +116,31 @@ export function getFlaggedPosts() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPinnedPosts(channelId) {
|
||||||
|
Client.getPinnedPosts(channelId,
|
||||||
|
(data) => {
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECEIVED_SEARCH_TERM,
|
||||||
|
term: null,
|
||||||
|
do_search: false,
|
||||||
|
is_mention_search: false
|
||||||
|
});
|
||||||
|
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECEIVED_SEARCH,
|
||||||
|
results: data,
|
||||||
|
is_flagged_posts: false,
|
||||||
|
is_pinned_posts: true
|
||||||
|
});
|
||||||
|
|
||||||
|
loadProfilesForPosts(data.posts);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
AsyncClient.dispatchError(err, 'getPinnedPosts');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function loadPosts(channelId = ChannelStore.getCurrentId(), isPost = false) {
|
export function loadPosts(channelId = ChannelStore.getCurrentId(), isPost = false) {
|
||||||
const postList = PostStore.getAllPosts(channelId);
|
const postList = PostStore.getAllPosts(channelId);
|
||||||
const latestPostTime = PostStore.getLatestPostFromPageTime(channelId);
|
const latestPostTime = PostStore.getLatestPostFromPageTime(channelId);
|
||||||
|
|||||||
@@ -1802,6 +1802,15 @@ export default class Client {
|
|||||||
this.trackEvent('api', 'api_posts_get_flagged', {team_id: this.getTeamId()});
|
this.trackEvent('api', 'api_posts_get_flagged', {team_id: this.getTeamId()});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPinnedPosts(channelId, success, error) {
|
||||||
|
request.
|
||||||
|
get(`${this.getChannelNeededRoute(channelId)}/pinned`).
|
||||||
|
set(this.defaultHeaders).
|
||||||
|
type('application/json').
|
||||||
|
accept('application/json').
|
||||||
|
end(this.handleResponse.bind(this, 'getPinnedPosts', success, error));
|
||||||
|
}
|
||||||
|
|
||||||
getFileInfosForPost(channelId, postId, success, error) {
|
getFileInfosForPost(channelId, postId, success, error) {
|
||||||
request.
|
request.
|
||||||
get(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/get_file_infos`).
|
get(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/get_file_infos`).
|
||||||
@@ -2187,6 +2196,24 @@ export default class Client {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pinPost(channelId, postId, success, error) {
|
||||||
|
request.
|
||||||
|
post(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/pin`).
|
||||||
|
set(this.defaultHeaders).
|
||||||
|
accept('application/json').
|
||||||
|
send().
|
||||||
|
end(this.handleResponse.bind(this, 'pinPost', success, error));
|
||||||
|
}
|
||||||
|
|
||||||
|
unpinPost(channelId, postId, success, error) {
|
||||||
|
request.
|
||||||
|
post(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/unpin`).
|
||||||
|
set(this.defaultHeaders).
|
||||||
|
accept('application/json').
|
||||||
|
send().
|
||||||
|
end(this.handleResponse.bind(this, 'unpinPost', success, error));
|
||||||
|
}
|
||||||
|
|
||||||
saveReaction(channelId, reaction, success, error) {
|
saveReaction(channelId, reaction, success, error) {
|
||||||
request.
|
request.
|
||||||
post(`${this.getChannelNeededRoute(channelId)}/posts/${reaction.post_id}/reactions/save`).
|
post(`${this.getChannelNeededRoute(channelId)}/posts/${reaction.post_id}/reactions/save`).
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import * as Utils from 'utils/utils.jsx';
|
|||||||
import * as ChannelUtils from 'utils/channel_utils.jsx';
|
import * as ChannelUtils from 'utils/channel_utils.jsx';
|
||||||
import {getSiteURL} from 'utils/url.jsx';
|
import {getSiteURL} from 'utils/url.jsx';
|
||||||
import * as TextFormatting from 'utils/text_formatting.jsx';
|
import * as TextFormatting from 'utils/text_formatting.jsx';
|
||||||
import {getFlaggedPosts} from 'actions/post_actions.jsx';
|
import {getFlaggedPosts, getPinnedPosts} from 'actions/post_actions.jsx';
|
||||||
|
|
||||||
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
||||||
|
|
||||||
@@ -53,6 +53,7 @@ export default class ChannelHeader extends React.Component {
|
|||||||
this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this);
|
this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this);
|
||||||
this.handleShortcut = this.handleShortcut.bind(this);
|
this.handleShortcut = this.handleShortcut.bind(this);
|
||||||
this.getFlagged = this.getFlagged.bind(this);
|
this.getFlagged = this.getFlagged.bind(this);
|
||||||
|
this.getPinnedPosts = this.getPinnedPosts.bind(this);
|
||||||
this.initWebrtc = this.initWebrtc.bind(this);
|
this.initWebrtc = this.initWebrtc.bind(this);
|
||||||
this.onBusy = this.onBusy.bind(this);
|
this.onBusy = this.onBusy.bind(this);
|
||||||
this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
|
this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
|
||||||
@@ -158,6 +159,15 @@ export default class ChannelHeader extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPinnedPosts(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (SearchStore.isPinnedPosts) {
|
||||||
|
GlobalActions.toggleSideBarAction(false);
|
||||||
|
} else {
|
||||||
|
getPinnedPosts(this.props.channelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getFlagged(e) {
|
getFlagged(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (SearchStore.isFlaggedPosts) {
|
if (SearchStore.isFlaggedPosts) {
|
||||||
@@ -211,6 +221,7 @@ export default class ChannelHeader extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const flagIcon = Constants.FLAG_ICON_SVG;
|
const flagIcon = Constants.FLAG_ICON_SVG;
|
||||||
|
const pinIcon = Constants.PIN_ICON;
|
||||||
|
|
||||||
if (!this.validState()) {
|
if (!this.validState()) {
|
||||||
// Use an empty div to make sure the header's height stays constant
|
// Use an empty div to make sure the header's height stays constant
|
||||||
@@ -762,8 +773,20 @@ export default class ChannelHeader extends React.Component {
|
|||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th className='header-list__members'>
|
<th className='header-list__right'>
|
||||||
{popoverListMembers}
|
{popoverListMembers}
|
||||||
|
<a
|
||||||
|
href='#'
|
||||||
|
type='button'
|
||||||
|
id='pinned-posts-button'
|
||||||
|
className='pinned-posts-button'
|
||||||
|
onClick={this.getPinnedPosts}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
dangerouslySetInnerHTML={{__html: pinIcon}}
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th className='search-bar__container'>
|
<th className='search-bar__container'>
|
||||||
<NavbarSearchBox
|
<NavbarSearchBox
|
||||||
|
|||||||
@@ -19,12 +19,15 @@ import UserStore from 'stores/user_store.jsx';
|
|||||||
import ChannelStore from 'stores/channel_store.jsx';
|
import ChannelStore from 'stores/channel_store.jsx';
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
import TeamStore from 'stores/team_store.jsx';
|
||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
import PreferenceStore from 'stores/preference_store.jsx';
|
||||||
|
import SearchStore from 'stores/search_store.jsx';
|
||||||
|
|
||||||
import ChannelSwitchModal from './channel_switch_modal.jsx';
|
import ChannelSwitchModal from './channel_switch_modal.jsx';
|
||||||
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import * as ChannelUtils from 'utils/channel_utils.jsx';
|
import * as ChannelUtils from 'utils/channel_utils.jsx';
|
||||||
import * as ChannelActions from 'actions/channel_actions.jsx';
|
import * as ChannelActions from 'actions/channel_actions.jsx';
|
||||||
|
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||||
|
import {getPinnedPosts} from 'actions/post_actions.jsx';
|
||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
const ActionTypes = Constants.ActionTypes;
|
const ActionTypes = Constants.ActionTypes;
|
||||||
@@ -62,6 +65,7 @@ export default class Navbar extends React.Component {
|
|||||||
this.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this);
|
this.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this);
|
||||||
|
|
||||||
this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
|
this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
|
||||||
|
this.getPinnedPosts = this.getPinnedPosts.bind(this);
|
||||||
|
|
||||||
const state = this.getStateFromStores();
|
const state = this.getStateFromStores();
|
||||||
state.showEditChannelPurposeModal = false;
|
state.showEditChannelPurposeModal = false;
|
||||||
@@ -216,6 +220,15 @@ export default class Navbar extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPinnedPosts(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (SearchStore.isPinnedPosts) {
|
||||||
|
GlobalActions.toggleSideBarAction(false);
|
||||||
|
} else {
|
||||||
|
getPinnedPosts(this.state.channel.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
toggleFavorite = (e) => {
|
toggleFavorite = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -244,6 +257,7 @@ export default class Navbar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let viewInfoOption;
|
let viewInfoOption;
|
||||||
|
let viewPinnedPostsOption;
|
||||||
let addMembersOption;
|
let addMembersOption;
|
||||||
let manageMembersOption;
|
let manageMembersOption;
|
||||||
let setChannelHeaderOption;
|
let setChannelHeaderOption;
|
||||||
@@ -335,6 +349,21 @@ export default class Navbar extends React.Component {
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
viewPinnedPostsOption = (
|
||||||
|
<li role='presentation'>
|
||||||
|
<a
|
||||||
|
role='menuitem'
|
||||||
|
href='#'
|
||||||
|
onClick={this.getPinnedPosts}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='navbar.viewPinnedPosts'
|
||||||
|
defaultMessage='View Pinned Posts'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
if (!ChannelStore.isDefault(channel)) {
|
if (!ChannelStore.isDefault(channel)) {
|
||||||
addMembersOption = (
|
addMembersOption = (
|
||||||
<li role='presentation'>
|
<li role='presentation'>
|
||||||
@@ -561,6 +590,7 @@ export default class Navbar extends React.Component {
|
|||||||
role='menu'
|
role='menu'
|
||||||
>
|
>
|
||||||
{viewInfoOption}
|
{viewInfoOption}
|
||||||
|
{viewPinnedPostsOption}
|
||||||
{notificationPreferenceOption}
|
{notificationPreferenceOption}
|
||||||
{addMembersOption}
|
{addMembersOption}
|
||||||
{manageMembersOption}
|
{manageMembersOption}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import TeamStore from 'stores/team_store.jsx';
|
|||||||
import UserStore from 'stores/user_store.jsx';
|
import UserStore from 'stores/user_store.jsx';
|
||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
import PreferenceStore from 'stores/preference_store.jsx';
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
import ChannelStore from 'stores/channel_store.jsx';
|
||||||
|
import PostStore from 'stores/post_store.jsx';
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||||
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
|
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
@@ -173,12 +174,25 @@ export default class NeedsTeam extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let channel = ChannelStore.getByName(this.props.params.channel);
|
||||||
|
if (channel == null) {
|
||||||
|
// the permalink view is not really tied to a particular channel but still needs it
|
||||||
|
const postId = PostStore.getFocusedPostId();
|
||||||
|
const post = PostStore.getEarliestPostFromPage(postId);
|
||||||
|
|
||||||
|
// the post take some time before being available on page load
|
||||||
|
if (post != null) {
|
||||||
|
channel = ChannelStore.get(post.channel_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='channel-view'>
|
<div className='channel-view'>
|
||||||
<ErrorBar/>
|
<ErrorBar/>
|
||||||
<WebrtcNotification/>
|
<WebrtcNotification/>
|
||||||
<div className='container-fluid'>
|
<div className='container-fluid'>
|
||||||
<SidebarRight/>
|
<SidebarRight channel={channel}/>
|
||||||
<SidebarRightMenu teamType={this.state.team.type}/>
|
<SidebarRightMenu teamType={this.state.team.type}/>
|
||||||
<WebrtcSidebar/>
|
<WebrtcSidebar/>
|
||||||
{content}
|
{content}
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ export default class PopoverListMembers extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='member-popover__container'>
|
||||||
<div
|
<div
|
||||||
id='member_popover'
|
id='member_popover'
|
||||||
className='member-popover__trigger'
|
className='member-popover__trigger'
|
||||||
@@ -243,13 +243,11 @@ export default class PopoverListMembers extends React.Component {
|
|||||||
AsyncClient.getProfilesInChannel(this.props.channel.id, 0);
|
AsyncClient.getProfilesInChannel(this.props.channel.id, 0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
{countText}
|
||||||
{countText}
|
<span
|
||||||
<span
|
className='fa fa-user'
|
||||||
className='fa fa-user'
|
aria-hidden='true'
|
||||||
aria-hidden='true'
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Overlay
|
<Overlay
|
||||||
rootClose={true}
|
rootClose={true}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export default class PostInfo extends React.Component {
|
|||||||
this.removePost = this.removePost.bind(this);
|
this.removePost = this.removePost.bind(this);
|
||||||
this.flagPost = this.flagPost.bind(this);
|
this.flagPost = this.flagPost.bind(this);
|
||||||
this.unflagPost = this.unflagPost.bind(this);
|
this.unflagPost = this.unflagPost.bind(this);
|
||||||
|
this.pinPost = this.pinPost.bind(this);
|
||||||
|
this.unpinPost = this.unpinPost.bind(this);
|
||||||
|
|
||||||
this.canEdit = false;
|
this.canEdit = false;
|
||||||
this.canDelete = false;
|
this.canDelete = false;
|
||||||
@@ -148,6 +150,42 @@ export default class PostInfo extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.post.is_pinned) {
|
||||||
|
dropdownContents.push(
|
||||||
|
<li
|
||||||
|
key='unpinLink'
|
||||||
|
role='presentation'
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href='#'
|
||||||
|
onClick={this.unpinPost}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='post_info.unpin'
|
||||||
|
defaultMessage='Un-pin from channel'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dropdownContents.push(
|
||||||
|
<li
|
||||||
|
key='pinLink'
|
||||||
|
role='presentation'
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href='#'
|
||||||
|
onClick={this.pinPost}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='post_info.pin'
|
||||||
|
defaultMessage='Pin to channel'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.canDelete) {
|
if (this.canDelete) {
|
||||||
dropdownContents.push(
|
dropdownContents.push(
|
||||||
<li
|
<li
|
||||||
@@ -250,6 +288,16 @@ export default class PostInfo extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pinPost(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
PostActions.pinPost(this.props.post.channel_id, this.props.post.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
unpinPost(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
PostActions.unpinPost(this.props.post.channel_id, this.props.post.id);
|
||||||
|
}
|
||||||
|
|
||||||
flagPost(e) {
|
flagPost(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
PostActions.flagPost(this.props.post.id);
|
PostActions.flagPost(this.props.post.id);
|
||||||
@@ -374,6 +422,18 @@ export default class PostInfo extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pinnedBadge;
|
||||||
|
if (post.is_pinned) {
|
||||||
|
pinnedBadge = (
|
||||||
|
<span className='post__pinned-badge'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='post_info.pinned'
|
||||||
|
defaultMessage='Pinned'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className='post__header--info'>
|
<ul className='post__header--info'>
|
||||||
<li className='col'>
|
<li className='col'>
|
||||||
@@ -384,6 +444,7 @@ export default class PostInfo extends React.Component {
|
|||||||
useMilitaryTime={this.props.useMilitaryTime}
|
useMilitaryTime={this.props.useMilitaryTime}
|
||||||
postId={post.id}
|
postId={post.id}
|
||||||
/>
|
/>
|
||||||
|
{pinnedBadge}
|
||||||
{flagTrigger}
|
{flagTrigger}
|
||||||
</li>
|
</li>
|
||||||
{options}
|
{options}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import ReactionListContainer from 'components/post_view/components/reaction_list
|
|||||||
import RhsDropdown from 'components/rhs_dropdown.jsx';
|
import RhsDropdown from 'components/rhs_dropdown.jsx';
|
||||||
|
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||||
import {flagPost, unflagPost} from 'actions/post_actions.jsx';
|
import {flagPost, unflagPost, pinPost, unpinPost} from 'actions/post_actions.jsx';
|
||||||
|
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
import TeamStore from 'stores/team_store.jsx';
|
||||||
|
|
||||||
@@ -36,6 +36,8 @@ export default class RhsComment extends React.Component {
|
|||||||
this.removePost = this.removePost.bind(this);
|
this.removePost = this.removePost.bind(this);
|
||||||
this.flagPost = this.flagPost.bind(this);
|
this.flagPost = this.flagPost.bind(this);
|
||||||
this.unflagPost = this.unflagPost.bind(this);
|
this.unflagPost = this.unflagPost.bind(this);
|
||||||
|
this.pinPost = this.pinPost.bind(this);
|
||||||
|
this.unpinPost = this.unpinPost.bind(this);
|
||||||
|
|
||||||
this.canEdit = false;
|
this.canEdit = false;
|
||||||
this.canDelete = false;
|
this.canDelete = false;
|
||||||
@@ -128,6 +130,16 @@ export default class RhsComment extends React.Component {
|
|||||||
unflagPost(this.props.post.id);
|
unflagPost(this.props.post.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pinPost(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
pinPost(this.props.post.channel_id, this.props.post.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
unpinPost(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
unpinPost(this.props.post.channel_id, this.props.post.id);
|
||||||
|
}
|
||||||
|
|
||||||
createDropdown() {
|
createDropdown() {
|
||||||
const post = this.props.post;
|
const post = this.props.post;
|
||||||
|
|
||||||
@@ -195,6 +207,42 @@ export default class RhsComment extends React.Component {
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (post.is_pinned) {
|
||||||
|
dropdownContents.push(
|
||||||
|
<li
|
||||||
|
key='rhs-comment-unpin'
|
||||||
|
role='presentation'
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href='#'
|
||||||
|
onClick={this.unpinPost}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='rhs_root.unpin'
|
||||||
|
defaultMessage='Un-pin from channel'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dropdownContents.push(
|
||||||
|
<li
|
||||||
|
key='rhs-comment-pin'
|
||||||
|
role='presentation'
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href='#'
|
||||||
|
onClick={this.pinPost}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='rhs_root.pin'
|
||||||
|
defaultMessage='Pin to channel'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.canDelete) {
|
if (this.canDelete) {
|
||||||
dropdownContents.push(
|
dropdownContents.push(
|
||||||
<li
|
<li
|
||||||
@@ -503,6 +551,18 @@ export default class RhsComment extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pinnedBadge;
|
||||||
|
if (post.is_pinned) {
|
||||||
|
pinnedBadge = (
|
||||||
|
<span className='post__pinned-badge'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='post_info.pinned'
|
||||||
|
defaultMessage='Pinned'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const timeOptions = {
|
const timeOptions = {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
@@ -524,6 +584,7 @@ export default class RhsComment extends React.Component {
|
|||||||
{botIndicator}
|
{botIndicator}
|
||||||
<li className='col'>
|
<li className='col'>
|
||||||
{this.renderTimeTag(post, timeOptions)}
|
{this.renderTimeTag(post, timeOptions)}
|
||||||
|
{pinnedBadge}
|
||||||
{flagTrigger}
|
{flagTrigger}
|
||||||
</li>
|
</li>
|
||||||
{options}
|
{options}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import UserStore from 'stores/user_store.jsx';
|
|||||||
import TeamStore from 'stores/team_store.jsx';
|
import TeamStore from 'stores/team_store.jsx';
|
||||||
|
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||||
import {flagPost, unflagPost} from 'actions/post_actions.jsx';
|
import {flagPost, unflagPost, pinPost, unpinPost} from 'actions/post_actions.jsx';
|
||||||
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import * as PostUtils from 'utils/post_utils.jsx';
|
import * as PostUtils from 'utils/post_utils.jsx';
|
||||||
@@ -35,6 +35,8 @@ export default class RhsRootPost extends React.Component {
|
|||||||
this.handlePermalink = this.handlePermalink.bind(this);
|
this.handlePermalink = this.handlePermalink.bind(this);
|
||||||
this.flagPost = this.flagPost.bind(this);
|
this.flagPost = this.flagPost.bind(this);
|
||||||
this.unflagPost = this.unflagPost.bind(this);
|
this.unflagPost = this.unflagPost.bind(this);
|
||||||
|
this.pinPost = this.pinPost.bind(this);
|
||||||
|
this.unpinPost = this.unpinPost.bind(this);
|
||||||
|
|
||||||
this.canEdit = false;
|
this.canEdit = false;
|
||||||
this.canDelete = false;
|
this.canDelete = false;
|
||||||
@@ -143,6 +145,16 @@ export default class RhsRootPost extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pinPost(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
pinPost(this.props.post.channel_id, this.props.post.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
unpinPost(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
unpinPost(this.props.post.channel_id, this.props.post.id);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const post = this.props.post;
|
const post = this.props.post;
|
||||||
const user = this.props.user;
|
const user = this.props.user;
|
||||||
@@ -240,6 +252,42 @@ export default class RhsRootPost extends React.Component {
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (post.is_pinned) {
|
||||||
|
dropdownContents.push(
|
||||||
|
<li
|
||||||
|
key='rhs-root-unpin'
|
||||||
|
role='presentation'
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href='#'
|
||||||
|
onClick={this.unpinPost}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='rhs_root.unpin'
|
||||||
|
defaultMessage='Un-pin from channel'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dropdownContents.push(
|
||||||
|
<li
|
||||||
|
key='rhs-root-pin'
|
||||||
|
role='presentation'
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href='#'
|
||||||
|
onClick={this.pinPost}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='rhs_root.pin'
|
||||||
|
defaultMessage='Pin to channel'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.canDelete) {
|
if (this.canDelete) {
|
||||||
dropdownContents.push(
|
dropdownContents.push(
|
||||||
<li
|
<li
|
||||||
@@ -450,6 +498,18 @@ export default class RhsRootPost extends React.Component {
|
|||||||
flagFunc = this.flagPost;
|
flagFunc = this.flagPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pinnedBadge;
|
||||||
|
if (post.is_pinned) {
|
||||||
|
pinnedBadge = (
|
||||||
|
<span className='post__pinned-badge'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='post_info.pinned'
|
||||||
|
defaultMessage='Pinned'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const timeOptions = {
|
const timeOptions = {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
@@ -470,6 +530,7 @@ export default class RhsRootPost extends React.Component {
|
|||||||
{botIndicator}
|
{botIndicator}
|
||||||
<li className='col'>
|
<li className='col'>
|
||||||
{this.renderTimeTag(post, timeOptions)}
|
{this.renderTimeTag(post, timeOptions)}
|
||||||
|
{pinnedBadge}
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
key={'rootpostflagtooltipkey' + flagVisible}
|
key={'rootpostflagtooltipkey' + flagVisible}
|
||||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||||
|
|||||||
@@ -213,6 +213,31 @@ export default class SearchResults extends React.Component {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else if (this.props.isPinnedPosts && noResults) {
|
||||||
|
ctls = (
|
||||||
|
<div className='sidebar--right__subheader'>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<FormattedHTMLMessage
|
||||||
|
id='search_results.usagePin1'
|
||||||
|
defaultMessage='There are no pinned messages yet.'
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<FormattedHTMLMessage
|
||||||
|
id='search_results.usagePin2'
|
||||||
|
defaultMessage={'You can pin a message by clicking the "Pin to channel" option from the message\'s menu.'}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<FormattedHTMLMessage
|
||||||
|
id='search_results.usagePin3'
|
||||||
|
defaultMessage='Pinned messages are accessible by all channel members and are a way to mark messages for future reference.'
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
} else if (!searchTerm && noResults) {
|
} else if (!searchTerm && noResults) {
|
||||||
ctls = (
|
ctls = (
|
||||||
<div className='sidebar--right__subheader'>
|
<div className='sidebar--right__subheader'>
|
||||||
@@ -289,6 +314,8 @@ export default class SearchResults extends React.Component {
|
|||||||
toggleSize={this.props.toggleSize}
|
toggleSize={this.props.toggleSize}
|
||||||
shrink={this.props.shrink}
|
shrink={this.props.shrink}
|
||||||
isFlaggedPosts={this.props.isFlaggedPosts}
|
isFlaggedPosts={this.props.isFlaggedPosts}
|
||||||
|
isPinnedPosts={this.props.isPinnedPosts}
|
||||||
|
channelDisplayName={this.props.channelDisplayName}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
id='search-items-container'
|
id='search-items-container'
|
||||||
@@ -307,5 +334,7 @@ SearchResults.propTypes = {
|
|||||||
useMilitaryTime: React.PropTypes.bool.isRequired,
|
useMilitaryTime: React.PropTypes.bool.isRequired,
|
||||||
toggleSize: React.PropTypes.func,
|
toggleSize: React.PropTypes.func,
|
||||||
shrink: React.PropTypes.func,
|
shrink: React.PropTypes.func,
|
||||||
isFlaggedPosts: React.PropTypes.bool
|
isFlaggedPosts: React.PropTypes.bool,
|
||||||
|
isPinnedPosts: React.PropTypes.bool,
|
||||||
|
channelDisplayName: React.PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,6 +79,16 @@ export default class SearchResultsHeader extends React.Component {
|
|||||||
defaultMessage='Flagged Posts'
|
defaultMessage='Flagged Posts'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (this.props.isPinnedPosts) {
|
||||||
|
title = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='search_header.title4'
|
||||||
|
defaultMessage='Pinned posts in {channelDisplayName}'
|
||||||
|
values={{
|
||||||
|
channelDisplayName: this.props.channelDisplayName
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -131,5 +141,7 @@ SearchResultsHeader.propTypes = {
|
|||||||
isMentionSearch: React.PropTypes.bool,
|
isMentionSearch: React.PropTypes.bool,
|
||||||
toggleSize: React.PropTypes.func,
|
toggleSize: React.PropTypes.func,
|
||||||
shrink: React.PropTypes.func,
|
shrink: React.PropTypes.func,
|
||||||
isFlaggedPosts: React.PropTypes.bool
|
isFlaggedPosts: React.PropTypes.bool,
|
||||||
|
isPinnedPosts: React.PropTypes.bool,
|
||||||
|
channelDisplayName: React.PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -289,6 +289,18 @@ export default class SearchResultsItem extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pinnedBadge;
|
||||||
|
if (post.is_pinned) {
|
||||||
|
pinnedBadge = (
|
||||||
|
<span className='post__pinned-badge'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='post_info.pinned'
|
||||||
|
defaultMessage='Pinned'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='search-item__container'>
|
<div className='search-item__container'>
|
||||||
<div className='date-separator'>
|
<div className='date-separator'>
|
||||||
@@ -322,6 +334,7 @@ export default class SearchResultsItem extends React.Component {
|
|||||||
{botIndicator}
|
{botIndicator}
|
||||||
<li className='col'>
|
<li className='col'>
|
||||||
{this.renderTimeTag(post)}
|
{this.renderTimeTag(post)}
|
||||||
|
{pinnedBadge}
|
||||||
{flagContent}
|
{flagContent}
|
||||||
</li>
|
</li>
|
||||||
{rhsControls}
|
{rhsControls}
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import UserStore from 'stores/user_store.jsx';
|
|||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
import PreferenceStore from 'stores/preference_store.jsx';
|
||||||
import WebrtcStore from 'stores/webrtc_store.jsx';
|
import WebrtcStore from 'stores/webrtc_store.jsx';
|
||||||
|
|
||||||
import {getFlaggedPosts} from 'actions/post_actions.jsx';
|
import {getFlaggedPosts, getPinnedPosts} from 'actions/post_actions.jsx';
|
||||||
import {trackEvent} from 'actions/diagnostics_actions.jsx';
|
import {trackEvent} from 'actions/diagnostics_actions.jsx';
|
||||||
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
|
|
||||||
import React from 'react';
|
import React, {PropTypes} from 'react';
|
||||||
|
|
||||||
export default class SidebarRight extends React.Component {
|
export default class SidebarRight extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -27,6 +27,7 @@ export default class SidebarRight extends React.Component {
|
|||||||
|
|
||||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||||
this.onSelectedChange = this.onSelectedChange.bind(this);
|
this.onSelectedChange = this.onSelectedChange.bind(this);
|
||||||
|
this.onPostPinnedChange = this.onPostPinnedChange.bind(this);
|
||||||
this.onSearchChange = this.onSearchChange.bind(this);
|
this.onSearchChange = this.onSearchChange.bind(this);
|
||||||
this.onUserChange = this.onUserChange.bind(this);
|
this.onUserChange = this.onUserChange.bind(this);
|
||||||
this.onShowSearch = this.onShowSearch.bind(this);
|
this.onShowSearch = this.onShowSearch.bind(this);
|
||||||
@@ -39,6 +40,7 @@ export default class SidebarRight extends React.Component {
|
|||||||
searchVisible: SearchStore.getSearchResults() !== null,
|
searchVisible: SearchStore.getSearchResults() !== null,
|
||||||
isMentionSearch: SearchStore.getIsMentionSearch(),
|
isMentionSearch: SearchStore.getIsMentionSearch(),
|
||||||
isFlaggedPosts: SearchStore.getIsFlaggedPosts(),
|
isFlaggedPosts: SearchStore.getIsFlaggedPosts(),
|
||||||
|
isPinnedPosts: SearchStore.getIsPinnedPosts(),
|
||||||
postRightVisible: Boolean(PostStore.getSelectedPost()),
|
postRightVisible: Boolean(PostStore.getSelectedPost()),
|
||||||
expanded: false,
|
expanded: false,
|
||||||
fromSearch: false,
|
fromSearch: false,
|
||||||
@@ -50,6 +52,7 @@ export default class SidebarRight extends React.Component {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
SearchStore.addSearchChangeListener(this.onSearchChange);
|
SearchStore.addSearchChangeListener(this.onSearchChange);
|
||||||
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
|
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
|
||||||
|
PostStore.addPostPinnedChangeListener(this.onPostPinnedChange);
|
||||||
SearchStore.addShowSearchListener(this.onShowSearch);
|
SearchStore.addShowSearchListener(this.onShowSearch);
|
||||||
UserStore.addChangeListener(this.onUserChange);
|
UserStore.addChangeListener(this.onUserChange);
|
||||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
||||||
@@ -59,6 +62,7 @@ export default class SidebarRight extends React.Component {
|
|||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
SearchStore.removeSearchChangeListener(this.onSearchChange);
|
SearchStore.removeSearchChangeListener(this.onSearchChange);
|
||||||
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
|
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
|
||||||
|
PostStore.removePostPinnedChangeListener(this.onPostPinnedChange);
|
||||||
SearchStore.removeShowSearchListener(this.onShowSearch);
|
SearchStore.removeShowSearchListener(this.onShowSearch);
|
||||||
UserStore.removeChangeListener(this.onUserChange);
|
UserStore.removeChangeListener(this.onUserChange);
|
||||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
||||||
@@ -137,6 +141,12 @@ export default class SidebarRight extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPostPinnedChange() {
|
||||||
|
if (this.props.channel && this.state.isPinnedPosts) {
|
||||||
|
getPinnedPosts(this.props.channel.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onShrink() {
|
onShrink() {
|
||||||
this.setState({
|
this.setState({
|
||||||
expanded: false
|
expanded: false
|
||||||
@@ -147,7 +157,8 @@ export default class SidebarRight extends React.Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
searchVisible: SearchStore.getSearchResults() !== null,
|
searchVisible: SearchStore.getSearchResults() !== null,
|
||||||
isMentionSearch: SearchStore.getIsMentionSearch(),
|
isMentionSearch: SearchStore.getIsMentionSearch(),
|
||||||
isFlaggedPosts: SearchStore.getIsFlaggedPosts()
|
isFlaggedPosts: SearchStore.getIsFlaggedPosts(),
|
||||||
|
isPinnedPosts: SearchStore.getIsPinnedPosts()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,9 +193,11 @@ export default class SidebarRight extends React.Component {
|
|||||||
<SearchResults
|
<SearchResults
|
||||||
isMentionSearch={this.state.isMentionSearch}
|
isMentionSearch={this.state.isMentionSearch}
|
||||||
isFlaggedPosts={this.state.isFlaggedPosts}
|
isFlaggedPosts={this.state.isFlaggedPosts}
|
||||||
|
isPinnedPosts={this.state.isPinnedPosts}
|
||||||
useMilitaryTime={this.state.useMilitaryTime}
|
useMilitaryTime={this.state.useMilitaryTime}
|
||||||
toggleSize={this.toggleSize}
|
toggleSize={this.toggleSize}
|
||||||
shrink={this.onShrink}
|
shrink={this.onShrink}
|
||||||
|
channelDisplayName={this.props.channel ? this.props.channel.display_name : ''}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (this.state.postRightVisible) {
|
} else if (this.state.postRightVisible) {
|
||||||
@@ -222,3 +235,7 @@ export default class SidebarRight extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SidebarRight.propTypes = {
|
||||||
|
channel: PropTypes.object
|
||||||
|
};
|
||||||
|
|||||||
@@ -1670,6 +1670,7 @@
|
|||||||
"navbar.toggle1": "Toggle sidebar",
|
"navbar.toggle1": "Toggle sidebar",
|
||||||
"navbar.toggle2": "Toggle sidebar",
|
"navbar.toggle2": "Toggle sidebar",
|
||||||
"navbar.viewInfo": "View Info",
|
"navbar.viewInfo": "View Info",
|
||||||
|
"navbar.viewPinnedPosts": "View Pinned Posts",
|
||||||
"navbar_dropdown.about": "About Mattermost",
|
"navbar_dropdown.about": "About Mattermost",
|
||||||
"navbar_dropdown.accountSettings": "Account Settings",
|
"navbar_dropdown.accountSettings": "Account Settings",
|
||||||
"navbar_dropdown.console": "System Console",
|
"navbar_dropdown.console": "System Console",
|
||||||
@@ -1726,6 +1727,9 @@
|
|||||||
"post_info.permalink": "Permalink",
|
"post_info.permalink": "Permalink",
|
||||||
"post_info.reply": "Reply",
|
"post_info.reply": "Reply",
|
||||||
"post_info.system": "System",
|
"post_info.system": "System",
|
||||||
|
"post_info.pin": "Pin to channel",
|
||||||
|
"post_info.unpin": "Un-pin from channel",
|
||||||
|
"post_info.pinned": "Pinned",
|
||||||
"post_message_view.edited": "(edited)",
|
"post_message_view.edited": "(edited)",
|
||||||
"posts_view.loadMore": "Load more messages",
|
"posts_view.loadMore": "Load more messages",
|
||||||
"posts_view.newMsg": "New Messages",
|
"posts_view.newMsg": "New Messages",
|
||||||
@@ -1778,11 +1782,14 @@
|
|||||||
"rhs_root.mobile.flag": "Flag",
|
"rhs_root.mobile.flag": "Flag",
|
||||||
"rhs_root.mobile.unflag": "Unflag",
|
"rhs_root.mobile.unflag": "Unflag",
|
||||||
"rhs_root.permalink": "Permalink",
|
"rhs_root.permalink": "Permalink",
|
||||||
|
"rhs_root.pin": "Pin to channel",
|
||||||
|
"rhs_root.unpin": "Un-pin from channel",
|
||||||
"search_bar.search": "Search",
|
"search_bar.search": "Search",
|
||||||
"search_bar.usage": "<h4>Search Options</h4><ul><li><span>Use </span><b>\"quotation marks\"</b><span> to search for phrases</span></li><li><span>Use </span><b>from:</b><span> to find posts from specific users and </span><b>in:</b><span> to find posts in specific channels</span></li></ul>",
|
"search_bar.usage": "<h4>Search Options</h4><ul><li><span>Use </span><b>\"quotation marks\"</b><span> to search for phrases</span></li><li><span>Use </span><b>from:</b><span> to find posts from specific users and </span><b>in:</b><span> to find posts in specific channels</span></li></ul>",
|
||||||
"search_header.results": "Search Results",
|
"search_header.results": "Search Results",
|
||||||
"search_header.title2": "Recent Mentions",
|
"search_header.title2": "Recent Mentions",
|
||||||
"search_header.title3": "Flagged Posts",
|
"search_header.title3": "Flagged Posts",
|
||||||
|
"search_header.title4": "Pinned posts in {channelDisplayName}",
|
||||||
"search_item.direct": "Direct Message (with {username})",
|
"search_item.direct": "Direct Message (with {username})",
|
||||||
"search_item.jump": "Jump",
|
"search_item.jump": "Jump",
|
||||||
"search_results.because": "<ul><li>If you're searching a partial phrase (ex. searching \"rea\", looking for \"reach\" or \"reaction\"), append a * to your search term.</li><li>Two letter searches and common words like \"this\", \"a\" and \"is\" won't appear in search results due to excessive results returned.</li></ul>",
|
"search_results.because": "<ul><li>If you're searching a partial phrase (ex. searching \"rea\", looking for \"reach\" or \"reaction\"), append a * to your search term.</li><li>Two letter searches and common words like \"this\", \"a\" and \"is\" won't appear in search results due to excessive results returned.</li></ul>",
|
||||||
@@ -1792,6 +1799,9 @@
|
|||||||
"search_results.usageFlag2": "You can add a flag to messages and comments by clicking the ",
|
"search_results.usageFlag2": "You can add a flag to messages and comments by clicking the ",
|
||||||
"search_results.usageFlag3": " icon next to the timestamp.",
|
"search_results.usageFlag3": " icon next to the timestamp.",
|
||||||
"search_results.usageFlag4": "Flags are a way to mark messages for follow up. Your flags are personal, and cannot be seen by other users.",
|
"search_results.usageFlag4": "Flags are a way to mark messages for follow up. Your flags are personal, and cannot be seen by other users.",
|
||||||
|
"search_results.usagePin1": "There are no pinned messages yet.",
|
||||||
|
"search_results.usagePin2": "You can pin a message by clicking the \"Pin to channel\" option from the message's menu.",
|
||||||
|
"search_results.usagePin3": "Pinned messages are accessible by all channel members and are a way to mark messages for future reference.",
|
||||||
"setting_item_max.cancel": "Cancel",
|
"setting_item_max.cancel": "Cancel",
|
||||||
"setting_item_max.save": "Save",
|
"setting_item_max.save": "Save",
|
||||||
"setting_item_min.edit": "Edit",
|
"setting_item_min.edit": "Edit",
|
||||||
|
|||||||
@@ -9,10 +9,20 @@
|
|||||||
.search-btns {
|
.search-btns {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.header-list__members {
|
.header-list__right {
|
||||||
|
// the negative margin-right is used
|
||||||
|
// to prevent the icons in the header from
|
||||||
|
// moving to the left when the RHS is open
|
||||||
|
//
|
||||||
|
// the below z-index is used to ensure the icons
|
||||||
|
// stays on the top and don't get hidden by the
|
||||||
|
// search's input block
|
||||||
|
position: relative;
|
||||||
|
z-index: 6;
|
||||||
|
|
||||||
margin-right: -18px;
|
margin-right: -18px;
|
||||||
float: right;
|
|
||||||
padding-right: 0px !important;
|
padding-right: 0px !important;
|
||||||
|
float: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,10 +33,20 @@
|
|||||||
.search-btns {
|
.search-btns {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.header-list__members {
|
.header-list__right {
|
||||||
|
// the negative margin-right is used
|
||||||
|
// to prevent the icons in the header from
|
||||||
|
// moving to the left when the RHS is open
|
||||||
|
//
|
||||||
|
// the below z-index is used to ensure the icons
|
||||||
|
// stays on the top and don't get hidden by the
|
||||||
|
// search's input block
|
||||||
|
position: relative;
|
||||||
|
z-index: 6;
|
||||||
|
|
||||||
margin-right: -18px;
|
margin-right: -18px;
|
||||||
float: right;
|
|
||||||
padding-right: 0px !important;
|
padding-right: 0px !important;
|
||||||
|
float: right
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,26 +7,43 @@
|
|||||||
line-height: 56px;
|
line-height: 56px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.member-popover__trigger {
|
.member-popover__trigger,
|
||||||
|
.pinned-posts-button {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 30px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
min-width: 60px;
|
margin-left: 10px;
|
||||||
padding-right: 10px;
|
text-align: center;
|
||||||
text-align: right;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
.fa {
|
.fa {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-popover__container,
|
||||||
|
.member-popover__trigger {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-popover__trigger {
|
||||||
|
.fa {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pinned-posts-button svg {
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
&.alt {
|
&.alt {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
th {
|
th {
|
||||||
font-weight: normal !important;
|
font-weight: normal !important;
|
||||||
|
|
||||||
&.header-list__members {
|
&.header-list__right {
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1343,15 +1343,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-indicator {
|
.bot-indicator,
|
||||||
|
.post__pinned-badge {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 2px 10px 0 -4px;
|
|
||||||
padding: 1px 4px;
|
padding: 1px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bot-indicator {
|
||||||
|
margin: 2px 10px 0 -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post__pinned-badge {
|
||||||
|
margin-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
.permalink-text {
|
.permalink-text {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.move--left {
|
||||||
|
.post {
|
||||||
|
&.post--root,
|
||||||
|
&.other--root {
|
||||||
|
.post__header {
|
||||||
|
padding-right: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.post--comment {
|
||||||
|
.post__header {
|
||||||
|
padding-right: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
@@ -273,6 +274,7 @@
|
|||||||
|
|
||||||
.post__header {
|
.post__header {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
padding-right: 70px;
|
||||||
|
|
||||||
.col__reply {
|
.col__reply {
|
||||||
top: -3px;
|
top: -3px;
|
||||||
|
|||||||
@@ -127,6 +127,15 @@
|
|||||||
top: auto;
|
top: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.move--left,
|
||||||
|
&.webrtc--show,
|
||||||
|
&.move--right {
|
||||||
|
.header-list__right {
|
||||||
|
// hide it behind the RHS
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.post {
|
.post {
|
||||||
.attachment {
|
.attachment {
|
||||||
@@ -182,6 +191,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar--right__title {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 300px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inner-wrap {
|
.inner-wrap {
|
||||||
@@ -213,6 +230,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post__pinned-badge {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.post--thread) {
|
&:not(.post--thread) {
|
||||||
padding: 5px .5em 0 77px;
|
padding: 5px .5em 0 77px;
|
||||||
|
|
||||||
@@ -359,9 +381,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.post__header {
|
.post__header {
|
||||||
|
float: left;
|
||||||
|
padding-top: 3px;
|
||||||
|
|
||||||
.col__reply {
|
.col__reply {
|
||||||
top: -21px;
|
top: -21px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post__pinned-badge {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.post--compact) {
|
&:not(.post--compact) {
|
||||||
@@ -381,6 +410,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.post--comment:not(.post--compact) {
|
||||||
|
.post__pinned-badge {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const FOCUSED_POST_CHANGE = 'focused_post_change';
|
|||||||
const EDIT_POST_EVENT = 'edit_post';
|
const EDIT_POST_EVENT = 'edit_post';
|
||||||
const POSTS_VIEW_JUMP_EVENT = 'post_list_jump';
|
const POSTS_VIEW_JUMP_EVENT = 'post_list_jump';
|
||||||
const SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
|
const SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
|
||||||
|
const POST_PINNED_CHANGE_EVENT = 'post_pinned_change';
|
||||||
|
|
||||||
class PostStoreClass extends EventEmitter {
|
class PostStoreClass extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -259,22 +260,42 @@ class PostStoreClass extends EventEmitter {
|
|||||||
this.postsInfo[id].postList = combinedPosts;
|
this.postsInfo[id].postList = combinedPosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
focusedPostListHasPost(id) {
|
||||||
|
const focusedPostId = this.getFocusedPostId();
|
||||||
|
if (focusedPostId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusedPostList = makePostListNonNull(this.getAllPosts(focusedPostId));
|
||||||
|
return focusedPostList.posts.hasOwnProperty(id);
|
||||||
|
}
|
||||||
|
|
||||||
storePost(post, isNewPost = false) {
|
storePost(post, isNewPost = false) {
|
||||||
const postList = makePostListNonNull(this.getAllPosts(post.channel_id));
|
const ids = [
|
||||||
|
post.channel_id
|
||||||
|
];
|
||||||
|
|
||||||
if (post.pending_post_id !== '') {
|
// update the post in the permalink view if it's there
|
||||||
this.removePendingPost(post.channel_id, post.pending_post_id);
|
if (!isNewPost && this.focusedPostListHasPost(post.id)) {
|
||||||
|
ids.push(this.getFocusedPostId());
|
||||||
}
|
}
|
||||||
|
|
||||||
post.pending_post_id = '';
|
ids.forEach((id) => {
|
||||||
|
const postList = makePostListNonNull(this.getAllPosts(id));
|
||||||
|
if (post.pending_post_id !== '') {
|
||||||
|
this.removePendingPost(post.channel_id, post.pending_post_id);
|
||||||
|
}
|
||||||
|
|
||||||
postList.posts[post.id] = post;
|
post.pending_post_id = '';
|
||||||
if (isNewPost && postList.order.indexOf(post.id) === -1) {
|
|
||||||
postList.order.unshift(post.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.makePostsInfo(post.channel_id);
|
postList.posts[post.id] = post;
|
||||||
this.postsInfo[post.channel_id].postList = postList;
|
if (isNewPost && postList.order.indexOf(post.id) === -1) {
|
||||||
|
postList.order.unshift(post.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.makePostsInfo(post.channel_id);
|
||||||
|
this.postsInfo[id].postList = postList;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
storeFocusedPost(postId, channelId, postList) {
|
storeFocusedPost(postId, channelId, postList) {
|
||||||
@@ -500,6 +521,18 @@ class PostStoreClass extends EventEmitter {
|
|||||||
this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
|
this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitPostPinnedChange() {
|
||||||
|
this.emit(POST_PINNED_CHANGE_EVENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
addPostPinnedChangeListener(callback) {
|
||||||
|
this.on(POST_PINNED_CHANGE_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
removePostPinnedChangeListener(callback) {
|
||||||
|
this.removeListener(POST_PINNED_CHANGE_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentUsersLatestPost(channelId, rootId) {
|
getCurrentUsersLatestPost(channelId, rootId) {
|
||||||
const userId = UserStore.getCurrentId();
|
const userId = UserStore.getCurrentId();
|
||||||
|
|
||||||
@@ -686,6 +719,10 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
|
|||||||
PostStore.storeSelectedPostId(action.postId);
|
PostStore.storeSelectedPostId(action.postId);
|
||||||
PostStore.emitSelectedPostChange(action.from_search, action.from_flagged_posts);
|
PostStore.emitSelectedPostChange(action.from_search, action.from_flagged_posts);
|
||||||
break;
|
break;
|
||||||
|
case ActionTypes.RECEIVED_POST_PINNED:
|
||||||
|
case ActionTypes.RECEIVED_POST_UNPINNED:
|
||||||
|
PostStore.emitPostPinnedChange();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class SearchStoreClass extends EventEmitter {
|
|||||||
this.searchResults = null;
|
this.searchResults = null;
|
||||||
this.isMentionSearch = false;
|
this.isMentionSearch = false;
|
||||||
this.isFlaggedPosts = false;
|
this.isFlaggedPosts = false;
|
||||||
|
this.isPinnedPosts = false;
|
||||||
this.isVisible = false;
|
this.isVisible = false;
|
||||||
this.searchTerm = '';
|
this.searchTerm = '';
|
||||||
}
|
}
|
||||||
@@ -83,6 +84,10 @@ class SearchStoreClass extends EventEmitter {
|
|||||||
return this.isFlaggedPosts;
|
return this.isFlaggedPosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getIsPinnedPosts() {
|
||||||
|
return this.isPinnedPosts;
|
||||||
|
}
|
||||||
|
|
||||||
storeSearchTerm(term) {
|
storeSearchTerm(term) {
|
||||||
this.searchTerm = term;
|
this.searchTerm = term;
|
||||||
}
|
}
|
||||||
@@ -91,10 +96,11 @@ class SearchStoreClass extends EventEmitter {
|
|||||||
return this.searchTerm;
|
return this.searchTerm;
|
||||||
}
|
}
|
||||||
|
|
||||||
storeSearchResults(results, isMentionSearch, isFlaggedPosts) {
|
storeSearchResults(results, isMentionSearch, isFlaggedPosts, isPinnedPosts) {
|
||||||
this.searchResults = results;
|
this.searchResults = results;
|
||||||
this.isMentionSearch = isMentionSearch;
|
this.isMentionSearch = isMentionSearch;
|
||||||
this.isFlaggedPosts = isFlaggedPosts;
|
this.isFlaggedPosts = isFlaggedPosts;
|
||||||
|
this.isPinnedPosts = isPinnedPosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
deletePost(post) {
|
deletePost(post) {
|
||||||
@@ -120,7 +126,7 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => {
|
|||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.RECEIVED_SEARCH:
|
case ActionTypes.RECEIVED_SEARCH:
|
||||||
SearchStore.storeSearchResults(action.results, action.is_mention_search, action.is_flagged_posts);
|
SearchStore.storeSearchResults(action.results, action.is_mention_search, action.is_flagged_posts, action.is_pinned_posts);
|
||||||
SearchStore.emitSearchChange();
|
SearchStore.emitSearchChange();
|
||||||
break;
|
break;
|
||||||
case ActionTypes.RECEIVED_SEARCH_TERM:
|
case ActionTypes.RECEIVED_SEARCH_TERM:
|
||||||
|
|||||||
@@ -1607,6 +1607,40 @@ export function deleteEmoji(id) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pinPost(channelId, reaction) {
|
||||||
|
Client.pinPost(
|
||||||
|
channelId,
|
||||||
|
reaction,
|
||||||
|
() => {
|
||||||
|
// the "post_edited" websocket event take cares of updating the posts
|
||||||
|
// the action below is mostly dispatched for the RHS to update
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECEIVED_POST_PINNED
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
dispatchError(err, 'pinPost');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unpinPost(channelId, reaction) {
|
||||||
|
Client.unpinPost(
|
||||||
|
channelId,
|
||||||
|
reaction,
|
||||||
|
() => {
|
||||||
|
// the "post_edited" websocket event take cares of updating the posts
|
||||||
|
// the action below is mostly dispatched for the RHS to update
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECEIVED_POST_UNPINNED
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
dispatchError(err, 'unpinPost');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function saveReaction(channelId, reaction) {
|
export function saveReaction(channelId, reaction) {
|
||||||
Client.saveReaction(
|
Client.saveReaction(
|
||||||
channelId,
|
channelId,
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ export const ActionTypes = keyMirror({
|
|||||||
RECEIVED_POST_SELECTED: null,
|
RECEIVED_POST_SELECTED: null,
|
||||||
RECEIVED_MENTION_DATA: null,
|
RECEIVED_MENTION_DATA: null,
|
||||||
RECEIVED_ADD_MENTION: null,
|
RECEIVED_ADD_MENTION: null,
|
||||||
|
RECEIVED_POST_PINNED: null,
|
||||||
|
RECEIVED_POST_UNPINNED: null,
|
||||||
|
|
||||||
RECEIVED_PROFILES: null,
|
RECEIVED_PROFILES: null,
|
||||||
RECEIVED_PROFILES_IN_TEAM: null,
|
RECEIVED_PROFILES_IN_TEAM: null,
|
||||||
@@ -419,6 +421,7 @@ export const Constants = {
|
|||||||
REPLY_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'viewBox='-158 242 18 18' style='enable-background:new -158 242 18 18;' xml:space='preserve'> <path d='M-142.2,252.6c-2-3-4.8-4.7-8.3-4.8v-3.3c0-0.2-0.1-0.3-0.2-0.3s-0.3,0-0.4,0.1l-6.9,6.2c-0.1,0.1-0.1,0.2-0.1,0.3 c0,0.1,0,0.2,0.1,0.3l6.9,6.4c0.1,0.1,0.3,0.1,0.4,0.1c0.1-0.1,0.2-0.2,0.2-0.4v-3.8c4.2,0,7.4,0.4,9.6,4.4c0.1,0.1,0.2,0.2,0.3,0.2 c0,0,0.1,0,0.1,0c0.2-0.1,0.3-0.3,0.2-0.4C-140.2,257.3-140.6,255-142.2,252.6z M-150.8,252.5c-0.2,0-0.4,0.2-0.4,0.4v3.3l-6-5.5 l6-5.3v2.8c0,0.2,0.2,0.4,0.4,0.4c3.3,0,6,1.5,8,4.5c0.5,0.8,0.9,1.6,1.2,2.3C-144,252.8-147.1,252.5-150.8,252.5z'/> </svg>",
|
REPLY_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'viewBox='-158 242 18 18' style='enable-background:new -158 242 18 18;' xml:space='preserve'> <path d='M-142.2,252.6c-2-3-4.8-4.7-8.3-4.8v-3.3c0-0.2-0.1-0.3-0.2-0.3s-0.3,0-0.4,0.1l-6.9,6.2c-0.1,0.1-0.1,0.2-0.1,0.3 c0,0.1,0,0.2,0.1,0.3l6.9,6.4c0.1,0.1,0.3,0.1,0.4,0.1c0.1-0.1,0.2-0.2,0.2-0.4v-3.8c4.2,0,7.4,0.4,9.6,4.4c0.1,0.1,0.2,0.2,0.3,0.2 c0,0,0.1,0,0.1,0c0.2-0.1,0.3-0.3,0.2-0.4C-140.2,257.3-140.6,255-142.2,252.6z M-150.8,252.5c-0.2,0-0.4,0.2-0.4,0.4v3.3l-6-5.5 l6-5.3v2.8c0,0.2,0.2,0.4,0.4,0.4c3.3,0,6,1.5,8,4.5c0.5,0.8,0.9,1.6,1.2,2.3C-144,252.8-147.1,252.5-150.8,252.5z'/> </svg>",
|
||||||
SCROLL_BOTTOM_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'viewBox='-239 239 21 23' style='enable-background:new -239 239 21 23;' xml:space='preserve'> <path d='M-239,241.4l2.4-2.4l8.1,8.2l8.1-8.2l2.4,2.4l-10.5,10.6L-239,241.4z M-228.5,257.2l8.1-8.2l2.4,2.4l-10.5,10.6l-10.5-10.6 l2.4-2.4L-228.5,257.2z'/> </svg>",
|
SCROLL_BOTTOM_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'viewBox='-239 239 21 23' style='enable-background:new -239 239 21 23;' xml:space='preserve'> <path d='M-239,241.4l2.4-2.4l8.1,8.2l8.1-8.2l2.4,2.4l-10.5,10.6L-239,241.4z M-228.5,257.2l8.1-8.2l2.4,2.4l-10.5,10.6l-10.5-10.6 l2.4-2.4L-228.5,257.2z'/> </svg>",
|
||||||
VIDEO_ICON: "<svg width='55%'height='100%'viewBox='0 0 13 8'> <g transform='matrix(1,0,0,1,-507,-146)'> <g transform='matrix(0.0133892,0,0,0.014499,500.635,142.838)'> <path d='M1158,547.286L1158,644.276C1158,684.245 1125.55,716.694 1085.58,716.694L579.341,716.694C539.372,716.694 506.922,684.245 506.922,644.276L506.922,306.322C506.922,266.353 539.371,233.904 579.341,233.903L1085.58,233.903C1125.55,233.904 1158,266.353 1158,306.322L1158,402.939L1359.75,253.14C1365.83,248.362 1373.43,245.973 1382.56,245.973C1386.61,245.973 1390.83,246.602 1395.22,247.859C1408.4,252.134 1414.99,259.552 1414.99,270.113L1414.99,680.485C1414.99,691.046 1408.4,698.464 1395.22,702.739C1390.83,703.996 1386.61,704.624 1382.56,704.624C1373.43,704.624 1365.83,702.236 1359.75,697.458L1158,547.286Z'/> </g> </g> </svg>",
|
VIDEO_ICON: "<svg width='55%'height='100%'viewBox='0 0 13 8'> <g transform='matrix(1,0,0,1,-507,-146)'> <g transform='matrix(0.0133892,0,0,0.014499,500.635,142.838)'> <path d='M1158,547.286L1158,644.276C1158,684.245 1125.55,716.694 1085.58,716.694L579.341,716.694C539.372,716.694 506.922,684.245 506.922,644.276L506.922,306.322C506.922,266.353 539.371,233.904 579.341,233.903L1085.58,233.903C1125.55,233.904 1158,266.353 1158,306.322L1158,402.939L1359.75,253.14C1365.83,248.362 1373.43,245.973 1382.56,245.973C1386.61,245.973 1390.83,246.602 1395.22,247.859C1408.4,252.134 1414.99,259.552 1414.99,270.113L1414.99,680.485C1414.99,691.046 1408.4,698.464 1395.22,702.739C1390.83,703.996 1386.61,704.624 1382.56,704.624C1373.43,704.624 1365.83,702.236 1359.75,697.458L1158,547.286Z'/> </g> </g> </svg>",
|
||||||
|
PIN_ICON: "<svg width='16px' height='16px' viewBox='0 0 25 25' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd' clip-rule='evenodd' stroke-linejoin='round' stroke-miterlimit='1.414'><path d='M24.78 9.236L15.863.316l-1.487 4.46-4.46 4.46L8.43 7.75 3.972 9.235l4.458 4.458L.776 24.388l10.627-7.72 4.46 4.46 1.485-4.46-1.486-1.485 4.46-4.46 4.46-1.487z' fill-rule='nonzero'/></svg>",
|
||||||
THEMES: {
|
THEMES: {
|
||||||
default: {
|
default: {
|
||||||
type: 'Organization',
|
type: 'Organization',
|
||||||
|
|||||||
@@ -591,6 +591,7 @@ export function applyTheme(theme) {
|
|||||||
changeCss('.app__body .markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07));
|
changeCss('.app__body .markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07));
|
||||||
changeCss('.app__body .channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8));
|
changeCss('.app__body .channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8));
|
||||||
changeCss('.app__body .channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8));
|
changeCss('.app__body .channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8));
|
||||||
|
changeCss('.app__body .channel-header #pinned-posts-button', 'fill:' + changeOpacity(theme.centerChannelColor, 0.8));
|
||||||
changeCss('.app__body .custom-textarea, .app__body .custom-textarea:focus, .app__body .file-preview, .app__body .post-image__details, .app__body .sidebar--right .sidebar-right__body, .app__body .markdown__table th, .app__body .markdown__table td, .app__body .suggestion-list__content, .app__body .modal .modal-content, .app__body .modal .settings-modal .settings-table .settings-content .divider-light, .app__body .webhooks__container, .app__body .dropdown-menu, .app__body .modal .modal-header, .app__body .popover', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
|
changeCss('.app__body .custom-textarea, .app__body .custom-textarea:focus, .app__body .file-preview, .app__body .post-image__details, .app__body .sidebar--right .sidebar-right__body, .app__body .markdown__table th, .app__body .markdown__table td, .app__body .suggestion-list__content, .app__body .modal .modal-content, .app__body .modal .settings-modal .settings-table .settings-content .divider-light, .app__body .webhooks__container, .app__body .dropdown-menu, .app__body .modal .modal-header, .app__body .popover', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
|
||||||
changeCss('.app__body .popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25));
|
changeCss('.app__body .popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25));
|
||||||
changeCss('.app__body .search-help-popover .search-autocomplete__divider span, .app__body .suggestion-list__divider > span', 'color:' + changeOpacity(theme.centerChannelColor, 0.7));
|
changeCss('.app__body .search-help-popover .search-autocomplete__divider span, .app__body .suggestion-list__divider > span', 'color:' + changeOpacity(theme.centerChannelColor, 0.7));
|
||||||
@@ -659,12 +660,12 @@ export function applyTheme(theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (theme.buttonBg) {
|
if (theme.buttonBg) {
|
||||||
changeCss('.app__body .btn.btn-primary, .app__body .tutorial__circles .circle.active', 'background:' + theme.buttonBg);
|
changeCss('.app__body .btn.btn-primary, .app__body .tutorial__circles .circle.active, .app__body .post__pinned-badge', 'background:' + theme.buttonBg);
|
||||||
changeCss('.app__body .btn.btn-primary:hover, .app__body .btn.btn-primary:active, .app__body .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25));
|
changeCss('.app__body .btn.btn-primary:hover, .app__body .btn.btn-primary:active, .app__body .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (theme.buttonColor) {
|
if (theme.buttonColor) {
|
||||||
changeCss('.app__body .btn.btn-primary', 'color:' + theme.buttonColor);
|
changeCss('.app__body .btn.btn-primary, .app__body .post__pinned-badge', 'color:' + theme.buttonColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (theme.mentionHighlightBg) {
|
if (theme.mentionHighlightBg) {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user