Merge branch 'master' into PLT-340

Этот коммит содержится в:
=Corey Hulen
2015-10-27 11:52:13 -07:00
родитель e22b9f5303 e0f69060fa
Коммит a133f82421
58 изменённых файлов: 1895 добавлений и 345 удалений

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

@@ -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
- 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?
If theres some other way youd like to contribute, please contact us at info@mattermost.com. Wed love to meet you!

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

@@ -26,7 +26,7 @@ func InitAdmin(r *mux.Router) {
sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST")
sr.Handle("/client_props", ApiAppHandler(getClientConfig)).Methods("GET")
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) {
@@ -142,3 +142,63 @@ func testEmail(c *Context, w http.ResponseWriter, r *http.Request) {
m["SUCCESS"] = "true"
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)
}
}
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()
}
}
}

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

@@ -52,6 +52,8 @@ const (
RotatedCCW = 6
RotatedCCWMirrored = 7
RotatedCW = 8
MaxImageSize = 4096 * 2160 // 4k resolution
)
var fileInfoCache *utils.Cache = utils.NewLru(1000)
@@ -125,6 +127,21 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
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
if err := writeFile(buf.Bytes(), path); err != nil {
@@ -132,11 +149,6 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if model.IsFileExtImage(filepath.Ext(files[i].Filename)) {
imageNameList = append(imageNameList, uid+"/"+filename)
imageDataList = append(imageDataList, buf.Bytes())
}
encName := utils.UrlEncode(filename)
fileUrl := "/" + channelId + "/" + c.Session.UserId + "/" + uid + "/" + encName

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

@@ -820,45 +820,23 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
plainSearchParams, hashtagSearchParams := model.ParseSearchParams(terms)
paramsList := model.ParseSearchParams(terms)
channels := []store.StoreChannel{}
var hchan store.StoreChannel
if hashtagSearchParams != nil {
hchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, hashtagSearchParams)
for _, params := range paramsList {
channels = append(channels, Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, params))
}
var pchan store.StoreChannel
if plainSearchParams != nil {
pchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, plainSearchParams)
}
mainList := &model.PostList{}
if hchan != nil {
if result := <-hchan; result.Err != nil {
posts := &model.PostList{}
for _, channel := range channels {
if result := <-channel; result.Err != nil {
c.Err = result.Err
return
} else {
mainList = result.Data.(*model.PostList)
data := result.Data.(*model.PostList)
posts.Extend(data)
}
}
plainList := &model.PostList{}
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()))
w.Write([]byte(posts.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 = 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 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
post3 := &model.Post{ChannelId: channel2.Id, Message: "other message with no return"}
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 {
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 {
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) {
@@ -510,11 +520,12 @@ func TestSearchPostsFromUser(t *testing.T) {
post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"}
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 {
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 {
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 {
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) {

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

@@ -855,6 +855,18 @@ func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
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
img, _, err := image.Decode(file)
if err != nil {

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
##### 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
- 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.
##### 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>.
#### Limitations
#### Known Issues
- The `attachments` payload used in Slack is not yet supported
- Overriding of usernames does not yet apply to notifications
- Cannot supply `icon_emoji` to override the message icon
- Webhook UI fails when connected to deleted channel

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 Обычный файл
Просмотреть файл

@@ -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) {
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
return nil, err

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

@@ -23,6 +23,13 @@ type IncomingWebhook struct {
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 {
b, err := json.Marshal(o)
if err != nil {
@@ -104,3 +111,14 @@ func (o *IncomingWebhook) PreSave() {
func (o *IncomingWebhook) PreUpdate() {
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
}
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 {
id := "0"

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

@@ -34,3 +34,37 @@ func TestPostListJson(t *testing.T) {
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")
}
}

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

@@ -8,10 +8,10 @@ import (
)
type SearchParams struct {
Terms string
IsHashtag bool
InChannel string
FromUser string
Terms string
IsHashtag bool
InChannels []string
FromUsers []string
}
var searchFlags = [...]string{"from", "channel", "in"}
@@ -31,9 +31,9 @@ func splitWords(text string) []string {
return words
}
func parseSearchFlags(input []string) ([]string, map[string]string) {
func parseSearchFlags(input []string) ([]string, [][2]string) {
words := []string{}
flags := make(map[string]string)
flags := [][2]string{}
skipNextWord := false
for i, word := range input {
@@ -52,10 +52,10 @@ func parseSearchFlags(input []string) ([]string, map[string]string) {
// check for case insensitive equality
if strings.EqualFold(flag, searchFlag) {
if value != "" {
flags[searchFlag] = value
flags = append(flags, [2]string{searchFlag, value})
isFlag = true
} else if i < len(input)-1 {
flags[searchFlag] = input[i+1]
flags = append(flags, [2]string{searchFlag, input[i+1]})
skipNextWord = true
isFlag = true
}
@@ -75,56 +75,66 @@ func parseSearchFlags(input []string) ([]string, map[string]string) {
return words, flags
}
func ParseSearchParams(text string) (*SearchParams, *SearchParams) {
func ParseSearchParams(text string) []*SearchParams {
words, flags := parseSearchFlags(splitWords(text))
hashtagTerms := []string{}
plainTerms := []string{}
hashtagTermList := []string{}
plainTermList := []string{}
for _, word := range words {
if validHashtag.MatchString(word) {
hashtagTerms = append(hashtagTerms, word)
hashtagTermList = append(hashtagTermList, word)
} else {
plainTerms = append(plainTerms, word)
plainTermList = append(plainTermList, word)
}
}
inChannel := flags["channel"]
if inChannel == "" {
inChannel = flags["in"]
hashtagTerms := strings.Join(hashtagTermList, " ")
plainTerms := strings.Join(plainTermList, " ")
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 {
plainParams = &SearchParams{
Terms: strings.Join(plainTerms, " "),
IsHashtag: false,
InChannel: inChannel,
FromUser: fromUser,
}
paramsList = append(paramsList, &SearchParams{
Terms: plainTerms,
IsHashtag: false,
InChannels: inChannels,
FromUsers: fromUsers,
})
}
var hashtagParams *SearchParams
if len(hashtagTerms) > 0 {
hashtagParams = &SearchParams{
Terms: strings.Join(hashtagTerms, " "),
IsHashtag: true,
InChannel: inChannel,
FromUser: fromUser,
}
paramsList = append(paramsList, &SearchParams{
Terms: hashtagTerms,
IsHashtag: true,
InChannels: inChannels,
FromUsers: fromUsers,
})
}
// special case for when no terms are specified but we still have a filter
if plainParams == nil && hashtagParams == nil && (inChannel != "" || fromUser != "") {
plainParams = &SearchParams{
Terms: "",
IsHashtag: false,
InChannel: inChannel,
FromUser: fromUser,
}
if len(plainTerms) == 0 && len(hashtagTerms) == 0 {
paramsList = append(paramsList, &SearchParams{
Terms: "",
IsHashtag: true,
InChannels: inChannels,
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" {
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)
}
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)
} 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)
}
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)
} 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)
}
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)
} 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)
}
@@ -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:" {
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)
}
}

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

@@ -40,7 +40,7 @@ func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
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)", "{}") {
// populate NotifyProps from existing NotifyLevel field
@@ -83,7 +83,6 @@ func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
s.RemoveColumnIfExists("ChannelMembers", "NotifyLevel")
}
// END REMOVE AFTER 1.1.0
}
func (s SqlChannelStore) CreateIndexesIfNotExists() {
@@ -829,3 +828,31 @@ func (s SqlChannelStore) GetForExport(teamId string) 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 {
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) {

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

@@ -6,6 +6,7 @@ package store
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/mattermost/platform/model"
@@ -413,10 +414,15 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
go func() {
result := StoreResult{}
queryParams := map[string]interface{}{
"TeamId": teamId,
"UserId": userId,
}
termMap := map[string]bool{}
terms := params.Terms
if terms == "" && params.InChannel == "" && params.FromUser == "" {
if terms == "" && len(params.InChannels) == 0 && len(params.FromUsers) == 0 {
result.Data = []*model.Post{}
storeChannel <- result
return
@@ -468,13 +474,45 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
ORDER BY CreateAt DESC
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)
} else {
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", `
AND UserId IN (
SELECT
@@ -506,13 +544,7 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
}
queryParams := map[string]interface{}{
"TeamId": teamId,
"UserId": userId,
"Terms": terms,
"InChannel": params.InChannel,
"FromUser": params.FromUser,
}
queryParams["Terms"] = terms
_, err := s.GetReplica().Select(&posts, searchQuery, queryParams)
if err != nil {
@@ -571,3 +603,152 @@ func (s SqlPostStore) GetForExport(channelId string) 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")
}
}
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()
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 schemaVersion != "" {
@@ -86,7 +87,11 @@ func NewSqlStore() Store {
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("Attempting to upgrade the database schema version to " + model.CurrentVersion)
} 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.DoesColumnExist("Sessions", "AltId") {
sqlStore.GetMaster().Exec("DROP TABLE IF EXISTS Sessions")
@@ -140,7 +145,7 @@ func NewSqlStore() Store {
sqlStore.webhook.(*SqlWebhookStore).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})
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() {
// REMOVE in 1.2
// REMOVE AFTER 1.2 SHIP see PLT-828
s.RemoveColumnIfExists("Teams", "AllowValet")
}

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

@@ -41,7 +41,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
}
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)", "{}")
}

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

@@ -75,6 +75,7 @@ type ChannelStore interface {
CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel
UpdateLastViewedAt(channelId string, userId string) StoreChannel
IncrementMentionCount(channelId string, userId string) StoreChannel
AnalyticsTypeCount(teamId string, channelType string) StoreChannel
}
type PostStore interface {
@@ -87,6 +88,9 @@ type PostStore interface {
GetEtag(channelId string) StoreChannel
Search(teamId string, userId string, params *model.SearchParams) StoreChannel
GetForExport(channelId string) StoreChannel
AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel
AnalyticsPostCountsByDay(teamId string) StoreChannel
AnalyticsPostCount(teamId string) StoreChannel
}
type UserStore interface {

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

@@ -159,6 +159,13 @@ func LoadConfig(fileName string) {
configureLog(&config.LogSettings)
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
SanitizeOptions = getSanitizeOptions(Cfg)
ClientCfg = getClientConfig(Cfg)

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

@@ -20,7 +20,8 @@
"globals": {
"React": false,
"ReactDOM": false,
"ReactBootstrap": false
"ReactBootstrap": false,
"Chart": false
},
"rules": {
"comma-dangle": [2, "never"],

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

@@ -18,6 +18,7 @@ var SqlSettingsTab = require('./sql_settings.jsx');
var TeamSettingsTab = require('./team_settings.jsx');
var ServiceSettingsTab = require('./service_settings.jsx');
var TeamUsersTab = require('./team_users.jsx');
var TeamAnalyticsTab = require('./team_analytics.jsx');
export default class AdminController extends React.Component {
constructor(props) {
@@ -149,6 +150,10 @@ export default class AdminController extends React.Component {
if (this.state.teams) {
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) {
e.preventDefault();
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) {
@@ -121,6 +121,15 @@ export default class AdminSidebar extends React.Component {
{'- Users'}
</a>
</li>
<li>
<a
href='#'
className={this.isSelected('team_analytics', team.id)}
onClick={this.handleClick.bind(this, 'team_analytics', team.id)}
>
{'- Statistics'}
</a>
</li>
</ul>
</li>
</ul>

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

@@ -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
};

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

@@ -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 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 AsyncClient = require('../utils/async_client.jsx');
const Client = require('../utils/client.jsx');
@@ -35,19 +35,19 @@ export default class ChannelHeader extends React.Component {
memberChannel: ChannelStore.getCurrentMember(),
memberTeam: UserStore.getCurrentUser(),
users: ChannelStore.getCurrentExtraInfo().members,
searchVisible: PostStore.getSearchResults() !== null
searchVisible: SearchStore.getSearchResults() !== null
};
}
componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange);
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
PostStore.addSearchChangeListener(this.onListenerChange);
SearchStore.addSearchChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
PostStore.removeSearchChangeListener(this.onListenerChange);
SearchStore.removeSearchChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
}
onListenerChange() {

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

@@ -253,8 +253,14 @@ export default class CreatePost extends React.Component {
this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
}
handleUploadError(err, clientId) {
let message = err;
if (message && typeof message !== 'string') {
// err is an AppError from the server
message = err.message;
}
if (clientId === -1) {
this.setState({serverError: err});
this.setState({serverError: message});
} else {
const draft = PostStore.getDraft(this.state.channelId);
@@ -265,7 +271,7 @@ export default class CreatePost extends React.Component {
PostStore.storeDraft(this.state.channelId, draft);
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err});
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: message});
}
}
handleTextDrop(text) {

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

@@ -9,12 +9,8 @@ export default class ErrorBar extends React.Component {
this.onErrorChange = this.onErrorChange.bind(this);
this.handleClose = this.handleClose.bind(this);
this.prevTimer = null;
this.state = ErrorStore.getLastError();
if (this.isValidError(this.state)) {
this.prevTimer = setTimeout(this.handleClose, 10000);
}
}
isValidError(s) {
@@ -56,16 +52,8 @@ export default class ErrorBar extends React.Component {
onErrorChange() {
var newState = ErrorStore.getLastError();
if (this.prevTimer != null) {
clearInterval(this.prevTimer);
this.prevTimer = null;
}
if (newState) {
this.setState(newState);
if (!this.isConnectionError(newState)) {
this.prevTimer = setTimeout(this.handleClose, 10000);
}
} else {
this.setState({message: null});
}

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

@@ -2,7 +2,7 @@
// See License.txt for license information.
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 Mention = require('./mention.jsx');
@@ -66,7 +66,7 @@ export default class MentionList extends React.Component {
}
}
componentDidMount() {
PostStore.addMentionDataChangeListener(this.onListenerChange);
SearchStore.addMentionDataChangeListener(this.onListenerChange);
$('.post-right__scroll').scroll(this.onScroll);
@@ -74,7 +74,7 @@ export default class MentionList extends React.Component {
$(document).click(this.onClick);
}
componentWillUnmount() {
PostStore.removeMentionDataChangeListener(this.onListenerChange);
SearchStore.removeMentionDataChangeListener(this.onListenerChange);
$('body').off('keydown.mentionlist', '#' + this.props.id);
}
@@ -217,12 +217,17 @@ export default class MentionList extends React.Component {
if (this.state.selectedMention === index) {
isFocused = 'mentions-focus';
}
if (!users[i].secondary_text) {
users[i].secondary_text = Utils.getFullName(users[i]);
}
mentions[index] = (
<Mention
key={'mention_key_' + index}
ref={'mention' + index}
username={users[i].username}
secondary_text={Utils.getFullName(users[i])}
secondary_text={users[i].secondary_text}
id={users[i].id}
listId={index}
isFocused={isFocused}

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

@@ -1,13 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// 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 PreferenceStore = require('../stores/preference_store.jsx');
const TeamStore = require('../stores/team_store.jsx');
const UserStore = require('../stores/user_store.jsx');
const Utils = require('../utils/utils.jsx');
@@ -70,52 +64,24 @@ export default class MoreDirectChannels extends React.Component {
}
handleShowDirectChannel(teammate, e) {
e.preventDefault();
if (this.state.loadingDMChannel !== -1) {
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});
channel = {
name: channelName,
last_post_at: 0,
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});
AsyncClient.getChannel(data.id);
Utils.switchChannel(data);
this.handleHide();
},
() => {
this.setState({loadingDMChannel: -1});
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channelName;
}
);
}
this.setState({loadingDMChannel: teammate.id});
Utils.openDirectChannelToUser(
teammate,
(channel) => {
Utils.switchChannel(channel);
this.setState({loadingDMChannel: -1});
this.handleHide();
},
() => {
this.setState({loadingDMChannel: -1});
}
);
}
handleUserChange() {

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

@@ -3,9 +3,23 @@
var UserStore = require('../stores/user_store.jsx');
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 {
constructor(props) {
super(props);
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
this.closePopover = this.closePopover.bind(this);
}
componentWillMount() {
this.setState({showPopover: false});
}
componentDidMount() {
const originalLeave = $.fn.popover.Constructor.prototype.leave;
$.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() {
let popoverHtml = [];
let count = 0;
let countText = '-';
const members = this.props.members;
const teamMembers = UserStore.getProfilesUsernameMap();
const currentUserId = UserStore.getCurrentId();
const ch = ChannelStore.getCurrent();
if (members && teamMembers) {
members.sort((a, b) => {
@@ -40,13 +78,74 @@ export default class PopoverListMembers extends React.Component {
});
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) {
popoverHtml.push(
<div
className='text--nowrap'
key={'popover-member-' + i}
>
{m.username}
<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}
</div>
<div
className='more-description'
>
{details}
</div>
</div>
<div
className='pull-right profile-action'
>
{button}
</div>
</div>
);
count++;
@@ -61,29 +160,37 @@ export default class PopoverListMembers extends React.Component {
}
return (
<OverlayTrigger
trigger='click'
placement='bottom'
rootClose={true}
overlay={
<div>
<div
id='member_popover'
ref='member_popover_target'
onClick={(e) => this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover})}
>
<div>
{countText}
<span
className='fa fa-user'
aria-hidden='true'
/>
</div>
</div>
<Overlay
rootClose={true}
onHide={this.closePopover}
show={this.state.showPopover}
target={() => this.state.popoverTarget}
placement='bottom'
>
<Popover
title='Members'
id='member-list-popover'
>
{popoverHtml}
<div>
{popoverHtml}
</div>
</Popover>
}
>
<div id='member_popover'>
<div>
{countText}
<span
className='fa fa-user'
aria-hidden='true'
/>
</div>
</Overlay>
</div>
</OverlayTrigger>
);
}
}

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

@@ -3,7 +3,7 @@
var client = require('../utils/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 utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
@@ -30,17 +30,17 @@ export default class SearchBar extends React.Component {
this.state = state;
}
getSearchTermStateFromStores() {
var term = PostStore.getSearchTerm() || '';
var term = SearchStore.getSearchTerm() || '';
return {
searchTerm: term
};
}
componentDidMount() {
PostStore.addSearchTermChangeListener(this.onListenerChange);
SearchStore.addSearchTermChangeListener(this.onListenerChange);
this.mounted = true;
}
componentWillUnmount() {
PostStore.removeSearchTermChangeListener(this.onListenerChange);
SearchStore.removeSearchTermChangeListener(this.onListenerChange);
this.mounted = false;
}
onListenerChange(doSearch, isMentionSearch) {
@@ -84,8 +84,8 @@ export default class SearchBar extends React.Component {
}
handleUserInput(e) {
var term = e.target.value;
PostStore.storeSearchTerm(term);
PostStore.emitSearchTermChange(false);
SearchStore.storeSearchTerm(term);
SearchStore.emitSearchTermChange(false);
this.setState({searchTerm: term});
this.refs.autocomplete.handleInputChange(e.target, term);
@@ -150,8 +150,8 @@ export default class SearchBar extends React.Component {
textbox.value = text;
utils.setCaretPosition(textbox, preText.length + word.length);
PostStore.storeSearchTerm(text);
PostStore.emitSearchTermChange(false);
SearchStore.storeSearchTerm(text);
SearchStore.emitSearchTermChange(false);
this.setState({searchTerm: text});
}

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// 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 SearchBox = require('./search_bar.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');
function getStateFromStores() {
return {results: PostStore.getSearchResults()};
return {results: SearchStore.getSearchResults()};
}
export default class SearchResults extends React.Component {
@@ -30,7 +30,7 @@ export default class SearchResults extends React.Component {
componentDidMount() {
this.mounted = true;
PostStore.addSearchChangeListener(this.onChange);
SearchStore.addSearchChangeListener(this.onChange);
this.resize();
window.addEventListener('resize', this.handleResize);
}
@@ -40,7 +40,7 @@ export default class SearchResults extends React.Component {
}
componentWillUnmount() {
PostStore.removeSearchChangeListener(this.onChange);
SearchStore.removeSearchChangeListener(this.onChange);
this.mounted = false;
window.removeEventListener('resize', this.handleResize);
}
@@ -78,7 +78,7 @@ export default class SearchResults extends React.Component {
searchForm = <SearchBox />;
}
var noResults = (!results || !results.order || !results.order.length);
var searchTerm = PostStore.getSearchTerm();
var searchTerm = SearchStore.getSearchTerm();
var ctls = null;

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// 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 UserStore = require('../stores/user_store.jsx');
var UserProfile = require('./user_profile.jsx');
@@ -32,7 +32,7 @@ export default class SearchResultsItem extends React.Component {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POST_SELECTED,
post_list: data,
from_search: PostStore.getSearchTerm()
from_search: SearchStore.getSearchTerm()
});
AppDispatcher.handleServerAction({

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

@@ -3,11 +3,12 @@
var SearchResults = require('./search_results.jsx');
var RhsThread = require('./rhs_thread.jsx');
var SearchStore = require('../stores/search_store.jsx');
var PostStore = require('../stores/post_store.jsx');
var Utils = require('../utils/utils.jsx');
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 {
@@ -22,11 +23,11 @@ export default class SidebarRight extends React.Component {
this.state = getStateFromStores();
}
componentDidMount() {
PostStore.addSearchChangeListener(this.onSearchChange);
SearchStore.addSearchChangeListener(this.onSearchChange);
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
}
componentWillUnmount() {
PostStore.removeSearchChangeListener(this.onSearchChange);
SearchStore.removeSearchChangeListener(this.onSearchChange);
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
}
componentDidUpdate() {

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

@@ -2,7 +2,7 @@
// See License.txt for license information.
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 ErrorStore = require('../stores/error_store.jsx');
@@ -54,7 +54,7 @@ export default class Textbox extends React.Component {
}
componentDidMount() {
PostStore.addAddMentionListener(this.onListenerChange);
SearchStore.addAddMentionListener(this.onListenerChange);
ErrorStore.addChangeListener(this.onRecievedError);
this.resize();
@@ -62,7 +62,7 @@ export default class Textbox extends React.Component {
}
componentWillUnmount() {
PostStore.removeAddMentionListener(this.onListenerChange);
SearchStore.removeAddMentionListener(this.onListenerChange);
ErrorStore.removeChangeListener(this.onRecievedError);
}

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

@@ -171,7 +171,7 @@ export default class UserSettingsGeneralTab extends React.Component {
}.bind(this),
function imageUploadFailure(err) {
var state = this.setupInitialState(this.props);
state.serverError = err;
state.serverError = err.message;
this.setState(state);
}.bind(this)
);

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

@@ -12,11 +12,7 @@ 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 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';
class PostStoreClass extends EventEmitter {
@@ -26,21 +22,15 @@ class PostStoreClass extends EventEmitter {
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.emitSelectedPostChange = this.emitSelectedPostChange.bind(this);
this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this);
this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this);
this.emitMentionDataChange = this.emitMentionDataChange.bind(this);
this.addMentionDataChangeListener = this.addMentionDataChangeListener.bind(this);
this.removeMentionDataChangeListener = this.removeMentionDataChangeListener.bind(this);
this.emitAddMention = this.emitAddMention.bind(this);
this.addAddMentionListener = this.addAddMentionListener.bind(this);
this.removeAddMentionListener = this.removeAddMentionListener.bind(this);
this.emitEditPost = this.emitEditPost.bind(this);
this.addEditPostListener = this.addEditPostListener.bind(this);
this.removeEditPostListener = this.removeEditPostListener.bind(this);
this.getCurrentPosts = this.getCurrentPosts.bind(this);
this.storePosts = this.storePosts.bind(this);
this.pStorePosts = this.pStorePosts.bind(this);
@@ -59,13 +49,8 @@ class PostStoreClass extends EventEmitter {
this.pRemovePendingPost = this.pRemovePendingPost.bind(this);
this.clearPendingPosts = this.clearPendingPosts.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.getSelectedPost = this.getSelectedPost.bind(this);
this.storeSearchTerm = this.storeSearchTerm.bind(this);
this.getSearchTerm = this.getSearchTerm.bind(this);
this.getEmptyDraft = this.getEmptyDraft.bind(this);
this.storeCurrentDraft = this.storeCurrentDraft.bind(this);
this.getCurrentDraft = this.getCurrentDraft.bind(this);
@@ -77,9 +62,6 @@ class PostStoreClass extends EventEmitter {
this.clearCommentDraftUploads = this.clearCommentDraftUploads.bind(this);
this.storeLatestUpdate = this.storeLatestUpdate.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);
}
emitChange() {
@@ -94,30 +76,6 @@ class PostStoreClass extends EventEmitter {
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) {
this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch);
}
@@ -130,30 +88,6 @@ class PostStoreClass extends EventEmitter {
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) {
this.emit(EDIT_POST_EVENT, post);
}
@@ -181,9 +115,9 @@ class PostStoreClass extends EventEmitter {
var postList = makePostListNonNull(this.getPosts(channelId));
for (let pid in newPostList.posts) {
for (const pid in newPostList.posts) {
if (newPostList.posts.hasOwnProperty(pid)) {
var np = newPostList.posts[pid];
const np = newPostList.posts[pid];
if (np.delete_at === 0) {
postList.posts[pid] = np;
if (postList.order.indexOf(pid) === -1) {
@@ -194,7 +128,7 @@ class PostStoreClass extends EventEmitter {
delete postList.posts[pid];
}
var index = postList.order.indexOf(pid);
const index = postList.order.indexOf(pid);
if (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) {
return -1;
}
@@ -306,7 +240,7 @@ class PostStoreClass extends EventEmitter {
var posts = postList.posts;
// 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) {
return 1;
}
@@ -371,7 +305,7 @@ class PostStoreClass extends EventEmitter {
this.pStorePendingPosts(channelId, postList);
}
clearPendingPosts() {
BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', function clearPending(key) {
BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', (key) => {
BrowserStore.removeItem(key);
});
}
@@ -387,28 +321,12 @@ class PostStoreClass extends EventEmitter {
this.pStorePendingPosts(post.channel_id, postList);
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) {
BrowserStore.setItem('select_post', postList);
}
getSelectedPost() {
return BrowserStore.getItem('select_post');
}
storeSearchTerm(term) {
BrowserStore.setItem('search_term', term);
}
getSearchTerm() {
return BrowserStore.getItem('search_term');
}
getEmptyDraft() {
return {message: '', uploadsInProgress: [], previews: []};
}
@@ -433,7 +351,7 @@ class PostStoreClass extends EventEmitter {
return BrowserStore.getGlobalItem('comment_draft_' + parentPostId, this.getEmptyDraft());
}
clearDraftUploads() {
BrowserStore.actionOnGlobalItemsWithPrefix('draft_', function clearUploads(key, value) {
BrowserStore.actionOnGlobalItemsWithPrefix('draft_', (key, value) => {
if (value) {
value.uploadsInProgress = [];
BrowserStore.setItem(key, value);
@@ -441,7 +359,7 @@ class PostStoreClass extends EventEmitter {
});
}
clearCommentDraftUploads() {
BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', function clearUploads(key, value) {
BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', (key, value) => {
if (value) {
value.uploadsInProgress = [];
BrowserStore.setItem(key, value);
@@ -458,7 +376,7 @@ class PostStoreClass extends EventEmitter {
var PostStore = new PostStoreClass();
PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
PostStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
@@ -469,24 +387,10 @@ PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
PostStore.pStorePost(action.post);
PostStore.emitChange();
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:
PostStore.storeSelectedPost(action.post_list);
PostStore.emitSelectedPostChange(action.from_search);
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:
PostStore.emitEditPost(action);
break;

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;
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();
};
@@ -160,7 +160,7 @@ function handleNewPostEvent(msg) {
if (window.isActive) {
AsyncClient.updateLastViewedAt(true);
}
} else {
} else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) {
AsyncClient.getChannel(msg.channel_id);
}

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

@@ -132,7 +132,7 @@ export function getChannel(id) {
callTracker['getChannel' + id] = utils.getTimestamp();
client.getChannel(id,
function getChannelSuccess(data, textStatus, xhr) {
(data, textStatus, xhr) => {
callTracker['getChannel' + id] = 0;
if (xhr.status === 304 || !data) {
@@ -145,7 +145,7 @@ export function getChannel(id) {
member: data.member
});
},
function getChannelFailure(err) {
(err) => {
callTracker['getChannel' + id] = 0;
dispatchError(err, 'getChannel');
}

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

@@ -34,7 +34,7 @@ function handleError(methodName, xhr, status, err) {
if (oldError && 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};
@@ -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) {
$.ajax({
url: '/api/v1/admin/save_config',

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

@@ -98,6 +98,7 @@ module.exports = {
POST_LOADING: 'loading',
POST_FAILED: 'failed',
POST_DELETED: 'deleted',
POST_TYPE_JOIN_LEAVE: 'join_leave',
RESERVED_TEAM_NAMES: [
'www',
'web',

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

@@ -121,8 +121,11 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
paragraph(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) {
outText = TextFormatting.doFormatEmoticons(text);
outText = TextFormatting.doFormatEmoticons(outText);
}
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>`;
}
text(text) {
return TextFormatting.doFormatText(text, this.formattingOptions);
text(txt) {
return TextFormatting.doFormatText(txt, this.formattingOptions);
}
}

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

@@ -47,8 +47,8 @@ export function doFormatText(text, options) {
const tokens = new Map();
// replace important words and phrases with tokens
output = autolinkUrls(output, tokens);
output = autolinkAtMentions(output, tokens);
output = autolinkUrls(output, tokens);
output = autolinkHashtags(output, tokens);
if (!('emoticons' in options) || options.emoticon) {
@@ -78,6 +78,13 @@ export function doFormatEmoticons(text) {
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) {
let output = text;
@@ -188,6 +195,7 @@ function autolinkAtMentions(text, tokens) {
let output = text;
output = output.replace(/(^|\s)(@([a-z0-9.\-_]*))/gi, replaceAtMentionWithToken);
return output;
}

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

@@ -8,6 +8,7 @@ var PreferenceStore = require('../stores/preference_store.jsx');
var TeamStore = require('../stores/team_store.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var Client = require('./client.jsx');
var AsyncClient = require('./async_client.jsx');
var client = require('./client.jsx');
var Autolinker = require('autolinker');
@@ -1009,3 +1010,44 @@ export function windowWidth() {
export function windowHeight() {
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 {
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--collapsable {
background: #333;

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

@@ -61,3 +61,36 @@
@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 поставляемый Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@@ -4,6 +4,7 @@
<html>
{{template "head" . }}
<body>
<script src="/static/js/Chart.min.js"></script>
<div id='error_bar'></div>

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

@@ -969,20 +969,20 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
r.ParseForm()
var props map[string]string
var parsedRequest *model.IncomingWebhookRequest
if r.Header.Get("Content-Type") == "application/json" {
props = model.MapFromJson(r.Body)
parsedRequest = model.IncomingWebhookRequestFromJson(r.Body)
} 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 {
c.Err = model.NewAppError("incomingWebhook", "No text specified", "")
return
}
channelName := props["channel"]
channelName := parsedRequest.ChannelName
var hook *model.IncomingWebhook
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)
}
overrideUsername := props["username"]
overrideIconUrl := props["icon_url"]
overrideUsername := parsedRequest.Username
overrideIconUrl := parsedRequest.IconURL
if result := <-cchan; result.Err != nil {
c.Err = model.NewAppError("incomingWebhook", "Couldn't find the channel", "err="+result.Err.Message)