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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
180adc79af
Коммит
c0ab2636d6
2
Makefile
2
Makefile
@@ -159,7 +159,7 @@ test-server: start-docker prepare-enterprise
|
||||
rm -f 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=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
|
||||
|
||||
@@ -94,6 +94,7 @@ func InitApi() {
|
||||
InitPreference()
|
||||
InitLicense()
|
||||
InitEmoji()
|
||||
InitStatus()
|
||||
|
||||
// 404 on any api route before web.go has a chance to serve it
|
||||
Srv.Router.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404))
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
)
|
||||
|
||||
var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE)
|
||||
var statusCache *utils.Cache = utils.NewLru(model.STATUS_CACHE_SIZE)
|
||||
|
||||
var allowedMethods []string = []string{
|
||||
"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 {
|
||||
go func() {
|
||||
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)
|
||||
}
|
||||
}()
|
||||
SetStatusOnline(c.Session.UserId, c.Session.Id)
|
||||
}
|
||||
|
||||
if c.Err == nil {
|
||||
|
||||
@@ -73,7 +73,12 @@ func ping(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(model.MapToJson(m)))
|
||||
}
|
||||
|
||||
func webSocketPing(req *model.WebSocketRequest, responseData map[string]interface{}) *model.AppError {
|
||||
responseData["text"] = "pong"
|
||||
return nil
|
||||
func webSocketPing(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) {
|
||||
data := map[string]interface{}{}
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
155
api/status.go
Обычный файл
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
Обычный файл
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")
|
||||
}
|
||||
}
|
||||
38
api/user.go
38
api/user.go
@@ -54,7 +54,6 @@ func InitUser() {
|
||||
BaseRoutes.Users.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
|
||||
BaseRoutes.Users.Handle("/me", ApiAppHandler(getMe)).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("/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")
|
||||
@@ -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 {
|
||||
|
||||
if !model.IsValidUsername(name) {
|
||||
@@ -2312,7 +2282,7 @@ func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.Err = error
|
||||
return
|
||||
} else {
|
||||
if user.LastActivityAt > 0 {
|
||||
if _, err := GetStatus(user.Id); err != nil {
|
||||
go SendEmailChangeVerifyEmail(c, user.Id, user.Email, c.GetSiteURL())
|
||||
} else {
|
||||
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 channelId string
|
||||
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
|
||||
@@ -2567,5 +2537,5 @@ func userTyping(req *model.WebSocketRequest, responseData map[string]interface{}
|
||||
event.Add("parent_id", parentId)
|
||||
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 = 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", LastActivityAt: time1, LastPingAt: time1, Roles: ""}
|
||||
user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", Roles: ""}
|
||||
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||
LinkUserToTeam(user, team)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||
@@ -683,15 +681,7 @@ func TestUserUpdate(t *testing.T) {
|
||||
Client.Login(user.Email, "passwd1")
|
||||
Client.SetTeamId(team.Id)
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
time2 := model.GetMillis()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
user.Nickname = "Jim Jimmy"
|
||||
user.LastActivityAt = time2
|
||||
user.LastPingAt = time2
|
||||
user.Roles = model.ROLE_TEAM_ADMIN
|
||||
user.LastPasswordUpdate = 123
|
||||
|
||||
@@ -701,12 +691,6 @@ func TestUserUpdate(t *testing.T) {
|
||||
if result.Data.(*model.User).Nickname != "Jim Jimmy" {
|
||||
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 != "" {
|
||||
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) {
|
||||
th := Setup()
|
||||
Client := th.CreateClient()
|
||||
|
||||
@@ -7,9 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/gorilla/websocket"
|
||||
goi18n "github.com/nicksnyder/go-i18n/i18n"
|
||||
)
|
||||
@@ -34,18 +32,7 @@ type WebConn struct {
|
||||
}
|
||||
|
||||
func NewWebConn(c *Context, ws *websocket.Conn) *WebConn {
|
||||
go func() {
|
||||
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)
|
||||
}
|
||||
}()
|
||||
go SetStatusOnline(c.Session.UserId, c.Session.Id)
|
||||
|
||||
return &WebConn{
|
||||
Send: make(chan model.WebSocketMessage, 64),
|
||||
@@ -68,13 +55,7 @@ func (c *WebConn) readPump() {
|
||||
c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT))
|
||||
c.WebSocket.SetPongHandler(func(string) error {
|
||||
c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT))
|
||||
|
||||
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)
|
||||
}
|
||||
}()
|
||||
|
||||
go SetStatusAwayIfNeeded(c.UserId)
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
@@ -67,10 +67,23 @@ func (h *Hub) Start() {
|
||||
h.connections[webCon] = true
|
||||
|
||||
case webCon := <-h.unregister:
|
||||
userId := webCon.UserId
|
||||
if _, ok := h.connections[webCon]; ok {
|
||||
delete(h.connections, webCon)
|
||||
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:
|
||||
for webCon := range h.connections {
|
||||
if webCon.UserId == userId {
|
||||
|
||||
@@ -10,12 +10,12 @@ import (
|
||||
"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}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -25,9 +25,10 @@ func (wh *webSocketHandler) ServeWebSocket(conn *WebConn, r *model.WebSocketRequ
|
||||
r.T = conn.T
|
||||
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)
|
||||
err.DetailedError = ""
|
||||
conn.Send <- model.NewWebSocketError(r.Seq, err)
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"RestrictDirectMessage": "any",
|
||||
"RestrictTeamInvite": "all",
|
||||
"RestrictPublicChannelManagement": "all",
|
||||
"RestrictPrivateChannelManagement": "all"
|
||||
"RestrictPrivateChannelManagement": "all",
|
||||
"UserStatusAwayTimeout": 300
|
||||
},
|
||||
"SqlSettings": {
|
||||
"DriverName": "mysql",
|
||||
|
||||
34
i18n/en.json
34
i18n/en.json
@@ -1980,12 +1980,12 @@
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"id": "api.web_conn.new_web_conn.last_ping.error",
|
||||
"translation": "Failed to update LastPingAt for user_id=%v, err=%v"
|
||||
"id": "api.status.save_status.error",
|
||||
"translation": "Failed to save status for user_id=%v, err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.web_hub.start.stopping.debug",
|
||||
@@ -4036,8 +4036,32 @@
|
||||
"translation": "We encountered an error while finding user profiles"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_user.get_total_active_users_count.app_error",
|
||||
"translation": "We could not count the users"
|
||||
"id": "store.sql_status.get_total_active_users_count.app_error",
|
||||
"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",
|
||||
|
||||
@@ -125,6 +125,8 @@ func main() {
|
||||
if flagRunCmds {
|
||||
runCmds()
|
||||
} else {
|
||||
resetStatuses()
|
||||
|
||||
api.StartServer()
|
||||
|
||||
// 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() {
|
||||
if result := <-api.Srv.Store.System().Get(); result.Err == nil {
|
||||
props := result.Data.(model.StringMap)
|
||||
@@ -200,7 +208,7 @@ func doSecurityAndDiagnostics() {
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
@@ -1415,8 +1415,9 @@ func (c *Client) AdminResetPassword(userId, newPassword string) (*Result, *AppEr
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetStatuses(data []string) (*Result, *AppError) {
|
||||
if r, err := c.DoApiPost("/users/status", ArrayToJson(data)); err != nil {
|
||||
// GetStatuses returns a map of string statuses using user id as the key
|
||||
func (c *Client) GetStatuses() (*Result, *AppError) {
|
||||
if r, err := c.DoApiGet("/users/status", "", ""); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
|
||||
@@ -195,6 +195,7 @@ type TeamSettings struct {
|
||||
RestrictTeamInvite *string
|
||||
RestrictPublicChannelManagement *string
|
||||
RestrictPrivateChannelManagement *string
|
||||
UserStatusAwayTimeout *int64
|
||||
}
|
||||
|
||||
type LdapSettings struct {
|
||||
@@ -435,6 +436,11 @@ func (o *Config) SetDefaults() {
|
||||
*o.TeamSettings.RestrictPrivateChannelManagement = PERMISSIONS_ALL
|
||||
}
|
||||
|
||||
if o.TeamSettings.UserStatusAwayTimeout == nil {
|
||||
o.TeamSettings.UserStatusAwayTimeout = new(int64)
|
||||
*o.TeamSettings.UserStatusAwayTimeout = 300
|
||||
}
|
||||
|
||||
if o.EmailSettings.EnableSignInWithEmail == nil {
|
||||
o.EmailSettings.EnableSignInWithEmail = new(bool)
|
||||
|
||||
|
||||
42
model/status.go
Обычный файл
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
Обычный файл
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 (
|
||||
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_MENTION = "mention"
|
||||
USER_NOTIFY_NONE = "none"
|
||||
@@ -44,8 +39,6 @@ type User struct {
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Roles string `json:"roles"`
|
||||
LastActivityAt int64 `json:"last_activity_at,omitempty"`
|
||||
LastPingAt int64 `json:"last_ping_at,omitempty"`
|
||||
AllowMarketing bool `json:"allow_marketing,omitempty"`
|
||||
Props StringMap `json:"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)
|
||||
}
|
||||
|
||||
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
|
||||
func (u *User) Sanitize(options map[string]bool) {
|
||||
u.Password = ""
|
||||
@@ -258,7 +243,6 @@ func (u *User) ClearNonProfileFields() {
|
||||
u.MfaActive = false
|
||||
u.MfaSecret = ""
|
||||
u.EmailVerified = false
|
||||
u.LastPingAt = 0
|
||||
u.AllowMarketing = false
|
||||
u.Props = StringMap{}
|
||||
u.NotifyProps = StringMap{}
|
||||
|
||||
@@ -92,6 +92,8 @@ func (wsc *WebSocketClient) SendMessage(action string, data map[string]interface
|
||||
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) {
|
||||
data := map[string]interface{}{
|
||||
"channel_id": channelId,
|
||||
@@ -100,3 +102,8 @@ func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
|
||||
|
||||
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
Обычный файл
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
Обычный файл
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
|
||||
recovery PasswordRecoveryStore
|
||||
emoji EmojiStore
|
||||
status StatusStore
|
||||
SchemaVersion string
|
||||
}
|
||||
|
||||
@@ -129,6 +130,7 @@ func NewSqlStore() Store {
|
||||
sqlStore.license = NewSqlLicenseStore(sqlStore)
|
||||
sqlStore.recovery = NewSqlPasswordRecoveryStore(sqlStore)
|
||||
sqlStore.emoji = NewSqlEmojiStore(sqlStore)
|
||||
sqlStore.status = NewSqlStatusStore(sqlStore)
|
||||
|
||||
err := sqlStore.master.CreateTablesIfNotExists()
|
||||
if err != nil {
|
||||
@@ -152,6 +154,7 @@ func NewSqlStore() Store {
|
||||
sqlStore.license.(*SqlLicenseStore).UpgradeSchemaIfNeeded()
|
||||
sqlStore.recovery.(*SqlPasswordRecoveryStore).UpgradeSchemaIfNeeded()
|
||||
sqlStore.emoji.(*SqlEmojiStore).UpgradeSchemaIfNeeded()
|
||||
sqlStore.status.(*SqlStatusStore).UpgradeSchemaIfNeeded()
|
||||
|
||||
sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists()
|
||||
sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists()
|
||||
@@ -168,6 +171,7 @@ func NewSqlStore() Store {
|
||||
sqlStore.license.(*SqlLicenseStore).CreateIndexesIfNotExists()
|
||||
sqlStore.recovery.(*SqlPasswordRecoveryStore).CreateIndexesIfNotExists()
|
||||
sqlStore.emoji.(*SqlEmojiStore).CreateIndexesIfNotExists()
|
||||
sqlStore.status.(*SqlStatusStore).CreateIndexesIfNotExists()
|
||||
|
||||
sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
|
||||
|
||||
@@ -696,6 +700,10 @@ func (ss SqlStore) Emoji() EmojiStore {
|
||||
return ss.emoji
|
||||
}
|
||||
|
||||
func (ss SqlStore) Status() StatusStore {
|
||||
return ss.status
|
||||
}
|
||||
|
||||
func (ss SqlStore) DropAllTables() {
|
||||
ss.master.TruncateTables()
|
||||
}
|
||||
|
||||
@@ -108,6 +108,10 @@ func (us SqlUserStore) UpgradeSchemaIfNeeded() {
|
||||
us.Preference().Save(&data)
|
||||
}
|
||||
}
|
||||
|
||||
// ADDED for 3.3 remove for 3.7
|
||||
us.RemoveColumnIfExists("Users", "LastActivityAt")
|
||||
us.RemoveColumnIfExists("Users", "LastPingAt")
|
||||
}
|
||||
|
||||
func themeMigrationFailed(err error) {
|
||||
@@ -187,8 +191,6 @@ func (us SqlUserStore) Update(user *model.User, trustedUpdateData bool) StoreCha
|
||||
user.Password = oldUser.Password
|
||||
user.LastPasswordUpdate = oldUser.LastPasswordUpdate
|
||||
user.LastPictureUpdate = oldUser.LastPictureUpdate
|
||||
user.LastActivityAt = oldUser.LastActivityAt
|
||||
user.LastPingAt = oldUser.LastPingAt
|
||||
user.EmailVerified = oldUser.EmailVerified
|
||||
user.FailedAttempts = oldUser.FailedAttempts
|
||||
user.MfaSecret = oldUser.MfaSecret
|
||||
@@ -283,65 +285,6 @@ func (us SqlUserStore) UpdateUpdateAt(userId string) 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 {
|
||||
|
||||
storeChannel := make(StoreChannel)
|
||||
@@ -988,27 +931,6 @@ func (us SqlUserStore) GetTotalUsersCount() 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 {
|
||||
|
||||
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) {
|
||||
Setup()
|
||||
|
||||
@@ -189,41 +145,7 @@ func TestUserStoreUpdateFailedPasswordAttempts(t *testing.T) {
|
||||
t.Fatal(r1.Err)
|
||||
} else {
|
||||
if r1.Data.(*model.User).FailedAttempts != 3 {
|
||||
t.Fatal("LastActivityAt 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")
|
||||
t.Fatal("FailedAttempts not updated correctly")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
Setup()
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ type Store interface {
|
||||
License() LicenseStore
|
||||
PasswordRecovery() PasswordRecoveryStore
|
||||
Emoji() EmojiStore
|
||||
Status() StatusStore
|
||||
MarkSystemRanUnitTests()
|
||||
Close()
|
||||
DropAllTables()
|
||||
@@ -126,9 +127,6 @@ type UserStore interface {
|
||||
Update(user *model.User, allowRoleUpdate bool) StoreChannel
|
||||
UpdateLastPictureUpdate(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
|
||||
UpdateAuthData(userId string, service string, authData *string, email string) StoreChannel
|
||||
UpdateMfaSecret(userId, secret string) StoreChannel
|
||||
@@ -150,7 +148,6 @@ type UserStore interface {
|
||||
GetEtagForDirectProfiles(userId string) StoreChannel
|
||||
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
|
||||
GetTotalUsersCount() StoreChannel
|
||||
GetTotalActiveUsersCount() StoreChannel
|
||||
GetSystemAdminProfiles() StoreChannel
|
||||
PermanentDelete(userId string) StoreChannel
|
||||
AnalyticsUniqueUserCount(teamId string) StoreChannel
|
||||
@@ -264,3 +261,12 @@ type EmojiStore interface {
|
||||
GetAll() 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.
|
||||
|
||||
import $ from 'jquery';
|
||||
|
||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import TeamStore from 'stores/team_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 * as Utils from 'utils/utils.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||
import * as UserActions from 'actions/user_actions.jsx';
|
||||
import {handleNewPost} from 'actions/post_actions.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const SocketEvents = Constants.SocketEvents;
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
|
||||
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';
|
||||
|
||||
WebSocketClient.initialize(connUrl);
|
||||
WebSocketClient.setEventCallback(handleEvent);
|
||||
WebSocketClient.setReconnectCallback(handleReconnect);
|
||||
WebSocketClient.setCloseCallback(handleClose);
|
||||
WebSocketClient.initialize(connUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,9 +50,21 @@ export function close() {
|
||||
WebSocketClient.close();
|
||||
}
|
||||
|
||||
export function getStatuses() {
|
||||
WebSocketClient.getStatuses(
|
||||
(resp) => {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_STATUSES,
|
||||
statuses: resp.data
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function handleReconnect() {
|
||||
AsyncClient.getChannels();
|
||||
AsyncClient.getPosts(ChannelStore.getCurrentId());
|
||||
getStatuses();
|
||||
ErrorStore.clearLastError();
|
||||
ErrorStore.emitChange();
|
||||
}
|
||||
@@ -112,6 +129,10 @@ function handleEvent(msg) {
|
||||
handleUserTypingEvent(msg);
|
||||
break;
|
||||
|
||||
case SocketEvents.STATUS_CHANGED:
|
||||
handleStatusChangedEvent(msg);
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -218,3 +239,7 @@ function handlePreferenceChangedEvent(msg) {
|
||||
function handleUserTypingEvent(msg) {
|
||||
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 Client from 'utils/web_client.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const UserStatuses = Constants.UserStatuses;
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {browserHistory} from 'react-router/es6';
|
||||
@@ -189,6 +190,7 @@ export default class ChannelHeader extends React.Component {
|
||||
if (teammate) {
|
||||
return UserStore.getStatus(teammate.id);
|
||||
}
|
||||
return UserStatuses.OFFLINE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -8,13 +8,12 @@ import UserStore from 'stores/user_store.jsx';
|
||||
import BrowserStore from 'stores/browser_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_store.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 WebSocketActions from 'actions/websocket_actions.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {browserHistory} from 'react-router/es6';
|
||||
|
||||
const CLIENT_STATUS_INTERVAL = 30000;
|
||||
const BACKSPACE_CHAR = 8;
|
||||
|
||||
import React from 'react';
|
||||
@@ -26,8 +25,8 @@ export default class LoggedIn extends React.Component {
|
||||
this.onUserChanged = this.onUserChanged.bind(this);
|
||||
this.setupUser = this.setupUser.bind(this);
|
||||
|
||||
// Initalize websockets
|
||||
Websockets.initialize();
|
||||
// Initalize websocket
|
||||
WebSocketActions.initialize();
|
||||
|
||||
// Force logout of all tabs if one tab is logged out
|
||||
$(window).bind('storage', (e) => {
|
||||
@@ -109,10 +108,6 @@ export default class LoggedIn extends React.Component {
|
||||
// Listen for user
|
||||
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) {
|
||||
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) => {
|
||||
if (e.which === BACKSPACE_CHAR && !$(e.target).is('input, textarea')) {
|
||||
e.preventDefault();
|
||||
@@ -159,9 +154,8 @@ export default class LoggedIn extends React.Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
$('#root').attr('class', '');
|
||||
clearInterval(this.intervalId);
|
||||
|
||||
Websockets.close();
|
||||
WebSocketActions.close();
|
||||
UserStore.removeChangeListener(this.onUserChanged);
|
||||
|
||||
$('body').off('click.userpopover');
|
||||
|
||||
@@ -125,7 +125,7 @@ export default class Sidebar extends React.Component {
|
||||
|
||||
directChannel.display_name = Utils.displayUsername(teammateId);
|
||||
directChannel.teammate_id = teammateId;
|
||||
directChannel.status = UserStore.getStatus(teammateId);
|
||||
directChannel.status = UserStore.getStatus(teammateId) || 'offline';
|
||||
|
||||
if (UserStore.hasTeamProfile(teammateId)) {
|
||||
directChannels.push(directChannel);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"keymirror": "0.1.1",
|
||||
"marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b",
|
||||
"match-at": "0.1.0",
|
||||
"mattermost": "mattermost/mattermost-javascript#4cdaeba22ff82bf93dc417af1ab4e89e3248d624",
|
||||
"mattermost": "mattermost/mattermost-javascript#84b6f1ebf33aa4b5d8e7ddd7be97d3f5bff5ed17",
|
||||
"object-assign": "4.1.0",
|
||||
"perfect-scrollbar": "0.6.12",
|
||||
"react": "15.2.1",
|
||||
|
||||
@@ -9,6 +9,7 @@ import LocalizationStore from './localization_store.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
const UserStatuses = Constants.UserStatuses;
|
||||
|
||||
const CHANGE_EVENT_DM_LIST = 'change_dm_list';
|
||||
const CHANGE_EVENT = 'change';
|
||||
@@ -292,18 +293,11 @@ class UserStoreClass extends EventEmitter {
|
||||
}
|
||||
|
||||
setStatuses(statuses) {
|
||||
this.pSetStatuses(statuses);
|
||||
this.emitStatusesChange();
|
||||
}
|
||||
|
||||
pSetStatuses(statuses) {
|
||||
this.statuses = statuses;
|
||||
this.statuses = Object.assign(this.statuses, statuses);
|
||||
}
|
||||
|
||||
setStatus(userId, status) {
|
||||
var statuses = this.getStatuses();
|
||||
statuses[userId] = status;
|
||||
this.pSetStatuses(statuses);
|
||||
this.statuses[userId] = status;
|
||||
this.emitStatusesChange();
|
||||
}
|
||||
|
||||
@@ -312,7 +306,7 @@ class UserStoreClass extends EventEmitter {
|
||||
}
|
||||
|
||||
getStatus(id) {
|
||||
return this.getStatuses()[id];
|
||||
return this.getStatuses()[id] || UserStatuses.OFFLINE;
|
||||
}
|
||||
|
||||
getNoAccounts() {
|
||||
@@ -370,7 +364,7 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => {
|
||||
UserStore.emitAuditsChange();
|
||||
break;
|
||||
case ActionTypes.RECEIVED_STATUSES:
|
||||
UserStore.pSetStatuses(action.statuses);
|
||||
UserStore.setStatuses(action.statuses);
|
||||
UserStore.emitStatusesChange();
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -7,7 +7,6 @@ import * as GlobalActions from 'actions/global_actions.jsx';
|
||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||
import BrowserStore from 'stores/browser_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import * as utils from './utils.jsx';
|
||||
@@ -744,21 +743,12 @@ export function getMe() {
|
||||
}
|
||||
|
||||
export function getStatuses() {
|
||||
const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
|
||||
|
||||
const teammateIds = [];
|
||||
for (const [name, value] of preferences) {
|
||||
if (value === 'true') {
|
||||
teammateIds.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
if (isCallInProgress('getStatuses') || teammateIds.length === 0) {
|
||||
if (isCallInProgress('getStatuses')) {
|
||||
return;
|
||||
}
|
||||
|
||||
callTracker.getStatuses = utils.getTimestamp();
|
||||
Client.getStatuses(teammateIds,
|
||||
Client.getStatuses(
|
||||
(data) => {
|
||||
callTracker.getStatuses = 0;
|
||||
|
||||
@@ -1535,4 +1525,4 @@ export function deleteEmoji(id) {
|
||||
dispatchError(err, 'deleteEmoji');
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +191,8 @@ export const Constants = {
|
||||
USER_REMOVED: 'user_removed',
|
||||
TYPING: 'typing',
|
||||
PREFERENCE_CHANGED: 'preference_changed',
|
||||
EPHEMERAL_MESSAGE: 'ephemeral_message'
|
||||
EPHEMERAL_MESSAGE: 'ephemeral_message',
|
||||
STATUS_CHANGED: 'status_change'
|
||||
},
|
||||
|
||||
UserUpdateEvents: {
|
||||
@@ -210,6 +211,12 @@ export const Constants = {
|
||||
POST: 5
|
||||
},
|
||||
|
||||
UserStatuses: {
|
||||
OFFLINE: 'offline',
|
||||
AWAY: 'away',
|
||||
ONLINE: 'online'
|
||||
},
|
||||
|
||||
SPECIAL_MENTIONS: ['all', 'channel'],
|
||||
CHARACTER_LIMIT: 4000,
|
||||
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'],
|
||||
|
||||
Ссылка в новой задаче
Block a user