Merge pull request #968 from hmhealey/plt322

PLT-322 Updated Direct Messages menu
Этот коммит содержится в:
Harrison Healey
2015-10-13 18:23:53 -04:00
родитель 998b5f7e11 97b2f6ffe7
Коммит 56b02f2eba
17 изменённых файлов: 1260 добавлений и 148 удалений

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

@@ -45,6 +45,7 @@ func InitApi() {
InitAdmin(r) InitAdmin(r)
InitOAuth(r) InitOAuth(r)
InitWebhook(r) InitWebhook(r)
InitPreference(r)
templatesDir := utils.FindDir("api/templates") templatesDir := utils.FindDir("api/templates")
l4g.Debug("Parsing server templates at %v", templatesDir) l4g.Debug("Parsing server templates at %v", templatesDir)

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

@@ -0,0 +1,121 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
l4g "code.google.com/p/log4go"
"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("/save", ApiUserRequired(savePreferences)).Methods("POST")
sr.Handle("/{category:[A-Za-z0-9_]+}", ApiUserRequired(getPreferenceCategory)).Methods("GET")
sr.Handle("/{category:[A-Za-z0-9_]+}/{name:[A-Za-z0-9_]+}", ApiUserRequired(getPreference)).Methods("GET")
}
func savePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
preferences, err := model.PreferencesFromJson(r.Body)
if err != nil {
c.Err = model.NewAppError("savePreferences", "Unable to decode preferences from request", err.Error())
c.Err.StatusCode = http.StatusBadRequest
return
}
for _, preference := range preferences {
if c.Session.UserId != preference.UserId {
c.Err = model.NewAppError("savePreferences", "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(&preferences); result.Err != nil {
c.Err = result.Err
return
}
w.Write([]byte("true"))
}
func getPreferenceCategory(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
category := params["category"]
if result := <-Srv.Store.Preference().GetCategory(c.Session.UserId, category); result.Err != nil {
c.Err = result.Err
} else {
data := result.Data.(model.Preferences)
data = transformPreferences(c, data, category)
w.Write([]byte(data.ToJson()))
}
}
func transformPreferences(c *Context, preferences model.Preferences, category string) model.Preferences {
if len(preferences) == 0 && category == model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW {
// add direct channels for a user that existed before preferences were added
preferences = addDirectChannels(c.Session.UserId, c.Session.TeamId)
}
return preferences
}
func addDirectChannels(userId, teamId string) model.Preferences {
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.Preferences{}
} else {
profiles = result.Data.(map[string]*model.User)
}
var preferences model.Preferences
for id := range profiles {
if id == userId {
continue
}
profile := profiles[id]
preference := model.Preference{
UserId: userId,
Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
Name: profile.Id,
Value: "true",
}
preferences = append(preferences, preference)
if len(preferences) >= 10 {
break
}
}
if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil {
l4g.Error("Failed to add direct channel preferences for user user_id=%s, eam_id=%s, err=%v", userId, teamId, result.Err.Error())
return model.Preferences{}
} else {
return preferences
}
}
func getPreference(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
category := params["category"]
name := params["name"]
if result := <-Srv.Store.Preference().Get(c.Session.UserId, category, name); result.Err != nil {
c.Err = result.Err
} else {
data := result.Data.(model.Preference)
w.Write([]byte(data.ToJson()))
}
}

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

@@ -0,0 +1,201 @@
// 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.Preferences
for i := 0; i < 10; i++ {
preference := model.Preference{
UserId: user1.Id,
Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
Name: 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))
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")
}
}
func TestGetPreferenceCategory(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))
category := model.NewId()
preferences1 := model.Preferences{
{
UserId: user1.Id,
Category: category,
Name: model.NewId(),
},
{
UserId: user1.Id,
Category: category,
Name: model.NewId(),
},
{
UserId: user1.Id,
Category: model.NewId(),
Name: model.NewId(),
},
}
Client.LoginByEmail(team.Name, user1.Email, "pwd")
Client.Must(Client.SetPreferences(&preferences1))
Client.LoginByEmail(team.Name, user1.Email, "pwd")
if result, err := Client.GetPreferenceCategory(category); err != nil {
t.Fatal(err)
} else if data := result.Data.(model.Preferences); 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.GetPreferenceCategory(category); err != nil {
t.Fatal(err)
} else if data := result.Data.(model.Preferences); len(data) != 0 {
t.Fatal("received the wrong number of preferences")
}
}
func TestTransformPreferences(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))
for i := 0; i < 5; i++ {
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
Client.Must(Client.CreateUser(user, ""))
}
Client.Must(Client.LoginByEmail(team.Name, user1.Email, "pwd"))
if result, err := Client.GetPreferenceCategory(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW); err != nil {
t.Fatal(err)
} else if data := result.Data.(model.Preferences); len(data) != 5 {
t.Fatal("received the wrong number of direct channels")
}
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))
for i := 0; i < 10; i++ {
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
Client.Must(Client.CreateUser(user, ""))
}
// make sure user1's preferences don't change
if result, err := Client.GetPreferenceCategory(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW); err != nil {
t.Fatal(err)
} else if data := result.Data.(model.Preferences); len(data) != 5 {
t.Fatal("received the wrong number of direct channels")
}
Client.Must(Client.LoginByEmail(team.Name, user2.Email, "pwd"))
if result, err := Client.GetPreferenceCategory(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW); err != nil {
t.Fatal(err)
} else if data := result.Data.(model.Preferences); len(data) != 10 {
t.Fatal("received the wrong number of direct channels")
}
}
func TestGetPreference(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")
preferences := model.Preferences{
{
UserId: user.Id,
Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
Name: model.NewId(),
Value: model.NewId(),
},
}
Client.Must(Client.SetPreferences(&preferences))
if result, err := Client.GetPreference(preferences[0].Category, preferences[0].Name); err != nil {
t.Fatal(err)
} else if data := result.Data.(*model.Preference); *data != preferences[0] {
t.Fatal("preference saved incorrectly")
}
preferences[0].Value = model.NewId()
Client.Must(Client.SetPreferences(&preferences))
if result, err := Client.GetPreference(preferences[0].Category, preferences[0].Name); err != nil {
t.Fatal(err)
} else if data := result.Data.(*model.Preference); *data != preferences[0] {
t.Fatal("preference updated incorrectly")
}
}

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

@@ -844,6 +844,32 @@ func (c *Client) ListIncomingWebhooks() (*Result, *AppError) {
} }
} }
func (c *Client) SetPreferences(preferences *Preferences) (*Result, *AppError) {
if r, err := c.DoApiPost("/preferences/save", preferences.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), preferences}, nil
}
}
func (c *Client) GetPreference(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), PreferenceFromJson(r.Body)}, nil
}
}
func (c *Client) GetPreferenceCategory(category string) (*Result, *AppError) {
if r, err := c.DoApiGet("/preferences/"+category, "", ""); err != nil {
return nil, err
} else {
preferences, _ := PreferencesFromJson(r.Body)
return &Result{r.Header.Get(HEADER_REQUEST_ID), r.Header.Get(HEADER_ETAG_SERVER), preferences}, nil
}
}
func (c *Client) MockSession(sessionToken string) { func (c *Client) MockSession(sessionToken string) {
c.AuthToken = sessionToken c.AuthToken = sessionToken
c.AuthType = HEADER_BEARER c.AuthType = HEADER_BEARER

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

@@ -0,0 +1,60 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW = "direct_channel_show"
)
type Preference struct {
UserId string `json:"user_id"`
Category string `json:"category"`
Name string `json:"name"`
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 {
return NewAppError("Preference.IsValid", "Invalid category", "category="+o.Category)
}
if len(o.Name) == 0 || len(o.Name) > 32 {
return NewAppError("Preference.IsValid", "Invalid name", "name="+o.Name)
}
if len(o.Value) > 128 {
return NewAppError("Preference.IsValid", "Value is too long", "value="+o.Value)
}
return nil
}

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

@@ -0,0 +1,56 @@
// 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{
UserId: "1234garbage",
Category: PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
Name: NewId(),
}
if err := preference.IsValid(); err == nil {
t.Fatal()
}
preference.UserId = NewId()
if err := preference.IsValid(); err != nil {
t.Fatal(err)
}
preference.Category = strings.Repeat("01234567890", 20)
if err := preference.IsValid(); err == nil {
t.Fatal()
}
preference.Category = PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW
if err := preference.IsValid(); err != nil {
t.Fatal()
}
preference.Name = strings.Repeat("01234567890", 20)
if err := preference.IsValid(); err == nil {
t.Fatal()
}
preference.Name = NewId()
if err := preference.IsValid(); err != nil {
t.Fatal()
}
preference.Value = strings.Repeat("01234567890", 20)
if err := preference.IsValid(); err == nil {
t.Fatal()
}
preference.Value = "1234garbage"
if err := preference.IsValid(); err != nil {
t.Fatal()
}
}

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

@@ -0,0 +1,31 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type Preferences []Preference
func (o *Preferences) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func PreferencesFromJson(data io.Reader) (Preferences, error) {
decoder := json.NewDecoder(data)
var o Preferences
err := decoder.Decode(&o)
if err == nil {
return o, nil
} else {
return nil, err
}
}

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

@@ -0,0 +1,214 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package store
import (
"github.com/go-gorp/gorp"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
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")
table.ColMap("UserId").SetMaxSize(26)
table.ColMap("Category").SetMaxSize(32)
table.ColMap("Name").SetMaxSize(32)
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(preferences *model.Preferences) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
// wrap in a transaction so that if one fails, everything fails
transaction, err := s.GetReplica().Begin()
if err != nil {
result.Err = model.NewAppError("SqlPreferenceStore.Save", "Unable to open transaction to save preferences", err.Error())
} else {
for _, preference := range *preferences {
if upsertResult := s.save(transaction, &preference); upsertResult.Err != nil {
result = upsertResult
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.Save", "Unable to commit transaction to save preferences", err.Error())
} else {
result.Data = len(*preferences)
}
} else {
if err := transaction.Rollback(); err != nil {
result.Err = model.NewAppError("SqlPreferenceStore.Save", "Unable to rollback transaction to save preferences", err.Error())
}
}
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlPreferenceStore) save(transaction *gorp.Transaction, preference *model.Preference) StoreResult {
result := StoreResult{}
if result.Err = preference.IsValid(); result.Err != nil {
return result
}
params := map[string]interface{}{
"UserId": preference.UserId,
"Category": preference.Category,
"Name": preference.Name,
"Value": preference.Value,
}
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
if _, err := transaction.Exec(
`INSERT INTO
Preferences
(UserId, Category, Name, Value)
VALUES
(:UserId, :Category, :Name, :Value)
ON DUPLICATE KEY UPDATE
Value = :Value`, params); err != nil {
result.Err = model.NewAppError("SqlPreferenceStore.save", "We encountered an error while updating preferences", err.Error())
}
} else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
// postgres has no way to upsert values until version 9.5 and trying inserting and then updating causes transactions to abort
count, err := transaction.SelectInt(
`SELECT
count(0)
FROM
Preferences
WHERE
UserId = :UserId
AND Category = :Category
AND Name = :Name`, params)
if err != nil {
result.Err = model.NewAppError("SqlPreferenceStore.save", "We encountered an error while updating preferences", err.Error())
return result
}
if count == 1 {
s.update(transaction, preference)
} else {
s.insert(transaction, preference)
}
} else {
result.Err = model.NewAppError("SqlPreferenceStore.save", "We encountered an error while updating preferences",
"Failed to update preference because of missing driver")
}
return result
}
func (s SqlPreferenceStore) insert(transaction *gorp.Transaction, preference *model.Preference) StoreResult {
result := StoreResult{}
if err := transaction.Insert(preference); err != nil {
if IsUniqueConstraintError(err.Error(), "UserId", "preferences_pkey") {
result.Err = model.NewAppError("SqlPreferenceStore.insert", "A preference with that user id, category, and name already exists",
"user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", "+err.Error())
} else {
result.Err = model.NewAppError("SqlPreferenceStore.insert", "We couldn't save the preference",
"user_id="+preference.UserId+", category="+preference.Category+", name="+preference.Name+", "+err.Error())
}
}
return result
}
func (s SqlPreferenceStore) update(transaction *gorp.Transaction, preference *model.Preference) StoreResult {
result := StoreResult{}
if _, err := transaction.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+", "+err.Error())
}
return result
}
func (s SqlPreferenceStore) Get(userId string, category string, name string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
var preference model.Preference
if err := s.GetReplica().SelectOne(&preference,
`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.Get", "We encounted an error while finding preferences", err.Error())
} else {
result.Data = preference
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlPreferenceStore) GetCategory(userId string, category string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
var preferences model.Preferences
if _, err := s.GetReplica().Select(&preferences,
`SELECT
*
FROM
Preferences
WHERE
UserId = :UserId
AND Category = :Category`, map[string]interface{}{"UserId": userId, "Category": category}); err != nil {
result.Err = model.NewAppError("SqlPreferenceStore.GetCategory", "We encounted an error while finding preferences", err.Error())
} else {
result.Data = preferences
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}

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

@@ -0,0 +1,146 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package store
import (
"github.com/mattermost/platform/model"
"testing"
)
func TestPreferenceSave(t *testing.T) {
Setup()
id := model.NewId()
preferences := model.Preferences{
{
UserId: id,
Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
Name: model.NewId(),
Value: "value1a",
},
{
UserId: id,
Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
Name: model.NewId(),
Value: "value1b",
},
}
if count := Must(store.Preference().Save(&preferences)); count != 2 {
t.Fatal("got incorrect number of rows saved")
}
for _, preference := range preferences {
if data := Must(store.Preference().Get(preference.UserId, preference.Category, preference.Name)).(model.Preference); preference != data {
t.Fatal("got incorrect preference after first Save")
}
}
preferences[0].Value = "value2a"
preferences[1].Value = "value2b"
if count := Must(store.Preference().Save(&preferences)); count != 2 {
t.Fatal("got incorrect number of rows saved")
}
for _, preference := range preferences {
if data := Must(store.Preference().Get(preference.UserId, preference.Category, preference.Name)).(model.Preference); preference != data {
t.Fatal("got incorrect preference after second Save")
}
}
}
func TestPreferenceGet(t *testing.T) {
Setup()
userId := model.NewId()
category := model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW
name := model.NewId()
preferences := model.Preferences{
{
UserId: userId,
Category: category,
Name: name,
},
{
UserId: userId,
Category: category,
Name: model.NewId(),
},
{
UserId: userId,
Category: model.NewId(),
Name: name,
},
{
UserId: model.NewId(),
Category: category,
Name: name,
},
}
Must(store.Preference().Save(&preferences))
if result := <-store.Preference().Get(userId, category, name); result.Err != nil {
t.Fatal(result.Err)
} else if data := result.Data.(model.Preference); data != preferences[0] {
t.Fatal("got incorrect preference")
}
// make sure getting a missing preference fails
if result := <-store.Preference().Get(model.NewId(), model.NewId(), model.NewId()); result.Err == nil {
t.Fatal("no error on getting a missing preference")
}
}
func TestPreferenceGetCategory(t *testing.T) {
Setup()
userId := model.NewId()
category := model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW
name := model.NewId()
preferences := model.Preferences{
{
UserId: userId,
Category: category,
Name: name,
},
// same user/category, different name
{
UserId: userId,
Category: category,
Name: model.NewId(),
},
// same user/name, different category
{
UserId: userId,
Category: model.NewId(),
Name: name,
},
// same name/category, different user
{
UserId: model.NewId(),
Category: category,
Name: name,
},
}
Must(store.Preference().Save(&preferences))
if result := <-store.Preference().GetCategory(userId, category); result.Err != nil {
t.Fatal(result.Err)
} else if data := result.Data.(model.Preferences); len(data) != 2 {
t.Fatal("got the wrong number of preferences")
} else if !((data[0] == preferences[0] && data[1] == preferences[1]) || (data[0] == preferences[1] && data[1] == preferences[0])) {
t.Fatal("got incorrect preferences")
}
// make sure getting a missing preference category doesn't fail
if result := <-store.Preference().GetCategory(model.NewId(), model.NewId()); result.Err != nil {
t.Fatal(result.Err)
} else if data := result.Data.(model.Preferences); len(data) != 0 {
t.Fatal("shouldn't have got any preferences")
}
}

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

@@ -30,17 +30,18 @@ import (
) )
type SqlStore struct { type SqlStore struct {
master *gorp.DbMap master *gorp.DbMap
replicas []*gorp.DbMap replicas []*gorp.DbMap
team TeamStore team TeamStore
channel ChannelStore channel ChannelStore
post PostStore post PostStore
user UserStore user UserStore
audit AuditStore audit AuditStore
session SessionStore session SessionStore
oauth OAuthStore oauth OAuthStore
system SystemStore system SystemStore
webhook WebhookStore webhook WebhookStore
preference PreferenceStore
} }
func NewSqlStore() Store { func NewSqlStore() Store {
@@ -93,6 +94,7 @@ func NewSqlStore() Store {
sqlStore.oauth = NewSqlOAuthStore(sqlStore) sqlStore.oauth = NewSqlOAuthStore(sqlStore)
sqlStore.system = NewSqlSystemStore(sqlStore) sqlStore.system = NewSqlSystemStore(sqlStore)
sqlStore.webhook = NewSqlWebhookStore(sqlStore) sqlStore.webhook = NewSqlWebhookStore(sqlStore)
sqlStore.preference = NewSqlPreferenceStore(sqlStore)
sqlStore.master.CreateTablesIfNotExists() sqlStore.master.CreateTablesIfNotExists()
@@ -105,6 +107,7 @@ func NewSqlStore() Store {
sqlStore.oauth.(*SqlOAuthStore).UpgradeSchemaIfNeeded() sqlStore.oauth.(*SqlOAuthStore).UpgradeSchemaIfNeeded()
sqlStore.system.(*SqlSystemStore).UpgradeSchemaIfNeeded() sqlStore.system.(*SqlSystemStore).UpgradeSchemaIfNeeded()
sqlStore.webhook.(*SqlWebhookStore).UpgradeSchemaIfNeeded() sqlStore.webhook.(*SqlWebhookStore).UpgradeSchemaIfNeeded()
sqlStore.preference.(*SqlPreferenceStore).UpgradeSchemaIfNeeded()
sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists() sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists()
sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists() sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists()
@@ -115,6 +118,7 @@ func NewSqlStore() Store {
sqlStore.oauth.(*SqlOAuthStore).CreateIndexesIfNotExists() sqlStore.oauth.(*SqlOAuthStore).CreateIndexesIfNotExists()
sqlStore.system.(*SqlSystemStore).CreateIndexesIfNotExists() sqlStore.system.(*SqlSystemStore).CreateIndexesIfNotExists()
sqlStore.webhook.(*SqlWebhookStore).CreateIndexesIfNotExists() sqlStore.webhook.(*SqlWebhookStore).CreateIndexesIfNotExists()
sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists()
if model.IsPreviousVersion(schemaVersion) { if model.IsPreviousVersion(schemaVersion) {
sqlStore.system.Update(&model.System{Name: "Version", Value: model.CurrentVersion}) sqlStore.system.Update(&model.System{Name: "Version", Value: model.CurrentVersion})
@@ -472,6 +476,10 @@ func (ss SqlStore) Webhook() WebhookStore {
return ss.webhook return ss.webhook
} }
func (ss SqlStore) Preference() PreferenceStore {
return ss.preference
}
type mattermConverter struct{} type mattermConverter struct{}
func (me mattermConverter) ToDb(val interface{}) (interface{}, error) { func (me mattermConverter) ToDb(val interface{}) (interface{}, error) {

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

@@ -37,6 +37,7 @@ type Store interface {
OAuth() OAuthStore OAuth() OAuthStore
System() SystemStore System() SystemStore
Webhook() WebhookStore Webhook() WebhookStore
Preference() PreferenceStore
Close() Close()
} }
@@ -149,3 +150,9 @@ type WebhookStore interface {
GetIncomingByUser(userId string) StoreChannel GetIncomingByUser(userId string) StoreChannel
DeleteIncoming(webhookId string, time int64) StoreChannel DeleteIncoming(webhookId string, time int64) StoreChannel
} }
type PreferenceStore interface {
Save(preferences *model.Preferences) StoreChannel
Get(userId string, category string, name string) StoreChannel
GetCategory(userId string, category string) StoreChannel
}

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

@@ -1,10 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
var ChannelStore = require('../stores/channel_store.jsx');
var TeamStore = require('../stores/team_store.jsx'); var TeamStore = require('../stores/team_store.jsx');
var Client = require('../utils/client.jsx'); var Client = require('../utils/client.jsx');
var Constants = require('../utils/constants.jsx');
var AsyncClient = require('../utils/async_client.jsx'); var AsyncClient = require('../utils/async_client.jsx');
var PreferenceStore = require('../stores/preference_store.jsx');
var utils = require('../utils/utils.jsx'); var utils = require('../utils/utils.jsx');
export default class MoreDirectChannels extends React.Component { export default class MoreDirectChannels extends React.Component {
@@ -15,27 +16,28 @@ export default class MoreDirectChannels extends React.Component {
} }
componentDidMount() { componentDidMount() {
var self = this; $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', (e) => {
$(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function showModal(e) {
var button = e.relatedTarget; 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
}); });
} }
render() { handleJoinDirectChannel(channel) {
var self = this; const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channel.teammate_id, 'true');
AsyncClient.savePreferences([preference]);
}
var directMessageItems = this.state.channels.map(function mapActivityToChannel(channel, index) { render() {
var directMessageItems = this.state.channels.map((channel, index) => {
var badge = ''; var badge = '';
var titleClass = ''; var titleClass = '';
var active = '';
var handleClick = null; var handleClick = null;
if (channel.fake) { if (channel.fake) {
// It's a direct message channel that doesn't exist yet so let's create it now // It's a direct message channel that doesn't exist yet so let's create it now
var otherUserId = utils.getUserIdFromChannelName(channel); var otherUserId = utils.getUserIdFromChannelName(channel);
if (self.state.loadingDMChannel === index) { if (this.state.loadingDMChannel === index) {
badge = ( badge = (
<img <img
className='channel-loading-gif pull-right' className='channel-loading-gif pull-right'
@@ -44,47 +46,42 @@ export default class MoreDirectChannels extends React.Component {
); );
} }
if (self.state.loadingDMChannel === -1) { if (this.state.loadingDMChannel === -1) {
handleClick = function clickHandler(e) { handleClick = (e) => {
e.preventDefault(); e.preventDefault();
self.setState({loadingDMChannel: index}); this.setState({loadingDMChannel: index});
this.handleJoinDirectChannel(channel);
Client.createDirectChannel(channel, otherUserId, Client.createDirectChannel(channel, otherUserId,
function success(data) { (data) => {
$(React.findDOMNode(self.refs.modal)).modal('hide'); $(React.findDOMNode(this.refs.modal)).modal('hide');
self.setState({loadingDMChannel: -1}); this.setState({loadingDMChannel: -1});
AsyncClient.getChannel(data.id); AsyncClient.getChannel(data.id);
utils.switchChannel(data); utils.switchChannel(data);
}, },
function error() { () => {
self.setState({loadingDMChannel: -1}); this.setState({loadingDMChannel: -1});
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name;
} }
); );
}; };
} }
} else { } else {
if (channel.id === ChannelStore.getCurrentId()) {
active = 'active';
}
if (channel.unread) { if (channel.unread) {
badge = <span className='badge pull-right small'>{channel.unread}</span>; badge = <span className='badge pull-right small'>{channel.unread}</span>;
titleClass = 'unread-title'; titleClass = 'unread-title';
} }
handleClick = function clickHandler(e) { handleClick = (e) => {
e.preventDefault(); e.preventDefault();
this.handleJoinDirectChannel(channel);
utils.switchChannel(channel); utils.switchChannel(channel);
$(React.findDOMNode(self.refs.modal)).modal('hide'); $(React.findDOMNode(this.refs.modal)).modal('hide');
}; };
} }
return ( return (
<li <li key={channel.name}>
key={channel.name}
className={active}
>
<a <a
className={'sidebar-channel ' + titleClass} className={'sidebar-channel ' + titleClass}
href='#' href='#'
@@ -111,10 +108,10 @@ export default class MoreDirectChannels extends React.Component {
className='close' className='close'
data-dismiss='modal' data-dismiss='modal'
> >
<span aria-hidden='true'>&times;</span> <span aria-hidden='true'>{'×'}</span>
<span className='sr-only'>Close</span> <span className='sr-only'>{'Close'}</span>
</button> </button>
<h4 className='modal-title'>More Direct Messages</h4> <h4 className='modal-title'>{'More Direct Messages'}</h4>
</div> </div>
<div className='modal-body'> <div className='modal-body'>
<ul className='nav nav-pills nav-stacked'> <ul className='nav nav-pills nav-stacked'>
@@ -126,7 +123,7 @@ export default class MoreDirectChannels extends React.Component {
type='button' type='button'
className='btn btn-default' className='btn btn-default'
data-dismiss='modal' data-dismiss='modal'
>Close</button> >{'Close'}</button>
</div> </div>
</div> </div>
</div> </div>

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

@@ -1,19 +1,20 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
var ChannelStore = require('../stores/channel_store.jsx'); const AsyncClient = require('../utils/async_client.jsx');
var Client = require('../utils/client.jsx'); const BrowserStore = require('../stores/browser_store.jsx');
var AsyncClient = require('../utils/async_client.jsx'); const ChannelStore = require('../stores/channel_store.jsx');
var SocketStore = require('../stores/socket_store.jsx'); const Client = require('../utils/client.jsx');
var UserStore = require('../stores/user_store.jsx'); const Constants = require('../utils/constants.jsx');
var TeamStore = require('../stores/team_store.jsx'); const PreferenceStore = require('../stores/preference_store.jsx');
var BrowserStore = require('../stores/browser_store.jsx'); const NewChannelFlow = require('./new_channel_flow.jsx');
var Utils = require('../utils/utils.jsx'); const SearchBox = require('./search_bar.jsx');
var SidebarHeader = require('./sidebar_header.jsx'); const SidebarHeader = require('./sidebar_header.jsx');
var SearchBox = require('./search_bar.jsx'); const SocketStore = require('../stores/socket_store.jsx');
var Constants = require('../utils/constants.jsx'); const TeamStore = require('../stores/team_store.jsx');
var NewChannelFlow = require('./new_channel_flow.jsx'); const UnreadChannelIndicator = require('./unread_channel_indicator.jsx');
var 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 { export default class Sidebar extends React.Component {
constructor(props) { constructor(props) {
@@ -23,12 +24,17 @@ export default class Sidebar extends React.Component {
this.firstUnreadChannel = null; this.firstUnreadChannel = null;
this.lastUnreadChannel = null; this.lastUnreadChannel = null;
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onChange = this.onChange.bind(this); this.onChange = this.onChange.bind(this);
this.onScroll = this.onScroll.bind(this); this.onScroll = this.onScroll.bind(this);
this.onResize = this.onResize.bind(this); this.onResize = this.onResize.bind(this);
this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this); this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this);
this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this);
this.createChannelElement = this.createChannelElement.bind(this); this.createChannelElement = this.createChannelElement.bind(this);
this.isLeaving = new Map();
const state = this.getStateFromStores(); const state = this.getStateFromStores();
state.modal = ''; state.modal = '';
state.loadingDMChannel = -1; state.loadingDMChannel = -1;
@@ -36,7 +42,7 @@ export default class Sidebar extends React.Component {
this.state = state; this.state = state;
} }
getStateFromStores() { getStateFromStores() {
var members = ChannelStore.getAllMembers(); const members = ChannelStore.getAllMembers();
var teamMemberMap = UserStore.getActiveOnlyProfiles(); var teamMemberMap = UserStore.getActiveOnlyProfiles();
var currentId = ChannelStore.getCurrentId(); var currentId = ChannelStore.getCurrentId();
@@ -48,11 +54,13 @@ export default class Sidebar extends React.Component {
teammates.push(teamMemberMap[id]); teammates.push(teamMemberMap[id]);
} }
const preferences = PreferenceStore.getPreferences(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
// Create lists of all read and unread direct channels // Create lists of all read and unread direct channels
var showDirectChannels = []; var visibleDirectChannels = [];
var readDirectChannels = []; var hiddenDirectChannels = [];
for (var i = 0; i < teammates.length; i++) { for (var i = 0; i < teammates.length; i++) {
var teammate = teammates[i]; const teammate = teammates[i];
if (teammate.id === UserStore.getCurrentId()) { if (teammate.id === UserStore.getCurrentId()) {
continue; continue;
@@ -65,90 +73,63 @@ export default class Sidebar extends React.Component {
channelName = teammate.id + '__' + UserStore.getCurrentId(); channelName = teammate.id + '__' + UserStore.getCurrentId();
} }
var channel = ChannelStore.getByName(channelName); let forceShow = false;
let channel = ChannelStore.getByName(channelName);
if (channel == null) { if (channel) {
var tempChannel = {}; const member = members[channel.id];
tempChannel.fake = true; const msgCount = channel.total_msg_count - member.msg_count;
tempChannel.name = channelName;
tempChannel.display_name = teammate.username; // always show a channel if either it is the current one or if it is unread, but it is not currently being left
tempChannel.teammate_username = teammate.username; forceShow = (currentId === channel.id || msgCount > 0) && !this.isLeaving.get(channel.id);
tempChannel.status = UserStore.getStatus(teammate.id);
tempChannel.last_post_at = 0;
tempChannel.total_msg_count = 0;
tempChannel.type = 'D';
readDirectChannels.push(tempChannel);
} else { } else {
channel.display_name = teammate.username; channel = {};
channel.teammate_username = teammate.username; channel.fake = true;
channel.name = channelName;
channel.last_post_at = 0;
channel.total_msg_count = 0;
channel.type = 'D';
}
channel.status = UserStore.getStatus(teammate.id); channel.display_name = teammate.username;
channel.teammate_id = teammate.id;
channel.status = UserStore.getStatus(teammate.id);
var channelMember = members[channel.id]; if (preferences.some((preference) => (preference.name === teammate.id && preference.value !== 'false'))) {
var msgCount = channel.total_msg_count - channelMember.msg_count; visibleDirectChannels.push(channel);
if (msgCount > 0) { } else if (forceShow) {
showDirectChannels.push(channel); // make sure that unread direct channels are visible
} else if (currentId === channel.id) { const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true');
showDirectChannels.push(channel); AsyncClient.savePreferences([preference]);
} else {
readDirectChannels.push(channel); visibleDirectChannels.push(channel);
} } else {
hiddenDirectChannels.push(channel);
} }
} }
// If we don't have MAX_DMS unread channels, sort the read list by last_post_at visibleDirectChannels.sort(this.sortChannelsByDisplayName);
if (showDirectChannels.length < Constants.MAX_DMS) { hiddenDirectChannels.sort(this.sortChannelsByDisplayName);
readDirectChannels.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 (showDirectChannels.length < Constants.MAX_DMS && index < readDirectChannels.length) {
showDirectChannels.push(readDirectChannels[index]);
index++;
}
readDirectChannels = readDirectChannels.slice(index);
showDirectChannels.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;
});
}
return { return {
activeId: currentId, activeId: currentId,
channels: ChannelStore.getAll(), channels: ChannelStore.getAll(),
members: members, members,
showDirectChannels: showDirectChannels, visibleDirectChannels,
hideDirectChannels: readDirectChannels hiddenDirectChannels
}; };
} }
componentDidMount() { componentDidMount() {
ChannelStore.addChangeListener(this.onChange); ChannelStore.addChangeListener(this.onChange);
UserStore.addChangeListener(this.onChange); UserStore.addChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onChange);
TeamStore.addChangeListener(this.onChange); TeamStore.addChangeListener(this.onChange);
SocketStore.addChangeListener(this.onSocketChange); SocketStore.addChangeListener(this.onSocketChange);
PreferenceStore.addChangeListener(this.onChange);
AsyncClient.getDirectChannelPreferences();
$('.nav-pills__container').perfectScrollbar(); $('.nav-pills__container').perfectScrollbar();
this.updateTitle(); this.updateTitle();
@@ -178,6 +159,7 @@ export default class Sidebar extends React.Component {
UserStore.removeStatusesChangeListener(this.onChange); UserStore.removeStatusesChangeListener(this.onChange);
TeamStore.removeChangeListener(this.onChange); TeamStore.removeChangeListener(this.onChange);
SocketStore.removeChangeListener(this.onSocketChange); SocketStore.removeChangeListener(this.onSocketChange);
PreferenceStore.removeChangeListener(this.onChange);
} }
onChange() { onChange() {
var newState = this.getStateFromStores(); var newState = this.getStateFromStores();
@@ -322,7 +304,37 @@ export default class Sidebar extends React.Component {
showBottomUnread showBottomUnread
}); });
} }
createChannelElement(channel, index) {
handleLeaveDirectChannel(channel) {
if (!this.isLeaving.get(channel.id)) {
this.isLeaving.set(channel.id, true);
const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channel.teammate_id, 'false');
// bypass AsyncClient since we've already saved the updated preferences
Client.savePreferences(
[preference],
() => {
this.isLeaving.set(channel.id, false);
},
() => {
this.isLeaving.set(channel.id, 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 members = this.state.members;
var activeId = this.state.activeId; var activeId = this.state.activeId;
var channelMember = members[channel.id]; var channelMember = members[channel.id];
@@ -405,8 +417,13 @@ export default class Sidebar extends React.Component {
if (!channel.fake) { if (!channel.fake) {
handleClick = function clickHandler(e) { handleClick = function clickHandler(e) {
if (e.target.attributes.getNamedItem('data-close')) {
handleClose(channel);
} else {
Utils.switchChannel(channel);
}
e.preventDefault(); e.preventDefault();
Utils.switchChannel(channel);
}; };
} else if (channel.fake && teamURL) { } else if (channel.fake && teamURL) {
// It's a direct message channel that doesn't exist yet so let's create it now // It's a direct message channel that doesn't exist yet so let's create it now
@@ -415,23 +432,40 @@ export default class Sidebar extends React.Component {
if (this.state.loadingDMChannel === -1) { if (this.state.loadingDMChannel === -1) {
handleClick = function clickHandler(e) { handleClick = function clickHandler(e) {
e.preventDefault(); e.preventDefault();
this.setState({loadingDMChannel: index});
Client.createDirectChannel(channel, otherUserId, if (e.target.attributes.getNamedItem('data-close')) {
function success(data) { handleClose(channel);
this.setState({loadingDMChannel: -1}); } else {
AsyncClient.getChannel(data.id); this.setState({loadingDMChannel: index});
Utils.switchChannel(data);
}.bind(this), Client.createDirectChannel(channel, otherUserId,
function error() { (data) => {
this.setState({loadingDMChannel: -1}); this.setState({loadingDMChannel: -1});
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; AsyncClient.getChannel(data.id);
}.bind(this) Utils.switchChannel(data);
); },
() => {
this.setState({loadingDMChannel: -1});
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name;
}
);
}
}.bind(this); }.bind(this);
} }
} }
let closeButton = null;
if (handleClose && !badge) {
closeButton = (
<span
className='sidebar-channel__close pull-right'
data-close='true'
>
{'×'}
</span>
);
}
return ( return (
<li <li
key={channel.name} key={channel.name}
@@ -446,6 +480,7 @@ export default class Sidebar extends React.Component {
{status} {status}
{channel.display_name} {channel.display_name}
{badge} {badge}
{closeButton}
</a> </a>
</li> </li>
); );
@@ -464,7 +499,9 @@ export default class Sidebar extends React.Component {
const privateChannels = this.state.channels.filter((channel) => channel.type === 'P'); const privateChannels = this.state.channels.filter((channel) => channel.type === 'P');
const privateChannelItems = privateChannels.map(this.createChannelElement); const privateChannelItems = privateChannels.map(this.createChannelElement);
const directMessageItems = this.state.showDirectChannels.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 // update the favicon to show if there are any notifications
var link = document.createElement('link'); var link = document.createElement('link');
@@ -484,17 +521,18 @@ export default class Sidebar extends React.Component {
head.appendChild(link); head.appendChild(link);
var directMessageMore = null; var directMessageMore = null;
if (this.state.hideDirectChannels.length > 0) { if (this.state.hiddenDirectChannels.length > 0) {
directMessageMore = ( directMessageMore = (
<li> <li key='more'>
<a <a
key={`more${this.state.hiddenDirectChannels.length}`}
href='#' href='#'
data-toggle='modal' data-toggle='modal'
className='nav-more' className='nav-more'
data-target='#more_direct_channels' data-target='#more_direct_channels'
data-channels={JSON.stringify(this.state.hideDirectChannels)} data-channels={JSON.stringify(this.state.hiddenDirectChannels)}
> >
{'More (' + this.state.hideDirectChannels.length + ')'} {'More (' + this.state.hiddenDirectChannels.length + ')'}
</a> </a>
</li> </li>
); );
@@ -538,7 +576,7 @@ export default class Sidebar extends React.Component {
<ul className='nav nav-pills nav-stacked'> <ul className='nav nav-pills nav-stacked'>
<li> <li>
<h4> <h4>
Channels {'Channels'}
<a <a
className='add-channel-btn' className='add-channel-btn'
href='#' href='#'
@@ -557,7 +595,7 @@ export default class Sidebar extends React.Component {
data-target='#more_channels' data-target='#more_channels'
data-channeltype='O' data-channeltype='O'
> >
More... {'More...'}
</a> </a>
</li> </li>
</ul> </ul>
@@ -565,7 +603,7 @@ export default class Sidebar extends React.Component {
<ul className='nav nav-pills nav-stacked'> <ul className='nav nav-pills nav-stacked'>
<li> <li>
<h4> <h4>
Private Groups {'Private Groups'}
<a <a
className='add-channel-btn' className='add-channel-btn'
href='#' href='#'
@@ -578,7 +616,7 @@ export default class Sidebar extends React.Component {
{privateChannelItems} {privateChannelItems}
</ul> </ul>
<ul className='nav nav-pills nav-stacked'> <ul className='nav nav-pills nav-stacked'>
<li><h4>Direct Messages</h4></li> <li><h4>{'Direct Messages'}</h4></li>
{directMessageItems} {directMessageItems}
{directMessageMore} {directMessageMore}
</ul> </ul>

122
web/react/stores/preference_store.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,122 @@
// 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';
function getPreferenceKey(category, name) {
return `${category}-${name}`;
}
function getPreferenceKeyForModel(preference) {
return `${preference.category}-${preference.name}`;
}
class PreferenceStoreClass extends EventEmitter {
constructor() {
super();
this.getAllPreferences = this.getAllPreferences.bind(this);
this.getPreference = this.getPreference.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.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);
}
getAllPreferences() {
return new Map(BrowserStore.getItem('preferences', []));
}
getPreference(category, name, defaultValue = '') {
return this.getAllPreferences().get(getPreferenceKey(category, name)) || defaultValue;
}
getPreferences(category) {
return this.getPreferencesWhere((preference) => (preference.category === category));
}
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) {
const preferences = this.getAllPreferences();
const key = getPreferenceKey(category, name);
let preference = preferences.get(key);
if (!preference) {
preference = {
user_id: UserStore.getCurrentId(),
category,
name
};
}
preference.value = value;
preferences.set(key, preference);
this.setAllPreferences(preferences);
return preference;
}
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(getPreferenceKeyForModel(preference), preference);
}
this.setAllPreferences(preferences);
this.emitChange(preferences);
}
}
}
const PreferenceStore = new PreferenceStoreClass();
export default PreferenceStore;

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

@@ -637,3 +637,55 @@ export function getMyTeam() {
} }
); );
} }
export function getDirectChannelPreferences() {
if (isCallInProgress('getDirectChannelPreferences')) {
return;
}
callTracker.getDirectChannelPreferences = utils.getTimestamp();
client.getPreferenceCategory(
Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
(data, textStatus, xhr) => {
callTracker.getDirectChannelPreferences = 0;
if (xhr.status === 304 || !data) {
return;
}
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_PREFERENCES,
preferences: data
});
},
(err) => {
callTracker.getDirectChannelPreferences = 0;
dispatchError(err, 'getDirectChannelPreferences');
}
);
}
export function savePreferences(preferences, success, error) {
client.savePreferences(
preferences,
(data, textStatus, xhr) => {
if (xhr.status !== 304) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_PREFERENCES,
preferences
});
}
if (success) {
success(data);
}
},
(err) => {
dispatchError(err, 'savePreferences');
if (error) {
error();
}
}
);
}

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

@@ -1141,3 +1141,31 @@ export function listIncomingHooks(success, error) {
} }
}); });
} }
export function getPreferenceCategory(category, success, error) {
$.ajax({
url: `/api/v1/preferences/${category}`,
dataType: 'json',
type: 'GET',
success,
error: (xhr, status, err) => {
var e = handleError('getPreferenceCategory', xhr, status, err);
error(e);
}
});
}
export function savePreferences(preferences, success, error) {
$.ajax({
url: '/api/v1/preferences/save',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(preferences),
success,
error: (xhr, status, err) => {
var e = handleError('savePreferences', xhr, status, err);
error(e);
}
});
}

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

@@ -28,6 +28,7 @@ module.exports = {
RECIEVED_AUDITS: null, RECIEVED_AUDITS: null,
RECIEVED_TEAMS: null, RECIEVED_TEAMS: null,
RECIEVED_STATUSES: null, RECIEVED_STATUSES: null,
RECIEVED_PREFERENCES: null,
RECIEVED_MSG: null, RECIEVED_MSG: null,
@@ -285,5 +286,8 @@ module.exports = {
id: 'mentionHighlightLink', id: 'mentionHighlightLink',
uiName: 'Mention Highlight Link' uiName: 'Mention Highlight Link'
} }
] ],
Preferences: {
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show'
}
}; };