Merge branch 'master' into PLT-340
Этот коммит содержится в:
@@ -101,6 +101,12 @@ Joining the Mattermost community is a great way to build relationships with othe
|
|||||||
- Review the [Mattermost Code Contribution Guidelines](http://docs.mattermost.org/developer/Code-Contribution-Guidelines/index.html) to submit patches for the core product
|
- Review the [Mattermost Code Contribution Guidelines](http://docs.mattermost.org/developer/Code-Contribution-Guidelines/index.html) to submit patches for the core product
|
||||||
- Consider building tools that help developers and IT professionals manage Mattermost more effectively (API documentation coming in Beta2)
|
- Consider building tools that help developers and IT professionals manage Mattermost more effectively (API documentation coming in Beta2)
|
||||||
|
|
||||||
|
##### Check out some projects for connecting to Mattermost:
|
||||||
|
|
||||||
|
- [Matterbridge](https://github.com/42wim/matterbridge) - an IRC bridge connecting to Mattermost
|
||||||
|
- [GitLab Integration Service for Mattermost](https://github.com/mattermost/mattermost-integration-gitlab) - connecting GitLab to Mattermost via incoming webhooks
|
||||||
|
- [Giphy Integration Service for Mattermost](https://github.com/mattermost/mattermost-integration-giphy) - connecting Mattermost to Giphy via outgoing webhooks
|
||||||
|
|
||||||
#### Have other ideas or suggestions?
|
#### Have other ideas or suggestions?
|
||||||
|
|
||||||
If there’s some other way you’d like to contribute, please contact us at info@mattermost.com. We’d love to meet you!
|
If there’s some other way you’d like to contribute, please contact us at info@mattermost.com. We’d love to meet you!
|
||||||
|
|||||||
62
api/admin.go
62
api/admin.go
@@ -26,7 +26,7 @@ func InitAdmin(r *mux.Router) {
|
|||||||
sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST")
|
sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST")
|
||||||
sr.Handle("/client_props", ApiAppHandler(getClientConfig)).Methods("GET")
|
sr.Handle("/client_props", ApiAppHandler(getClientConfig)).Methods("GET")
|
||||||
sr.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST")
|
sr.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST")
|
||||||
|
sr.Handle("/analytics/{id:[A-Za-z0-9]+}/{name:[A-Za-z0-9_]+}", ApiAppHandler(getAnalytics)).Methods("GET")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
|
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -142,3 +142,63 @@ func testEmail(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
m["SUCCESS"] = "true"
|
m["SUCCESS"] = "true"
|
||||||
w.Write([]byte(model.MapToJson(m)))
|
w.Write([]byte(model.MapToJson(m)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !c.HasSystemAdminPermissions("getAnalytics") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params := mux.Vars(r)
|
||||||
|
teamId := params["id"]
|
||||||
|
name := params["name"]
|
||||||
|
|
||||||
|
if name == "standard" {
|
||||||
|
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 3)
|
||||||
|
rows[0] = &model.AnalyticsRow{"channel_open_count", 0}
|
||||||
|
rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
|
||||||
|
rows[2] = &model.AnalyticsRow{"post_count", 0}
|
||||||
|
openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
|
||||||
|
privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
|
||||||
|
postChan := Srv.Store.Post().AnalyticsPostCount(teamId)
|
||||||
|
|
||||||
|
if r := <-openChan; r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rows[0].Value = float64(r.Data.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r := <-privateChan; r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rows[1].Value = float64(r.Data.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r := <-postChan; r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rows[2].Value = float64(r.Data.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(rows.ToJson()))
|
||||||
|
} else if name == "post_counts_day" {
|
||||||
|
if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
|
||||||
|
}
|
||||||
|
} else if name == "user_counts_with_posts_day" {
|
||||||
|
if r := <-Srv.Store.Post().AnalyticsUserCountsWithPostsByDay(teamId); r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.SetInvalidParam("getAnalytics", "name")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -150,3 +150,151 @@ func TestEmailTest(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetAnalyticsStandard(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||||
|
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||||
|
|
||||||
|
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||||
|
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||||
|
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||||
|
|
||||||
|
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||||
|
|
||||||
|
channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
|
||||||
|
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
|
||||||
|
|
||||||
|
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
|
||||||
|
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
|
||||||
|
|
||||||
|
if _, err := Client.GetAnalytics(team.Id, "standard"); err == nil {
|
||||||
|
t.Fatal("Shouldn't have permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Context{}
|
||||||
|
c.RequestId = model.NewId()
|
||||||
|
c.IpAddress = "cmd_line"
|
||||||
|
UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
|
||||||
|
|
||||||
|
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||||
|
|
||||||
|
if result, err := Client.GetAnalytics(team.Id, "standard"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
rows := result.Data.(model.AnalyticsRows)
|
||||||
|
|
||||||
|
if rows[0].Name != "channel_open_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[0].Value != 2 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[1].Name != "channel_private_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[1].Value != 1 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[2].Name != "post_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[2].Value != 1 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPostCount(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||||
|
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||||
|
|
||||||
|
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||||
|
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||||
|
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||||
|
|
||||||
|
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||||
|
|
||||||
|
channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
|
||||||
|
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
|
||||||
|
|
||||||
|
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
|
||||||
|
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
|
||||||
|
|
||||||
|
if _, err := Client.GetAnalytics(team.Id, "post_counts_day"); err == nil {
|
||||||
|
t.Fatal("Shouldn't have permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Context{}
|
||||||
|
c.RequestId = model.NewId()
|
||||||
|
c.IpAddress = "cmd_line"
|
||||||
|
UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
|
||||||
|
|
||||||
|
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||||
|
|
||||||
|
if result, err := Client.GetAnalytics(team.Id, "post_counts_day"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
rows := result.Data.(model.AnalyticsRows)
|
||||||
|
|
||||||
|
if rows[0].Value != 1 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserCountsWithPostsByDay(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||||
|
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||||
|
|
||||||
|
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||||
|
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||||
|
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||||
|
|
||||||
|
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||||
|
|
||||||
|
channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
|
||||||
|
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
|
||||||
|
|
||||||
|
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
|
||||||
|
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
|
||||||
|
|
||||||
|
if _, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err == nil {
|
||||||
|
t.Fatal("Shouldn't have permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Context{}
|
||||||
|
c.RequestId = model.NewId()
|
||||||
|
c.IpAddress = "cmd_line"
|
||||||
|
UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
|
||||||
|
|
||||||
|
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||||
|
|
||||||
|
if result, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
rows := result.Data.(model.AnalyticsRows)
|
||||||
|
|
||||||
|
if rows[0].Value != 1 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
22
api/file.go
22
api/file.go
@@ -52,6 +52,8 @@ const (
|
|||||||
RotatedCCW = 6
|
RotatedCCW = 6
|
||||||
RotatedCCWMirrored = 7
|
RotatedCCWMirrored = 7
|
||||||
RotatedCW = 8
|
RotatedCW = 8
|
||||||
|
|
||||||
|
MaxImageSize = 4096 * 2160 // 4k resolution
|
||||||
)
|
)
|
||||||
|
|
||||||
var fileInfoCache *utils.Cache = utils.NewLru(1000)
|
var fileInfoCache *utils.Cache = utils.NewLru(1000)
|
||||||
@@ -125,6 +127,21 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
uid := model.NewId()
|
uid := model.NewId()
|
||||||
|
|
||||||
|
if model.IsFileExtImage(filepath.Ext(files[i].Filename)) {
|
||||||
|
imageNameList = append(imageNameList, uid+"/"+filename)
|
||||||
|
imageDataList = append(imageDataList, buf.Bytes())
|
||||||
|
|
||||||
|
// Decode image config first to check dimensions before loading the whole thing into memory later on
|
||||||
|
config, _, err := image.DecodeConfig(bytes.NewReader(buf.Bytes()))
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError("uploadFile", "Unable to upload image file.", err.Error())
|
||||||
|
return
|
||||||
|
} else if config.Width*config.Height > MaxImageSize {
|
||||||
|
c.Err = model.NewAppError("uploadFile", "Unable to upload image file. File is too large.", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
path := "teams/" + c.Session.TeamId + "/channels/" + channelId + "/users/" + c.Session.UserId + "/" + uid + "/" + filename
|
path := "teams/" + c.Session.TeamId + "/channels/" + channelId + "/users/" + c.Session.UserId + "/" + uid + "/" + filename
|
||||||
|
|
||||||
if err := writeFile(buf.Bytes(), path); err != nil {
|
if err := writeFile(buf.Bytes(), path); err != nil {
|
||||||
@@ -132,11 +149,6 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if model.IsFileExtImage(filepath.Ext(files[i].Filename)) {
|
|
||||||
imageNameList = append(imageNameList, uid+"/"+filename)
|
|
||||||
imageDataList = append(imageDataList, buf.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
encName := utils.UrlEncode(filename)
|
encName := utils.UrlEncode(filename)
|
||||||
|
|
||||||
fileUrl := "/" + channelId + "/" + c.Session.UserId + "/" + uid + "/" + encName
|
fileUrl := "/" + channelId + "/" + c.Session.UserId + "/" + uid + "/" + encName
|
||||||
|
|||||||
42
api/post.go
42
api/post.go
@@ -820,45 +820,23 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
plainSearchParams, hashtagSearchParams := model.ParseSearchParams(terms)
|
paramsList := model.ParseSearchParams(terms)
|
||||||
|
channels := []store.StoreChannel{}
|
||||||
|
|
||||||
var hchan store.StoreChannel
|
for _, params := range paramsList {
|
||||||
if hashtagSearchParams != nil {
|
channels = append(channels, Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, params))
|
||||||
hchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, hashtagSearchParams)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var pchan store.StoreChannel
|
posts := &model.PostList{}
|
||||||
if plainSearchParams != nil {
|
for _, channel := range channels {
|
||||||
pchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, plainSearchParams)
|
if result := <-channel; result.Err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
mainList := &model.PostList{}
|
|
||||||
if hchan != nil {
|
|
||||||
if result := <-hchan; result.Err != nil {
|
|
||||||
c.Err = result.Err
|
c.Err = result.Err
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
mainList = result.Data.(*model.PostList)
|
data := result.Data.(*model.PostList)
|
||||||
|
posts.Extend(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plainList := &model.PostList{}
|
w.Write([]byte(posts.ToJson()))
|
||||||
if pchan != nil {
|
|
||||||
if result := <-pchan; result.Err != nil {
|
|
||||||
c.Err = result.Err
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
plainList = result.Data.(*model.PostList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, postId := range plainList.Order {
|
|
||||||
if _, ok := mainList.Posts[postId]; !ok {
|
|
||||||
mainList.AddPost(plainList.Posts[postId])
|
|
||||||
mainList.AddOrder(postId)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write([]byte(mainList.ToJson()))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -427,12 +427,18 @@ func TestSearchPostsInChannel(t *testing.T) {
|
|||||||
channel2 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
|
channel2 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
|
||||||
channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
|
channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
|
||||||
|
|
||||||
|
channel3 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
|
||||||
|
channel3 = Client.Must(Client.CreateChannel(channel3)).Data.(*model.Channel)
|
||||||
|
|
||||||
post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"}
|
post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"}
|
||||||
post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
|
post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
|
||||||
|
|
||||||
post3 := &model.Post{ChannelId: channel2.Id, Message: "other message with no return"}
|
post3 := &model.Post{ChannelId: channel2.Id, Message: "other message with no return"}
|
||||||
post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post)
|
post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post)
|
||||||
|
|
||||||
|
post4 := &model.Post{ChannelId: channel3.Id, Message: "other message with no return"}
|
||||||
|
post4 = Client.Must(Client.CreatePost(post4)).Data.(*model.Post)
|
||||||
|
|
||||||
if result := Client.Must(Client.SearchPosts("channel:")).Data.(*model.PostList); len(result.Order) != 0 {
|
if result := Client.Must(Client.SearchPosts("channel:")).Data.(*model.PostList); len(result.Order) != 0 {
|
||||||
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||||
}
|
}
|
||||||
@@ -476,6 +482,10 @@ func TestSearchPostsInChannel(t *testing.T) {
|
|||||||
if result := Client.Must(Client.SearchPosts("sgtitlereview channel: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 1 {
|
if result := Client.Must(Client.SearchPosts("sgtitlereview channel: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 1 {
|
||||||
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if result := Client.Must(Client.SearchPosts("channel: " + channel2.Name + " channel: " + channel3.Name)).Data.(*model.PostList); len(result.Order) != 3 {
|
||||||
|
t.Fatalf("wrong number of posts returned :) %v :) %v", result.Posts, result.Order)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchPostsFromUser(t *testing.T) {
|
func TestSearchPostsFromUser(t *testing.T) {
|
||||||
@@ -510,11 +520,12 @@ func TestSearchPostsFromUser(t *testing.T) {
|
|||||||
post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"}
|
post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"}
|
||||||
post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
|
post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
|
||||||
|
|
||||||
|
// includes "X has joined the channel" messages for both user2 and user3
|
||||||
|
|
||||||
if result := Client.Must(Client.SearchPosts("from: " + user1.Username)).Data.(*model.PostList); len(result.Order) != 1 {
|
if result := Client.Must(Client.SearchPosts("from: " + user1.Username)).Data.(*model.PostList); len(result.Order) != 1 {
|
||||||
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||||
}
|
}
|
||||||
|
|
||||||
// note that this includes the "User2 has joined the channel" system messages
|
|
||||||
if result := Client.Must(Client.SearchPosts("from: " + user2.Username)).Data.(*model.PostList); len(result.Order) != 3 {
|
if result := Client.Must(Client.SearchPosts("from: " + user2.Username)).Data.(*model.PostList); len(result.Order) != 3 {
|
||||||
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||||
}
|
}
|
||||||
@@ -526,6 +537,33 @@ func TestSearchPostsFromUser(t *testing.T) {
|
|||||||
if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " in:" + channel1.Name)).Data.(*model.PostList); len(result.Order) != 1 {
|
if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " in:" + channel1.Name)).Data.(*model.PostList); len(result.Order) != 1 {
|
||||||
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user3 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||||
|
user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User)
|
||||||
|
store.Must(Srv.Store.User().VerifyEmail(user3.Id))
|
||||||
|
|
||||||
|
Client.LoginByEmail(team.Name, user3.Email, "pwd")
|
||||||
|
Client.Must(Client.JoinChannel(channel1.Id))
|
||||||
|
Client.Must(Client.JoinChannel(channel2.Id))
|
||||||
|
|
||||||
|
// wait for the join/leave messages to be created for user3 since they're done asynchronously
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
if result := Client.Must(Client.SearchPosts("from: " + user2.Username)).Data.(*model.PostList); len(result.Order) != 3 {
|
||||||
|
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " from: " + user3.Username)).Data.(*model.PostList); len(result.Order) != 5 {
|
||||||
|
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " from: " + user3.Username + " in:" + channel2.Name)).Data.(*model.PostList); len(result.Order) != 3 {
|
||||||
|
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " from: " + user3.Username + " in:" + channel2.Name + " joined")).Data.(*model.PostList); len(result.Order) != 2 {
|
||||||
|
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetPostsCache(t *testing.T) {
|
func TestGetPostsCache(t *testing.T) {
|
||||||
|
|||||||
12
api/user.go
12
api/user.go
@@ -855,6 +855,18 @@ func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decode image config first to check dimensions before loading the whole thing into memory later on
|
||||||
|
config, _, err := image.DecodeConfig(file)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError("uploadProfileFile", "Could not decode profile image config.", err.Error())
|
||||||
|
return
|
||||||
|
} else if config.Width*config.Height > MaxImageSize {
|
||||||
|
c.Err = model.NewAppError("uploadProfileFile", "Unable to upload profile image. File is too large.", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Seek(0, 0)
|
||||||
|
|
||||||
// Decode image into Image object
|
// Decode image into Image object
|
||||||
img, _, err := image.Decode(file)
|
img, _, err := image.Decode(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
12
doc/install/Administration.md
Обычный файл
12
doc/install/Administration.md
Обычный файл
@@ -0,0 +1,12 @@
|
|||||||
|
# Administration
|
||||||
|
|
||||||
|
This document provides instructions for common administrator tasks
|
||||||
|
|
||||||
|
##### Creating System Administrator account from commandline
|
||||||
|
- If the System Administrator account becomes unavailable, a person leaving the organization for example, you can set a new system admin from the commandline using `./platform -assign_role -team_name="yourteam" -email="you@example.com" -role="system_admin"`.
|
||||||
|
- After assigning the role the user needs to log out and log back in before the System Administrator role is applied.
|
||||||
|
|
||||||
|
##### Deactivating a user
|
||||||
|
|
||||||
|
- Team Admin or System Admin can go to **Main Menu** > **Manage Members** > **Make Inactive** to deactivate a user, which removes them from the team.
|
||||||
|
- To preserve audit history, users are never deleted from the system. It is highly recommended that System Administrators do not attempt to delete users manually from the database, as this may compromise system integrity and ability to upgrade in future.
|
||||||
@@ -8,10 +8,22 @@
|
|||||||
|
|
||||||
#### Common Issues
|
#### Common Issues
|
||||||
|
|
||||||
##### Error message in logs when attempting to sign-up: `x509: certificate signed by unknown authority`
|
|
||||||
- This error may appear when attempt to use a self-signed certificate to setup SSL, which is not yet supported by Mattermost. You can resolve this issue by setting up a load balancer like Ngnix. A ticket exists to [add support for self-signed certificates in future](x509: certificate signed by unknown authority).
|
|
||||||
|
|
||||||
##### Lost System Administrator account
|
##### Lost System Administrator account
|
||||||
- If the System Administrator account becomes unavailable, a person leaving the organization for example, you can set a new system admin from the commandline using `./platform -assign_role -team_name="yourteam" -email="you@example.com" -role="system_admin"`.
|
- If the System Administrator account becomes unavailable, a person leaving the organization for example, you can set a new system admin from the commandline using `./platform -assign_role -team_name="yourteam" -email="you@example.com" -role="system_admin"`.
|
||||||
- After assigning the role the user needs to log out and log back in before the System Administrator role is applied.
|
- After assigning the role the user needs to log out and log back in before the System Administrator role is applied.
|
||||||
|
|
||||||
|
##### Deactivate a user
|
||||||
|
|
||||||
|
- Team Admin or System Admin can go to **Main Menu** > **Manage Members** > **Make Inactive** to deactivate a user, which removes them from the team.
|
||||||
|
- To preserve audit history, users are never deleted from the system. It is highly recommended that System Administrators do not attempt to delete users manually from the database, as this may compromise system integrity and ability to upgrade in future.
|
||||||
|
|
||||||
|
#### Error Messages
|
||||||
|
|
||||||
|
The following is a list of common error messages and solutions:
|
||||||
|
|
||||||
|
###### `Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.`
|
||||||
|
- Message appears in blue bar on team site. Check that [your websocket port is properly configured](https://github.com/mattermost/platform/blob/master/doc/install/Production-Ubuntu.md#set-up-nginx-server).
|
||||||
|
|
||||||
|
|
||||||
|
###### `x509: certificate signed by unknown authority` in server logs when attempting to sign-up
|
||||||
|
- This error may appear when attempt to use a self-signed certificate to setup SSL, which is not yet supported by Mattermost. You can resolve this issue by setting up a load balancer like Ngnix. A ticket exists to [add support for self-signed certificates in future](x509: certificate signed by unknown authority).
|
||||||
|
|||||||
@@ -90,8 +90,9 @@ As mentioned above, Mattermost makes it easy to take integrations written for Sl
|
|||||||
|
|
||||||
To see samples and community contributions, please visit <http://mattermost.org/webhooks>.
|
To see samples and community contributions, please visit <http://mattermost.org/webhooks>.
|
||||||
|
|
||||||
#### Limitations
|
#### Known Issues
|
||||||
|
|
||||||
- The `attachments` payload used in Slack is not yet supported
|
- The `attachments` payload used in Slack is not yet supported
|
||||||
- Overriding of usernames does not yet apply to notifications
|
- Overriding of usernames does not yet apply to notifications
|
||||||
- Cannot supply `icon_emoji` to override the message icon
|
- Cannot supply `icon_emoji` to override the message icon
|
||||||
|
- Webhook UI fails when connected to deleted channel
|
||||||
|
|||||||
55
model/analytics_row.go
Обычный файл
55
model/analytics_row.go
Обычный файл
@@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AnalyticsRow struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsRows []*AnalyticsRow
|
||||||
|
|
||||||
|
func (me *AnalyticsRow) ToJson() string {
|
||||||
|
b, err := json.Marshal(me)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnalyticsRowFromJson(data io.Reader) *AnalyticsRow {
|
||||||
|
decoder := json.NewDecoder(data)
|
||||||
|
var me AnalyticsRow
|
||||||
|
err := decoder.Decode(&me)
|
||||||
|
if err == nil {
|
||||||
|
return &me
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me AnalyticsRows) ToJson() string {
|
||||||
|
if b, err := json.Marshal(me); err != nil {
|
||||||
|
return "[]"
|
||||||
|
} else {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnalyticsRowsFromJson(data io.Reader) AnalyticsRows {
|
||||||
|
decoder := json.NewDecoder(data)
|
||||||
|
var me AnalyticsRows
|
||||||
|
err := decoder.Decode(&me)
|
||||||
|
if err == nil {
|
||||||
|
return me
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
37
model/analytics_row_test.go
Обычный файл
37
model/analytics_row_test.go
Обычный файл
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAnalyticsRowJson(t *testing.T) {
|
||||||
|
a1 := AnalyticsRow{}
|
||||||
|
a1.Name = "2015-10-12"
|
||||||
|
a1.Value = 12345.0
|
||||||
|
json := a1.ToJson()
|
||||||
|
ra1 := AnalyticsRowFromJson(strings.NewReader(json))
|
||||||
|
|
||||||
|
if a1.Name != ra1.Name {
|
||||||
|
t.Fatal("days didn't match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnalyticsRowsJson(t *testing.T) {
|
||||||
|
a1 := AnalyticsRow{}
|
||||||
|
a1.Name = "2015-10-12"
|
||||||
|
a1.Value = 12345.0
|
||||||
|
|
||||||
|
var a1s AnalyticsRows = make([]*AnalyticsRow, 1)
|
||||||
|
a1s[0] = &a1
|
||||||
|
|
||||||
|
ljson := a1s.ToJson()
|
||||||
|
results := AnalyticsRowsFromJson(strings.NewReader(ljson))
|
||||||
|
|
||||||
|
if a1s[0].Name != results[0].Name {
|
||||||
|
t.Fatal("Ids do not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -416,6 +416,15 @@ func (c *Client) TestEmail(config *Config) (*Result, *AppError) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetAnalytics(teamId, name string) (*Result, *AppError) {
|
||||||
|
if r, err := c.DoApiGet("/admin/analytics/"+teamId+"/"+name, "", ""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||||
|
r.Header.Get(HEADER_ETAG_SERVER), AnalyticsRowsFromJson(r.Body)}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
|
func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
|
||||||
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
|
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ type IncomingWebhook struct {
|
|||||||
TeamId string `json:"team_id"`
|
TeamId string `json:"team_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IncomingWebhookRequest struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
IconURL string `json:"icon_url"`
|
||||||
|
ChannelName string `json:"channel"`
|
||||||
|
}
|
||||||
|
|
||||||
func (o *IncomingWebhook) ToJson() string {
|
func (o *IncomingWebhook) ToJson() string {
|
||||||
b, err := json.Marshal(o)
|
b, err := json.Marshal(o)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -104,3 +111,14 @@ func (o *IncomingWebhook) PreSave() {
|
|||||||
func (o *IncomingWebhook) PreUpdate() {
|
func (o *IncomingWebhook) PreUpdate() {
|
||||||
o.UpdateAt = GetMillis()
|
o.UpdateAt = GetMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IncomingWebhookRequestFromJson(data io.Reader) *IncomingWebhookRequest {
|
||||||
|
decoder := json.NewDecoder(data)
|
||||||
|
var o IncomingWebhookRequest
|
||||||
|
err := decoder.Decode(&o)
|
||||||
|
if err == nil {
|
||||||
|
return &o
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ func (o *PostList) AddPost(post *Post) {
|
|||||||
o.Posts[post.Id] = post
|
o.Posts[post.Id] = post
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *PostList) Extend(other *PostList) {
|
||||||
|
for _, postId := range other.Order {
|
||||||
|
if _, ok := o.Posts[postId]; !ok {
|
||||||
|
o.AddPost(other.Posts[postId])
|
||||||
|
o.AddOrder(postId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (o *PostList) Etag() string {
|
func (o *PostList) Etag() string {
|
||||||
|
|
||||||
id := "0"
|
id := "0"
|
||||||
|
|||||||
@@ -34,3 +34,37 @@ func TestPostListJson(t *testing.T) {
|
|||||||
t.Fatal("failed to serialize")
|
t.Fatal("failed to serialize")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPostListExtend(t *testing.T) {
|
||||||
|
l1 := PostList{}
|
||||||
|
|
||||||
|
p1 := &Post{Id: NewId(), Message: NewId()}
|
||||||
|
l1.AddPost(p1)
|
||||||
|
l1.AddOrder(p1.Id)
|
||||||
|
|
||||||
|
p2 := &Post{Id: NewId(), Message: NewId()}
|
||||||
|
l1.AddPost(p2)
|
||||||
|
l1.AddOrder(p2.Id)
|
||||||
|
|
||||||
|
l2 := PostList{}
|
||||||
|
|
||||||
|
p3 := &Post{Id: NewId(), Message: NewId()}
|
||||||
|
l2.AddPost(p3)
|
||||||
|
l2.AddOrder(p3.Id)
|
||||||
|
|
||||||
|
l2.Extend(&l1)
|
||||||
|
|
||||||
|
if len(l1.Posts) != 2 || len(l1.Order) != 2 {
|
||||||
|
t.Fatal("extending l2 changed l1")
|
||||||
|
} else if len(l2.Posts) != 3 {
|
||||||
|
t.Fatal("failed to extend posts l2")
|
||||||
|
} else if l2.Order[0] != p3.Id || l2.Order[1] != p1.Id || l2.Order[2] != p2.Id {
|
||||||
|
t.Fatal("failed to extend order of l2")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(l1.Posts) != 2 || len(l1.Order) != 2 {
|
||||||
|
t.Fatal("extending l2 again changed l1")
|
||||||
|
} else if len(l2.Posts) != 3 || len(l2.Order) != 3 {
|
||||||
|
t.Fatal("extending l2 again changed l2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
type SearchParams struct {
|
type SearchParams struct {
|
||||||
Terms string
|
Terms string
|
||||||
IsHashtag bool
|
IsHashtag bool
|
||||||
InChannel string
|
InChannels []string
|
||||||
FromUser string
|
FromUsers []string
|
||||||
}
|
}
|
||||||
|
|
||||||
var searchFlags = [...]string{"from", "channel", "in"}
|
var searchFlags = [...]string{"from", "channel", "in"}
|
||||||
@@ -31,9 +31,9 @@ func splitWords(text string) []string {
|
|||||||
return words
|
return words
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSearchFlags(input []string) ([]string, map[string]string) {
|
func parseSearchFlags(input []string) ([]string, [][2]string) {
|
||||||
words := []string{}
|
words := []string{}
|
||||||
flags := make(map[string]string)
|
flags := [][2]string{}
|
||||||
|
|
||||||
skipNextWord := false
|
skipNextWord := false
|
||||||
for i, word := range input {
|
for i, word := range input {
|
||||||
@@ -52,10 +52,10 @@ func parseSearchFlags(input []string) ([]string, map[string]string) {
|
|||||||
// check for case insensitive equality
|
// check for case insensitive equality
|
||||||
if strings.EqualFold(flag, searchFlag) {
|
if strings.EqualFold(flag, searchFlag) {
|
||||||
if value != "" {
|
if value != "" {
|
||||||
flags[searchFlag] = value
|
flags = append(flags, [2]string{searchFlag, value})
|
||||||
isFlag = true
|
isFlag = true
|
||||||
} else if i < len(input)-1 {
|
} else if i < len(input)-1 {
|
||||||
flags[searchFlag] = input[i+1]
|
flags = append(flags, [2]string{searchFlag, input[i+1]})
|
||||||
skipNextWord = true
|
skipNextWord = true
|
||||||
isFlag = true
|
isFlag = true
|
||||||
}
|
}
|
||||||
@@ -75,56 +75,66 @@ func parseSearchFlags(input []string) ([]string, map[string]string) {
|
|||||||
return words, flags
|
return words, flags
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseSearchParams(text string) (*SearchParams, *SearchParams) {
|
func ParseSearchParams(text string) []*SearchParams {
|
||||||
words, flags := parseSearchFlags(splitWords(text))
|
words, flags := parseSearchFlags(splitWords(text))
|
||||||
|
|
||||||
hashtagTerms := []string{}
|
hashtagTermList := []string{}
|
||||||
plainTerms := []string{}
|
plainTermList := []string{}
|
||||||
|
|
||||||
for _, word := range words {
|
for _, word := range words {
|
||||||
if validHashtag.MatchString(word) {
|
if validHashtag.MatchString(word) {
|
||||||
hashtagTerms = append(hashtagTerms, word)
|
hashtagTermList = append(hashtagTermList, word)
|
||||||
} else {
|
} else {
|
||||||
plainTerms = append(plainTerms, word)
|
plainTermList = append(plainTermList, word)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inChannel := flags["channel"]
|
hashtagTerms := strings.Join(hashtagTermList, " ")
|
||||||
if inChannel == "" {
|
plainTerms := strings.Join(plainTermList, " ")
|
||||||
inChannel = flags["in"]
|
|
||||||
|
inChannels := []string{}
|
||||||
|
fromUsers := []string{}
|
||||||
|
|
||||||
|
for _, flagPair := range flags {
|
||||||
|
flag := flagPair[0]
|
||||||
|
value := flagPair[1]
|
||||||
|
|
||||||
|
if flag == "in" || flag == "channel" {
|
||||||
|
inChannels = append(inChannels, value)
|
||||||
|
} else if flag == "from" {
|
||||||
|
fromUsers = append(fromUsers, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fromUser := flags["from"]
|
paramsList := []*SearchParams{}
|
||||||
|
|
||||||
var plainParams *SearchParams
|
|
||||||
if len(plainTerms) > 0 {
|
if len(plainTerms) > 0 {
|
||||||
plainParams = &SearchParams{
|
paramsList = append(paramsList, &SearchParams{
|
||||||
Terms: strings.Join(plainTerms, " "),
|
Terms: plainTerms,
|
||||||
IsHashtag: false,
|
IsHashtag: false,
|
||||||
InChannel: inChannel,
|
InChannels: inChannels,
|
||||||
FromUser: fromUser,
|
FromUsers: fromUsers,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var hashtagParams *SearchParams
|
|
||||||
if len(hashtagTerms) > 0 {
|
if len(hashtagTerms) > 0 {
|
||||||
hashtagParams = &SearchParams{
|
paramsList = append(paramsList, &SearchParams{
|
||||||
Terms: strings.Join(hashtagTerms, " "),
|
Terms: hashtagTerms,
|
||||||
IsHashtag: true,
|
IsHashtag: true,
|
||||||
InChannel: inChannel,
|
InChannels: inChannels,
|
||||||
FromUser: fromUser,
|
FromUsers: fromUsers,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// special case for when no terms are specified but we still have a filter
|
// special case for when no terms are specified but we still have a filter
|
||||||
if plainParams == nil && hashtagParams == nil && (inChannel != "" || fromUser != "") {
|
if len(plainTerms) == 0 && len(hashtagTerms) == 0 {
|
||||||
plainParams = &SearchParams{
|
paramsList = append(paramsList, &SearchParams{
|
||||||
Terms: "",
|
Terms: "",
|
||||||
IsHashtag: false,
|
IsHashtag: true,
|
||||||
InChannel: inChannel,
|
InChannels: inChannels,
|
||||||
FromUser: fromUser,
|
FromUsers: fromUsers,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return plainParams, hashtagParams
|
return paramsList
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,25 +28,25 @@ func TestParseSearchFlags(t *testing.T) {
|
|||||||
|
|
||||||
if words, flags := parseSearchFlags(splitWords("apple banana from:chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
|
if words, flags := parseSearchFlags(splitWords("apple banana from:chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
|
||||||
t.Fatalf("got incorrect words %v", words)
|
t.Fatalf("got incorrect words %v", words)
|
||||||
} else if len(flags) != 1 || flags["from"] != "chan" {
|
} else if len(flags) != 1 || flags[0][0] != "from" || flags[0][1] != "chan" {
|
||||||
t.Fatalf("got incorrect flags %v", flags)
|
t.Fatalf("got incorrect flags %v", flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
if words, flags := parseSearchFlags(splitWords("apple banana from: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
|
if words, flags := parseSearchFlags(splitWords("apple banana from: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
|
||||||
t.Fatalf("got incorrect words %v", words)
|
t.Fatalf("got incorrect words %v", words)
|
||||||
} else if len(flags) != 1 || flags["from"] != "chan" {
|
} else if len(flags) != 1 || flags[0][0] != "from" || flags[0][1] != "chan" {
|
||||||
t.Fatalf("got incorrect flags %v", flags)
|
t.Fatalf("got incorrect flags %v", flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
if words, flags := parseSearchFlags(splitWords("apple banana in: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
|
if words, flags := parseSearchFlags(splitWords("apple banana in: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
|
||||||
t.Fatalf("got incorrect words %v", words)
|
t.Fatalf("got incorrect words %v", words)
|
||||||
} else if len(flags) != 1 || flags["in"] != "chan" {
|
} else if len(flags) != 1 || flags[0][0] != "in" || flags[0][1] != "chan" {
|
||||||
t.Fatalf("got incorrect flags %v", flags)
|
t.Fatalf("got incorrect flags %v", flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
if words, flags := parseSearchFlags(splitWords("apple banana channel:chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
|
if words, flags := parseSearchFlags(splitWords("apple banana channel:chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
|
||||||
t.Fatalf("got incorrect words %v", words)
|
t.Fatalf("got incorrect words %v", words)
|
||||||
} else if len(flags) != 1 || flags["channel"] != "chan" {
|
} else if len(flags) != 1 || flags[0][0] != "channel" || flags[0][1] != "chan" {
|
||||||
t.Fatalf("got incorrect flags %v", flags)
|
t.Fatalf("got incorrect flags %v", flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,14 @@ func TestParseSearchFlags(t *testing.T) {
|
|||||||
|
|
||||||
if words, flags := parseSearchFlags(splitWords("channel: first in: second from:")); len(words) != 1 || words[0] != "from:" {
|
if words, flags := parseSearchFlags(splitWords("channel: first in: second from:")); len(words) != 1 || words[0] != "from:" {
|
||||||
t.Fatalf("got incorrect words %v", words)
|
t.Fatalf("got incorrect words %v", words)
|
||||||
} else if len(flags) != 2 || flags["channel"] != "first" || flags["in"] != "second" {
|
} else if len(flags) != 2 || flags[0][0] != "channel" || flags[0][1] != "first" || flags[1][0] != "in" || flags[1][1] != "second" {
|
||||||
|
t.Fatalf("got incorrect flags %v", flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if words, flags := parseSearchFlags(splitWords("channel: first channel: second from: third from: fourth")); len(words) != 0 {
|
||||||
|
t.Fatalf("got incorrect words %v", words)
|
||||||
|
} else if len(flags) != 4 || flags[0][0] != "channel" || flags[0][1] != "first" || flags[1][0] != "channel" || flags[1][1] != "second" ||
|
||||||
|
flags[2][0] != "from" || flags[2][1] != "third" || flags[3][0] != "from" || flags[3][1] != "fourth" {
|
||||||
t.Fatalf("got incorrect flags %v", flags)
|
t.Fatalf("got incorrect flags %v", flags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
|
|||||||
|
|
||||||
func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
|
func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
|
||||||
|
|
||||||
// BEGIN REMOVE AFTER 1.1.0
|
// REMOVE AFTER 1.2 SHIP see PLT-828
|
||||||
if s.CreateColumnIfNotExists("ChannelMembers", "NotifyProps", "varchar(2000)", "varchar(2000)", "{}") {
|
if s.CreateColumnIfNotExists("ChannelMembers", "NotifyProps", "varchar(2000)", "varchar(2000)", "{}") {
|
||||||
// populate NotifyProps from existing NotifyLevel field
|
// populate NotifyProps from existing NotifyLevel field
|
||||||
|
|
||||||
@@ -83,7 +83,6 @@ func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
|
|||||||
|
|
||||||
s.RemoveColumnIfExists("ChannelMembers", "NotifyLevel")
|
s.RemoveColumnIfExists("ChannelMembers", "NotifyLevel")
|
||||||
}
|
}
|
||||||
// END REMOVE AFTER 1.1.0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SqlChannelStore) CreateIndexesIfNotExists() {
|
func (s SqlChannelStore) CreateIndexesIfNotExists() {
|
||||||
@@ -829,3 +828,31 @@ func (s SqlChannelStore) GetForExport(teamId string) StoreChannel {
|
|||||||
|
|
||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlChannelStore) AnalyticsTypeCount(teamId string, channelType string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
v, err := s.GetReplica().SelectInt(
|
||||||
|
`SELECT
|
||||||
|
COUNT(Id) AS Value
|
||||||
|
FROM
|
||||||
|
Channels
|
||||||
|
WHERE
|
||||||
|
TeamId = :TeamId
|
||||||
|
AND Type = :ChannelType`,
|
||||||
|
map[string]interface{}{"TeamId": teamId, "ChannelType": channelType})
|
||||||
|
if err != nil {
|
||||||
|
result.Err = model.NewAppError("SqlChannelStore.AnalyticsTypeCount", "We couldn't get channel type counts", err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = v
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|||||||
@@ -561,6 +561,24 @@ func TestChannelStoreGetMoreChannels(t *testing.T) {
|
|||||||
if list.Channels[0].Name != o3.Name {
|
if list.Channels[0].Name != o3.Name {
|
||||||
t.Fatal("missing channel")
|
t.Fatal("missing channel")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r1 := <-store.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_OPEN); r1.Err != nil {
|
||||||
|
t.Fatal(r1.Err)
|
||||||
|
} else {
|
||||||
|
if r1.Data.(int64) != 2 {
|
||||||
|
t.Log(r1.Data)
|
||||||
|
t.Fatal("wrong value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r1 := <-store.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_PRIVATE); r1.Err != nil {
|
||||||
|
t.Fatal(r1.Err)
|
||||||
|
} else {
|
||||||
|
if r1.Data.(int64) != 2 {
|
||||||
|
t.Log(r1.Data)
|
||||||
|
t.Fatal("wrong value")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChannelStoreGetChannelCounts(t *testing.T) {
|
func TestChannelStoreGetChannelCounts(t *testing.T) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package store
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mattermost/platform/model"
|
"github.com/mattermost/platform/model"
|
||||||
@@ -413,10 +414,15 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
|
|||||||
go func() {
|
go func() {
|
||||||
result := StoreResult{}
|
result := StoreResult{}
|
||||||
|
|
||||||
|
queryParams := map[string]interface{}{
|
||||||
|
"TeamId": teamId,
|
||||||
|
"UserId": userId,
|
||||||
|
}
|
||||||
|
|
||||||
termMap := map[string]bool{}
|
termMap := map[string]bool{}
|
||||||
terms := params.Terms
|
terms := params.Terms
|
||||||
|
|
||||||
if terms == "" && params.InChannel == "" && params.FromUser == "" {
|
if terms == "" && len(params.InChannels) == 0 && len(params.FromUsers) == 0 {
|
||||||
result.Data = []*model.Post{}
|
result.Data = []*model.Post{}
|
||||||
storeChannel <- result
|
storeChannel <- result
|
||||||
return
|
return
|
||||||
@@ -468,13 +474,45 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
|
|||||||
ORDER BY CreateAt DESC
|
ORDER BY CreateAt DESC
|
||||||
LIMIT 100`
|
LIMIT 100`
|
||||||
|
|
||||||
if params.InChannel != "" {
|
if len(params.InChannels) > 1 {
|
||||||
|
inClause := ":InChannel0"
|
||||||
|
queryParams["InChannel0"] = params.InChannels[0]
|
||||||
|
|
||||||
|
for i := 1; i < len(params.InChannels); i++ {
|
||||||
|
paramName := "InChannel" + strconv.FormatInt(int64(i), 10)
|
||||||
|
inClause += ", :" + paramName
|
||||||
|
queryParams[paramName] = params.InChannels[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "AND Name IN ("+inClause+")", 1)
|
||||||
|
} else if len(params.InChannels) == 1 {
|
||||||
|
queryParams["InChannel"] = params.InChannels[0]
|
||||||
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "AND Name = :InChannel", 1)
|
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "AND Name = :InChannel", 1)
|
||||||
} else {
|
} else {
|
||||||
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "", 1)
|
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if params.FromUser != "" {
|
if len(params.FromUsers) > 1 {
|
||||||
|
inClause := ":FromUser0"
|
||||||
|
queryParams["FromUser0"] = params.FromUsers[0]
|
||||||
|
|
||||||
|
for i := 1; i < len(params.FromUsers); i++ {
|
||||||
|
paramName := "FromUser" + strconv.FormatInt(int64(i), 10)
|
||||||
|
inClause += ", :" + paramName
|
||||||
|
queryParams[paramName] = params.FromUsers[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
searchQuery = strings.Replace(searchQuery, "POST_FILTER", `
|
||||||
|
AND UserId IN (
|
||||||
|
SELECT
|
||||||
|
Id
|
||||||
|
FROM
|
||||||
|
Users
|
||||||
|
WHERE
|
||||||
|
TeamId = :TeamId
|
||||||
|
AND Username IN (`+inClause+`))`, 1)
|
||||||
|
} else if len(params.FromUsers) == 1 {
|
||||||
|
queryParams["FromUser"] = params.FromUsers[0]
|
||||||
searchQuery = strings.Replace(searchQuery, "POST_FILTER", `
|
searchQuery = strings.Replace(searchQuery, "POST_FILTER", `
|
||||||
AND UserId IN (
|
AND UserId IN (
|
||||||
SELECT
|
SELECT
|
||||||
@@ -506,13 +544,7 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
|
|||||||
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
|
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
queryParams := map[string]interface{}{
|
queryParams["Terms"] = terms
|
||||||
"TeamId": teamId,
|
|
||||||
"UserId": userId,
|
|
||||||
"Terms": terms,
|
|
||||||
"InChannel": params.InChannel,
|
|
||||||
"FromUser": params.FromUser,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := s.GetReplica().Select(&posts, searchQuery, queryParams)
|
_, err := s.GetReplica().Select(&posts, searchQuery, queryParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -571,3 +603,152 @@ func (s SqlPostStore) GetForExport(channelId string) StoreChannel {
|
|||||||
|
|
||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
query :=
|
||||||
|
`SELECT
|
||||||
|
t1.Name, COUNT(t1.UserId) AS Value
|
||||||
|
FROM
|
||||||
|
(SELECT DISTINCT
|
||||||
|
DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
|
||||||
|
Posts.UserId
|
||||||
|
FROM
|
||||||
|
Posts, Channels
|
||||||
|
WHERE
|
||||||
|
Posts.ChannelId = Channels.Id
|
||||||
|
AND Channels.TeamId = :TeamId
|
||||||
|
ORDER BY Name DESC) AS t1
|
||||||
|
GROUP BY Name
|
||||||
|
ORDER BY Name DESC
|
||||||
|
LIMIT 30`
|
||||||
|
|
||||||
|
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
|
||||||
|
query =
|
||||||
|
`SELECT
|
||||||
|
TO_CHAR(t1.Name, 'YYYY-MM-DD') AS Name, COUNT(t1.UserId) AS Value
|
||||||
|
FROM
|
||||||
|
(SELECT DISTINCT
|
||||||
|
DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)) AS Name,
|
||||||
|
Posts.UserId
|
||||||
|
FROM
|
||||||
|
Posts, Channels
|
||||||
|
WHERE
|
||||||
|
Posts.ChannelId = Channels.Id
|
||||||
|
AND Channels.TeamId = :TeamId
|
||||||
|
ORDER BY Name DESC) AS t1
|
||||||
|
GROUP BY Name
|
||||||
|
ORDER BY Name DESC
|
||||||
|
LIMIT 30`
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows model.AnalyticsRows
|
||||||
|
_, err := s.GetReplica().Select(
|
||||||
|
&rows,
|
||||||
|
query,
|
||||||
|
map[string]interface{}{"TeamId": teamId, "Time": model.GetMillis() - 1000*60*60*24*31})
|
||||||
|
if err != nil {
|
||||||
|
result.Err = model.NewAppError("SqlPostStore.AnalyticsUserCountsWithPostsByDay", "We couldn't get user counts with posts", err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = rows
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
query :=
|
||||||
|
`SELECT
|
||||||
|
Name, COUNT(Value) AS Value
|
||||||
|
FROM
|
||||||
|
(SELECT
|
||||||
|
DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
|
||||||
|
'1' AS Value
|
||||||
|
FROM
|
||||||
|
Posts, Channels
|
||||||
|
WHERE
|
||||||
|
Posts.ChannelId = Channels.Id
|
||||||
|
AND Channels.TeamId = :TeamId
|
||||||
|
AND Posts.CreateAt >:Time) AS t1
|
||||||
|
GROUP BY Name
|
||||||
|
ORDER BY Name DESC
|
||||||
|
LIMIT 30`
|
||||||
|
|
||||||
|
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
|
||||||
|
query =
|
||||||
|
`SELECT
|
||||||
|
Name, COUNT(Value) AS Value
|
||||||
|
FROM
|
||||||
|
(SELECT
|
||||||
|
TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name,
|
||||||
|
'1' AS Value
|
||||||
|
FROM
|
||||||
|
Posts, Channels
|
||||||
|
WHERE
|
||||||
|
Posts.ChannelId = Channels.Id
|
||||||
|
AND Channels.TeamId = :TeamId
|
||||||
|
AND Posts.CreateAt > :Time) AS t1
|
||||||
|
GROUP BY Name
|
||||||
|
ORDER BY Name DESC
|
||||||
|
LIMIT 30`
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows model.AnalyticsRows
|
||||||
|
_, err := s.GetReplica().Select(
|
||||||
|
&rows,
|
||||||
|
query,
|
||||||
|
map[string]interface{}{"TeamId": teamId, "Time": model.GetMillis() - 1000*60*60*24*31})
|
||||||
|
if err != nil {
|
||||||
|
result.Err = model.NewAppError("SqlPostStore.AnalyticsPostCountsByDay", "We couldn't get post counts by day", err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = rows
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlPostStore) AnalyticsPostCount(teamId string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
v, err := s.GetReplica().SelectInt(
|
||||||
|
`SELECT
|
||||||
|
COUNT(Posts.Id) AS Value
|
||||||
|
FROM
|
||||||
|
Posts,
|
||||||
|
Channels
|
||||||
|
WHERE
|
||||||
|
Posts.ChannelId = Channels.Id
|
||||||
|
AND Channels.TeamId = :TeamId`,
|
||||||
|
map[string]interface{}{"TeamId": teamId})
|
||||||
|
if err != nil {
|
||||||
|
result.Err = model.NewAppError("SqlPostStore.AnalyticsPostCount", "We couldn't get post counts", err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = v
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|||||||
@@ -580,3 +580,135 @@ func TestPostStoreSearch(t *testing.T) {
|
|||||||
t.Fatal("returned wrong search result")
|
t.Fatal("returned wrong search result")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUserCountsWithPostsByDay(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
t1 := &model.Team{}
|
||||||
|
t1.DisplayName = "DisplayName"
|
||||||
|
t1.Name = "a" + model.NewId() + "b"
|
||||||
|
t1.Email = model.NewId() + "@nowhere.com"
|
||||||
|
t1.Type = model.TEAM_OPEN
|
||||||
|
t1 = Must(store.Team().Save(t1)).(*model.Team)
|
||||||
|
|
||||||
|
c1 := &model.Channel{}
|
||||||
|
c1.TeamId = t1.Id
|
||||||
|
c1.DisplayName = "Channel2"
|
||||||
|
c1.Name = "a" + model.NewId() + "b"
|
||||||
|
c1.Type = model.CHANNEL_OPEN
|
||||||
|
c1 = Must(store.Channel().Save(c1)).(*model.Channel)
|
||||||
|
|
||||||
|
o1 := &model.Post{}
|
||||||
|
o1.ChannelId = c1.Id
|
||||||
|
o1.UserId = model.NewId()
|
||||||
|
o1.CreateAt = model.GetMillis()
|
||||||
|
o1.Message = "a" + model.NewId() + "b"
|
||||||
|
o1 = Must(store.Post().Save(o1)).(*model.Post)
|
||||||
|
|
||||||
|
o1a := &model.Post{}
|
||||||
|
o1a.ChannelId = c1.Id
|
||||||
|
o1a.UserId = model.NewId()
|
||||||
|
o1a.CreateAt = o1.CreateAt
|
||||||
|
o1a.Message = "a" + model.NewId() + "b"
|
||||||
|
o1a = Must(store.Post().Save(o1a)).(*model.Post)
|
||||||
|
|
||||||
|
o2 := &model.Post{}
|
||||||
|
o2.ChannelId = c1.Id
|
||||||
|
o2.UserId = model.NewId()
|
||||||
|
o2.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
|
||||||
|
o2.Message = "a" + model.NewId() + "b"
|
||||||
|
o2 = Must(store.Post().Save(o2)).(*model.Post)
|
||||||
|
|
||||||
|
o2a := &model.Post{}
|
||||||
|
o2a.ChannelId = c1.Id
|
||||||
|
o2a.UserId = o2.UserId
|
||||||
|
o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
|
||||||
|
o2a.Message = "a" + model.NewId() + "b"
|
||||||
|
o2a = Must(store.Post().Save(o2a)).(*model.Post)
|
||||||
|
|
||||||
|
if r1 := <-store.Post().AnalyticsUserCountsWithPostsByDay(t1.Id); r1.Err != nil {
|
||||||
|
t.Fatal(r1.Err)
|
||||||
|
} else {
|
||||||
|
row1 := r1.Data.(model.AnalyticsRows)[0]
|
||||||
|
if row1.Value != 2 {
|
||||||
|
t.Fatal("wrong value")
|
||||||
|
}
|
||||||
|
|
||||||
|
row2 := r1.Data.(model.AnalyticsRows)[1]
|
||||||
|
if row2.Value != 1 {
|
||||||
|
t.Fatal("wrong value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCountsByDay(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
t1 := &model.Team{}
|
||||||
|
t1.DisplayName = "DisplayName"
|
||||||
|
t1.Name = "a" + model.NewId() + "b"
|
||||||
|
t1.Email = model.NewId() + "@nowhere.com"
|
||||||
|
t1.Type = model.TEAM_OPEN
|
||||||
|
t1 = Must(store.Team().Save(t1)).(*model.Team)
|
||||||
|
|
||||||
|
c1 := &model.Channel{}
|
||||||
|
c1.TeamId = t1.Id
|
||||||
|
c1.DisplayName = "Channel2"
|
||||||
|
c1.Name = "a" + model.NewId() + "b"
|
||||||
|
c1.Type = model.CHANNEL_OPEN
|
||||||
|
c1 = Must(store.Channel().Save(c1)).(*model.Channel)
|
||||||
|
|
||||||
|
o1 := &model.Post{}
|
||||||
|
o1.ChannelId = c1.Id
|
||||||
|
o1.UserId = model.NewId()
|
||||||
|
o1.CreateAt = model.GetMillis()
|
||||||
|
o1.Message = "a" + model.NewId() + "b"
|
||||||
|
o1 = Must(store.Post().Save(o1)).(*model.Post)
|
||||||
|
|
||||||
|
o1a := &model.Post{}
|
||||||
|
o1a.ChannelId = c1.Id
|
||||||
|
o1a.UserId = model.NewId()
|
||||||
|
o1a.CreateAt = o1.CreateAt
|
||||||
|
o1a.Message = "a" + model.NewId() + "b"
|
||||||
|
o1a = Must(store.Post().Save(o1a)).(*model.Post)
|
||||||
|
|
||||||
|
o2 := &model.Post{}
|
||||||
|
o2.ChannelId = c1.Id
|
||||||
|
o2.UserId = model.NewId()
|
||||||
|
o2.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24 * 2)
|
||||||
|
o2.Message = "a" + model.NewId() + "b"
|
||||||
|
o2 = Must(store.Post().Save(o2)).(*model.Post)
|
||||||
|
|
||||||
|
o2a := &model.Post{}
|
||||||
|
o2a.ChannelId = c1.Id
|
||||||
|
o2a.UserId = o2.UserId
|
||||||
|
o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24 * 2)
|
||||||
|
o2a.Message = "a" + model.NewId() + "b"
|
||||||
|
o2a = Must(store.Post().Save(o2a)).(*model.Post)
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
t.Log(t1.Id)
|
||||||
|
|
||||||
|
if r1 := <-store.Post().AnalyticsPostCountsByDay(t1.Id); r1.Err != nil {
|
||||||
|
t.Fatal(r1.Err)
|
||||||
|
} else {
|
||||||
|
row1 := r1.Data.(model.AnalyticsRows)[0]
|
||||||
|
if row1.Value != 2 {
|
||||||
|
t.Log(row1)
|
||||||
|
t.Fatal("wrong value")
|
||||||
|
}
|
||||||
|
|
||||||
|
row2 := r1.Data.(model.AnalyticsRows)[1]
|
||||||
|
if row2.Value != 2 {
|
||||||
|
t.Fatal("wrong value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r1 := <-store.Post().AnalyticsPostCount(t1.Id); r1.Err != nil {
|
||||||
|
t.Fatal(r1.Err)
|
||||||
|
} else {
|
||||||
|
if r1.Data.(int64) != 4 {
|
||||||
|
t.Fatal("wrong value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ func NewSqlStore() Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
schemaVersion := sqlStore.GetCurrentSchemaVersion()
|
schemaVersion := sqlStore.GetCurrentSchemaVersion()
|
||||||
isSchemaVersion07 := false
|
isSchemaVersion07 := false // REMOVE AFTER 1.2 SHIP see PLT-828
|
||||||
|
isSchemaVersion10 := false // REMOVE AFTER 1.2 SHIP see PLT-828
|
||||||
|
|
||||||
// If the version is already set then we are potentially in an 'upgrade needed' state
|
// If the version is already set then we are potentially in an 'upgrade needed' state
|
||||||
if schemaVersion != "" {
|
if schemaVersion != "" {
|
||||||
@@ -86,7 +87,11 @@ func NewSqlStore() Store {
|
|||||||
isSchemaVersion07 = true
|
isSchemaVersion07 = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if model.IsPreviousVersion(schemaVersion) || isSchemaVersion07 {
|
if schemaVersion == "1.0.0" {
|
||||||
|
isSchemaVersion10 = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.IsPreviousVersion(schemaVersion) || isSchemaVersion07 || isSchemaVersion10 {
|
||||||
l4g.Warn("The database schema version of " + schemaVersion + " appears to be out of date")
|
l4g.Warn("The database schema version of " + schemaVersion + " appears to be out of date")
|
||||||
l4g.Warn("Attempting to upgrade the database schema version to " + model.CurrentVersion)
|
l4g.Warn("Attempting to upgrade the database schema version to " + model.CurrentVersion)
|
||||||
} else {
|
} else {
|
||||||
@@ -98,7 +103,7 @@ func NewSqlStore() Store {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// REMOVE in 1.2
|
// REMOVE AFTER 1.2 SHIP see PLT-828
|
||||||
if sqlStore.DoesTableExist("Sessions") {
|
if sqlStore.DoesTableExist("Sessions") {
|
||||||
if sqlStore.DoesColumnExist("Sessions", "AltId") {
|
if sqlStore.DoesColumnExist("Sessions", "AltId") {
|
||||||
sqlStore.GetMaster().Exec("DROP TABLE IF EXISTS Sessions")
|
sqlStore.GetMaster().Exec("DROP TABLE IF EXISTS Sessions")
|
||||||
@@ -140,7 +145,7 @@ func NewSqlStore() Store {
|
|||||||
sqlStore.webhook.(*SqlWebhookStore).CreateIndexesIfNotExists()
|
sqlStore.webhook.(*SqlWebhookStore).CreateIndexesIfNotExists()
|
||||||
sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists()
|
sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists()
|
||||||
|
|
||||||
if model.IsPreviousVersion(schemaVersion) || isSchemaVersion07 {
|
if model.IsPreviousVersion(schemaVersion) || isSchemaVersion07 || isSchemaVersion10 {
|
||||||
sqlStore.system.Update(&model.System{Name: "Version", Value: model.CurrentVersion})
|
sqlStore.system.Update(&model.System{Name: "Version", Value: model.CurrentVersion})
|
||||||
l4g.Warn("The database schema has been upgraded to version " + model.CurrentVersion)
|
l4g.Warn("The database schema has been upgraded to version " + model.CurrentVersion)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func NewSqlTeamStore(sqlStore *SqlStore) TeamStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s SqlTeamStore) UpgradeSchemaIfNeeded() {
|
func (s SqlTeamStore) UpgradeSchemaIfNeeded() {
|
||||||
// REMOVE in 1.2
|
// REMOVE AFTER 1.2 SHIP see PLT-828
|
||||||
s.RemoveColumnIfExists("Teams", "AllowValet")
|
s.RemoveColumnIfExists("Teams", "AllowValet")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (us SqlUserStore) UpgradeSchemaIfNeeded() {
|
func (us SqlUserStore) UpgradeSchemaIfNeeded() {
|
||||||
// REMOVE in 1.2
|
// REMOVE AFTER 1.2 SHIP see PLT-828
|
||||||
us.CreateColumnIfNotExists("Users", "ThemeProps", "varchar(2000)", "character varying(2000)", "{}")
|
us.CreateColumnIfNotExists("Users", "ThemeProps", "varchar(2000)", "character varying(2000)", "{}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ type ChannelStore interface {
|
|||||||
CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel
|
CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel
|
||||||
UpdateLastViewedAt(channelId string, userId string) StoreChannel
|
UpdateLastViewedAt(channelId string, userId string) StoreChannel
|
||||||
IncrementMentionCount(channelId string, userId string) StoreChannel
|
IncrementMentionCount(channelId string, userId string) StoreChannel
|
||||||
|
AnalyticsTypeCount(teamId string, channelType string) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostStore interface {
|
type PostStore interface {
|
||||||
@@ -87,6 +88,9 @@ type PostStore interface {
|
|||||||
GetEtag(channelId string) StoreChannel
|
GetEtag(channelId string) StoreChannel
|
||||||
Search(teamId string, userId string, params *model.SearchParams) StoreChannel
|
Search(teamId string, userId string, params *model.SearchParams) StoreChannel
|
||||||
GetForExport(channelId string) StoreChannel
|
GetForExport(channelId string) StoreChannel
|
||||||
|
AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel
|
||||||
|
AnalyticsPostCountsByDay(teamId string) StoreChannel
|
||||||
|
AnalyticsPostCount(teamId string) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserStore interface {
|
type UserStore interface {
|
||||||
|
|||||||
@@ -159,6 +159,13 @@ func LoadConfig(fileName string) {
|
|||||||
configureLog(&config.LogSettings)
|
configureLog(&config.LogSettings)
|
||||||
TestConnection(&config)
|
TestConnection(&config)
|
||||||
|
|
||||||
|
if config.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
|
||||||
|
dir := config.FileSettings.Directory
|
||||||
|
if len(dir) > 0 && dir[len(dir)-1:] != "/" {
|
||||||
|
config.FileSettings.Directory += "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Cfg = &config
|
Cfg = &config
|
||||||
SanitizeOptions = getSanitizeOptions(Cfg)
|
SanitizeOptions = getSanitizeOptions(Cfg)
|
||||||
ClientCfg = getClientConfig(Cfg)
|
ClientCfg = getClientConfig(Cfg)
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
"globals": {
|
"globals": {
|
||||||
"React": false,
|
"React": false,
|
||||||
"ReactDOM": false,
|
"ReactDOM": false,
|
||||||
"ReactBootstrap": false
|
"ReactBootstrap": false,
|
||||||
|
"Chart": false
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"comma-dangle": [2, "never"],
|
"comma-dangle": [2, "never"],
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ var SqlSettingsTab = require('./sql_settings.jsx');
|
|||||||
var TeamSettingsTab = require('./team_settings.jsx');
|
var TeamSettingsTab = require('./team_settings.jsx');
|
||||||
var ServiceSettingsTab = require('./service_settings.jsx');
|
var ServiceSettingsTab = require('./service_settings.jsx');
|
||||||
var TeamUsersTab = require('./team_users.jsx');
|
var TeamUsersTab = require('./team_users.jsx');
|
||||||
|
var TeamAnalyticsTab = require('./team_analytics.jsx');
|
||||||
|
|
||||||
export default class AdminController extends React.Component {
|
export default class AdminController extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -149,6 +150,10 @@ export default class AdminController extends React.Component {
|
|||||||
if (this.state.teams) {
|
if (this.state.teams) {
|
||||||
tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
|
tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
|
||||||
}
|
}
|
||||||
|
} else if (this.state.selected === 'team_analytics') {
|
||||||
|
if (this.state.teams) {
|
||||||
|
tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]} />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default class AdminSidebar extends React.Component {
|
|||||||
handleClick(name, teamId, e) {
|
handleClick(name, teamId, e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.selectTab(name, teamId);
|
this.props.selectTab(name, teamId);
|
||||||
history.pushState({name: name, teamId: teamId}, null, `/admin_console/${name}/${teamId || ''}`);
|
history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
isSelected(name, teamId) {
|
isSelected(name, teamId) {
|
||||||
@@ -121,6 +121,15 @@ export default class AdminSidebar extends React.Component {
|
|||||||
{'- Users'}
|
{'- Users'}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href='#'
|
||||||
|
className={this.isSelected('team_analytics', team.id)}
|
||||||
|
onClick={this.handleClick.bind(this, 'team_analytics', team.id)}
|
||||||
|
>
|
||||||
|
{'- Statistics'}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
50
web/react/components/admin_console/line_chart.jsx
Обычный файл
50
web/react/components/admin_console/line_chart.jsx
Обычный файл
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
export default class LineChart extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.initChart = this.initChart.bind(this);
|
||||||
|
this.chart = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.initChart(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.destroy();
|
||||||
|
this.initChart(nextProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initChart(props) {
|
||||||
|
var el = ReactDOM.findDOMNode(this);
|
||||||
|
var ctx = el.getContext('2d');
|
||||||
|
this.chart = new Chart(ctx).Line(props.data, props.options || {}); //eslint-disable-line new-cap
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
width={this.props.width}
|
||||||
|
height={this.props.height}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LineChart.propTypes = {
|
||||||
|
width: React.PropTypes.string,
|
||||||
|
height: React.PropTypes.string,
|
||||||
|
data: React.PropTypes.object,
|
||||||
|
options: React.PropTypes.object
|
||||||
|
};
|
||||||
353
web/react/components/admin_console/team_analytics.jsx
Обычный файл
353
web/react/components/admin_console/team_analytics.jsx
Обычный файл
@@ -0,0 +1,353 @@
|
|||||||
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
var Client = require('../../utils/client.jsx');
|
||||||
|
var Utils = require('../../utils/utils.jsx');
|
||||||
|
var LineChart = require('./line_chart.jsx');
|
||||||
|
|
||||||
|
export default class TeamAnalytics extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.getData = this.getData.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
users: null,
|
||||||
|
serverError: null,
|
||||||
|
channel_open_count: null,
|
||||||
|
channel_private_count: null,
|
||||||
|
post_count: null,
|
||||||
|
post_counts_day: null,
|
||||||
|
user_counts_with_posts_day: null,
|
||||||
|
recent_active_users: null,
|
||||||
|
newly_created_users: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.getData(this.props.team.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getData(teamId) {
|
||||||
|
Client.getAnalytics(
|
||||||
|
teamId,
|
||||||
|
'standard',
|
||||||
|
(data) => {
|
||||||
|
for (var index in data) {
|
||||||
|
if (data[index].name === 'channel_open_count') {
|
||||||
|
this.setState({channel_open_count: data[index].value});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data[index].name === 'channel_private_count') {
|
||||||
|
this.setState({channel_private_count: data[index].value});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data[index].name === 'post_count') {
|
||||||
|
this.setState({post_count: data[index].value});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this.setState({serverError: err.message});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Client.getAnalytics(
|
||||||
|
teamId,
|
||||||
|
'post_counts_day',
|
||||||
|
(data) => {
|
||||||
|
var chartData = {
|
||||||
|
labels: [],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Total Posts',
|
||||||
|
fillColor: 'rgba(151,187,205,0.2)',
|
||||||
|
strokeColor: 'rgba(151,187,205,1)',
|
||||||
|
pointColor: 'rgba(151,187,205,1)',
|
||||||
|
pointStrokeColor: '#fff',
|
||||||
|
pointHighlightFill: '#fff',
|
||||||
|
pointHighlightStroke: 'rgba(151,187,205,1)',
|
||||||
|
data: []
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var index in data) {
|
||||||
|
if (data[index]) {
|
||||||
|
var row = data[index];
|
||||||
|
chartData.labels.push(row.name);
|
||||||
|
chartData.datasets[0].data.push(row.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({post_counts_day: chartData});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this.setState({serverError: err.message});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Client.getAnalytics(
|
||||||
|
teamId,
|
||||||
|
'user_counts_with_posts_day',
|
||||||
|
(data) => {
|
||||||
|
var chartData = {
|
||||||
|
labels: [],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Active Users With Posts',
|
||||||
|
fillColor: 'rgba(151,187,205,0.2)',
|
||||||
|
strokeColor: 'rgba(151,187,205,1)',
|
||||||
|
pointColor: 'rgba(151,187,205,1)',
|
||||||
|
pointStrokeColor: '#fff',
|
||||||
|
pointHighlightFill: '#fff',
|
||||||
|
pointHighlightStroke: 'rgba(151,187,205,1)',
|
||||||
|
data: []
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var index in data) {
|
||||||
|
if (data[index]) {
|
||||||
|
var row = data[index];
|
||||||
|
chartData.labels.push(row.name);
|
||||||
|
chartData.datasets[0].data.push(row.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({user_counts_with_posts_day: chartData});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this.setState({serverError: err.message});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Client.getProfilesForTeam(
|
||||||
|
teamId,
|
||||||
|
(users) => {
|
||||||
|
this.setState({users});
|
||||||
|
|
||||||
|
var usersList = [];
|
||||||
|
for (var id in users) {
|
||||||
|
if (users.hasOwnProperty(id)) {
|
||||||
|
usersList.push(users[id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usersList.sort((a, b) => {
|
||||||
|
if (a.last_activity_at < b.last_activity_at) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.last_activity_at > b.last_activity_at) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
var recentActive = [];
|
||||||
|
for (let i = 0; i < usersList.length; i++) {
|
||||||
|
recentActive.push(usersList[i]);
|
||||||
|
if (i > 19) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({recent_active_users: recentActive});
|
||||||
|
|
||||||
|
usersList.sort((a, b) => {
|
||||||
|
if (a.create_at < b.create_at) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.create_at > b.create_at) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
var newlyCreated = [];
|
||||||
|
for (let i = 0; i < usersList.length; i++) {
|
||||||
|
newlyCreated.push(usersList[i]);
|
||||||
|
if (i > 19) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({newly_created_users: newlyCreated});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this.setState({serverError: err.message});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(newProps) {
|
||||||
|
this.setState({
|
||||||
|
users: null,
|
||||||
|
serverError: null,
|
||||||
|
channel_open_count: null,
|
||||||
|
channel_private_count: null,
|
||||||
|
post_count: null,
|
||||||
|
post_counts_day: null,
|
||||||
|
user_counts_with_posts_day: null,
|
||||||
|
recent_active_users: null,
|
||||||
|
newly_created_users: null
|
||||||
|
});
|
||||||
|
|
||||||
|
this.getData(newProps.team.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
var serverError = '';
|
||||||
|
if (this.state.serverError) {
|
||||||
|
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = (
|
||||||
|
<div className='total-count text-center'>
|
||||||
|
<div>{'Total Users'}</div>
|
||||||
|
<div>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
var openChannelCount = (
|
||||||
|
<div className='total-count text-center'>
|
||||||
|
<div>{'Public Groups'}</div>
|
||||||
|
<div>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
var openPrivateCount = (
|
||||||
|
<div className='total-count text-center'>
|
||||||
|
<div>{'Private Groups'}</div>
|
||||||
|
<div>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
var postCount = (
|
||||||
|
<div className='total-count text-center'>
|
||||||
|
<div>{'Total Posts'}</div>
|
||||||
|
<div>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
var postCountsByDay = (
|
||||||
|
<div className='total-count-by-day'>
|
||||||
|
<div>{'Total Posts'}</div>
|
||||||
|
<div>{'Loading...'}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.state.post_counts_day != null) {
|
||||||
|
postCountsByDay = (
|
||||||
|
<div className='total-count-by-day'>
|
||||||
|
<div>{'Total Posts'}</div>
|
||||||
|
<LineChart
|
||||||
|
data={this.state.post_counts_day}
|
||||||
|
width='740'
|
||||||
|
height='225'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var usersWithPostsByDay = (
|
||||||
|
<div className='total-count-by-day'>
|
||||||
|
<div>{'Total Posts'}</div>
|
||||||
|
<div>{'Loading...'}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.state.user_counts_with_posts_day != null) {
|
||||||
|
usersWithPostsByDay = (
|
||||||
|
<div className='total-count-by-day'>
|
||||||
|
<div>{'Active Users With Posts'}</div>
|
||||||
|
<LineChart
|
||||||
|
data={this.state.user_counts_with_posts_day}
|
||||||
|
width='740'
|
||||||
|
height='225'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var recentActiveUser = (
|
||||||
|
<div className='recent-active-users'>
|
||||||
|
<div>{'Recent Active Users'}</div>
|
||||||
|
<div>{'Loading...'}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.state.recent_active_users != null) {
|
||||||
|
recentActiveUser = (
|
||||||
|
<div className='recent-active-users'>
|
||||||
|
<div>{'Recent Active Users'}</div>
|
||||||
|
<table width='90%'>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
this.state.recent_active_users.map((user) => {
|
||||||
|
return (
|
||||||
|
<tr key={user.id}>
|
||||||
|
<td className='recent-active-users-td'>{user.email}</td>
|
||||||
|
<td className='recent-active-users-td'>{Utils.displayDateTime(user.last_activity_at)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newUsers = (
|
||||||
|
<div className='recent-active-users'>
|
||||||
|
<div>{'Newly Created Users'}</div>
|
||||||
|
<div>{'Loading...'}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.state.newly_created_users != null) {
|
||||||
|
newUsers = (
|
||||||
|
<div className='recent-active-users'>
|
||||||
|
<div>{'Newly Created Users'}</div>
|
||||||
|
<table width='90%'>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
this.state.newly_created_users.map((user) => {
|
||||||
|
return (
|
||||||
|
<tr key={user.id}>
|
||||||
|
<td className='recent-active-users-td'>{user.email}</td>
|
||||||
|
<td className='recent-active-users-td'>{Utils.displayDateTime(user.create_at)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='wrapper--fixed'>
|
||||||
|
<h2>{'Statistics for ' + this.props.team.name}</h2>
|
||||||
|
{serverError}
|
||||||
|
{totalCount}
|
||||||
|
{postCount}
|
||||||
|
{openChannelCount}
|
||||||
|
{openPrivateCount}
|
||||||
|
{postCountsByDay}
|
||||||
|
{usersWithPostsByDay}
|
||||||
|
{recentActiveUser}
|
||||||
|
{newUsers}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TeamAnalytics.propTypes = {
|
||||||
|
team: React.PropTypes.object
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
const ChannelStore = require('../stores/channel_store.jsx');
|
const ChannelStore = require('../stores/channel_store.jsx');
|
||||||
const UserStore = require('../stores/user_store.jsx');
|
const UserStore = require('../stores/user_store.jsx');
|
||||||
const PostStore = require('../stores/post_store.jsx');
|
const SearchStore = require('../stores/search_store.jsx');
|
||||||
const NavbarSearchBox = require('./search_bar.jsx');
|
const NavbarSearchBox = require('./search_bar.jsx');
|
||||||
const AsyncClient = require('../utils/async_client.jsx');
|
const AsyncClient = require('../utils/async_client.jsx');
|
||||||
const Client = require('../utils/client.jsx');
|
const Client = require('../utils/client.jsx');
|
||||||
@@ -35,19 +35,19 @@ export default class ChannelHeader extends React.Component {
|
|||||||
memberChannel: ChannelStore.getCurrentMember(),
|
memberChannel: ChannelStore.getCurrentMember(),
|
||||||
memberTeam: UserStore.getCurrentUser(),
|
memberTeam: UserStore.getCurrentUser(),
|
||||||
users: ChannelStore.getCurrentExtraInfo().members,
|
users: ChannelStore.getCurrentExtraInfo().members,
|
||||||
searchVisible: PostStore.getSearchResults() !== null
|
searchVisible: SearchStore.getSearchResults() !== null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ChannelStore.addChangeListener(this.onListenerChange);
|
ChannelStore.addChangeListener(this.onListenerChange);
|
||||||
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
|
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
|
||||||
PostStore.addSearchChangeListener(this.onListenerChange);
|
SearchStore.addSearchChangeListener(this.onListenerChange);
|
||||||
UserStore.addChangeListener(this.onListenerChange);
|
UserStore.addChangeListener(this.onListenerChange);
|
||||||
}
|
}
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
ChannelStore.removeChangeListener(this.onListenerChange);
|
ChannelStore.removeChangeListener(this.onListenerChange);
|
||||||
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
|
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
|
||||||
PostStore.removeSearchChangeListener(this.onListenerChange);
|
SearchStore.removeSearchChangeListener(this.onListenerChange);
|
||||||
UserStore.addChangeListener(this.onListenerChange);
|
UserStore.addChangeListener(this.onListenerChange);
|
||||||
}
|
}
|
||||||
onListenerChange() {
|
onListenerChange() {
|
||||||
|
|||||||
@@ -253,8 +253,14 @@ export default class CreatePost extends React.Component {
|
|||||||
this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
|
this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
|
||||||
}
|
}
|
||||||
handleUploadError(err, clientId) {
|
handleUploadError(err, clientId) {
|
||||||
|
let message = err;
|
||||||
|
if (message && typeof message !== 'string') {
|
||||||
|
// err is an AppError from the server
|
||||||
|
message = err.message;
|
||||||
|
}
|
||||||
|
|
||||||
if (clientId === -1) {
|
if (clientId === -1) {
|
||||||
this.setState({serverError: err});
|
this.setState({serverError: message});
|
||||||
} else {
|
} else {
|
||||||
const draft = PostStore.getDraft(this.state.channelId);
|
const draft = PostStore.getDraft(this.state.channelId);
|
||||||
|
|
||||||
@@ -265,7 +271,7 @@ export default class CreatePost extends React.Component {
|
|||||||
|
|
||||||
PostStore.storeDraft(this.state.channelId, draft);
|
PostStore.storeDraft(this.state.channelId, draft);
|
||||||
|
|
||||||
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err});
|
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: message});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleTextDrop(text) {
|
handleTextDrop(text) {
|
||||||
|
|||||||
@@ -9,12 +9,8 @@ export default class ErrorBar extends React.Component {
|
|||||||
|
|
||||||
this.onErrorChange = this.onErrorChange.bind(this);
|
this.onErrorChange = this.onErrorChange.bind(this);
|
||||||
this.handleClose = this.handleClose.bind(this);
|
this.handleClose = this.handleClose.bind(this);
|
||||||
this.prevTimer = null;
|
|
||||||
|
|
||||||
this.state = ErrorStore.getLastError();
|
this.state = ErrorStore.getLastError();
|
||||||
if (this.isValidError(this.state)) {
|
|
||||||
this.prevTimer = setTimeout(this.handleClose, 10000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isValidError(s) {
|
isValidError(s) {
|
||||||
@@ -56,16 +52,8 @@ export default class ErrorBar extends React.Component {
|
|||||||
onErrorChange() {
|
onErrorChange() {
|
||||||
var newState = ErrorStore.getLastError();
|
var newState = ErrorStore.getLastError();
|
||||||
|
|
||||||
if (this.prevTimer != null) {
|
|
||||||
clearInterval(this.prevTimer);
|
|
||||||
this.prevTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newState) {
|
if (newState) {
|
||||||
this.setState(newState);
|
this.setState(newState);
|
||||||
if (!this.isConnectionError(newState)) {
|
|
||||||
this.prevTimer = setTimeout(this.handleClose, 10000);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({message: null});
|
this.setState({message: null});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
var UserStore = require('../stores/user_store.jsx');
|
var UserStore = require('../stores/user_store.jsx');
|
||||||
var PostStore = require('../stores/post_store.jsx');
|
var SearchStore = require('../stores/search_store.jsx');
|
||||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||||
var Mention = require('./mention.jsx');
|
var Mention = require('./mention.jsx');
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ export default class MentionList extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
PostStore.addMentionDataChangeListener(this.onListenerChange);
|
SearchStore.addMentionDataChangeListener(this.onListenerChange);
|
||||||
|
|
||||||
$('.post-right__scroll').scroll(this.onScroll);
|
$('.post-right__scroll').scroll(this.onScroll);
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ export default class MentionList extends React.Component {
|
|||||||
$(document).click(this.onClick);
|
$(document).click(this.onClick);
|
||||||
}
|
}
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
PostStore.removeMentionDataChangeListener(this.onListenerChange);
|
SearchStore.removeMentionDataChangeListener(this.onListenerChange);
|
||||||
$('body').off('keydown.mentionlist', '#' + this.props.id);
|
$('body').off('keydown.mentionlist', '#' + this.props.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,12 +217,17 @@ export default class MentionList extends React.Component {
|
|||||||
if (this.state.selectedMention === index) {
|
if (this.state.selectedMention === index) {
|
||||||
isFocused = 'mentions-focus';
|
isFocused = 'mentions-focus';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!users[i].secondary_text) {
|
||||||
|
users[i].secondary_text = Utils.getFullName(users[i]);
|
||||||
|
}
|
||||||
|
|
||||||
mentions[index] = (
|
mentions[index] = (
|
||||||
<Mention
|
<Mention
|
||||||
key={'mention_key_' + index}
|
key={'mention_key_' + index}
|
||||||
ref={'mention' + index}
|
ref={'mention' + index}
|
||||||
username={users[i].username}
|
username={users[i].username}
|
||||||
secondary_text={Utils.getFullName(users[i])}
|
secondary_text={users[i].secondary_text}
|
||||||
id={users[i].id}
|
id={users[i].id}
|
||||||
listId={index}
|
listId={index}
|
||||||
isFocused={isFocused}
|
isFocused={isFocused}
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
const AsyncClient = require('../utils/async_client.jsx');
|
|
||||||
const ChannelStore = require('../stores/channel_store.jsx');
|
|
||||||
const Constants = require('../utils/constants.jsx');
|
|
||||||
const Client = require('../utils/client.jsx');
|
|
||||||
const Modal = ReactBootstrap.Modal;
|
const Modal = ReactBootstrap.Modal;
|
||||||
const PreferenceStore = require('../stores/preference_store.jsx');
|
|
||||||
const TeamStore = require('../stores/team_store.jsx');
|
|
||||||
const UserStore = require('../stores/user_store.jsx');
|
const UserStore = require('../stores/user_store.jsx');
|
||||||
const Utils = require('../utils/utils.jsx');
|
const Utils = require('../utils/utils.jsx');
|
||||||
|
|
||||||
@@ -70,53 +64,25 @@ export default class MoreDirectChannels extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleShowDirectChannel(teammate, e) {
|
handleShowDirectChannel(teammate, e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
if (this.state.loadingDMChannel !== -1) {
|
if (this.state.loadingDMChannel !== -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), teammate.id);
|
|
||||||
let channel = ChannelStore.getByName(channelName);
|
|
||||||
|
|
||||||
const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true');
|
|
||||||
AsyncClient.savePreferences([preference]);
|
|
||||||
|
|
||||||
if (channel) {
|
|
||||||
Utils.switchChannel(channel);
|
|
||||||
|
|
||||||
this.handleHide();
|
|
||||||
} else {
|
|
||||||
this.setState({loadingDMChannel: teammate.id});
|
this.setState({loadingDMChannel: teammate.id});
|
||||||
|
Utils.openDirectChannelToUser(
|
||||||
channel = {
|
teammate,
|
||||||
name: channelName,
|
(channel) => {
|
||||||
last_post_at: 0,
|
Utils.switchChannel(channel);
|
||||||
total_msg_count: 0,
|
|
||||||
type: 'D',
|
|
||||||
display_name: teammate.username,
|
|
||||||
teammate_id: teammate.id,
|
|
||||||
status: UserStore.getStatus(teammate.id)
|
|
||||||
};
|
|
||||||
|
|
||||||
Client.createDirectChannel(
|
|
||||||
channel,
|
|
||||||
teammate.id,
|
|
||||||
(data) => {
|
|
||||||
this.setState({loadingDMChannel: -1});
|
this.setState({loadingDMChannel: -1});
|
||||||
|
|
||||||
AsyncClient.getChannel(data.id);
|
|
||||||
Utils.switchChannel(data);
|
|
||||||
|
|
||||||
this.handleHide();
|
this.handleHide();
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.setState({loadingDMChannel: -1});
|
this.setState({loadingDMChannel: -1});
|
||||||
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channelName;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
handleUserChange() {
|
handleUserChange() {
|
||||||
this.setState({users: this.getUsersFromStore()});
|
this.setState({users: this.getUsersFromStore()});
|
||||||
|
|||||||
@@ -3,9 +3,23 @@
|
|||||||
|
|
||||||
var UserStore = require('../stores/user_store.jsx');
|
var UserStore = require('../stores/user_store.jsx');
|
||||||
var Popover = ReactBootstrap.Popover;
|
var Popover = ReactBootstrap.Popover;
|
||||||
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
|
var Overlay = ReactBootstrap.Overlay;
|
||||||
|
const Utils = require('../utils/utils.jsx');
|
||||||
|
|
||||||
|
const ChannelStore = require('../stores/channel_store.jsx');
|
||||||
|
|
||||||
export default class PopoverListMembers extends React.Component {
|
export default class PopoverListMembers extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
|
||||||
|
this.closePopover = this.closePopover.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.setState({showPopover: false});
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const originalLeave = $.fn.popover.Constructor.prototype.leave;
|
const originalLeave = $.fn.popover.Constructor.prototype.leave;
|
||||||
$.fn.popover.Constructor.prototype.leave = function onLeave(obj) {
|
$.fn.popover.Constructor.prototype.leave = function onLeave(obj) {
|
||||||
@@ -27,12 +41,36 @@ export default class PopoverListMembers extends React.Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleShowDirectChannel(teammate, e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
Utils.openDirectChannelToUser(
|
||||||
|
teammate,
|
||||||
|
(channel, channelAlreadyExisted) => {
|
||||||
|
Utils.switchChannel(channel);
|
||||||
|
if (channelAlreadyExisted) {
|
||||||
|
this.closePopover();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.closePopover();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
closePopover() {
|
||||||
|
this.setState({showPopover: false});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let popoverHtml = [];
|
let popoverHtml = [];
|
||||||
let count = 0;
|
let count = 0;
|
||||||
let countText = '-';
|
let countText = '-';
|
||||||
const members = this.props.members;
|
const members = this.props.members;
|
||||||
const teamMembers = UserStore.getProfilesUsernameMap();
|
const teamMembers = UserStore.getProfilesUsernameMap();
|
||||||
|
const currentUserId = UserStore.getCurrentId();
|
||||||
|
const ch = ChannelStore.getCurrent();
|
||||||
|
|
||||||
if (members && teamMembers) {
|
if (members && teamMembers) {
|
||||||
members.sort((a, b) => {
|
members.sort((a, b) => {
|
||||||
@@ -40,14 +78,75 @@ export default class PopoverListMembers extends React.Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
members.forEach((m, i) => {
|
members.forEach((m, i) => {
|
||||||
|
const details = [];
|
||||||
|
|
||||||
|
const fullName = Utils.getFullName(m);
|
||||||
|
if (fullName) {
|
||||||
|
details.push(
|
||||||
|
<span
|
||||||
|
key={`${m.id}__full-name`}
|
||||||
|
className='full-name'
|
||||||
|
>
|
||||||
|
{fullName}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m.nickname) {
|
||||||
|
const separator = fullName ? ' - ' : '';
|
||||||
|
details.push(
|
||||||
|
<span
|
||||||
|
key={`${m.nickname}__nickname`}
|
||||||
|
>
|
||||||
|
{separator + m.nickname}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let button = '';
|
||||||
|
if (currentUserId !== m.id && ch.type !== 'D') {
|
||||||
|
button = (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn btn-primary btn-message'
|
||||||
|
onClick={(e) => this.handleShowDirectChannel(m, e)}
|
||||||
|
>
|
||||||
|
{'Message'}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
|
if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
|
||||||
popoverHtml.push(
|
popoverHtml.push(
|
||||||
<div
|
<div
|
||||||
className='text--nowrap'
|
className='text--nowrap'
|
||||||
key={'popover-member-' + i}
|
key={'popover-member-' + i}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<img
|
||||||
|
className='profile-img pull-left'
|
||||||
|
width='38'
|
||||||
|
height='38'
|
||||||
|
src={`/api/v1/users/${m.id}/image?time=${m.update_at}&${Utils.getSessionIndex()}`}
|
||||||
|
/>
|
||||||
|
<div className='pull-left'>
|
||||||
|
<div
|
||||||
|
className='more-name'
|
||||||
|
>
|
||||||
{m.username}
|
{m.username}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className='more-description'
|
||||||
|
>
|
||||||
|
{details}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='pull-right profile-action'
|
||||||
|
>
|
||||||
|
{button}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
@@ -61,20 +160,12 @@ export default class PopoverListMembers extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OverlayTrigger
|
<div>
|
||||||
trigger='click'
|
<div
|
||||||
placement='bottom'
|
id='member_popover'
|
||||||
rootClose={true}
|
ref='member_popover_target'
|
||||||
overlay={
|
onClick={(e) => this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover})}
|
||||||
<Popover
|
|
||||||
title='Members'
|
|
||||||
id='member-list-popover'
|
|
||||||
>
|
>
|
||||||
{popoverHtml}
|
|
||||||
</Popover>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div id='member_popover'>
|
|
||||||
<div>
|
<div>
|
||||||
{countText}
|
{countText}
|
||||||
<span
|
<span
|
||||||
@@ -83,7 +174,23 @@ export default class PopoverListMembers extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</OverlayTrigger>
|
<Overlay
|
||||||
|
rootClose={true}
|
||||||
|
onHide={this.closePopover}
|
||||||
|
show={this.state.showPopover}
|
||||||
|
target={() => this.state.popoverTarget}
|
||||||
|
placement='bottom'
|
||||||
|
>
|
||||||
|
<Popover
|
||||||
|
title='Members'
|
||||||
|
id='member-list-popover'
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{popoverHtml}
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</Overlay>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
var client = require('../utils/client.jsx');
|
var client = require('../utils/client.jsx');
|
||||||
var AsyncClient = require('../utils/async_client.jsx');
|
var AsyncClient = require('../utils/async_client.jsx');
|
||||||
var PostStore = require('../stores/post_store.jsx');
|
var SearchStore = require('../stores/search_store.jsx');
|
||||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||||
var utils = require('../utils/utils.jsx');
|
var utils = require('../utils/utils.jsx');
|
||||||
var Constants = require('../utils/constants.jsx');
|
var Constants = require('../utils/constants.jsx');
|
||||||
@@ -30,17 +30,17 @@ export default class SearchBar extends React.Component {
|
|||||||
this.state = state;
|
this.state = state;
|
||||||
}
|
}
|
||||||
getSearchTermStateFromStores() {
|
getSearchTermStateFromStores() {
|
||||||
var term = PostStore.getSearchTerm() || '';
|
var term = SearchStore.getSearchTerm() || '';
|
||||||
return {
|
return {
|
||||||
searchTerm: term
|
searchTerm: term
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
PostStore.addSearchTermChangeListener(this.onListenerChange);
|
SearchStore.addSearchTermChangeListener(this.onListenerChange);
|
||||||
this.mounted = true;
|
this.mounted = true;
|
||||||
}
|
}
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
PostStore.removeSearchTermChangeListener(this.onListenerChange);
|
SearchStore.removeSearchTermChangeListener(this.onListenerChange);
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
}
|
}
|
||||||
onListenerChange(doSearch, isMentionSearch) {
|
onListenerChange(doSearch, isMentionSearch) {
|
||||||
@@ -84,8 +84,8 @@ export default class SearchBar extends React.Component {
|
|||||||
}
|
}
|
||||||
handleUserInput(e) {
|
handleUserInput(e) {
|
||||||
var term = e.target.value;
|
var term = e.target.value;
|
||||||
PostStore.storeSearchTerm(term);
|
SearchStore.storeSearchTerm(term);
|
||||||
PostStore.emitSearchTermChange(false);
|
SearchStore.emitSearchTermChange(false);
|
||||||
this.setState({searchTerm: term});
|
this.setState({searchTerm: term});
|
||||||
|
|
||||||
this.refs.autocomplete.handleInputChange(e.target, term);
|
this.refs.autocomplete.handleInputChange(e.target, term);
|
||||||
@@ -150,8 +150,8 @@ export default class SearchBar extends React.Component {
|
|||||||
textbox.value = text;
|
textbox.value = text;
|
||||||
utils.setCaretPosition(textbox, preText.length + word.length);
|
utils.setCaretPosition(textbox, preText.length + word.length);
|
||||||
|
|
||||||
PostStore.storeSearchTerm(text);
|
SearchStore.storeSearchTerm(text);
|
||||||
PostStore.emitSearchTermChange(false);
|
SearchStore.emitSearchTermChange(false);
|
||||||
this.setState({searchTerm: text});
|
this.setState({searchTerm: text});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
var PostStore = require('../stores/post_store.jsx');
|
var SearchStore = require('../stores/search_store.jsx');
|
||||||
var UserStore = require('../stores/user_store.jsx');
|
var UserStore = require('../stores/user_store.jsx');
|
||||||
var SearchBox = require('./search_bar.jsx');
|
var SearchBox = require('./search_bar.jsx');
|
||||||
var Utils = require('../utils/utils.jsx');
|
var Utils = require('../utils/utils.jsx');
|
||||||
@@ -9,7 +9,7 @@ var SearchResultsHeader = require('./search_results_header.jsx');
|
|||||||
var SearchResultsItem = require('./search_results_item.jsx');
|
var SearchResultsItem = require('./search_results_item.jsx');
|
||||||
|
|
||||||
function getStateFromStores() {
|
function getStateFromStores() {
|
||||||
return {results: PostStore.getSearchResults()};
|
return {results: SearchStore.getSearchResults()};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SearchResults extends React.Component {
|
export default class SearchResults extends React.Component {
|
||||||
@@ -30,7 +30,7 @@ export default class SearchResults extends React.Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.mounted = true;
|
this.mounted = true;
|
||||||
PostStore.addSearchChangeListener(this.onChange);
|
SearchStore.addSearchChangeListener(this.onChange);
|
||||||
this.resize();
|
this.resize();
|
||||||
window.addEventListener('resize', this.handleResize);
|
window.addEventListener('resize', this.handleResize);
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export default class SearchResults extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
PostStore.removeSearchChangeListener(this.onChange);
|
SearchStore.removeSearchChangeListener(this.onChange);
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ export default class SearchResults extends React.Component {
|
|||||||
searchForm = <SearchBox />;
|
searchForm = <SearchBox />;
|
||||||
}
|
}
|
||||||
var noResults = (!results || !results.order || !results.order.length);
|
var noResults = (!results || !results.order || !results.order.length);
|
||||||
var searchTerm = PostStore.getSearchTerm();
|
var searchTerm = SearchStore.getSearchTerm();
|
||||||
|
|
||||||
var ctls = null;
|
var ctls = null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
var PostStore = require('../stores/post_store.jsx');
|
var SearchStore = require('../stores/search_store.jsx');
|
||||||
var ChannelStore = require('../stores/channel_store.jsx');
|
var ChannelStore = require('../stores/channel_store.jsx');
|
||||||
var UserStore = require('../stores/user_store.jsx');
|
var UserStore = require('../stores/user_store.jsx');
|
||||||
var UserProfile = require('./user_profile.jsx');
|
var UserProfile = require('./user_profile.jsx');
|
||||||
@@ -32,7 +32,7 @@ export default class SearchResultsItem extends React.Component {
|
|||||||
AppDispatcher.handleServerAction({
|
AppDispatcher.handleServerAction({
|
||||||
type: ActionTypes.RECIEVED_POST_SELECTED,
|
type: ActionTypes.RECIEVED_POST_SELECTED,
|
||||||
post_list: data,
|
post_list: data,
|
||||||
from_search: PostStore.getSearchTerm()
|
from_search: SearchStore.getSearchTerm()
|
||||||
});
|
});
|
||||||
|
|
||||||
AppDispatcher.handleServerAction({
|
AppDispatcher.handleServerAction({
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
|
|
||||||
var SearchResults = require('./search_results.jsx');
|
var SearchResults = require('./search_results.jsx');
|
||||||
var RhsThread = require('./rhs_thread.jsx');
|
var RhsThread = require('./rhs_thread.jsx');
|
||||||
|
var SearchStore = require('../stores/search_store.jsx');
|
||||||
var PostStore = require('../stores/post_store.jsx');
|
var PostStore = require('../stores/post_store.jsx');
|
||||||
var Utils = require('../utils/utils.jsx');
|
var Utils = require('../utils/utils.jsx');
|
||||||
|
|
||||||
function getStateFromStores() {
|
function getStateFromStores() {
|
||||||
return {search_visible: PostStore.getSearchResults() != null, post_right_visible: PostStore.getSelectedPost() != null, is_mention_search: PostStore.getIsMentionSearch()};
|
return {search_visible: SearchStore.getSearchResults() != null, post_right_visible: PostStore.getSelectedPost() != null, is_mention_search: SearchStore.getIsMentionSearch()};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SidebarRight extends React.Component {
|
export default class SidebarRight extends React.Component {
|
||||||
@@ -22,11 +23,11 @@ export default class SidebarRight extends React.Component {
|
|||||||
this.state = getStateFromStores();
|
this.state = getStateFromStores();
|
||||||
}
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
PostStore.addSearchChangeListener(this.onSearchChange);
|
SearchStore.addSearchChangeListener(this.onSearchChange);
|
||||||
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
|
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
|
||||||
}
|
}
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
PostStore.removeSearchChangeListener(this.onSearchChange);
|
SearchStore.removeSearchChangeListener(this.onSearchChange);
|
||||||
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
|
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
|
||||||
}
|
}
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||||
const PostStore = require('../stores/post_store.jsx');
|
const SearchStore = require('../stores/search_store.jsx');
|
||||||
const CommandList = require('./command_list.jsx');
|
const CommandList = require('./command_list.jsx');
|
||||||
const ErrorStore = require('../stores/error_store.jsx');
|
const ErrorStore = require('../stores/error_store.jsx');
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export default class Textbox extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
PostStore.addAddMentionListener(this.onListenerChange);
|
SearchStore.addAddMentionListener(this.onListenerChange);
|
||||||
ErrorStore.addChangeListener(this.onRecievedError);
|
ErrorStore.addChangeListener(this.onRecievedError);
|
||||||
|
|
||||||
this.resize();
|
this.resize();
|
||||||
@@ -62,7 +62,7 @@ export default class Textbox extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
PostStore.removeAddMentionListener(this.onListenerChange);
|
SearchStore.removeAddMentionListener(this.onListenerChange);
|
||||||
ErrorStore.removeChangeListener(this.onRecievedError);
|
ErrorStore.removeChangeListener(this.onRecievedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export default class UserSettingsGeneralTab extends React.Component {
|
|||||||
}.bind(this),
|
}.bind(this),
|
||||||
function imageUploadFailure(err) {
|
function imageUploadFailure(err) {
|
||||||
var state = this.setupInitialState(this.props);
|
var state = this.setupInitialState(this.props);
|
||||||
state.serverError = err;
|
state.serverError = err.message;
|
||||||
this.setState(state);
|
this.setState(state);
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,11 +12,7 @@ var Constants = require('../utils/constants.jsx');
|
|||||||
var ActionTypes = Constants.ActionTypes;
|
var ActionTypes = Constants.ActionTypes;
|
||||||
|
|
||||||
var CHANGE_EVENT = 'change';
|
var CHANGE_EVENT = 'change';
|
||||||
var SEARCH_CHANGE_EVENT = 'search_change';
|
|
||||||
var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
|
|
||||||
var SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
|
var SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
|
||||||
var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
|
|
||||||
var ADD_MENTION_EVENT = 'add_mention';
|
|
||||||
var EDIT_POST_EVENT = 'edit_post';
|
var EDIT_POST_EVENT = 'edit_post';
|
||||||
|
|
||||||
class PostStoreClass extends EventEmitter {
|
class PostStoreClass extends EventEmitter {
|
||||||
@@ -26,21 +22,15 @@ class PostStoreClass extends EventEmitter {
|
|||||||
this.emitChange = this.emitChange.bind(this);
|
this.emitChange = this.emitChange.bind(this);
|
||||||
this.addChangeListener = this.addChangeListener.bind(this);
|
this.addChangeListener = this.addChangeListener.bind(this);
|
||||||
this.removeChangeListener = this.removeChangeListener.bind(this);
|
this.removeChangeListener = this.removeChangeListener.bind(this);
|
||||||
this.emitSearchChange = this.emitSearchChange.bind(this);
|
|
||||||
this.addSearchChangeListener = this.addSearchChangeListener.bind(this);
|
|
||||||
this.removeSearchChangeListener = this.removeSearchChangeListener.bind(this);
|
|
||||||
this.emitSearchTermChange = this.emitSearchTermChange.bind(this);
|
|
||||||
this.addSearchTermChangeListener = this.addSearchTermChangeListener.bind(this);
|
|
||||||
this.removeSearchTermChangeListener = this.removeSearchTermChangeListener.bind(this);
|
|
||||||
this.emitSelectedPostChange = this.emitSelectedPostChange.bind(this);
|
this.emitSelectedPostChange = this.emitSelectedPostChange.bind(this);
|
||||||
this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this);
|
this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this);
|
||||||
this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this);
|
this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this);
|
||||||
this.emitMentionDataChange = this.emitMentionDataChange.bind(this);
|
|
||||||
this.addMentionDataChangeListener = this.addMentionDataChangeListener.bind(this);
|
this.emitEditPost = this.emitEditPost.bind(this);
|
||||||
this.removeMentionDataChangeListener = this.removeMentionDataChangeListener.bind(this);
|
this.addEditPostListener = this.addEditPostListener.bind(this);
|
||||||
this.emitAddMention = this.emitAddMention.bind(this);
|
this.removeEditPostListener = this.removeEditPostListener.bind(this);
|
||||||
this.addAddMentionListener = this.addAddMentionListener.bind(this);
|
|
||||||
this.removeAddMentionListener = this.removeAddMentionListener.bind(this);
|
|
||||||
this.getCurrentPosts = this.getCurrentPosts.bind(this);
|
this.getCurrentPosts = this.getCurrentPosts.bind(this);
|
||||||
this.storePosts = this.storePosts.bind(this);
|
this.storePosts = this.storePosts.bind(this);
|
||||||
this.pStorePosts = this.pStorePosts.bind(this);
|
this.pStorePosts = this.pStorePosts.bind(this);
|
||||||
@@ -59,13 +49,8 @@ class PostStoreClass extends EventEmitter {
|
|||||||
this.pRemovePendingPost = this.pRemovePendingPost.bind(this);
|
this.pRemovePendingPost = this.pRemovePendingPost.bind(this);
|
||||||
this.clearPendingPosts = this.clearPendingPosts.bind(this);
|
this.clearPendingPosts = this.clearPendingPosts.bind(this);
|
||||||
this.updatePendingPost = this.updatePendingPost.bind(this);
|
this.updatePendingPost = this.updatePendingPost.bind(this);
|
||||||
this.storeSearchResults = this.storeSearchResults.bind(this);
|
|
||||||
this.getSearchResults = this.getSearchResults.bind(this);
|
|
||||||
this.getIsMentionSearch = this.getIsMentionSearch.bind(this);
|
|
||||||
this.storeSelectedPost = this.storeSelectedPost.bind(this);
|
this.storeSelectedPost = this.storeSelectedPost.bind(this);
|
||||||
this.getSelectedPost = this.getSelectedPost.bind(this);
|
this.getSelectedPost = this.getSelectedPost.bind(this);
|
||||||
this.storeSearchTerm = this.storeSearchTerm.bind(this);
|
|
||||||
this.getSearchTerm = this.getSearchTerm.bind(this);
|
|
||||||
this.getEmptyDraft = this.getEmptyDraft.bind(this);
|
this.getEmptyDraft = this.getEmptyDraft.bind(this);
|
||||||
this.storeCurrentDraft = this.storeCurrentDraft.bind(this);
|
this.storeCurrentDraft = this.storeCurrentDraft.bind(this);
|
||||||
this.getCurrentDraft = this.getCurrentDraft.bind(this);
|
this.getCurrentDraft = this.getCurrentDraft.bind(this);
|
||||||
@@ -77,9 +62,6 @@ class PostStoreClass extends EventEmitter {
|
|||||||
this.clearCommentDraftUploads = this.clearCommentDraftUploads.bind(this);
|
this.clearCommentDraftUploads = this.clearCommentDraftUploads.bind(this);
|
||||||
this.storeLatestUpdate = this.storeLatestUpdate.bind(this);
|
this.storeLatestUpdate = this.storeLatestUpdate.bind(this);
|
||||||
this.getLatestUpdate = this.getLatestUpdate.bind(this);
|
this.getLatestUpdate = this.getLatestUpdate.bind(this);
|
||||||
this.emitEditPost = this.emitEditPost.bind(this);
|
|
||||||
this.addEditPostListener = this.addEditPostListener.bind(this);
|
|
||||||
this.removeEditPostListener = this.removeEditPostListener.bind(this);
|
|
||||||
this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this);
|
this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this);
|
||||||
}
|
}
|
||||||
emitChange() {
|
emitChange() {
|
||||||
@@ -94,30 +76,6 @@ class PostStoreClass extends EventEmitter {
|
|||||||
this.removeListener(CHANGE_EVENT, callback);
|
this.removeListener(CHANGE_EVENT, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitSearchChange() {
|
|
||||||
this.emit(SEARCH_CHANGE_EVENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
addSearchChangeListener(callback) {
|
|
||||||
this.on(SEARCH_CHANGE_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeSearchChangeListener(callback) {
|
|
||||||
this.removeListener(SEARCH_CHANGE_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitSearchTermChange(doSearch, isMentionSearch) {
|
|
||||||
this.emit(SEARCH_TERM_CHANGE_EVENT, doSearch, isMentionSearch);
|
|
||||||
}
|
|
||||||
|
|
||||||
addSearchTermChangeListener(callback) {
|
|
||||||
this.on(SEARCH_TERM_CHANGE_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeSearchTermChangeListener(callback) {
|
|
||||||
this.removeListener(SEARCH_TERM_CHANGE_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitSelectedPostChange(fromSearch) {
|
emitSelectedPostChange(fromSearch) {
|
||||||
this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch);
|
this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch);
|
||||||
}
|
}
|
||||||
@@ -130,30 +88,6 @@ class PostStoreClass extends EventEmitter {
|
|||||||
this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
|
this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitMentionDataChange(id, mentionText) {
|
|
||||||
this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText);
|
|
||||||
}
|
|
||||||
|
|
||||||
addMentionDataChangeListener(callback) {
|
|
||||||
this.on(MENTION_DATA_CHANGE_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeMentionDataChangeListener(callback) {
|
|
||||||
this.removeListener(MENTION_DATA_CHANGE_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitAddMention(id, username) {
|
|
||||||
this.emit(ADD_MENTION_EVENT, id, username);
|
|
||||||
}
|
|
||||||
|
|
||||||
addAddMentionListener(callback) {
|
|
||||||
this.on(ADD_MENTION_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAddMentionListener(callback) {
|
|
||||||
this.removeListener(ADD_MENTION_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitEditPost(post) {
|
emitEditPost(post) {
|
||||||
this.emit(EDIT_POST_EVENT, post);
|
this.emit(EDIT_POST_EVENT, post);
|
||||||
}
|
}
|
||||||
@@ -181,9 +115,9 @@ class PostStoreClass extends EventEmitter {
|
|||||||
|
|
||||||
var postList = makePostListNonNull(this.getPosts(channelId));
|
var postList = makePostListNonNull(this.getPosts(channelId));
|
||||||
|
|
||||||
for (let pid in newPostList.posts) {
|
for (const pid in newPostList.posts) {
|
||||||
if (newPostList.posts.hasOwnProperty(pid)) {
|
if (newPostList.posts.hasOwnProperty(pid)) {
|
||||||
var np = newPostList.posts[pid];
|
const np = newPostList.posts[pid];
|
||||||
if (np.delete_at === 0) {
|
if (np.delete_at === 0) {
|
||||||
postList.posts[pid] = np;
|
postList.posts[pid] = np;
|
||||||
if (postList.order.indexOf(pid) === -1) {
|
if (postList.order.indexOf(pid) === -1) {
|
||||||
@@ -194,7 +128,7 @@ class PostStoreClass extends EventEmitter {
|
|||||||
delete postList.posts[pid];
|
delete postList.posts[pid];
|
||||||
}
|
}
|
||||||
|
|
||||||
var index = postList.order.indexOf(pid);
|
const index = postList.order.indexOf(pid);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
postList.order.splice(index, 1);
|
postList.order.splice(index, 1);
|
||||||
}
|
}
|
||||||
@@ -202,7 +136,7 @@ class PostStoreClass extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
postList.order.sort(function postSort(a, b) {
|
postList.order.sort((a, b) => {
|
||||||
if (postList.posts[a].create_at > postList.posts[b].create_at) {
|
if (postList.posts[a].create_at > postList.posts[b].create_at) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -306,7 +240,7 @@ class PostStoreClass extends EventEmitter {
|
|||||||
var posts = postList.posts;
|
var posts = postList.posts;
|
||||||
|
|
||||||
// sort failed posts to the bottom
|
// sort failed posts to the bottom
|
||||||
postList.order.sort(function postSort(a, b) {
|
postList.order.sort((a, b) => {
|
||||||
if (posts[a].state === Constants.POST_LOADING && posts[b].state === Constants.POST_FAILED) {
|
if (posts[a].state === Constants.POST_LOADING && posts[b].state === Constants.POST_FAILED) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -371,7 +305,7 @@ class PostStoreClass extends EventEmitter {
|
|||||||
this.pStorePendingPosts(channelId, postList);
|
this.pStorePendingPosts(channelId, postList);
|
||||||
}
|
}
|
||||||
clearPendingPosts() {
|
clearPendingPosts() {
|
||||||
BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', function clearPending(key) {
|
BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', (key) => {
|
||||||
BrowserStore.removeItem(key);
|
BrowserStore.removeItem(key);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -387,28 +321,12 @@ class PostStoreClass extends EventEmitter {
|
|||||||
this.pStorePendingPosts(post.channel_id, postList);
|
this.pStorePendingPosts(post.channel_id, postList);
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
}
|
}
|
||||||
storeSearchResults(results, isMentionSearch) {
|
|
||||||
BrowserStore.setItem('search_results', results);
|
|
||||||
BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch));
|
|
||||||
}
|
|
||||||
getSearchResults() {
|
|
||||||
return BrowserStore.getItem('search_results');
|
|
||||||
}
|
|
||||||
getIsMentionSearch() {
|
|
||||||
return BrowserStore.getItem('is_mention_search');
|
|
||||||
}
|
|
||||||
storeSelectedPost(postList) {
|
storeSelectedPost(postList) {
|
||||||
BrowserStore.setItem('select_post', postList);
|
BrowserStore.setItem('select_post', postList);
|
||||||
}
|
}
|
||||||
getSelectedPost() {
|
getSelectedPost() {
|
||||||
return BrowserStore.getItem('select_post');
|
return BrowserStore.getItem('select_post');
|
||||||
}
|
}
|
||||||
storeSearchTerm(term) {
|
|
||||||
BrowserStore.setItem('search_term', term);
|
|
||||||
}
|
|
||||||
getSearchTerm() {
|
|
||||||
return BrowserStore.getItem('search_term');
|
|
||||||
}
|
|
||||||
getEmptyDraft() {
|
getEmptyDraft() {
|
||||||
return {message: '', uploadsInProgress: [], previews: []};
|
return {message: '', uploadsInProgress: [], previews: []};
|
||||||
}
|
}
|
||||||
@@ -433,7 +351,7 @@ class PostStoreClass extends EventEmitter {
|
|||||||
return BrowserStore.getGlobalItem('comment_draft_' + parentPostId, this.getEmptyDraft());
|
return BrowserStore.getGlobalItem('comment_draft_' + parentPostId, this.getEmptyDraft());
|
||||||
}
|
}
|
||||||
clearDraftUploads() {
|
clearDraftUploads() {
|
||||||
BrowserStore.actionOnGlobalItemsWithPrefix('draft_', function clearUploads(key, value) {
|
BrowserStore.actionOnGlobalItemsWithPrefix('draft_', (key, value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
value.uploadsInProgress = [];
|
value.uploadsInProgress = [];
|
||||||
BrowserStore.setItem(key, value);
|
BrowserStore.setItem(key, value);
|
||||||
@@ -441,7 +359,7 @@ class PostStoreClass extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
clearCommentDraftUploads() {
|
clearCommentDraftUploads() {
|
||||||
BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', function clearUploads(key, value) {
|
BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', (key, value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
value.uploadsInProgress = [];
|
value.uploadsInProgress = [];
|
||||||
BrowserStore.setItem(key, value);
|
BrowserStore.setItem(key, value);
|
||||||
@@ -458,7 +376,7 @@ class PostStoreClass extends EventEmitter {
|
|||||||
|
|
||||||
var PostStore = new PostStoreClass();
|
var PostStore = new PostStoreClass();
|
||||||
|
|
||||||
PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
|
PostStore.dispatchToken = AppDispatcher.register((payload) => {
|
||||||
var action = payload.action;
|
var action = payload.action;
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@@ -469,24 +387,10 @@ PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
|
|||||||
PostStore.pStorePost(action.post);
|
PostStore.pStorePost(action.post);
|
||||||
PostStore.emitChange();
|
PostStore.emitChange();
|
||||||
break;
|
break;
|
||||||
case ActionTypes.RECIEVED_SEARCH:
|
|
||||||
PostStore.storeSearchResults(action.results, action.is_mention_search);
|
|
||||||
PostStore.emitSearchChange();
|
|
||||||
break;
|
|
||||||
case ActionTypes.RECIEVED_SEARCH_TERM:
|
|
||||||
PostStore.storeSearchTerm(action.term);
|
|
||||||
PostStore.emitSearchTermChange(action.do_search, action.is_mention_search);
|
|
||||||
break;
|
|
||||||
case ActionTypes.RECIEVED_POST_SELECTED:
|
case ActionTypes.RECIEVED_POST_SELECTED:
|
||||||
PostStore.storeSelectedPost(action.post_list);
|
PostStore.storeSelectedPost(action.post_list);
|
||||||
PostStore.emitSelectedPostChange(action.from_search);
|
PostStore.emitSelectedPostChange(action.from_search);
|
||||||
break;
|
break;
|
||||||
case ActionTypes.RECIEVED_MENTION_DATA:
|
|
||||||
PostStore.emitMentionDataChange(action.id, action.mention_text);
|
|
||||||
break;
|
|
||||||
case ActionTypes.RECIEVED_ADD_MENTION:
|
|
||||||
PostStore.emitAddMention(action.id, action.username);
|
|
||||||
break;
|
|
||||||
case ActionTypes.RECIEVED_EDIT_POST:
|
case ActionTypes.RECIEVED_EDIT_POST:
|
||||||
PostStore.emitEditPost(action);
|
PostStore.emitEditPost(action);
|
||||||
break;
|
break;
|
||||||
|
|||||||
153
web/react/stores/search_store.jsx
Обычный файл
153
web/react/stores/search_store.jsx
Обычный файл
@@ -0,0 +1,153 @@
|
|||||||
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||||
|
var EventEmitter = require('events').EventEmitter;
|
||||||
|
|
||||||
|
var BrowserStore = require('../stores/browser_store.jsx');
|
||||||
|
|
||||||
|
var Constants = require('../utils/constants.jsx');
|
||||||
|
var ActionTypes = Constants.ActionTypes;
|
||||||
|
|
||||||
|
var CHANGE_EVENT = 'change';
|
||||||
|
var SEARCH_CHANGE_EVENT = 'search_change';
|
||||||
|
var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
|
||||||
|
var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
|
||||||
|
var ADD_MENTION_EVENT = 'add_mention';
|
||||||
|
|
||||||
|
class SearchStoreClass extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.emitChange = this.emitChange.bind(this);
|
||||||
|
this.addChangeListener = this.addChangeListener.bind(this);
|
||||||
|
this.removeChangeListener = this.removeChangeListener.bind(this);
|
||||||
|
|
||||||
|
this.emitSearchChange = this.emitSearchChange.bind(this);
|
||||||
|
this.addSearchChangeListener = this.addSearchChangeListener.bind(this);
|
||||||
|
this.removeSearchChangeListener = this.removeSearchChangeListener.bind(this);
|
||||||
|
|
||||||
|
this.emitSearchTermChange = this.emitSearchTermChange.bind(this);
|
||||||
|
this.addSearchTermChangeListener = this.addSearchTermChangeListener.bind(this);
|
||||||
|
this.removeSearchTermChangeListener = this.removeSearchTermChangeListener.bind(this);
|
||||||
|
|
||||||
|
this.emitMentionDataChange = this.emitMentionDataChange.bind(this);
|
||||||
|
this.addMentionDataChangeListener = this.addMentionDataChangeListener.bind(this);
|
||||||
|
this.removeMentionDataChangeListener = this.removeMentionDataChangeListener.bind(this);
|
||||||
|
|
||||||
|
this.getSearchResults = this.getSearchResults.bind(this);
|
||||||
|
this.getIsMentionSearch = this.getIsMentionSearch.bind(this);
|
||||||
|
|
||||||
|
this.storeSearchTerm = this.storeSearchTerm.bind(this);
|
||||||
|
this.getSearchTerm = this.getSearchTerm.bind(this);
|
||||||
|
|
||||||
|
this.storeSearchResults = this.storeSearchResults.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitChange() {
|
||||||
|
this.emit(CHANGE_EVENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
addChangeListener(callback) {
|
||||||
|
this.on(CHANGE_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeChangeListener(callback) {
|
||||||
|
this.removeListener(CHANGE_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitSearchChange() {
|
||||||
|
this.emit(SEARCH_CHANGE_EVENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
addSearchChangeListener(callback) {
|
||||||
|
this.on(SEARCH_CHANGE_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSearchChangeListener(callback) {
|
||||||
|
this.removeListener(SEARCH_CHANGE_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitSearchTermChange(doSearch, isMentionSearch) {
|
||||||
|
this.emit(SEARCH_TERM_CHANGE_EVENT, doSearch, isMentionSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
addSearchTermChangeListener(callback) {
|
||||||
|
this.on(SEARCH_TERM_CHANGE_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSearchTermChangeListener(callback) {
|
||||||
|
this.removeListener(SEARCH_TERM_CHANGE_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearchResults() {
|
||||||
|
return BrowserStore.getItem('search_results');
|
||||||
|
}
|
||||||
|
|
||||||
|
getIsMentionSearch() {
|
||||||
|
return BrowserStore.getItem('is_mention_search');
|
||||||
|
}
|
||||||
|
|
||||||
|
storeSearchTerm(term) {
|
||||||
|
BrowserStore.setItem('search_term', term);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearchTerm() {
|
||||||
|
return BrowserStore.getItem('search_term');
|
||||||
|
}
|
||||||
|
|
||||||
|
emitMentionDataChange(id, mentionText) {
|
||||||
|
this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText);
|
||||||
|
}
|
||||||
|
|
||||||
|
addMentionDataChangeListener(callback) {
|
||||||
|
this.on(MENTION_DATA_CHANGE_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMentionDataChangeListener(callback) {
|
||||||
|
this.removeListener(MENTION_DATA_CHANGE_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitAddMention(id, username) {
|
||||||
|
this.emit(ADD_MENTION_EVENT, id, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
addAddMentionListener(callback) {
|
||||||
|
this.on(ADD_MENTION_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAddMentionListener(callback) {
|
||||||
|
this.removeListener(ADD_MENTION_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
storeSearchResults(results, isMentionSearch) {
|
||||||
|
BrowserStore.setItem('search_results', results);
|
||||||
|
BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var SearchStore = new SearchStoreClass();
|
||||||
|
|
||||||
|
SearchStore.dispatchToken = AppDispatcher.register((payload) => {
|
||||||
|
var action = payload.action;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.RECIEVED_SEARCH:
|
||||||
|
SearchStore.storeSearchResults(action.results, action.is_mention_search);
|
||||||
|
SearchStore.emitSearchChange();
|
||||||
|
break;
|
||||||
|
case ActionTypes.RECIEVED_SEARCH_TERM:
|
||||||
|
SearchStore.storeSearchTerm(action.term);
|
||||||
|
SearchStore.emitSearchTermChange(action.do_search, action.is_mention_search);
|
||||||
|
break;
|
||||||
|
case ActionTypes.RECIEVED_MENTION_DATA:
|
||||||
|
SearchStore.emitMentionDataChange(action.id, action.mention_text);
|
||||||
|
break;
|
||||||
|
case ActionTypes.RECIEVED_ADD_MENTION:
|
||||||
|
SearchStore.emitAddMention(action.id, action.username);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SearchStore;
|
||||||
@@ -86,7 +86,7 @@ class SocketStoreClass extends EventEmitter {
|
|||||||
|
|
||||||
this.failCount = this.failCount + 1;
|
this.failCount = this.failCount + 1;
|
||||||
|
|
||||||
ErrorStore.storeLastError({connErrorCount: this.failCount, message: 'We cannot reach the Mattermost service. The service may be down or misconfigured. Please contact an administrator to make sure the WebSocket port is configured properly.'});
|
ErrorStore.storeLastError({connErrorCount: this.failCount, message: 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.'});
|
||||||
ErrorStore.emitChange();
|
ErrorStore.emitChange();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ function handleNewPostEvent(msg) {
|
|||||||
if (window.isActive) {
|
if (window.isActive) {
|
||||||
AsyncClient.updateLastViewedAt(true);
|
AsyncClient.updateLastViewedAt(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) {
|
||||||
AsyncClient.getChannel(msg.channel_id);
|
AsyncClient.getChannel(msg.channel_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export function getChannel(id) {
|
|||||||
callTracker['getChannel' + id] = utils.getTimestamp();
|
callTracker['getChannel' + id] = utils.getTimestamp();
|
||||||
|
|
||||||
client.getChannel(id,
|
client.getChannel(id,
|
||||||
function getChannelSuccess(data, textStatus, xhr) {
|
(data, textStatus, xhr) => {
|
||||||
callTracker['getChannel' + id] = 0;
|
callTracker['getChannel' + id] = 0;
|
||||||
|
|
||||||
if (xhr.status === 304 || !data) {
|
if (xhr.status === 304 || !data) {
|
||||||
@@ -145,7 +145,7 @@ export function getChannel(id) {
|
|||||||
member: data.member
|
member: data.member
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function getChannelFailure(err) {
|
(err) => {
|
||||||
callTracker['getChannel' + id] = 0;
|
callTracker['getChannel' + id] = 0;
|
||||||
dispatchError(err, 'getChannel');
|
dispatchError(err, 'getChannel');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function handleError(methodName, xhr, status, err) {
|
|||||||
|
|
||||||
if (oldError && oldError.connErrorCount) {
|
if (oldError && oldError.connErrorCount) {
|
||||||
errorCount += oldError.connErrorCount;
|
errorCount += oldError.connErrorCount;
|
||||||
connectError = 'We cannot reach the Mattermost service. The service may be down or misconfigured. Please contact an administrator to make sure the WebSocket port is configured properly.';
|
connectError = 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.';
|
||||||
}
|
}
|
||||||
|
|
||||||
e = {message: connectError, connErrorCount: errorCount};
|
e = {message: connectError, connErrorCount: errorCount};
|
||||||
@@ -328,6 +328,20 @@ export function getConfig(success, error) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAnalytics(teamId, name, success, error) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/v1/admin/analytics/' + teamId + '/' + name,
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: 'application/json',
|
||||||
|
type: 'GET',
|
||||||
|
success,
|
||||||
|
error: (xhr, status, err) => {
|
||||||
|
var e = handleError('getAnalytics', xhr, status, err);
|
||||||
|
error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function saveConfig(config, success, error) {
|
export function saveConfig(config, success, error) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/v1/admin/save_config',
|
url: '/api/v1/admin/save_config',
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ module.exports = {
|
|||||||
POST_LOADING: 'loading',
|
POST_LOADING: 'loading',
|
||||||
POST_FAILED: 'failed',
|
POST_FAILED: 'failed',
|
||||||
POST_DELETED: 'deleted',
|
POST_DELETED: 'deleted',
|
||||||
|
POST_TYPE_JOIN_LEAVE: 'join_leave',
|
||||||
RESERVED_TEAM_NAMES: [
|
RESERVED_TEAM_NAMES: [
|
||||||
'www',
|
'www',
|
||||||
'web',
|
'web',
|
||||||
|
|||||||
@@ -121,8 +121,11 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
|
|||||||
paragraph(text) {
|
paragraph(text) {
|
||||||
let outText = text;
|
let outText = text;
|
||||||
|
|
||||||
|
// required so markdown does not strip '_' from @user_names
|
||||||
|
outText = TextFormatting.doFormatMentions(text);
|
||||||
|
|
||||||
if (!('emoticons' in this.options) || this.options.emoticon) {
|
if (!('emoticons' in this.options) || this.options.emoticon) {
|
||||||
outText = TextFormatting.doFormatEmoticons(text);
|
outText = TextFormatting.doFormatEmoticons(outText);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.formattingOptions.singleline) {
|
if (this.formattingOptions.singleline) {
|
||||||
@@ -136,7 +139,7 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
|
|||||||
return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`;
|
return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
text(text) {
|
text(txt) {
|
||||||
return TextFormatting.doFormatText(text, this.formattingOptions);
|
return TextFormatting.doFormatText(txt, this.formattingOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ export function doFormatText(text, options) {
|
|||||||
const tokens = new Map();
|
const tokens = new Map();
|
||||||
|
|
||||||
// replace important words and phrases with tokens
|
// replace important words and phrases with tokens
|
||||||
output = autolinkUrls(output, tokens);
|
|
||||||
output = autolinkAtMentions(output, tokens);
|
output = autolinkAtMentions(output, tokens);
|
||||||
|
output = autolinkUrls(output, tokens);
|
||||||
output = autolinkHashtags(output, tokens);
|
output = autolinkHashtags(output, tokens);
|
||||||
|
|
||||||
if (!('emoticons' in options) || options.emoticon) {
|
if (!('emoticons' in options) || options.emoticon) {
|
||||||
@@ -78,6 +78,13 @@ export function doFormatEmoticons(text) {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doFormatMentions(text) {
|
||||||
|
const tokens = new Map();
|
||||||
|
let output = autolinkAtMentions(text, tokens);
|
||||||
|
output = replaceTokens(output, tokens);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
export function sanitizeHtml(text) {
|
export function sanitizeHtml(text) {
|
||||||
let output = text;
|
let output = text;
|
||||||
|
|
||||||
@@ -188,6 +195,7 @@ function autolinkAtMentions(text, tokens) {
|
|||||||
|
|
||||||
let output = text;
|
let output = text;
|
||||||
output = output.replace(/(^|\s)(@([a-z0-9.\-_]*))/gi, replaceAtMentionWithToken);
|
output = output.replace(/(^|\s)(@([a-z0-9.\-_]*))/gi, replaceAtMentionWithToken);
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ var PreferenceStore = require('../stores/preference_store.jsx');
|
|||||||
var TeamStore = require('../stores/team_store.jsx');
|
var TeamStore = require('../stores/team_store.jsx');
|
||||||
var Constants = require('../utils/constants.jsx');
|
var Constants = require('../utils/constants.jsx');
|
||||||
var ActionTypes = Constants.ActionTypes;
|
var ActionTypes = Constants.ActionTypes;
|
||||||
|
var Client = require('./client.jsx');
|
||||||
var AsyncClient = require('./async_client.jsx');
|
var AsyncClient = require('./async_client.jsx');
|
||||||
var client = require('./client.jsx');
|
var client = require('./client.jsx');
|
||||||
var Autolinker = require('autolinker');
|
var Autolinker = require('autolinker');
|
||||||
@@ -1009,3 +1010,44 @@ export function windowWidth() {
|
|||||||
export function windowHeight() {
|
export function windowHeight() {
|
||||||
return $(window).height();
|
return $(window).height();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openDirectChannelToUser(user, successCb, errorCb) {
|
||||||
|
const channelName = this.getDirectChannelName(UserStore.getCurrentId(), user.id);
|
||||||
|
let channel = ChannelStore.getByName(channelName);
|
||||||
|
|
||||||
|
const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, 'true');
|
||||||
|
AsyncClient.savePreferences([preference]);
|
||||||
|
|
||||||
|
if (channel) {
|
||||||
|
if ($.isFunction(successCb)) {
|
||||||
|
successCb(channel, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel = {
|
||||||
|
name: channelName,
|
||||||
|
last_post_at: 0,
|
||||||
|
total_msg_count: 0,
|
||||||
|
type: 'D',
|
||||||
|
display_name: user.username,
|
||||||
|
teammate_id: user.id,
|
||||||
|
status: UserStore.getStatus(user.id)
|
||||||
|
};
|
||||||
|
|
||||||
|
Client.createDirectChannel(
|
||||||
|
channel,
|
||||||
|
user.id,
|
||||||
|
(data) => {
|
||||||
|
AsyncClient.getChannel(data.id);
|
||||||
|
if ($.isFunction(successCb)) {
|
||||||
|
successCb(data, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channelName;
|
||||||
|
if ($.isFunction(errorCb)) {
|
||||||
|
errorCb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,63 @@
|
|||||||
.table {
|
.table {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.total-count {
|
||||||
|
width: 175px;
|
||||||
|
height: 100px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 22px 10px 10px 10px;
|
||||||
|
margin: 10px 10px 10px 10px;
|
||||||
|
background: #fff;
|
||||||
|
float: left;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-count-by-day {
|
||||||
|
width: 760px;
|
||||||
|
height: 275px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 5px 10px 10px 10px;
|
||||||
|
margin: 10px 10px 10px 10px;
|
||||||
|
background: #fff;
|
||||||
|
clear: both;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-active-users {
|
||||||
|
width: 365px;
|
||||||
|
height: 375px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 5px 10px 10px 10px;
|
||||||
|
margin: 10px 10px 10px 10px;
|
||||||
|
background: #fff;
|
||||||
|
float: left;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
> table {
|
||||||
|
margin: 10px 10px 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-active-users-td {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 300;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 3px 3px 3px 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar--left {
|
.sidebar--left {
|
||||||
&.sidebar--collapsable {
|
&.sidebar--collapsable {
|
||||||
background: #333;
|
background: #333;
|
||||||
|
|||||||
@@ -61,3 +61,36 @@
|
|||||||
@include opacity(1);
|
@include opacity(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#member-list-popover {
|
||||||
|
max-width: initial;
|
||||||
|
.popover-content > div {
|
||||||
|
max-height: 350px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
> div {
|
||||||
|
border-bottom: 1px solid rgba(51,51,51,0.1);
|
||||||
|
padding: 8px 8px 8px 15px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: content-box;
|
||||||
|
@include clearfix;
|
||||||
|
.profile-img {
|
||||||
|
border-radius: 50px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.more-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.more-description {
|
||||||
|
@include opacity(0.7);
|
||||||
|
}
|
||||||
|
.profile-action {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
11
web/static/js/Chart.min.js
поставляемый
Обычный файл
11
web/static/js/Chart.min.js
поставляемый
Обычный файл
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
@@ -4,6 +4,7 @@
|
|||||||
<html>
|
<html>
|
||||||
{{template "head" . }}
|
{{template "head" . }}
|
||||||
<body>
|
<body>
|
||||||
|
<script src="/static/js/Chart.min.js"></script>
|
||||||
|
|
||||||
<div id='error_bar'></div>
|
<div id='error_bar'></div>
|
||||||
|
|
||||||
|
|||||||
14
web/web.go
14
web/web.go
@@ -969,20 +969,20 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
|
|
||||||
var props map[string]string
|
var parsedRequest *model.IncomingWebhookRequest
|
||||||
if r.Header.Get("Content-Type") == "application/json" {
|
if r.Header.Get("Content-Type") == "application/json" {
|
||||||
props = model.MapFromJson(r.Body)
|
parsedRequest = model.IncomingWebhookRequestFromJson(r.Body)
|
||||||
} else {
|
} else {
|
||||||
props = model.MapFromJson(strings.NewReader(r.FormValue("payload")))
|
parsedRequest = model.IncomingWebhookRequestFromJson(strings.NewReader(r.FormValue("payload")))
|
||||||
}
|
}
|
||||||
|
|
||||||
text := props["text"]
|
text := parsedRequest.Text
|
||||||
if len(text) == 0 {
|
if len(text) == 0 {
|
||||||
c.Err = model.NewAppError("incomingWebhook", "No text specified", "")
|
c.Err = model.NewAppError("incomingWebhook", "No text specified", "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
channelName := props["channel"]
|
channelName := parsedRequest.ChannelName
|
||||||
|
|
||||||
var hook *model.IncomingWebhook
|
var hook *model.IncomingWebhook
|
||||||
if result := <-hchan; result.Err != nil {
|
if result := <-hchan; result.Err != nil {
|
||||||
@@ -1012,8 +1012,8 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
cchan = api.Srv.Store.Channel().Get(hook.ChannelId)
|
cchan = api.Srv.Store.Channel().Get(hook.ChannelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
overrideUsername := props["username"]
|
overrideUsername := parsedRequest.Username
|
||||||
overrideIconUrl := props["icon_url"]
|
overrideIconUrl := parsedRequest.IconURL
|
||||||
|
|
||||||
if result := <-cchan; result.Err != nil {
|
if result := <-cchan; result.Err != nil {
|
||||||
c.Err = model.NewAppError("incomingWebhook", "Couldn't find the channel", "err="+result.Err.Message)
|
c.Err = model.NewAppError("incomingWebhook", "Couldn't find the channel", "err="+result.Err.Message)
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user