PLT-4403 Add server-based channel autocomplete, search and paging (#4585)

* Add more channel paging API

* Add channel paging support to client

* Add DB channel search functions

* Add API for searching more channels

* Add more channel search functionality to client

* Add API for autocompleting channels

* Add channel autocomplete functionality to the client

* Move to be deprecated APIs to their own file

* Final clean-up

* Fixes related to feedback

* Localization changes

* Add unit as suffix to timeout constants
Этот коммит содержится в:
Joram Wilander
2016-11-24 09:35:09 -05:00
коммит произвёл Harrison Healey
родитель c96ecae6da
Коммит 981ea33b8e
27 изменённых файлов: 1337 добавлений и 381 удалений

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

@@ -103,6 +103,7 @@ func InitApi() {
InitEmoji() InitEmoji()
InitStatus() InitStatus()
InitWebrtc() InitWebrtc()
InitDeprecated()
// 404 on any api route before web.go has a chance to serve it // 404 on any api route before web.go has a chance to serve it
Srv.Router.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404)) Srv.Router.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404))

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

@@ -6,6 +6,7 @@ package api
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"strings" "strings"
l4g "github.com/alecthomas/log4go" l4g "github.com/alecthomas/log4go"
@@ -19,7 +20,8 @@ func InitChannel() {
l4g.Debug(utils.T("api.channel.init.debug")) l4g.Debug(utils.T("api.channel.init.debug"))
BaseRoutes.Channels.Handle("/", ApiUserRequired(getChannels)).Methods("GET") BaseRoutes.Channels.Handle("/", ApiUserRequired(getChannels)).Methods("GET")
BaseRoutes.Channels.Handle("/more", ApiUserRequired(getMoreChannels)).Methods("GET") BaseRoutes.Channels.Handle("/more/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getMoreChannelsPage)).Methods("GET")
BaseRoutes.Channels.Handle("/more/search", ApiUserRequired(searchMoreChannels)).Methods("POST")
BaseRoutes.Channels.Handle("/counts", ApiUserRequired(getChannelCounts)).Methods("GET") BaseRoutes.Channels.Handle("/counts", ApiUserRequired(getChannelCounts)).Methods("GET")
BaseRoutes.Channels.Handle("/members", ApiUserRequired(getMyChannelMembers)).Methods("GET") BaseRoutes.Channels.Handle("/members", ApiUserRequired(getMyChannelMembers)).Methods("GET")
BaseRoutes.Channels.Handle("/create", ApiUserRequired(createChannel)).Methods("POST") BaseRoutes.Channels.Handle("/create", ApiUserRequired(createChannel)).Methods("POST")
@@ -28,6 +30,7 @@ func InitChannel() {
BaseRoutes.Channels.Handle("/update_header", ApiUserRequired(updateChannelHeader)).Methods("POST") BaseRoutes.Channels.Handle("/update_header", ApiUserRequired(updateChannelHeader)).Methods("POST")
BaseRoutes.Channels.Handle("/update_purpose", ApiUserRequired(updateChannelPurpose)).Methods("POST") BaseRoutes.Channels.Handle("/update_purpose", ApiUserRequired(updateChannelPurpose)).Methods("POST")
BaseRoutes.Channels.Handle("/update_notify_props", ApiUserRequired(updateNotifyProps)).Methods("POST") BaseRoutes.Channels.Handle("/update_notify_props", ApiUserRequired(updateNotifyProps)).Methods("POST")
BaseRoutes.Channels.Handle("/autocomplete", ApiUserRequired(autocompleteChannels)).Methods("GET")
BaseRoutes.NeedChannelName.Handle("/join", ApiUserRequired(join)).Methods("POST") BaseRoutes.NeedChannelName.Handle("/join", ApiUserRequired(join)).Methods("POST")
@@ -41,6 +44,7 @@ func InitChannel() {
BaseRoutes.NeedChannel.Handle("/remove", ApiUserRequired(removeMember)).Methods("POST") BaseRoutes.NeedChannel.Handle("/remove", ApiUserRequired(removeMember)).Methods("POST")
BaseRoutes.NeedChannel.Handle("/update_last_viewed_at", ApiUserRequired(updateLastViewedAt)).Methods("POST") BaseRoutes.NeedChannel.Handle("/update_last_viewed_at", ApiUserRequired(updateLastViewedAt)).Methods("POST")
BaseRoutes.NeedChannel.Handle("/set_last_viewed_at", ApiUserRequired(setLastViewedAt)).Methods("POST") BaseRoutes.NeedChannel.Handle("/set_last_viewed_at", ApiUserRequired(setLastViewedAt)).Methods("POST")
} }
func createChannel(c *Context, w http.ResponseWriter, r *http.Request) { func createChannel(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -416,18 +420,29 @@ func getChannels(c *Context, w http.ResponseWriter, r *http.Request) {
} }
} }
func getMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) { func getMoreChannelsPage(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
offset, err := strconv.Atoi(params["offset"])
if err != nil {
c.SetInvalidParam("getProfiles", "offset")
return
}
limit, err := strconv.Atoi(params["limit"])
if err != nil {
c.SetInvalidParam("getProfiles", "limit")
return
}
// user is already in the team // user is already in the team
if !HasPermissionToTeamContext(c, c.TeamId, model.PERMISSION_LIST_TEAM_CHANNELS) { if !HasPermissionToTeamContext(c, c.TeamId, model.PERMISSION_LIST_TEAM_CHANNELS) {
return return
} }
if result := <-Srv.Store.Channel().GetMoreChannels(c.TeamId, c.Session.UserId); result.Err != nil { if result := <-Srv.Store.Channel().GetMoreChannels(c.TeamId, c.Session.UserId, offset, limit); result.Err != nil {
c.Err = result.Err c.Err = result.Err
return return
} else if HandleEtag(result.Data.(*model.ChannelList).Etag(), w, r) {
return
} else { } else {
data := result.Data.(*model.ChannelList) data := result.Data.(*model.ChannelList)
w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag()) w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag())
@@ -1182,3 +1197,51 @@ func updateNotifyProps(c *Context, w http.ResponseWriter, r *http.Request) {
} }
} }
func searchMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.ChannelSearchFromJson(r.Body)
if props == nil {
c.SetInvalidParam("searchMoreChannels", "")
return
}
if c.Session.GetTeamByTeamId(c.TeamId) == nil {
if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
return
}
}
if len(props.Term) == 0 {
c.SetInvalidParam("searchMoreChannels", "term")
return
}
if result := <-Srv.Store.Channel().SearchMore(c.Session.UserId, c.TeamId, props.Term); result.Err != nil {
c.Err = result.Err
return
} else {
channels := result.Data.(*model.ChannelList)
w.Write([]byte(channels.ToJson()))
}
}
func autocompleteChannels(c *Context, w http.ResponseWriter, r *http.Request) {
term := r.URL.Query().Get("term")
if c.Session.GetTeamByTeamId(c.TeamId) == nil {
if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
return
}
}
var channels *model.ChannelList
if result := <-Srv.Store.Channel().SearchInTeam(c.TeamId, term); result.Err != nil {
c.Err = result.Err
return
} else {
channels = result.Data.(*model.ChannelList)
}
w.Write([]byte(channels.ToJson()))
}

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

@@ -705,7 +705,7 @@ func TestGetChannel(t *testing.T) {
} }
} }
func TestGetMoreChannel(t *testing.T) { func TestGetMoreChannelsPage(t *testing.T) {
th := Setup().InitBasic() th := Setup().InitBasic()
Client := th.BasicClient Client := th.BasicClient
team := th.BasicTeam team := th.BasicTeam
@@ -713,13 +713,23 @@ func TestGetMoreChannel(t *testing.T) {
channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
channel2 := &model.Channel{DisplayName: "B Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel2 := &model.Channel{DisplayName: "B Test API Name", Name: "b" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
channel3 := &model.Channel{DisplayName: "C Test API Name", Name: "c" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
channel3 = Client.Must(Client.CreateChannel(channel3)).Data.(*model.Channel)
th.LoginBasic2() th.LoginBasic2()
rget := Client.Must(Client.GetMoreChannels("")) if r, err := Client.GetMoreChannelsPage(0, 100); err != nil {
channels := rget.Data.(*model.ChannelList) t.Fatal(err)
} else {
channels := r.Data.(*model.ChannelList)
// 1 for BasicChannel, 2 for open channels created above
if len(*channels) != 3 {
t.Fatal("wrong length")
}
if (*channels)[0].DisplayName != channel1.DisplayName { if (*channels)[0].DisplayName != channel1.DisplayName {
t.Fatal("full name didn't match") t.Fatal("full name didn't match")
@@ -728,13 +738,39 @@ func TestGetMoreChannel(t *testing.T) {
if (*channels)[1].DisplayName != channel2.DisplayName { if (*channels)[1].DisplayName != channel2.DisplayName {
t.Fatal("full name didn't match") t.Fatal("full name didn't match")
} }
}
// test etag caching if r, err := Client.GetMoreChannelsPage(0, 1); err != nil {
if cache_result, err := Client.GetMoreChannels(rget.Etag); err != nil {
t.Fatal(err) t.Fatal(err)
} else if cache_result.Data.(*model.ChannelList) != nil { } else {
t.Log(cache_result.Data) channels := r.Data.(*model.ChannelList)
t.Fatal("cache should be empty")
if len(*channels) != 1 {
t.Fatal("wrong length")
}
if (*channels)[0].DisplayName != channel1.DisplayName {
t.Fatal("full name didn't match")
}
}
if r, err := Client.GetMoreChannelsPage(1, 1); err != nil {
t.Fatal(err)
} else {
channels := r.Data.(*model.ChannelList)
if len(*channels) != 1 {
t.Fatal("wrong length")
}
if (*channels)[0].DisplayName != channel2.DisplayName {
t.Fatal("full name didn't match")
}
}
Client.SetTeamId("junk")
if _, err := Client.GetMoreChannelsPage(0, 1); err == nil {
t.Fatal("should have failed - bad team id")
} }
} }
@@ -1448,3 +1484,148 @@ func TestGetChannelMember(t *testing.T) {
t.Fatal("should have failed - bad channel and user id") t.Fatal("should have failed - bad channel and user id")
} }
} }
func TestSearchMoreChannels(t *testing.T) {
th := Setup().InitBasic()
Client := th.BasicClient
team := th.BasicTeam
channel1 := &model.Channel{DisplayName: "TestAPINameA", 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: "TestAPINameB", Name: "b" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
th.LoginBasic2()
if result, err := Client.SearchMoreChannels(model.ChannelSearch{Term: "TestAPIName"}); err != nil {
t.Fatal(err)
} else {
channels := result.Data.(*model.ChannelList)
if (*channels)[0].DisplayName != channel1.DisplayName {
t.Fatal("full name didn't match")
}
if (*channels)[1].DisplayName != channel2.DisplayName {
t.Fatal("full name didn't match")
}
}
if result, err := Client.SearchMoreChannels(model.ChannelSearch{Term: "TestAPINameA"}); err != nil {
t.Fatal(err)
} else {
channels := result.Data.(*model.ChannelList)
if (*channels)[0].DisplayName != channel1.DisplayName {
t.Fatal("full name didn't match")
}
}
if result, err := Client.SearchMoreChannels(model.ChannelSearch{Term: "TestAPINameB"}); err != nil {
t.Fatal(err)
} else {
channels := result.Data.(*model.ChannelList)
if (*channels)[0].DisplayName != channel2.DisplayName {
t.Fatal("full name didn't match")
}
}
if result, err := Client.SearchMoreChannels(model.ChannelSearch{Term: channel1.Name}); err != nil {
t.Fatal(err)
} else {
channels := result.Data.(*model.ChannelList)
if (*channels)[0].DisplayName != channel1.DisplayName {
t.Fatal("full name didn't match")
}
}
if _, err := Client.SearchMoreChannels(model.ChannelSearch{Term: ""}); err == nil {
t.Fatal("should have errored - empty term")
}
if result, err := Client.SearchMoreChannels(model.ChannelSearch{Term: "blargh"}); err != nil {
t.Fatal(err)
} else {
channels := result.Data.(*model.ChannelList)
if len(*channels) != 0 {
t.Fatal("should have no channels")
}
}
Client.SetTeamId("junk")
if _, err := Client.SearchMoreChannels(model.ChannelSearch{Term: "blargh"}); err == nil {
t.Fatal("should have errored - bad team id")
}
}
func TestAutocompleteChannels(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
Client := th.BasicClient
team := th.BasicTeam
channel1 := &model.Channel{DisplayName: "TestAPINameA", 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: "TestAPINameB", Name: "b" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
channel3 := &model.Channel{DisplayName: "BadChannelC", Name: "c" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: model.NewId()}
channel3 = th.SystemAdminClient.Must(th.SystemAdminClient.CreateChannel(channel3)).Data.(*model.Channel)
channel4 := &model.Channel{DisplayName: "BadChannelD", Name: "d" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
channel4 = Client.Must(Client.CreateChannel(channel4)).Data.(*model.Channel)
if result, err := Client.AutocompleteChannels("TestAPIName"); err != nil {
t.Fatal(err)
} else {
channels := result.Data.(*model.ChannelList)
if (*channels)[0].DisplayName != channel1.DisplayName {
t.Fatal("full name didn't match")
}
if (*channels)[1].DisplayName != channel2.DisplayName {
t.Fatal("full name didn't match")
}
}
if result, err := Client.AutocompleteChannels(channel1.Name); err != nil {
t.Fatal(err)
} else {
channels := result.Data.(*model.ChannelList)
if (*channels)[0].DisplayName != channel1.DisplayName {
t.Fatal("full name didn't match")
}
}
if result, err := Client.AutocompleteChannels("BadChannelC"); err != nil {
t.Fatal(err)
} else {
channels := result.Data.(*model.ChannelList)
if len(*channels) != 0 {
t.Fatal("should have been empty")
}
}
if result, err := Client.AutocompleteChannels("BadChannelD"); err != nil {
t.Fatal(err)
} else {
channels := result.Data.(*model.ChannelList)
if len(*channels) != 0 {
t.Fatal("should have been empty")
}
}
Client.SetTeamId("junk")
if _, err := Client.AutocompleteChannels("BadChannelD"); err == nil {
t.Fatal("should have failed - bad team id")
}
}

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

@@ -33,25 +33,22 @@ func (me *JoinProvider) GetCommand(c *Context) *model.Command {
} }
func (me *JoinProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse { func (me *JoinProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse {
if result := <-Srv.Store.Channel().GetMoreChannels(c.TeamId, c.Session.UserId); result.Err != nil { if result := <-Srv.Store.Channel().GetByName(c.TeamId, message); result.Err != nil {
return &model.CommandResponse{Text: c.T("api.command_join.list.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} return &model.CommandResponse{Text: c.T("api.command_join.list.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
} else { } else {
channels := result.Data.(*model.ChannelList) channel := result.Data.(*model.Channel)
for _, v := range *channels { if channel.Name == message {
if v.Name == message { if channel.Type != model.CHANNEL_OPEN {
if v.Type != model.CHANNEL_OPEN {
return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
} }
if err, _ := JoinChannelById(c, c.Session.UserId, v.Id); err != nil { if err, _ := JoinChannelById(c, c.Session.UserId, channel.Id); err != nil {
return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
} }
return &model.CommandResponse{GotoLocation: c.GetTeamURL() + "/channels/" + v.Name, Text: c.T("api.command_join.success"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} return &model.CommandResponse{GotoLocation: c.GetTeamURL() + "/channels/" + channel.Name, Text: c.T("api.command_join.success"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
}
} }
} }

39
api/deprecated.go Обычный файл
Просмотреть файл

@@ -0,0 +1,39 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"net/http"
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
// ONLY FOR APIs SCHEDULED TO BE DEPRECATED
func InitDeprecated() {
l4g.Debug(utils.T("api.channel.init.debug"))
BaseRoutes.Channels.Handle("/more", ApiUserRequired(getMoreChannels)).Methods("GET") // SCHEDULED FOR DEPRECATION IN 3.7
}
func getMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) {
// user is already in the team
if !HasPermissionToTeamContext(c, c.TeamId, model.PERMISSION_LIST_TEAM_CHANNELS) {
return
}
if result := <-Srv.Store.Channel().GetMoreChannels(c.TeamId, c.Session.UserId, 0, 100000); result.Err != nil {
c.Err = result.Err
return
} else if HandleEtag(result.Data.(*model.ChannelList).Etag(), w, r) {
return
} else {
data := result.Data.(*model.ChannelList)
w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag())
w.Write([]byte(data.ToJson()))
}
}

43
api/deprecated_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,43 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"testing"
"github.com/mattermost/platform/model"
)
func TestGetMoreChannel(t *testing.T) {
th := Setup().InitBasic()
Client := th.BasicClient
team := th.BasicTeam
channel1 := &model.Channel{DisplayName: "A Test API Name", 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: "B Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
th.LoginBasic2()
rget := Client.Must(Client.GetMoreChannels(""))
channels := rget.Data.(*model.ChannelList)
if (*channels)[0].DisplayName != channel1.DisplayName {
t.Fatal("full name didn't match")
}
if (*channels)[1].DisplayName != channel2.DisplayName {
t.Fatal("full name didn't match")
}
// test etag caching
if cache_result, err := Client.GetMoreChannels(rget.Etag); err != nil {
t.Fatal(err)
} else if cache_result.Data.(*model.ChannelList) != nil {
t.Log(cache_result.Data)
t.Fatal("cache should be empty")
}
}

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

@@ -2712,8 +2712,8 @@ func autocompleteUsersInChannel(c *Context, w http.ResponseWriter, r *http.Reque
searchOptions := map[string]bool{} searchOptions := map[string]bool{}
searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true
uchan := Srv.Store.User().SearchInChannel(channelId, term, map[string]bool{}) uchan := Srv.Store.User().SearchInChannel(channelId, term, searchOptions)
nuchan := Srv.Store.User().SearchNotInChannel(teamId, channelId, term, map[string]bool{}) nuchan := Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions)
autocomplete := &model.UserAutocompleteInChannel{} autocomplete := &model.UserAutocompleteInChannel{}
@@ -2758,7 +2758,10 @@ func autocompleteUsersInTeam(c *Context, w http.ResponseWriter, r *http.Request)
} }
} }
uchan := Srv.Store.User().Search(teamId, term, map[string]bool{}) searchOptions := map[string]bool{}
searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true
uchan := Srv.Store.User().Search(teamId, term, searchOptions)
autocomplete := &model.UserAutocompleteInTeam{} autocomplete := &model.UserAutocompleteInTeam{}

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

@@ -4035,6 +4035,10 @@
"id": "store.sql_channel.analytics_type_count.app_error", "id": "store.sql_channel.analytics_type_count.app_error",
"translation": "We couldn't get channel type counts" "translation": "We couldn't get channel type counts"
}, },
{
"id": "store.sql_channel.search.app_error",
"translation": "We encountered an error searching channels"
},
{ {
"id": "store.sql_channel.check_open_channel_permissions.app_error", "id": "store.sql_channel.check_open_channel_permissions.app_error",
"translation": "We couldn't check the permissions" "translation": "We couldn't check the permissions"

35
model/channel_search.go Обычный файл
Просмотреть файл

@@ -0,0 +1,35 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type ChannelSearch struct {
Term string `json:"term"`
}
// ToJson convert a Channel to a json string
func (c *ChannelSearch) ToJson() string {
b, err := json.Marshal(c)
if err != nil {
return ""
} else {
return string(b)
}
}
// ChannelSearchFromJson will decode the input and return a Channel
func ChannelSearchFromJson(data io.Reader) *ChannelSearch {
decoder := json.NewDecoder(data)
var cs ChannelSearch
err := decoder.Decode(&cs)
if err == nil {
return &cs
} else {
return nil
}
}

19
model/channel_search_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,19 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"strings"
"testing"
)
func TestChannelSearchJson(t *testing.T) {
channelSearch := ChannelSearch{Term: NewId()}
json := channelSearch.ToJson()
rchannelSearch := ChannelSearchFromJson(strings.NewReader(json))
if channelSearch.Term != rchannelSearch.Term {
t.Fatal("Terms do not match")
}
}

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

@@ -1164,6 +1164,7 @@ func (c *Client) GetChannel(id, etag string) (*Result, *AppError) {
} }
} }
// SCHEDULED FOR DEPRECATION IN 3.7 - use GetMoreChannelsPage instead
func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) { func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/more", "", etag); err != nil { if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/more", "", etag); err != nil {
return nil, err return nil, err
@@ -1174,6 +1175,43 @@ func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) {
} }
} }
// GetMoreChannelsPage will return a page of open channels the user is not in based on
// the provided offset and limit. Must be authenticated.
func (c *Client) GetMoreChannelsPage(offset int, limit int) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf(c.GetTeamRoute()+"/channels/more/%v/%v", offset, limit), "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil
}
}
// SearchMoreChannels will return a list of open channels the user is not in, that matches
// the search criteria provided. Must be authenticated.
func (c *Client) SearchMoreChannels(channelSearch ChannelSearch) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/more/search", channelSearch.ToJson()); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil
}
}
// AutocompleteChannels will return a list of open channels that match the provided
// string. Must be authenticated.
func (c *Client) AutocompleteChannels(term string) (*Result, *AppError) {
url := fmt.Sprintf("%s/channels/autocomplete?term=%s", c.GetTeamRoute(), url.QueryEscape(term))
if r, err := c.DoApiGet(url, "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil
}
}
func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) { func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/counts", "", etag); err != nil { if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/counts", "", etag); err != nil {
return nil, err return nil, err

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

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

@@ -5,6 +5,8 @@ package store
import ( import (
"database/sql" "database/sql"
"fmt"
"strings"
l4g "github.com/alecthomas/log4go" l4g "github.com/alecthomas/log4go"
"github.com/go-gorp/gorp" "github.com/go-gorp/gorp"
@@ -65,6 +67,8 @@ func (s SqlChannelStore) CreateIndexesIfNotExists() {
s.CreateIndexIfNotExists("idx_channelmembers_channel_id", "ChannelMembers", "ChannelId") s.CreateIndexIfNotExists("idx_channelmembers_channel_id", "ChannelMembers", "ChannelId")
s.CreateIndexIfNotExists("idx_channelmembers_user_id", "ChannelMembers", "UserId") s.CreateIndexIfNotExists("idx_channelmembers_user_id", "ChannelMembers", "UserId")
s.CreateFullTextIndexIfNotExists("idx_channels_txt", "Channels", "Name, DisplayName")
} }
func (s SqlChannelStore) Save(channel *model.Channel) StoreChannel { func (s SqlChannelStore) Save(channel *model.Channel) StoreChannel {
@@ -392,7 +396,7 @@ func (s SqlChannelStore) GetChannels(teamId string, userId string) StoreChannel
return storeChannel return storeChannel
} }
func (s SqlChannelStore) GetMoreChannels(teamId string, userId string) StoreChannel { func (s SqlChannelStore) GetMoreChannels(teamId string, userId string, offset int, limit int) StoreChannel {
storeChannel := make(StoreChannel, 1) storeChannel := make(StoreChannel, 1)
go func() { go func() {
@@ -418,8 +422,10 @@ func (s SqlChannelStore) GetMoreChannels(teamId string, userId string) StoreChan
AND TeamId = :TeamId2 AND TeamId = :TeamId2
AND UserId = :UserId AND UserId = :UserId
AND DeleteAt = 0) AND DeleteAt = 0)
ORDER BY DisplayName`, ORDER BY DisplayName
map[string]interface{}{"TeamId1": teamId, "TeamId2": teamId, "UserId": userId}) LIMIT :Limit
OFFSET :Offset`,
map[string]interface{}{"TeamId1": teamId, "TeamId2": teamId, "UserId": userId, "Limit": limit, "Offset": offset})
if err != nil { if err != nil {
result.Err = model.NewLocAppError("SqlChannelStore.GetMoreChannels", "store.sql_channel.get_more_channels.get.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error()) result.Err = model.NewLocAppError("SqlChannelStore.GetMoreChannels", "store.sql_channel.get_more_channels.get.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error())
@@ -1104,3 +1110,110 @@ func (s SqlChannelStore) GetMembersForUser(teamId string, userId string) StoreCh
return storeChannel return storeChannel
} }
func (s SqlChannelStore) SearchInTeam(teamId string, term string) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
searchQuery := `
SELECT
*
FROM
Channels
WHERE
TeamId = :TeamId
AND Type = 'O'
AND DeleteAt = 0
SEARCH_CLAUSE
ORDER BY DisplayName
LIMIT 100`
storeChannel <- s.performSearch(searchQuery, term, map[string]interface{}{"TeamId": teamId})
close(storeChannel)
}()
return storeChannel
}
func (s SqlChannelStore) SearchMore(userId string, teamId string, term string) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
searchQuery := `
SELECT
*
FROM
Channels
WHERE
TeamId = :TeamId
AND Type = 'O'
AND DeleteAt = 0
AND Id NOT IN (SELECT
Channels.Id
FROM
Channels,
ChannelMembers
WHERE
Id = ChannelId
AND TeamId = :TeamId
AND UserId = :UserId
AND DeleteAt = 0)
SEARCH_CLAUSE
ORDER BY DisplayName
LIMIT 100`
storeChannel <- s.performSearch(searchQuery, term, map[string]interface{}{"TeamId": teamId, "UserId": userId})
close(storeChannel)
}()
return storeChannel
}
func (s SqlChannelStore) performSearch(searchQuery string, term string, parameters map[string]interface{}) StoreResult {
result := StoreResult{}
// these chars have special meaning and can be treated as spaces
for _, c := range specialSearchChar {
term = strings.Replace(term, c, " ", -1)
}
if term == "" {
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1)
} else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
splitTerm := strings.Fields(term)
for i, t := range strings.Fields(term) {
if i == len(splitTerm)-1 {
splitTerm[i] = t + ":*"
} else {
splitTerm[i] = t + ":* &"
}
}
term = strings.Join(splitTerm, " ")
searchClause := fmt.Sprintf("AND (%s) @@ to_tsquery('simple', :Term)", "Name || ' ' || DisplayName")
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
} else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
splitTerm := strings.Fields(term)
for i, t := range strings.Fields(term) {
splitTerm[i] = "+" + t + "*"
}
term = strings.Join(splitTerm, " ")
searchClause := fmt.Sprintf("AND MATCH(%s) AGAINST (:Term IN BOOLEAN MODE)", "Name, DisplayName")
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
}
var channels model.ChannelList
parameters["Term"] = term
if _, err := s.GetReplica().Select(&channels, searchQuery, parameters); err != nil {
result.Err = model.NewLocAppError("SqlChannelStore.Search", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error())
} else {
result.Data = &channels
}
return result
}

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

@@ -309,7 +309,7 @@ func TestChannelStoreDelete(t *testing.T) {
t.Fatal("invalid number of channels") t.Fatal("invalid number of channels")
} }
cresult = <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId) cresult = <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 0, 100)
list = cresult.Data.(*model.ChannelList) list = cresult.Data.(*model.ChannelList)
if len(*list) != 1 { if len(*list) != 1 {
@@ -621,7 +621,10 @@ func TestChannelStoreGetMoreChannels(t *testing.T) {
o5.Type = model.CHANNEL_PRIVATE o5.Type = model.CHANNEL_PRIVATE
Must(store.Channel().Save(&o5)) Must(store.Channel().Save(&o5))
cresult := <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId) cresult := <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 0, 100)
if cresult.Err != nil {
t.Fatal(cresult.Err)
}
list := cresult.Data.(*model.ChannelList) list := cresult.Data.(*model.ChannelList)
if len(*list) != 1 { if len(*list) != 1 {
@@ -632,10 +635,38 @@ func TestChannelStoreGetMoreChannels(t *testing.T) {
t.Fatal("missing channel") t.Fatal("missing channel")
} }
o6 := model.Channel{}
o6.TeamId = o1.TeamId
o6.DisplayName = "ChannelA"
o6.Name = "a" + model.NewId() + "b"
o6.Type = model.CHANNEL_OPEN
Must(store.Channel().Save(&o6))
cresult = <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 0, 100)
list = cresult.Data.(*model.ChannelList)
if len(*list) != 2 {
t.Fatal("wrong list length")
}
cresult = <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 0, 1)
list = cresult.Data.(*model.ChannelList)
if len(*list) != 1 {
t.Fatal("wrong list length")
}
cresult = <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 1, 1)
list = cresult.Data.(*model.ChannelList)
if len(*list) != 1 {
t.Fatal("wrong list length")
}
if r1 := <-store.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_OPEN); r1.Err != nil { if r1 := <-store.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_OPEN); r1.Err != nil {
t.Fatal(r1.Err) t.Fatal(r1.Err)
} else { } else {
if r1.Data.(int64) != 2 { if r1.Data.(int64) != 3 {
t.Log(r1.Data) t.Log(r1.Data)
t.Fatal("wrong value") t.Fatal("wrong value")
} }
@@ -1055,3 +1086,180 @@ func TestUpdateExtrasByUser(t *testing.T) {
t.Fatal("failed to update extras by user: %v", result.Err) t.Fatal("failed to update extras by user: %v", result.Err)
} }
} }
func TestChannelStoreSearchMore(t *testing.T) {
Setup()
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "ChannelA"
o1.Name = "a" + model.NewId() + "b"
o1.Type = model.CHANNEL_OPEN
Must(store.Channel().Save(&o1))
o2 := model.Channel{}
o2.TeamId = model.NewId()
o2.DisplayName = "Channel2"
o2.Name = "a" + model.NewId() + "b"
o2.Type = model.CHANNEL_OPEN
Must(store.Channel().Save(&o2))
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2))
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = model.NewId()
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m3))
o3 := model.Channel{}
o3.TeamId = o1.TeamId
o3.DisplayName = "ChannelA"
o3.Name = "a" + model.NewId() + "b"
o3.Type = model.CHANNEL_OPEN
Must(store.Channel().Save(&o3))
o4 := model.Channel{}
o4.TeamId = o1.TeamId
o4.DisplayName = "ChannelB"
o4.Name = "a" + model.NewId() + "b"
o4.Type = model.CHANNEL_PRIVATE
Must(store.Channel().Save(&o4))
o5 := model.Channel{}
o5.TeamId = o1.TeamId
o5.DisplayName = "ChannelC"
o5.Name = "a" + model.NewId() + "b"
o5.Type = model.CHANNEL_PRIVATE
Must(store.Channel().Save(&o5))
if result := <-store.Channel().SearchMore(m1.UserId, o1.TeamId, "ChannelA"); result.Err != nil {
t.Fatal(result.Err)
} else {
channels := result.Data.(*model.ChannelList)
if len(*channels) == 0 {
t.Fatal("should not be empty")
}
if (*channels)[0].Name != o3.Name {
t.Fatal("wrong channel returned")
}
}
if result := <-store.Channel().SearchMore(m1.UserId, o1.TeamId, o4.Name); result.Err != nil {
t.Fatal(result.Err)
} else {
channels := result.Data.(*model.ChannelList)
if len(*channels) != 0 {
t.Fatal("should be empty")
}
}
if result := <-store.Channel().SearchMore(m1.UserId, o1.TeamId, o3.Name); result.Err != nil {
t.Fatal(result.Err)
} else {
channels := result.Data.(*model.ChannelList)
if len(*channels) == 0 {
t.Fatal("should not be empty")
}
if (*channels)[0].Name != o3.Name {
t.Fatal("wrong channel returned")
}
}
}
func TestChannelStoreSearchInTeam(t *testing.T) {
Setup()
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "ChannelA"
o1.Name = "a" + model.NewId() + "b"
o1.Type = model.CHANNEL_OPEN
Must(store.Channel().Save(&o1))
o2 := model.Channel{}
o2.TeamId = model.NewId()
o2.DisplayName = "Channel2"
o2.Name = "a" + model.NewId() + "b"
o2.Type = model.CHANNEL_OPEN
Must(store.Channel().Save(&o2))
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2))
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = model.NewId()
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m3))
o3 := model.Channel{}
o3.TeamId = o1.TeamId
o3.DisplayName = "ChannelA"
o3.Name = "a" + model.NewId() + "b"
o3.Type = model.CHANNEL_OPEN
Must(store.Channel().Save(&o3))
o4 := model.Channel{}
o4.TeamId = o1.TeamId
o4.DisplayName = "ChannelB"
o4.Name = "a" + model.NewId() + "b"
o4.Type = model.CHANNEL_PRIVATE
Must(store.Channel().Save(&o4))
o5 := model.Channel{}
o5.TeamId = o1.TeamId
o5.DisplayName = "ChannelC"
o5.Name = "a" + model.NewId() + "b"
o5.Type = model.CHANNEL_PRIVATE
Must(store.Channel().Save(&o5))
if result := <-store.Channel().SearchInTeam(o1.TeamId, "ChannelA"); result.Err != nil {
t.Fatal(result.Err)
} else {
channels := result.Data.(*model.ChannelList)
if len(*channels) != 2 {
t.Fatal("wrong length")
}
}
if result := <-store.Channel().SearchInTeam(o1.TeamId, ""); result.Err != nil {
t.Fatal(result.Err)
} else {
channels := result.Data.(*model.ChannelList)
if len(*channels) == 0 {
t.Fatal("should not be empty")
}
}
if result := <-store.Channel().SearchInTeam(o1.TeamId, "blargh"); result.Err != nil {
t.Fatal(result.Err)
} else {
channels := result.Data.(*model.ChannelList)
if len(*channels) != 0 {
t.Fatal("should be empty")
}
}
}

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

@@ -1120,7 +1120,7 @@ func (us SqlUserStore) Search(teamId string, term string, options map[string]boo
SEARCH_CLAUSE SEARCH_CLAUSE
INACTIVE_CLAUSE INACTIVE_CLAUSE
ORDER BY Username ASC ORDER BY Username ASC
LIMIT 50` LIMIT 100`
} else { } else {
searchQuery = ` searchQuery = `
SELECT SELECT
@@ -1264,7 +1264,7 @@ func (us SqlUserStore) performSearch(searchQuery string, term string, options ma
term = strings.Join(splitTerm, " ") term = strings.Join(splitTerm, " ")
searchType = convertMySQLFullTextColumnsToPostgres(searchType) searchType = convertMySQLFullTextColumnsToPostgres(searchType)
searchClause := fmt.Sprintf("AND (%s) @@ to_tsquery(:Term)", searchType) searchClause := fmt.Sprintf("AND (%s) @@ to_tsquery('simple', :Term)", searchType)
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1) searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
} else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL { } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
splitTerm := strings.Fields(term) splitTerm := strings.Fields(term)

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

@@ -90,7 +90,7 @@ type ChannelStore interface {
PermanentDeleteByTeam(teamId string) StoreChannel PermanentDeleteByTeam(teamId string) StoreChannel
GetByName(team_id string, domain string) StoreChannel GetByName(team_id string, domain string) StoreChannel
GetChannels(teamId string, userId string) StoreChannel GetChannels(teamId string, userId string) StoreChannel
GetMoreChannels(teamId string, userId string) StoreChannel GetMoreChannels(teamId string, userId string, offset int, limit int) StoreChannel
GetChannelCounts(teamId string, userId string) StoreChannel GetChannelCounts(teamId string, userId string) StoreChannel
GetTeamChannels(teamId string) StoreChannel GetTeamChannels(teamId string) StoreChannel
GetAll(teamId string) StoreChannel GetAll(teamId string) StoreChannel
@@ -113,6 +113,8 @@ type ChannelStore interface {
AnalyticsTypeCount(teamId string, channelType string) StoreChannel AnalyticsTypeCount(teamId string, channelType string) StoreChannel
ExtraUpdateByUser(userId string, time int64) StoreChannel ExtraUpdateByUser(userId string, time int64) StoreChannel
GetMembersForUser(teamId string, userId string) StoreChannel GetMembersForUser(teamId string, userId string) StoreChannel
SearchInTeam(teamId string, term string) StoreChannel
SearchMore(userId string, teamId string, term string) StoreChannel
} }
type PostStore interface { type PostStore interface {

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

@@ -191,3 +191,55 @@ export function loadChannelsForCurrentUser() {
AsyncClient.getChannels(); AsyncClient.getChannels();
AsyncClient.getMyChannelMembers(); AsyncClient.getMyChannelMembers();
} }
export function joinChannel(channel, success, error) {
Client.joinChannel(
channel.id,
() => {
ChannelStore.removeMoreChannel(channel.id);
if (success) {
success();
}
},
() => {
if (error) {
error();
}
}
);
}
export function searchMoreChannels(term, success, error) {
Client.searchMoreChannels(
term,
(data) => {
if (success) {
success(data);
}
},
(err) => {
if (error) {
error(err);
}
}
);
}
export function autocompleteChannels(term, success, error) {
Client.autocompleteChannels(
term,
(data) => {
if (success) {
success(data);
}
},
(err) => {
AsyncClient.dispatchError(err, 'autocompleteChannels');
if (error) {
error(err);
}
}
);
}

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

@@ -482,14 +482,6 @@ export function emitUserLoggedOutEvent(redirectTo = '/', shouldSignalLogout = tr
); );
} }
export function emitJoinChannelEvent(channel, success, failure) {
Client.joinChannel(
channel.id,
success,
failure
);
}
export function emitSearchMentionsEvent(user) { export function emitSearchMentionsEvent(user) {
let terms = ''; let terms = '';
if (user.notify_props && user.notify_props.mention_keys) { if (user.notify_props && user.notify_props.mention_keys) {

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

@@ -1362,6 +1362,7 @@ export default class Client {
this.track('api', 'api_channel_get'); this.track('api', 'api_channel_get');
} }
// SCHEDULED FOR DEPRECATION IN 3.7 - use getMoreChannelsPage instead
getMoreChannels(success, error) { getMoreChannels(success, error) {
request. request.
get(`${this.getChannelsRoute()}/more`). get(`${this.getChannelsRoute()}/more`).
@@ -1371,6 +1372,34 @@ export default class Client {
end(this.handleResponse.bind(this, 'getMoreChannels', success, error)); end(this.handleResponse.bind(this, 'getMoreChannels', success, error));
} }
getMoreChannelsPage(offset, limit, success, error) {
request.
get(`${this.getChannelsRoute()}/more/${offset}/${limit}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getMoreChannelsPage', success, error));
}
searchMoreChannels(term, success, error) {
request.
post(`${this.getChannelsRoute()}/more/search`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
send({term}).
end(this.handleResponse.bind(this, 'searchMoreChannels', success, error));
}
autocompleteChannels(term, success, error) {
request.
get(`${this.getChannelsRoute()}/autocomplete?term=${encodeURIComponent(term)}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'autocompleteChannels', success, error));
}
getChannelCounts(success, error) { getChannelCounts(success, error) {
request. request.
get(`${this.getChannelsRoute()}/counts`). get(`${this.getChannelsRoute()}/counts`).

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

@@ -1,180 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import $ from 'jquery';
import ReactDOM from 'react-dom';
import * as UserAgent from 'utils/user_agent.jsx';
import {localizeMessage} from 'utils/utils.jsx';
import {FormattedMessage} from 'react-intl';
import React from 'react';
import loadingGif from 'images/load.gif';
export default class FilteredChannelList extends React.Component {
constructor(props) {
super(props);
this.handleFilterChange = this.handleFilterChange.bind(this);
this.createChannelRow = this.createChannelRow.bind(this);
this.filterChannels = this.filterChannels.bind(this);
this.state = {
filter: '',
joiningChannel: '',
channels: this.filterChannels(props.channels)
};
}
componentWillReceiveProps(nextProps) {
// assume the channel list is immutable
if (this.props.channels !== nextProps.channels) {
this.setState({
channels: this.filterChannels(nextProps.channels)
});
}
}
componentDidMount() {
// only focus the search box on desktop so that we don't cause the keyboard to open on mobile
if (!UserAgent.isMobile()) {
ReactDOM.findDOMNode(this.refs.filter).focus();
}
}
componentDidUpdate(prevProps, prevState) {
if (prevState.filter !== this.state.filter) {
$(ReactDOM.findDOMNode(this.refs.channelList)).scrollTop(0);
}
}
handleJoin(channel) {
this.setState({joiningChannel: channel.id});
this.props.handleJoin(
channel,
() => {
this.setState({joiningChannel: ''});
});
}
createChannelRow(channel) {
let joinButton;
if (this.state.joiningChannel === channel.id) {
joinButton = (
<img
className='join-channel-loading-gif'
src={loadingGif}
/>
);
} else {
joinButton = (
<button
onClick={this.handleJoin.bind(this, channel)}
className='btn btn-primary'
>
<FormattedMessage
id='more_channels.join'
defaultMessage='Join'
/>
</button>
);
}
return (
<div
className='more-modal__row'
key={channel.id}
>
<div className='more-modal__details'>
<p className='more-modal__name'>{channel.display_name}</p>
<p className='more-modal__description'>{channel.purpose}</p>
</div>
<div className='more-modal__actions'>
{joinButton}
</div>
</div>
);
}
filterChannels(channels) {
if (!this.state || !this.state.filter) {
return channels;
}
return channels.filter((chan) => {
const filter = this.state.filter.toLowerCase();
return Boolean((chan.name.toLowerCase().indexOf(filter) !== -1 || chan.display_name.toLowerCase().indexOf(filter) !== -1) && chan.delete_at === 0);
});
}
handleFilterChange(e) {
this.setState({
filter: e.target.value
});
}
render() {
let channels = this.state.channels;
if (this.state.filter && this.state.filter.length > 0) {
channels = this.filterChannels(channels);
}
let count;
if (channels.length === this.props.channels.length) {
count = (
<FormattedMessage
id='filtered_channels_list.count'
defaultMessage='{count} {count, plural, =0 {0 channels} one {channel} other {channels}}'
values={{
count: channels.length
}}
/>
);
} else {
count = (
<FormattedMessage
id='filtered_channels_list.countTotal'
defaultMessage='{count} {count, plural, =0 {0 channels} one {channel} other {channels}} of {total} Total'
values={{
count: channels.length,
total: this.props.channels.length
}}
/>
);
}
return (
<div className='filtered-user-list'>
<div className='filter-row'>
<div className='col-sm-6'>
<input
ref='filter'
className='form-control filter-textbox'
placeholder={localizeMessage('filtered_channels_list.search', 'Search channels')}
onInput={this.handleFilterChange}
/>
</div>
<div className='col-sm-12'>
<span className='channel-count pull-left'>{count}</span>
</div>
</div>
<div
ref='channelList'
className='more-modal__list'
>
{channels.map(this.createChannelRow)}
</div>
</div>
);
}
}
FilteredChannelList.defaultProps = {
channels: []
};
FilteredChannelList.propTypes = {
channels: React.PropTypes.arrayOf(React.PropTypes.object),
handleJoin: React.PropTypes.func.isRequired
};

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

@@ -2,18 +2,16 @@
// See License.txt for license information. // See License.txt for license information.
import $ from 'jquery'; import $ from 'jquery';
import LoadingScreen from './loading_screen.jsx';
import NewChannelFlow from './new_channel_flow.jsx'; import NewChannelFlow from './new_channel_flow.jsx';
import FilteredChannelList from './filtered_channel_list.jsx'; import SearchableChannelList from './searchable_channel_list.jsx';
import ChannelStore from 'stores/channel_store.jsx'; import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx'; import TeamStore from 'stores/team_store.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
import * as AsyncClient from 'utils/async_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx';
import * as GlobalActions from 'actions/global_actions.jsx'; import {joinChannel, searchMoreChannels} from 'actions/channel_actions.jsx';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6'; import {browserHistory} from 'react-router/es6';
@@ -21,19 +19,28 @@ import {browserHistory} from 'react-router/es6';
import React from 'react'; import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin'; import PureRenderMixin from 'react-addons-pure-render-mixin';
const CHANNELS_CHUNK_SIZE = 50;
const CHANNELS_PER_PAGE = 50;
const SEARCH_TIMEOUT_MILLISECONDS = 100;
export default class MoreChannels extends React.Component { export default class MoreChannels extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.onListenerChange = this.onListenerChange.bind(this); this.onChange = this.onChange.bind(this);
this.handleJoin = this.handleJoin.bind(this); this.handleJoin = this.handleJoin.bind(this);
this.handleNewChannel = this.handleNewChannel.bind(this); this.handleNewChannel = this.handleNewChannel.bind(this);
this.nextPage = this.nextPage.bind(this);
this.search = this.search.bind(this);
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
this.searchTimeoutId = 0;
this.state = { this.state = {
channelType: '', channelType: '',
showNewChannelModal: false, showNewChannelModal: false,
search: false,
channels: null, channels: null,
serverError: null serverError: null
}; };
@@ -41,10 +48,10 @@ export default class MoreChannels extends React.Component {
componentDidMount() { componentDidMount() {
const self = this; const self = this;
ChannelStore.addChangeListener(this.onListenerChange); ChannelStore.addChangeListener(this.onChange);
$(this.refs.modal).on('shown.bs.modal', () => { $(this.refs.modal).on('shown.bs.modal', () => {
AsyncClient.getMoreChannels(true); AsyncClient.getMoreChannelsPage(0, CHANNELS_CHUNK_SIZE * 2);
}); });
$(this.refs.modal).on('show.bs.modal', (e) => { $(this.refs.modal).on('show.bs.modal', (e) => {
@@ -54,25 +61,26 @@ export default class MoreChannels extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange); ChannelStore.removeChangeListener(this.onChange);
} }
getStateFromStores() { onChange(force) {
return { if (this.state.search && !force) {
channels: ChannelStore.getMoreAll(), return;
}
this.setState({
channels: ChannelStore.getMoreChannelsList(),
serverError: null serverError: null
}; });
} }
onListenerChange() { nextPage(page) {
const newState = this.getStateFromStores(); AsyncClient.getMoreChannelsPage((page + 1) * CHANNELS_PER_PAGE, CHANNELS_PER_PAGE);
if (!Utils.areObjectsEqual(newState.channels, this.state.channels)) {
this.setState(newState);
}
} }
handleJoin(channel, done) { handleJoin(channel, done) {
GlobalActions.emitJoinChannelEvent( joinChannel(
channel, channel,
() => { () => {
$(this.refs.modal).modal('hide'); $(this.refs.modal).modal('hide');
@@ -95,6 +103,28 @@ export default class MoreChannels extends React.Component {
this.setState({showNewChannelModal: true}); this.setState({showNewChannelModal: true});
} }
search(term) {
if (term === '') {
this.onChange(true);
this.setState({search: false});
return;
}
clearTimeout(this.searchTimeoutId);
this.searchTimeoutId = setTimeout(
() => {
searchMoreChannels(
term,
(channels) => {
this.setState({search: true, channels});
}
);
},
SEARCH_TIMEOUT_MILLISECONDS
);
}
render() { render() {
let serverError; let serverError;
if (this.state.serverError) { if (this.state.serverError) {
@@ -136,31 +166,6 @@ export default class MoreChannels extends React.Component {
} }
} }
let moreChannels;
const channels = this.state.channels;
if (channels == null) {
moreChannels = <LoadingScreen/>;
} else if (channels.length) {
moreChannels = (
<FilteredChannelList
channels={channels}
handleJoin={this.handleJoin}
/>
);
} else {
moreChannels = (
<div className='no-channel-message'>
<p className='primary-message'>
<FormattedMessage
id='more_channels.noMore'
defaultMessage='No more channels to join'
/>
</p>
{createChannelHelpText}
</div>
);
}
return ( return (
<div <div
className='modal fade more-channel__modal' className='modal fade more-channel__modal'
@@ -200,7 +205,14 @@ export default class MoreChannels extends React.Component {
/> />
</div> </div>
<div className='modal-body'> <div className='modal-body'>
{moreChannels} <SearchableChannelList
channels={this.state.channels}
channelsPerPage={CHANNELS_PER_PAGE}
nextPage={this.nextPage}
search={this.search}
handleJoin={this.handleJoin}
noResultsText={createChannelHelpText}
/>
{serverError} {serverError}
</div> </div>
<div className='modal-footer'> <div className='modal-footer'>

205
webapp/components/searchable_channel_list.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,205 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import LoadingScreen from './loading_screen.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
import $ from 'jquery';
import React from 'react';
import {localizeMessage} from 'utils/utils.jsx';
import {FormattedMessage} from 'react-intl';
import loadingGif from 'images/load.gif';
const NEXT_BUTTON_TIMEOUT_MILLISECONDS = 500;
export default class SearchableChannelList extends React.Component {
constructor(props) {
super(props);
this.createChannelRow = this.createChannelRow.bind(this);
this.nextPage = this.nextPage.bind(this);
this.previousPage = this.previousPage.bind(this);
this.doSearch = this.doSearch.bind(this);
this.nextTimeoutId = 0;
this.state = {
joiningChannel: '',
page: 0,
nextDisabled: false
};
}
componentDidMount() {
// only focus the search box on desktop so that we don't cause the keyboard to open on mobile
if (!UserAgent.isMobile()) {
this.refs.filter.focus();
}
}
componentDidUpdate(prevProps, prevState) {
if (prevState.page !== this.state.page) {
$(this.refs.channelList).scrollTop(0);
}
}
handleJoin(channel) {
this.setState({joiningChannel: channel.id});
this.props.handleJoin(
channel,
() => {
this.setState({joiningChannel: ''});
}
);
}
createChannelRow(channel) {
let joinButton;
if (this.state.joiningChannel === channel.id) {
joinButton = (
<img
className='join-channel-loading-gif'
src={loadingGif}
/>
);
} else {
joinButton = (
<button
onClick={this.handleJoin.bind(this, channel)}
className='btn btn-primary'
>
<FormattedMessage
id='more_channels.join'
defaultMessage='Join'
/>
</button>
);
}
return (
<div
className='more-modal__row'
key={channel.id}
>
<div className='more-modal__details'>
<p className='more-modal__name'>{channel.display_name}</p>
<p className='more-modal__description'>{channel.purpose}</p>
</div>
<div className='more-modal__actions'>
{joinButton}
</div>
</div>
);
}
nextPage(e) {
e.preventDefault();
this.setState({page: this.state.page + 1, nextDisabled: true});
this.nextTimeoutId = setTimeout(() => this.setState({nextDisabled: false}), NEXT_BUTTON_TIMEOUT_MILLISECONDS);
this.props.nextPage(this.state.page + 1);
}
previousPage(e) {
e.preventDefault();
this.setState({page: this.state.page - 1});
}
doSearch() {
const term = this.refs.filter.value;
this.props.search(term);
if (term === '') {
this.setState({page: 0});
}
}
render() {
const channels = this.props.channels;
let listContent;
let nextButton;
let previousButton;
if (channels == null) {
listContent = <LoadingScreen/>;
} else if (channels.length === 0) {
listContent = (
<div className='no-channel-message'>
<p className='primary-message'>
<FormattedMessage
id='more_channels.noMore'
defaultMessage='No more channels to join'
/>
</p>
{this.props.noResultsText}
</div>
);
} else {
const pageStart = this.state.page * this.props.channelsPerPage;
const pageEnd = pageStart + this.props.channelsPerPage;
const channelsToDisplay = this.props.channels.slice(pageStart, pageEnd);
listContent = channelsToDisplay.map(this.createChannelRow);
if (channelsToDisplay.length >= this.props.channelsPerPage) {
nextButton = (
<button
className='btn btn-default filter-control filter-control__next'
onClick={this.nextPage}
disabled={this.state.nextDisabled}
>
{'Next'}
</button>
);
}
if (this.state.page > 0) {
previousButton = (
<button
className='btn btn-default filter-control filter-control__prev'
onClick={this.previousPage}
>
{'Previous'}
</button>
);
}
}
return (
<div className='filtered-user-list'>
<div className='filter-row'>
<div className='col-sm-6'>
<input
ref='filter'
className='form-control filter-textbox'
placeholder={localizeMessage('filtered_channels_list.search', 'Search channels')}
onInput={this.doSearch}
/>
</div>
</div>
<div
ref='channelList'
className='more-modal__list'
>
{listContent}
</div>
<div className='filter-controls'>
{previousButton}
{nextButton}
</div>
</div>
);
}
}
SearchableChannelList.defaultProps = {
channels: []
};
SearchableChannelList.propTypes = {
channels: React.PropTypes.arrayOf(React.PropTypes.object),
channelsPerPage: React.PropTypes.number,
nextPage: React.PropTypes.func.isRequired,
search: React.PropTypes.func.isRequired,
handleJoin: React.PropTypes.func.isRequired,
noResultsText: React.PropTypes.object
};

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

@@ -1,15 +1,16 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import React from 'react';
import SuggestionStore from 'stores/suggestion_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import Constants from 'utils/constants.jsx';
import Suggestion from './suggestion.jsx'; import Suggestion from './suggestion.jsx';
const MaxChannelSuggestions = 40; import {autocompleteChannels} from 'actions/channel_actions.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import {Constants, ActionTypes} from 'utils/constants.jsx';
import React from 'react';
class ChannelMentionSuggestion extends Suggestion { class ChannelMentionSuggestion extends Suggestion {
render() { render() {
@@ -48,84 +49,71 @@ class ChannelMentionSuggestion extends Suggestion {
} }
} }
function filterChannelsByPrefix(channels, prefix, limit) {
const filtered = [];
for (const id of Object.keys(channels)) {
if (filtered.length >= limit) {
break;
}
const channel = channels[id];
if (channel.delete_at > 0) {
continue;
}
if (channel.display_name.toLowerCase().startsWith(prefix) || channel.name.startsWith(prefix)) {
filtered.push(channel);
}
}
return filtered;
}
export default class ChannelMentionProvider { export default class ChannelMentionProvider {
constructor() {
this.timeoutId = '';
}
componentWillUnmount() {
clearTimeout(this.timeoutId);
}
handlePretextChanged(suggestionId, pretext) { handlePretextChanged(suggestionId, pretext) {
const captured = (/(^|\s)(~([^~]*))$/i).exec(pretext.toLowerCase()); const captured = (/(^|\s)(~([^~]*))$/i).exec(pretext.toLowerCase());
if (captured) { if (captured) {
const prefix = captured[3]; const prefix = captured[3];
const channels = ChannelStore.getAll(); function autocomplete() {
const moreChannels = ChannelStore.getMoreAll(); autocompleteChannels(
prefix,
// Remove private channels from the list. (data) => {
const publicChannels = channels.filter((channel) => { const channels = data;
return channel.type === 'O';
});
// Filter channels by prefix.
const filteredChannels = filterChannelsByPrefix(
publicChannels, prefix, MaxChannelSuggestions);
const filteredMoreChannels = filterChannelsByPrefix(
moreChannels, prefix, MaxChannelSuggestions - filteredChannels.length);
// Sort channels by display name.
[filteredChannels, filteredMoreChannels].forEach((items) => {
items.sort((a, b) => {
const aPrefix = a.display_name.startsWith(prefix);
const bPrefix = b.display_name.startsWith(prefix);
if (aPrefix === bPrefix) {
return a.display_name.localeCompare(b.display_name);
} else if (aPrefix) {
return -1;
}
return 1;
});
});
// Wrap channels in an outer object to avoid overwriting the 'type' property. // Wrap channels in an outer object to avoid overwriting the 'type' property.
const wrappedChannels = filteredChannels.map((item) => { const wrappedChannels = [];
return { const wrappedMoreChannels = [];
const moreChannels = [];
channels.forEach((item) => {
if (ChannelStore.get(item.id)) {
wrappedChannels.push({
type: Constants.MENTION_CHANNELS, type: Constants.MENTION_CHANNELS,
channel: item channel: item
};
}); });
const wrappedMoreChannels = filteredMoreChannels.map((item) => { return;
return { }
wrappedMoreChannels.push({
type: Constants.MENTION_MORE_CHANNELS, type: Constants.MENTION_MORE_CHANNELS,
channel: item channel: item
}; });
moreChannels.push(item);
}); });
const wrapped = wrappedChannels.concat(wrappedMoreChannels); const wrapped = wrappedChannels.concat(wrappedMoreChannels);
const mentions = wrapped.map((item) => '~' + item.channel.name); const mentions = wrapped.map((item) => '~' + item.channel.name);
SuggestionStore.clearSuggestions(suggestionId); AppDispatcher.handleServerAction({
SuggestionStore.addSuggestions(suggestionId, mentions, wrapped, ChannelMentionSuggestion, captured[2]); type: ActionTypes.RECEIVED_MORE_CHANNELS,
channels: moreChannels
});
AppDispatcher.handleServerAction({
type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
id: suggestionId,
matchedPretext: captured[2],
terms: mentions,
items: wrapped,
component: ChannelMentionSuggestion
});
}
);
}
this.timeoutId = setTimeout(
autocomplete.bind(this),
Constants.AUTOCOMPLETE_TIMEOUT
);
} }
} }
} }

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

@@ -1,13 +1,16 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import React from 'react'; import Suggestion from './suggestion.jsx';
import {autocompleteChannels} from 'actions/channel_actions.jsx';
import ChannelStore from 'stores/channel_store.jsx'; import ChannelStore from 'stores/channel_store.jsx';
import Constants from 'utils/constants.jsx';
import SuggestionStore from 'stores/suggestion_store.jsx';
import Suggestion from './suggestion.jsx'; import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import {Constants, ActionTypes} from 'utils/constants.jsx';
import React from 'react';
class SearchChannelSuggestion extends Suggestion { class SearchChannelSuggestion extends Suggestion {
render() { render() {
@@ -30,37 +33,64 @@ class SearchChannelSuggestion extends Suggestion {
} }
export default class SearchChannelProvider { export default class SearchChannelProvider {
constructor() {
this.timeoutId = '';
}
componentWillUnmount() {
clearTimeout(this.timeoutId);
}
handlePretextChanged(suggestionId, pretext) { handlePretextChanged(suggestionId, pretext) {
const captured = (/\b(?:in|channel):\s*(\S*)$/i).exec(pretext.toLowerCase()); const captured = (/\b(?:in|channel):\s*(\S*)$/i).exec(pretext.toLowerCase());
if (captured) { if (captured) {
const channelPrefix = captured[1]; const channelPrefix = captured[1];
const channels = ChannelStore.getAll(); function autocomplete() {
const publicChannels = []; autocompleteChannels(
channelPrefix,
(data) => {
const publicChannels = data;
const localChannels = ChannelStore.getAll();
const privateChannels = []; const privateChannels = [];
for (const id of Object.keys(channels)) { for (const id of Object.keys(localChannels)) {
const channel = channels[id]; const channel = localChannels[id];
if (channel.name.startsWith(channelPrefix) && channel.type === Constants.PRIVATE_CHANNEL) {
// don't show direct channels
if (channel.type !== Constants.DM_CHANNEL && channel.name.startsWith(channelPrefix)) {
if (channel.type === Constants.OPEN_CHANNEL) {
publicChannels.push(channel);
} else {
privateChannels.push(channel); privateChannels.push(channel);
} }
} }
}
publicChannels.sort((a, b) => a.name.localeCompare(b.name)); const filteredPublicChannels = [];
const publicChannelNames = publicChannels.map((channel) => channel.name); publicChannels.forEach((item) => {
if (item.name.startsWith(channelPrefix)) {
filteredPublicChannels.push(item);
}
});
privateChannels.sort((a, b) => a.name.localeCompare(b.name)); privateChannels.sort((a, b) => a.name.localeCompare(b.name));
const privateChannelNames = privateChannels.map((channel) => channel.name); filteredPublicChannels.sort((a, b) => a.name.localeCompare(b.name));
SuggestionStore.clearSuggestions(suggestionId); const channels = filteredPublicChannels.concat(privateChannels);
SuggestionStore.addSuggestions(suggestionId, publicChannelNames, publicChannels, SearchChannelSuggestion, channelPrefix); const channelNames = channels.map((channel) => channel.name);
SuggestionStore.addSuggestions(suggestionId, privateChannelNames, privateChannels, SearchChannelSuggestion, channelPrefix);
AppDispatcher.handleServerAction({
type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
id: suggestionId,
matchedPretext: channelPrefix,
terms: channelNames,
items: channels,
component: SearchChannelSuggestion
});
}
);
}
this.timeoutId = setTimeout(
autocomplete.bind(this),
Constants.AUTOCOMPLETE_TIMEOUT
);
} }
} }
} }

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

@@ -234,11 +234,23 @@ class ChannelStoreClass extends EventEmitter {
} }
storeMoreChannels(channels) { storeMoreChannels(channels) {
this.moreChannels = channels; const newChannels = {};
for (let i = 0; i < channels.length; i++) {
newChannels[channels[i].id] = channels[i];
}
this.moreChannels = Object.assign({}, this.moreChannels, newChannels);
}
removeMoreChannel(channelId) {
Reflect.deleteProperty(this.moreChannels, channelId);
} }
getMoreChannels() { getMoreChannels() {
return this.moreChannels; return Object.assign({}, this.moreChannels);
}
getMoreChannelsList() {
return Object.keys(this.moreChannels).map((cid) => this.moreChannels[cid]);
} }
storeStats(stats) { storeStats(stats) {

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

@@ -271,6 +271,52 @@ describe('Client.Channels', function() {
}); });
}); });
it('getMoreChannelsPage', function(done) {
TestHelper.initBasic(() => {
TestHelper.basicClient().getMoreChannelsPage(
0,
100,
function(data) {
assert.equal(data.length, 0);
done();
},
function(err) {
done(new Error(err.message));
}
);
});
});
it('searchMoreChannels', function(done) {
TestHelper.initBasic(() => {
TestHelper.basicClient().searchMoreChannels(
'blargh',
function(data) {
assert.equal(data.length, 0);
done();
},
function(err) {
done(new Error(err.message));
}
);
});
});
it('autocompleteChannels', function(done) {
TestHelper.initBasic(() => {
TestHelper.basicClient().autocompleteChannels(
TestHelper.basicChannel().name,
function(data) {
assert.equal(data != null, true);
done();
},
function(err) {
done(new Error(err.message));
}
);
});
});
it('getChannelCounts', function(done) { it('getChannelCounts', function(done) {
TestHelper.initBasic(() => { TestHelper.initBasic(() => {
TestHelper.basicClient().getChannelCounts( TestHelper.basicClient().getChannelCounts(

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

@@ -239,6 +239,30 @@ export function getMoreChannels(force) {
} }
} }
export function getMoreChannelsPage(offset, limit) {
if (isCallInProgress('getMoreChannelsPage')) {
return;
}
callTracker.getMoreChannelsPage = utils.getTimestamp();
Client.getMoreChannelsPage(
offset,
limit,
(data) => {
callTracker.getMoreChannelsPage = 0;
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_MORE_CHANNELS,
channels: data
});
},
(err) => {
callTracker.getMoreChannelsPage = 0;
dispatchError(err, 'getMoreChannelsPage');
}
);
}
export function getChannelStats(channelId = ChannelStore.getCurrentId(), doVersionCheck = false) { export function getChannelStats(channelId = ChannelStore.getCurrentId(), doVersionCheck = false) {
if (isCallInProgress('getChannelStats' + channelId) || channelId == null) { if (isCallInProgress('getChannelStats' + channelId) || channelId == null) {
return; return;