diff options
-rw-r--r-- | api/file.go | 10 | ||||
-rw-r--r-- | api/post.go | 20 | ||||
-rw-r--r-- | api/post_test.go | 122 | ||||
-rw-r--r-- | api/user.go | 20 | ||||
-rw-r--r-- | doc/integrations/services/Gitlab-Integration-Service-for-Mattermost.md | 9 | ||||
-rw-r--r-- | doc/integrations/webhooks/Incoming-Webhooks.md | 18 | ||||
-rw-r--r-- | mattermost.go | 4 | ||||
-rw-r--r-- | model/search_params.go | 130 | ||||
-rw-r--r-- | model/search_params_test.go | 70 | ||||
-rw-r--r-- | model/utils.go | 4 | ||||
-rw-r--r-- | store/sql_post_store.go | 128 | ||||
-rw-r--r-- | store/sql_post_store_test.go | 22 | ||||
-rw-r--r-- | store/store.go | 2 | ||||
-rw-r--r-- | utils/apns.go | 2 | ||||
-rw-r--r-- | web/react/components/create_comment.jsx | 15 | ||||
-rw-r--r-- | web/react/components/create_post.jsx | 23 | ||||
-rw-r--r-- | web/react/components/rhs_thread.jsx | 27 | ||||
-rw-r--r-- | web/react/components/search_results.jsx | 27 | ||||
-rw-r--r-- | web/react/components/sidebar.jsx | 29 | ||||
-rw-r--r-- | web/react/utils/utils.jsx | 8 | ||||
-rw-r--r-- | web/web.go | 4 |
21 files changed, 558 insertions, 136 deletions
diff --git a/api/file.go b/api/file.go index 429347596..142ef7ac7 100644 --- a/api/file.go +++ b/api/file.go @@ -146,12 +146,12 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { resStruct.ClientIds = append(resStruct.ClientIds, clientId) } - fireAndForgetHandleImages(imageNameList, imageDataList, c.Session.TeamId, channelId, c.Session.UserId) + handleImagesAndForget(imageNameList, imageDataList, c.Session.TeamId, channelId, c.Session.UserId) w.Write([]byte(resStruct.ToJson())) } -func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, channelId, userId string) { +func handleImagesAndForget(filenames []string, fileData [][]byte, teamId, channelId, userId string) { go func() { dest := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" @@ -311,7 +311,7 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) { } else { fileData := make(chan []byte) - asyncGetFile(path, fileData) + getFileAndForget(path, fileData) f := <-fileData @@ -378,7 +378,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) { } fileData := make(chan []byte) - asyncGetFile(path, fileData) + getFileAndForget(path, fileData) if len(hash) > 0 && len(data) > 0 && len(teamId) == 26 { if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt)) { @@ -423,7 +423,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(f) } -func asyncGetFile(path string, fileData chan []byte) { +func getFileAndForget(path string, fileData chan []byte) { go func() { data, getErr := readFile(path) if getErr != nil { diff --git a/api/post.go b/api/post.go index 73a63cb72..c5bcd4f5a 100644 --- a/api/post.go +++ b/api/post.go @@ -201,7 +201,7 @@ func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks boo channel = result.Data.(*model.Channel) } - fireAndForgetNotifications(c, post, team, channel) + sendNotificationsAndForget(c, post, team, channel) var user *model.User if result := <-uchan; result.Err != nil { @@ -299,7 +299,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team } -func fireAndForgetNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel) { +func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team, channel *model.Channel) { go func() { // Get a list of user names (to be used as keywords) and ids for the given team @@ -434,7 +434,7 @@ func fireAndForgetNotifications(c *Context, post *model.Post, team *model.Team, } for id := range toEmailMap { - fireAndForgetMentionUpdate(post.ChannelId, id) + updateMentionCountAndForget(post.ChannelId, id) } } @@ -530,7 +530,7 @@ func fireAndForgetNotifications(c *Context, post *model.Post, team *model.Team, alreadySeen[session.DeviceId] = session.DeviceId - utils.FireAndForgetSendAppleNotify(session.DeviceId, subjectPage.Render(), 1) + utils.SendAppleNotifyAndForget(session.DeviceId, subjectPage.Render(), 1) } } } @@ -562,7 +562,7 @@ func fireAndForgetNotifications(c *Context, post *model.Post, team *model.Team, }() } -func fireAndForgetMentionUpdate(channelId, userId string) { +func updateMentionCountAndForget(channelId, userId string) { go func() { if result := <-Srv.Store.Channel().IncrementMentionCount(channelId, userId); result.Err != nil { l4g.Error("Failed to update mention count for user_id=%v on channel_id=%v err=%v", userId, channelId, result.Err) @@ -820,16 +820,16 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) { return } - hashtagTerms, plainTerms := model.ParseHashtags(terms) + plainSearchParams, hashtagSearchParams := model.ParseSearchParams(terms) var hchan store.StoreChannel - if len(hashtagTerms) != 0 { - hchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, hashtagTerms, true) + if hashtagSearchParams != nil { + hchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, hashtagSearchParams) } var pchan store.StoreChannel - if len(plainTerms) != 0 { - pchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, terms, false) + if plainSearchParams != nil { + pchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, plainSearchParams) } mainList := &model.PostList{} diff --git a/api/post_test.go b/api/post_test.go index 1971b6114..ac9d5668b 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -406,6 +406,128 @@ func TestSearchHashtagPosts(t *testing.T) { } } +func TestSearchPostsInChannel(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) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + post1 := &model.Post{ChannelId: channel1.Id, Message: "sgtitlereview with space"} + post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + + 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) + + 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) + + 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)) + } + + if result := Client.Must(Client.SearchPosts("in:")).Data.(*model.PostList); len(result.Order) != 0 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("channel:" + channel1.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("in: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("channel: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("ChAnNeL: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("sgtitlereview")).Data.(*model.PostList); len(result.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("sgtitlereview in:")).Data.(*model.PostList); len(result.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("sgtitlereview channel:" + channel1.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("sgtitlereview in: " + 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("sgtitlereview channel: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } +} + +func TestSearchPostsFromUser(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) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + 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) + + post1 := &model.Post{ChannelId: channel1.Id, Message: "sgtitlereview with space"} + post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + + Client.LoginByEmail(team.Name, user2.Email, "pwd") + Client.Must(Client.JoinChannel(channel1.Id)) + Client.Must(Client.JoinChannel(channel2.Id)) + + post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"} + post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post) + + 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)) + } + + if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " sgtitlereview")).Data.(*model.PostList); len(result.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + 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)) + } +} + func TestGetPostsCache(t *testing.T) { Setup() diff --git a/api/user.go b/api/user.go index ac33e81a1..0c7278711 100644 --- a/api/user.go +++ b/api/user.go @@ -198,7 +198,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err) } - fireAndForgetWelcomeEmail(ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), user.EmailVerified) + sendWelcomeEmailAndForget(ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), user.EmailVerified) addDirectChannelsAndForget(ruser) @@ -219,7 +219,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { } } -func fireAndForgetWelcomeEmail(userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) { +func sendWelcomeEmailAndForget(userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) { go func() { subjectPage := NewServerTemplatePage("welcome_subject") @@ -278,7 +278,7 @@ func addDirectChannelsAndForget(user *model.User) { }() } -func FireAndForgetVerifyEmail(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { +func SendVerifyEmailAndForget(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) @@ -931,10 +931,10 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) { l4g.Error(tresult.Err.Message) } else { team := tresult.Data.(*model.Team) - fireAndForgetEmailChangeEmail(rusers[1].Email, rusers[0].Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL()) + sendEmailChangeEmailAndForget(rusers[1].Email, rusers[0].Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL()) if utils.Cfg.EmailSettings.RequireEmailVerification { - FireAndForgetEmailChangeVerifyEmail(rusers[0].Id, rusers[0].Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + SendEmailChangeVerifyEmailAndForget(rusers[0].Id, rusers[0].Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) } } } @@ -1014,7 +1014,7 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) { l4g.Error(tresult.Err.Message) } else { team := tresult.Data.(*model.Team) - fireAndForgetPasswordChangeEmail(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using the settings menu") + sendPasswordChangeEmailAndForget(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using the settings menu") } data := make(map[string]string) @@ -1351,13 +1351,13 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAuditWithUserId(userId, "success") } - fireAndForgetPasswordChangeEmail(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using a reset password link") + sendPasswordChangeEmailAndForget(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using a reset password link") props["new_password"] = "" w.Write([]byte(model.MapToJson(props))) } -func fireAndForgetPasswordChangeEmail(email, teamDisplayName, teamURL, siteURL, method string) { +func sendPasswordChangeEmailAndForget(email, teamDisplayName, teamURL, siteURL, method string) { go func() { subjectPage := NewServerTemplatePage("password_change_subject") @@ -1376,7 +1376,7 @@ func fireAndForgetPasswordChangeEmail(email, teamDisplayName, teamURL, siteURL, }() } -func fireAndForgetEmailChangeEmail(oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) { +func sendEmailChangeEmailAndForget(oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) { go func() { subjectPage := NewServerTemplatePage("email_change_subject") @@ -1395,7 +1395,7 @@ func fireAndForgetEmailChangeEmail(oldEmail, newEmail, teamDisplayName, teamURL, }() } -func FireAndForgetEmailChangeVerifyEmail(userId, newUserEmail, teamName, teamDisplayName, siteURL, teamURL string) { +func SendEmailChangeVerifyEmailAndForget(userId, newUserEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail) diff --git a/doc/integrations/services/Gitlab-Integration-Service-for-Mattermost.md b/doc/integrations/services/Gitlab-Integration-Service-for-Mattermost.md new file mode 100644 index 000000000..2ce56bb72 --- /dev/null +++ b/doc/integrations/services/Gitlab-Integration-Service-for-Mattermost.md @@ -0,0 +1,9 @@ +# [GitLab Integration Service for Mattermost](https://github.com/mattermost/mattermost-integration-gitlab) + +This [open source integration service](https://github.com/mattermost/mattermost-integration-gitlab) let you configure real-time notifications on GitLab issues, merge requests and comments to be delivered to selected Mattermost channels. + +The service can be installed on any Linux-based web server and instructions for **Heroku** and **Ubuntu 14.04** are included. Please see [Mattermost incoming webhooks documentation](https://github.com/mattermost/platform/blob/master/doc/integrations/webhooks/Incoming-Webhooks.md) for details on formatting options within the service. + +The Mattermost community is invited to fork, extend and repurpose this service for other applications. If you'd like your integration featured on http://mattermost.org/webhooks, please mail info@mattermost.org or tweet to us at @mattermosthq. + +![webhooks](https://gitlab.com/gitlab-org/omnibus-gitlab/uploads/677b0aa055693c4dcabad0ee580c61b8/730_gitlab_feature_request.png) diff --git a/doc/integrations/webhooks/Incoming-Webhooks.md b/doc/integrations/webhooks/Incoming-Webhooks.md index c6323a24a..be17d6a8e 100644 --- a/doc/integrations/webhooks/Incoming-Webhooks.md +++ b/doc/integrations/webhooks/Incoming-Webhooks.md @@ -13,26 +13,26 @@ Suppose you wanted to create a notification of the status of a daily build, with ``` payload={"text": " -*** +--- ##### Build Break - Project X - December 12, 2015 - 15:32 GMT +0 | Component | Tests Run | Tests Failed | |:-----------|:------------|:-----------------------------------------------| | Server | 948 | :white_check_mark: 0 | | Web Client | 123 | :warning: [2 (see details)](http://linktologs) | | iOS Client | 78 | :warning: [3 (see details)](http://linktologs) | -*** +--- "} ``` Which would render in a Mattermost message as follows: -*** +--- ##### Build Break - Project X - December 12, 2015 - 15:32 GMT +0 -| Component | Tests Run | Tests Failed | -|:------------ |:---------------|:-----| -| Server | 948 | :white_check_mark: 0 | -| Web Client | 123 | :warning: [2 (see details)](http://linktologs) | -| iOS Client | 78 | :warning: [3 (see details)](http://linktologs) | -*** +| Component | Tests Run | Tests Failed | +|:-----------|:------------|:-----------------------------------------------| +| Server | 948 | :white_check_mark: 0 | +| Web Client | 123 | :warning: [2 (see details)](http://linktologs) | +| iOS Client | 78 | :warning: [3 (see details)](http://linktologs) | +--- ### Enabling Incoming Webhooks Incoming webhooks should be enabled on your Mattermost instance by default, but if they are not you'll need to get your system administrator to enable them. If you are the system administrator you can enable them by doing the following: diff --git a/mattermost.go b/mattermost.go index 48487ee73..e1ae58904 100644 --- a/mattermost.go +++ b/mattermost.go @@ -66,7 +66,7 @@ func main() { manualtesting.InitManualTesting() } - securityAndDiagnosticsJob() + runSecurityAndDiagnosticsJobAndForget() // wait for kill signal before attempting to gracefully shutdown // the running service @@ -78,7 +78,7 @@ func main() { } } -func securityAndDiagnosticsJob() { +func runSecurityAndDiagnosticsJobAndForget() { go func() { for { if *utils.Cfg.ServiceSettings.EnableSecurityFixAlert { diff --git a/model/search_params.go b/model/search_params.go new file mode 100644 index 000000000..7eeeed10f --- /dev/null +++ b/model/search_params.go @@ -0,0 +1,130 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" +) + +type SearchParams struct { + Terms string + IsHashtag bool + InChannel string + FromUser string +} + +var searchFlags = [...]string{"from", "channel", "in"} + +func splitWords(text string) []string { + words := []string{} + + for _, word := range strings.Fields(text) { + word = puncStart.ReplaceAllString(word, "") + word = puncEnd.ReplaceAllString(word, "") + + if len(word) != 0 { + words = append(words, word) + } + } + + return words +} + +func parseSearchFlags(input []string) ([]string, map[string]string) { + words := []string{} + flags := make(map[string]string) + + skipNextWord := false + for i, word := range input { + if skipNextWord { + skipNextWord = false + continue + } + + isFlag := false + + if colon := strings.Index(word, ":"); colon != -1 { + flag := word[:colon] + value := word[colon+1:] + + for _, searchFlag := range searchFlags { + // check for case insensitive equality + if strings.EqualFold(flag, searchFlag) { + if value != "" { + flags[searchFlag] = value + isFlag = true + } else if i < len(input)-1 { + flags[searchFlag] = input[i+1] + skipNextWord = true + isFlag = true + } + + if isFlag { + break + } + } + } + } + + if !isFlag { + words = append(words, word) + } + } + + return words, flags +} + +func ParseSearchParams(text string) (*SearchParams, *SearchParams) { + words, flags := parseSearchFlags(splitWords(text)) + + hashtagTerms := []string{} + plainTerms := []string{} + + for _, word := range words { + if validHashtag.MatchString(word) { + hashtagTerms = append(hashtagTerms, word) + } else { + plainTerms = append(plainTerms, word) + } + } + + inChannel := flags["channel"] + if inChannel == "" { + inChannel = flags["in"] + } + + fromUser := flags["from"] + + var plainParams *SearchParams + if len(plainTerms) > 0 { + plainParams = &SearchParams{ + Terms: strings.Join(plainTerms, " "), + IsHashtag: false, + InChannel: inChannel, + FromUser: fromUser, + } + } + + var hashtagParams *SearchParams + if len(hashtagTerms) > 0 { + hashtagParams = &SearchParams{ + Terms: strings.Join(hashtagTerms, " "), + IsHashtag: true, + InChannel: inChannel, + FromUser: fromUser, + } + } + + // 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, + } + } + + return plainParams, hashtagParams +} diff --git a/model/search_params_test.go b/model/search_params_test.go new file mode 100644 index 000000000..2eba20f4c --- /dev/null +++ b/model/search_params_test.go @@ -0,0 +1,70 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "testing" +) + +func TestParseSearchFlags(t *testing.T) { + if words, flags := parseSearchFlags(splitWords("")); len(words) != 0 { + t.Fatal("got words from empty input") + } else if len(flags) != 0 { + t.Fatal("got flags from empty input") + } + + if words, flags := parseSearchFlags(splitWords("word")); len(words) != 1 || words[0] != "word" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 0 { + t.Fatalf("got incorrect flags %v", flags) + } + + if words, flags := parseSearchFlags(splitWords("apple banana cherry")); len(words) != 3 || words[0] != "apple" || words[1] != "banana" || words[2] != "cherry" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 0 { + 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" { + 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" { + 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" { + 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" { + t.Fatalf("got incorrect flags %v", flags) + } + + if words, flags := parseSearchFlags(splitWords("fruit: cherry")); len(words) != 2 || words[0] != "fruit:" || words[1] != "cherry" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 0 { + t.Fatalf("got incorrect flags %v", flags) + } + + if words, flags := parseSearchFlags(splitWords("channel:")); len(words) != 1 || words[0] != "channel:" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 0 { + t.Fatalf("got incorrect flags %v", flags) + } + + 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" { + t.Fatalf("got incorrect flags %v", flags) + } +} diff --git a/model/utils.go b/model/utils.go index 269144afc..bb0669df7 100644 --- a/model/utils.go +++ b/model/utils.go @@ -242,10 +242,10 @@ func Etag(parts ...interface{}) string { var validHashtag = regexp.MustCompile(`^(#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$`) var puncStart = regexp.MustCompile(`^[.,()&$!\[\]{}"':;\\]+`) -var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"':;\\]+$`) +var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"';\\]+$`) func ParseHashtags(text string) (string, string) { - words := strings.Split(strings.Replace(text, "\n", " ", -1), " ") + words := strings.Fields(text) hashtagString := "" plainString := "" diff --git a/store/sql_post_store.go b/store/sql_post_store.go index 07077bd64..6971de9d7 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -407,15 +407,23 @@ var specialSearchChar = []string{ "@", } -func (s SqlPostStore) Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel { +func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchParams) StoreChannel { storeChannel := make(StoreChannel) go func() { result := StoreResult{} + termMap := map[string]bool{} + terms := params.Terms + + if terms == "" && params.InChannel == "" && params.FromUser == "" { + result.Data = []*model.Post{} + storeChannel <- result + return + } searchType := "Message" - if isHashtagSearch { + if params.IsHashtag { searchType = "Hashtags" for _, term := range strings.Split(terms, " ") { termMap[term] = true @@ -430,63 +438,85 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht var posts []*model.Post if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES { + // Parse text for wildcards + if wildcard, err := regexp.Compile("\\*($| )"); err == nil { + terms = wildcard.ReplaceAllLiteralString(terms, "* ") + } + } + searchQuery := ` + SELECT + * + FROM + Posts + WHERE + DeleteAt = 0 + POST_FILTER + AND ChannelId IN ( + SELECT + Id + FROM + Channels, + ChannelMembers + WHERE + Id = ChannelId + AND TeamId = :TeamId + AND UserId = :UserId + AND DeleteAt = 0 + CHANNEL_FILTER) + SEARCH_CLAUSE + ORDER BY CreateAt DESC + LIMIT 100` + + if params.InChannel != "" { + searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "AND Name = :InChannel", 1) + } else { + searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "", 1) + } + + if params.FromUser != "" { + searchQuery = strings.Replace(searchQuery, "POST_FILTER", ` + AND UserId IN ( + SELECT + Id + FROM + Users + WHERE + TeamId = :TeamId + AND Username = :FromUser)`, 1) + } else { + searchQuery = strings.Replace(searchQuery, "POST_FILTER", "", 1) + } + + if terms == "" { + // we've already confirmed that we have a channel or user to search for + searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1) + } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES { // Parse text for wildcards if wildcard, err := regexp.Compile("\\*($| )"); err == nil { terms = wildcard.ReplaceAllLiteralString(terms, ":* ") } - searchQuery := fmt.Sprintf(`SELECT - * - FROM - Posts - WHERE - DeleteAt = 0 - AND ChannelId IN (SELECT - Id - FROM - Channels, - ChannelMembers - WHERE - Id = ChannelId AND TeamId = $1 - AND UserId = $2 - AND DeleteAt = 0) - AND %s @@ to_tsquery($3) - ORDER BY CreateAt DESC - LIMIT 100`, searchType) - terms = strings.Join(strings.Fields(terms), " | ") - _, err := s.GetReplica().Select(&posts, searchQuery, teamId, userId, terms) - if err != nil { - result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error()) - - } + searchClause := fmt.Sprintf("AND %s @@ to_tsquery(:Terms)", searchType) + searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1) } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL { - searchQuery := fmt.Sprintf(`SELECT - * - FROM - Posts - WHERE - DeleteAt = 0 - AND ChannelId IN (SELECT - Id - FROM - Channels, - ChannelMembers - WHERE - Id = ChannelId AND TeamId = ? - AND UserId = ? - AND DeleteAt = 0) - AND MATCH (%s) AGAINST (? IN BOOLEAN MODE) - ORDER BY CreateAt DESC - LIMIT 100`, searchType) - - _, err := s.GetReplica().Select(&posts, searchQuery, teamId, userId, terms) - if err != nil { - result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error()) + searchClause := fmt.Sprintf("AND MATCH (%s) AGAINST (:Terms IN BOOLEAN MODE)", searchType) + searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1) + } - } + queryParams := map[string]interface{}{ + "TeamId": teamId, + "UserId": userId, + "Terms": terms, + "InChannel": params.InChannel, + "FromUser": params.FromUser, + } + + _, err := s.GetReplica().Select(&posts, searchQuery, queryParams) + if err != nil { + result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error()) } list := &model.PostList{Order: make([]string, 0, len(posts))} diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go index 9a7679454..b2256417e 100644 --- a/store/sql_post_store_test.go +++ b/store/sql_post_store_test.go @@ -525,57 +525,57 @@ func TestPostStoreSearch(t *testing.T) { o5.Hashtags = "#secret #howdy" o5 = (<-store.Post().Save(o5)).Data.(*model.Post) - r1 := (<-store.Post().Search(teamId, userId, "corey", false)).Data.(*model.PostList) + r1 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "corey", IsHashtag: false})).Data.(*model.PostList) if len(r1.Order) != 1 && r1.Order[0] != o1.Id { t.Fatal("returned wrong search result") } - r3 := (<-store.Post().Search(teamId, userId, "new", false)).Data.(*model.PostList) + r3 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "new", IsHashtag: false})).Data.(*model.PostList) if len(r3.Order) != 2 && r3.Order[0] != o1.Id { t.Fatal("returned wrong search result") } - r4 := (<-store.Post().Search(teamId, userId, "john", false)).Data.(*model.PostList) + r4 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "john", IsHashtag: false})).Data.(*model.PostList) if len(r4.Order) != 1 && r4.Order[0] != o2.Id { t.Fatal("returned wrong search result") } - r5 := (<-store.Post().Search(teamId, userId, "matter*", false)).Data.(*model.PostList) + r5 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "matter*", IsHashtag: false})).Data.(*model.PostList) if len(r5.Order) != 1 && r5.Order[0] != o1.Id { t.Fatal("returned wrong search result") } - r6 := (<-store.Post().Search(teamId, userId, "#hashtag", true)).Data.(*model.PostList) + r6 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "#hashtag", IsHashtag: true})).Data.(*model.PostList) if len(r6.Order) != 1 && r6.Order[0] != o4.Id { t.Fatal("returned wrong search result") } - r7 := (<-store.Post().Search(teamId, userId, "#secret", true)).Data.(*model.PostList) + r7 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "#secret", IsHashtag: true})).Data.(*model.PostList) if len(r7.Order) != 1 && r7.Order[0] != o5.Id { t.Fatal("returned wrong search result") } - r8 := (<-store.Post().Search(teamId, userId, "@thisshouldmatchnothing", true)).Data.(*model.PostList) + r8 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "@thisshouldmatchnothing", IsHashtag: true})).Data.(*model.PostList) if len(r8.Order) != 0 { t.Fatal("returned wrong search result") } - r9 := (<-store.Post().Search(teamId, userId, "mattermost jersey", false)).Data.(*model.PostList) + r9 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "mattermost jersey", IsHashtag: false})).Data.(*model.PostList) if len(r9.Order) != 2 { t.Fatal("returned wrong search result") } - r10 := (<-store.Post().Search(teamId, userId, "matter* jer*", false)).Data.(*model.PostList) + r10 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "matter* jer*", IsHashtag: false})).Data.(*model.PostList) if len(r10.Order) != 2 { t.Fatal("returned wrong search result") } - r11 := (<-store.Post().Search(teamId, userId, "message blargh", false)).Data.(*model.PostList) + r11 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "message blargh", IsHashtag: false})).Data.(*model.PostList) if len(r11.Order) != 1 { t.Fatal("returned wrong search result") } - r12 := (<-store.Post().Search(teamId, userId, "blargh>", false)).Data.(*model.PostList) + r12 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "blargh>", IsHashtag: false})).Data.(*model.PostList) if len(r12.Order) != 1 { t.Fatal("returned wrong search result") } diff --git a/store/store.go b/store/store.go index 70980a15c..27731cee1 100644 --- a/store/store.go +++ b/store/store.go @@ -84,7 +84,7 @@ type PostStore interface { GetPosts(channelId string, offset int, limit int) StoreChannel GetPostsSince(channelId string, time int64) StoreChannel GetEtag(channelId string) StoreChannel - Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel + Search(teamId string, userId string, params *model.SearchParams) StoreChannel GetForExport(channelId string) StoreChannel } diff --git a/utils/apns.go b/utils/apns.go index 3d07f17ec..06e8ce6ef 100644 --- a/utils/apns.go +++ b/utils/apns.go @@ -10,7 +10,7 @@ import ( "github.com/mattermost/platform/model" ) -func FireAndForgetSendAppleNotify(deviceId string, message string, badge int) { +func SendAppleNotifyAndForget(deviceId string, message string, badge int) { go func() { if err := SendAppleNotify(deviceId, message, badge); err != nil { l4g.Error(fmt.Sprintf("%v %v", err.Message, err.DetailedError)) diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 2df3dc40f..12d1af6ff 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -32,6 +32,7 @@ export default class CreateComment extends React.Component { this.removePreview = this.removePreview.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.getFileCount = this.getFileCount.bind(this); + this.handleResize = this.handleResize.bind(this); PostStore.clearCommentDraftUploads(); @@ -40,13 +41,23 @@ export default class CreateComment extends React.Component { messageText: draft.message, uploadsInProgress: draft.uploadsInProgress, previews: draft.previews, - submitting: false + submitting: false, + windowWidth: Utils.windowWidth() }; } + componentDidMount() { + window.addEventListener('resize', this.handleResize); + } + componentWillUnmount() { + window.removeEventListener('resize', this.handleResize); + } + handleResize() { + this.setState({windowWidth: Utils.windowWidth()}); + } componentDidUpdate(prevProps, prevState) { if (prevState.uploadsInProgress < this.state.uploadsInProgress) { $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight); - if ($(window).width() > 768) { + if (this.state.windowWidth > 768) { $('.post-right__scroll').perfectScrollbar('update'); } } diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 2581bdcca..035899592 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -37,6 +37,7 @@ export default class CreatePost extends React.Component { this.onChange = this.onChange.bind(this); this.getFileCount = this.getFileCount.bind(this); this.handleArrowUp = this.handleArrowUp.bind(this); + this.handleResize = this.handleResize.bind(this); PostStore.clearDraftUploads(); @@ -48,9 +49,17 @@ export default class CreatePost extends React.Component { uploadsInProgress: draft.uploadsInProgress, previews: draft.previews, submitting: false, - initialText: draft.messageText + initialText: draft.messageText, + windowWidth: Utils.windowWidth(), + windowHeigth: Utils.windowHeight() }; } + handleResize() { + this.setState({ + windowWidth: Utils.windowWidth(), + windowHeight: Utils.windowHeight() + }); + } componentDidUpdate(prevProps, prevState) { if (prevState.previews.length !== this.state.previews.length) { this.resizePostHolder(); @@ -61,6 +70,11 @@ export default class CreatePost extends React.Component { this.resizePostHolder(); return; } + + if (prevState.windowWidth !== this.state.windowWidth || prevState.windowHeight !== this.state.windowHeigth) { + this.resizePostHolder(); + return; + } } getCurrentDraft() { const draft = PostStore.getCurrentDraft(); @@ -194,10 +208,9 @@ export default class CreatePost extends React.Component { PostStore.storeCurrentDraft(draft); } resizePostHolder() { - const height = $(window).height() - $(ReactDOM.findDOMNode(this.refs.topDiv)).height() - 50; + const height = this.state.windowHeigth - $(ReactDOM.findDOMNode(this.refs.topDiv)).height() - 50; $('.post-list-holder-by-time').css('height', `${height}px`); - $(window).trigger('resize'); - if ($(window).width() > 960) { + if (this.state.windowWidth > 960) { $('#post_textbox').focus(); } } @@ -274,9 +287,11 @@ export default class CreatePost extends React.Component { componentDidMount() { ChannelStore.addChangeListener(this.onChange); this.resizePostHolder(); + window.addEventListener('resize', this.handleResize); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChange); + window.removeEventListener('resize', this.handleResize); } onChange() { const channelId = ChannelStore.getCurrentId(); diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx index 467d74681..bcdec2870 100644 --- a/web/react/components/rhs_thread.jsx +++ b/web/react/components/rhs_thread.jsx @@ -4,7 +4,7 @@ var PostStore = require('../stores/post_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var PreferenceStore = require('../stores/preference_store.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); var SearchBox = require('./search_bar.jsx'); var CreateComment = require('./create_comment.jsx'); var RhsHeaderPost = require('./rhs_header_post.jsx'); @@ -20,8 +20,12 @@ export default class RhsThread extends React.Component { this.onChange = this.onChange.bind(this); this.onChangeAll = this.onChangeAll.bind(this); this.forceUpdateInfo = this.forceUpdateInfo.bind(this); + this.handleResize = this.handleResize.bind(this); - this.state = this.getStateFromStores(); + const state = this.getStateFromStores(); + state.windowWidth = Utils.windowWidth(); + state.windowHeight = Utils.windowHeight(); + this.state = state; } getStateFromStores() { var postList = PostStore.getSelectedPost(); @@ -47,9 +51,7 @@ export default class RhsThread extends React.Component { PostStore.addChangeListener(this.onChangeAll); PreferenceStore.addChangeListener(this.forceUpdateInfo); this.resize(); - $(window).resize(function resize() { - this.resize(); - }.bind(this)); + window.addEventListener('resize', this.handleResize); } componentDidUpdate() { if ($('.post-right__scroll')[0]) { @@ -61,6 +63,7 @@ export default class RhsThread extends React.Component { PostStore.removeSelectedPostChangeListener(this.onChange); PostStore.removeChangeListener(this.onChangeAll); PreferenceStore.removeChangeListener(this.forceUpdateInfo); + window.removeEventListener('resize', this.handleResize); } forceUpdateInfo() { if (this.state.postList) { @@ -71,9 +74,15 @@ export default class RhsThread extends React.Component { } } } + handleResize() { + this.setState({ + windowWidth: Utils.windowWidth(), + windowHeight: Utils.windowHeight() + }); + } onChange() { var newState = this.getStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { + if (!Utils.areStatesEqual(newState, this.state)) { this.setState(newState); } } @@ -103,15 +112,15 @@ export default class RhsThread extends React.Component { } var newState = this.getStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { + if (!Utils.areStatesEqual(newState, this.state)) { this.setState(newState); } } resize() { - var height = $(window).height() - $('#error_bar').outerHeight() - 100; + var height = this.state.windowHeight - $('#error_bar').outerHeight() - 100; $('.post-right__scroll').css('height', height + 'px'); $('.post-right__scroll').scrollTop(100000); - if ($(window).width() > 768) { + if (this.state.windowWidth > 768) { $('.post-right__scroll').perfectScrollbar(); $('.post-right__scroll').perfectScrollbar('update'); } diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx index e55fd3752..30e15d0ad 100644 --- a/web/react/components/search_results.jsx +++ b/web/react/components/search_results.jsx @@ -4,7 +4,7 @@ var PostStore = require('../stores/post_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var SearchBox = require('./search_bar.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); var SearchResultsHeader = require('./search_results_header.jsx'); var SearchResultsItem = require('./search_results_item.jsx'); @@ -20,18 +20,19 @@ export default class SearchResults extends React.Component { this.onChange = this.onChange.bind(this); this.resize = this.resize.bind(this); + this.handleResize = this.handleResize.bind(this); - this.state = getStateFromStores(); + const state = getStateFromStores(); + state.windowWidth = Utils.windowWidth(); + state.windowHeight = Utils.windowHeight(); + this.state = state; } componentDidMount() { this.mounted = true; PostStore.addSearchChangeListener(this.onChange); this.resize(); - var self = this; - $(window).resize(function resize() { - self.resize(); - }); + window.addEventListener('resize', this.handleResize); } componentDidUpdate() { @@ -41,22 +42,30 @@ export default class SearchResults extends React.Component { componentWillUnmount() { PostStore.removeSearchChangeListener(this.onChange); this.mounted = false; + window.removeEventListener('resize', this.handleResize); + } + + handleResize() { + this.setState({ + windowWidth: Utils.windowWidth(), + windowHeight: Utils.windowHeight() + }); } onChange() { if (this.mounted) { var newState = getStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { + if (!Utils.areStatesEqual(newState, this.state)) { this.setState(newState); } } } resize() { - var height = $(window).height() - $('#error_bar').outerHeight() - 100; + var height = this.state.windowHeight - $('#error_bar').outerHeight() - 100; $('#search-items-container').css('height', height + 'px'); $('#search-items-container').scrollTop(0); - if ($(window).width() > 768) { + if (this.state.windowWidth > 768) { $('#search-items-container').perfectScrollbar(); } } diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index f88522fb9..d1fe37300 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -29,9 +29,10 @@ export default class Sidebar extends React.Component { this.onChange = this.onChange.bind(this); this.onScroll = this.onScroll.bind(this); - this.onResize = this.onResize.bind(this); this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this); this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this); + this.updateScrollbar = this.updateScrollbar.bind(this); + this.handleResize = this.handleResize.bind(this); this.showNewChannelModal = this.showNewChannelModal.bind(this); this.hideNewChannelModal = this.hideNewChannelModal.bind(this); @@ -46,6 +47,7 @@ export default class Sidebar extends React.Component { state.newChannelModalType = ''; state.showDirectChannelsModal = false; state.loadingDMChannel = -1; + state.windowWidth = Utils.windowWidth(); this.state = state; } @@ -129,14 +131,11 @@ export default class Sidebar extends React.Component { TeamStore.addChangeListener(this.onChange); PreferenceStore.addChangeListener(this.onChange); - if ($(window).width() > 768) { - $('.nav-pills__container').perfectScrollbar(); - } - this.updateTitle(); this.updateUnreadIndicators(); + this.updateScrollbar(); - $(window).on('resize', this.onResize); + window.addEventListener('resize', this.handleResize); } shouldComponentUpdate(nextProps, nextState) { if (!Utils.areStatesEqual(nextProps, this.props)) { @@ -151,9 +150,10 @@ export default class Sidebar extends React.Component { componentDidUpdate() { this.updateTitle(); this.updateUnreadIndicators(); + this.updateScrollbar(); } componentWillUnmount() { - $(window).off('resize', this.onResize); + window.removeEventListener('resize', this.handleResize); ChannelStore.removeChangeListener(this.onChange); UserStore.removeChangeListener(this.onChange); @@ -161,6 +161,18 @@ export default class Sidebar extends React.Component { TeamStore.removeChangeListener(this.onChange); PreferenceStore.removeChangeListener(this.onChange); } + handleResize() { + this.setState({ + windowWidth: Utils.windowWidth(), + windowHeight: Utils.windowHeight() + }); + } + updateScrollbar() { + if (this.state.windowWidth > 768) { + $('.nav-pills__container').perfectScrollbar(); + $('.nav-pills__container').perfectScrollbar('update'); + } + } onChange() { var newState = this.getStateFromStores(); if (!Utils.areStatesEqual(newState, this.state)) { @@ -186,9 +198,6 @@ export default class Sidebar extends React.Component { onScroll() { this.updateUnreadIndicators(); } - onResize() { - this.updateUnreadIndicators(); - } updateUnreadIndicators() { const container = $(ReactDOM.findDOMNode(this.refs.container)); diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index fb0690d66..5f266bba3 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -968,3 +968,11 @@ export function getShortenedTeamURL() { } return teamURL + '/'; } + +export function windowWidth() { + return $(window).width(); +} + +export function windowHeight() { + return $(window).height(); +} diff --git a/web/web.go b/web/web.go index a24f1589d..3bfed371b 100644 --- a/web/web.go +++ b/web/web.go @@ -429,9 +429,9 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { user := result.Data.(*model.User) if user.LastActivityAt > 0 { - api.FireAndForgetEmailChangeVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + api.SendEmailChangeVerifyEmailAndForget(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) } else { - api.FireAndForgetVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + api.SendVerifyEmailAndForget(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) } newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1) |