summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api4/channel.go25
-rw-r--r--app/channel.go10
-rw-r--r--model/client4.go11
-rw-r--r--store/sqlstore/channel_store.go47
-rw-r--r--store/store.go1
-rw-r--r--store/storetest/channel_store.go87
-rw-r--r--store/storetest/mocks/ChannelStore.go16
7 files changed, 197 insertions, 0 deletions
diff --git a/api4/channel.go b/api4/channel.go
index d497c9793..b29d2a94c 100644
--- a/api4/channel.go
+++ b/api4/channel.go
@@ -22,6 +22,7 @@ func (api *API) InitChannel() {
api.BaseRoutes.ChannelsForTeam.Handle("/ids", api.ApiSessionRequired(getPublicChannelsByIdsForTeam)).Methods("POST")
api.BaseRoutes.ChannelsForTeam.Handle("/search", api.ApiSessionRequired(searchChannelsForTeam)).Methods("POST")
api.BaseRoutes.ChannelsForTeam.Handle("/autocomplete", api.ApiSessionRequired(autocompleteChannelsForTeam)).Methods("GET")
+ api.BaseRoutes.ChannelsForTeam.Handle("/search_autocomplete", api.ApiSessionRequired(autocompleteChannelsForTeamForSearch)).Methods("GET")
api.BaseRoutes.User.Handle("/teams/{team_id:[A-Za-z0-9]+}/channels", api.ApiSessionRequired(getChannelsForTeamForUser)).Methods("GET")
api.BaseRoutes.Channel.Handle("", api.ApiSessionRequired(getChannel)).Methods("GET")
@@ -642,6 +643,30 @@ func autocompleteChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Requ
w.Write([]byte(channels.ToJson()))
}
+func autocompleteChannelsForTeamForSearch(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.RequireTeamId()
+ if c.Err != nil {
+ return
+ }
+
+ if !c.App.SessionHasPermissionToTeam(c.Session, c.Params.TeamId, model.PERMISSION_LIST_TEAM_CHANNELS) {
+ c.SetPermissionError(model.PERMISSION_LIST_TEAM_CHANNELS)
+ return
+ }
+
+ name := r.URL.Query().Get("name")
+
+ channels, err := c.App.AutocompleteChannelsForSearch(c.Params.TeamId, c.Session.UserId, name)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ // Don't fill in channels props, since unused by client and potentially expensive.
+
+ w.Write([]byte(channels.ToJson()))
+}
+
func searchChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
diff --git a/app/channel.go b/app/channel.go
index 0ac2894fb..4867908f9 100644
--- a/app/channel.go
+++ b/app/channel.go
@@ -1507,6 +1507,16 @@ func (a *App) AutocompleteChannels(teamId string, term string) (*model.ChannelLi
}
}
+func (a *App) AutocompleteChannelsForSearch(teamId string, userId string, term string) (*model.ChannelList, *model.AppError) {
+ includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
+
+ if result := <-a.Srv.Store.Channel().AutocompleteInTeamForSearch(teamId, userId, term, includeDeleted); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelList), nil
+ }
+}
+
func (a *App) SearchChannels(teamId string, term string) (*model.ChannelList, *model.AppError) {
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
diff --git a/model/client4.go b/model/client4.go
index 47d227742..c95aaad0b 100644
--- a/model/client4.go
+++ b/model/client4.go
@@ -1961,6 +1961,17 @@ func (c *Client4) AutocompleteChannelsForTeam(teamId, name string) (*ChannelList
}
}
+// AutocompleteChannelsForTeamForSearch will return an ordered list of your channels autocomplete suggestions
+func (c *Client4) AutocompleteChannelsForTeamForSearch(teamId, name string) (*ChannelList, *Response) {
+ query := fmt.Sprintf("?name=%v", name)
+ if r, err := c.DoApiGet(c.GetChannelsForTeamRoute(teamId)+"/search_autocomplete"+query, ""); err != nil {
+ return nil, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return ChannelListFromJson(r.Body), BuildResponse(r)
+ }
+}
+
// Post Section
// CreatePost creates a post based on the provided post struct.
diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go
index 1b5ae7ff7..820fe1e9f 100644
--- a/store/sqlstore/channel_store.go
+++ b/store/sqlstore/channel_store.go
@@ -1589,6 +1589,53 @@ func (s SqlChannelStore) AutocompleteInTeam(teamId string, term string, includeD
})
}
+func (s SqlChannelStore) AutocompleteInTeamForSearch(teamId string, userId string, term string, includeDeleted bool) store.StoreChannel {
+ return store.Do(func(result *store.StoreResult) {
+ deleteFilter := "AND DeleteAt = 0"
+ if includeDeleted {
+ deleteFilter = ""
+ }
+
+ queryFormat := `
+ SELECT
+ C.*
+ FROM
+ Channels AS C
+ JOIN
+ ChannelMembers AS CM ON CM.ChannelId = C.Id
+ WHERE
+ C.TeamId = :TeamId
+ AND CM.UserId = :UserId
+ ` + deleteFilter + `
+ %v
+ LIMIT 50`
+
+ var channels model.ChannelList
+
+ if likeClause, likeTerm := s.buildLIKEClause(term); likeClause == "" {
+ if _, err := s.GetReplica().Select(&channels, fmt.Sprintf(queryFormat, ""), map[string]interface{}{"TeamId": teamId, "UserId": userId}); err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.AutocompleteInTeamForSearch", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError)
+ }
+ } else {
+ // Using a UNION results in index_merge and fulltext queries and is much faster than the ref
+ // query you would get using an OR of the LIKE and full-text clauses.
+ fulltextClause, fulltextTerm := s.buildFulltextClause(term)
+ likeQuery := fmt.Sprintf(queryFormat, "AND "+likeClause)
+ fulltextQuery := fmt.Sprintf(queryFormat, "AND "+fulltextClause)
+ query := fmt.Sprintf("(%v) UNION (%v) LIMIT 50", likeQuery, fulltextQuery)
+
+ if _, err := s.GetReplica().Select(&channels, query, map[string]interface{}{"TeamId": teamId, "UserId": userId, "LikeTerm": likeTerm, "FulltextTerm": fulltextTerm}); err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.AutocompleteInTeamForSearch", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError)
+ }
+ }
+
+ sort.Slice(channels, func(a, b int) bool {
+ return strings.ToLower(channels[a].DisplayName) < strings.ToLower(channels[b].DisplayName)
+ })
+ result.Data = &channels
+ })
+}
+
func (s SqlChannelStore) SearchInTeam(teamId string, term string, includeDeleted bool) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
deleteFilter := "AND DeleteAt = 0"
diff --git a/store/store.go b/store/store.go
index 0c89a0a91..8da70d7ec 100644
--- a/store/store.go
+++ b/store/store.go
@@ -162,6 +162,7 @@ type ChannelStore interface {
AnalyticsTypeCount(teamId string, channelType string) StoreChannel
GetMembersForUser(teamId string, userId string) StoreChannel
AutocompleteInTeam(teamId string, term string, includeDeleted bool) StoreChannel
+ AutocompleteInTeamForSearch(teamId string, userId string, term string, includeDeleted bool) StoreChannel
SearchInTeam(teamId string, term string, includeDeleted bool) StoreChannel
SearchMore(userId string, teamId string, term string) StoreChannel
GetMembersByIds(channelId string, userIds []string) StoreChannel
diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go
index 776434b6e..54316d1ce 100644
--- a/store/storetest/channel_store.go
+++ b/store/storetest/channel_store.go
@@ -48,6 +48,7 @@ func TestChannelStore(t *testing.T, ss store.Store) {
t.Run("GetMemberCount", func(t *testing.T) { testGetMemberCount(t, ss) })
t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, ss) })
t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) })
+ t.Run("AutocompleteInTeamForSearch", func(t *testing.T) { testChannelStoreAutocompleteInTeamForSearch(t, ss) })
t.Run("GetMembersByIds", func(t *testing.T) { testChannelStoreGetMembersByIds(t, ss) })
t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) })
t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) })
@@ -2054,6 +2055,92 @@ func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) {
}
}
+func testChannelStoreAutocompleteInTeamForSearch(t *testing.T, ss store.Store) {
+ o1 := model.Channel{}
+ o1.TeamId = model.NewId()
+ o1.DisplayName = "ChannelA"
+ o1.Name = "zz" + model.NewId() + "b"
+ o1.Type = model.CHANNEL_OPEN
+ store.Must(ss.Channel().Save(&o1, -1))
+
+ m1 := model.ChannelMember{}
+ m1.ChannelId = o1.Id
+ m1.UserId = model.NewId()
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
+ store.Must(ss.Channel().SaveMember(&m1))
+
+ o2 := model.Channel{}
+ o2.TeamId = model.NewId()
+ o2.DisplayName = "Channel2"
+ o2.Name = "zz" + model.NewId() + "b"
+ o2.Type = model.CHANNEL_OPEN
+ store.Must(ss.Channel().Save(&o2, -1))
+
+ m2 := model.ChannelMember{}
+ m2.ChannelId = o2.Id
+ m2.UserId = m1.UserId
+ m2.NotifyProps = model.GetDefaultChannelNotifyProps()
+ store.Must(ss.Channel().SaveMember(&m2))
+
+ o3 := model.Channel{}
+ o3.TeamId = o1.TeamId
+ o3.DisplayName = "ChannelA"
+ o3.Name = "zz" + model.NewId() + "b"
+ o3.Type = model.CHANNEL_OPEN
+ store.Must(ss.Channel().Save(&o3, -1))
+
+ m3 := model.ChannelMember{}
+ m3.ChannelId = o3.Id
+ m3.UserId = m1.UserId
+ m3.NotifyProps = model.GetDefaultChannelNotifyProps()
+ store.Must(ss.Channel().SaveMember(&m3))
+
+ store.Must(ss.Channel().SetDeleteAt(o3.Id, 100, 100))
+
+ o4 := model.Channel{}
+ o4.TeamId = o1.TeamId
+ o4.DisplayName = "ChannelA"
+ o4.Name = "zz" + model.NewId() + "b"
+ o4.Type = model.CHANNEL_PRIVATE
+ store.Must(ss.Channel().Save(&o4, -1))
+
+ m4 := model.ChannelMember{}
+ m4.ChannelId = o4.Id
+ m4.UserId = m1.UserId
+ m4.NotifyProps = model.GetDefaultChannelNotifyProps()
+ store.Must(ss.Channel().SaveMember(&m4))
+
+ o5 := model.Channel{}
+ o5.TeamId = o1.TeamId
+ o5.DisplayName = "ChannelC"
+ o5.Name = "zz" + model.NewId() + "b"
+ o5.Type = model.CHANNEL_PRIVATE
+ store.Must(ss.Channel().Save(&o5, -1))
+
+ tt := []struct {
+ name string
+ term string
+ includeDeleted bool
+ expectedMatches int
+ }{
+ {"Empty search (list all)", "", false, 3},
+ {"Narrow search", "ChannelA", false, 2},
+ {"Wide search", "Cha", false, 3},
+ {"Wide search with archived channels", "Cha", true, 4},
+ {"Narrow with archived channels", "ChannelA", true, 3},
+ {"Search without results", "blarg", true, 0},
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ result := <-ss.Channel().AutocompleteInTeamForSearch(o1.TeamId, m1.UserId, "ChannelA", false)
+ require.Nil(t, result.Err)
+ channels := result.Data.(*model.ChannelList)
+ require.Len(t, *channels, 2)
+ })
+ }
+}
+
func testChannelStoreGetMembersByIds(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go
index 747a844ec..63f6bc6a9 100644
--- a/store/storetest/mocks/ChannelStore.go
+++ b/store/storetest/mocks/ChannelStore.go
@@ -61,6 +61,22 @@ func (_m *ChannelStore) AutocompleteInTeam(teamId string, term string, includeDe
return r0
}
+// AutocompleteInTeamForSearch provides a mock function with given fields: teamId, userId, term, includeDeleted
+func (_m *ChannelStore) AutocompleteInTeamForSearch(teamId string, userId string, term string, includeDeleted bool) store.StoreChannel {
+ ret := _m.Called(teamId, userId, term, includeDeleted)
+
+ var r0 store.StoreChannel
+ if rf, ok := ret.Get(0).(func(string, string, string, bool) store.StoreChannel); ok {
+ r0 = rf(teamId, userId, term, includeDeleted)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.StoreChannel)
+ }
+ }
+
+ return r0
+}
+
// ClearAllCustomRoleAssignments provides a mock function with given fields:
func (_m *ChannelStore) ClearAllCustomRoleAssignments() store.StoreChannel {
ret := _m.Called()