summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/preference.go28
-rw-r--r--api/preference_test.go46
-rw-r--r--i18n/en.json24
-rw-r--r--model/client.go10
-rw-r--r--model/preference.go44
-rw-r--r--model/preference_test.go39
-rw-r--r--model/user.go21
-rw-r--r--model/user_test.go13
-rw-r--r--store/sql_preference_store.go27
-rw-r--r--store/sql_preference_store_test.go27
-rw-r--r--store/sql_user_store.go70
-rw-r--r--store/store.go1
-rw-r--r--webapp/actions/user_actions.jsx56
-rw-r--r--webapp/components/logged_in.jsx9
-rw-r--r--webapp/components/needs_team.jsx37
-rw-r--r--webapp/components/setting_item_max.jsx4
-rw-r--r--webapp/components/user_settings/import_theme_modal.jsx91
-rw-r--r--webapp/components/user_settings/user_settings_theme.jsx122
-rw-r--r--webapp/dispatcher/app_dispatcher.jsx4
-rw-r--r--webapp/sass/routes/_settings.scss5
-rw-r--r--webapp/stores/preference_store.jsx32
-rw-r--r--webapp/utils/async_client.jsx23
-rw-r--r--webapp/utils/constants.jsx235
23 files changed, 683 insertions, 285 deletions
diff --git a/api/preference.go b/api/preference.go
index d9ddb1a21..240ead571 100644
--- a/api/preference.go
+++ b/api/preference.go
@@ -16,6 +16,7 @@ func InitPreference() {
BaseRoutes.Preferences.Handle("/", ApiUserRequired(getAllPreferences)).Methods("GET")
BaseRoutes.Preferences.Handle("/save", ApiUserRequired(savePreferences)).Methods("POST")
+ BaseRoutes.Preferences.Handle("/delete", ApiUserRequired(deletePreferences)).Methods("POST")
BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}", ApiUserRequired(getPreferenceCategory)).Methods("GET")
BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}/{name:[A-Za-z0-9_]+}", ApiUserRequired(getPreference)).Methods("GET")
}
@@ -81,3 +82,30 @@ func getPreference(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(data.ToJson()))
}
}
+
+func deletePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
+ preferences, err := model.PreferencesFromJson(r.Body)
+ if err != nil {
+ c.Err = model.NewLocAppError("savePreferences", "api.preference.delete_preferences.decode.app_error", nil, err.Error())
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ for _, preference := range preferences {
+ if c.Session.UserId != preference.UserId {
+ c.Err = model.NewLocAppError("deletePreferences", "api.preference.delete_preferences.user_id.app_error",
+ nil, "session.user_id="+c.Session.UserId+",preference.user_id="+preference.UserId)
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+ }
+
+ for _, preference := range preferences {
+ if result := <-Srv.Store.Preference().Delete(c.Session.UserId, preference.Category, preference.Name); result.Err != nil {
+ c.Err = result.Err
+ return
+ }
+ }
+
+ ReturnStatusOK(w)
+}
diff --git a/api/preference_test.go b/api/preference_test.go
index 082f02527..3e41c884f 100644
--- a/api/preference_test.go
+++ b/api/preference_test.go
@@ -161,3 +161,49 @@ func TestGetPreference(t *testing.T) {
t.Fatal("preference updated incorrectly")
}
}
+
+func TestDeletePreferences(t *testing.T) {
+ th := Setup().InitBasic()
+ Client := th.BasicClient
+ user1 := th.BasicUser
+
+ var originalCount int
+ if result, err := Client.GetAllPreferences(); err != nil {
+ t.Fatal(err)
+ } else {
+ originalCount = len(result.Data.(model.Preferences))
+ }
+
+ // 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)
+ }
+
+ // delete 10 preferences
+ th.LoginBasic2()
+
+ if _, err := Client.DeletePreferences(&preferences); err == nil {
+ t.Fatal("shouldn't have been able to delete another user's preferences")
+ }
+
+ th.LoginBasic()
+ if _, err := Client.DeletePreferences(&preferences); err != nil {
+ t.Fatal(err)
+ }
+
+ if result, err := Client.GetAllPreferences(); err != nil {
+ t.Fatal(err)
+ } else if data := result.Data.(model.Preferences); len(data) != originalCount {
+ t.Fatal("should've deleted preferences")
+ }
+}
diff --git a/i18n/en.json b/i18n/en.json
index 961ddc50c..ab6dd4f87 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1140,6 +1140,14 @@
"translation": "Unable to get post"
},
{
+ "id": "api.preference.delete_preferences.decode.app_error",
+ "translation": "Unable to decode preferences from request"
+ },
+ {
+ "id": "api.preference.delete_preferences.user_id.app_error",
+ "translation": "Unable to delete preferences for other user"
+ },
+ {
"id": "api.preference.init.debug",
"translation": "Initializing preference api routes"
},
@@ -3012,6 +3020,10 @@
"translation": "Invalid name"
},
{
+ "id": "model.preference.is_valid.theme.app_error",
+ "translation": "Invalid theme"
+ },
+ {
"id": "model.preference.is_valid.value.app_error",
"translation": "Value is too long"
},
@@ -3176,10 +3188,6 @@
"translation": "Invalid team id"
},
{
- "id": "model.user.is_valid.theme.app_error",
- "translation": "Invalid theme"
- },
- {
"id": "model.user.is_valid.update_at.app_error",
"translation": "Update at must be a valid time"
},
@@ -3776,6 +3784,10 @@
"translation": "We couldn't update the Post"
},
{
+ "id": "store.sql_preference.delete.app_error",
+ "translation": "We encountered an error while deleting preferences"
+ },
+ {
"id": "store.sql_preference.delete_unused_features.debug",
"translation": "Deleting any unused pre-release features"
},
@@ -4056,6 +4068,10 @@
"translation": "We could not get the unread message count for the user"
},
{
+ "id": "store.sql_user.migrate_theme.critical",
+ "translation": "Failed to migrate User.ThemeProps to Preferences table %v"
+ },
+ {
"id": "store.sql_user.missing_account.const",
"translation": "We couldn't find an existing account matching your email address for this team. This team may require an invite from the team owner to join."
},
diff --git a/model/client.go b/model/client.go
index 5ccf2c63c..e12cd595d 100644
--- a/model/client.go
+++ b/model/client.go
@@ -1556,6 +1556,16 @@ func (c *Client) GetPreferenceCategory(category string) (*Result, *AppError) {
}
}
+// DeletePreferences deletes a list of preferences owned by the current user. If successful,
+// it will return status=ok. Otherwise, an error will be returned.
+func (c *Client) DeletePreferences(preferences *Preferences) (bool, *AppError) {
+ if r, err := c.DoApiPost("/preferences/delete", preferences.ToJson()); err != nil {
+ return false, err
+ } else {
+ return c.CheckStatusOK(r), nil
+ }
+}
+
func (c *Client) CreateOutgoingWebhook(hook *OutgoingWebhook) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/outgoing/create", hook.ToJson()); err != nil {
return nil, err
diff --git a/model/preference.go b/model/preference.go
index 22858e043..779c41e50 100644
--- a/model/preference.go
+++ b/model/preference.go
@@ -6,6 +6,8 @@ package model
import (
"encoding/json"
"io"
+ "regexp"
+ "strings"
"unicode/utf8"
)
@@ -17,6 +19,9 @@ const (
PREFERENCE_CATEGORY_DISPLAY_SETTINGS = "display_settings"
PREFERENCE_NAME_COLLAPSE_SETTING = "collapse_previews"
+ PREFERENCE_CATEGORY_THEME = "theme"
+ // the name for theme props is the team id
+
PREFERENCE_CATEGORY_LAST = "last"
PREFERENCE_NAME_LAST_CHANNEL = "channel"
)
@@ -57,13 +62,48 @@ func (o *Preference) IsValid() *AppError {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.category.app_error", nil, "category="+o.Category)
}
- if len(o.Name) == 0 || len(o.Name) > 32 {
+ if len(o.Name) > 32 {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.name.app_error", nil, "name="+o.Name)
}
- if utf8.RuneCountInString(o.Value) > 128 {
+ if utf8.RuneCountInString(o.Value) > 2000 {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.value.app_error", nil, "value="+o.Value)
}
+ if o.Category == PREFERENCE_CATEGORY_THEME {
+ var unused map[string]string
+ if err := json.NewDecoder(strings.NewReader(o.Value)).Decode(&unused); err != nil {
+ return NewLocAppError("Preference.IsValid", "model.preference.is_valid.theme.app_error", nil, "value="+o.Value)
+ }
+ }
+
return nil
}
+
+func (o *Preference) PreUpdate() {
+ if o.Category == PREFERENCE_CATEGORY_THEME {
+ // decode the value of theme (a map of strings to string) and eliminate any invalid values
+ var props map[string]string
+ if err := json.NewDecoder(strings.NewReader(o.Value)).Decode(&props); err != nil {
+ // just continue, the invalid preference value should get caught by IsValid before saving
+ return
+ }
+
+ colorPattern := regexp.MustCompile(`^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$`)
+
+ // blank out any invalid theme values
+ for name, value := range props {
+ if name == "image" || name == "type" || name == "codeTheme" {
+ continue
+ }
+
+ if !colorPattern.MatchString(value) {
+ props[name] = "#ffffff"
+ }
+ }
+
+ if b, err := json.Marshal(props); err == nil {
+ o.Value = string(b)
+ }
+ }
+}
diff --git a/model/preference_test.go b/model/preference_test.go
index e29250bba..df7fe612d 100644
--- a/model/preference_test.go
+++ b/model/preference_test.go
@@ -4,6 +4,7 @@
package model
import (
+ "encoding/json"
"strings"
"testing"
)
@@ -31,7 +32,7 @@ func TestPreferenceIsValid(t *testing.T) {
preference.Category = PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW
if err := preference.IsValid(); err != nil {
- t.Fatal()
+ t.Fatal(err)
}
preference.Name = strings.Repeat("01234567890", 20)
@@ -41,16 +42,48 @@ func TestPreferenceIsValid(t *testing.T) {
preference.Name = NewId()
if err := preference.IsValid(); err != nil {
- t.Fatal()
+ t.Fatal(err)
}
- preference.Value = strings.Repeat("01234567890", 20)
+ preference.Value = strings.Repeat("01234567890", 201)
if err := preference.IsValid(); err == nil {
t.Fatal()
}
preference.Value = "1234garbage"
if err := preference.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+
+ preference.Category = PREFERENCE_CATEGORY_THEME
+ if err := preference.IsValid(); err == nil {
t.Fatal()
}
+
+ preference.Value = `{"color": "#ff0000", "color2": "#faf"}`
+ if err := preference.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPreferencePreUpdate(t *testing.T) {
+ preference := Preference{
+ Category: PREFERENCE_CATEGORY_THEME,
+ Value: `{"color": "#ff0000", "color2": "#faf", "codeTheme": "github", "invalid": "invalid"}`,
+ }
+
+ preference.PreUpdate()
+
+ var props map[string]string
+ if err := json.NewDecoder(strings.NewReader(preference.Value)).Decode(&props); err != nil {
+ t.Fatal(err)
+ }
+
+ if props["color"] != "#ff0000" || props["color2"] != "#faf" || props["codeTheme"] != "github" {
+ t.Fatal("shouldn't have changed valid props")
+ }
+
+ if props["invalid"] == "invalid" {
+ t.Fatal("should have changed invalid prop")
+ }
}
diff --git a/model/user.go b/model/user.go
index c792f80d1..4444352d3 100644
--- a/model/user.go
+++ b/model/user.go
@@ -49,7 +49,6 @@ type User struct {
AllowMarketing bool `json:"allow_marketing,omitempty"`
Props StringMap `json:"props,omitempty"`
NotifyProps StringMap `json:"notify_props,omitempty"`
- ThemeProps StringMap `json:"theme_props,omitempty"`
LastPasswordUpdate int64 `json:"last_password_update,omitempty"`
LastPictureUpdate int64 `json:"last_picture_update,omitempty"`
FailedAttempts int `json:"failed_attempts,omitempty"`
@@ -106,10 +105,6 @@ func (u *User) IsValid() *AppError {
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_pwd.app_error", nil, "user_id="+u.Id)
}
- if len(u.ThemeProps) > 2000 {
- return NewLocAppError("User.IsValid", "model.user.is_valid.theme.app_error", nil, "user_id="+u.Id)
- }
-
return nil
}
@@ -179,21 +174,6 @@ func (u *User) PreUpdate() {
}
u.NotifyProps["mention_keys"] = strings.Join(goodKeys, ",")
}
-
- if u.ThemeProps != nil {
- colorPattern := regexp.MustCompile(`^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$`)
-
- // blank out any invalid theme values
- for name, value := range u.ThemeProps {
- if name == "image" || name == "type" || name == "codeTheme" {
- continue
- }
-
- if !colorPattern.MatchString(value) {
- u.ThemeProps[name] = "#ffffff"
- }
- }
- }
}
func (u *User) SetDefaultNotifications() {
@@ -282,7 +262,6 @@ func (u *User) ClearNonProfileFields() {
u.AllowMarketing = false
u.Props = StringMap{}
u.NotifyProps = StringMap{}
- u.ThemeProps = StringMap{}
u.LastPasswordUpdate = 0
u.LastPictureUpdate = 0
u.FailedAttempts = 0
diff --git a/model/user_test.go b/model/user_test.go
index 899542a05..16ac2583b 100644
--- a/model/user_test.go
+++ b/model/user_test.go
@@ -39,19 +39,6 @@ func TestUserPreSave(t *testing.T) {
func TestUserPreUpdate(t *testing.T) {
user := User{Password: "test"}
user.PreUpdate()
-
- user.ThemeProps = StringMap{
- "codeTheme": "github",
- "awayIndicator": "#cdbd4e",
- "buttonColor": "invalid",
- }
- user.PreUpdate()
-
- if user.ThemeProps["codeTheme"] != "github" || user.ThemeProps["awayIndicator"] != "#cdbd4e" {
- t.Fatal("shouldn't have changed valid theme props")
- } else if user.ThemeProps["buttonColor"] != "#ffffff" {
- t.Fatal("should've changed invalid theme prop")
- }
}
func TestUserUpdateMentionKeysFromUsername(t *testing.T) {
diff --git a/store/sql_preference_store.go b/store/sql_preference_store.go
index 83bf92ead..a701c3cb8 100644
--- a/store/sql_preference_store.go
+++ b/store/sql_preference_store.go
@@ -26,7 +26,7 @@ func NewSqlPreferenceStore(sqlStore *SqlStore) PreferenceStore {
table.ColMap("UserId").SetMaxSize(26)
table.ColMap("Category").SetMaxSize(32)
table.ColMap("Name").SetMaxSize(32)
- table.ColMap("Value").SetMaxSize(128)
+ table.ColMap("Value").SetMaxSize(2000)
}
return s
@@ -100,6 +100,8 @@ func (s SqlPreferenceStore) Save(preferences *model.Preferences) StoreChannel {
func (s SqlPreferenceStore) save(transaction *gorp.Transaction, preference *model.Preference) StoreResult {
result := StoreResult{}
+ preference.PreUpdate()
+
if result.Err = preference.IsValid(); result.Err != nil {
return result
}
@@ -304,3 +306,26 @@ func (s SqlPreferenceStore) IsFeatureEnabled(feature, userId string) StoreChanne
return storeChannel
}
+
+func (s SqlPreferenceStore) Delete(userId, category, name string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := s.GetMaster().Exec(
+ `DELETE 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.NewLocAppError("SqlPreferenceStore.Delete", "store.sql_preference.delete.app_error", nil, err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_preference_store_test.go b/store/sql_preference_store_test.go
index ec9d1df6c..8c6a2b6af 100644
--- a/store/sql_preference_store_test.go
+++ b/store/sql_preference_store_test.go
@@ -193,7 +193,7 @@ func TestPreferenceGetAll(t *testing.T) {
}
}
-func TestPreferenceDelete(t *testing.T) {
+func TestPreferenceDeleteByUser(t *testing.T) {
Setup()
userId := model.NewId()
@@ -367,3 +367,28 @@ func TestDeleteUnusedFeatures(t *testing.T) {
t.Fatalf("Found %d features with value 'true', expected to find at least %d features", val, 2)
}
}
+
+func TestPreferenceDelete(t *testing.T) {
+ Setup()
+
+ preference := model.Preference{
+ UserId: model.NewId(),
+ Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
+ Name: model.NewId(),
+ Value: "value1a",
+ }
+
+ Must(store.Preference().Save(&model.Preferences{preference}))
+
+ if prefs := Must(store.Preference().GetAll(preference.UserId)).(model.Preferences); len([]model.Preference(prefs)) != 1 {
+ t.Fatal("should've returned 1 preference")
+ }
+
+ if result := <-store.Preference().Delete(preference.UserId, preference.Category, preference.Name); result.Err != nil {
+ t.Fatal(result.Err)
+ }
+
+ if prefs := Must(store.Preference().GetAll(preference.UserId)).(model.Preferences); len([]model.Preference(prefs)) != 0 {
+ t.Fatal("should've returned no preferences")
+ }
+}
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index d4b65d04d..867445aac 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -9,7 +9,9 @@ import (
"fmt"
"strconv"
"strings"
+ "time"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
@@ -40,7 +42,6 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
table.ColMap("Roles").SetMaxSize(64)
table.ColMap("Props").SetMaxSize(4000)
table.ColMap("NotifyProps").SetMaxSize(2000)
- table.ColMap("ThemeProps").SetMaxSize(2000)
table.ColMap("Locale").SetMaxSize(5)
table.ColMap("MfaSecret").SetMaxSize(128)
}
@@ -53,27 +54,66 @@ func (us SqlUserStore) UpgradeSchemaIfNeeded() {
us.CreateColumnIfNotExists("Users", "Locale", "varchar(5)", "character varying(5)", model.DEFAULT_LOCALE)
// ADDED for 3.2 REMOVE for 3.6
- var data []*model.User
- if _, err := us.GetReplica().Select(&data, "SELECT * FROM Users WHERE ThemeProps LIKE '%solarized%'"); err == nil {
- for _, user := range data {
- shouldUpdate := false
- if user.ThemeProps["codeTheme"] == "solarized_dark" {
- user.ThemeProps["codeTheme"] = "solarized-dark"
- shouldUpdate = true
- } else if user.ThemeProps["codeTheme"] == "solarized_light" {
- user.ThemeProps["codeTheme"] = "solarized-light"
- shouldUpdate = true
+ if us.DoesColumnExist("Users", "ThemeProps") {
+ params := map[string]interface{}{
+ "Category": model.PREFERENCE_CATEGORY_THEME,
+ "Name": "",
+ }
+
+ transaction, err := us.GetMaster().Begin()
+ if err != nil {
+ themeMigrationFailed(err)
+ }
+
+ // increase size of Value column of Preferences table to match the size of the ThemeProps column
+ if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
+ if _, err := transaction.Exec("ALTER TABLE Preferences ALTER COLUMN Value TYPE varchar(2000)"); err != nil {
+ themeMigrationFailed(err)
}
+ } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
+ if _, err := transaction.Exec("ALTER TABLE Preferences MODIFY Value text"); err != nil {
+ themeMigrationFailed(err)
+ }
+ }
- if shouldUpdate {
- if result := <-us.Update(user, true); result.Err != nil {
- return
- }
+ // copy data across
+ if _, err := transaction.Exec(
+ `INSERT INTO
+ Preferences(UserId, Category, Name, Value)
+ SELECT
+ Id, '`+model.PREFERENCE_CATEGORY_THEME+`', '', ThemeProps
+ FROM
+ Users`, params); err != nil {
+ themeMigrationFailed(err)
+ }
+
+ // delete old data
+ if _, err := transaction.Exec("ALTER TABLE Users DROP COLUMN ThemeProps"); err != nil {
+ themeMigrationFailed(err)
+ }
+
+ if err := transaction.Commit(); err != nil {
+ themeMigrationFailed(err)
+ }
+
+ // rename solarized_* code themes to solarized-* to match client changes in 3.0
+ var data model.Preferences
+ if _, err := us.GetReplica().Select(&data, "SELECT * FROM Preferences WHERE Category = '"+model.PREFERENCE_CATEGORY_THEME+"' AND Value LIKE '%solarized_%'"); err == nil {
+ for i := range data {
+ data[i].Value = strings.Replace(data[i].Value, "solarized_", "solarized-", -1)
}
+
+ us.Preference().Save(&data)
}
}
}
+func themeMigrationFailed(err error) {
+ l4g.Critical(utils.T("store.sql_user.migrate_theme.critical"), err)
+ time.Sleep(time.Second)
+ panic(fmt.Sprintf(utils.T("store.sql_user.migrate_theme.critical"), err.Error()))
+}
+
func (us SqlUserStore) CreateIndexesIfNotExists() {
us.CreateIndexIfNotExists("idx_users_email", "Users", "Email")
}
diff --git a/store/store.go b/store/store.go
index 445de440a..0c19fd5b6 100644
--- a/store/store.go
+++ b/store/store.go
@@ -243,6 +243,7 @@ type PreferenceStore interface {
Get(userId string, category string, name string) StoreChannel
GetCategory(userId string, category string) StoreChannel
GetAll(userId string) StoreChannel
+ Delete(userId, category, name string) StoreChannel
PermanentDeleteByUser(userId string) StoreChannel
IsFeatureEnabled(feature, userId string) StoreChannel
}
diff --git a/webapp/actions/user_actions.jsx b/webapp/actions/user_actions.jsx
index 2f6eb9942..6d14e9fba 100644
--- a/webapp/actions/user_actions.jsx
+++ b/webapp/actions/user_actions.jsx
@@ -1,10 +1,15 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import Client from 'utils/web_client.jsx';
+import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
+import Client from 'utils/web_client.jsx';
+import PreferenceStore from 'stores/preference_store.jsx';
import TeamStore from 'stores/team_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+
+import {ActionTypes, Preferences} from 'utils/constants.jsx';
export function switchFromLdapToEmail(email, password, ldapPassword, onSuccess, onError) {
Client.ldapToEmail(
@@ -28,3 +33,52 @@ export function getMoreDmList() {
AsyncClient.getProfilesForDirectMessageList();
AsyncClient.getTeamMembers(TeamStore.getCurrentId());
}
+
+export function saveTheme(teamId, theme, onSuccess, onError) {
+ AsyncClient.savePreference(
+ Preferences.CATEGORY_THEME,
+ teamId,
+ JSON.stringify(theme),
+ () => {
+ onThemeSaved(teamId, theme, onSuccess);
+ },
+ (err) => {
+ onError(err);
+ }
+ );
+}
+
+function onThemeSaved(teamId, theme, onSuccess) {
+ const themePreferences = PreferenceStore.getCategory(Preferences.CATEGORY_THEME);
+
+ if (teamId !== '' && themePreferences.size > 1) {
+ // no extra handling to be done to delete team-specific themes
+ onSuccess();
+ return;
+ }
+
+ const toDelete = [];
+
+ for (const [name] of themePreferences) {
+ if (name === '') {
+ continue;
+ }
+
+ toDelete.push({
+ user_id: UserStore.getCurrentId(),
+ category: Preferences.CATEGORY_THEME,
+ name
+ });
+ }
+
+ // we're saving a new global theme so delete any team-specific ones
+ AsyncClient.deletePreferences(toDelete);
+
+ // delete them locally before we hear from the server so that the UI flow is smoother
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.DELETED_PREFERENCES,
+ preferences: toDelete
+ });
+
+ onSuccess();
+} \ No newline at end of file
diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx
index f637e9dc5..2ac858dfb 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -92,15 +92,6 @@ export default class LoggedIn extends React.Component {
id: user.id
});
}
-
- // Update CSS classes to match user theme
- if (user) {
- if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
- Utils.applyTheme(user.theme_props);
- } else {
- Utils.applyTheme(Constants.THEMES.default);
- }
- }
}
onUserChanged() {
diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx
index 07b90636d..a8c7b3508 100644
--- a/webapp/components/needs_team.jsx
+++ b/webapp/components/needs_team.jsx
@@ -41,19 +41,34 @@ export default class NeedsTeam extends React.Component {
constructor(params) {
super(params);
- this.onChanged = this.onChanged.bind(this);
+ this.onTeamChanged = this.onTeamChanged.bind(this);
+ this.onPreferencesChanged = this.onPreferencesChanged.bind(this);
+
+ const team = TeamStore.getCurrent();
this.state = {
- team: TeamStore.getCurrent()
+ team,
+ theme: PreferenceStore.getTheme(team.id)
};
}
- onChanged() {
+ onTeamChanged() {
+ const team = TeamStore.getCurrent();
+
this.setState({
- team: TeamStore.getCurrent()
+ team,
+ theme: PreferenceStore.getTheme(team.id)
});
}
+ onPreferencesChanged(category) {
+ if (!category || category === Preferences.CATEGORY_THEME) {
+ this.setState({
+ theme: PreferenceStore.getTheme(this.state.team.id)
+ });
+ }
+ }
+
componentWillMount() {
// Go to tutorial if we are first arriving
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
@@ -63,7 +78,8 @@ export default class NeedsTeam extends React.Component {
}
componentDidMount() {
- TeamStore.addChangeListener(this.onChanged);
+ TeamStore.addChangeListener(this.onTeamChanged);
+ PreferenceStore.addChangeListener(this.onPreferencesChanged);
// Emit view action
GlobalActions.viewLoggedIn();
@@ -80,10 +96,19 @@ export default class NeedsTeam extends React.Component {
$(window).on('blur', () => {
window.isActive = false;
});
+
+ Utils.applyTheme(this.state.theme);
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (!Utils.areObjectsEqual(prevState.theme, this.state.theme)) {
+ Utils.applyTheme(this.state.theme);
+ }
}
componentWillUnmount() {
- TeamStore.removeChangeListener(this.onChanged);
+ TeamStore.removeChangeListener(this.onTeamChanged);
+ PreferenceStore.removeChangeListener(this.onPreferencesChanged);
$(window).off('focus');
$(window).off('blur');
}
diff --git a/webapp/components/setting_item_max.jsx b/webapp/components/setting_item_max.jsx
index ec496a765..ad765a7d6 100644
--- a/webapp/components/setting_item_max.jsx
+++ b/webapp/components/setting_item_max.jsx
@@ -84,6 +84,7 @@ export default class SettingItemMax extends React.Component {
</li>
<li className='setting-list-item'>
<hr/>
+ {this.props.submitExtra}
{serverError}
{clientError}
{submit}
@@ -113,5 +114,6 @@ SettingItemMax.propTypes = {
updateSection: React.PropTypes.func,
submit: React.PropTypes.func,
title: React.PropTypes.node,
- width: React.PropTypes.string
+ width: React.PropTypes.string,
+ submitExtra: React.PropTypes.node
};
diff --git a/webapp/components/user_settings/import_theme_modal.jsx b/webapp/components/user_settings/import_theme_modal.jsx
index 552659c4c..32c6837e8 100644
--- a/webapp/components/user_settings/import_theme_modal.jsx
+++ b/webapp/components/user_settings/import_theme_modal.jsx
@@ -1,30 +1,18 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import ReactDOM from 'react-dom';
import ModalStore from 'stores/modal_store.jsx';
-import UserStore from 'stores/user_store.jsx';
-import * as Utils from 'utils/utils.jsx';
-import Client from 'utils/web_client.jsx';
import {Modal} from 'react-bootstrap';
-import AppDispatcher from '../../dispatcher/app_dispatcher.jsx';
import Constants from 'utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
-
-const holders = defineMessages({
- submitError: {
- id: 'user.settings.import_theme.submitError',
- defaultMessage: 'Invalid format, please try copying and pasting in again.'
- }
-});
+import {FormattedMessage} from 'react-intl';
const ActionTypes = Constants.ActionTypes;
import React from 'react';
-class ImportThemeModal extends React.Component {
+export default class ImportThemeModal extends React.Component {
constructor(props) {
super(props);
@@ -33,26 +21,42 @@ class ImportThemeModal extends React.Component {
this.handleChange = this.handleChange.bind(this);
this.state = {
+ value: '',
inputError: '',
- show: false
+ show: false,
+ callback: null
};
}
+
componentDidMount() {
ModalStore.addModalListener(ActionTypes.TOGGLE_IMPORT_THEME_MODAL, this.updateShow);
}
+
componentWillUnmount() {
ModalStore.removeModalListener(ActionTypes.TOGGLE_IMPORT_THEME_MODAL, this.updateShow);
}
- updateShow(show) {
- this.setState({show});
+
+ updateShow(show, args) {
+ this.setState({
+ show,
+ callback: args.callback
+ });
}
+
handleSubmit(e) {
e.preventDefault();
- const text = ReactDOM.findDOMNode(this.refs.input).value;
+ const text = this.state.value;
if (!this.isInputValid(text)) {
- this.setState({inputError: this.props.intl.formatMessage(holders.submitError)});
+ this.setState({
+ inputError: (
+ <FormattedMessage
+ id='user.settings.import_theme.submitError'
+ defaultMessage='Invalid format, please try copying and pasting in again.'
+ />
+ )
+ });
return;
}
@@ -81,26 +85,13 @@ class ImportThemeModal extends React.Component {
theme.mentionHighlightLink = '#2f81b7';
theme.codeTheme = 'github';
- const user = UserStore.getCurrentUser();
- user.theme_props = theme;
-
- Client.updateUser(user, Constants.UserUpdateEvents.THEME,
- (data) => {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_ME,
- me: data
- });
-
- this.setState({show: false});
- Utils.applyTheme(theme);
- },
- (err) => {
- var state = this.getStateFromStores();
- state.serverError = err;
- this.setState(state);
- }
- );
+ this.state.callback(theme);
+ this.setState({
+ show: false,
+ callback: null
+ });
}
+
isInputValid(text) {
if (text.length === 0) {
return false;
@@ -134,13 +125,25 @@ class ImportThemeModal extends React.Component {
return true;
}
+
handleChange(e) {
- if (this.isInputValid(e.target.value)) {
+ const value = e.target.value;
+ this.setState({value});
+
+ if (this.isInputValid(value)) {
this.setState({inputError: null});
} else {
- this.setState({inputError: this.props.intl.formatMessage(holders.submitError)});
+ this.setState({
+ inputError: (
+ <FormattedMessage
+ id='user.settings.import_theme.submitError'
+ defaultMessage='Invalid format, please try copying and pasting in again.'
+ />
+ )
+ });
}
}
+
render() {
return (
<span>
@@ -170,9 +173,9 @@ class ImportThemeModal extends React.Component {
<div className='form-group less'>
<div className='col-sm-9'>
<input
- ref='input'
type='text'
className='form-control'
+ value={this.state.value}
onChange={this.handleChange}
/>
<div className='input__help'>
@@ -210,9 +213,3 @@ class ImportThemeModal extends React.Component {
);
}
}
-
-ImportThemeModal.propTypes = {
- intl: intlShape.isRequired
-};
-
-export default injectIntl(ImportThemeModal);
diff --git a/webapp/components/user_settings/user_settings_theme.jsx b/webapp/components/user_settings/user_settings_theme.jsx
index 94516ec8c..4ff08402a 100644
--- a/webapp/components/user_settings/user_settings_theme.jsx
+++ b/webapp/components/user_settings/user_settings_theme.jsx
@@ -8,28 +8,18 @@ import PremadeThemeChooser from './premade_theme_chooser.jsx';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
+import PreferenceStore from 'stores/preference_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import AppDispatcher from '../../dispatcher/app_dispatcher.jsx';
-import Client from 'utils/web_client.jsx';
-import * as Utils from 'utils/utils.jsx';
+import * as UserActions from 'actions/user_actions.jsx';
-import Constants from 'utils/constants.jsx';
+import * as Utils from 'utils/utils.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
+import {FormattedMessage} from 'react-intl';
-const ActionTypes = Constants.ActionTypes;
-
-const holders = defineMessages({
- themeTitle: {
- id: 'user.settings.display.theme.title',
- defaultMessage: 'Theme'
- },
- themeDescribe: {
- id: 'user.settings.display.theme.describe',
- defaultMessage: 'Open to manage your theme'
- }
-});
+import {ActionTypes, Constants, Preferences} from 'utils/constants.jsx';
import React from 'react';
@@ -47,6 +37,7 @@ export default class ThemeSetting extends React.Component {
this.originalTheme = Object.assign({}, this.state.theme);
}
+
componentDidMount() {
UserStore.addChangeListener(this.onChange);
@@ -54,17 +45,20 @@ export default class ThemeSetting extends React.Component {
$(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
}
}
+
componentDidUpdate() {
if (this.props.selected) {
$('.color-btn').removeClass('active-border');
$(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
}
}
+
componentWillReceiveProps(nextProps) {
if (this.props.selected && !nextProps.selected) {
this.resetFields();
}
}
+
componentWillUnmount() {
UserStore.removeChangeListener(this.onChange);
@@ -73,27 +67,35 @@ export default class ThemeSetting extends React.Component {
Utils.applyTheme(state.theme);
}
}
+
getStateFromStores() {
- const user = UserStore.getCurrentUser();
- let theme = null;
+ const teamId = TeamStore.getCurrentId();
- if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
- theme = Object.assign({}, user.theme_props);
- } else {
- theme = $.extend(true, {}, Constants.THEMES.default);
+ const theme = PreferenceStore.getTheme(teamId);
+ if (!theme.codeTheme) {
+ theme.codeTheme = Constants.DEFAULT_CODE_THEME;
}
- let type = 'premade';
- if (theme.type === 'custom') {
- type = 'custom';
- }
+ let showAllTeamsCheckbox = false;
+ let applyToAllTeams = true;
- if (!theme.codeTheme) {
- theme.codeTheme = Constants.DEFAULT_CODE_THEME;
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true') {
+ // show the "apply to all teams" checkbox if the user is on more than one team
+ showAllTeamsCheckbox = Object.keys(TeamStore.getAll()).length > 1;
+
+ // check the "apply to all teams" checkbox by default if the user has any team-specific themes
+ applyToAllTeams = PreferenceStore.getCategory(Preferences.CATEGORY_THEME).size <= 1;
}
- return {theme, type};
+ return {
+ teamId: TeamStore.getCurrentId(),
+ theme,
+ type: theme.type || 'premade',
+ showAllTeamsCheckbox,
+ applyToAllTeams
+ };
}
+
onChange() {
const newState = this.getStateFromStores();
@@ -103,21 +105,20 @@ export default class ThemeSetting extends React.Component {
this.props.setEnforceFocus(true);
}
+
scrollToTop() {
$('.ps-container.modal-body').scrollTop(0);
}
+
submitTheme(e) {
e.preventDefault();
- var user = UserStore.getCurrentUser();
- user.theme_props = this.state.theme;
- Client.updateUser(user, Constants.UserUpdateEvents.THEME,
- (data) => {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_ME,
- me: data
- });
+ const teamId = this.state.applyToAllTeams ? '' : this.state.teamId;
+ UserActions.saveTheme(
+ teamId,
+ this.state.theme,
+ () => {
this.props.setRequireConfirm(false);
this.originalTheme = Object.assign({}, this.state.theme);
this.scrollToTop();
@@ -130,6 +131,7 @@ export default class ThemeSetting extends React.Component {
}
);
}
+
updateTheme(theme) {
let themeChanged = this.state.theme.length === theme.length;
if (!themeChanged) {
@@ -148,9 +150,11 @@ export default class ThemeSetting extends React.Component {
this.setState({theme});
Utils.applyTheme(theme);
}
+
updateType(type) {
this.setState({type});
}
+
resetFields() {
const state = this.getStateFromStores();
state.serverError = null;
@@ -161,17 +165,18 @@ export default class ThemeSetting extends React.Component {
this.props.setRequireConfirm(false);
}
+
handleImportModal() {
AppDispatcher.handleViewAction({
type: ActionTypes.TOGGLE_IMPORT_THEME_MODAL,
- value: true
+ value: true,
+ callback: this.updateTheme
});
this.props.setEnforceFocus(false);
}
- render() {
- const {formatMessage} = this.props.intl;
+ render() {
var serverError;
if (this.state.serverError) {
serverError = this.state.serverError;
@@ -266,9 +271,29 @@ export default class ThemeSetting extends React.Component {
</div>
);
+ let allTeamsCheckbox = null;
+ if (this.state.showAllTeamsCheckbox) {
+ allTeamsCheckbox = (
+ <div className='checkbox user-settings__submit-checkbox'>
+ <label>
+ <input
+ type='checkbox'
+ checked={this.state.applyToAllTeams}
+ onChange={(e) => this.setState({applyToAllTeams: e.target.checked})}
+ />
+ <FormattedMessage
+ id='user.settings.display.theme.applyToAllTeams'
+ defaultMessage='Apply New Theme to All Teams'
+ />
+ </label>
+ </div>
+ );
+ }
+
themeUI = (
<SettingItemMax
inputs={inputs}
+ submitExtra={allTeamsCheckbox}
submit={this.submitTheme}
server_error={serverError}
width='full'
@@ -281,8 +306,18 @@ export default class ThemeSetting extends React.Component {
} else {
themeUI = (
<SettingItemMin
- title={formatMessage(holders.themeTitle)}
- describe={formatMessage(holders.themeDescribe)}
+ title={
+ <FormattedMessage
+ id='user.settings.display.theme.title'
+ defaultMessage='Theme'
+ />
+ }
+ describe={
+ <FormattedMessage
+ id='user.settings.display.theme.describe'
+ defaultMessage='Open to manage your theme'
+ />
+ }
updateSection={() => {
this.props.updateSection('theme');
}}
@@ -295,11 +330,8 @@ export default class ThemeSetting extends React.Component {
}
ThemeSetting.propTypes = {
- intl: intlShape.isRequired,
selected: React.PropTypes.bool.isRequired,
updateSection: React.PropTypes.func.isRequired,
setRequireConfirm: React.PropTypes.func.isRequired,
setEnforceFocus: React.PropTypes.func.isRequired
};
-
-export default injectIntl(ThemeSetting);
diff --git a/webapp/dispatcher/app_dispatcher.jsx b/webapp/dispatcher/app_dispatcher.jsx
index 5e43d3ad7..8ab38563b 100644
--- a/webapp/dispatcher/app_dispatcher.jsx
+++ b/webapp/dispatcher/app_dispatcher.jsx
@@ -9,7 +9,7 @@ const PayloadSources = Constants.PayloadSources;
const AppDispatcher = Object.assign(new Flux.Dispatcher(), {
handleServerAction: function performServerAction(action) {
if (!action.type) {
- console.warning('handleServerAction called with undefined action type'); // eslint-disable-line no-console
+ console.warn('handleServerAction called with undefined action type'); // eslint-disable-line no-console
}
var payload = {
@@ -21,7 +21,7 @@ const AppDispatcher = Object.assign(new Flux.Dispatcher(), {
handleViewAction: function performViewAction(action) {
if (!action.type) {
- console.warning('handleViewAction called with undefined action type'); // eslint-disable-line no-console
+ console.warn('handleViewAction called with undefined action type'); // eslint-disable-line no-console
}
var payload = {
diff --git a/webapp/sass/routes/_settings.scss b/webapp/sass/routes/_settings.scss
index f67d1b49b..501c20e20 100644
--- a/webapp/sass/routes/_settings.scss
+++ b/webapp/sass/routes/_settings.scss
@@ -475,3 +475,8 @@
.no-resize {
resize: none;
}
+
+.user-settings__submit-checkbox {
+ padding-top: 0px;
+ padding-bottom: 20px;
+}
diff --git a/webapp/stores/preference_store.jsx b/webapp/stores/preference_store.jsx
index 324ec4864..654036ae8 100644
--- a/webapp/stores/preference_store.jsx
+++ b/webapp/stores/preference_store.jsx
@@ -54,6 +54,16 @@ class PreferenceStoreClass extends EventEmitter {
return parseInt(this.preferences.get(key), 10);
}
+ getObject(category, name, defaultValue = null) {
+ const key = this.getKey(category, name);
+
+ if (!this.preferences.has(key)) {
+ return defaultValue;
+ }
+
+ return JSON.parse(this.preferences.get(key));
+ }
+
getCategory(category) {
const prefix = category + '--';
@@ -78,6 +88,10 @@ class PreferenceStoreClass extends EventEmitter {
}
}
+ deletePreference(preference) {
+ this.preferences.delete(this.getKey(preference.category, preference.name));
+ }
+
clear() {
this.preferences.clear();
}
@@ -94,6 +108,18 @@ class PreferenceStoreClass extends EventEmitter {
this.removeListener(CHANGE_EVENT, callback);
}
+ getTheme(teamId) {
+ if (this.preferences.has(this.getKey(Constants.Preferences.CATEGORY_THEME, teamId))) {
+ return this.getObject(Constants.Preferences.CATEGORY_THEME, teamId);
+ }
+
+ if (this.preferences.has(this.getKey(Constants.Preferences.CATEGORY_THEME, ''))) {
+ return this.getObject(Constants.Preferences.CATEGORY_THEME, '');
+ }
+
+ return Constants.THEMES.default;
+ }
+
handleEventPayload(payload) {
const action = payload.action;
@@ -108,6 +134,12 @@ class PreferenceStoreClass extends EventEmitter {
this.setPreferencesFromServer(action.preferences);
this.emitChange();
break;
+ case ActionTypes.DELETED_PREFERENCES:
+ for (const preference of action.preferences) {
+ this.deletePreference(preference);
+ }
+ this.emitChange();
+ break;
}
}
}
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index 2e26278b2..b31a2a6b9 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -852,6 +852,29 @@ export function savePreferences(preferences, success, error) {
);
}
+export function deletePreferences(preferences, success, error) {
+ Client.deletePreferences(
+ preferences,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.DELETED_PREFERENCES,
+ preferences
+ });
+
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ dispatchError(err, 'deletePreferences');
+
+ if (error) {
+ error();
+ }
+ }
+ );
+}
+
export function getSuggestedCommands(command, suggestionId, component) {
Client.listCommands(
(data) => {
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index df9cb3ba4..d780efe30 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -33,100 +33,125 @@ import mattermostDarkThemeImage from 'images/themes/mattermost_dark.png';
import mattermostThemeImage from 'images/themes/mattermost.png';
import windows10ThemeImage from 'images/themes/windows_dark.png';
-export default {
- ActionTypes: keyMirror({
- RECEIVED_ERROR: null,
-
- CLICK_CHANNEL: null,
- CREATE_CHANNEL: null,
- LEAVE_CHANNEL: null,
- CREATE_POST: null,
- CREATE_COMMENT: null,
- POST_DELETED: null,
- REMOVE_POST: null,
-
- RECEIVED_CHANNELS: null,
- RECEIVED_CHANNEL: null,
- RECEIVED_MORE_CHANNELS: null,
- RECEIVED_CHANNEL_EXTRA_INFO: null,
-
- FOCUS_POST: null,
- RECEIVED_POSTS: null,
- RECEIVED_FOCUSED_POST: null,
- RECEIVED_POST: null,
- RECEIVED_EDIT_POST: null,
- RECEIVED_SEARCH: null,
- RECEIVED_SEARCH_TERM: null,
- RECEIVED_POST_SELECTED: null,
- RECEIVED_MENTION_DATA: null,
- RECEIVED_ADD_MENTION: null,
-
- RECEIVED_PROFILES_FOR_DM_LIST: null,
- RECEIVED_PROFILES: null,
- RECEIVED_DIRECT_PROFILES: null,
- RECEIVED_ME: null,
- RECEIVED_SESSIONS: null,
- RECEIVED_AUDITS: null,
- RECEIVED_TEAMS: null,
- RECEIVED_STATUSES: null,
- RECEIVED_PREFERENCE: null,
- RECEIVED_PREFERENCES: null,
- RECEIVED_FILE_INFO: null,
- RECEIVED_ANALYTICS: null,
-
- RECEIVED_INCOMING_WEBHOOKS: null,
- RECEIVED_INCOMING_WEBHOOK: null,
- REMOVED_INCOMING_WEBHOOK: null,
- RECEIVED_OUTGOING_WEBHOOKS: null,
- RECEIVED_OUTGOING_WEBHOOK: null,
- UPDATED_OUTGOING_WEBHOOK: null,
- REMOVED_OUTGOING_WEBHOOK: null,
- RECEIVED_COMMANDS: null,
- RECEIVED_COMMAND: null,
- UPDATED_COMMAND: null,
- REMOVED_COMMAND: null,
-
- RECEIVED_CUSTOM_EMOJIS: null,
- RECEIVED_CUSTOM_EMOJI: null,
- UPDATED_CUSTOM_EMOJI: null,
- REMOVED_CUSTOM_EMOJI: null,
-
- RECEIVED_MSG: null,
-
- RECEIVED_MY_TEAM: null,
- CREATED_TEAM: null,
-
- RECEIVED_CONFIG: null,
- RECEIVED_LOGS: null,
- RECEIVED_SERVER_AUDITS: null,
- RECEIVED_SERVER_COMPLIANCE_REPORTS: null,
- RECEIVED_ALL_TEAMS: null,
- RECEIVED_ALL_TEAM_LISTINGS: null,
- RECEIVED_TEAM_MEMBERS: null,
- RECEIVED_MEMBERS_FOR_TEAM: null,
-
- RECEIVED_LOCALE: null,
-
- SHOW_SEARCH: null,
-
- USER_TYPING: null,
-
- TOGGLE_IMPORT_THEME_MODAL: null,
- TOGGLE_INVITE_MEMBER_MODAL: null,
- TOGGLE_LEAVE_TEAM_MODAL: null,
- TOGGLE_DELETE_POST_MODAL: null,
- TOGGLE_GET_POST_LINK_MODAL: null,
- TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null,
- TOGGLE_REGISTER_APP_MODAL: null,
- TOGGLE_GET_PUBLIC_LINK_MODAL: null,
-
- SUGGESTION_PRETEXT_CHANGED: null,
- SUGGESTION_RECEIVED_SUGGESTIONS: null,
- SUGGESTION_CLEAR_SUGGESTIONS: null,
- SUGGESTION_COMPLETE_WORD: null,
- SUGGESTION_SELECT_NEXT: null,
- SUGGESTION_SELECT_PREVIOUS: null
- }),
+export const Preferences = {
+ CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
+ CATEGORY_DISPLAY_SETTINGS: 'display_settings',
+ DISPLAY_PREFER_NICKNAME: 'nickname_full_name',
+ DISPLAY_PREFER_FULL_NAME: 'full_name',
+ CATEGORY_ADVANCED_SETTINGS: 'advanced_settings',
+ TUTORIAL_STEP: 'tutorial_step',
+ CHANNEL_DISPLAY_MODE: 'channel_display_mode',
+ CHANNEL_DISPLAY_MODE_CENTERED: 'centered',
+ CHANNEL_DISPLAY_MODE_FULL_SCREEN: 'full',
+ CHANNEL_DISPLAY_MODE_DEFAULT: 'centered',
+ MESSAGE_DISPLAY: 'message_display',
+ MESSAGE_DISPLAY_CLEAN: 'clean',
+ MESSAGE_DISPLAY_COMPACT: 'compact',
+ MESSAGE_DISPLAY_DEFAULT: 'clean',
+ COLLAPSE_DISPLAY: 'collapse_previews',
+ COLLAPSE_DISPLAY_DEFAULT: 'false',
+ USE_MILITARY_TIME: 'use_military_time',
+ CATEGORY_THEME: 'theme'
+};
+
+export const ActionTypes = keyMirror({
+ RECEIVED_ERROR: null,
+
+ CLICK_CHANNEL: null,
+ CREATE_CHANNEL: null,
+ LEAVE_CHANNEL: null,
+ CREATE_POST: null,
+ CREATE_COMMENT: null,
+ POST_DELETED: null,
+ REMOVE_POST: null,
+
+ RECEIVED_CHANNELS: null,
+ RECEIVED_CHANNEL: null,
+ RECEIVED_MORE_CHANNELS: null,
+ RECEIVED_CHANNEL_EXTRA_INFO: null,
+
+ FOCUS_POST: null,
+ RECEIVED_POSTS: null,
+ RECEIVED_FOCUSED_POST: null,
+ RECEIVED_POST: null,
+ RECEIVED_EDIT_POST: null,
+ RECEIVED_SEARCH: null,
+ RECEIVED_SEARCH_TERM: null,
+ RECEIVED_POST_SELECTED: null,
+ RECEIVED_MENTION_DATA: null,
+ RECEIVED_ADD_MENTION: null,
+
+ RECEIVED_PROFILES_FOR_DM_LIST: null,
+ RECEIVED_PROFILES: null,
+ RECEIVED_DIRECT_PROFILES: null,
+ RECEIVED_ME: null,
+ RECEIVED_SESSIONS: null,
+ RECEIVED_AUDITS: null,
+ RECEIVED_TEAMS: null,
+ RECEIVED_STATUSES: null,
+ RECEIVED_PREFERENCE: null,
+ RECEIVED_PREFERENCES: null,
+ DELETED_PREFERENCES: null,
+ RECEIVED_FILE_INFO: null,
+ RECEIVED_ANALYTICS: null,
+
+ RECEIVED_INCOMING_WEBHOOKS: null,
+ RECEIVED_INCOMING_WEBHOOK: null,
+ REMOVED_INCOMING_WEBHOOK: null,
+ RECEIVED_OUTGOING_WEBHOOKS: null,
+ RECEIVED_OUTGOING_WEBHOOK: null,
+ UPDATED_OUTGOING_WEBHOOK: null,
+ REMOVED_OUTGOING_WEBHOOK: null,
+ RECEIVED_COMMANDS: null,
+ RECEIVED_COMMAND: null,
+ UPDATED_COMMAND: null,
+ REMOVED_COMMAND: null,
+
+ RECEIVED_CUSTOM_EMOJIS: null,
+ RECEIVED_CUSTOM_EMOJI: null,
+ UPDATED_CUSTOM_EMOJI: null,
+ REMOVED_CUSTOM_EMOJI: null,
+
+ RECEIVED_MSG: null,
+
+ RECEIVED_MY_TEAM: null,
+ CREATED_TEAM: null,
+
+ RECEIVED_CONFIG: null,
+ RECEIVED_LOGS: null,
+ RECEIVED_SERVER_AUDITS: null,
+ RECEIVED_SERVER_COMPLIANCE_REPORTS: null,
+ RECEIVED_ALL_TEAMS: null,
+ RECEIVED_ALL_TEAM_LISTINGS: null,
+ RECEIVED_TEAM_MEMBERS: null,
+ RECEIVED_MEMBERS_FOR_TEAM: null,
+
+ RECEIVED_LOCALE: null,
+
+ SHOW_SEARCH: null,
+
+ USER_TYPING: null,
+
+ TOGGLE_IMPORT_THEME_MODAL: null,
+ TOGGLE_INVITE_MEMBER_MODAL: null,
+ TOGGLE_LEAVE_TEAM_MODAL: null,
+ TOGGLE_DELETE_POST_MODAL: null,
+ TOGGLE_GET_POST_LINK_MODAL: null,
+ TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null,
+ TOGGLE_REGISTER_APP_MODAL: null,
+ TOGGLE_GET_PUBLIC_LINK_MODAL: null,
+
+ SUGGESTION_PRETEXT_CHANGED: null,
+ SUGGESTION_RECEIVED_SUGGESTIONS: null,
+ SUGGESTION_CLEAR_SUGGESTIONS: null,
+ SUGGESTION_COMPLETE_WORD: null,
+ SUGGESTION_SELECT_NEXT: null,
+ SUGGESTION_SELECT_PREVIOUS: null
+});
+
+export const Constants = {
+ Preferences,
+ ActionTypes,
PayloadSources: keyMirror({
SERVER_ACTION: null,
@@ -174,7 +199,6 @@ export default {
FULLNAME: 'fullname',
NICKNAME: 'nickname',
EMAIL: 'email',
- THEME: 'theme',
LANGUAGE: 'language'
},
@@ -551,25 +575,6 @@ export default {
Ubuntu: 'font--ubuntu'
},
DEFAULT_FONT: 'Open Sans',
- Preferences: {
- CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
- CATEGORY_DISPLAY_SETTINGS: 'display_settings',
- DISPLAY_PREFER_NICKNAME: 'nickname_full_name',
- DISPLAY_PREFER_FULL_NAME: 'full_name',
- CATEGORY_ADVANCED_SETTINGS: 'advanced_settings',
- TUTORIAL_STEP: 'tutorial_step',
- CHANNEL_DISPLAY_MODE: 'channel_display_mode',
- CHANNEL_DISPLAY_MODE_CENTERED: 'centered',
- CHANNEL_DISPLAY_MODE_FULL_SCREEN: 'full',
- CHANNEL_DISPLAY_MODE_DEFAULT: 'full',
- MESSAGE_DISPLAY: 'message_display',
- MESSAGE_DISPLAY_CLEAN: 'clean',
- MESSAGE_DISPLAY_COMPACT: 'compact',
- MESSAGE_DISPLAY_DEFAULT: 'clean',
- COLLAPSE_DISPLAY: 'collapse_previews',
- COLLAPSE_DISPLAY_DEFAULT: 'false',
- USE_MILITARY_TIME: 'use_military_time'
- },
TutorialSteps: {
INTRO_SCREENS: 0,
POST_POPOVER: 1,
@@ -779,3 +784,5 @@ export default {
PERMISSIONS_TEAM_ADMIN: 'team_admin',
PERMISSIONS_SYSTEM_ADMIN: 'system_admin'
};
+
+export default Constants;