From caabfbcdd56bdced7c5c1d38e00f488adffe7c60 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Thu, 14 Jul 2016 10:08:36 -0400 Subject: PLT-2992 Added the ability to use different themes for each team (#3411) * Cleaned up user_settings_theme.jsx and import_theme_modal.jsx * Made ImportThemeModal use a callback to return the theme to the user settings modal instead of saving it directly * Moved user theme from model to preferences * Added serverside API to delete preferences TODO update package with client stuff * Changed constants.jsx so that Preferences and ActionTypes can be imported on their own * Updated ThemeProps migration code to properly rename solarized code themes * Fixed warnings thrown by AppDispatcher * Added clientside UI to support team-specific themes * Removed debugging code from test * Fixed setting a user's theme when they haven't set their theme before --- api/preference.go | 28 +++ api/preference_test.go | 46 ++++ i18n/en.json | 24 ++- model/client.go | 10 + model/preference.go | 44 +++- model/preference_test.go | 39 +++- model/user.go | 21 -- model/user_test.go | 13 -- store/sql_preference_store.go | 27 ++- store/sql_preference_store_test.go | 27 ++- store/sql_user_store.go | 70 ++++-- store/store.go | 1 + webapp/actions/user_actions.jsx | 56 ++++- webapp/components/logged_in.jsx | 9 - webapp/components/needs_team.jsx | 37 +++- webapp/components/setting_item_max.jsx | 4 +- .../user_settings/import_theme_modal.jsx | 91 ++++---- .../user_settings/user_settings_theme.jsx | 122 +++++++---- webapp/dispatcher/app_dispatcher.jsx | 4 +- webapp/sass/routes/_settings.scss | 5 + webapp/stores/preference_store.jsx | 32 +++ webapp/utils/async_client.jsx | 23 ++ webapp/utils/constants.jsx | 235 +++++++++++---------- 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 @@ -1139,6 +1139,14 @@ "id": "api.post_get_post_by_id.get.app_error", "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" @@ -3011,6 +3019,10 @@ "id": "model.preference.is_valid.name.app_error", "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" @@ -3175,10 +3187,6 @@ "id": "model.user.is_valid.team_id.app_error", "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" @@ -3775,6 +3783,10 @@ "id": "store.sql_post.update.app_error", "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" @@ -4055,6 +4067,10 @@ "id": "store.sql_user.get_unread_count.app_error", "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 {

  • + {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: ( + + ) + }); 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: ( + + ) + }); } } + render() { return ( @@ -170,9 +173,9 @@ class ImportThemeModal extends React.Component {
    @@ -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 {
    ); + let allTeamsCheckbox = null; + if (this.state.showAllTeamsCheckbox) { + allTeamsCheckbox = ( +
    + +
    + ); + } + themeUI = ( + } + describe={ + + } 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; -- cgit v1.2.3-1-g7c22