diff --git a/api/channel.go b/api/channel.go index 803274d324..151627623f 100644 --- a/api/channel.go +++ b/api/channel.go @@ -18,11 +18,13 @@ func InitChannel(r *mux.Router) { sr := r.PathPrefix("/channels").Subrouter() sr.Handle("/", ApiUserRequiredActivity(getChannels, false)).Methods("GET") sr.Handle("/more", ApiUserRequired(getMoreChannels)).Methods("GET") + sr.Handle("/counts", ApiUserRequiredActivity(getChannelCounts, false)).Methods("GET") sr.Handle("/create", ApiUserRequired(createChannel)).Methods("POST") sr.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST") sr.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST") sr.Handle("/update_desc", ApiUserRequired(updateChannelDesc)).Methods("POST") sr.Handle("/update_notify_level", ApiUserRequired(updateNotifyLevel)).Methods("POST") + sr.Handle("/{id:[A-Za-z0-9]+}/", ApiUserRequiredActivity(getChannel, false)).Methods("GET") sr.Handle("/{id:[A-Za-z0-9]+}/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET") sr.Handle("/{id:[A-Za-z0-9]+}/join", ApiUserRequired(joinChannel)).Methods("POST") sr.Handle("/{id:[A-Za-z0-9]+}/leave", ApiUserRequired(leaveChannel)).Methods("POST") @@ -275,7 +277,7 @@ func updateChannelDesc(c *Context, w http.ResponseWriter, r *http.Request) { func getChannels(c *Context, w http.ResponseWriter, r *http.Request) { - // user is already in the newtork + // user is already in the team if result := <-Srv.Store.Channel().GetChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil { if result.Err.Message == "No channels were found" { @@ -300,7 +302,7 @@ func getChannels(c *Context, w http.ResponseWriter, r *http.Request) { func getMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) { - // user is already in the newtork + // user is already in the team if result := <-Srv.Store.Channel().GetMoreChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil { c.Err = result.Err @@ -314,6 +316,22 @@ func getMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) { } } +func getChannelCounts(c *Context, w http.ResponseWriter, r *http.Request) { + + // user is already in the team + + if result := <-Srv.Store.Channel().GetChannelCounts(c.Session.TeamId, c.Session.UserId); result.Err != nil { + c.Err = model.NewAppError("getChannelCounts", "Unable to get channel counts from the database", result.Err.Message) + return + } else if HandleEtag(result.Data.(*model.ChannelCounts).Etag(), w, r) { + return + } else { + data := result.Data.(*model.ChannelCounts) + w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag()) + w.Write([]byte(data.ToJson())) + } +} + func joinChannel(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) @@ -548,6 +566,37 @@ func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(result))) } +func getChannel(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id := params["id"] + + //pchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, id, c.Session.UserId) + cchan := Srv.Store.Channel().Get(id) + cmchan := Srv.Store.Channel().GetMember(id, c.Session.UserId) + + if cresult := <-cchan; cresult.Err != nil { + c.Err = cresult.Err + return + } else if cmresult := <-cmchan; cmresult.Err != nil { + c.Err = cmresult.Err + return + } else { + data := &model.ChannelData{} + data.Channel = cresult.Data.(*model.Channel) + member := cmresult.Data.(model.ChannelMember) + data.Member = &member + + if HandleEtag(data.Etag(), w, r) { + return + } else { + w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag()) + w.Header().Set("Expires", "-1") + w.Write([]byte(data.ToJson())) + } + } + +} + func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) diff --git a/api/channel_test.go b/api/channel_test.go index d4fb11bd81..d65aff66c5 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -320,6 +320,27 @@ func TestGetChannel(t *testing.T) { if _, err := Client.UpdateLastViewedAt(channel2.Id); err != nil { t.Fatal(err) } + + if resp, err := Client.GetChannel(channel1.Id, ""); err != nil { + t.Fatal(err) + } else { + data := resp.Data.(*model.ChannelData) + if data.Channel.DisplayName != channel1.DisplayName { + t.Fatal("name didn't match") + } + + // test etag caching + if cache_result, err := Client.GetChannel(channel1.Id, resp.Etag); err != nil { + t.Fatal(err) + } else if cache_result.Data.(*model.ChannelData) != nil { + t.Log(cache_result.Data) + t.Fatal("cache should be empty") + } + } + + if _, err := Client.GetChannel("junk", ""); err == nil { + t.Fatal("should have failed - bad channel id") + } } func TestGetMoreChannel(t *testing.T) { @@ -366,6 +387,47 @@ func TestGetMoreChannel(t *testing.T) { } } +func TestGetChannelCounts(t *testing.T) { + Setup() + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user.Id)) + + Client.LoginByEmail(team.Name, user.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "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) + + if result, err := Client.GetChannelCounts(""); err != nil { + t.Fatal(err) + } else { + counts := result.Data.(*model.ChannelCounts) + + if len(counts.Counts) != 4 { + t.Fatal("wrong number of channel counts") + } + + if len(counts.UpdateTimes) != 4 { + t.Fatal("wrong number of channel update times") + } + + if cache_result, err := Client.GetChannelCounts(result.Etag); err != nil { + t.Fatal(err) + } else if cache_result.Data.(*model.ChannelCounts) != nil { + t.Log(cache_result.Data) + t.Fatal("result data should be empty") + } + } + +} + func TestJoinChannel(t *testing.T) { Setup() diff --git a/model/channel_count.go b/model/channel_count.go new file mode 100644 index 0000000000..d5daba14e9 --- /dev/null +++ b/model/channel_count.go @@ -0,0 +1,63 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "crypto/md5" + "encoding/json" + "fmt" + "io" + "sort" + "strconv" +) + +type ChannelCounts struct { + Counts map[string]int64 `json:"counts"` + UpdateTimes map[string]int64 `json:"update_times"` +} + +func (o *ChannelCounts) Etag() string { + + ids := []string{} + for id, _ := range o.Counts { + ids = append(ids, id) + } + sort.Strings(ids) + + str := "" + for _, id := range ids { + str += id + strconv.FormatInt(o.Counts[id], 10) + } + + md5Counts := fmt.Sprintf("%x", md5.Sum([]byte(str))) + + var update int64 = 0 + for _, u := range o.UpdateTimes { + if u > update { + update = u + } + } + + return Etag(md5Counts, update) +} + +func (o *ChannelCounts) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func ChannelCountsFromJson(data io.Reader) *ChannelCounts { + decoder := json.NewDecoder(data) + var o ChannelCounts + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/channel_data.go b/model/channel_data.go new file mode 100644 index 0000000000..234bdec6e9 --- /dev/null +++ b/model/channel_data.go @@ -0,0 +1,43 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type ChannelData struct { + Channel *Channel `json:"channel"` + Member *ChannelMember `json:"member"` +} + +func (o *ChannelData) Etag() string { + var mt int64 = 0 + if o.Member != nil { + mt = o.Member.LastUpdateAt + } + + return Etag(o.Channel.Id, o.Channel.UpdateAt, o.Channel.LastPostAt, mt) +} + +func (o *ChannelData) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func ChannelDataFromJson(data io.Reader) *ChannelData { + decoder := json.NewDecoder(data) + var o ChannelData + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/client.go b/model/client.go index a5016fa2cf..6fcfa50435 100644 --- a/model/client.go +++ b/model/client.go @@ -390,6 +390,15 @@ func (c *Client) GetChannels(etag string) (*Result, *AppError) { } } +func (c *Client) GetChannel(id, etag string) (*Result, *AppError) { + if r, err := c.DoGet("/channels/"+id+"/", "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ChannelDataFromJson(r.Body)}, nil + } +} + func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) { if r, err := c.DoGet("/channels/more", "", etag); err != nil { return nil, err @@ -399,6 +408,15 @@ func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) { } } +func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) { + if r, err := c.DoGet("/channels/counts", "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ChannelCountsFromJson(r.Body)}, nil + } +} + func (c *Client) JoinChannel(id string) (*Result, *AppError) { if r, err := c.DoPost("/channels/"+id+"/join", ""); err != nil { return nil, err diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go index cac5c681b9..b8bf8b5ac1 100644 --- a/store/sql_channel_store.go +++ b/store/sql_channel_store.go @@ -281,6 +281,41 @@ func (s SqlChannelStore) GetMoreChannels(teamId string, userId string) StoreChan return storeChannel } +type channelIdWithCountAndUpdateAt struct { + Id string + TotalMsgCount int64 + UpdateAt int64 +} + +func (s SqlChannelStore) GetChannelCounts(teamId string, userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var data []channelIdWithCountAndUpdateAt + _, err := s.GetReplica().Select(&data, "SELECT Id, TotalMsgCount, UpdateAt FROM Channels WHERE Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = :UserId) AND TeamId = :TeamId AND DeleteAt = 0 ORDER BY DisplayName", map[string]interface{}{"TeamId": teamId, "UserId": userId}) + + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.GetChannelCounts", "We couldn't get the channel counts", "teamId="+teamId+", userId="+userId+", err="+err.Error()) + } else { + counts := &model.ChannelCounts{Counts: make(map[string]int64), UpdateTimes: make(map[string]int64)} + for i := range data { + v := data[i] + counts.Counts[v.Id] = v.TotalMsgCount + counts.UpdateTimes[v.Id] = v.UpdateAt + } + + result.Data = counts + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (s SqlChannelStore) GetByName(teamId string, name string) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go index b14883843f..dabe399040 100644 --- a/store/sql_channel_store_test.go +++ b/store/sql_channel_store_test.go @@ -462,6 +462,53 @@ func TestChannelStoreGetMoreChannels(t *testing.T) { } } +func TestChannelStoreGetChannelCounts(t *testing.T) { + Setup() + + 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)) + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "Channel1" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + Must(store.Channel().Save(&o1)) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = model.NewId() + m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL + Must(store.Channel().SaveMember(&m1)) + + m2 := model.ChannelMember{} + m2.ChannelId = o1.Id + m2.UserId = model.NewId() + m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL + Must(store.Channel().SaveMember(&m2)) + + m3 := model.ChannelMember{} + m3.ChannelId = o2.Id + m3.UserId = model.NewId() + m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL + Must(store.Channel().SaveMember(&m3)) + + cresult := <-store.Channel().GetChannelCounts(o1.TeamId, m1.UserId) + counts := cresult.Data.(*model.ChannelCounts) + + if len(counts.Counts) != 1 { + t.Fatal("wrong number of counts") + } + + if len(counts.UpdateTimes) != 1 { + t.Fatal("wrong number of update times") + } +} + func TestChannelStoreUpdateLastViewedAt(t *testing.T) { Setup() diff --git a/store/store.go b/store/store.go index 0934fe84b1..613fe41983 100644 --- a/store/store.go +++ b/store/store.go @@ -50,6 +50,7 @@ type ChannelStore interface { GetByName(team_id string, domain string) StoreChannel GetChannels(teamId string, userId string) StoreChannel GetMoreChannels(teamId string, userId string) StoreChannel + GetChannelCounts(teamId string, userId string) StoreChannel SaveMember(member *model.ChannelMember) StoreChannel GetMembers(channelId string) StoreChannel diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx index 06d7fc3e85..dcff5b89d8 100644 --- a/web/react/components/edit_channel_modal.jsx +++ b/web/react/components/edit_channel_modal.jsx @@ -14,7 +14,7 @@ module.exports = React.createClass({ Client.updateChannelDesc(data, function(data) { this.setState({ server_error: "" }); - AsyncClient.getChannels(true); + AsyncClient.getChannel(this.state.channel_id); $(this.refs.modal.getDOMNode()).modal('hide'); }.bind(this), function(err) { diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx index 007476f9b9..5261ed6a7d 100644 --- a/web/react/components/more_channels.jsx +++ b/web/react/components/more_channels.jsx @@ -1,34 +1,32 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. - var utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); var asyncClient = require('../utils/async_client.jsx'); -var UserStore = require('../stores/user_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); var LoadingScreen = require('./loading_screen.jsx'); function getStateFromStores() { - return { - channels: ChannelStore.getMoreAll(), - server_error: null - }; + return { + channels: ChannelStore.getMoreAll(), + serverError: null + }; } module.exports = React.createClass({ - displayName: "MoreChannelsModal", + displayName: 'MoreChannelsModal', componentDidMount: function() { ChannelStore.addMoreChangeListener(this._onChange); - $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function (e) { + $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function shown() { asyncClient.getMoreChannels(true); }); var self = this; - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { + $(this.refs.modal.getDOMNode()).on('show.bs.modal', function show(e) { var button = e.relatedTarget; - self.setState({ channel_type: $(button).attr('data-channeltype') }); + self.setState({channelType: $(button).attr('data-channeltype')}); }); }, componentWillUnmount: function() { @@ -42,18 +40,17 @@ module.exports = React.createClass({ }, getInitialState: function() { var initState = getStateFromStores(); - initState.channel_type = ""; + initState.channelType = ''; return initState; }, - handleJoin: function(e) { - var self = this; - client.joinChannel(e, - function(data) { - $(self.refs.modal.getDOMNode()).modal('hide'); - asyncClient.getChannels(true); + handleJoin: function(id) { + client.joinChannel(id, + function() { + $(this.refs.modal.getDOMNode()).modal('hide'); + asyncClient.getChannel(id); }.bind(this), function(err) { - this.state.server_error = err.message; + this.state.serverError = err.message; this.setState(this.state); }.bind(this) ); @@ -62,52 +59,66 @@ module.exports = React.createClass({ $(this.refs.modal.getDOMNode()).modal('hide'); }, render: function() { - var server_error = this.state.server_error ?
: null; + var serverError; + if (this.state.serverError) { + serverError = ; + } + var outter = this; var moreChannels; - if (this.state.channels != null) - moreChannels = this.state.channels; + if (this.state.channels != null) { + var channels = this.state.channels; + if (!channels.loading) { + if (channels.length) { + moreChannels = ( +|
+ {channel.display_name} +{channel.description} + |
+ + |
No more channels to join
+Click 'Create New Channel' to make a new one
+