Этот коммит содержится в:
Gabin Aureche
2017-03-13 13:25:08 +01:00
коммит произвёл George Goldberg
родитель 482a0fb5fc
Коммит fe38d6d5bb
38 изменённых файлов: 885 добавлений и 52 удалений

Просмотреть файл

@@ -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")
}
}

Просмотреть файл

@@ -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) {