PLT-2241 Refactored statuses into a more real-time system (#3573)

* Refactored statuses into a more real-time system

* Updated package.json with correct commit and fixed minor bug

* Minor updates to statuses based on feedback

* When setting status online, update only LastActivityAt if status already exists
Этот коммит содержится в:
Joram Wilander
2016-07-18 11:10:03 -04:00
коммит произвёл GitHub
родитель 180adc79af
Коммит c0ab2636d6
35 изменённых файлов: 800 добавлений и 376 удалений

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

@@ -159,7 +159,7 @@ test-server: start-docker prepare-enterprise
rm -f cover.out rm -f cover.out
echo "mode: count" > cover.out echo "mode: count" > cover.out
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=340s -covermode=count -coverprofile=capi.out ./api || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=440s -covermode=count -coverprofile=capi.out ./api || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=cmodel.out ./model || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=cmodel.out ./model || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s -covermode=count -coverprofile=cstore.out ./store || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s -covermode=count -coverprofile=cstore.out ./store || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s -covermode=count -coverprofile=cutils.out ./utils || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s -covermode=count -coverprofile=cutils.out ./utils || exit 1

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

@@ -94,6 +94,7 @@ func InitApi() {
InitPreference() InitPreference()
InitLicense() InitLicense()
InitEmoji() InitEmoji()
InitStatus()
// 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))

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

@@ -20,6 +20,7 @@ import (
) )
var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE) var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE)
var statusCache *utils.Cache = utils.NewLru(model.STATUS_CACHE_SIZE)
var allowedMethods []string = []string{ var allowedMethods []string = []string{
"POST", "POST",
@@ -196,11 +197,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
if c.Err == nil && h.isUserActivity && token != "" && len(c.Session.UserId) > 0 { if c.Err == nil && h.isUserActivity && token != "" && len(c.Session.UserId) > 0 {
go func() { SetStatusOnline(c.Session.UserId, c.Session.Id)
if err := (<-Srv.Store.User().UpdateUserAndSessionActivity(c.Session.UserId, c.Session.Id, model.GetMillis())).Err; err != nil {
l4g.Error(utils.T("api.context.last_activity_at.error"), c.Session.UserId, c.Session.Id, err)
}
}()
} }
if c.Err == nil { if c.Err == nil {

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

@@ -73,7 +73,12 @@ func ping(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(m))) w.Write([]byte(model.MapToJson(m)))
} }
func webSocketPing(req *model.WebSocketRequest, responseData map[string]interface{}) *model.AppError { func webSocketPing(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) {
responseData["text"] = "pong" data := map[string]interface{}{}
return nil data["text"] = "pong"
data["version"] = model.CurrentVersion
data["server_time"] = model.GetMillis()
data["node_id"] = ""
return data, nil
} }

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

@@ -597,7 +597,13 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
for _, id := range mentionedUsersList { for _, id := range mentionedUsersList {
userAllowsEmails := profileMap[id].NotifyProps["email"] != "false" userAllowsEmails := profileMap[id].NotifyProps["email"] != "false"
if userAllowsEmails && (profileMap[id].IsAway() || profileMap[id].IsOffline()) { var status *model.Status
var err *model.AppError
if status, err = GetStatus(id); err != nil {
status = &model.Status{id, model.STATUS_OFFLINE, 0}
}
if userAllowsEmails && status.Status != model.STATUS_ONLINE {
sendNotificationEmail(c, post, profileMap[id], channel, team, senderName) sendNotificationEmail(c, post, profileMap[id], channel, team, senderName)
} }
} }

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

@@ -0,0 +1,155 @@
// 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/store"
"github.com/mattermost/platform/utils"
)
func InitStatus() {
l4g.Debug(utils.T("api.status.init.debug"))
BaseRoutes.Users.Handle("/status", ApiUserRequiredActivity(getStatusesHttp, false)).Methods("GET")
BaseRoutes.WebSocket.Handle("get_statuses", ApiWebSocketHandler(getStatusesWebSocket))
}
func getStatusesHttp(c *Context, w http.ResponseWriter, r *http.Request) {
statusMap, err := GetAllStatuses()
if err != nil {
c.Err = err
return
}
w.Write([]byte(model.StringInterfaceToJson(statusMap)))
}
func getStatusesWebSocket(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) {
statusMap, err := GetAllStatuses()
if err != nil {
return nil, err
}
return statusMap, nil
}
func GetAllStatuses() (map[string]interface{}, *model.AppError) {
if result := <-Srv.Store.Status().GetOnlineAway(); result.Err != nil {
return nil, result.Err
} else {
statuses := result.Data.([]*model.Status)
statusMap := map[string]interface{}{}
for _, s := range statuses {
statusMap[s.UserId] = s.Status
}
return statusMap, nil
}
}
func SetStatusOnline(userId string, sessionId string) {
broadcast := false
saveStatus := false
var status *model.Status
var err *model.AppError
if status, err = GetStatus(userId); err != nil {
status = &model.Status{userId, model.STATUS_ONLINE, model.GetMillis()}
broadcast = true
saveStatus = true
} else {
if status.Status != model.STATUS_ONLINE {
broadcast = true
}
status.Status = model.STATUS_ONLINE
status.LastActivityAt = model.GetMillis()
}
statusCache.Add(status.UserId, status)
achan := Srv.Store.Session().UpdateLastActivityAt(sessionId, model.GetMillis())
var schan store.StoreChannel
if saveStatus {
schan = Srv.Store.Status().SaveOrUpdate(status)
} else {
schan = Srv.Store.Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt)
}
if result := <-achan; result.Err != nil {
l4g.Error(utils.T("api.status.last_activity.error"), userId, sessionId, result.Err)
}
if result := <-schan; result.Err != nil {
l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
}
if broadcast {
event := model.NewWebSocketEvent("", "", status.UserId, model.WEBSOCKET_EVENT_STATUS_CHANGE)
event.Add("status", model.STATUS_ONLINE)
go Publish(event)
}
}
func SetStatusOffline(userId string) {
status := &model.Status{userId, model.STATUS_OFFLINE, model.GetMillis()}
statusCache.Add(status.UserId, status)
if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
}
event := model.NewWebSocketEvent("", "", status.UserId, model.WEBSOCKET_EVENT_STATUS_CHANGE)
event.Add("status", model.STATUS_OFFLINE)
go Publish(event)
}
func SetStatusAwayIfNeeded(userId string) {
status, err := GetStatus(userId)
if err != nil {
status = &model.Status{userId, model.STATUS_OFFLINE, 0}
}
if status.Status == model.STATUS_AWAY {
return
}
if !IsUserAway(status.LastActivityAt) {
return
}
status.Status = model.STATUS_AWAY
statusCache.Add(status.UserId, status)
if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
}
event := model.NewWebSocketEvent("", "", status.UserId, model.WEBSOCKET_EVENT_STATUS_CHANGE)
event.Add("status", model.STATUS_AWAY)
go Publish(event)
}
func GetStatus(userId string) (*model.Status, *model.AppError) {
if status, ok := statusCache.Get(userId); ok {
return status.(*model.Status), nil
}
if result := <-Srv.Store.Status().Get(userId); result.Err != nil {
return nil, result.Err
} else {
return result.Data.(*model.Status), nil
}
}
func IsUserAway(lastActivityAt int64) bool {
return model.GetMillis()-lastActivityAt >= *utils.Cfg.TeamSettings.UserStatusAwayTimeout*1000
}

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

@@ -0,0 +1,153 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"strings"
"testing"
"time"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
)
func TestStatuses(t *testing.T) {
th := Setup().InitBasic()
Client := th.BasicClient
WebSocketClient, err := th.CreateWebSocketClient()
if err != nil {
t.Fatal(err)
}
defer WebSocketClient.Close()
WebSocketClient.Listen()
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
LinkUserToTeam(ruser, rteam.Data.(*model.Team))
store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser2 := Client.Must(Client.CreateUser(&user2, "")).Data.(*model.User)
LinkUserToTeam(ruser2, rteam.Data.(*model.Team))
store.Must(Srv.Store.User().VerifyEmail(ruser2.Id))
Client.Login(user.Email, user.Password)
Client.SetTeamId(team.Id)
r1, err := Client.GetStatuses()
if err != nil {
t.Fatal(err)
}
statuses := r1.Data.(map[string]string)
for _, status := range statuses {
if status != model.STATUS_OFFLINE && status != model.STATUS_AWAY && status != model.STATUS_ONLINE {
t.Fatal("one of the statuses had an invalid value")
}
}
th.LoginBasic2()
WebSocketClient2, err2 := th.CreateWebSocketClient()
if err2 != nil {
t.Fatal(err2)
}
time.Sleep(300 * time.Millisecond)
WebSocketClient.GetStatuses()
if resp := <-WebSocketClient.ResponseChannel; resp.Error != nil {
t.Fatal(resp.Error)
} else {
if resp.SeqReply != WebSocketClient.Sequence-1 {
t.Fatal("bad sequence number")
}
for _, status := range resp.Data {
if status != model.STATUS_OFFLINE && status != model.STATUS_AWAY && status != model.STATUS_ONLINE {
t.Fatal("one of the statuses had an invalid value")
}
}
if status, ok := resp.Data[th.BasicUser2.Id]; !ok {
t.Fatal("should have had user status")
} else if status != model.STATUS_ONLINE {
t.Log(status)
t.Fatal("status should have been online")
}
}
SetStatusAwayIfNeeded(th.BasicUser2.Id)
awayTimeout := *utils.Cfg.TeamSettings.UserStatusAwayTimeout
defer func() {
*utils.Cfg.TeamSettings.UserStatusAwayTimeout = awayTimeout
}()
*utils.Cfg.TeamSettings.UserStatusAwayTimeout = 1
time.Sleep(1500 * time.Millisecond)
SetStatusAwayIfNeeded(th.BasicUser2.Id)
SetStatusAwayIfNeeded(th.BasicUser2.Id)
WebSocketClient2.Close()
time.Sleep(300 * time.Millisecond)
WebSocketClient.GetStatuses()
if resp := <-WebSocketClient.ResponseChannel; resp.Error != nil {
t.Fatal(resp.Error)
} else {
if resp.SeqReply != WebSocketClient.Sequence-1 {
t.Fatal("bad sequence number")
}
if _, ok := resp.Data[th.BasicUser2.Id]; ok {
t.Fatal("should not have had user status")
}
}
stop := make(chan bool)
onlineHit := false
awayHit := false
offlineHit := false
go func() {
for {
select {
case resp := <-WebSocketClient.EventChannel:
if resp.Event == model.WEBSOCKET_EVENT_STATUS_CHANGE && resp.UserId == th.BasicUser2.Id {
status := resp.Data["status"].(string)
if status == model.STATUS_ONLINE {
onlineHit = true
} else if status == model.STATUS_AWAY {
awayHit = true
} else if status == model.STATUS_OFFLINE {
offlineHit = true
}
}
case <-stop:
return
}
}
}()
time.Sleep(500 * time.Millisecond)
stop <- true
if !onlineHit {
t.Fatal("didn't get online event")
}
if !awayHit {
t.Fatal("didn't get away event")
}
if !offlineHit {
t.Fatal("didn't get offline event")
}
}

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

@@ -54,7 +54,6 @@ func InitUser() {
BaseRoutes.Users.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST") BaseRoutes.Users.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
BaseRoutes.Users.Handle("/me", ApiAppHandler(getMe)).Methods("GET") BaseRoutes.Users.Handle("/me", ApiAppHandler(getMe)).Methods("GET")
BaseRoutes.Users.Handle("/initial_load", ApiAppHandler(getInitialLoad)).Methods("GET") BaseRoutes.Users.Handle("/initial_load", ApiAppHandler(getInitialLoad)).Methods("GET")
BaseRoutes.Users.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("POST")
BaseRoutes.Users.Handle("/direct_profiles", ApiUserRequired(getDirectProfiles)).Methods("GET") BaseRoutes.Users.Handle("/direct_profiles", ApiUserRequired(getDirectProfiles)).Methods("GET")
BaseRoutes.Users.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET") BaseRoutes.Users.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET")
BaseRoutes.Users.Handle("/profiles_for_dm_list/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfilesForDirectMessageList)).Methods("GET") BaseRoutes.Users.Handle("/profiles_for_dm_list/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfilesForDirectMessageList)).Methods("GET")
@@ -1955,35 +1954,6 @@ func updateUserNotify(c *Context, w http.ResponseWriter, r *http.Request) {
} }
} }
func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) {
userIds := model.ArrayFromJson(r.Body)
if len(userIds) == 0 {
c.SetInvalidParam("getStatuses", "userIds")
return
}
if result := <-Srv.Store.User().GetProfileByIds(userIds); result.Err != nil {
c.Err = result.Err
return
} else {
profiles := result.Data.(map[string]*model.User)
statuses := map[string]string{}
for _, profile := range profiles {
if profile.IsOffline() {
statuses[profile.Id] = model.USER_OFFLINE
} else if profile.IsAway() {
statuses[profile.Id] = model.USER_AWAY
} else {
statuses[profile.Id] = model.USER_ONLINE
}
}
w.Write([]byte(model.MapToJson(statuses)))
return
}
}
func IsUsernameTaken(name string) bool { func IsUsernameTaken(name string) bool {
if !model.IsValidUsername(name) { if !model.IsValidUsername(name) {
@@ -2312,7 +2282,7 @@ func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) {
c.Err = error c.Err = error
return return
} else { } else {
if user.LastActivityAt > 0 { if _, err := GetStatus(user.Id); err != nil {
go SendEmailChangeVerifyEmail(c, user.Id, user.Email, c.GetSiteURL()) go SendEmailChangeVerifyEmail(c, user.Id, user.Email, c.GetSiteURL())
} else { } else {
go SendVerifyEmail(c, user.Id, user.Email, c.GetSiteURL()) go SendVerifyEmail(c, user.Id, user.Email, c.GetSiteURL())
@@ -2551,11 +2521,11 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
} }
} }
func userTyping(req *model.WebSocketRequest, responseData map[string]interface{}) *model.AppError { func userTyping(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) {
var ok bool var ok bool
var channelId string var channelId string
if channelId, ok = req.Data["channel_id"].(string); !ok || len(channelId) != 26 { if channelId, ok = req.Data["channel_id"].(string); !ok || len(channelId) != 26 {
return NewInvalidWebSocketParamError(req.Action, "channel_id") return nil, NewInvalidWebSocketParamError(req.Action, "channel_id")
} }
var parentId string var parentId string
@@ -2567,5 +2537,5 @@ func userTyping(req *model.WebSocketRequest, responseData map[string]interface{}
event.Add("parent_id", parentId) event.Add("parent_id", parentId)
go Publish(event) go Publish(event)
return nil return nil, nil
} }

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

@@ -669,9 +669,7 @@ func TestUserUpdate(t *testing.T) {
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} 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) team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
time1 := model.GetMillis() user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", Roles: ""}
user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", LastActivityAt: time1, LastPingAt: time1, Roles: ""}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team) LinkUserToTeam(user, team)
store.Must(Srv.Store.User().VerifyEmail(user.Id)) store.Must(Srv.Store.User().VerifyEmail(user.Id))
@@ -683,15 +681,7 @@ func TestUserUpdate(t *testing.T) {
Client.Login(user.Email, "passwd1") Client.Login(user.Email, "passwd1")
Client.SetTeamId(team.Id) Client.SetTeamId(team.Id)
time.Sleep(100 * time.Millisecond)
time2 := model.GetMillis()
time.Sleep(100 * time.Millisecond)
user.Nickname = "Jim Jimmy" user.Nickname = "Jim Jimmy"
user.LastActivityAt = time2
user.LastPingAt = time2
user.Roles = model.ROLE_TEAM_ADMIN user.Roles = model.ROLE_TEAM_ADMIN
user.LastPasswordUpdate = 123 user.LastPasswordUpdate = 123
@@ -701,12 +691,6 @@ func TestUserUpdate(t *testing.T) {
if result.Data.(*model.User).Nickname != "Jim Jimmy" { if result.Data.(*model.User).Nickname != "Jim Jimmy" {
t.Fatal("Nickname did not update properly") t.Fatal("Nickname did not update properly")
} }
if result.Data.(*model.User).LastActivityAt == time2 {
t.Fatal("LastActivityAt should not have updated")
}
if result.Data.(*model.User).LastPingAt == time2 {
t.Fatal("LastPingAt should not have updated")
}
if result.Data.(*model.User).Roles != "" { if result.Data.(*model.User).Roles != "" {
t.Fatal("Roles should not have updated") t.Fatal("Roles should not have updated")
} }
@@ -1347,48 +1331,6 @@ func TestFuzzyUserCreate(t *testing.T) {
} }
} }
func TestStatuses(t *testing.T) {
th := Setup()
Client := th.CreateClient()
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
LinkUserToTeam(ruser, rteam.Data.(*model.Team))
store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser2 := Client.Must(Client.CreateUser(&user2, "")).Data.(*model.User)
LinkUserToTeam(ruser2, rteam.Data.(*model.Team))
store.Must(Srv.Store.User().VerifyEmail(ruser2.Id))
Client.Login(user.Email, user.Password)
Client.SetTeamId(team.Id)
userIds := []string{ruser2.Id}
r1, err := Client.GetStatuses(userIds)
if err != nil {
t.Fatal(err)
}
statuses := r1.Data.(map[string]string)
if len(statuses) != 1 {
t.Log(statuses)
t.Fatal("invalid number of statuses")
}
for _, status := range statuses {
if status != model.USER_OFFLINE && status != model.USER_AWAY && status != model.USER_ONLINE {
t.Fatal("one of the statuses had an invalid value")
}
}
}
func TestEmailToOAuth(t *testing.T) { func TestEmailToOAuth(t *testing.T) {
th := Setup() th := Setup()
Client := th.CreateClient() Client := th.CreateClient()

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

@@ -7,9 +7,7 @@ import (
"time" "time"
"github.com/mattermost/platform/model" "github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
goi18n "github.com/nicksnyder/go-i18n/i18n" goi18n "github.com/nicksnyder/go-i18n/i18n"
) )
@@ -34,18 +32,7 @@ type WebConn struct {
} }
func NewWebConn(c *Context, ws *websocket.Conn) *WebConn { func NewWebConn(c *Context, ws *websocket.Conn) *WebConn {
go func() { go SetStatusOnline(c.Session.UserId, c.Session.Id)
achan := Srv.Store.User().UpdateUserAndSessionActivity(c.Session.UserId, c.Session.Token, model.GetMillis())
pchan := Srv.Store.User().UpdateLastPingAt(c.Session.UserId, model.GetMillis())
if result := <-achan; result.Err != nil {
l4g.Error(utils.T("api.web_conn.new_web_conn.last_activity.error"), c.Session.UserId, c.Session.Token, result.Err)
}
if result := <-pchan; result.Err != nil {
l4g.Error(utils.T("api.web_conn.new_web_conn.last_ping.error"), c.Session.UserId, result.Err)
}
}()
return &WebConn{ return &WebConn{
Send: make(chan model.WebSocketMessage, 64), Send: make(chan model.WebSocketMessage, 64),
@@ -68,13 +55,7 @@ func (c *WebConn) readPump() {
c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT)) c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT))
c.WebSocket.SetPongHandler(func(string) error { c.WebSocket.SetPongHandler(func(string) error {
c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT)) c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT))
go SetStatusAwayIfNeeded(c.UserId)
go func() {
if result := <-Srv.Store.User().UpdateLastPingAt(c.UserId, model.GetMillis()); result.Err != nil {
l4g.Error(utils.T("api.web_conn.new_web_conn.last_ping.error"), c.UserId, result.Err)
}
}()
return nil return nil
}) })

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

@@ -67,10 +67,23 @@ func (h *Hub) Start() {
h.connections[webCon] = true h.connections[webCon] = true
case webCon := <-h.unregister: case webCon := <-h.unregister:
userId := webCon.UserId
if _, ok := h.connections[webCon]; ok { if _, ok := h.connections[webCon]; ok {
delete(h.connections, webCon) delete(h.connections, webCon)
close(webCon.Send) close(webCon.Send)
} }
found := false
for webCon := range h.connections {
if userId == webCon.UserId {
found = true
break
}
}
if !found {
go SetStatusOffline(userId)
}
case userId := <-h.invalidateUser: case userId := <-h.invalidateUser:
for webCon := range h.connections { for webCon := range h.connections {
if webCon.UserId == userId { if webCon.UserId == userId {

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

@@ -10,12 +10,12 @@ import (
"github.com/mattermost/platform/utils" "github.com/mattermost/platform/utils"
) )
func ApiWebSocketHandler(wh func(*model.WebSocketRequest, map[string]interface{}) *model.AppError) *webSocketHandler { func ApiWebSocketHandler(wh func(*model.WebSocketRequest) (map[string]interface{}, *model.AppError)) *webSocketHandler {
return &webSocketHandler{wh} return &webSocketHandler{wh}
} }
type webSocketHandler struct { type webSocketHandler struct {
handlerFunc func(*model.WebSocketRequest, map[string]interface{}) *model.AppError handlerFunc func(*model.WebSocketRequest) (map[string]interface{}, *model.AppError)
} }
func (wh *webSocketHandler) ServeWebSocket(conn *WebConn, r *model.WebSocketRequest) { func (wh *webSocketHandler) ServeWebSocket(conn *WebConn, r *model.WebSocketRequest) {
@@ -25,9 +25,10 @@ func (wh *webSocketHandler) ServeWebSocket(conn *WebConn, r *model.WebSocketRequ
r.T = conn.T r.T = conn.T
r.Locale = conn.Locale r.Locale = conn.Locale
data := make(map[string]interface{}) var data map[string]interface{}
var err *model.AppError
if err := wh.handlerFunc(r, data); err != nil { if data, err = wh.handlerFunc(r); err != nil {
l4g.Error(utils.T("api.web_socket_handler.log.error"), "/api/v3/users/websocket", r.Action, r.Seq, r.Session.UserId, err.SystemMessage(utils.T), err.DetailedError) l4g.Error(utils.T("api.web_socket_handler.log.error"), "/api/v3/users/websocket", r.Action, r.Seq, r.Session.UserId, err.SystemMessage(utils.T), err.DetailedError)
err.DetailedError = "" err.DetailedError = ""
conn.Send <- model.NewWebSocketError(r.Seq, err) conn.Send <- model.NewWebSocketError(r.Seq, err)

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

@@ -40,7 +40,8 @@
"RestrictDirectMessage": "any", "RestrictDirectMessage": "any",
"RestrictTeamInvite": "all", "RestrictTeamInvite": "all",
"RestrictPublicChannelManagement": "all", "RestrictPublicChannelManagement": "all",
"RestrictPrivateChannelManagement": "all" "RestrictPrivateChannelManagement": "all",
"UserStatusAwayTimeout": 300
}, },
"SqlSettings": { "SqlSettings": {
"DriverName": "mysql", "DriverName": "mysql",

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

@@ -1980,12 +1980,12 @@
"translation": "Bad verify email link." "translation": "Bad verify email link."
}, },
{ {
"id": "api.web_conn.new_web_conn.last_activity.error", "id": "api.status.last_activity.error",
"translation": "Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v" "translation": "Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v"
}, },
{ {
"id": "api.web_conn.new_web_conn.last_ping.error", "id": "api.status.save_status.error",
"translation": "Failed to update LastPingAt for user_id=%v, err=%v" "translation": "Failed to save status for user_id=%v, err=%v"
}, },
{ {
"id": "api.web_hub.start.stopping.debug", "id": "api.web_hub.start.stopping.debug",
@@ -4036,8 +4036,32 @@
"translation": "We encountered an error while finding user profiles" "translation": "We encountered an error while finding user profiles"
}, },
{ {
"id": "store.sql_user.get_total_active_users_count.app_error", "id": "store.sql_status.get_total_active_users_count.app_error",
"translation": "We could not count the users" "translation": "We could not count the active users"
},
{
"id": "store.sql_status.reset_all.app_error",
"translation": "Encountered an error resetting all the statuses"
},
{
"id": "store.sql_status.get_online_away.app_error",
"translation": "Encountered an error retrieving all the online/away statuses"
},
{
"id": "store.sql_status.get.app_error",
"translation": "Encountered an error retrieving the status"
},
{
"id": "store.sql_status.get.missing.app_error",
"translation": "No entry for that status exists"
},
{
"id": "store.sql_status.update.app_error",
"translation": "Encountered an error updating the status"
},
{
"id": "store.sql_status.save.app_error",
"translation": "Encountered an error saving the status"
}, },
{ {
"id": "store.sql_user.get_total_users_count.app_error", "id": "store.sql_user.get_total_users_count.app_error",

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

@@ -125,6 +125,8 @@ func main() {
if flagRunCmds { if flagRunCmds {
runCmds() runCmds()
} else { } else {
resetStatuses()
api.StartServer() api.StartServer()
// If we allow testing then listen for manual testing URL hits // If we allow testing then listen for manual testing URL hits
@@ -149,6 +151,12 @@ func main() {
} }
} }
func resetStatuses() {
if result := <-api.Srv.Store.Status().ResetAll(); result.Err != nil {
l4g.Error(utils.T("mattermost.reset_status.error"), result.Err.Error())
}
}
func setDiagnosticId() { func setDiagnosticId() {
if result := <-api.Srv.Store.System().Get(); result.Err == nil { if result := <-api.Srv.Store.System().Get(); result.Err == nil {
props := result.Data.(model.StringMap) props := result.Data.(model.StringMap)
@@ -200,7 +208,7 @@ func doSecurityAndDiagnostics() {
v.Set(utils.PROP_DIAGNOSTIC_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10)) v.Set(utils.PROP_DIAGNOSTIC_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10))
} }
if ucr := <-api.Srv.Store.User().GetTotalActiveUsersCount(); ucr.Err == nil { if ucr := <-api.Srv.Store.Status().GetTotalActiveUsersCount(); ucr.Err == nil {
v.Set(utils.PROP_DIAGNOSTIC_ACTIVE_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10)) v.Set(utils.PROP_DIAGNOSTIC_ACTIVE_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10))
} }

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

@@ -1415,8 +1415,9 @@ func (c *Client) AdminResetPassword(userId, newPassword string) (*Result, *AppEr
} }
} }
func (c *Client) GetStatuses(data []string) (*Result, *AppError) { // GetStatuses returns a map of string statuses using user id as the key
if r, err := c.DoApiPost("/users/status", ArrayToJson(data)); err != nil { func (c *Client) GetStatuses() (*Result, *AppError) {
if r, err := c.DoApiGet("/users/status", "", ""); err != nil {
return nil, err return nil, err
} else { } else {
defer closeBody(r) defer closeBody(r)

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

@@ -195,6 +195,7 @@ type TeamSettings struct {
RestrictTeamInvite *string RestrictTeamInvite *string
RestrictPublicChannelManagement *string RestrictPublicChannelManagement *string
RestrictPrivateChannelManagement *string RestrictPrivateChannelManagement *string
UserStatusAwayTimeout *int64
} }
type LdapSettings struct { type LdapSettings struct {
@@ -435,6 +436,11 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.RestrictPrivateChannelManagement = PERMISSIONS_ALL *o.TeamSettings.RestrictPrivateChannelManagement = PERMISSIONS_ALL
} }
if o.TeamSettings.UserStatusAwayTimeout == nil {
o.TeamSettings.UserStatusAwayTimeout = new(int64)
*o.TeamSettings.UserStatusAwayTimeout = 300
}
if o.EmailSettings.EnableSignInWithEmail == nil { if o.EmailSettings.EnableSignInWithEmail == nil {
o.EmailSettings.EnableSignInWithEmail = new(bool) o.EmailSettings.EnableSignInWithEmail = new(bool)

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

@@ -0,0 +1,42 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
STATUS_OFFLINE = "offline"
STATUS_AWAY = "away"
STATUS_ONLINE = "online"
STATUS_CACHE_SIZE = 10000
)
type Status struct {
UserId string `json:"user_id"`
Status string `json:"status"`
LastActivityAt int64 `json:"last_activity_at"`
}
func (o *Status) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func StatusFromJson(data io.Reader) *Status {
decoder := json.NewDecoder(data)
var o Status
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

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

@@ -0,0 +1,27 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"strings"
"testing"
)
func TestStatus(t *testing.T) {
status := Status{NewId(), STATUS_ONLINE, 0}
json := status.ToJson()
status2 := StatusFromJson(strings.NewReader(json))
if status.UserId != status2.UserId {
t.Fatal("UserId should have matched")
}
if status.Status != status2.Status {
t.Fatal("Status should have matched")
}
if status.LastActivityAt != status2.LastActivityAt {
t.Fatal("LastActivityAt should have matched")
}
}

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

@@ -16,11 +16,6 @@ import (
const ( const (
ROLE_SYSTEM_ADMIN = "system_admin" ROLE_SYSTEM_ADMIN = "system_admin"
USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute
USER_OFFLINE = "offline"
USER_AWAY = "away"
USER_ONLINE = "online"
USER_NOTIFY_ALL = "all" USER_NOTIFY_ALL = "all"
USER_NOTIFY_MENTION = "mention" USER_NOTIFY_MENTION = "mention"
USER_NOTIFY_NONE = "none" USER_NOTIFY_NONE = "none"
@@ -44,8 +39,6 @@ type User struct {
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
Roles string `json:"roles"` Roles string `json:"roles"`
LastActivityAt int64 `json:"last_activity_at,omitempty"`
LastPingAt int64 `json:"last_ping_at,omitempty"`
AllowMarketing bool `json:"allow_marketing,omitempty"` AllowMarketing bool `json:"allow_marketing,omitempty"`
Props StringMap `json:"props,omitempty"` Props StringMap `json:"props,omitempty"`
NotifyProps StringMap `json:"notify_props,omitempty"` NotifyProps StringMap `json:"notify_props,omitempty"`
@@ -222,14 +215,6 @@ func (u *User) Etag(showFullName, showEmail bool) string {
return Etag(u.Id, u.UpdateAt, showFullName, showEmail) return Etag(u.Id, u.UpdateAt, showFullName, showEmail)
} }
func (u *User) IsOffline() bool {
return (GetMillis()-u.LastPingAt) > USER_OFFLINE_TIMEOUT && (GetMillis()-u.LastActivityAt) > USER_OFFLINE_TIMEOUT
}
func (u *User) IsAway() bool {
return (GetMillis() - u.LastActivityAt) > USER_AWAY_TIMEOUT
}
// Remove any private data from the user object // Remove any private data from the user object
func (u *User) Sanitize(options map[string]bool) { func (u *User) Sanitize(options map[string]bool) {
u.Password = "" u.Password = ""
@@ -258,7 +243,6 @@ func (u *User) ClearNonProfileFields() {
u.MfaActive = false u.MfaActive = false
u.MfaSecret = "" u.MfaSecret = ""
u.EmailVerified = false u.EmailVerified = false
u.LastPingAt = 0
u.AllowMarketing = false u.AllowMarketing = false
u.Props = StringMap{} u.Props = StringMap{}
u.NotifyProps = StringMap{} u.NotifyProps = StringMap{}

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

@@ -92,6 +92,8 @@ func (wsc *WebSocketClient) SendMessage(action string, data map[string]interface
wsc.Conn.WriteJSON(req) wsc.Conn.WriteJSON(req)
} }
// UserTyping will push a user_typing event out to all connected users
// who are in the specified channel
func (wsc *WebSocketClient) UserTyping(channelId, parentId string) { func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
data := map[string]interface{}{ data := map[string]interface{}{
"channel_id": channelId, "channel_id": channelId,
@@ -100,3 +102,8 @@ func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
wsc.SendMessage("user_typing", data) wsc.SendMessage("user_typing", data)
} }
// GetStatuses will return a map of string statuses using user id as the key
func (wsc *WebSocketClient) GetStatuses() {
wsc.SendMessage("get_statuses", nil)
}

166
store/sql_status_store.go Обычный файл
Просмотреть файл

@@ -0,0 +1,166 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package store
import (
"database/sql"
"github.com/mattermost/platform/model"
)
const (
MISSING_STATUS_ERROR = "store.sql_status.get.missing.app_error"
)
type SqlStatusStore struct {
*SqlStore
}
func NewSqlStatusStore(sqlStore *SqlStore) StatusStore {
s := &SqlStatusStore{sqlStore}
for _, db := range sqlStore.GetAllConns() {
table := db.AddTableWithName(model.Status{}, "Status").SetKeys(false, "UserId")
table.ColMap("UserId").SetMaxSize(26)
table.ColMap("Status").SetMaxSize(32)
}
return s
}
func (s SqlStatusStore) UpgradeSchemaIfNeeded() {
}
func (s SqlStatusStore) CreateIndexesIfNotExists() {
s.CreateIndexIfNotExists("idx_status_user_id", "Status", "UserId")
s.CreateIndexIfNotExists("idx_status_status", "Status", "Status")
}
func (s SqlStatusStore) SaveOrUpdate(status *model.Status) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if err := s.GetReplica().SelectOne(&model.Status{}, "SELECT * FROM Status WHERE UserId = :UserId", map[string]interface{}{"UserId": status.UserId}); err == nil {
if _, err := s.GetMaster().Update(status); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.SaveOrUpdate", "store.sql_status.update.app_error", nil, "")
}
} else {
if err := s.GetMaster().Insert(status); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.SaveOrUpdate", "store.sql_status.save.app_error", nil, "")
}
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlStatusStore) Get(userId string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
var status model.Status
if err := s.GetReplica().SelectOne(&status,
`SELECT
*
FROM
Status
WHERE
UserId = :UserId`, map[string]interface{}{"UserId": userId}); err != nil {
if err == sql.ErrNoRows {
result.Err = model.NewLocAppError("SqlStatusStore.Get", MISSING_STATUS_ERROR, nil, err.Error())
} else {
result.Err = model.NewLocAppError("SqlStatusStore.Get", "store.sql_status.get.app_error", nil, err.Error())
}
} else {
result.Data = &status
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlStatusStore) GetOnlineAway() StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
var statuses []*model.Status
if _, err := s.GetReplica().Select(&statuses, "SELECT * FROM Status WHERE Status = :Online OR Status = :Away", map[string]interface{}{"Online": model.STATUS_ONLINE, "Away": model.STATUS_AWAY}); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.GetOnline", "store.sql_status.get_online_away.app_error", nil, err.Error())
} else {
result.Data = statuses
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlStatusStore) ResetAll() StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if _, err := s.GetMaster().Exec("UPDATE Status SET Status = :Status", map[string]interface{}{"Status": model.STATUS_OFFLINE}); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.ResetAll", "store.sql_status.reset_all.app_error", nil, "")
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlStatusStore) GetTotalActiveUsersCount() StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
time := model.GetMillis() - (1000 * 60 * 60 * 24)
if count, err := s.GetReplica().SelectInt("SELECT COUNT(UserId) FROM Status WHERE LastActivityAt > :Time", map[string]interface{}{"Time": time}); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.GetTotalActiveUsersCount", "store.sql_status.get_total_active_users_count.app_error", nil, err.Error())
} else {
result.Data = count
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlStatusStore) UpdateLastActivityAt(userId string, lastActivityAt int64) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if _, err := s.GetMaster().Exec("UPDATE Status SET LastActivityAt = :Time WHERE UserId = :UserId", map[string]interface{}{"UserId": userId, "Time": lastActivityAt}); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.UpdateLastActivityAt", "store.sql_status.update_last_activity_at.app_error", nil, "")
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}

83
store/sql_status_store_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,83 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package store
import (
"github.com/mattermost/platform/model"
"testing"
)
func TestSqlStatusStore(t *testing.T) {
Setup()
status := &model.Status{model.NewId(), model.STATUS_ONLINE, 0}
if err := (<-store.Status().SaveOrUpdate(status)).Err; err != nil {
t.Fatal(err)
}
status.LastActivityAt = 10
if err := (<-store.Status().SaveOrUpdate(status)).Err; err != nil {
t.Fatal(err)
}
if err := (<-store.Status().Get(status.UserId)).Err; err != nil {
t.Fatal(err)
}
status2 := &model.Status{model.NewId(), model.STATUS_AWAY, 0}
if err := (<-store.Status().SaveOrUpdate(status2)).Err; err != nil {
t.Fatal(err)
}
status3 := &model.Status{model.NewId(), model.STATUS_OFFLINE, 0}
if err := (<-store.Status().SaveOrUpdate(status3)).Err; err != nil {
t.Fatal(err)
}
if result := <-store.Status().GetOnlineAway(); result.Err != nil {
t.Fatal(result.Err)
} else {
statuses := result.Data.([]*model.Status)
for _, status := range statuses {
if status.Status == model.STATUS_OFFLINE {
t.Fatal("should not have returned offline statuses")
}
}
}
if err := (<-store.Status().ResetAll()).Err; err != nil {
t.Fatal(err)
}
if result := <-store.Status().Get(status.UserId); result.Err != nil {
t.Fatal(result.Err)
} else {
status := result.Data.(*model.Status)
if status.Status != model.STATUS_OFFLINE {
t.Fatal("should be offline")
}
}
if result := <-store.Status().UpdateLastActivityAt(status.UserId, 10); result.Err != nil {
t.Fatal(result.Err)
}
}
func TestActiveUserCount(t *testing.T) {
Setup()
status := &model.Status{model.NewId(), model.STATUS_ONLINE, model.GetMillis()}
Must(store.Status().SaveOrUpdate(status))
if result := <-store.Status().GetTotalActiveUsersCount(); result.Err != nil {
t.Fatal(result.Err)
} else {
count := result.Data.(int64)
if count <= 0 {
t.Fatal()
}
}
}

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

@@ -53,6 +53,7 @@ type SqlStore struct {
license LicenseStore license LicenseStore
recovery PasswordRecoveryStore recovery PasswordRecoveryStore
emoji EmojiStore emoji EmojiStore
status StatusStore
SchemaVersion string SchemaVersion string
} }
@@ -129,6 +130,7 @@ func NewSqlStore() Store {
sqlStore.license = NewSqlLicenseStore(sqlStore) sqlStore.license = NewSqlLicenseStore(sqlStore)
sqlStore.recovery = NewSqlPasswordRecoveryStore(sqlStore) sqlStore.recovery = NewSqlPasswordRecoveryStore(sqlStore)
sqlStore.emoji = NewSqlEmojiStore(sqlStore) sqlStore.emoji = NewSqlEmojiStore(sqlStore)
sqlStore.status = NewSqlStatusStore(sqlStore)
err := sqlStore.master.CreateTablesIfNotExists() err := sqlStore.master.CreateTablesIfNotExists()
if err != nil { if err != nil {
@@ -152,6 +154,7 @@ func NewSqlStore() Store {
sqlStore.license.(*SqlLicenseStore).UpgradeSchemaIfNeeded() sqlStore.license.(*SqlLicenseStore).UpgradeSchemaIfNeeded()
sqlStore.recovery.(*SqlPasswordRecoveryStore).UpgradeSchemaIfNeeded() sqlStore.recovery.(*SqlPasswordRecoveryStore).UpgradeSchemaIfNeeded()
sqlStore.emoji.(*SqlEmojiStore).UpgradeSchemaIfNeeded() sqlStore.emoji.(*SqlEmojiStore).UpgradeSchemaIfNeeded()
sqlStore.status.(*SqlStatusStore).UpgradeSchemaIfNeeded()
sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists() sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists()
sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists() sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists()
@@ -168,6 +171,7 @@ func NewSqlStore() Store {
sqlStore.license.(*SqlLicenseStore).CreateIndexesIfNotExists() sqlStore.license.(*SqlLicenseStore).CreateIndexesIfNotExists()
sqlStore.recovery.(*SqlPasswordRecoveryStore).CreateIndexesIfNotExists() sqlStore.recovery.(*SqlPasswordRecoveryStore).CreateIndexesIfNotExists()
sqlStore.emoji.(*SqlEmojiStore).CreateIndexesIfNotExists() sqlStore.emoji.(*SqlEmojiStore).CreateIndexesIfNotExists()
sqlStore.status.(*SqlStatusStore).CreateIndexesIfNotExists()
sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures() sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
@@ -696,6 +700,10 @@ func (ss SqlStore) Emoji() EmojiStore {
return ss.emoji return ss.emoji
} }
func (ss SqlStore) Status() StatusStore {
return ss.status
}
func (ss SqlStore) DropAllTables() { func (ss SqlStore) DropAllTables() {
ss.master.TruncateTables() ss.master.TruncateTables()
} }

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

@@ -108,6 +108,10 @@ func (us SqlUserStore) UpgradeSchemaIfNeeded() {
us.Preference().Save(&data) us.Preference().Save(&data)
} }
} }
// ADDED for 3.3 remove for 3.7
us.RemoveColumnIfExists("Users", "LastActivityAt")
us.RemoveColumnIfExists("Users", "LastPingAt")
} }
func themeMigrationFailed(err error) { func themeMigrationFailed(err error) {
@@ -187,8 +191,6 @@ func (us SqlUserStore) Update(user *model.User, trustedUpdateData bool) StoreCha
user.Password = oldUser.Password user.Password = oldUser.Password
user.LastPasswordUpdate = oldUser.LastPasswordUpdate user.LastPasswordUpdate = oldUser.LastPasswordUpdate
user.LastPictureUpdate = oldUser.LastPictureUpdate user.LastPictureUpdate = oldUser.LastPictureUpdate
user.LastActivityAt = oldUser.LastActivityAt
user.LastPingAt = oldUser.LastPingAt
user.EmailVerified = oldUser.EmailVerified user.EmailVerified = oldUser.EmailVerified
user.FailedAttempts = oldUser.FailedAttempts user.FailedAttempts = oldUser.FailedAttempts
user.MfaSecret = oldUser.MfaSecret user.MfaSecret = oldUser.MfaSecret
@@ -283,65 +285,6 @@ func (us SqlUserStore) UpdateUpdateAt(userId string) StoreChannel {
return storeChannel return storeChannel
} }
func (us SqlUserStore) UpdateLastPingAt(userId string, time int64) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if _, err := us.GetMaster().Exec("UPDATE Users SET LastPingAt = :LastPingAt WHERE Id = :UserId", map[string]interface{}{"LastPingAt": time, "UserId": userId}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.UpdateLastPingAt", "store.sql_user.update_last_ping.app_error", nil, "user_id="+userId)
} else {
result.Data = userId
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (us SqlUserStore) UpdateLastActivityAt(userId string, time int64) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if _, err := us.GetMaster().Exec("UPDATE Users SET LastActivityAt = :LastActivityAt WHERE Id = :UserId", map[string]interface{}{"LastActivityAt": time, "UserId": userId}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.UpdateLastActivityAt", "store.sql_user.update_last_activity.app_error", nil, "user_id="+userId)
} else {
result.Data = userId
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (us SqlUserStore) UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if _, err := us.GetMaster().Exec("UPDATE Users SET LastActivityAt = :UserLastActivityAt WHERE Id = :UserId", map[string]interface{}{"UserLastActivityAt": time, "UserId": userId}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.UpdateLastActivityAt", "store.sql_user.update_last_activity.app_error", nil, "1 user_id="+userId+" session_id="+sessionId+" err="+err.Error())
} else if _, err := us.GetMaster().Exec("UPDATE Sessions SET LastActivityAt = :SessionLastActivityAt WHERE Id = :SessionId", map[string]interface{}{"SessionLastActivityAt": time, "SessionId": sessionId}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.UpdateLastActivityAt", "store.sql_user.update_last_activity.app_error", nil, "2 user_id="+userId+" session_id="+sessionId+" err="+err.Error())
} else {
result.Data = userId
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (us SqlUserStore) UpdatePassword(userId, hashedPassword string) StoreChannel { func (us SqlUserStore) UpdatePassword(userId, hashedPassword string) StoreChannel {
storeChannel := make(StoreChannel) storeChannel := make(StoreChannel)
@@ -988,27 +931,6 @@ func (us SqlUserStore) GetTotalUsersCount() StoreChannel {
return storeChannel return storeChannel
} }
func (us SqlUserStore) GetTotalActiveUsersCount() StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
time := model.GetMillis() - (1000 * 60 * 60 * 24)
if count, err := us.GetReplica().SelectInt("SELECT COUNT(Id) FROM Users WHERE LastActivityAt > :Time", map[string]interface{}{"Time": time}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.GetTotalActiveUsersCount", "store.sql_user.get_total_active_users_count.app_error", nil, err.Error())
} else {
result.Data = count
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (us SqlUserStore) PermanentDelete(userId string) StoreChannel { func (us SqlUserStore) PermanentDelete(userId string) StoreChannel {
storeChannel := make(StoreChannel) storeChannel := make(StoreChannel)

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

@@ -129,50 +129,6 @@ func TestUserStoreUpdateUpdateAt(t *testing.T) {
} }
func TestUserStoreUpdateLastPingAt(t *testing.T) {
Setup()
u1 := &model.User{}
u1.Email = model.NewId()
Must(store.User().Save(u1))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}))
if err := (<-store.User().UpdateLastPingAt(u1.Id, 1234567890)).Err; err != nil {
t.Fatal(err)
}
if r1 := <-store.User().Get(u1.Id); r1.Err != nil {
t.Fatal(r1.Err)
} else {
if r1.Data.(*model.User).LastPingAt != 1234567890 {
t.Fatal("LastPingAt not updated correctly")
}
}
}
func TestUserStoreUpdateLastActivityAt(t *testing.T) {
Setup()
u1 := &model.User{}
u1.Email = model.NewId()
Must(store.User().Save(u1))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}))
if err := (<-store.User().UpdateLastActivityAt(u1.Id, 1234567890)).Err; err != nil {
t.Fatal(err)
}
if r1 := <-store.User().Get(u1.Id); r1.Err != nil {
t.Fatal(r1.Err)
} else {
if r1.Data.(*model.User).LastActivityAt != 1234567890 {
t.Fatal("LastActivityAt not updated correctly")
}
}
}
func TestUserStoreUpdateFailedPasswordAttempts(t *testing.T) { func TestUserStoreUpdateFailedPasswordAttempts(t *testing.T) {
Setup() Setup()
@@ -189,41 +145,7 @@ func TestUserStoreUpdateFailedPasswordAttempts(t *testing.T) {
t.Fatal(r1.Err) t.Fatal(r1.Err)
} else { } else {
if r1.Data.(*model.User).FailedAttempts != 3 { if r1.Data.(*model.User).FailedAttempts != 3 {
t.Fatal("LastActivityAt not updated correctly") t.Fatal("FailedAttempts not updated correctly")
}
}
}
func TestUserStoreUpdateUserAndSessionActivity(t *testing.T) {
Setup()
u1 := &model.User{}
u1.Email = model.NewId()
Must(store.User().Save(u1))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}))
s1 := model.Session{}
s1.UserId = u1.Id
Must(store.Session().Save(&s1))
if err := (<-store.User().UpdateUserAndSessionActivity(u1.Id, s1.Id, 1234567890)).Err; err != nil {
t.Fatal(err)
}
if r1 := <-store.User().Get(u1.Id); r1.Err != nil {
t.Fatal(r1.Err)
} else {
if r1.Data.(*model.User).LastActivityAt != 1234567890 {
t.Fatal("LastActivityAt not updated correctly for user")
}
}
if r2 := <-store.Session().Get(s1.Id); r2.Err != nil {
t.Fatal(r2.Err)
} else {
if r2.Data.(*model.Session).LastActivityAt != 1234567890 {
t.Fatal("LastActivityAt not updated correctly for session")
} }
} }
@@ -268,25 +190,6 @@ func TestUserCount(t *testing.T) {
} }
} }
func TestActiveUserCount(t *testing.T) {
Setup()
u1 := &model.User{}
u1.Email = model.NewId()
Must(store.User().Save(u1))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}))
<-store.User().UpdateLastActivityAt(u1.Id, model.GetMillis())
if result := <-store.User().GetTotalActiveUsersCount(); result.Err != nil {
t.Fatal(result.Err)
} else {
count := result.Data.(int64)
if count <= 0 {
t.Fatal()
}
}
}
func TestUserStoreGetAllProfiles(t *testing.T) { func TestUserStoreGetAllProfiles(t *testing.T) {
Setup() Setup()

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

@@ -43,6 +43,7 @@ type Store interface {
License() LicenseStore License() LicenseStore
PasswordRecovery() PasswordRecoveryStore PasswordRecovery() PasswordRecoveryStore
Emoji() EmojiStore Emoji() EmojiStore
Status() StatusStore
MarkSystemRanUnitTests() MarkSystemRanUnitTests()
Close() Close()
DropAllTables() DropAllTables()
@@ -126,9 +127,6 @@ type UserStore interface {
Update(user *model.User, allowRoleUpdate bool) StoreChannel Update(user *model.User, allowRoleUpdate bool) StoreChannel
UpdateLastPictureUpdate(userId string) StoreChannel UpdateLastPictureUpdate(userId string) StoreChannel
UpdateUpdateAt(userId string) StoreChannel UpdateUpdateAt(userId string) StoreChannel
UpdateLastPingAt(userId string, time int64) StoreChannel
UpdateLastActivityAt(userId string, time int64) StoreChannel
UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel
UpdatePassword(userId, newPassword string) StoreChannel UpdatePassword(userId, newPassword string) StoreChannel
UpdateAuthData(userId string, service string, authData *string, email string) StoreChannel UpdateAuthData(userId string, service string, authData *string, email string) StoreChannel
UpdateMfaSecret(userId, secret string) StoreChannel UpdateMfaSecret(userId, secret string) StoreChannel
@@ -150,7 +148,6 @@ type UserStore interface {
GetEtagForDirectProfiles(userId string) StoreChannel GetEtagForDirectProfiles(userId string) StoreChannel
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
GetTotalUsersCount() StoreChannel GetTotalUsersCount() StoreChannel
GetTotalActiveUsersCount() StoreChannel
GetSystemAdminProfiles() StoreChannel GetSystemAdminProfiles() StoreChannel
PermanentDelete(userId string) StoreChannel PermanentDelete(userId string) StoreChannel
AnalyticsUniqueUserCount(teamId string) StoreChannel AnalyticsUniqueUserCount(teamId string) StoreChannel
@@ -264,3 +261,12 @@ type EmojiStore interface {
GetAll() StoreChannel GetAll() StoreChannel
Delete(id string, time int64) StoreChannel Delete(id string, time int64) StoreChannel
} }
type StatusStore interface {
SaveOrUpdate(status *model.Status) StoreChannel
Get(userId string) StoreChannel
GetOnlineAway() StoreChannel
ResetAll() StoreChannel
GetTotalActiveUsersCount() StoreChannel
UpdateLastActivityAt(userId string, lastActivityAt int64) StoreChannel
}

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

@@ -2,6 +2,9 @@
// See License.txt for license information. // See License.txt for license information.
import $ from 'jquery'; import $ from 'jquery';
import AppDispatcher from '../dispatcher/app_dispatcher.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 PostStore from 'stores/post_store.jsx'; import PostStore from 'stores/post_store.jsx';
@@ -14,12 +17,14 @@ import Client from 'utils/web_client.jsx';
import WebSocketClient from 'utils/websocket_client.jsx'; import WebSocketClient from 'utils/websocket_client.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.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 * as GlobalActions from 'actions/global_actions.jsx';
import * as UserActions from 'actions/user_actions.jsx'; import * as UserActions from 'actions/user_actions.jsx';
import {handleNewPost} from 'actions/post_actions.jsx'; import {handleNewPost} from 'actions/post_actions.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
const SocketEvents = Constants.SocketEvents; const SocketEvents = Constants.SocketEvents;
const ActionTypes = Constants.ActionTypes;
import {browserHistory} from 'react-router/es6'; import {browserHistory} from 'react-router/es6';
@@ -34,10 +39,10 @@ export function initialize() {
const connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + Client.getUsersRoute() + '/websocket'; const connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + Client.getUsersRoute() + '/websocket';
WebSocketClient.initialize(connUrl);
WebSocketClient.setEventCallback(handleEvent); WebSocketClient.setEventCallback(handleEvent);
WebSocketClient.setReconnectCallback(handleReconnect); WebSocketClient.setReconnectCallback(handleReconnect);
WebSocketClient.setCloseCallback(handleClose); WebSocketClient.setCloseCallback(handleClose);
WebSocketClient.initialize(connUrl);
} }
} }
@@ -45,9 +50,21 @@ export function close() {
WebSocketClient.close(); WebSocketClient.close();
} }
export function getStatuses() {
WebSocketClient.getStatuses(
(resp) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_STATUSES,
statuses: resp.data
});
}
);
}
function handleReconnect() { function handleReconnect() {
AsyncClient.getChannels(); AsyncClient.getChannels();
AsyncClient.getPosts(ChannelStore.getCurrentId()); AsyncClient.getPosts(ChannelStore.getCurrentId());
getStatuses();
ErrorStore.clearLastError(); ErrorStore.clearLastError();
ErrorStore.emitChange(); ErrorStore.emitChange();
} }
@@ -112,6 +129,10 @@ function handleEvent(msg) {
handleUserTypingEvent(msg); handleUserTypingEvent(msg);
break; break;
case SocketEvents.STATUS_CHANGED:
handleStatusChangedEvent(msg);
break;
default: default:
} }
} }
@@ -218,3 +239,7 @@ function handlePreferenceChangedEvent(msg) {
function handleUserTypingEvent(msg) { function handleUserTypingEvent(msg) {
GlobalActions.emitRemoteUserTypingEvent(msg.channel_id, msg.user_id, msg.data.parent_id); GlobalActions.emitRemoteUserTypingEvent(msg.channel_id, msg.user_id, msg.data.parent_id);
} }
function handleStatusChangedEvent(msg) {
UserStore.setStatus(msg.user_id, msg.data.status);
}

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

@@ -29,6 +29,7 @@ import * as TextFormatting from 'utils/text_formatting.jsx';
import * as AsyncClient from 'utils/async_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'utils/web_client.jsx'; import Client from 'utils/web_client.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
const UserStatuses = Constants.UserStatuses;
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6'; import {browserHistory} from 'react-router/es6';
@@ -189,6 +190,7 @@ export default class ChannelHeader extends React.Component {
if (teammate) { if (teammate) {
return UserStore.getStatus(teammate.id); return UserStore.getStatus(teammate.id);
} }
return UserStatuses.OFFLINE;
} }
return null; return null;
} }

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

@@ -8,13 +8,12 @@ import UserStore from 'stores/user_store.jsx';
import BrowserStore from 'stores/browser_store.jsx'; import BrowserStore from 'stores/browser_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import * as Websockets from 'actions/websocket_actions.jsx';
import * as GlobalActions from 'actions/global_actions.jsx'; import * as GlobalActions from 'actions/global_actions.jsx';
import * as WebSocketActions from 'actions/websocket_actions.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
import {browserHistory} from 'react-router/es6'; import {browserHistory} from 'react-router/es6';
const CLIENT_STATUS_INTERVAL = 30000;
const BACKSPACE_CHAR = 8; const BACKSPACE_CHAR = 8;
import React from 'react'; import React from 'react';
@@ -26,8 +25,8 @@ export default class LoggedIn extends React.Component {
this.onUserChanged = this.onUserChanged.bind(this); this.onUserChanged = this.onUserChanged.bind(this);
this.setupUser = this.setupUser.bind(this); this.setupUser = this.setupUser.bind(this);
// Initalize websockets // Initalize websocket
Websockets.initialize(); WebSocketActions.initialize();
// Force logout of all tabs if one tab is logged out // Force logout of all tabs if one tab is logged out
$(window).bind('storage', (e) => { $(window).bind('storage', (e) => {
@@ -109,10 +108,6 @@ export default class LoggedIn extends React.Component {
// Listen for user // Listen for user
UserStore.addChangeListener(this.onUserChanged); UserStore.addChangeListener(this.onUserChanged);
// Get all statuses regularally. (Soon to be switched to websocket)
AsyncClient.getStatuses();
this.intervalId = setInterval(() => AsyncClient.getStatuses(), CLIENT_STATUS_INTERVAL);
// ??? // ???
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) { $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
if (ev.type === 'mouseenter') { if (ev.type === 'mouseenter') {
@@ -144,7 +139,7 @@ export default class LoggedIn extends React.Component {
} }
}); });
// Pervent backspace from navigating back a page // Prevent backspace from navigating back a page
$(window).on('keydown.preventBackspace', (e) => { $(window).on('keydown.preventBackspace', (e) => {
if (e.which === BACKSPACE_CHAR && !$(e.target).is('input, textarea')) { if (e.which === BACKSPACE_CHAR && !$(e.target).is('input, textarea')) {
e.preventDefault(); e.preventDefault();
@@ -159,9 +154,8 @@ export default class LoggedIn extends React.Component {
componentWillUnmount() { componentWillUnmount() {
$('#root').attr('class', ''); $('#root').attr('class', '');
clearInterval(this.intervalId);
Websockets.close(); WebSocketActions.close();
UserStore.removeChangeListener(this.onUserChanged); UserStore.removeChangeListener(this.onUserChanged);
$('body').off('click.userpopover'); $('body').off('click.userpopover');

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

@@ -125,7 +125,7 @@ export default class Sidebar extends React.Component {
directChannel.display_name = Utils.displayUsername(teammateId); directChannel.display_name = Utils.displayUsername(teammateId);
directChannel.teammate_id = teammateId; directChannel.teammate_id = teammateId;
directChannel.status = UserStore.getStatus(teammateId); directChannel.status = UserStore.getStatus(teammateId) || 'offline';
if (UserStore.hasTeamProfile(teammateId)) { if (UserStore.hasTeamProfile(teammateId)) {
directChannels.push(directChannel); directChannels.push(directChannel);

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

@@ -18,7 +18,7 @@
"keymirror": "0.1.1", "keymirror": "0.1.1",
"marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b", "marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b",
"match-at": "0.1.0", "match-at": "0.1.0",
"mattermost": "mattermost/mattermost-javascript#4cdaeba22ff82bf93dc417af1ab4e89e3248d624", "mattermost": "mattermost/mattermost-javascript#84b6f1ebf33aa4b5d8e7ddd7be97d3f5bff5ed17",
"object-assign": "4.1.0", "object-assign": "4.1.0",
"perfect-scrollbar": "0.6.12", "perfect-scrollbar": "0.6.12",
"react": "15.2.1", "react": "15.2.1",

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

@@ -9,6 +9,7 @@ import LocalizationStore from './localization_store.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes; const ActionTypes = Constants.ActionTypes;
const UserStatuses = Constants.UserStatuses;
const CHANGE_EVENT_DM_LIST = 'change_dm_list'; const CHANGE_EVENT_DM_LIST = 'change_dm_list';
const CHANGE_EVENT = 'change'; const CHANGE_EVENT = 'change';
@@ -292,18 +293,11 @@ class UserStoreClass extends EventEmitter {
} }
setStatuses(statuses) { setStatuses(statuses) {
this.pSetStatuses(statuses); this.statuses = Object.assign(this.statuses, statuses);
this.emitStatusesChange();
}
pSetStatuses(statuses) {
this.statuses = statuses;
} }
setStatus(userId, status) { setStatus(userId, status) {
var statuses = this.getStatuses(); this.statuses[userId] = status;
statuses[userId] = status;
this.pSetStatuses(statuses);
this.emitStatusesChange(); this.emitStatusesChange();
} }
@@ -312,7 +306,7 @@ class UserStoreClass extends EventEmitter {
} }
getStatus(id) { getStatus(id) {
return this.getStatuses()[id]; return this.getStatuses()[id] || UserStatuses.OFFLINE;
} }
getNoAccounts() { getNoAccounts() {
@@ -370,7 +364,7 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => {
UserStore.emitAuditsChange(); UserStore.emitAuditsChange();
break; break;
case ActionTypes.RECEIVED_STATUSES: case ActionTypes.RECEIVED_STATUSES:
UserStore.pSetStatuses(action.statuses); UserStore.setStatuses(action.statuses);
UserStore.emitStatusesChange(); UserStore.emitStatusesChange();
break; break;
default: default:

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

@@ -7,7 +7,6 @@ import * as GlobalActions from 'actions/global_actions.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import BrowserStore from 'stores/browser_store.jsx'; import BrowserStore from 'stores/browser_store.jsx';
import ChannelStore from 'stores/channel_store.jsx'; import ChannelStore from 'stores/channel_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import PostStore from 'stores/post_store.jsx'; import PostStore from 'stores/post_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';
import * as utils from './utils.jsx'; import * as utils from './utils.jsx';
@@ -744,21 +743,12 @@ export function getMe() {
} }
export function getStatuses() { export function getStatuses() {
const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); if (isCallInProgress('getStatuses')) {
const teammateIds = [];
for (const [name, value] of preferences) {
if (value === 'true') {
teammateIds.push(name);
}
}
if (isCallInProgress('getStatuses') || teammateIds.length === 0) {
return; return;
} }
callTracker.getStatuses = utils.getTimestamp(); callTracker.getStatuses = utils.getTimestamp();
Client.getStatuses(teammateIds, Client.getStatuses(
(data) => { (data) => {
callTracker.getStatuses = 0; callTracker.getStatuses = 0;
@@ -1535,4 +1525,4 @@ export function deleteEmoji(id) {
dispatchError(err, 'deleteEmoji'); dispatchError(err, 'deleteEmoji');
} }
); );
} }

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

@@ -191,7 +191,8 @@ export const Constants = {
USER_REMOVED: 'user_removed', USER_REMOVED: 'user_removed',
TYPING: 'typing', TYPING: 'typing',
PREFERENCE_CHANGED: 'preference_changed', PREFERENCE_CHANGED: 'preference_changed',
EPHEMERAL_MESSAGE: 'ephemeral_message' EPHEMERAL_MESSAGE: 'ephemeral_message',
STATUS_CHANGED: 'status_change'
}, },
UserUpdateEvents: { UserUpdateEvents: {
@@ -210,6 +211,12 @@ export const Constants = {
POST: 5 POST: 5
}, },
UserStatuses: {
OFFLINE: 'offline',
AWAY: 'away',
ONLINE: 'online'
},
SPECIAL_MENTIONS: ['all', 'channel'], SPECIAL_MENTIONS: ['all', 'channel'],
CHARACTER_LIMIT: 4000, CHARACTER_LIMIT: 4000,
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'], IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'],