Commit f82667f3 authored by enahum's avatar enahum Committed by Harrison Healey

PLT-4430 improve slow channel switching (#4331)

* PLT-4430 improve slow channel switching

* Update client side unit tests

* Convert getChannelsUnread to getMyChannelMembers and address other feedback

* Pull channel members on websocket reconnect
parent 14ce4713
......@@ -21,6 +21,7 @@ func InitChannel() {
BaseRoutes.Channels.Handle("/", ApiUserRequired(getChannels)).Methods("GET")
BaseRoutes.Channels.Handle("/more", ApiUserRequired(getMoreChannels)).Methods("GET")
BaseRoutes.Channels.Handle("/counts", ApiUserRequired(getChannelCounts)).Methods("GET")
BaseRoutes.Channels.Handle("/members", ApiUserRequired(getMyChannelMembers)).Methods("GET")
BaseRoutes.Channels.Handle("/create", ApiUserRequired(createChannel)).Methods("POST")
BaseRoutes.Channels.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST")
BaseRoutes.Channels.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST")
......@@ -81,7 +82,7 @@ func createChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
} else {
data := result.Data.(*model.ChannelList)
if int64(len(data.Channels)+1) > *utils.Cfg.TeamSettings.MaxChannelsPerTeam {
if int64(len(*data)+1) > *utils.Cfg.TeamSettings.MaxChannelsPerTeam {
c.Err = model.NewLocAppError("createChannel", "api.channel.create_channel.max_channel_limit.app_error", map[string]interface{}{"MaxChannelsPerTeam": *utils.Cfg.TeamSettings.MaxChannelsPerTeam}, "")
return
}
......@@ -987,6 +988,16 @@ func getChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
func getMyChannelMembers(c *Context, w http.ResponseWriter, r *http.Request) {
if result := <-Srv.Store.Channel().GetMembersForUser(c.TeamId, c.Session.UserId); result.Err != nil {
c.Err = result.Err
return
} else {
data := result.Data.(*model.ChannelMembers)
w.Write([]byte(data.ToJson()))
}
}
func addMember(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["channel_id"]
......
......@@ -35,7 +35,7 @@ func TestCreateChannel(t *testing.T) {
rget := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
nameMatch := false
for _, c := range rget.Channels {
for _, c := range *rget {
if c.Name == channel.Name {
nameMatch = true
}
......@@ -240,8 +240,8 @@ func TestUpdateChannel(t *testing.T) {
}
rget := Client.Must(Client.GetChannels(""))
data := rget.Data.(*model.ChannelList)
for _, c := range data.Channels {
channels := rget.Data.(*model.ChannelList)
for _, c := range *channels {
if c.Name == model.DEFAULT_CHANNEL {
c.Header = "new header"
c.Name = "pseudo-square"
......@@ -654,13 +654,13 @@ func TestGetChannel(t *testing.T) {
channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
rget := Client.Must(Client.GetChannels(""))
data := rget.Data.(*model.ChannelList)
channels := rget.Data.(*model.ChannelList)
if data.Channels[0].DisplayName != channel1.DisplayName {
if (*channels)[0].DisplayName != channel1.DisplayName {
t.Fatal("full name didn't match")
}
if data.Channels[1].DisplayName != channel2.DisplayName {
if (*channels)[1].DisplayName != channel2.DisplayName {
t.Fatal("full name didn't match")
}
......@@ -717,13 +717,13 @@ func TestGetMoreChannel(t *testing.T) {
th.LoginBasic2()
rget := Client.Must(Client.GetMoreChannels(""))
data := rget.Data.(*model.ChannelList)
channels := rget.Data.(*model.ChannelList)
if data.Channels[0].DisplayName != channel1.DisplayName {
if (*channels)[0].DisplayName != channel1.DisplayName {
t.Fatal("full name didn't match")
}
if data.Channels[1].DisplayName != channel2.DisplayName {
if (*channels)[1].DisplayName != channel2.DisplayName {
t.Fatal("full name didn't match")
}
......@@ -770,6 +770,30 @@ func TestGetChannelCounts(t *testing.T) {
}
func TestGetMyChannelMembers(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)
if result, err := Client.GetMyChannelMembers(); err != nil {
t.Fatal(err)
} else {
members := result.Data.(*model.ChannelMembers)
// town-square, off-topic, basic test channel, channel1, channel2
if len(*members) != 5 {
t.Fatal("wrong number of members", len(*members))
}
}
}
func TestJoinChannelById(t *testing.T) {
th := Setup().InitBasic()
Client := th.BasicClient
......@@ -905,7 +929,7 @@ func TestLeaveChannel(t *testing.T) {
rget := Client.Must(Client.GetChannels(""))
cdata := rget.Data.(*model.ChannelList)
for _, c := range cdata.Channels {
for _, c := range *cdata {
if c.Name == model.DEFAULT_CHANNEL {
if _, err := Client.LeaveChannel(c.Id); err == nil {
t.Fatal("should have errored on leaving default channel")
......@@ -969,7 +993,7 @@ func TestDeleteChannel(t *testing.T) {
rget := Client.Must(Client.GetChannels(""))
cdata := rget.Data.(*model.ChannelList)
for _, c := range cdata.Channels {
for _, c := range *cdata {
if c.Name == model.DEFAULT_CHANNEL {
if _, err := Client.DeleteChannel(c.Id); err == nil {
t.Fatal("should have errored on deleting default channel")
......@@ -1249,7 +1273,7 @@ func TestUpdateNotifyProps(t *testing.T) {
data["user_id"] = user.Id
data["desktop"] = model.CHANNEL_NOTIFY_MENTION
timeBeforeUpdate := model.GetMillis()
//timeBeforeUpdate := model.GetMillis()
time.Sleep(100 * time.Millisecond)
// test updating desktop
......@@ -1261,14 +1285,6 @@ func TestUpdateNotifyProps(t *testing.T) {
t.Fatalf("NotifyProps[\"mark_unread\"] changed to %v", notifyProps["mark_unread"])
}
rget := Client.Must(Client.GetChannels(""))
rdata := rget.Data.(*model.ChannelList)
if len(rdata.Members) == 0 || rdata.Members[channel1.Id].NotifyProps["desktop"] != data["desktop"] {
t.Fatal("NotifyProps[\"desktop\"] did not update properly")
} else if rdata.Members[channel1.Id].LastUpdateAt <= timeBeforeUpdate {
t.Fatal("LastUpdateAt did not update")
}
// test an empty update
delete(data, "desktop")
......
......@@ -38,7 +38,7 @@ func (me *JoinProvider) DoCommand(c *Context, channelId string, message string)
} else {
channels := result.Data.(*model.ChannelList)
for _, v := range channels.Channels {
for _, v := range *channels {
if v.Name == message {
......
......@@ -42,7 +42,7 @@ func TestJoinCommands(t *testing.T) {
c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
found := false
for _, c := range c1.Channels {
for _, c := range *c1 {
if c.Id == channel2.Id {
found = true
}
......
......@@ -325,20 +325,20 @@ func LeaveTeam(team *model.Team, user *model.User) *model.AppError {
teamMember = result.Data.(model.TeamMember)
}
var channelMembers *model.ChannelList
var channelList *model.ChannelList
if result := <-Srv.Store.Channel().GetChannels(team.Id, user.Id); result.Err != nil {
if result.Err.Id == "store.sql_channel.get_channels.not_found.app_error" {
channelMembers = &model.ChannelList{make([]*model.Channel, 0), make(map[string]*model.ChannelMember)}
channelList = &model.ChannelList{}
} else {
return result.Err
}
} else {
channelMembers = result.Data.(*model.ChannelList)
channelList = result.Data.(*model.ChannelList)
}
for _, channel := range channelMembers.Channels {
for _, channel := range *channelList {
if channel.Type != model.CHANNEL_DIRECT {
Srv.Store.User().InvalidateProfilesInChannelCache(channel.Id)
if result := <-Srv.Store.Channel().RemoveMember(channel.Id, user.Id); result.Err != nil {
......
......@@ -63,7 +63,7 @@ func TestCreateFromSignupTeam(t *testing.T) {
}
c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
if len(c1.Channels) != 2 {
if len(*c1) != 2 {
t.Fatal("default channels not created")
}
......@@ -94,7 +94,7 @@ func TestCreateTeam(t *testing.T) {
Client.SetTeamId(rteam.Data.(*model.Team).Id)
c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
if len(c1.Channels) != 2 {
if len(*c1) != 2 {
t.Fatal("default channels not created")
}
......
......@@ -159,13 +159,13 @@ func getChannelID(channelname string, teamid string, userid string) (id string,
return "", false
}
data := result.Data.(*model.ChannelList)
data := result.Data.(model.ChannelList)
for _, channel := range data.Channels {
for _, channel := range data {
if channel.Name == channelname {
return channel.Id, true
}
}
l4g.Debug(utils.T("manaultesting.get_channel_id.no_found.debug"), channelname, strconv.Itoa(len(data.Channels)))
l4g.Debug(utils.T("manaultesting.get_channel_id.no_found.debug"), channelname, strconv.Itoa(len(data)))
return "", false
}
......@@ -8,15 +8,11 @@ import (
"io"
)
type ChannelList struct {
Channels []*Channel `json:"channels"`
Members map[string]*ChannelMember `json:"members"`
}
type ChannelList []*Channel
func (o *ChannelList) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
if b, err := json.Marshal(o); err != nil {
return "[]"
} else {
return string(b)
}
......@@ -28,7 +24,7 @@ func (o *ChannelList) Etag() string {
var t int64 = 0
var delta int64 = 0
for _, v := range o.Channels {
for _, v := range *o {
if v.LastPostAt > t {
t = v.LastPostAt
id = v.Id
......@@ -39,30 +35,9 @@ func (o *ChannelList) Etag() string {
id = v.Id
}
member := o.Members[v.Id]
if member != nil {
max := v.LastPostAt
if v.UpdateAt > max {
max = v.UpdateAt
}
delta += max - member.LastViewedAt
if member.LastViewedAt > t {
t = member.LastViewedAt
id = v.Id
}
if member.LastUpdateAt > t {
t = member.LastUpdateAt
id = v.Id
}
}
}
return Etag(id, t, delta, len(o.Channels))
return Etag(id, t, delta, len(*o))
}
func ChannelListFromJson(data io.Reader) *ChannelList {
......
......@@ -29,6 +29,27 @@ type ChannelMember struct {
LastUpdateAt int64 `json:"last_update_at"`
}
type ChannelMembers []ChannelMember
func (o *ChannelMembers) ToJson() string {
if b, err := json.Marshal(o); err != nil {
return "[]"
} else {
return string(b)
}
}
func ChannelMembersFromJson(data io.Reader) *ChannelMembers {
decoder := json.NewDecoder(data)
var o ChannelMembers
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}
func (o *ChannelMember) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
......
......@@ -1124,13 +1124,13 @@ func (c *Client) UpdateNotifyProps(data map[string]string) (*Result, *AppError)
}
}
func (c *Client) GetChannels(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/", "", etag); err != nil {
func (c *Client) GetMyChannelMembers() (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/members", "", ""); 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
r.Header.Get(HEADER_ETAG_SERVER), ChannelMembersFromJson(r.Body)}, nil
}
}
......@@ -1164,6 +1164,16 @@ func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) {
}
}
func (c *Client) GetChannels(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/", "", etag); 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) JoinChannel(id string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/join", ""); err != nil {
return nil, err
......
......@@ -367,23 +367,16 @@ func (s SqlChannelStore) GetChannels(teamId string, userId string) StoreChannel
go func() {
result := StoreResult{}
var data []channelWithMember
_, err := s.GetReplica().Select(&data, "SELECT * FROM Channels, ChannelMembers WHERE Id = ChannelId AND UserId = :UserId AND DeleteAt = 0 AND (TeamId = :TeamId OR TeamId = '') ORDER BY DisplayName", map[string]interface{}{"TeamId": teamId, "UserId": userId})
data := &model.ChannelList{}
_, err := s.GetReplica().Select(data, "SELECT Channels.* FROM Channels, ChannelMembers WHERE Id = ChannelId AND UserId = :UserId AND DeleteAt = 0 AND (TeamId = :TeamId OR TeamId = '') ORDER BY DisplayName", map[string]interface{}{"TeamId": teamId, "UserId": userId})
if err != nil {
result.Err = model.NewLocAppError("SqlChannelStore.GetChannels", "store.sql_channel.get_channels.get.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error())
} else {
channels := &model.ChannelList{make([]*model.Channel, len(data)), make(map[string]*model.ChannelMember)}
for i := range data {
v := data[i]
channels.Channels[i] = &v.Channel
channels.Members[v.Channel.Id] = &v.ChannelMember
}
if len(channels.Channels) == 0 {
if len(*data) == 0 {
result.Err = model.NewLocAppError("SqlChannelStore.GetChannels", "store.sql_channel.get_channels.not_found.app_error", nil, "teamId="+teamId+", userId="+userId)
} else {
result.Data = channels
result.Data = data
}
}
......@@ -400,8 +393,8 @@ func (s SqlChannelStore) GetMoreChannels(teamId string, userId string) StoreChan
go func() {
result := StoreResult{}
var data []*model.Channel
_, err := s.GetReplica().Select(&data,
data := &model.ChannelList{}
_, err := s.GetReplica().Select(data,
`SELECT
*
FROM
......@@ -426,7 +419,7 @@ func (s SqlChannelStore) GetMoreChannels(teamId string, userId string) StoreChan
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())
} else {
result.Data = &model.ChannelList{data, make(map[string]*model.ChannelMember)}
result.Data = data
}
storeChannel <- result
......@@ -918,11 +911,12 @@ func (s SqlChannelStore) IncrementMentionCount(channelId string, userId string)
`UPDATE
ChannelMembers
SET
MentionCount = MentionCount + 1
MentionCount = MentionCount + 1,
LastUpdateAt = :LastUpdateAt
WHERE
UserId = :UserId
AND ChannelId = :ChannelId`,
map[string]interface{}{"ChannelId": channelId, "UserId": userId})
map[string]interface{}{"ChannelId": channelId, "UserId": userId, "LastUpdateAt": model.GetMillis()})
if err != nil {
result.Err = model.NewLocAppError("SqlChannelStore.IncrementMentionCount", "store.sql_channel.increment_mention_count.app_error", nil, "channel_id="+channelId+", user_id="+userId+", "+err.Error())
}
......@@ -1032,3 +1026,32 @@ func (s SqlChannelStore) ExtraUpdateByUser(userId string, time int64) StoreChann
return storeChannel
}
func (s SqlChannelStore) GetMembersForUser(teamId string, userId string) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
result := StoreResult{}
members := &model.ChannelMembers{}
_, err := s.GetReplica().Select(members, `
SELECT cm.*
FROM ChannelMembers cm
INNER JOIN Channels c
ON c.Id = cm.ChannelId
AND c.TeamId = :TeamId
WHERE cm.UserId = :UserId
`, map[string]interface{}{"TeamId": teamId, "UserId": userId})
if err != nil {
result.Err = model.NewLocAppError("SqlChannelStore.GetMembersForUser", "store.sql_channel.get_members.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error())
} else {
result.Data = members
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
......@@ -305,14 +305,14 @@ func TestChannelStoreDelete(t *testing.T) {
cresult := <-store.Channel().GetChannels(o1.TeamId, m1.UserId)
list := cresult.Data.(*model.ChannelList)
if len(list.Channels) != 1 {
if len(*list) != 1 {
t.Fatal("invalid number of channels")
}
cresult = <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId)
list = cresult.Data.(*model.ChannelList)
if len(list.Channels) != 1 {
if len(*list) != 1 {
t.Fatal("invalid number of channels")
}
}
......@@ -514,7 +514,7 @@ func TestChannelStoreGetChannels(t *testing.T) {
cresult := <-store.Channel().GetChannels(o1.TeamId, m1.UserId)
list := cresult.Data.(*model.ChannelList)
if list.Channels[0].Id != o1.Id {
if (*list)[0].Id != o1.Id {
t.Fatal("missing channel")
}
......@@ -614,11 +614,11 @@ func TestChannelStoreGetMoreChannels(t *testing.T) {
cresult := <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId)
list := cresult.Data.(*model.ChannelList)
if len(list.Channels) != 1 {
if len(*list) != 1 {
t.Fatal("wrong list")
}
if list.Channels[0].Name != o3.Name {
if (*list)[0].Name != o3.Name {
t.Fatal("missing channel")
}
......@@ -688,6 +688,51 @@ func TestChannelStoreGetChannelCounts(t *testing.T) {
}
}
func TestChannelStoreGetMembersForUser(t *testing.T) {
Setup()
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = model.NewId()
t1.Email = model.NewId() + "@nowhere.com"
t1.Type = model.TEAM_OPEN
Must(store.Team().Save(&t1))
o1 := model.Channel{}
o1.TeamId = t1.Id
o1.DisplayName = "Channel1"
o1.Name = "a" + model.NewId() + "b"
o1.Type = model.CHANNEL_OPEN
Must(store.Channel().Save(&o1))
o2 := model.Channel{}
o2.TeamId = o1.TeamId
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 = o2.Id
m2.UserId = m1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2))
cresult := <-store.Channel().GetMembersForUser(o1.TeamId, m1.UserId)
members := cresult.Data.(*model.ChannelMembers)
// no unread messages
if len(*members) != 2 {
t.Fatal("wrong number of members")
}
}
func TestChannelStoreUpdateLastViewedAt(t *testing.T) {
Setup()
......
......@@ -109,6 +109,7 @@ type ChannelStore interface {
IncrementMentionCount(channelId string, userId string) StoreChannel
AnalyticsTypeCount(teamId string, channelType string) StoreChannel
ExtraUpdateByUser(userId string, time int64) StoreChannel
GetMembersForUser(teamId string, userId string) StoreChannel
}
type PostStore interface {
......
......@@ -42,8 +42,6 @@ export function emitChannelClickEvent(channel) {
);
}
function switchToChannel(chan) {
AsyncClient.getChannels(true);
AsyncClient.getMoreChannels(true);
AsyncClient.getChannelStats(chan.id);
AsyncClient.updateLastViewedAt(chan.id);
loadPosts(chan.id);
......@@ -436,10 +434,6 @@ export function loadDefaultLocale() {
}
export function viewLoggedIn() {
AsyncClient.getChannels();
AsyncClient.getMoreChannels();
AsyncClient.getChannelStats();
// Clear pending posts (shouldn't have pending posts if we are loading)
PostStore.clearPendingPosts();
}
......
......@@ -120,7 +120,7 @@ export function setUnreadPost(channelId, postId) {
member.msg_count = channel.total_msg_count - unreadPosts;
member.mention_count = 0;
ChannelStore.storeMyChannelMember(member);
ChannelStore.setUnreadCount(channelId);
ChannelStore.setUnreadCountByChannel(channelId);
AsyncClient.setLastViewedAt(lastViewed, channelId);
}
......
......@@ -76,6 +76,7 @@ function handleFirstConnect() {
function handleReconnect() {
if (Client.teamId) {
AsyncClient.getChannels();
AsyncClient.getMyChannelMembers();
loadPosts(ChannelStore.getCurrentId());
}
......
......@@ -1357,6 +1357,15 @@ export default class Client {
end(this.handleResponse.bind(this, 'getChannelCounts', success, error));
}
getMyChannelMembers(success, error) {
request.
get(`${this.getChannelsRoute()}/members`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getMyChannelMembers', success, error));
}
getChannelStats(channelId, success, error) {
request.
get(`${this.getChannelNeededRoute(channelId)}/stats`).
......
......@@ -75,10 +75,11 @@ function preNeedsTeam(nextState, replace, callback) {
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_CHANNELS,
channels: data.channels,
members: data.members
channels: data
});
AsyncClient.getMyChannelMembers();
d1.resolve();
},
(err) => {
......
......@@ -156,7 +156,7 @@ class ChannelStoreClass extends EventEmitter {
if (c) {
cm[cmid].msg_count = this.get(id).total_msg_count;
cm[cmid].mention_count = 0;
this.setUnreadCount(id);
this.setUnreadCountByChannel(id);
}
break;
}
......@@ -250,6 +250,12 @@ class ChannelStoreClass extends EventEmitter {
this.myChannelMembers = channelMembers;
}
storeMyChannelMembersList(channelMembers) {
channelMembers.forEach((m) => {
this.myChannelMembers[m.channel_id] = m;
});
}
getMyMembers() {
return this.myChannelMembers;
}
......@@ -278,7 +284,13 @@ class ChannelStoreClass extends EventEmitter {
return this.postMode;
}
setUnreadCount(id) {
setUnreadCountsByMembers(members) {
members.forEach((m) => {
this.setUnreadCountByChannel(m.channel_id);
});
}
setUnreadCountByChannel(id) {
const ch = this.get(id);
const chMember = this.getMyMember(id);
......@@ -292,13 +304,6 @@ class ChannelStoreClass extends EventEmitter {
this.unreadCounts[id] = {msgs: chUnreadCount, mentions: chMentionCount};
}