From 43b5f060c9b5f3a928710415b4fa37b17db3d98a Mon Sep 17 00:00:00 2001 From: Girish S Date: Sun, 18 Oct 2015 09:33:47 +0530 Subject: translate ;) to wink emoji --- web/react/utils/emoticons.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/react/utils/emoticons.jsx b/web/react/utils/emoticons.jsx index 94bb91503..7b43e48b4 100644 --- a/web/react/utils/emoticons.jsx +++ b/web/react/utils/emoticons.jsx @@ -3,6 +3,7 @@ const emoticonPatterns = { smile: /(^|\s)(:-?\))($|\s)/g, // :) + wink: /(^|\s)(;-?\))($|\s)/g, // ;) open_mouth: /(^|\s)(:o)($|\s)/gi, // :o scream: /(^|\s)(:-o)($|\s)/gi, // :-o smirk: /(^|\s)(:-?])($|\s)/g, // :] -- cgit v1.2.3-1-g7c22 From 06fd374c1907add3faeeba7916b279e0a3302a4e Mon Sep 17 00:00:00 2001 From: hmhealey Date: Sat, 17 Oct 2015 14:37:51 -0400 Subject: Added from:, in:, and channel: search modifiers --- api/post.go | 10 ++-- api/post_test.go | 122 ++++++++++++++++++++++++++++++++++++++++ model/search_params.go | 130 +++++++++++++++++++++++++++++++++++++++++++ model/search_params_test.go | 70 +++++++++++++++++++++++ model/utils.go | 4 +- store/sql_post_store.go | 128 ++++++++++++++++++++++++++---------------- store/sql_post_store_test.go | 22 ++++---- store/store.go | 2 +- 8 files changed, 420 insertions(+), 68 deletions(-) create mode 100644 model/search_params.go create mode 100644 model/search_params_test.go diff --git a/api/post.go b/api/post.go index 58fd3488a..ecc319a91 100644 --- a/api/post.go +++ b/api/post.go @@ -680,16 +680,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/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..dc1b78ea9 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 := 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), " | ") + 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) + } - _, 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()) + 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, ":* ") } + + terms = strings.Join(strings.Fields(terms), " | ") + + 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 de335cc2b..7f31325fd 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 } -- cgit v1.2.3-1-g7c22 From 2d42aefeafd2a0bea6419a6ef79c0c9b1fd313eb Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Mon, 19 Oct 2015 22:58:47 +0500 Subject: Multiple UI Improvements --- web/react/components/more_direct_channels.jsx | 9 +++++---- web/react/utils/utils.jsx | 4 ++-- web/sass-files/sass/partials/_modal.scss | 4 ++-- web/sass-files/sass/partials/_responsive.scss | 3 ++- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index 105199035..1db22ebaa 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -140,12 +140,11 @@ export default class MoreDirectChannels extends React.Component { if (user.nickname) { const separator = fullName ? ' - ' : ''; details.push( -

{separator + user.nickname} -

+ ); } @@ -184,7 +183,9 @@ export default class MoreDirectChannels extends React.Component {
{user.username}
- {details} +
+ {details} +
{joinButton} diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 38ac68d58..fb0690d66 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -519,11 +519,11 @@ export function applyTheme(theme) { changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1); changeCss('.channel-intro, .settings-modal .settings-table .settings-content .divider-dark, hr, .settings-modal .settings-table .settings-links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, pre', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); - changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body, .modal .more-channel-table tbody>tr td, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 2); + changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body, .modal .more-table tbody>tr td, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 2); changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); - changeCss('.post:hover, .modal .more-channel-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); + changeCss('.post:hover, .modal .more-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1); changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1); diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss index 0e474a1e2..5570b5ce4 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -194,7 +194,7 @@ position: relative; max-width: 90%; min-height: 100px; - min-width: 240px; + min-width: 320px; @include border-radius(3px); display: table; margin: 0 auto; @@ -338,7 +338,7 @@ .modal-direct-channels { .user-list { - margin-top: 20px; + margin-top: 10px; overflow: auto; -webkit-overflow-scrolling: touch; max-height: 500px; diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index c41199cac..09ac2047c 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -267,6 +267,7 @@ } } .file-details { + width: 100%; height: auto; } } @@ -697,7 +698,7 @@ .modal-image { .image-wrapper { font-size: 12px; - min-width: 280px; + min-width: 250px; .modal-close { @include opacity(1); } -- cgit v1.2.3-1-g7c22 From f7e51b14d7a05cef38e3b439cef9d392e8ebee7d Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Mon, 19 Oct 2015 11:23:44 -0700 Subject: Allowing salts longer than 32 chars --- model/config.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/model/config.go b/model/config.go index 8a11b7bb7..59caf0884 100644 --- a/model/config.go +++ b/model/config.go @@ -184,8 +184,8 @@ func (o *Config) IsValid() *AppError { return NewAppError("Config.IsValid", "Invalid maximum users per team for team settings. Must be a positive number.", "") } - if len(o.SqlSettings.AtRestEncryptKey) != 32 { - return NewAppError("Config.IsValid", "Invalid at rest encrypt key for SQL settings. Must be 32 chars.", "") + if len(o.SqlSettings.AtRestEncryptKey) < 32 { + return NewAppError("Config.IsValid", "Invalid at rest encrypt key for SQL settings. Must be 32 chars or more.", "") } if !(o.SqlSettings.DriverName == DATABASE_DRIVER_MYSQL || o.SqlSettings.DriverName == DATABASE_DRIVER_POSTGRES) { @@ -232,20 +232,20 @@ func (o *Config) IsValid() *AppError { return NewAppError("Config.IsValid", "Invalid thumbnail width for file settings. Must be a positive number.", "") } - if len(o.FileSettings.PublicLinkSalt) != 32 { - return NewAppError("Config.IsValid", "Invalid public link salt for file settings. Must be 32 chars.", "") + if len(o.FileSettings.PublicLinkSalt) < 32 { + return NewAppError("Config.IsValid", "Invalid public link salt for file settings. Must be 32 chars or more.", "") } if !(o.EmailSettings.ConnectionSecurity == CONN_SECURITY_NONE || o.EmailSettings.ConnectionSecurity == CONN_SECURITY_TLS || o.EmailSettings.ConnectionSecurity == CONN_SECURITY_STARTTLS) { return NewAppError("Config.IsValid", "Invalid connection security for email settings. Must be '', 'TLS', or 'STARTTLS'", "") } - if len(o.EmailSettings.InviteSalt) != 32 { - return NewAppError("Config.IsValid", "Invalid invite salt for email settings. Must be 32 chars.", "") + if len(o.EmailSettings.InviteSalt) < 32 { + return NewAppError("Config.IsValid", "Invalid invite salt for email settings. Must be 32 chars or more.", "") } - if len(o.EmailSettings.PasswordResetSalt) != 32 { - return NewAppError("Config.IsValid", "Invalid password reset salt for email settings. Must be 32 chars.", "") + if len(o.EmailSettings.PasswordResetSalt) < 32 { + return NewAppError("Config.IsValid", "Invalid password reset salt for email settings. Must be 32 chars or more.", "") } if o.RateLimitSettings.MemoryStoreSize <= 0 { -- cgit v1.2.3-1-g7c22 From b950d99b3a786b94cc5e97b67442bf4181463725 Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Mon, 19 Oct 2015 23:59:17 +0500 Subject: Updating direct messages title --- web/react/components/more_direct_channels.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index 1db22ebaa..d5b44d86b 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -243,7 +243,7 @@ export default class MoreDirectChannels extends React.Component { onHide={this.handleHide} > - {'Team Directory'} + {'Direct Messages'}
-- cgit v1.2.3-1-g7c22 From 995c5a276bb27f50332047684b4d8f4cfc02243b Mon Sep 17 00:00:00 2001 From: hmhealey Date: Mon, 19 Oct 2015 15:30:43 -0400 Subject: Fixed incorrectly escaped * in wildcard search for postgres --- store/sql_post_store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/sql_post_store.go b/store/sql_post_store.go index dc1b78ea9..6971de9d7 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -440,7 +440,7 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES { // Parse text for wildcards if wildcard, err := regexp.Compile("\\*($| )"); err == nil { - terms = wildcard.ReplaceAllLiteralString(terms, ":* ") + terms = wildcard.ReplaceAllLiteralString(terms, "* ") } } -- cgit v1.2.3-1-g7c22 From 1d4e96605e04f9bf70b4d467c1ff24584c0a25a0 Mon Sep 17 00:00:00 2001 From: Steven Harms Date: Mon, 19 Oct 2015 16:02:30 -0400 Subject: Fixed missing colon --- model/access.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/access.go b/model/access.go index 89a1271c1..6c9254004 100644 --- a/model/access.go +++ b/model/access.go @@ -16,7 +16,7 @@ const ( type AccessData struct { AuthCode string `json:"auth_code"` - Token string `json"token"` + Token string `json:"token"` RefreshToken string `json:"refresh_token"` RedirectUri string `json:"redirect_uri"` } -- cgit v1.2.3-1-g7c22 From 04bf527966db559523fb0ec317af5076241f1bc5 Mon Sep 17 00:00:00 2001 From: Reed Garmsen Date: Wed, 14 Oct 2015 16:56:13 -0700 Subject: Changed all goroutine functions to use '...AndForget' as the standard naming system --- api/file.go | 10 +++++----- api/post.go | 10 +++++----- api/user.go | 20 ++++++++++---------- mattermost.go | 4 ++-- utils/apns.go | 2 +- web/web.go | 4 ++-- 6 files changed, 25 insertions(+), 25 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..1d6cec5dd 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) 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/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/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/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) -- cgit v1.2.3-1-g7c22 From 4404912909d1ce445e5d80fb622f7d9fe91ff78d Mon Sep 17 00:00:00 2001 From: it33 Date: Mon, 19 Oct 2015 17:41:39 -0700 Subject: Minor formatting adjustment --- doc/integrations/webhooks/Incoming-Webhooks.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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: -- cgit v1.2.3-1-g7c22 From d3f99e821733b7c86ad297c136489678a2a9fffb Mon Sep 17 00:00:00 2001 From: Antti Ahti Date: Fri, 16 Oct 2015 12:32:19 +0300 Subject: Handle window resize events in React way Use the React-way of handling resize events. Essentially store the window size in component state instead of doing some custom handling. See http://facebook.github.io/react/tips/dom-event-listeners.html --- web/react/components/create_comment.jsx | 15 +++++++++++++-- web/react/components/create_post.jsx | 23 +++++++++++++++++++---- web/react/components/rhs_thread.jsx | 27 ++++++++++++++++++--------- web/react/components/search_results.jsx | 27 ++++++++++++++++++--------- web/react/components/sidebar.jsx | 29 +++++++++++++++++++---------- web/react/utils/utils.jsx | 8 ++++++++ 6 files changed, 95 insertions(+), 34 deletions(-) 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(); +} -- cgit v1.2.3-1-g7c22 From 88b3aa4d80e7ee540083f897cdce3f9634c4a8f9 Mon Sep 17 00:00:00 2001 From: it33 Date: Tue, 20 Oct 2015 02:11:41 -0700 Subject: Create Gitlab-Integration-Service-for-Mattermost.md --- .../services/Gitlab-Integration-Service-for-Mattermost.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 doc/integrations/services/Gitlab-Integration-Service-for-Mattermost.md 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) -- cgit v1.2.3-1-g7c22 From 437efdaaaa102f53f4ed8630d5d85645cfd06999 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Tue, 20 Oct 2015 04:49:42 -0700 Subject: Hotfix to allow .7 to be upgraded to 1.1 --- store/sql_store.go | 16 +++++++++++++++- store/sql_team_store.go | 2 ++ store/sql_user_store.go | 2 ++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/store/sql_store.go b/store/sql_store.go index 900543460..3d10772d9 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -72,7 +72,14 @@ func NewSqlStore() Store { // Check to see if it's the most current database schema version if !model.IsCurrentVersion(schemaVersion) { // If we are upgrading from the previous version then print a warning and continue - if model.IsPreviousVersion(schemaVersion) { + + // Special case + isSchemaVersion07 := false + if schemaVersion == "0.7.1" || schemaVersion == "0.7.0" { + isSchemaVersion07 = true + } + + if model.IsPreviousVersion(schemaVersion) || isSchemaVersion07 { 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 { @@ -84,6 +91,13 @@ func NewSqlStore() Store { } } + // REMOVE in 1.2 + if sqlStore.DoesTableExist("Sessions") { + if sqlStore.DoesColumnExist("Sessions", "AltId") { + sqlStore.GetMaster().Exec("DROP TABLE IF EXISTS Sessions") + } + } + sqlStore.team = NewSqlTeamStore(sqlStore) sqlStore.channel = NewSqlChannelStore(sqlStore) sqlStore.post = NewSqlPostStore(sqlStore) diff --git a/store/sql_team_store.go b/store/sql_team_store.go index de44782cf..2d65435b0 100644 --- a/store/sql_team_store.go +++ b/store/sql_team_store.go @@ -28,6 +28,8 @@ func NewSqlTeamStore(sqlStore *SqlStore) TeamStore { } func (s SqlTeamStore) UpgradeSchemaIfNeeded() { + // REMOVE in 1.2 + s.RemoveColumnIfExists("Teams", "AllowValet") } func (s SqlTeamStore) CreateIndexesIfNotExists() { diff --git a/store/sql_user_store.go b/store/sql_user_store.go index dc6b07a16..a2b317afa 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -41,6 +41,8 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore { } func (us SqlUserStore) UpgradeSchemaIfNeeded() { + // REMOVE in 1.2 + us.CreateColumnIfNotExists("Users", "ThemeProps", "varchar(2000)", "character varying(2000)", "{}") } func (us SqlUserStore) CreateIndexesIfNotExists() { -- cgit v1.2.3-1-g7c22 From fcc67b77cd9ed01ef540f8e86f9a2bd9a03c3635 Mon Sep 17 00:00:00 2001 From: it33 Date: Tue, 20 Oct 2015 07:26:39 -0700 Subject: Update CHANGELOG.md --- CHANGELOG.md | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 169 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66e430f7c..26875cf97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,8 @@ # Mattermost Changelog -## UNDER DEVELOPMENT - Release v1.1.0 +## Release v1.1.0 --The "UNDER DEVELOPMENT" section of the Mattermost changelog appears in the product's `master` branch to note key changes committed to master and are on their way to the next stable release. When a stable release is pushed the "UNDER DEVELOPMENT" heading is removed from the final changelog of the release. -- --- **Final release anticipated:** 2015-10-16 +Released: 2015-10-16 ### Release Highlights @@ -45,6 +43,31 @@ Messaging and Notifications - Fixed bug where System Administrator did not have Team Administrator permissions - Fixed bug causing scrolling to jump when the right hand sidebar opened and closed +### Known Issues + +- Slack import is unstable due to change in Slack export format +- Uploading a .flac file breaks the file previewer on iOS + +### Compatibility + +#### Config.json Changes from v1.0 to v1.1 + +##### Service Settings + +Multiple settings were added to [`config.json`](./config/config.json) and System Console UI. Prior to upgrading the Mattermost binaries from the previous versions, these options would need to be manually updated in existing config.json file. This is a list of changes and their new default values in a fresh install: +- Under `ServiceSettings` in `config.json`: + - Added: `"EnablePostIconOverride": false` to control whether webhooks can override profile pictures + - Added: `"EnablePostUsernameOverride": false` to control whether webhooks can override profile pictures + - Added: `"EnableSecurityFixAlert": true` to control whether the system is alerted to security updates + +#### Database Changes from v1.0 to v1.1 + +The following is for informational purposes only, no action needed. Mattermost automatically upgrades database tables from the previous version's schema using only additions. Sessions table is dropped and rebuilt, no team data is affected by this. + +##### ChannelMembers Table +1. Removed `NotifyLevel` column +2. Added `NotifyProps` column with type `varchar(2000)` and default value `{}` + ### Contributors Many thanks to our external contributors. In no particular order: @@ -166,6 +189,148 @@ Licensing - Fixed issue so that SSO option automatically set EmailVerified=true (it was false previously) +### Compatibility + +A large number of settings were changed in [`config.json`](./config/config.json) and a System Console UI was added. This is a very large change due to Mattermost releasing as v1.0 and it's unlikely a change of this size would happen again. + +Prior to upgrading the Mattermost binaries from the previous versions, the below options would need to be manually updated in existing config.json file to migrate successfully. This is a list of changes and their new default values in a fresh install: +#### Config.json Changes from v0.7 to v1.0 + +##### Service Settings + +- Under `ServiceSettings` in [`config.json`](./config/config.json): + - **Moved:** `"SiteName": "Mattermost"` which was added to `TeamSettings` + - **Removed:** `"Mode" : "dev"` which deprecates a high level dev mode, now replaced by granular controls + - **Renamed:** `"AllowTesting" : false` to `"EnableTesting": false` which allows the use of `/loadtest` slash commands during development + - **Removed:** `"UseSSL": false` boolean replaced by `"ConnectionSecurity": ""` under `Security` with new options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ ('"StartTLS"`) + - **Renamed**: `"Port": "8065"` to `"ListenAddress": ":8065"` to define address on which to listen. Must be prepended with a colon. + - **Removed:** `"Version": "developer"` removed and version information now stored in `model/version.go` + - **Removed:** `"Shards": {}` which was not used + - **Moved:** `"InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"` to `EmailSettings` + - **Moved:** `"PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4"` to `FileSettings` + - **Renamed and Moved** `"ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t"` to `"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL"` and moved to `EmailSettings` + - **Removed:** `"AnalyticsUrl": ""` which was not used + - **Removed:** `"UseLocalStorage": true` which is replaced by `"DriverName": "local"` in `FileSettings` + - **Renamed and Moved:** `"StorageDirectory": "./data/"` to `Directory` and moved to `FileSettings` + - **Renamed:** `"AllowedLoginAttempts": 10` to `"MaximumLoginAttempts": 10` + - **Renamed, Reversed and Moved:** `"DisableEmailSignUp": false` renamed `"EnableSignUpWithEmail": true`, reversed meaning of `true`, and moved to `EmailSettings` + - **Added:** `"EnableOAuthServiceProvider": false` to enable OAuth2 service provider functionality + - **Added:** `"EnableIncomingWebhooks": false` to enable incoming webhooks feature + +##### Team Settings + +- Under `TeamSettings` in [`config.json`](./config/config.json): + - **Renamed:** `"AllowPublicLink": true` renamed to `"EnablePublicLink": true` and moved to `FileSettings` + - **Removed:** `AllowValetDefault` which was a guest account feature that is deprecated + - **Removed:** `"TermsLink": "/static/help/configure_links.html"` removed since option didn't need configuration + - **Removed:** `"PrivacyLink": "/static/help/configure_links.html"` removed since option didn't need configuration + - **Removed:** `"AboutLink": "/static/help/configure_links.html"` removed since option didn't need configuration + - **Removed:** `"HelpLink": "/static/help/configure_links.html"` removed since option didn't need configuration + - **Removed:** `"ReportProblemLink": "/static/help/configure_links.html"` removed since option didn't need configuration + - **Removed:** `"TourLink": "/static/help/configure_links.html"` removed since option didn't need configuration + - **Removed:** `"DefaultThemeColor": "#2389D7"` removed since theme colors changed from 1 to 18, default theme color option may be added back later after theme color design stablizes + - **Renamed:** `"DisableTeamCreation": false` to `"EnableUserCreation": true` and reversed + - **Added:** ` "EnableUserCreation": true` added to disable ability to create new user accounts in the system + +##### SSO Settings + +- Under `SSOSettings` in [`config.json`](./config/config.json): + - **Renamed Category:** `SSOSettings` to `GitLabSettings` + - **Renamed:** `"Allow": false` to `"Enable": false` to enable GitLab SSO + +##### AWS Settings + +- Under `AWSSettings` in [`config.json`](./config/config.json): + - This section was removed and settings moved to `FileSettings` + - **Renamed and Moved:** `"S3AccessKeyId": ""` renamed `"AmazonS3AccessKeyId": "",` and moved to `FileSettings` + - **Renamed and Moved:** `"S3SecretAccessKey": ""` renamed `"AmazonS3SecretAccessKey": "",` and moved to `FileSettings` + - **Renamed and Moved:** `"S3Bucket": ""` renamed `"AmazonS3Bucket": "",` and moved to `FileSettings` + - **Renamed and Moved:** `"S3Region": ""` renamed `"AmazonS3Region": "",` and moved to `FileSettings` + +##### Image Settings + +- Under `ImageSettings` in [`config.json`](./config/config.json): + - **Renamed:** `"ImageSettings"` section to `"FileSettings"` + - **Added:** `"DriverName" : "local"` to specify the file storage method, `amazons3` can also be used to setup S3 + +##### EmailSettings + +- Under `EmailSettings` in [`config.json`](./config/config.json): + - **Removed:** `"ByPassEmail": "true"` which is replaced with `SendEmailNotifications` and `RequireEmailVerification` + - **Added:** `"SendEmailNotifications" : "false"` to control whether email notifications are sent + - **Added:** `"RequireEmailVerification" : "false"` to control if users need to verify their emails + - **Replaced:** `"UseTLS": "false"` with `"ConnectionSecurity": ""` with options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ ('"StartTLS"`) + - **Replaced:** `"UseStartTLS": "false"` with `"ConnectionSecurity": ""` with options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ ('"StartTLS"`) + +##### Privacy Settings + +- Under `PrivacySettings` in [`config.json`](./config/config.json): + - **Removed:** `"ShowPhoneNumber": "true"` which was not used + - **Removed:** `"ShowSkypeId" : "true"` which was not used + +### Database Changes from v0.7 to v1.0 + +The following is for informational purposes only, no action needed. Mattermost automatically upgrades database tables from the previous version's schema using only additions. Sessions table is dropped and rebuilt, no team data is affected by this. + +##### Users Table +1. Added `ThemeProps` column with type `varchar(2000)` and default value `{}` + +##### Teams Table +1. Removed `AllowValet` column + +##### Sessions Table +1. Renamed `Id` column `Token` +2. Renamed `AltId` column `Id` +3. Added `IsOAuth` column with type `tinyint(1)` and default value `0` + +##### OAuthAccessData Table +1. Added new table `OAuthAccessData` +2. Added `AuthCode` column with type `varchar(128)` +3. Added `Token` column with type `varchar(26)` as the primary key +4. Added `RefreshToken` column with type `varchar(26)` +5. Added `RedirectUri` column with type `varchar(256)` +6. Added index on `AuthCode` column + +##### OAuthApps Table +1. Added new table `OAuthApps` +2. Added `Id` column with type `varchar(26)` as primary key +2. Added `CreatorId` column with type `varchar(26)` +2. Added `CreateAt` column with type `bigint(20)` +2. Added `UpdateAt` column with type `bigint(20)` +2. Added `ClientSecret` column with type `varchar(128)` +2. Added `Name` column with type `varchar(64)` +2. Added `Description` column with type `varchar(512)` +2. Added `CallbackUrls` column with type `varchar(1024)` +2. Added `Homepage` column with type `varchar(256)` +3. Added index on `CreatorId` column + +##### OAuthAuthData Table +1. Added new table `OAuthAuthData` +2. Added `ClientId` column with type `varchar(26)` +2. Added `UserId` column with type `varchar(26)` +2. Added `Code` column with type `varchar(128)` as primary key +2. Added `ExpiresIn` column with type `int(11)` +2. Added `CreateAt` column with type `bigint(20)` +2. Added `State` column with type `varchar(128)` +2. Added `Scope` column with type `varchar(128)` + +##### IncomingWebhooks Table +1. Added new table `IncomingWebhooks` +2. Added `Id` column with type `varchar(26)` as primary key +2. Added `CreateAt` column with type `bigint(20)` +2. Added `UpdateAt` column with type `bigint(20)` +2. Added `DeleteAt` column with type `bigint(20)` +2. Added `UserId` column with type `varchar(26)` +2. Added `ChannelId` column with type `varchar(26)` +2. Added `TeamId` column with type `varchar(26)` +3. Added index on `UserId` column +3. Added index on `TeamId` column + +##### Systems Table +1. Added new table `Systems` +2. Added `Name` column with type `varchar(64)` as primary key +3. Added `Value column with type `varchar(1024)` + ### Contributors Many thanks to our external contributors. In no particular order: -- cgit v1.2.3-1-g7c22 From 84241249874a6434ed9e33d00eda777928a4e4a6 Mon Sep 17 00:00:00 2001 From: it33 Date: Tue, 20 Oct 2015 11:15:39 -0700 Subject: Create Upgrade-Guide.md --- doc/install/Upgrade-Guide.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 doc/install/Upgrade-Guide.md diff --git a/doc/install/Upgrade-Guide.md b/doc/install/Upgrade-Guide.md new file mode 100644 index 000000000..e86cf8166 --- /dev/null +++ b/doc/install/Upgrade-Guide.md @@ -0,0 +1,16 @@ +# Mattermost Upgrade Guide + +### Upgrading Mattermost v0.7 to v1.1 + +If you've manually changed Mattermost v0.7 configuration by updating the `config.json` file, you'll need to port those changes to Mattermost v1.1: + +1. Go to the `config.json` file that you manually updated and note any differences from the [default `config.json` file in Mattermost 0.7](https://github.com/mattermost/platform/blob/v0.7.0/config/config.json). + +2. For each setting that you changed, check [the changelog documentation](https://github.com/mattermost/platform/blob/master/CHANGELOG.md#configjson-changes-from-v07-to-v10) on whether the configuration setting has changed between v0.7 and v1.1 + +3. Update your new [`config.json` file in Mattermost v1.1](https://github.com/mattermost/platform/blob/v1.1.0/config/config.json), based on your preferences and the changelog documentation above. + +Optionally, you can use the new [System Console user interface](https://github.com/mattermost/platform/blob/master/doc/install/Configuration-Settings.md) to make changes to your new `config.json` file. + + + -- cgit v1.2.3-1-g7c22