From 3229457de0eb6e04fff6ce4fe1466a828be5f6f6 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 1 Oct 2015 09:16:25 -0400 Subject: Added model class for user preferences object --- model/preference.go | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ model/preference_test.go | 62 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 model/preference.go create mode 100644 model/preference_test.go diff --git a/model/preference.go b/model/preference.go new file mode 100644 index 000000000..900c1d7a0 --- /dev/null +++ b/model/preference.go @@ -0,0 +1,74 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + PREFERENCE_CATEGORY_DIRECT_CHANNELS = "direct_channels" + PREFERENCE_NAME_SHOWHIDE = "show_hide" +) + +type Preference struct { + UserId string `json:"user_id"` + Category string `json:"category"` + Name string `json:"name"` + AltId string `json:"alt_id"` + Value string `json:"value"` +} + +func (o *Preference) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func PreferenceFromJson(data io.Reader) *Preference { + decoder := json.NewDecoder(data) + var o Preference + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func (o *Preference) IsValid() *AppError { + if len(o.UserId) != 26 { + return NewAppError("Preference.IsValid", "Invalid user id", "user_id="+o.UserId) + } + + if len(o.Category) == 0 || len(o.Category) > 32 || !IsPreferenceCategoryValid(o.Category) { + return NewAppError("Preference.IsValid", "Invalid category", "category="+o.Category) + } + + if len(o.Name) == 0 || len(o.Name) > 32 || !IsPreferenceNameValid(o.Name) { + return NewAppError("Preference.IsValid", "Invalid name", "name="+o.Name) + } + + if o.AltId != "" && len(o.AltId) != 26 { + return NewAppError("Preference.IsValid", "Invalid alternate id", "altId="+o.AltId) + } + + if len(o.Value) > 128 { + return NewAppError("Preference.IsValid", "Value is too long", "value="+o.Value) + } + + return nil +} + +func IsPreferenceCategoryValid(category string) bool { + return category == PREFERENCE_CATEGORY_DIRECT_CHANNELS +} + +func IsPreferenceNameValid(name string) bool { + return name == PREFERENCE_NAME_SHOWHIDE +} diff --git a/model/preference_test.go b/model/preference_test.go new file mode 100644 index 000000000..8640876a8 --- /dev/null +++ b/model/preference_test.go @@ -0,0 +1,62 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestPreferenceIsValid(t *testing.T) { + preference := Preference{} + + if err := preference.IsValid(); err == nil { + t.Fatal() + } + + preference.UserId = NewId() + if err := preference.IsValid(); err == nil { + t.Fatal() + } + + preference.Category = "1234garbage" + if err := preference.IsValid(); err == nil { + t.Fatal() + } + + preference.Category = PREFERENCE_CATEGORY_DIRECT_CHANNELS + if err := preference.IsValid(); err == nil { + t.Fatal() + } + + preference.Name = "1234garbage" + if err := preference.IsValid(); err == nil { + t.Fatal() + } + + preference.Name = PREFERENCE_NAME_SHOWHIDE + if err := preference.IsValid(); err != nil { + t.Fatal() + } + + preference.AltId = "1234garbage" + if err := preference.IsValid(); err == nil { + t.Fatal() + } + + preference.AltId = NewId() + if err := preference.IsValid(); err != nil { + t.Fatal() + } + + preference.Value = "1234garbage" + if err := preference.IsValid(); err != nil { + t.Fatal() + } + + preference.Value = strings.Repeat("01234567890", 20) + if err := preference.IsValid(); err == nil { + t.Fatal() + } +} -- cgit v1.2.3-1-g7c22 From da66599fa39ddbff96b0844fabac161e130a2bc4 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 1 Oct 2015 10:56:07 -0400 Subject: Added Preferences table to store user preferences --- model/preference.go | 6 +- store/sql_preference_store.go | 125 +++++++++++++++++++++++++++ store/sql_preference_store_test.go | 170 +++++++++++++++++++++++++++++++++++++ store/sql_store.go | 30 ++++--- store/store.go | 7 ++ 5 files changed, 325 insertions(+), 13 deletions(-) create mode 100644 store/sql_preference_store.go create mode 100644 store/sql_preference_store_test.go diff --git a/model/preference.go b/model/preference.go index 900c1d7a0..f363434e3 100644 --- a/model/preference.go +++ b/model/preference.go @@ -10,7 +10,9 @@ import ( const ( PREFERENCE_CATEGORY_DIRECT_CHANNELS = "direct_channels" + PREFERENCE_CATEGORY_TEST = "test" // do not use, just for testing uniqueness while there's only one real category PREFERENCE_NAME_SHOWHIDE = "show_hide" + PREFERENCE_NAME_TEST = "test" // do not use, just for testing uniqueness while there's only one real name ) type Preference struct { @@ -66,9 +68,9 @@ func (o *Preference) IsValid() *AppError { } func IsPreferenceCategoryValid(category string) bool { - return category == PREFERENCE_CATEGORY_DIRECT_CHANNELS + return category == PREFERENCE_CATEGORY_DIRECT_CHANNELS || category == PREFERENCE_CATEGORY_TEST } func IsPreferenceNameValid(name string) bool { - return name == PREFERENCE_NAME_SHOWHIDE + return name == PREFERENCE_NAME_SHOWHIDE || name == PREFERENCE_NAME_TEST } diff --git a/store/sql_preference_store.go b/store/sql_preference_store.go new file mode 100644 index 000000000..3e3a91f61 --- /dev/null +++ b/store/sql_preference_store.go @@ -0,0 +1,125 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" +) + +type SqlPreferenceStore struct { + *SqlStore +} + +func NewSqlPreferenceStore(sqlStore *SqlStore) PreferenceStore { + s := &SqlPreferenceStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(model.Preference{}, "Preferences").SetKeys(false, "UserId", "Category", "Name", "AltId") + table.ColMap("UserId").SetMaxSize(26) + table.ColMap("Category").SetMaxSize(32) + table.ColMap("Name").SetMaxSize(32) + table.ColMap("AltId").SetMaxSize(26) + table.ColMap("Value").SetMaxSize(128) + } + + return s +} + +func (s SqlPreferenceStore) UpgradeSchemaIfNeeded() { +} + +func (s SqlPreferenceStore) CreateIndexesIfNotExists() { + s.CreateIndexIfNotExists("idx_preferences_user_id", "Preferences", "UserId") + s.CreateIndexIfNotExists("idx_preferences_category", "Preferences", "Category") + s.CreateIndexIfNotExists("idx_preferences_name", "Preferences", "Name") +} + +func (s SqlPreferenceStore) Save(preference *model.Preference) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if result.Err = preference.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if err := s.GetMaster().Insert(preference); err != nil { + if IsUniqueConstraintError(err.Error(), "UserId", "preferences_pkey") { + result.Err = model.NewAppError("SqlPreferenceStore.Save", "A preference with that user id, category, name, and alt id already exists", + "user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", alt_id="+preference.AltId+", "+err.Error()) + } else { + result.Err = model.NewAppError("SqlPreferenceStore.Save", "We couldn't save the preference", + "user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", alt_id="+preference.AltId+", "+err.Error()) + } + } else { + result.Data = preference + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlPreferenceStore) Update(preference *model.Preference) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if result.Err = preference.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if count, err := s.GetMaster().Update(preference); err != nil { + result.Err = model.NewAppError("SqlPreferenceStore.Update", "We couldn't update the preference", + "user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", alt_id="+preference.AltId+", "+err.Error()) + } else if count != 1 { + result.Err = model.NewAppError("SqlPreferenceStore.Update", "We couldn't update the preference", + "user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", alt_id="+preference.AltId) + } else { + result.Data = preference + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlPreferenceStore) GetByName(userId string, category string, name string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var preferences []*model.Preference + + if _, err := s.GetReplica().Select(&preferences, + `SELECT + * + FROM + Preferences + WHERE + UserId = :UserId + AND Category = :Category + AND Name = :Name`, map[string]interface{}{"UserId": userId, "Category": category, "Name": name}); err != nil { + result.Err = model.NewAppError("SqlPreferenceStore.GetByName", "We encounted an error while finding preferences", err.Error()) + } else { + result.Data = preferences + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_preference_store_test.go b/store/sql_preference_store_test.go new file mode 100644 index 000000000..d7ef26374 --- /dev/null +++ b/store/sql_preference_store_test.go @@ -0,0 +1,170 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "testing" +) + +func TestPreferenceStoreSave(t *testing.T) { + Setup() + + p1 := model.Preference{ + UserId: model.NewId(), + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_SHOWHIDE, + AltId: model.NewId(), + } + + if err := (<-store.Preference().Save(&p1)).Err; err != nil { + t.Fatal("couldn't save preference", err) + } + + if err := (<-store.Preference().Save(&p1)).Err; err == nil { + t.Fatal("shouldn't be able to save duplicate preference") + } + + p2 := model.Preference{ + UserId: model.NewId(), + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_SHOWHIDE, + AltId: p1.AltId, + } + + if err := (<-store.Preference().Save(&p2)).Err; err != nil { + t.Fatal("couldn't save preference with duplicate category, name, alternate id", err) + } + + p3 := model.Preference{ + UserId: p1.UserId, + Category: model.PREFERENCE_CATEGORY_TEST, + Name: model.PREFERENCE_NAME_SHOWHIDE, + AltId: p1.AltId, + } + + if err := (<-store.Preference().Save(&p3)).Err; err != nil { + t.Fatal("couldn't save preference with duplicate user id, name, alternate id", err) + } + + p4 := model.Preference{ + UserId: p1.UserId, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_TEST, + AltId: p1.AltId, + } + + if err := (<-store.Preference().Save(&p4)).Err; err != nil { + t.Fatal("couldn't save preference with duplicate user id, category, alternate id", err) + } + + p5 := model.Preference{ + UserId: p1.UserId, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_SHOWHIDE, + AltId: model.NewId(), + } + + if err := (<-store.Preference().Save(&p5)).Err; err != nil { + t.Fatal("couldn't save preference with duplicate user id, category, name", err) + } +} + +func TestPreferenceStoreUpdate(t *testing.T) { + Setup() + + id := model.NewId() + + p1 := model.Preference{ + UserId: id, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_SHOWHIDE, + } + Must(store.Preference().Save(&p1)) + + p1.Value = "1234garbage" + if err := (<-store.Preference().Update(&p1)).Err; err != nil { + t.Fatal(err) + } + + p1.UserId = model.NewId() + if err := (<-store.Preference().Update(&p1)).Err; err == nil { + t.Fatal("update should have failed because of changed user id") + } + + p1.UserId = id + p1.Category = model.PREFERENCE_CATEGORY_TEST + if err := (<-store.Preference().Update(&p1)).Err; err == nil { + t.Fatal("update should have failed because of changed category") + } + + p1.Category = model.PREFERENCE_CATEGORY_DIRECT_CHANNELS + p1.Name = model.PREFERENCE_NAME_TEST + if err := (<-store.Preference().Update(&p1)).Err; err == nil { + t.Fatal("update should have failed because of changed name") + } + + p1.Name = model.PREFERENCE_NAME_SHOWHIDE + p1.AltId = model.NewId() + if err := (<-store.Preference().Update(&p1)).Err; err == nil { + t.Fatal("update should have failed because of changed alternate id") + } +} + +func TestPreferenceGetByName(t *testing.T) { + Setup() + + p1 := model.Preference{ + UserId: model.NewId(), + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_SHOWHIDE, + AltId: model.NewId(), + } + + // same user/category/name, different alt id + p2 := model.Preference{ + UserId: p1.UserId, + Category: p1.Category, + Name: p1.Name, + AltId: model.NewId(), + } + + // same user/category/alt id, different name + p3 := model.Preference{ + UserId: p1.UserId, + Category: p1.Category, + Name: model.PREFERENCE_NAME_TEST, + AltId: p1.AltId, + } + + // same user/name/alt id, different category + p4 := model.Preference{ + UserId: p1.UserId, + Category: model.PREFERENCE_CATEGORY_TEST, + Name: p1.Name, + AltId: p1.AltId, + } + + // same name/category/alt id, different user + p5 := model.Preference{ + UserId: model.NewId(), + Category: p1.Category, + Name: p1.Name, + AltId: p1.AltId, + } + + Must(store.Preference().Save(&p1)) + Must(store.Preference().Save(&p2)) + Must(store.Preference().Save(&p3)) + Must(store.Preference().Save(&p4)) + Must(store.Preference().Save(&p5)) + + if result := <-store.Preference().GetByName(p1.UserId, p1.Category, p1.Name); result.Err != nil { + t.Fatal(result.Err) + } else if data := result.Data.([]*model.Preference); len(data) != 2 { + t.Fatal("got the wrong number of preferences") + } else if !((*data[0] == p1 && *data[1] == p2) || (*data[0] == p2 && *data[1] == p1)) { + t.Fatal("got incorrect preferences") + } +} diff --git a/store/sql_store.go b/store/sql_store.go index 900543460..4b055e455 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -30,17 +30,18 @@ import ( ) type SqlStore struct { - master *gorp.DbMap - replicas []*gorp.DbMap - team TeamStore - channel ChannelStore - post PostStore - user UserStore - audit AuditStore - session SessionStore - oauth OAuthStore - system SystemStore - webhook WebhookStore + master *gorp.DbMap + replicas []*gorp.DbMap + team TeamStore + channel ChannelStore + post PostStore + user UserStore + audit AuditStore + session SessionStore + oauth OAuthStore + system SystemStore + webhook WebhookStore + preference PreferenceStore } func NewSqlStore() Store { @@ -93,6 +94,7 @@ func NewSqlStore() Store { sqlStore.oauth = NewSqlOAuthStore(sqlStore) sqlStore.system = NewSqlSystemStore(sqlStore) sqlStore.webhook = NewSqlWebhookStore(sqlStore) + sqlStore.preference = NewSqlPreferenceStore(sqlStore) sqlStore.master.CreateTablesIfNotExists() @@ -105,6 +107,7 @@ func NewSqlStore() Store { sqlStore.oauth.(*SqlOAuthStore).UpgradeSchemaIfNeeded() sqlStore.system.(*SqlSystemStore).UpgradeSchemaIfNeeded() sqlStore.webhook.(*SqlWebhookStore).UpgradeSchemaIfNeeded() + sqlStore.preference.(*SqlPreferenceStore).UpgradeSchemaIfNeeded() sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists() sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists() @@ -115,6 +118,7 @@ func NewSqlStore() Store { sqlStore.oauth.(*SqlOAuthStore).CreateIndexesIfNotExists() sqlStore.system.(*SqlSystemStore).CreateIndexesIfNotExists() sqlStore.webhook.(*SqlWebhookStore).CreateIndexesIfNotExists() + sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists() if model.IsPreviousVersion(schemaVersion) { sqlStore.system.Update(&model.System{Name: "Version", Value: model.CurrentVersion}) @@ -472,6 +476,10 @@ func (ss SqlStore) Webhook() WebhookStore { return ss.webhook } +func (ss SqlStore) Preference() PreferenceStore { + return ss.preference +} + type mattermConverter struct{} func (me mattermConverter) ToDb(val interface{}) (interface{}, error) { diff --git a/store/store.go b/store/store.go index e539bc98a..8a8faae85 100644 --- a/store/store.go +++ b/store/store.go @@ -37,6 +37,7 @@ type Store interface { OAuth() OAuthStore System() SystemStore Webhook() WebhookStore + Preference() PreferenceStore Close() } @@ -149,3 +150,9 @@ type WebhookStore interface { GetIncomingByUser(userId string) StoreChannel DeleteIncoming(webhookId string, time int64) StoreChannel } + +type PreferenceStore interface { + Save(preference *model.Preference) StoreChannel + Update(preference *model.Preference) StoreChannel + GetByName(userId string, category string, name string) StoreChannel +} -- cgit v1.2.3-1-g7c22 From a087403e9f25373d5bdea5e10fafb0c5d496a703 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 1 Oct 2015 15:22:04 -0400 Subject: Added api to get and set preferences --- api/api.go | 1 + api/preference.go | 63 ++++++++++++++++++ api/preference_test.go | 176 +++++++++++++++++++++++++++++++++++++++++++++++++ model/client.go | 18 +++++ model/preference.go | 20 ++++++ 5 files changed, 278 insertions(+) create mode 100644 api/preference.go create mode 100644 api/preference_test.go diff --git a/api/api.go b/api/api.go index 5c3c0d8c6..4da1de62d 100644 --- a/api/api.go +++ b/api/api.go @@ -45,6 +45,7 @@ func InitApi() { InitAdmin(r) InitOAuth(r) InitWebhook(r) + InitPreference(r) templatesDir := utils.FindDir("api/templates") l4g.Debug("Parsing server templates at %v", templatesDir) diff --git a/api/preference.go b/api/preference.go new file mode 100644 index 000000000..9fadfd2e4 --- /dev/null +++ b/api/preference.go @@ -0,0 +1,63 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" + "encoding/json" + "github.com/gorilla/mux" + "github.com/mattermost/platform/model" + "net/http" +) + +func InitPreference(r *mux.Router) { + l4g.Debug("Initializing preference api routes") + + sr := r.PathPrefix("/preferences").Subrouter() + sr.Handle("/set", ApiAppHandler(setPreferences)).Methods("POST") + sr.Handle("/{category:[A-Za-z0-9_]+}/{name:[A-Za-z0-9_]+}", ApiAppHandler(getPreferencesByName)).Methods("GET") +} + +func setPreferences(c *Context, w http.ResponseWriter, r *http.Request) { + var preferences []model.Preference + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&preferences); err != nil { + c.Err = model.NewAppError("setPreferences", "Unable to decode preferences from request", err.Error()) + c.Err.StatusCode = http.StatusBadRequest + return + } + + // just attempt to save/update them one by one and abort if one fails + // in the future, this could probably be done in a transaction, but that's unnecessary now + for _, preference := range preferences { + if c.Session.UserId != preference.UserId { + c.Err = model.NewAppError("setPreferences", "Unable to set preferences for other user", "session.user_id="+c.Session.UserId+", preference.user_id="+preference.UserId) + c.Err.StatusCode = http.StatusUnauthorized + return + } + + if result := <-Srv.Store.Preference().Save(&preference); result.Err != nil { + if result = <-Srv.Store.Preference().Update(&preference); result.Err != nil { + c.Err = result.Err + return + } + } + } + + w.Write([]byte("true")) +} + +func getPreferencesByName(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + category := params["category"] + name := params["name"] + + if result := <-Srv.Store.Preference().GetByName(c.Session.UserId, category, name); result.Err != nil { + c.Err = result.Err + return + } else { + w.Write([]byte(model.PreferenceListToJson(result.Data.([]*model.Preference)))) + } +} diff --git a/api/preference_test.go b/api/preference_test.go new file mode 100644 index 000000000..63aaeaf3e --- /dev/null +++ b/api/preference_test.go @@ -0,0 +1,176 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "testing" +) + +func TestSetPreferences(t *testing.T) { + Setup() + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + // save 10 preferences + var preferences []*model.Preference + for i := 0; i < 10; i++ { + preference := model.Preference{ + UserId: user1.Id, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_SHOWHIDE, + AltId: model.NewId(), + } + preferences = append(preferences, &preference) + } + + if _, err := Client.SetPreferences(preferences); err != nil { + t.Fatal(err) + } + + // update 10 preferences + for _, preference := range preferences { + preference.Value = "1234garbage" + } + + if _, err := Client.SetPreferences(preferences); err != nil { + t.Fatal(err) + } + + // not able to update as a different user + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + + if _, err := Client.SetPreferences(preferences); err == nil { + t.Fatal("shouldn't have been able to update another user's preferences") + } +} + +func TestGetPreferencesByName(t *testing.T) { + Setup() + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + + preferences1 := []*model.Preference{ + &model.Preference{ + UserId: user1.Id, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_SHOWHIDE, + AltId: model.NewId(), + }, + &model.Preference{ + UserId: user1.Id, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_SHOWHIDE, + AltId: model.NewId(), + }, + &model.Preference{ + UserId: user1.Id, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_TEST, + AltId: model.NewId(), + }, + &model.Preference{ + UserId: user1.Id, + Category: model.PREFERENCE_CATEGORY_TEST, + Name: model.PREFERENCE_NAME_SHOWHIDE, + AltId: model.NewId(), + }, + } + + preferences2 := []*model.Preference{ + &model.Preference{ + UserId: user2.Id, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_SHOWHIDE, + AltId: model.NewId(), + }, + } + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + Client.Must(Client.SetPreferences(preferences1)) + + Client.LoginByEmail(team.Name, user2.Email, "pwd") + Client.Must(Client.SetPreferences(preferences2)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + if result, err := Client.GetPreferencesByName(model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, model.PREFERENCE_NAME_SHOWHIDE); err != nil { + t.Fatal(err) + } else if data := result.Data.([]*model.Preference); len(data) != 2 { + t.Fatal("received the wrong number of preferences") + } else if !((*data[0] == *preferences1[0] && *data[1] == *preferences1[1]) || (*data[0] == *preferences1[1] && *data[1] == *preferences1[0])) { + t.Fatal("received incorrect preferences") + } + + Client.LoginByEmail(team.Name, user2.Email, "pwd") + + if result, err := Client.GetPreferencesByName(model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, model.PREFERENCE_NAME_SHOWHIDE); err != nil { + t.Fatal(err) + } else if data := result.Data.([]*model.Preference); len(data) != 1 { + t.Fatal("received the wrong number of preferences") + } else if *data[0] != *preferences2[0] { + t.Fatal("received incorrect preference") + } +} + +func TestSetAndGetProperties(t *testing.T) { + Setup() + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user.Id)) + + Client.LoginByEmail(team.Name, user.Email, "pwd") + + p := model.Preference{ + UserId: user.Id, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_SHOWHIDE, + AltId: model.NewId(), + Value: model.NewId(), + } + + Client.Must(Client.SetPreferences([]*model.Preference{&p})) + + if result, err := Client.GetPreferencesByName(p.Category, p.Name); err != nil { + t.Fatal(err) + } else if data := result.Data.([]*model.Preference); len(data) != 1 { + t.Fatal("received too many preferences") + } else if *data[0] != p { + t.Fatal("preference saved incorrectly") + } + + p.Value = model.NewId() + Client.Must(Client.SetPreferences([]*model.Preference{&p})) + + if result, err := Client.GetPreferencesByName(p.Category, p.Name); err != nil { + t.Fatal(err) + } else if data := result.Data.([]*model.Preference); len(data) != 1 { + t.Fatal("received too many preferences") + } else if *data[0] != p { + t.Fatal("preference updated incorrectly") + } +} diff --git a/model/client.go b/model/client.go index 11beb9a87..892d3e979 100644 --- a/model/client.go +++ b/model/client.go @@ -844,6 +844,24 @@ func (c *Client) ListIncomingWebhooks() (*Result, *AppError) { } } +func (c *Client) SetPreferences(preferences []*Preference) (*Result, *AppError) { + if r, err := c.DoApiPost("/preferences/set", PreferenceListToJson(preferences)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), nil}, nil + } +} + +func (c *Client) GetPreferencesByName(category string, name string) (*Result, *AppError) { + if r, err := c.DoApiGet("/preferences/"+category+"/"+name, "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), PreferenceListFromJson(r.Body)}, nil + } +} + func (c *Client) MockSession(sessionToken string) { c.AuthToken = sessionToken c.AuthType = HEADER_BEARER diff --git a/model/preference.go b/model/preference.go index f363434e3..8e2ff14bc 100644 --- a/model/preference.go +++ b/model/preference.go @@ -32,6 +32,15 @@ func (o *Preference) ToJson() string { } } +func PreferenceListToJson(o []*Preference) string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + func PreferenceFromJson(data io.Reader) *Preference { decoder := json.NewDecoder(data) var o Preference @@ -43,6 +52,17 @@ func PreferenceFromJson(data io.Reader) *Preference { } } +func PreferenceListFromJson(data io.Reader) []*Preference { + decoder := json.NewDecoder(data) + var o []*Preference + err := decoder.Decode(&o) + if err == nil { + return o + } else { + return nil + } +} + func (o *Preference) IsValid() *AppError { if len(o.UserId) != 26 { return NewAppError("Preference.IsValid", "Invalid user id", "user_id="+o.UserId) -- cgit v1.2.3-1-g7c22 From 7d03c24b44a2f4eba86adf86954280fa73e726e4 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Fri, 2 Oct 2015 09:50:34 -0400 Subject: Changed direct channels in the sidebar to be displayed based on user preferences --- web/react/components/sidebar.jsx | 166 ++++++++++++++++++++++++++-------- web/react/stores/preference_store.jsx | 135 +++++++++++++++++++++++++++ web/react/utils/async_client.jsx | 29 ++++++ web/react/utils/client.jsx | 14 +++ web/react/utils/constants.jsx | 1 + 5 files changed, 308 insertions(+), 37 deletions(-) create mode 100644 web/react/stores/preference_store.jsx diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 4ac1fd4a0..115e9c9c6 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -1,19 +1,20 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var ChannelStore = require('../stores/channel_store.jsx'); -var Client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var SocketStore = require('../stores/socket_store.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var TeamStore = require('../stores/team_store.jsx'); -var BrowserStore = require('../stores/browser_store.jsx'); -var Utils = require('../utils/utils.jsx'); -var SidebarHeader = require('./sidebar_header.jsx'); -var SearchBox = require('./search_bar.jsx'); -var Constants = require('../utils/constants.jsx'); -var NewChannelFlow = require('./new_channel_flow.jsx'); -var UnreadChannelIndicator = require('./unread_channel_indicator.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const BrowserStore = require('../stores/browser_store.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); +const Client = require('../utils/client.jsx'); +const Constants = require('../utils/constants.jsx'); +const PreferenceStore = require('../stores/preference_store.jsx'); +const NewChannelFlow = require('./new_channel_flow.jsx'); +const SearchBox = require('./search_bar.jsx'); +const SidebarHeader = require('./sidebar_header.jsx'); +const SocketStore = require('../stores/socket_store.jsx'); +const TeamStore = require('../stores/team_store.jsx'); +const UnreadChannelIndicator = require('./unread_channel_indicator.jsx'); +const UserStore = require('../stores/user_store.jsx'); +const Utils = require('../utils/utils.jsx'); export default class Sidebar extends React.Component { constructor(props) { @@ -23,6 +24,9 @@ export default class Sidebar extends React.Component { this.firstUnreadChannel = null; this.lastUnreadChannel = null; + this.getStateFromStores = this.getStateFromStores.bind(this); + //this.getDirectChannelsFromStores = this.getDirectChannelsFromStores.bind(this); + this.onChange = this.onChange.bind(this); this.onScroll = this.onScroll.bind(this); this.onResize = this.onResize.bind(this); @@ -36,7 +40,7 @@ export default class Sidebar extends React.Component { this.state = state; } getStateFromStores() { - var members = ChannelStore.getAllMembers(); + const members = ChannelStore.getAllMembers(); var teamMemberMap = UserStore.getActiveOnlyProfiles(); var currentId = ChannelStore.getCurrentId(); @@ -48,11 +52,13 @@ export default class Sidebar extends React.Component { teammates.push(teamMemberMap[id]); } + const preferences = PreferenceStore.getPreferences('direct_channels', 'show_hide'); + // Create lists of all read and unread direct channels - var showDirectChannels = []; - var readDirectChannels = []; + var visibleDirectChannels = []; + var hiddenDirectChannels = []; for (var i = 0; i < teammates.length; i++) { - var teammate = teammates[i]; + const teammate = teammates[i]; if (teammate.id === UserStore.getCurrentId()) { continue; @@ -65,7 +71,7 @@ export default class Sidebar extends React.Component { channelName = teammate.id + '__' + UserStore.getCurrentId(); } - var channel = ChannelStore.getByName(channelName); + let channel = ChannelStore.getByName(channelName); if (channel == null) { var tempChannel = {}; @@ -84,21 +90,44 @@ export default class Sidebar extends React.Component { channel.status = UserStore.getStatus(teammate.id); - var channelMember = members[channel.id]; + /*var channelMember = members[channel.id]; var msgCount = channel.total_msg_count - channelMember.msg_count; if (msgCount > 0) { - showDirectChannels.push(channel); + visibleDirectChannels.push(channel); } else if (currentId === channel.id) { - showDirectChannels.push(channel); + visibleDirectChannels.push(channel); } else { - readDirectChannels.push(channel); - } + hiddenDirectChannels.push(channel); + }*/ + } else { + channel = {}; + channel.fake = true; + channel.name = channelName; + channel.display_name = teammate.username; + channel.teammate_username = teammate.username; + channel.status = UserStore.getStatus(teammate.id); + channel.last_post_at = 0; + channel.total_msg_count = 0; + channel.type = 'D'; + } + + if (preferences.some((preference) => (preference.alt_id === teammate.id && preference.value !== 'false'))) { + visibleDirectChannels.push(channel); + } else { + hiddenDirectChannels.push(channel); } } - // If we don't have MAX_DMS unread channels, sort the read list by last_post_at - if (showDirectChannels.length < Constants.MAX_DMS) { - readDirectChannels.sort(function sortByLastPost(a, b) { + function sortByDisplayName(a, b) { + return a.display_name.localeCompare(b.display_name); + } + + visibleDirectChannels.sort(sortByDisplayName); + hiddenDirectChannels.sort(sortByDisplayName); + + /*// If we don't have MAX_DMS unread channels, sort the read list by last_post_at + if (visibleDirectChannels.length < Constants.MAX_DMS) { + hiddenDirectChannels.sort(function sortByLastPost(a, b) { // sort by last_post_at first if (a.last_post_at > b.last_post_at) { return -1; @@ -118,13 +147,13 @@ export default class Sidebar extends React.Component { }); var index = 0; - while (showDirectChannels.length < Constants.MAX_DMS && index < readDirectChannels.length) { - showDirectChannels.push(readDirectChannels[index]); + while (visibleDirectChannels.length < Constants.MAX_DMS && index < hiddenDirectChannels.length) { + visibleDirectChannels.push(hiddenDirectChannels[index]); index++; } - readDirectChannels = readDirectChannels.slice(index); + hiddenDirectChannels = hiddenDirectChannels.slice(index); - showDirectChannels.sort(function directSort(a, b) { + visibleDirectChannels.sort(function directSort(a, b) { if (a.display_name < b.display_name) { return -1; } @@ -133,22 +162,84 @@ export default class Sidebar extends React.Component { } return 0; }); - } + }*/ return { activeId: currentId, channels: ChannelStore.getAll(), members: members, - showDirectChannels: showDirectChannels, - hideDirectChannels: readDirectChannels + visibleDirectChannels: visibleDirectChannels, + hiddenDirectChannels: hiddenDirectChannels }; } + + /*getDirectChannelsFromStores() { + const id = UserStore.getCurrentId(); + + const channels = []; + const preferences = PreferenceStore.getPreferences('direct_channels', 'show_hide'); + for (const preference of preferences) { + if (preference.value !== 'true') { + continue; + } + + const otherId = preference.alt_id; + + if (otherId === id) { + continue; + } + + const teammate = UserStore.getProfile(otherId); + + if (!teammate) { + continue; + } + + let channelName = ''; + if (otherId > id) { + channelName = `${id}__${otherId}`; + } else { + channelName = `${otherId}__${id}`; + } + + const channel = ChannelStore.getByName(channelName); + + if (channel != null) { + channel.display_name = teammate.username; + channel.teammate_username = teammate.username; + + channel.status = UserStore.getStatus(otherId); + + channels.push(channel); + } else { + const tempChannel = {}; + tempChannel.fake = true; + tempChannel.name = channelName; + tempChannel.display_name = teammate.username; + tempChannel.teammate_username = teammate.username; + tempChannel.status = UserStore.getStatus(teammate.id); + tempChannel.last_post_at = 0; + tempChannel.total_msg_count = 0; + tempChannel.type = 'D'; + channels.push(tempChannel); + } + } + + channels.sort((a, b) => a.display_name.localeCompare(b)); + + return channels; + }*/ + componentDidMount() { ChannelStore.addChangeListener(this.onChange); UserStore.addChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onChange); TeamStore.addChangeListener(this.onChange); SocketStore.addChangeListener(this.onSocketChange); + PreferenceStore.addChangeListener(this.onChange); + + AsyncClient.getDirectChannels(); + $('.nav-pills__container').perfectScrollbar(); this.updateTitle(); @@ -178,6 +269,7 @@ export default class Sidebar extends React.Component { UserStore.removeStatusesChangeListener(this.onChange); TeamStore.removeChangeListener(this.onChange); SocketStore.removeChangeListener(this.onSocketChange); + PreferenceStore.removeChangeListener(this.onChange); } onChange() { var newState = this.getStateFromStores(); @@ -464,7 +556,7 @@ export default class Sidebar extends React.Component { const privateChannels = this.state.channels.filter((channel) => channel.type === 'P'); const privateChannelItems = privateChannels.map(this.createChannelElement); - const directMessageItems = this.state.showDirectChannels.map(this.createChannelElement); + const directMessageItems = this.state.visibleDirectChannels.map(this.createChannelElement); // update the favicon to show if there are any notifications var link = document.createElement('link'); @@ -484,7 +576,7 @@ export default class Sidebar extends React.Component { head.appendChild(link); var directMessageMore = null; - if (this.state.hideDirectChannels.length > 0) { + if (this.state.hiddenDirectChannels.length > 0) { directMessageMore = (
  • - {'More (' + this.state.hideDirectChannels.length + ')'} + {'More (' + this.state.hiddenDirectChannels.length + ')'}
  • ); diff --git a/web/react/stores/preference_store.jsx b/web/react/stores/preference_store.jsx new file mode 100644 index 000000000..c13c61e1d --- /dev/null +++ b/web/react/stores/preference_store.jsx @@ -0,0 +1,135 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +const ActionTypes = require('../utils/constants.jsx').ActionTypes; +const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +const BrowserStore = require('./browser_store.jsx'); +const EventEmitter = require('events').EventEmitter; +const UserStore = require('../stores/user_store.jsx'); + +const CHANGE_EVENT = 'change'; + +class PreferenceStoreClass extends EventEmitter { + constructor() { + super(); + + this.getAllPreferences = this.getAllPreferences.bind(this); + this.getPreference = this.getPreference.bind(this); + this.getPreferenceWithAltId = this.getPreferenceWithAltId.bind(this); + this.getPreferences = this.getPreferences.bind(this); + this.getPreferencesWhere = this.getPreferencesWhere.bind(this); + this.setAllPreferences = this.setAllPreferences.bind(this); + this.setPreference = this.setPreference.bind(this); + this.setPreferenceWithAltId = this.setPreferenceWithAltId.bind(this); + + this.emitChange = this.emitChange.bind(this); + this.addChangeListener = this.addChangeListener.bind(this); + this.removeChangeListener = this.removeChangeListener.bind(this); + + this.handleEventPayload = this.handleEventPayload.bind(this); + this.dispatchToken = AppDispatcher.register(this.handleEventPayload); + } + + getKey(category, name, altId = '') { + return `${category}-${name}-${altId}`; + } + + getKeyForModel(preference) { + return `${preference.category}-${preference.name}-${preference.alt_id}`; + } + + getAllPreferences() { + console.log('getting preferences'); // eslint-disable-line no-console + return new Map(BrowserStore.getItem('preferences', [])); + } + + getPreference(category, name, defaultValue = '') { + return this.getAllPreferences().get(this.getKey(category, name)) || defaultValue; + } + + getPreferenceWithAltId(category, name, altId, defaultValue = '') { + return this.getAllPreferences().get(this.getKey(category, name, altId)) || defaultValue; + } + + getPreferences(category, name) { + return this.getPreferencesWhere((preference) => (preference.category === category && preference.name === name)); + } + + getPreferencesWhere(pred) { + const all = this.getAllPreferences(); + const preferences = []; + + for (const [, preference] of all) { + if (pred(preference)) { + preferences.push(preference); + } + } + + return preferences; + } + + setAllPreferences(preferences) { + // note that we store the preferences as an array of key-value pairs so that we can deserialize + // it as a proper Map instead of an object + BrowserStore.setItem('preferences', [...preferences]); + } + + setPreference(category, name, value) { + this.setPreferenceWithAltId(category, name, '', value); + } + + setPreferenceWithAltId(category, name, altId, value) { + const preferences = this.getAllPreferences(); + + const key = this.getKey(category, name); + let preference = preferences.get(key); + + if (!preference) { + preference = { + user_id: UserStore.getCurrentId(), + category, + name, + alt_id: altId + }; + } + preference.value = value; + + preferences.set(key, preference); + + this.setAllPreferences(preferences); + } + + emitChange(preferences) { + this.emit(CHANGE_EVENT, preferences); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + handleEventPayload(payload) { + const action = payload.action; + + switch (action.type) { + case ActionTypes.RECIEVED_PREFERENCES: + const preferences = this.getAllPreferences(); + + for (const preference of action.preferences) { + preferences.set(this.getKeyForModel(preference), preference); + } + + this.setAllPreferences(preferences); + this.emitChange(preferences); + } + } +} + +const PreferenceStore = new PreferenceStoreClass(); +export default PreferenceStore; + +// TODO remove me +global.PreferenceStore = PreferenceStore; diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index a903f055b..3f084578a 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -637,3 +637,32 @@ export function getMyTeam() { } ); } + +export function getDirectChannels() { + if (isCallInProgress('getDirectChannels')) { + return; + } + + callTracker.getDirectChannels = utils.getTimestamp(); + client.getPreferencesByName( + 'direct_channels', + 'show_hide', + (data, textStatus, xhr) => { + callTracker.getDirectChannels = 0; + + if (xhr.status === 304 || !data) { + return; + } + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_PREFERENCES, + preferences: data + }); + }, + (err) => { + callTracker.getDirectChannels = 0; + dispatchError(err, 'getDirectChannels'); + } + ); +} + diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 6dccfcdeb..f1827f296 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -1141,3 +1141,17 @@ export function listIncomingHooks(success, error) { } }); } + +export function getPreferencesByName(category, name, success, error) { + $.ajax({ + url: `/api/v1/preferences/${category}/${name}`, + dataType: 'json', + type: 'GET', + success, + error: (xhr, status, err) => { + var e = handleError('getPreferencesByName', xhr, status, err); + error(e); + } + }); +} + diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index e3cbfccde..56c47a244 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -28,6 +28,7 @@ module.exports = { RECIEVED_AUDITS: null, RECIEVED_TEAMS: null, RECIEVED_STATUSES: null, + RECIEVED_PREFERENCES: null, RECIEVED_MSG: null, -- cgit v1.2.3-1-g7c22 From ed31538893ad2790de46ace7eeac5c1aa015a7f1 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Fri, 2 Oct 2015 14:25:55 -0400 Subject: Changed direct messages channels so users can show/hide them --- web/react/components/more_direct_channels.jsx | 32 +++- web/react/components/sidebar.jsx | 229 +++++++++----------------- web/react/stores/preference_store.jsx | 9 +- web/react/utils/async_client.jsx | 24 +++ web/react/utils/client.jsx | 13 ++ 5 files changed, 143 insertions(+), 164 deletions(-) diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index 31ecb4c5d..fc720e928 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -1,10 +1,10 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var ChannelStore = require('../stores/channel_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); +var PreferenceStore = require('../stores/preference_store.jsx'); var utils = require('../utils/utils.jsx'); export default class MoreDirectChannels extends React.Component { @@ -22,16 +22,32 @@ export default class MoreDirectChannels extends React.Component { }); } + handleJoinDirectChannel(channel) { + const preference = PreferenceStore.setPreferenceWithAltId('direct_channels', 'show_hide', channel.teammate_id, 'true'); + AsyncClient.setPreferences([preference]); + } + render() { var self = this; - var directMessageItems = this.state.channels.map(function mapActivityToChannel(channel, index) { + var directMessageItems = this.state.channels.map((channel, index) => { var badge = ''; var titleClass = ''; - var active = ''; var handleClick = null; - if (channel.fake) { + if (!channel.fake) { + if (channel.unread) { + badge = {channel.unread}; + titleClass = 'unread-title'; + } + + handleClick = (e) => { + e.preventDefault(); + this.handleJoinDirectChannel(channel); + utils.switchChannel(channel); + $(React.findDOMNode(self.refs.modal)).modal('hide'); + }; + } else { // It's a direct message channel that doesn't exist yet so let's create it now var otherUserId = utils.getUserIdFromChannelName(channel); @@ -45,9 +61,10 @@ export default class MoreDirectChannels extends React.Component { } if (self.state.loadingDMChannel === -1) { - handleClick = function clickHandler(e) { + handleClick = (e) => { e.preventDefault(); self.setState({loadingDMChannel: index}); + this.handleJoinDirectChannel(channel); Client.createDirectChannel(channel, otherUserId, function success(data) { @@ -81,10 +98,7 @@ export default class MoreDirectChannels extends React.Component { } return ( -
  • +
  • 0) { - visibleDirectChannels.push(channel); - } else if (currentId === channel.id) { - visibleDirectChannels.push(channel); - } else { - hiddenDirectChannels.push(channel); - }*/ - } else { + if (!channel) { channel = {}; channel.fake = true; channel.name = channelName; - channel.display_name = teammate.username; - channel.teammate_username = teammate.username; - channel.status = UserStore.getStatus(teammate.id); channel.last_post_at = 0; channel.total_msg_count = 0; channel.type = 'D'; } + channel.display_name = teammate.username; + channel.teammate_id = teammate.id; + channel.status = UserStore.getStatus(teammate.id); + if (preferences.some((preference) => (preference.alt_id === teammate.id && preference.value !== 'false'))) { visibleDirectChannels.push(channel); } else { @@ -118,51 +93,8 @@ export default class Sidebar extends React.Component { } } - function sortByDisplayName(a, b) { - return a.display_name.localeCompare(b.display_name); - } - - visibleDirectChannels.sort(sortByDisplayName); - hiddenDirectChannels.sort(sortByDisplayName); - - /*// If we don't have MAX_DMS unread channels, sort the read list by last_post_at - if (visibleDirectChannels.length < Constants.MAX_DMS) { - hiddenDirectChannels.sort(function sortByLastPost(a, b) { - // sort by last_post_at first - if (a.last_post_at > b.last_post_at) { - return -1; - } - if (a.last_post_at < b.last_post_at) { - return 1; - } - - // if last_post_at is equal, sort by name - if (a.display_name < b.display_name) { - return -1; - } - if (a.display_name > b.display_name) { - return 1; - } - return 0; - }); - - var index = 0; - while (visibleDirectChannels.length < Constants.MAX_DMS && index < hiddenDirectChannels.length) { - visibleDirectChannels.push(hiddenDirectChannels[index]); - index++; - } - hiddenDirectChannels = hiddenDirectChannels.slice(index); - - visibleDirectChannels.sort(function directSort(a, b) { - if (a.display_name < b.display_name) { - return -1; - } - if (a.display_name > b.display_name) { - return 1; - } - return 0; - }); - }*/ + visibleDirectChannels.sort(this.sortChannelsByDisplayName); + hiddenDirectChannels.sort(this.sortChannelsByDisplayName); return { activeId: currentId, @@ -173,63 +105,6 @@ export default class Sidebar extends React.Component { }; } - /*getDirectChannelsFromStores() { - const id = UserStore.getCurrentId(); - - const channels = []; - const preferences = PreferenceStore.getPreferences('direct_channels', 'show_hide'); - for (const preference of preferences) { - if (preference.value !== 'true') { - continue; - } - - const otherId = preference.alt_id; - - if (otherId === id) { - continue; - } - - const teammate = UserStore.getProfile(otherId); - - if (!teammate) { - continue; - } - - let channelName = ''; - if (otherId > id) { - channelName = `${id}__${otherId}`; - } else { - channelName = `${otherId}__${id}`; - } - - const channel = ChannelStore.getByName(channelName); - - if (channel != null) { - channel.display_name = teammate.username; - channel.teammate_username = teammate.username; - - channel.status = UserStore.getStatus(otherId); - - channels.push(channel); - } else { - const tempChannel = {}; - tempChannel.fake = true; - tempChannel.name = channelName; - tempChannel.display_name = teammate.username; - tempChannel.teammate_username = teammate.username; - tempChannel.status = UserStore.getStatus(teammate.id); - tempChannel.last_post_at = 0; - tempChannel.total_msg_count = 0; - tempChannel.type = 'D'; - channels.push(tempChannel); - } - } - - channels.sort((a, b) => a.display_name.localeCompare(b)); - - return channels; - }*/ - componentDidMount() { ChannelStore.addChangeListener(this.onChange); UserStore.addChangeListener(this.onChange); @@ -414,7 +289,35 @@ export default class Sidebar extends React.Component { showBottomUnread }); } - createChannelElement(channel, index) { + + handleLeaveDirectChannel(channel) { + if (!channel.leaving) { + channel.leaving = true; + + const preference = PreferenceStore.setPreferenceWithAltId('direct_channels', 'show_hide', channel.teammate_id, 'false'); + AsyncClient.setPreferences( + [preference], + () => { + channel.leaving = false; + }, + () => { + channel.leaving = false; + } + ); + + this.setState(this.getStateFromStores()); + } + + if (channel.id === this.state.activeId) { + Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL)); + } + } + + sortChannelsByDisplayName(a, b) { + return a.display_name.localeCompare(b.display_name); + } + + createChannelElement(channel, index, arr, handleClose) { var members = this.state.members; var activeId = this.state.activeId; var channelMember = members[channel.id]; @@ -497,8 +400,13 @@ export default class Sidebar extends React.Component { if (!channel.fake) { handleClick = function clickHandler(e) { + if (!e.target.attributes.getNamedItem('data-close')) { + Utils.switchChannel(channel); + } else { + handleClose(channel); + } + e.preventDefault(); - Utils.switchChannel(channel); }; } else if (channel.fake && teamURL) { // It's a direct message channel that doesn't exist yet so let's create it now @@ -507,23 +415,40 @@ export default class Sidebar extends React.Component { if (this.state.loadingDMChannel === -1) { handleClick = function clickHandler(e) { e.preventDefault(); - this.setState({loadingDMChannel: index}); - - Client.createDirectChannel(channel, otherUserId, - function success(data) { - this.setState({loadingDMChannel: -1}); - AsyncClient.getChannel(data.id); - Utils.switchChannel(data); - }.bind(this), - function error() { - this.setState({loadingDMChannel: -1}); - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; - }.bind(this) - ); + + if (!e.target.attributes.getNamedItem('data-close')) { + this.setState({loadingDMChannel: index}); + + Client.createDirectChannel(channel, otherUserId, + function success(data) { + this.setState({loadingDMChannel: -1}); + AsyncClient.getChannel(data.id); + Utils.switchChannel(data); + }.bind(this), + function error() { + this.setState({loadingDMChannel: -1}); + window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; + }.bind(this) + ); + } else { + handleClose(channel); + } }.bind(this); } } + let closeButton = null; + if (handleClose) { + closeButton = ( + + {'×'} + + ); + } + return (
  • ); @@ -556,7 +482,9 @@ export default class Sidebar extends React.Component { const privateChannels = this.state.channels.filter((channel) => channel.type === 'P'); const privateChannelItems = privateChannels.map(this.createChannelElement); - const directMessageItems = this.state.visibleDirectChannels.map(this.createChannelElement); + const directMessageItems = this.state.visibleDirectChannels.map((channel, index, arr) => { + return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel); + }); // update the favicon to show if there are any notifications var link = document.createElement('link'); @@ -578,8 +506,9 @@ export default class Sidebar extends React.Component { var directMessageMore = null; if (this.state.hiddenDirectChannels.length > 0) { directMessageMore = ( -
  • +
  • { + if (xhr.status !== 304) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_PREFERENCES, + preferences + }); + } + + if (success) { + success(data); + } + }, + (err) => { + dispatchError(err, 'setPreferences'); + + if (error) { + error(); + } + } + ); +} diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index f1827f296..2134dc0f5 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -1155,3 +1155,16 @@ export function getPreferencesByName(category, name, success, error) { }); } +export function setPreferences(preferences, success, error) { + $.ajax({ + url: '/api/v1/preferences/set', + dataType: 'json', + type: 'POST', + data: JSON.stringify(preferences), + success, + error: (xhr, status, err) => { + var e = handleError('setPreferences', xhr, status, err); + error(e); + } + }); +} -- cgit v1.2.3-1-g7c22 From 4678794133d74c9ae018889e72106d5b0ce5da4b Mon Sep 17 00:00:00 2001 From: hmhealey Date: Mon, 5 Oct 2015 09:48:54 -0400 Subject: Re-added default direct channels for new users --- api/user.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/api/user.go b/api/user.go index faf828cf9..e8e206ad3 100644 --- a/api/user.go +++ b/api/user.go @@ -198,6 +198,8 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err) } + fireAndForgetAddDirectChannels(ruser, team) + fireAndForgetWelcomeEmail(ruser.Email, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) if user.EmailVerified { if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil { @@ -218,6 +220,46 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { } } +func fireAndForgetAddDirectChannels(user *model.User, team *model.Team) { + go func() { + var profiles map[string]*model.User + if result := <-Srv.Store.User().GetProfiles(team.Id); result.Err != nil { + l4g.Error("Failed to add direct channel preferences for new user user_id=%s, team_id=%s, err=%v", user.Id, team.Id, result.Err.Error()) + return + } else { + profiles = result.Data.(map[string]*model.User) + } + + count := 10 + + for id := range profiles { + if id == user.Id { + continue + } + + profile := profiles[id] + + preference := &model.Preference{ + UserId: user.Id, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_SHOWHIDE, + AltId: profile.Id, + Value: "true", + } + + if result := <-Srv.Store.Preference().Save(preference); result.Err != nil { + l4g.Error("Failed to add direct channel preferences for new user user_id=%s, alt_id=%s, team_id=%s, err=%v", user.Id, profile.Id, team.Id, result.Err.Error()) + } + + count -= 1 + + if count == 0 { + break + } + } + }() +} + func fireAndForgetWelcomeEmail(email, teamDisplayName, siteURL, teamURL string) { go func() { -- cgit v1.2.3-1-g7c22 From 415f959614f6a3fd7fcb6551e32b722df2b015c5 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Mon, 5 Oct 2015 11:47:34 -0400 Subject: Made direct channels visible when receiving a message on one --- web/react/components/sidebar.jsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 449a3eaea..de353deb4 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -71,6 +71,7 @@ export default class Sidebar extends React.Component { channelName = teammate.id + '__' + UserStore.getCurrentId(); } + let forceShow = false; let channel = ChannelStore.getByName(channelName); if (!channel) { @@ -80,6 +81,11 @@ export default class Sidebar extends React.Component { channel.last_post_at = 0; channel.total_msg_count = 0; channel.type = 'D'; + } else { + const member = members[channel.id]; + const msgCount = channel.total_msg_count - member.msg_count; + + forceShow = currentId === channel.id || msgCount > 0; } channel.display_name = teammate.username; @@ -87,6 +93,12 @@ export default class Sidebar extends React.Component { channel.status = UserStore.getStatus(teammate.id); if (preferences.some((preference) => (preference.alt_id === teammate.id && preference.value !== 'false'))) { + visibleDirectChannels.push(channel); + } else if (forceShow) { + // make sure that unread direct channels are visible + const preference = PreferenceStore.setPreferenceWithAltId('direct_channels', 'show_hide', teammate.id, 'true'); + AsyncClient.setPreferences([preference]); + visibleDirectChannels.push(channel); } else { hiddenDirectChannels.push(channel); -- cgit v1.2.3-1-g7c22 From 599644fb2fa75d1760420806c8c821959fc6b645 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Mon, 5 Oct 2015 12:03:27 -0400 Subject: Renamed show_hide preference to show --- api/preference_test.go | 16 ++++++++-------- api/user.go | 2 +- model/preference.go | 4 ++-- model/preference_test.go | 2 +- store/sql_preference_store_test.go | 14 +++++++------- web/react/components/more_direct_channels.jsx | 4 +++- web/react/components/sidebar.jsx | 8 +++++--- web/react/utils/async_client.jsx | 4 ++-- web/react/utils/constants.jsx | 6 +++++- 9 files changed, 34 insertions(+), 26 deletions(-) diff --git a/api/preference_test.go b/api/preference_test.go index 63aaeaf3e..d4046cc6e 100644 --- a/api/preference_test.go +++ b/api/preference_test.go @@ -27,7 +27,7 @@ func TestSetPreferences(t *testing.T) { preference := model.Preference{ UserId: user1.Id, Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, - Name: model.PREFERENCE_NAME_SHOWHIDE, + Name: model.PREFERENCE_NAME_SHOW, AltId: model.NewId(), } preferences = append(preferences, &preference) @@ -74,13 +74,13 @@ func TestGetPreferencesByName(t *testing.T) { &model.Preference{ UserId: user1.Id, Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, - Name: model.PREFERENCE_NAME_SHOWHIDE, + Name: model.PREFERENCE_NAME_SHOW, AltId: model.NewId(), }, &model.Preference{ UserId: user1.Id, Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, - Name: model.PREFERENCE_NAME_SHOWHIDE, + Name: model.PREFERENCE_NAME_SHOW, AltId: model.NewId(), }, &model.Preference{ @@ -92,7 +92,7 @@ func TestGetPreferencesByName(t *testing.T) { &model.Preference{ UserId: user1.Id, Category: model.PREFERENCE_CATEGORY_TEST, - Name: model.PREFERENCE_NAME_SHOWHIDE, + Name: model.PREFERENCE_NAME_SHOW, AltId: model.NewId(), }, } @@ -101,7 +101,7 @@ func TestGetPreferencesByName(t *testing.T) { &model.Preference{ UserId: user2.Id, Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, - Name: model.PREFERENCE_NAME_SHOWHIDE, + Name: model.PREFERENCE_NAME_SHOW, AltId: model.NewId(), }, } @@ -114,7 +114,7 @@ func TestGetPreferencesByName(t *testing.T) { Client.LoginByEmail(team.Name, user1.Email, "pwd") - if result, err := Client.GetPreferencesByName(model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, model.PREFERENCE_NAME_SHOWHIDE); err != nil { + if result, err := Client.GetPreferencesByName(model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, model.PREFERENCE_NAME_SHOW); err != nil { t.Fatal(err) } else if data := result.Data.([]*model.Preference); len(data) != 2 { t.Fatal("received the wrong number of preferences") @@ -124,7 +124,7 @@ func TestGetPreferencesByName(t *testing.T) { Client.LoginByEmail(team.Name, user2.Email, "pwd") - if result, err := Client.GetPreferencesByName(model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, model.PREFERENCE_NAME_SHOWHIDE); err != nil { + if result, err := Client.GetPreferencesByName(model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, model.PREFERENCE_NAME_SHOW); err != nil { t.Fatal(err) } else if data := result.Data.([]*model.Preference); len(data) != 1 { t.Fatal("received the wrong number of preferences") @@ -148,7 +148,7 @@ func TestSetAndGetProperties(t *testing.T) { p := model.Preference{ UserId: user.Id, Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, - Name: model.PREFERENCE_NAME_SHOWHIDE, + Name: model.PREFERENCE_NAME_SHOW, AltId: model.NewId(), Value: model.NewId(), } diff --git a/api/user.go b/api/user.go index e8e206ad3..b769a2912 100644 --- a/api/user.go +++ b/api/user.go @@ -242,7 +242,7 @@ func fireAndForgetAddDirectChannels(user *model.User, team *model.Team) { preference := &model.Preference{ UserId: user.Id, Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, - Name: model.PREFERENCE_NAME_SHOWHIDE, + Name: model.PREFERENCE_NAME_SHOW, AltId: profile.Id, Value: "true", } diff --git a/model/preference.go b/model/preference.go index 8e2ff14bc..5a5f0d64e 100644 --- a/model/preference.go +++ b/model/preference.go @@ -11,7 +11,7 @@ import ( const ( PREFERENCE_CATEGORY_DIRECT_CHANNELS = "direct_channels" PREFERENCE_CATEGORY_TEST = "test" // do not use, just for testing uniqueness while there's only one real category - PREFERENCE_NAME_SHOWHIDE = "show_hide" + PREFERENCE_NAME_SHOW = "show" PREFERENCE_NAME_TEST = "test" // do not use, just for testing uniqueness while there's only one real name ) @@ -92,5 +92,5 @@ func IsPreferenceCategoryValid(category string) bool { } func IsPreferenceNameValid(name string) bool { - return name == PREFERENCE_NAME_SHOWHIDE || name == PREFERENCE_NAME_TEST + return name == PREFERENCE_NAME_SHOW || name == PREFERENCE_NAME_TEST } diff --git a/model/preference_test.go b/model/preference_test.go index 8640876a8..bfbe81f39 100644 --- a/model/preference_test.go +++ b/model/preference_test.go @@ -35,7 +35,7 @@ func TestPreferenceIsValid(t *testing.T) { t.Fatal() } - preference.Name = PREFERENCE_NAME_SHOWHIDE + preference.Name = PREFERENCE_NAME_SHOW if err := preference.IsValid(); err != nil { t.Fatal() } diff --git a/store/sql_preference_store_test.go b/store/sql_preference_store_test.go index d7ef26374..99b183274 100644 --- a/store/sql_preference_store_test.go +++ b/store/sql_preference_store_test.go @@ -14,7 +14,7 @@ func TestPreferenceStoreSave(t *testing.T) { p1 := model.Preference{ UserId: model.NewId(), Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, - Name: model.PREFERENCE_NAME_SHOWHIDE, + Name: model.PREFERENCE_NAME_SHOW, AltId: model.NewId(), } @@ -29,7 +29,7 @@ func TestPreferenceStoreSave(t *testing.T) { p2 := model.Preference{ UserId: model.NewId(), Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, - Name: model.PREFERENCE_NAME_SHOWHIDE, + Name: model.PREFERENCE_NAME_SHOW, AltId: p1.AltId, } @@ -40,7 +40,7 @@ func TestPreferenceStoreSave(t *testing.T) { p3 := model.Preference{ UserId: p1.UserId, Category: model.PREFERENCE_CATEGORY_TEST, - Name: model.PREFERENCE_NAME_SHOWHIDE, + Name: model.PREFERENCE_NAME_SHOW, AltId: p1.AltId, } @@ -62,7 +62,7 @@ func TestPreferenceStoreSave(t *testing.T) { p5 := model.Preference{ UserId: p1.UserId, Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, - Name: model.PREFERENCE_NAME_SHOWHIDE, + Name: model.PREFERENCE_NAME_SHOW, AltId: model.NewId(), } @@ -79,7 +79,7 @@ func TestPreferenceStoreUpdate(t *testing.T) { p1 := model.Preference{ UserId: id, Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, - Name: model.PREFERENCE_NAME_SHOWHIDE, + Name: model.PREFERENCE_NAME_SHOW, } Must(store.Preference().Save(&p1)) @@ -105,7 +105,7 @@ func TestPreferenceStoreUpdate(t *testing.T) { t.Fatal("update should have failed because of changed name") } - p1.Name = model.PREFERENCE_NAME_SHOWHIDE + p1.Name = model.PREFERENCE_NAME_SHOW p1.AltId = model.NewId() if err := (<-store.Preference().Update(&p1)).Err; err == nil { t.Fatal("update should have failed because of changed alternate id") @@ -118,7 +118,7 @@ func TestPreferenceGetByName(t *testing.T) { p1 := model.Preference{ UserId: model.NewId(), Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, - Name: model.PREFERENCE_NAME_SHOWHIDE, + Name: model.PREFERENCE_NAME_SHOW, AltId: model.NewId(), } diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index fc720e928..ed9c6fc58 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -3,6 +3,7 @@ var TeamStore = require('../stores/team_store.jsx'); var Client = require('../utils/client.jsx'); +var Constants = require('../utils/constants.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var PreferenceStore = require('../stores/preference_store.jsx'); var utils = require('../utils/utils.jsx'); @@ -23,7 +24,8 @@ export default class MoreDirectChannels extends React.Component { } handleJoinDirectChannel(channel) { - const preference = PreferenceStore.setPreferenceWithAltId('direct_channels', 'show_hide', channel.teammate_id, 'true'); + const preference = PreferenceStore.setPreferenceWithAltId(Constants.Preferences.CATEGORY_DIRECT_CHANNELS, + Constants.Preferences.NAME_SHOW, channel.teammate_id, 'true'); AsyncClient.setPreferences([preference]); } diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index de353deb4..97f705f32 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -52,7 +52,7 @@ export default class Sidebar extends React.Component { teammates.push(teamMemberMap[id]); } - const preferences = PreferenceStore.getPreferences('direct_channels', 'show_hide'); + const preferences = PreferenceStore.getPreferences(Constants.Preferences.CATEGORY_DIRECT_CHANNELS, Constants.Preferences.NAME_SHOW); // Create lists of all read and unread direct channels var visibleDirectChannels = []; @@ -96,7 +96,8 @@ export default class Sidebar extends React.Component { visibleDirectChannels.push(channel); } else if (forceShow) { // make sure that unread direct channels are visible - const preference = PreferenceStore.setPreferenceWithAltId('direct_channels', 'show_hide', teammate.id, 'true'); + const preference = PreferenceStore.setPreferenceWithAltId(Constants.Preferences.CATEGORY_DIRECT_CHANNELS, + Constants.Preferences.NAME_SHOW, teammate.id, 'true'); AsyncClient.setPreferences([preference]); visibleDirectChannels.push(channel); @@ -306,7 +307,8 @@ export default class Sidebar extends React.Component { if (!channel.leaving) { channel.leaving = true; - const preference = PreferenceStore.setPreferenceWithAltId('direct_channels', 'show_hide', channel.teammate_id, 'false'); + const preference = PreferenceStore.setPreferenceWithAltId(Constants.Preferences.CATEGORY_DIRECT_CHANNELS, + Constants.Preferences.NAME_SHOW, channel.teammate_id, 'false'); AsyncClient.setPreferences( [preference], () => { diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index d665dfc94..a0ccccd88 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -645,8 +645,8 @@ export function getDirectChannels() { callTracker.getDirectChannels = utils.getTimestamp(); client.getPreferencesByName( - 'direct_channels', - 'show_hide', + Constants.Preferences.CATEGORY_DIRECT_CHANNELS, + Constants.Preferences.NAME_SHOW, (data, textStatus, xhr) => { callTracker.getDirectChannels = 0; diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 56c47a244..a576b9098 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -286,5 +286,9 @@ module.exports = { id: 'mentionHighlightLink', uiName: 'Mention Highlight Link' } - ] + ], + Preferences: { + CATEGORY_DIRECT_CHANNELS: 'direct_channels', + NAME_SHOW: 'show' + } }; -- cgit v1.2.3-1-g7c22 From a11421c74b338eee6eedb4f4260a75f38aa3fd4c Mon Sep 17 00:00:00 2001 From: hmhealey Date: Mon, 5 Oct 2015 12:45:15 -0400 Subject: Added default direct channels for previously existing users --- api/preference.go | 11 +++++++++- api/user.go | 63 ++++++++++++++++++++++++++++++------------------------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/api/preference.go b/api/preference.go index 9fadfd2e4..84cfc130c 100644 --- a/api/preference.go +++ b/api/preference.go @@ -58,6 +58,15 @@ func getPreferencesByName(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = result.Err return } else { - w.Write([]byte(model.PreferenceListToJson(result.Data.([]*model.Preference)))) + data := result.Data.([]*model.Preference) + + if len(data) == 0 { + if category == model.PREFERENCE_CATEGORY_DIRECT_CHANNELS && name == model.PREFERENCE_NAME_SHOW { + // add direct channels for a user that existed before preferences were added + data = AddDirectChannels(c.Session.UserId, c.Session.TeamId) + } + } + + w.Write([]byte(model.PreferenceListToJson(data))) } } diff --git a/api/user.go b/api/user.go index b769a2912..204f00dcd 100644 --- a/api/user.go +++ b/api/user.go @@ -222,42 +222,49 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { func fireAndForgetAddDirectChannels(user *model.User, team *model.Team) { go func() { - var profiles map[string]*model.User - if result := <-Srv.Store.User().GetProfiles(team.Id); result.Err != nil { - l4g.Error("Failed to add direct channel preferences for new user user_id=%s, team_id=%s, err=%v", user.Id, team.Id, result.Err.Error()) - return - } else { - profiles = result.Data.(map[string]*model.User) - } + AddDirectChannels(user.Id, team.Id) + }() +} - count := 10 +func AddDirectChannels(userId, teamId string) []*model.Preference { + var profiles map[string]*model.User + if result := <-Srv.Store.User().GetProfiles(teamId); result.Err != nil { + l4g.Error("Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v", userId, teamId, result.Err.Error()) + return []*model.Preference{} + } else { + profiles = result.Data.(map[string]*model.User) + } - for id := range profiles { - if id == user.Id { - continue - } + var preferences []*model.Preference - profile := profiles[id] + for id := range profiles { + if id == userId { + continue + } - preference := &model.Preference{ - UserId: user.Id, - Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, - Name: model.PREFERENCE_NAME_SHOW, - AltId: profile.Id, - Value: "true", - } + profile := profiles[id] - if result := <-Srv.Store.Preference().Save(preference); result.Err != nil { - l4g.Error("Failed to add direct channel preferences for new user user_id=%s, alt_id=%s, team_id=%s, err=%v", user.Id, profile.Id, team.Id, result.Err.Error()) - } + preference := &model.Preference{ + UserId: userId, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_SHOW, + AltId: profile.Id, + Value: "true", + } - count -= 1 + if result := <-Srv.Store.Preference().Save(preference); result.Err != nil { + l4g.Error("Failed to add direct channel preferences for user user_id=%s, alt_id=%s, team_id=%s, err=%v", userId, profile.Id, teamId, result.Err.Error()) + continue + } - if count == 0 { - break - } + preferences = append(preferences, preference) + + if len(preferences) >= 10 { + break } - }() + } + + return preferences } func fireAndForgetWelcomeEmail(email, teamDisplayName, siteURL, teamURL string) { -- cgit v1.2.3-1-g7c22 From ae0eb91180130e27767d797bc47376b88f62f88b Mon Sep 17 00:00:00 2001 From: hmhealey Date: Mon, 5 Oct 2015 13:16:39 -0400 Subject: Hid the close button when the unread badge is visible and moved it to the right side of the sidebar --- web/react/components/sidebar.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 97f705f32..2619798eb 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -452,10 +452,10 @@ export default class Sidebar extends React.Component { } let closeButton = null; - if (handleClose) { + if (handleClose && !badge) { closeButton = ( {'×'} -- cgit v1.2.3-1-g7c22 From 5832232b6d3d79023204d357b0de33eff9e00370 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Mon, 5 Oct 2015 13:50:54 -0400 Subject: Fixed preference tests --- api/preference_test.go | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/api/preference_test.go b/api/preference_test.go index d4046cc6e..77562bb06 100644 --- a/api/preference_test.go +++ b/api/preference_test.go @@ -51,6 +51,8 @@ func TestSetPreferences(t *testing.T) { user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + Client.LoginByEmail(team.Name, user2.Email, "pwd") + if _, err := Client.SetPreferences(preferences); err == nil { t.Fatal("shouldn't have been able to update another user's preferences") } @@ -71,25 +73,25 @@ func TestGetPreferencesByName(t *testing.T) { store.Must(Srv.Store.User().VerifyEmail(user2.Id)) preferences1 := []*model.Preference{ - &model.Preference{ + { UserId: user1.Id, Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, Name: model.PREFERENCE_NAME_SHOW, AltId: model.NewId(), }, - &model.Preference{ + { UserId: user1.Id, Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, Name: model.PREFERENCE_NAME_SHOW, AltId: model.NewId(), }, - &model.Preference{ + { UserId: user1.Id, Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, Name: model.PREFERENCE_NAME_TEST, AltId: model.NewId(), }, - &model.Preference{ + { UserId: user1.Id, Category: model.PREFERENCE_CATEGORY_TEST, Name: model.PREFERENCE_NAME_SHOW, @@ -97,21 +99,9 @@ func TestGetPreferencesByName(t *testing.T) { }, } - preferences2 := []*model.Preference{ - &model.Preference{ - UserId: user2.Id, - Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, - Name: model.PREFERENCE_NAME_SHOW, - AltId: model.NewId(), - }, - } - Client.LoginByEmail(team.Name, user1.Email, "pwd") Client.Must(Client.SetPreferences(preferences1)) - Client.LoginByEmail(team.Name, user2.Email, "pwd") - Client.Must(Client.SetPreferences(preferences2)) - Client.LoginByEmail(team.Name, user1.Email, "pwd") if result, err := Client.GetPreferencesByName(model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, model.PREFERENCE_NAME_SHOW); err != nil { @@ -124,12 +114,11 @@ func TestGetPreferencesByName(t *testing.T) { Client.LoginByEmail(team.Name, user2.Email, "pwd") + // note that user2 will start with a preference to show user1 in the sidebar by default if result, err := Client.GetPreferencesByName(model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, model.PREFERENCE_NAME_SHOW); err != nil { t.Fatal(err) } else if data := result.Data.([]*model.Preference); len(data) != 1 { t.Fatal("received the wrong number of preferences") - } else if *data[0] != *preferences2[0] { - t.Fatal("received incorrect preference") } } -- cgit v1.2.3-1-g7c22 From a6cd2a79612d6d96e0e929ab769ec5e70cd35f5f Mon Sep 17 00:00:00 2001 From: hmhealey Date: Wed, 7 Oct 2015 10:19:02 -0400 Subject: Moved saving multiple user preferences into a database transaction --- api/preference.go | 14 ++--- store/sql_preference_store.go | 112 +++++++++++++++++++++++++++---------- store/sql_preference_store_test.go | 59 +++++++++++++++---- store/sql_store.go | 6 ++ store/store.go | 1 + 5 files changed, 144 insertions(+), 48 deletions(-) diff --git a/api/preference.go b/api/preference.go index 84cfc130c..810917a7c 100644 --- a/api/preference.go +++ b/api/preference.go @@ -20,7 +20,7 @@ func InitPreference(r *mux.Router) { } func setPreferences(c *Context, w http.ResponseWriter, r *http.Request) { - var preferences []model.Preference + var preferences []*model.Preference decoder := json.NewDecoder(r.Body) if err := decoder.Decode(&preferences); err != nil { @@ -29,21 +29,17 @@ func setPreferences(c *Context, w http.ResponseWriter, r *http.Request) { return } - // just attempt to save/update them one by one and abort if one fails - // in the future, this could probably be done in a transaction, but that's unnecessary now for _, preference := range preferences { if c.Session.UserId != preference.UserId { c.Err = model.NewAppError("setPreferences", "Unable to set preferences for other user", "session.user_id="+c.Session.UserId+", preference.user_id="+preference.UserId) c.Err.StatusCode = http.StatusUnauthorized return } + } - if result := <-Srv.Store.Preference().Save(&preference); result.Err != nil { - if result = <-Srv.Store.Preference().Update(&preference); result.Err != nil { - c.Err = result.Err - return - } - } + if result := <-Srv.Store.Preference().SaveOrUpdate(preferences...); result.Err != nil { + c.Err = result.Err + return } w.Write([]byte("true")) diff --git a/store/sql_preference_store.go b/store/sql_preference_store.go index 3e3a91f61..366a63fa6 100644 --- a/store/sql_preference_store.go +++ b/store/sql_preference_store.go @@ -39,53 +39,97 @@ func (s SqlPreferenceStore) Save(preference *model.Preference) StoreChannel { storeChannel := make(StoreChannel) go func() { - result := StoreResult{} + storeChannel <- s.save(s.GetMaster(), preference) + close(storeChannel) + }() - if result.Err = preference.IsValid(); result.Err != nil { - storeChannel <- result - close(storeChannel) - return - } + return storeChannel +} - if err := s.GetMaster().Insert(preference); err != nil { - if IsUniqueConstraintError(err.Error(), "UserId", "preferences_pkey") { - result.Err = model.NewAppError("SqlPreferenceStore.Save", "A preference with that user id, category, name, and alt id already exists", - "user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", alt_id="+preference.AltId+", "+err.Error()) - } else { - result.Err = model.NewAppError("SqlPreferenceStore.Save", "We couldn't save the preference", - "user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", alt_id="+preference.AltId+", "+err.Error()) - } +func (s SqlPreferenceStore) save(queryable Queryable, preference *model.Preference) StoreResult { + result := StoreResult{} + + if result.Err = preference.IsValid(); result.Err != nil { + return result + } + + if err := queryable.Insert(preference); err != nil { + if IsUniqueConstraintError(err.Error(), "UserId", "preferences_pkey") { + result.Err = model.NewAppError("SqlPreferenceStore.Save", "A preference with that user id, category, name, and alt id already exists", + "user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", alt_id="+preference.AltId+", "+err.Error()) } else { - result.Data = preference + result.Err = model.NewAppError("SqlPreferenceStore.Save", "We couldn't save the preference", + "user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", alt_id="+preference.AltId+", "+err.Error()) } + } else { + result.Data = preference + } - storeChannel <- result + return result +} + +func (s SqlPreferenceStore) Update(preference *model.Preference) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + storeChannel <- s.update(s.GetMaster(), preference) close(storeChannel) }() return storeChannel } -func (s SqlPreferenceStore) Update(preference *model.Preference) StoreChannel { +func (s SqlPreferenceStore) update(queryable Queryable, preference *model.Preference) StoreResult { + result := StoreResult{} + + if result.Err = preference.IsValid(); result.Err != nil { + return result + } + + if count, err := queryable.Update(preference); err != nil { + result.Err = model.NewAppError("SqlPreferenceStore.Update", "We couldn't update the preference", + "user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", alt_id="+preference.AltId+", "+err.Error()) + } else { + result.Data = count + } + + return result +} + +func (s SqlPreferenceStore) SaveOrUpdate(preferences ...*model.Preference) StoreChannel { storeChannel := make(StoreChannel) go func() { result := StoreResult{} - if result.Err = preference.IsValid(); result.Err != nil { - storeChannel <- result - close(storeChannel) - return - } + db := s.GetReplica() - if count, err := s.GetMaster().Update(preference); err != nil { - result.Err = model.NewAppError("SqlPreferenceStore.Update", "We couldn't update the preference", - "user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", alt_id="+preference.AltId+", "+err.Error()) - } else if count != 1 { - result.Err = model.NewAppError("SqlPreferenceStore.Update", "We couldn't update the preference", - "user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", alt_id="+preference.AltId) + if len(preferences) > 1 { + // wrap in a transaction so that if one fails, everything fails + transaction, err := db.Begin() + if err != nil { + result.Err = model.NewAppError("SqlPreferenceStore.SaveOrUpdateMultiple", "Unable to open transaction to update preferences", err.Error()) + } else { + for _, preference := range preferences { + if err := s.saveOrUpdate(transaction, preference); err != nil { + result.Err = err + break + } + } + + if result.Err == nil { + if err := transaction.Commit(); err != nil { + // don't need to rollback here since the transaction is already closed + result.Err = model.NewAppError("SqlPreferenceStore.SaveOrUpdateMultiple", "Unable to commit transaction to update preferences", err.Error()) + } + } else { + if err := transaction.Rollback(); err != nil { + result.Err = model.NewAppError("SqlPreferenceStore.SaveOrUpdateMultiple", "Unable to rollback transaction to update preferences", err.Error()) + } + } + } } else { - result.Data = preference + result.Err = s.saveOrUpdate(db, preferences[0]) } storeChannel <- result @@ -95,6 +139,16 @@ func (s SqlPreferenceStore) Update(preference *model.Preference) StoreChannel { return storeChannel } +func (s SqlPreferenceStore) saveOrUpdate(queryable Queryable, preference *model.Preference) *model.AppError { + if result := s.save(queryable, preference); result.Err != nil { + if result := s.update(queryable, preference); result.Err != nil { + return result.Err + } + } + + return nil +} + func (s SqlPreferenceStore) GetByName(userId string, category string, name string) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/sql_preference_store_test.go b/store/sql_preference_store_test.go index 99b183274..0574ee954 100644 --- a/store/sql_preference_store_test.go +++ b/store/sql_preference_store_test.go @@ -84,31 +84,70 @@ func TestPreferenceStoreUpdate(t *testing.T) { Must(store.Preference().Save(&p1)) p1.Value = "1234garbage" - if err := (<-store.Preference().Update(&p1)).Err; err != nil { - t.Fatal(err) + if result := (<-store.Preference().Update(&p1)); result.Err != nil { + t.Fatal(result.Err) + } else if result.Data.(int64) != 1 { + t.Fatal("update should have changed only 1 row") } p1.UserId = model.NewId() - if err := (<-store.Preference().Update(&p1)).Err; err == nil { - t.Fatal("update should have failed because of changed user id") + if result := (<-store.Preference().Update(&p1)); result.Err != nil { + t.Fatal(result.Err) + } else if result.Data.(int64) != 0 { + t.Fatal("update shouldn't have made changes because of changed user id") } p1.UserId = id p1.Category = model.PREFERENCE_CATEGORY_TEST - if err := (<-store.Preference().Update(&p1)).Err; err == nil { - t.Fatal("update should have failed because of changed category") + if result := (<-store.Preference().Update(&p1)); result.Err != nil { + t.Fatal(result.Err) + } else if result.Data.(int64) != 0 { + t.Fatal("update shouldn't have made changes because of changed category") } p1.Category = model.PREFERENCE_CATEGORY_DIRECT_CHANNELS p1.Name = model.PREFERENCE_NAME_TEST - if err := (<-store.Preference().Update(&p1)).Err; err == nil { - t.Fatal("update should have failed because of changed name") + if result := (<-store.Preference().Update(&p1)); result.Err != nil { + t.Fatal(result.Err) + } else if result.Data.(int64) != 0 { + t.Fatal("update shouldn't have made changes because of changed name") } p1.Name = model.PREFERENCE_NAME_SHOW p1.AltId = model.NewId() - if err := (<-store.Preference().Update(&p1)).Err; err == nil { - t.Fatal("update should have failed because of changed alternate id") + if result := (<-store.Preference().Update(&p1)); result.Err != nil { + t.Fatal(result.Err) + } else if result.Data.(int64) != 0 { + t.Fatal("update shouldn't have made changes because of changed alt id") + } +} + +func TestPreferenceSaveOrUpdate(t *testing.T) { + Setup() + + id := model.NewId() + + p1 := model.Preference{ + UserId: id, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_SHOW, + Value: "value1", + } + Must(store.Preference().SaveOrUpdate(&p1)) + + if preferences := Must(store.Preference().GetByName(p1.UserId, p1.Category, p1.Name)).([]*model.Preference); len(preferences) != 1 { + t.Fatal("got incorrect number of preferences after SaveOrUpdate") + } else if preferences[0].Value != "value1" { + t.Fatal("should have received value1 after SaveOrUpdate") + } + + p1.Value = "value2" + Must(store.Preference().SaveOrUpdate(&p1)) + + if preferences := Must(store.Preference().GetByName(p1.UserId, p1.Category, p1.Name)).([]*model.Preference); len(preferences) != 1 { + t.Fatal("got incorrect number of preferences after second SaveOrUpdate") + } else if preferences[0].Value != "value2" { + t.Fatal("should have received value2 after SaveOrUpdate") } } diff --git a/store/sql_store.go b/store/sql_store.go index 4b055e455..f8c585979 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -612,3 +612,9 @@ func decrypt(key []byte, cryptoText string) (string, error) { return fmt.Sprintf("%s", ciphertext), nil } + +// Interface for both gorp.DbMap and gorp.Transaction to allow code for one to be reused with the other +type Queryable interface { + Insert(list ...interface{}) error + Update(list ...interface{}) (int64, error) +} diff --git a/store/store.go b/store/store.go index 8a8faae85..df55bc6d8 100644 --- a/store/store.go +++ b/store/store.go @@ -154,5 +154,6 @@ type WebhookStore interface { type PreferenceStore interface { Save(preference *model.Preference) StoreChannel Update(preference *model.Preference) StoreChannel + SaveOrUpdate(preferences ...*model.Preference) StoreChannel GetByName(userId string, category string, name string) StoreChannel } -- cgit v1.2.3-1-g7c22 From ed6b2cd164b249c67db0e0421cc78964450240c9 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Wed, 7 Oct 2015 10:39:36 -0400 Subject: Removed setting direct channels for a new user at creation time --- api/preference.go | 43 ++++++++++++++++++++++++++++++++++++++++++- api/user.go | 49 ------------------------------------------------- 2 files changed, 42 insertions(+), 50 deletions(-) diff --git a/api/preference.go b/api/preference.go index 810917a7c..aa19ee071 100644 --- a/api/preference.go +++ b/api/preference.go @@ -59,10 +59,51 @@ func getPreferencesByName(c *Context, w http.ResponseWriter, r *http.Request) { if len(data) == 0 { if category == model.PREFERENCE_CATEGORY_DIRECT_CHANNELS && name == model.PREFERENCE_NAME_SHOW { // add direct channels for a user that existed before preferences were added - data = AddDirectChannels(c.Session.UserId, c.Session.TeamId) + data = addDirectChannels(c.Session.UserId, c.Session.TeamId) } } w.Write([]byte(model.PreferenceListToJson(data))) } } + +func addDirectChannels(userId, teamId string) []*model.Preference { + var profiles map[string]*model.User + if result := <-Srv.Store.User().GetProfiles(teamId); result.Err != nil { + l4g.Error("Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v", userId, teamId, result.Err.Error()) + return []*model.Preference{} + } else { + profiles = result.Data.(map[string]*model.User) + } + + var preferences []*model.Preference + + for id := range profiles { + if id == userId { + continue + } + + profile := profiles[id] + + preference := &model.Preference{ + UserId: userId, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, + Name: model.PREFERENCE_NAME_SHOW, + AltId: profile.Id, + Value: "true", + } + + if result := <-Srv.Store.Preference().Save(preference); result.Err != nil { + l4g.Error("Failed to add direct channel preferences for user user_id=%s, alt_id=%s, team_id=%s, err=%v", userId, profile.Id, teamId, result.Err.Error()) + continue + } + + preferences = append(preferences, preference) + + if len(preferences) >= 10 { + break + } + } + + return preferences +} diff --git a/api/user.go b/api/user.go index 204f00dcd..faf828cf9 100644 --- a/api/user.go +++ b/api/user.go @@ -198,8 +198,6 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err) } - fireAndForgetAddDirectChannels(ruser, team) - fireAndForgetWelcomeEmail(ruser.Email, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) if user.EmailVerified { if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil { @@ -220,53 +218,6 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { } } -func fireAndForgetAddDirectChannels(user *model.User, team *model.Team) { - go func() { - AddDirectChannels(user.Id, team.Id) - }() -} - -func AddDirectChannels(userId, teamId string) []*model.Preference { - var profiles map[string]*model.User - if result := <-Srv.Store.User().GetProfiles(teamId); result.Err != nil { - l4g.Error("Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v", userId, teamId, result.Err.Error()) - return []*model.Preference{} - } else { - profiles = result.Data.(map[string]*model.User) - } - - var preferences []*model.Preference - - for id := range profiles { - if id == userId { - continue - } - - profile := profiles[id] - - preference := &model.Preference{ - UserId: userId, - Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNELS, - Name: model.PREFERENCE_NAME_SHOW, - AltId: profile.Id, - Value: "true", - } - - if result := <-Srv.Store.Preference().Save(preference); result.Err != nil { - l4g.Error("Failed to add direct channel preferences for user user_id=%s, alt_id=%s, team_id=%s, err=%v", userId, profile.Id, teamId, result.Err.Error()) - continue - } - - preferences = append(preferences, preference) - - if len(preferences) >= 10 { - break - } - } - - return preferences -} - func fireAndForgetWelcomeEmail(email, teamDisplayName, siteURL, teamURL string) { go func() { -- cgit v1.2.3-1-g7c22 From 097d236f437b0c5af167cd383c6ee4c3ee45f495 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Wed, 7 Oct 2015 11:34:29 -0400 Subject: Fixed edge cases with leaving a direct channel while viewing that channel --- web/react/components/more_direct_channels.jsx | 15 --------------- web/react/components/sidebar.jsx | 15 ++++++++++----- web/react/stores/preference_store.jsx | 1 - web/react/utils/client.jsx | 1 + 4 files changed, 11 insertions(+), 21 deletions(-) diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index ed9c6fc58..96c08c441 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -82,21 +82,6 @@ export default class MoreDirectChannels extends React.Component { ); }; } - } else { - if (channel.id === ChannelStore.getCurrentId()) { - active = 'active'; - } - - if (channel.unread) { - badge = {channel.unread}; - titleClass = 'unread-title'; - } - - handleClick = function clickHandler(e) { - e.preventDefault(); - utils.switchChannel(channel); - $(React.findDOMNode(self.refs.modal)).modal('hide'); - }; } return ( diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 2619798eb..54edb3c31 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -33,6 +33,8 @@ export default class Sidebar extends React.Component { this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this); this.createChannelElement = this.createChannelElement.bind(this); + this.isLeaving = new Map(); + const state = this.getStateFromStores(); state.modal = ''; state.loadingDMChannel = -1; @@ -85,7 +87,8 @@ export default class Sidebar extends React.Component { const member = members[channel.id]; const msgCount = channel.total_msg_count - member.msg_count; - forceShow = currentId === channel.id || msgCount > 0; + // always show a channel if either it is the current one or if it is unread, but it is not currently being left + forceShow = (currentId === channel.id || msgCount > 0) && !this.isLeaving.get(channel.id); } channel.display_name = teammate.username; @@ -93,8 +96,10 @@ export default class Sidebar extends React.Component { channel.status = UserStore.getStatus(teammate.id); if (preferences.some((preference) => (preference.alt_id === teammate.id && preference.value !== 'false'))) { + console.log(teammate.id + " is visible"); visibleDirectChannels.push(channel); } else if (forceShow) { + console.log(teammate.id + " needs to be visible"); // make sure that unread direct channels are visible const preference = PreferenceStore.setPreferenceWithAltId(Constants.Preferences.CATEGORY_DIRECT_CHANNELS, Constants.Preferences.NAME_SHOW, teammate.id, 'true'); @@ -304,18 +309,18 @@ export default class Sidebar extends React.Component { } handleLeaveDirectChannel(channel) { - if (!channel.leaving) { - channel.leaving = true; + if (!this.isLeaving.get(channel.id)) { + this.isLeaving.set(channel.id, true); const preference = PreferenceStore.setPreferenceWithAltId(Constants.Preferences.CATEGORY_DIRECT_CHANNELS, Constants.Preferences.NAME_SHOW, channel.teammate_id, 'false'); AsyncClient.setPreferences( [preference], () => { - channel.leaving = false; + this.isLeaving.set(channel.id, false); }, () => { - channel.leaving = false; + this.isLeaving.set(channel.id, false); } ); diff --git a/web/react/stores/preference_store.jsx b/web/react/stores/preference_store.jsx index d731e7d27..8101452ed 100644 --- a/web/react/stores/preference_store.jsx +++ b/web/react/stores/preference_store.jsx @@ -39,7 +39,6 @@ class PreferenceStoreClass extends EventEmitter { } getAllPreferences() { - console.log('getting preferences'); // eslint-disable-line no-console return new Map(BrowserStore.getItem('preferences', [])); } diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 2134dc0f5..4e3505ad2 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -1159,6 +1159,7 @@ export function setPreferences(preferences, success, error) { $.ajax({ url: '/api/v1/preferences/set', dataType: 'json', + contentType: 'application/json', type: 'POST', data: JSON.stringify(preferences), success, -- cgit v1.2.3-1-g7c22 From a2517ee991df84803ca508b556910e543c535c1d Mon Sep 17 00:00:00 2001 From: hmhealey Date: Wed, 7 Oct 2015 13:07:59 -0400 Subject: Cleaned up JSX errors --- web/react/components/more_direct_channels.jsx | 57 +++++++++++++-------------- web/react/components/sidebar.jsx | 50 +++++++++++------------ 2 files changed, 51 insertions(+), 56 deletions(-) diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index 96c08c441..0fbd90096 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -16,10 +16,9 @@ export default class MoreDirectChannels extends React.Component { } componentDidMount() { - var self = this; - $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function showModal(e) { + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', (e) => { var button = e.relatedTarget; - self.setState({channels: $(button).data('channels')}); + this.setState({channels: $(button).data('channels')}); // eslint-disable-line react/no-did-mount-set-state }); } @@ -30,30 +29,16 @@ export default class MoreDirectChannels extends React.Component { } render() { - var self = this; - var directMessageItems = this.state.channels.map((channel, index) => { var badge = ''; var titleClass = ''; var handleClick = null; - if (!channel.fake) { - if (channel.unread) { - badge = {channel.unread}; - titleClass = 'unread-title'; - } - - handleClick = (e) => { - e.preventDefault(); - this.handleJoinDirectChannel(channel); - utils.switchChannel(channel); - $(React.findDOMNode(self.refs.modal)).modal('hide'); - }; - } else { + if (channel.fake) { // It's a direct message channel that doesn't exist yet so let's create it now var otherUserId = utils.getUserIdFromChannelName(channel); - if (self.state.loadingDMChannel === index) { + if (this.state.loadingDMChannel === index) { badge = ( { e.preventDefault(); - self.setState({loadingDMChannel: index}); + this.setState({loadingDMChannel: index}); this.handleJoinDirectChannel(channel); Client.createDirectChannel(channel, otherUserId, - function success(data) { - $(React.findDOMNode(self.refs.modal)).modal('hide'); - self.setState({loadingDMChannel: -1}); + (data) => { + $(React.findDOMNode(this.refs.modal)).modal('hide'); + this.setState({loadingDMChannel: -1}); AsyncClient.getChannel(data.id); utils.switchChannel(data); }, - function error() { - self.setState({loadingDMChannel: -1}); + () => { + this.setState({loadingDMChannel: -1}); window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; } ); }; } + } else { + if (channel.unread) { + badge = {channel.unread}; + titleClass = 'unread-title'; + } + + handleClick = (e) => { + e.preventDefault(); + this.handleJoinDirectChannel(channel); + utils.switchChannel(channel); + $(React.findDOMNode(this.refs.modal)).modal('hide'); + }; } return ( @@ -112,10 +109,10 @@ export default class MoreDirectChannels extends React.Component { className='close' data-dismiss='modal' > - - Close + + {'Close'} -

    More Direct Messages

    +

    {'More Direct Messages'}

      @@ -127,7 +124,7 @@ export default class MoreDirectChannels extends React.Component { type='button' className='btn btn-default' data-dismiss='modal' - >Close + >{'Close'}
    diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 54edb3c31..431f2ce1b 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -76,19 +76,19 @@ export default class Sidebar extends React.Component { let forceShow = false; let channel = ChannelStore.getByName(channelName); - if (!channel) { + if (channel) { + const member = members[channel.id]; + const msgCount = channel.total_msg_count - member.msg_count; + + // always show a channel if either it is the current one or if it is unread, but it is not currently being left + forceShow = (currentId === channel.id || msgCount > 0) && !this.isLeaving.get(channel.id); + } else { channel = {}; channel.fake = true; channel.name = channelName; channel.last_post_at = 0; channel.total_msg_count = 0; channel.type = 'D'; - } else { - const member = members[channel.id]; - const msgCount = channel.total_msg_count - member.msg_count; - - // always show a channel if either it is the current one or if it is unread, but it is not currently being left - forceShow = (currentId === channel.id || msgCount > 0) && !this.isLeaving.get(channel.id); } channel.display_name = teammate.username; @@ -96,10 +96,8 @@ export default class Sidebar extends React.Component { channel.status = UserStore.getStatus(teammate.id); if (preferences.some((preference) => (preference.alt_id === teammate.id && preference.value !== 'false'))) { - console.log(teammate.id + " is visible"); visibleDirectChannels.push(channel); } else if (forceShow) { - console.log(teammate.id + " needs to be visible"); // make sure that unread direct channels are visible const preference = PreferenceStore.setPreferenceWithAltId(Constants.Preferences.CATEGORY_DIRECT_CHANNELS, Constants.Preferences.NAME_SHOW, teammate.id, 'true'); @@ -117,9 +115,9 @@ export default class Sidebar extends React.Component { return { activeId: currentId, channels: ChannelStore.getAll(), - members: members, - visibleDirectChannels: visibleDirectChannels, - hiddenDirectChannels: hiddenDirectChannels + members, + visibleDirectChannels, + hiddenDirectChannels }; } @@ -419,10 +417,10 @@ export default class Sidebar extends React.Component { if (!channel.fake) { handleClick = function clickHandler(e) { - if (!e.target.attributes.getNamedItem('data-close')) { - Utils.switchChannel(channel); - } else { + if (e.target.attributes.getNamedItem('data-close')) { handleClose(channel); + } else { + Utils.switchChannel(channel); } e.preventDefault(); @@ -435,22 +433,22 @@ export default class Sidebar extends React.Component { handleClick = function clickHandler(e) { e.preventDefault(); - if (!e.target.attributes.getNamedItem('data-close')) { + if (e.target.attributes.getNamedItem('data-close')) { + handleClose(channel); + } else { this.setState({loadingDMChannel: index}); Client.createDirectChannel(channel, otherUserId, - function success(data) { + (data) => { this.setState({loadingDMChannel: -1}); AsyncClient.getChannel(data.id); Utils.switchChannel(data); - }.bind(this), - function error() { + }, + () => { this.setState({loadingDMChannel: -1}); window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; - }.bind(this) + } ); - } else { - handleClose(channel); } }.bind(this); } @@ -578,7 +576,7 @@ export default class Sidebar extends React.Component {
    @@ -605,7 +603,7 @@ export default class Sidebar extends React.Component {