mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Add pinned posts (#4217)
This commit is contained in:
committed by
George Goldberg
parent
482a0fb5fc
commit
fe38d6d5bb
@@ -225,7 +225,7 @@ func TestGetTeamAnalyticsStandard(t *testing.T) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[0].Value != 3 {
|
||||
if rows[0].Value != 4 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -245,7 +245,7 @@ func TestGetTeamAnalyticsStandard(t *testing.T) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[2].Value != 5 {
|
||||
if rows[2].Value != 6 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ type TestHelper struct {
|
||||
BasicUser2 *model.User
|
||||
BasicChannel *model.Channel
|
||||
BasicPost *model.Post
|
||||
PinnedPost *model.Post
|
||||
|
||||
SystemAdminClient *model.Client
|
||||
SystemAdminTeam *model.Team
|
||||
@@ -91,6 +92,9 @@ func (me *TestHelper) InitBasic() *TestHelper {
|
||||
me.BasicChannel = me.CreateChannel(me.BasicClient, me.BasicTeam)
|
||||
me.BasicPost = me.CreatePost(me.BasicClient, me.BasicChannel)
|
||||
|
||||
pinnedPostChannel := me.CreateChannel(me.BasicClient, me.BasicTeam)
|
||||
me.PinnedPost = me.CreatePinnedPost(me.BasicClient, pinnedPostChannel)
|
||||
|
||||
return me
|
||||
}
|
||||
|
||||
@@ -265,6 +269,21 @@ func (me *TestHelper) CreatePost(client *model.Client, channel *model.Channel) *
|
||||
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() {
|
||||
utils.DisableDebugLogForTest()
|
||||
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("/members/{user_id:[A-Za-z0-9]+}", ApiUserRequired(getChannelMember)).Methods("GET")
|
||||
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("/leave", ApiUserRequired(leave)).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) {
|
||||
params := mux.Vars(r)
|
||||
id := params["channel_id"]
|
||||
|
||||
@@ -926,8 +926,8 @@ func TestGetMoreChannelsPage(t *testing.T) {
|
||||
} else {
|
||||
channels := r.Data.(*model.ChannelList)
|
||||
|
||||
// 1 for BasicChannel, 2 for open channels created above
|
||||
if len(*channels) != 3 {
|
||||
// 1 for BasicChannel, 1 for PinnedPostChannel, 2 for open channels created above
|
||||
if len(*channels) != 4 {
|
||||
t.Fatal("wrong length")
|
||||
}
|
||||
|
||||
@@ -990,11 +990,11 @@ func TestGetChannelCounts(t *testing.T) {
|
||||
} else {
|
||||
counts := result.Data.(*model.ChannelCounts)
|
||||
|
||||
if len(counts.Counts) != 5 {
|
||||
if len(counts.Counts) != 6 {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -1024,8 +1024,8 @@ func TestGetMyChannelMembers(t *testing.T) {
|
||||
} else {
|
||||
members := result.Data.(*model.ChannelMembers)
|
||||
|
||||
// town-square, off-topic, basic test channel, channel1, channel2
|
||||
if len(*members) != 5 {
|
||||
// town-square, off-topic, basic test channel, pinned post channel, channel1, channel2
|
||||
if len(*members) != 6 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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("/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("/pin", ApiUserRequired(pinPost)).Methods("POST")
|
||||
BaseRoutes.NeedPost.Handle("/unpin", ApiUserRequired(unpinPost)).Methods("POST")
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
|
||||
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) {
|
||||
params := mux.Vars(r)
|
||||
|
||||
|
||||
@@ -1378,3 +1378,49 @@ func TestGetOpenGraphMetadata(t *testing.T) {
|
||||
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",
|
||||
"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",
|
||||
"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) {
|
||||
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"`
|
||||
EditAt int64 `json:"edit_at"`
|
||||
DeleteAt int64 `json:"delete_at"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
UserId string `json:"user_id"`
|
||||
ChannelId string `json:"channel_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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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_root_id", "Posts", "RootId")
|
||||
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_hashtags_txt", "Posts", "Hashtags")
|
||||
|
||||
@@ -118,6 +118,7 @@ type ChannelStore interface {
|
||||
InvalidateMemberCount(channelId string)
|
||||
GetMemberCountFromCache(channelId string) int64
|
||||
GetMemberCount(channelId string, allowFromCache bool) StoreChannel
|
||||
GetPinnedPosts(channelId string) StoreChannel
|
||||
RemoveMember(channelId string, userId string) StoreChannel
|
||||
PermanentDeleteMembersByUser(userId 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) {
|
||||
trackEvent('api', 'api_posts_flagged');
|
||||
AsyncClient.savePreference(Preferences.CATEGORY_FLAGGED_POST, postId, 'true');
|
||||
@@ -96,7 +104,8 @@ export function getFlaggedPosts() {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_SEARCH,
|
||||
results: data,
|
||||
is_flagged_posts: true
|
||||
is_flagged_posts: true,
|
||||
is_pinned_posts: false
|
||||
});
|
||||
|
||||
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) {
|
||||
const postList = PostStore.getAllPosts(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()});
|
||||
}
|
||||
|
||||
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) {
|
||||
request.
|
||||
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) {
|
||||
request.
|
||||
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 {getSiteURL} from 'utils/url.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';
|
||||
|
||||
@@ -53,6 +53,7 @@ export default class ChannelHeader extends React.Component {
|
||||
this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this);
|
||||
this.handleShortcut = this.handleShortcut.bind(this);
|
||||
this.getFlagged = this.getFlagged.bind(this);
|
||||
this.getPinnedPosts = this.getPinnedPosts.bind(this);
|
||||
this.initWebrtc = this.initWebrtc.bind(this);
|
||||
this.onBusy = this.onBusy.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) {
|
||||
e.preventDefault();
|
||||
if (SearchStore.isFlaggedPosts) {
|
||||
@@ -211,6 +221,7 @@ export default class ChannelHeader extends React.Component {
|
||||
|
||||
render() {
|
||||
const flagIcon = Constants.FLAG_ICON_SVG;
|
||||
const pinIcon = Constants.PIN_ICON;
|
||||
|
||||
if (!this.validState()) {
|
||||
// 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>
|
||||
</div>
|
||||
</th>
|
||||
<th className='header-list__members'>
|
||||
<th className='header-list__right'>
|
||||
{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 className='search-bar__container'>
|
||||
<NavbarSearchBox
|
||||
|
||||
@@ -19,12 +19,15 @@ import UserStore from 'stores/user_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import SearchStore from 'stores/search_store.jsx';
|
||||
|
||||
import ChannelSwitchModal from './channel_switch_modal.jsx';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as ChannelUtils from 'utils/channel_utils.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';
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
@@ -62,6 +65,7 @@ export default class Navbar extends React.Component {
|
||||
this.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this);
|
||||
|
||||
this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
|
||||
this.getPinnedPosts = this.getPinnedPosts.bind(this);
|
||||
|
||||
const state = this.getStateFromStores();
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -244,6 +257,7 @@ export default class Navbar extends React.Component {
|
||||
}
|
||||
|
||||
let viewInfoOption;
|
||||
let viewPinnedPostsOption;
|
||||
let addMembersOption;
|
||||
let manageMembersOption;
|
||||
let setChannelHeaderOption;
|
||||
@@ -335,6 +349,21 @@ export default class Navbar extends React.Component {
|
||||
</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)) {
|
||||
addMembersOption = (
|
||||
<li role='presentation'>
|
||||
@@ -561,6 +590,7 @@ export default class Navbar extends React.Component {
|
||||
role='menu'
|
||||
>
|
||||
{viewInfoOption}
|
||||
{viewPinnedPostsOption}
|
||||
{notificationPreferenceOption}
|
||||
{addMembersOption}
|
||||
{manageMembersOption}
|
||||
|
||||
@@ -12,6 +12,7 @@ import TeamStore from 'stores/team_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_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 {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
@@ -173,12 +174,25 @@ export default class NeedsTeam extends React.Component {
|
||||
</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 (
|
||||
<div className='channel-view'>
|
||||
<ErrorBar/>
|
||||
<WebrtcNotification/>
|
||||
<div className='container-fluid'>
|
||||
<SidebarRight/>
|
||||
<SidebarRight channel={channel}/>
|
||||
<SidebarRightMenu teamType={this.state.team.type}/>
|
||||
<WebrtcSidebar/>
|
||||
{content}
|
||||
|
||||
@@ -233,7 +233,7 @@ export default class PopoverListMembers extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='member-popover__container'>
|
||||
<div
|
||||
id='member_popover'
|
||||
className='member-popover__trigger'
|
||||
@@ -243,13 +243,11 @@ export default class PopoverListMembers extends React.Component {
|
||||
AsyncClient.getProfilesInChannel(this.props.channel.id, 0);
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{countText}
|
||||
<span
|
||||
className='fa fa-user'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</div>
|
||||
{countText}
|
||||
<span
|
||||
className='fa fa-user'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</div>
|
||||
<Overlay
|
||||
rootClose={true}
|
||||
|
||||
@@ -26,6 +26,8 @@ export default class PostInfo extends React.Component {
|
||||
this.removePost = this.removePost.bind(this);
|
||||
this.flagPost = this.flagPost.bind(this);
|
||||
this.unflagPost = this.unflagPost.bind(this);
|
||||
this.pinPost = this.pinPost.bind(this);
|
||||
this.unpinPost = this.unpinPost.bind(this);
|
||||
|
||||
this.canEdit = 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) {
|
||||
dropdownContents.push(
|
||||
<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) {
|
||||
e.preventDefault();
|
||||
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 (
|
||||
<ul className='post__header--info'>
|
||||
<li className='col'>
|
||||
@@ -384,6 +444,7 @@ export default class PostInfo extends React.Component {
|
||||
useMilitaryTime={this.props.useMilitaryTime}
|
||||
postId={post.id}
|
||||
/>
|
||||
{pinnedBadge}
|
||||
{flagTrigger}
|
||||
</li>
|
||||
{options}
|
||||
|
||||
@@ -10,7 +10,7 @@ import ReactionListContainer from 'components/post_view/components/reaction_list
|
||||
import RhsDropdown from 'components/rhs_dropdown.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';
|
||||
|
||||
@@ -36,6 +36,8 @@ export default class RhsComment extends React.Component {
|
||||
this.removePost = this.removePost.bind(this);
|
||||
this.flagPost = this.flagPost.bind(this);
|
||||
this.unflagPost = this.unflagPost.bind(this);
|
||||
this.pinPost = this.pinPost.bind(this);
|
||||
this.unpinPost = this.unpinPost.bind(this);
|
||||
|
||||
this.canEdit = false;
|
||||
this.canDelete = false;
|
||||
@@ -128,6 +130,16 @@ export default class RhsComment extends React.Component {
|
||||
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() {
|
||||
const post = this.props.post;
|
||||
|
||||
@@ -195,6 +207,42 @@ export default class RhsComment extends React.Component {
|
||||
</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) {
|
||||
dropdownContents.push(
|
||||
<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 = {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
@@ -524,6 +584,7 @@ export default class RhsComment extends React.Component {
|
||||
{botIndicator}
|
||||
<li className='col'>
|
||||
{this.renderTimeTag(post, timeOptions)}
|
||||
{pinnedBadge}
|
||||
{flagTrigger}
|
||||
</li>
|
||||
{options}
|
||||
|
||||
@@ -14,7 +14,7 @@ import UserStore from 'stores/user_store.jsx';
|
||||
import TeamStore from 'stores/team_store.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 PostUtils from 'utils/post_utils.jsx';
|
||||
@@ -35,6 +35,8 @@ export default class RhsRootPost extends React.Component {
|
||||
this.handlePermalink = this.handlePermalink.bind(this);
|
||||
this.flagPost = this.flagPost.bind(this);
|
||||
this.unflagPost = this.unflagPost.bind(this);
|
||||
this.pinPost = this.pinPost.bind(this);
|
||||
this.unpinPost = this.unpinPost.bind(this);
|
||||
|
||||
this.canEdit = 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() {
|
||||
const post = this.props.post;
|
||||
const user = this.props.user;
|
||||
@@ -240,6 +252,42 @@ export default class RhsRootPost extends React.Component {
|
||||
</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) {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
@@ -450,6 +498,18 @@ export default class RhsRootPost extends React.Component {
|
||||
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 = {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
@@ -470,6 +530,7 @@ export default class RhsRootPost extends React.Component {
|
||||
{botIndicator}
|
||||
<li className='col'>
|
||||
{this.renderTimeTag(post, timeOptions)}
|
||||
{pinnedBadge}
|
||||
<OverlayTrigger
|
||||
key={'rootpostflagtooltipkey' + flagVisible}
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
|
||||
@@ -213,6 +213,31 @@ export default class SearchResults extends React.Component {
|
||||
</ul>
|
||||
</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) {
|
||||
ctls = (
|
||||
<div className='sidebar--right__subheader'>
|
||||
@@ -289,6 +314,8 @@ export default class SearchResults extends React.Component {
|
||||
toggleSize={this.props.toggleSize}
|
||||
shrink={this.props.shrink}
|
||||
isFlaggedPosts={this.props.isFlaggedPosts}
|
||||
isPinnedPosts={this.props.isPinnedPosts}
|
||||
channelDisplayName={this.props.channelDisplayName}
|
||||
/>
|
||||
<div
|
||||
id='search-items-container'
|
||||
@@ -307,5 +334,7 @@ SearchResults.propTypes = {
|
||||
useMilitaryTime: React.PropTypes.bool.isRequired,
|
||||
toggleSize: 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'
|
||||
/>
|
||||
);
|
||||
} else if (this.props.isPinnedPosts) {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='search_header.title4'
|
||||
defaultMessage='Pinned posts in {channelDisplayName}'
|
||||
values={{
|
||||
channelDisplayName: this.props.channelDisplayName
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -131,5 +141,7 @@ SearchResultsHeader.propTypes = {
|
||||
isMentionSearch: React.PropTypes.bool,
|
||||
toggleSize: 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 (
|
||||
<div className='search-item__container'>
|
||||
<div className='date-separator'>
|
||||
@@ -322,6 +334,7 @@ export default class SearchResultsItem extends React.Component {
|
||||
{botIndicator}
|
||||
<li className='col'>
|
||||
{this.renderTimeTag(post)}
|
||||
{pinnedBadge}
|
||||
{flagContent}
|
||||
</li>
|
||||
{rhsControls}
|
||||
|
||||
@@ -11,13 +11,13 @@ import UserStore from 'stores/user_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_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 * as Utils from 'utils/utils.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
|
||||
export default class SidebarRight extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -27,6 +27,7 @@ export default class SidebarRight extends React.Component {
|
||||
|
||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||
this.onSelectedChange = this.onSelectedChange.bind(this);
|
||||
this.onPostPinnedChange = this.onPostPinnedChange.bind(this);
|
||||
this.onSearchChange = this.onSearchChange.bind(this);
|
||||
this.onUserChange = this.onUserChange.bind(this);
|
||||
this.onShowSearch = this.onShowSearch.bind(this);
|
||||
@@ -39,6 +40,7 @@ export default class SidebarRight extends React.Component {
|
||||
searchVisible: SearchStore.getSearchResults() !== null,
|
||||
isMentionSearch: SearchStore.getIsMentionSearch(),
|
||||
isFlaggedPosts: SearchStore.getIsFlaggedPosts(),
|
||||
isPinnedPosts: SearchStore.getIsPinnedPosts(),
|
||||
postRightVisible: Boolean(PostStore.getSelectedPost()),
|
||||
expanded: false,
|
||||
fromSearch: false,
|
||||
@@ -50,6 +52,7 @@ export default class SidebarRight extends React.Component {
|
||||
componentDidMount() {
|
||||
SearchStore.addSearchChangeListener(this.onSearchChange);
|
||||
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
|
||||
PostStore.addPostPinnedChangeListener(this.onPostPinnedChange);
|
||||
SearchStore.addShowSearchListener(this.onShowSearch);
|
||||
UserStore.addChangeListener(this.onUserChange);
|
||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
||||
@@ -59,6 +62,7 @@ export default class SidebarRight extends React.Component {
|
||||
componentWillUnmount() {
|
||||
SearchStore.removeSearchChangeListener(this.onSearchChange);
|
||||
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
|
||||
PostStore.removePostPinnedChangeListener(this.onPostPinnedChange);
|
||||
SearchStore.removeShowSearchListener(this.onShowSearch);
|
||||
UserStore.removeChangeListener(this.onUserChange);
|
||||
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() {
|
||||
this.setState({
|
||||
expanded: false
|
||||
@@ -147,7 +157,8 @@ export default class SidebarRight extends React.Component {
|
||||
this.setState({
|
||||
searchVisible: SearchStore.getSearchResults() !== null,
|
||||
isMentionSearch: SearchStore.getIsMentionSearch(),
|
||||
isFlaggedPosts: SearchStore.getIsFlaggedPosts()
|
||||
isFlaggedPosts: SearchStore.getIsFlaggedPosts(),
|
||||
isPinnedPosts: SearchStore.getIsPinnedPosts()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -182,9 +193,11 @@ export default class SidebarRight extends React.Component {
|
||||
<SearchResults
|
||||
isMentionSearch={this.state.isMentionSearch}
|
||||
isFlaggedPosts={this.state.isFlaggedPosts}
|
||||
isPinnedPosts={this.state.isPinnedPosts}
|
||||
useMilitaryTime={this.state.useMilitaryTime}
|
||||
toggleSize={this.toggleSize}
|
||||
shrink={this.onShrink}
|
||||
channelDisplayName={this.props.channel ? this.props.channel.display_name : ''}
|
||||
/>
|
||||
);
|
||||
} 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.toggle2": "Toggle sidebar",
|
||||
"navbar.viewInfo": "View Info",
|
||||
"navbar.viewPinnedPosts": "View Pinned Posts",
|
||||
"navbar_dropdown.about": "About Mattermost",
|
||||
"navbar_dropdown.accountSettings": "Account Settings",
|
||||
"navbar_dropdown.console": "System Console",
|
||||
@@ -1726,6 +1727,9 @@
|
||||
"post_info.permalink": "Permalink",
|
||||
"post_info.reply": "Reply",
|
||||
"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)",
|
||||
"posts_view.loadMore": "Load more messages",
|
||||
"posts_view.newMsg": "New Messages",
|
||||
@@ -1778,11 +1782,14 @@
|
||||
"rhs_root.mobile.flag": "Flag",
|
||||
"rhs_root.mobile.unflag": "Unflag",
|
||||
"rhs_root.permalink": "Permalink",
|
||||
"rhs_root.pin": "Pin to channel",
|
||||
"rhs_root.unpin": "Un-pin from channel",
|
||||
"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_header.results": "Search Results",
|
||||
"search_header.title2": "Recent Mentions",
|
||||
"search_header.title3": "Flagged Posts",
|
||||
"search_header.title4": "Pinned posts in {channelDisplayName}",
|
||||
"search_item.direct": "Direct Message (with {username})",
|
||||
"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>",
|
||||
@@ -1792,6 +1799,9 @@
|
||||
"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.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.save": "Save",
|
||||
"setting_item_min.edit": "Edit",
|
||||
|
||||
@@ -9,10 +9,20 @@
|
||||
.search-btns {
|
||||
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;
|
||||
float: right;
|
||||
padding-right: 0px !important;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +33,20 @@
|
||||
.search-btns {
|
||||
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;
|
||||
float: right;
|
||||
padding-right: 0px !important;
|
||||
float: right
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,26 +7,43 @@
|
||||
line-height: 56px;
|
||||
width: 100%;
|
||||
|
||||
.member-popover__trigger {
|
||||
.member-popover__trigger,
|
||||
.pinned-posts-button {
|
||||
display: inline-block;
|
||||
min-width: 30px;
|
||||
cursor: pointer;
|
||||
min-width: 60px;
|
||||
padding-right: 10px;
|
||||
text-align: right;
|
||||
margin-left: 10px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
|
||||
.fa {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.member-popover__container,
|
||||
.member-popover__trigger {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.member-popover__trigger {
|
||||
.fa {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.pinned-posts-button svg {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
&.alt {
|
||||
margin: 0;
|
||||
|
||||
th {
|
||||
font-weight: normal !important;
|
||||
|
||||
&.header-list__members {
|
||||
&.header-list__right {
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1343,15 +1343,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bot-indicator {
|
||||
.bot-indicator,
|
||||
.post__pinned-badge {
|
||||
border-radius: 2px;
|
||||
font-family: inherit;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
margin: 2px 10px 0 -4px;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
.bot-indicator {
|
||||
margin: 2px 10px 0 -4px;
|
||||
}
|
||||
|
||||
.post__pinned-badge {
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.permalink-text {
|
||||
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 {
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -273,6 +274,7 @@
|
||||
|
||||
.post__header {
|
||||
margin-bottom: 0;
|
||||
padding-right: 70px;
|
||||
|
||||
.col__reply {
|
||||
top: -3px;
|
||||
|
||||
@@ -127,6 +127,15 @@
|
||||
top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.move--left,
|
||||
&.webrtc--show,
|
||||
&.move--right {
|
||||
.header-list__right {
|
||||
// hide it behind the RHS
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.post {
|
||||
.attachment {
|
||||
@@ -182,6 +191,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar--right__title {
|
||||
display: inline-block;
|
||||
max-width: 300px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.inner-wrap {
|
||||
@@ -213,6 +230,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.post__pinned-badge {
|
||||
margin-left: 0;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
&:not(.post--thread) {
|
||||
padding: 5px .5em 0 77px;
|
||||
|
||||
@@ -359,9 +381,16 @@
|
||||
}
|
||||
|
||||
.post__header {
|
||||
float: left;
|
||||
padding-top: 3px;
|
||||
|
||||
.col__reply {
|
||||
top: -21px;
|
||||
}
|
||||
|
||||
.post__pinned-badge {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&: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 POSTS_VIEW_JUMP_EVENT = 'post_list_jump';
|
||||
const SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
|
||||
const POST_PINNED_CHANGE_EVENT = 'post_pinned_change';
|
||||
|
||||
class PostStoreClass extends EventEmitter {
|
||||
constructor() {
|
||||
@@ -259,22 +260,42 @@ class PostStoreClass extends EventEmitter {
|
||||
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) {
|
||||
const postList = makePostListNonNull(this.getAllPosts(post.channel_id));
|
||||
const ids = [
|
||||
post.channel_id
|
||||
];
|
||||
|
||||
if (post.pending_post_id !== '') {
|
||||
this.removePendingPost(post.channel_id, post.pending_post_id);
|
||||
// update the post in the permalink view if it's there
|
||||
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;
|
||||
if (isNewPost && postList.order.indexOf(post.id) === -1) {
|
||||
postList.order.unshift(post.id);
|
||||
}
|
||||
post.pending_post_id = '';
|
||||
|
||||
this.makePostsInfo(post.channel_id);
|
||||
this.postsInfo[post.channel_id].postList = postList;
|
||||
postList.posts[post.id] = post;
|
||||
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) {
|
||||
@@ -500,6 +521,18 @@ class PostStoreClass extends EventEmitter {
|
||||
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) {
|
||||
const userId = UserStore.getCurrentId();
|
||||
|
||||
@@ -686,6 +719,10 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
|
||||
PostStore.storeSelectedPostId(action.postId);
|
||||
PostStore.emitSelectedPostChange(action.from_search, action.from_flagged_posts);
|
||||
break;
|
||||
case ActionTypes.RECEIVED_POST_PINNED:
|
||||
case ActionTypes.RECEIVED_POST_UNPINNED:
|
||||
PostStore.emitPostPinnedChange();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ class SearchStoreClass extends EventEmitter {
|
||||
this.searchResults = null;
|
||||
this.isMentionSearch = false;
|
||||
this.isFlaggedPosts = false;
|
||||
this.isPinnedPosts = false;
|
||||
this.isVisible = false;
|
||||
this.searchTerm = '';
|
||||
}
|
||||
@@ -83,6 +84,10 @@ class SearchStoreClass extends EventEmitter {
|
||||
return this.isFlaggedPosts;
|
||||
}
|
||||
|
||||
getIsPinnedPosts() {
|
||||
return this.isPinnedPosts;
|
||||
}
|
||||
|
||||
storeSearchTerm(term) {
|
||||
this.searchTerm = term;
|
||||
}
|
||||
@@ -91,10 +96,11 @@ class SearchStoreClass extends EventEmitter {
|
||||
return this.searchTerm;
|
||||
}
|
||||
|
||||
storeSearchResults(results, isMentionSearch, isFlaggedPosts) {
|
||||
storeSearchResults(results, isMentionSearch, isFlaggedPosts, isPinnedPosts) {
|
||||
this.searchResults = results;
|
||||
this.isMentionSearch = isMentionSearch;
|
||||
this.isFlaggedPosts = isFlaggedPosts;
|
||||
this.isPinnedPosts = isPinnedPosts;
|
||||
}
|
||||
|
||||
deletePost(post) {
|
||||
@@ -120,7 +126,7 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => {
|
||||
|
||||
switch (action.type) {
|
||||
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();
|
||||
break;
|
||||
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) {
|
||||
Client.saveReaction(
|
||||
channelId,
|
||||
|
||||
@@ -90,6 +90,8 @@ export const ActionTypes = keyMirror({
|
||||
RECEIVED_POST_SELECTED: null,
|
||||
RECEIVED_MENTION_DATA: null,
|
||||
RECEIVED_ADD_MENTION: null,
|
||||
RECEIVED_POST_PINNED: null,
|
||||
RECEIVED_POST_UNPINNED: null,
|
||||
|
||||
RECEIVED_PROFILES: 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>",
|
||||
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>",
|
||||
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: {
|
||||
default: {
|
||||
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 .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 #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 .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));
|
||||
@@ -659,12 +660,12 @@ export function applyTheme(theme) {
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user