diff --git a/api/preference.go b/api/preference.go index 88cb132f88..6d6ac1a7ff 100644 --- a/api/preference.go +++ b/api/preference.go @@ -14,11 +14,22 @@ func InitPreference(r *mux.Router) { l4g.Debug("Initializing preference api routes") sr := r.PathPrefix("/preferences").Subrouter() + sr.Handle("/", ApiUserRequired(getAllPreferences)).Methods("GET") sr.Handle("/save", ApiUserRequired(savePreferences)).Methods("POST") sr.Handle("/{category:[A-Za-z0-9_]+}", ApiUserRequired(getPreferenceCategory)).Methods("GET") sr.Handle("/{category:[A-Za-z0-9_]+}/{name:[A-Za-z0-9_]+}", ApiUserRequired(getPreference)).Methods("GET") } +func getAllPreferences(c *Context, w http.ResponseWriter, r *http.Request) { + if result := <-Srv.Store.Preference().GetAll(c.Session.UserId); result.Err != nil { + c.Err = result.Err + } else { + data := result.Data.(model.Preferences) + + w.Write([]byte(data.ToJson())) + } +} + func savePreferences(c *Context, w http.ResponseWriter, r *http.Request) { preferences, err := model.PreferencesFromJson(r.Body) if err != nil { @@ -52,61 +63,10 @@ func getPreferenceCategory(c *Context, w http.ResponseWriter, r *http.Request) { } else { data := result.Data.(model.Preferences) - data = transformPreferences(c, data, category) - w.Write([]byte(data.ToJson())) } } -func transformPreferences(c *Context, preferences model.Preferences, category string) model.Preferences { - if len(preferences) == 0 && category == model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW { - // add direct channels for a user that existed before preferences were added - preferences = addDirectChannels(c.Session.UserId, c.Session.TeamId) - } - - return preferences -} - -func addDirectChannels(userId, teamId string) model.Preferences { - var profiles map[string]*model.User - if result := <-Srv.Store.User().GetProfiles(teamId); result.Err != nil { - l4g.Error("Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v", userId, teamId, result.Err.Error()) - return model.Preferences{} - } else { - profiles = result.Data.(map[string]*model.User) - } - - var preferences model.Preferences - - for id := range profiles { - if id == userId { - continue - } - - profile := profiles[id] - - preference := model.Preference{ - UserId: userId, - Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, - Name: profile.Id, - Value: "true", - } - - preferences = append(preferences, preference) - - if len(preferences) >= 10 { - break - } - } - - if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil { - l4g.Error("Failed to add direct channel preferences for user user_id=%s, eam_id=%s, err=%v", userId, teamId, result.Err.Error()) - return model.Preferences{} - } else { - return preferences - } -} - func getPreference(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) category := params["category"] diff --git a/api/preference_test.go b/api/preference_test.go index 318ce95826..9d3db9e2fc 100644 --- a/api/preference_test.go +++ b/api/preference_test.go @@ -9,6 +9,64 @@ import ( "testing" ) +func TestGetAllPreferences(t *testing.T) { + Setup() + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + + category := model.NewId() + + preferences1 := model.Preferences{ + { + UserId: user1.Id, + Category: category, + Name: model.NewId(), + }, + { + UserId: user1.Id, + Category: category, + Name: model.NewId(), + }, + { + UserId: user1.Id, + Category: model.NewId(), + Name: model.NewId(), + }, + } + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + Client.Must(Client.SetPreferences(&preferences1)) + + if result, err := Client.GetAllPreferences(); err != nil { + t.Fatal(err) + } else if data := result.Data.(model.Preferences); len(data) != 3 { + t.Fatal("received the wrong number of preferences") + } else if !((data[0] == preferences1[0] && data[1] == preferences1[1]) || (data[0] == preferences1[1] && data[1] == preferences1[0])) { + for i := 0; i < 3; i++ { + if data[0] != preferences1[i] && data[1] != preferences1[i] && data[2] != preferences1[i] { + t.Fatal("got incorrect preferences") + } + } + } + + Client.LoginByEmail(team.Name, user2.Email, "pwd") + + if result, err := Client.GetAllPreferences(); err != nil { + t.Fatal(err) + } else if data := result.Data.(model.Preferences); len(data) != 0 { + t.Fatal("received the wrong number of preferences") + } +} + func TestSetPreferences(t *testing.T) { Setup() @@ -113,54 +171,6 @@ func TestGetPreferenceCategory(t *testing.T) { } } -func TestTransformPreferences(t *testing.T) { - Setup() - - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - for i := 0; i < 5; i++ { - user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} - Client.Must(Client.CreateUser(user, "")) - } - - Client.Must(Client.LoginByEmail(team.Name, user1.Email, "pwd")) - - if result, err := Client.GetPreferenceCategory(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW); err != nil { - t.Fatal(err) - } else if data := result.Data.(model.Preferences); len(data) != 5 { - t.Fatal("received the wrong number of direct channels") - } - - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - for i := 0; i < 10; i++ { - user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} - Client.Must(Client.CreateUser(user, "")) - } - - // make sure user1's preferences don't change - if result, err := Client.GetPreferenceCategory(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW); err != nil { - t.Fatal(err) - } else if data := result.Data.(model.Preferences); len(data) != 5 { - t.Fatal("received the wrong number of direct channels") - } - - Client.Must(Client.LoginByEmail(team.Name, user2.Email, "pwd")) - - if result, err := Client.GetPreferenceCategory(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW); err != nil { - t.Fatal(err) - } else if data := result.Data.(model.Preferences); len(data) != 10 { - t.Fatal("received the wrong number of direct channels") - } -} - func TestGetPreference(t *testing.T) { Setup() diff --git a/api/team.go b/api/team.go index 6aa5ec1bb2..f6038566a8 100644 --- a/api/team.go +++ b/api/team.go @@ -52,7 +52,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !isTreamCreationAllowed(c, email) { + if !isTeamCreationAllowed(c, email) { return } @@ -100,7 +100,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !isTreamCreationAllowed(c, team.Email) { + if !isTeamCreationAllowed(c, team.Email) { return } @@ -169,7 +169,7 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !isTreamCreationAllowed(c, teamSignup.Team.Email) { + if !isTeamCreationAllowed(c, teamSignup.Team.Email) { return } @@ -257,7 +257,7 @@ func CreateTeam(c *Context, team *model.Team) *model.Team { return nil } - if !isTreamCreationAllowed(c, team.Email) { + if !isTeamCreationAllowed(c, team.Email) { return nil } @@ -276,12 +276,12 @@ func CreateTeam(c *Context, team *model.Team) *model.Team { } } -func isTreamCreationAllowed(c *Context, email string) bool { +func isTeamCreationAllowed(c *Context, email string) bool { email = strings.ToLower(email) if !utils.Cfg.TeamSettings.EnableTeamCreation { - c.Err = model.NewAppError("isTreamCreationAllowed", "Team creation has been disabled. Please ask your systems administrator for details.", "") + c.Err = model.NewAppError("isTeamCreationAllowed", "Team creation has been disabled. Please ask your systems administrator for details.", "") return false } @@ -298,7 +298,7 @@ func isTreamCreationAllowed(c *Context, email string) bool { } if len(utils.Cfg.TeamSettings.RestrictCreationToDomains) > 0 && !matched { - c.Err = model.NewAppError("isTreamCreationAllowed", "Email must be from a specific domain (e.g. @example.com). Please ask your systems administrator for details.", "") + c.Err = model.NewAppError("isTeamCreationAllowed", "Email must be from a specific domain (e.g. @example.com). Please ask your systems administrator for details.", "") return false } @@ -409,14 +409,13 @@ func findTeams(c *Context, w http.ResponseWriter, r *http.Request) { return } else { teams := result.Data.([]*model.Team) - - s := make([]string, 0, len(teams)) - + m := make(map[string]*model.Team) for _, v := range teams { - s = append(s, v.Name) + v.Sanitize() + m[v.Id] = v } - w.Write([]byte(model.ArrayToJson(s))) + w.Write([]byte(model.TeamMapToJson(m))) } } diff --git a/api/team_test.go b/api/team_test.go index 9b701911b9..507f4252a3 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -121,9 +121,12 @@ func TestFindTeamByEmail(t *testing.T) { if r1, err := Client.FindTeams(user.Email); err != nil { t.Fatal(err) } else { - domains := r1.Data.([]string) - if domains[0] != team.Name { - t.Fatal(domains) + teams := r1.Data.(map[string]*model.Team) + if teams[team.Id].Name != team.Name { + t.Fatal() + } + if teams[team.Id].DisplayName != team.DisplayName { + t.Fatal() } } diff --git a/api/user.go b/api/user.go index 146ede0154..ac33e81a18 100644 --- a/api/user.go +++ b/api/user.go @@ -198,7 +198,9 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err) } - fireAndForgetWelcomeEmail(result.Data.(*model.User).Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), user.EmailVerified) + fireAndForgetWelcomeEmail(ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), user.EmailVerified) + + addDirectChannelsAndForget(ruser) if user.EmailVerified { if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil { @@ -237,6 +239,45 @@ func fireAndForgetWelcomeEmail(userId, email, teamName, teamDisplayName, siteURL }() } +func addDirectChannelsAndForget(user *model.User) { + go func() { + var profiles map[string]*model.User + if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil { + l4g.Error("Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v", user.Id, user.TeamId, result.Err.Error()) + return + } else { + profiles = result.Data.(map[string]*model.User) + } + + var preferences model.Preferences + + for id := range profiles { + if id == user.Id { + continue + } + + profile := profiles[id] + + preference := model.Preference{ + UserId: user.Id, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, + Name: profile.Id, + Value: "true", + } + + preferences = append(preferences, preference) + + if len(preferences) >= 10 { + break + } + } + + if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil { + l4g.Error("Failed to add direct channel preferences for new user user_id=%s, eam_id=%s, err=%v", user.Id, user.TeamId, result.Err.Error()) + } + }() +} + func FireAndForgetVerifyEmail(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { diff --git a/doc/install/Troubleshooting.md b/doc/install/Troubleshooting.md index 8d82100d8c..b87663ab35 100644 --- a/doc/install/Troubleshooting.md +++ b/doc/install/Troubleshooting.md @@ -3,5 +3,5 @@ #### Important notes 1. **DO NOT manipulate the Mattermost database** - - In particular, DO NOT delete data from the database, as this will most likely crash Mattermost in strange ways. Mattermost is designed to archive content continously and generally assumes data is never deleted. + - In particular, DO NOT delete data from the database, as Mattermost is designed to stop working if data integrity has been compromised. The system is designed to archive content continously and generally assumes data is never deleted. diff --git a/model/client.go b/model/client.go index 77b0aaad28..eea65c50ea 100644 --- a/model/client.go +++ b/model/client.go @@ -185,7 +185,7 @@ func (c *Client) FindTeams(email string) (*Result, *AppError) { } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil + r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil } } @@ -844,6 +844,15 @@ func (c *Client) ListIncomingWebhooks() (*Result, *AppError) { } } +func (c *Client) GetAllPreferences() (*Result, *AppError) { + if r, err := c.DoApiGet("/preferences/", "", ""); err != nil { + return nil, err + } else { + preferences, _ := PreferencesFromJson(r.Body) + return &Result{r.Header.Get(HEADER_REQUEST_ID), r.Header.Get(HEADER_ETAG_SERVER), preferences}, nil + } +} + func (c *Client) SetPreferences(preferences *Preferences) (*Result, *AppError) { if r, err := c.DoApiPost("/preferences/save", preferences.ToJson()); err != nil { return nil, err diff --git a/model/team.go b/model/team.go index c0f6524cd5..584c78f8d8 100644 --- a/model/team.go +++ b/model/team.go @@ -219,3 +219,9 @@ func CleanTeamName(s string) string { func (o *Team) PreExport() { } + +func (o *Team) Sanitize() { + o.Email = "" + o.Type = "" + o.AllowedDomains = "" +} diff --git a/store/sql_preference_store.go b/store/sql_preference_store.go index 46cef38b15..bf6e030bfb 100644 --- a/store/sql_preference_store.go +++ b/store/sql_preference_store.go @@ -43,7 +43,7 @@ func (s SqlPreferenceStore) Save(preferences *model.Preferences) StoreChannel { result := StoreResult{} // wrap in a transaction so that if one fails, everything fails - transaction, err := s.GetReplica().Begin() + transaction, err := s.GetMaster().Begin() if err != nil { result.Err = model.NewAppError("SqlPreferenceStore.Save", "Unable to open transaction to save preferences", err.Error()) } else { @@ -212,3 +212,30 @@ func (s SqlPreferenceStore) GetCategory(userId string, category string) StoreCha return storeChannel } + +func (s SqlPreferenceStore) GetAll(userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var preferences model.Preferences + + if _, err := s.GetReplica().Select(&preferences, + `SELECT + * + FROM + Preferences + WHERE + UserId = :UserId`, map[string]interface{}{"UserId": userId}); err != nil { + result.Err = model.NewAppError("SqlPreferenceStore.GetAll", "We encounted an error while finding preferences", err.Error()) + } else { + result.Data = preferences + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_preference_store_test.go b/store/sql_preference_store_test.go index 76b1bcb17e..e68203cc33 100644 --- a/store/sql_preference_store_test.go +++ b/store/sql_preference_store_test.go @@ -144,3 +144,51 @@ func TestPreferenceGetCategory(t *testing.T) { t.Fatal("shouldn't have got any preferences") } } + +func TestPreferenceGetAll(t *testing.T) { + Setup() + + userId := model.NewId() + category := model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW + name := model.NewId() + + preferences := model.Preferences{ + { + UserId: userId, + Category: category, + Name: name, + }, + // same user/category, different name + { + UserId: userId, + Category: category, + Name: model.NewId(), + }, + // same user/name, different category + { + UserId: userId, + Category: model.NewId(), + Name: name, + }, + // same name/category, different user + { + UserId: model.NewId(), + Category: category, + Name: name, + }, + } + + Must(store.Preference().Save(&preferences)) + + if result := <-store.Preference().GetAll(userId); result.Err != nil { + t.Fatal(result.Err) + } else if data := result.Data.(model.Preferences); len(data) != 3 { + t.Fatal("got the wrong number of preferences") + } else { + for i := 0; i < 3; i++ { + if data[0] != preferences[i] && data[1] != preferences[i] && data[2] != preferences[i] { + t.Fatal("got incorrect preferences") + } + } + } +} diff --git a/store/store.go b/store/store.go index b436a5c403..de335cc2b5 100644 --- a/store/store.go +++ b/store/store.go @@ -156,4 +156,5 @@ type PreferenceStore interface { Save(preferences *model.Preferences) StoreChannel Get(userId string, category string, name string) StoreChannel GetCategory(userId string, category string) StoreChannel + GetAll(userId string) StoreChannel } diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index e0410800f7..1cb13bbe52 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -11,7 +11,24 @@ var AboutBuildModal = require('./about_build_modal.jsx'); var Constants = require('../utils/constants.jsx'); function getStateFromStores() { - return {teams: UserStore.getTeams()}; + let teams = []; + let teamsObject = UserStore.getTeams(); + for (let teamId in teamsObject) { + if (teamsObject.hasOwnProperty(teamId)) { + teams.push(teamsObject[teamId]); + } + } + teams.sort(function sortByDisplayName(teamA, teamB) { + let teamADisplayName = teamA.display_name.toLowerCase(); + let teamBDisplayName = teamB.display_name.toLowerCase(); + if (teamADisplayName < teamBDisplayName) { + return -1; + } else if (teamADisplayName > teamBDisplayName) { + return 1; + } + return 0; + }); + return {teams}; } export default class NavbarDropdown extends React.Component { @@ -154,9 +171,9 @@ export default class NavbarDropdown extends React.Component { ); - this.state.teams.forEach((teamName) => { - if (teamName !== this.props.teamName) { - teams.push(
  • {'Switch to ' + teamName}
  • ); + this.state.teams.forEach((team) => { + if (team.name !== this.props.teamName) { + teams.push(
  • {'Switch to ' + team.display_name}
  • ); } }); } diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 7e5160c2b9..29728d3684 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -4,6 +4,7 @@ var PostStore = require('../stores/post_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); var UserStore = require('../stores/user_store.jsx'); +var PreferenceStore = require('../stores/preference_store.jsx'); var UserProfile = require('./user_profile.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var Post = require('./post.jsx'); @@ -105,6 +106,7 @@ export default class PostList extends React.Component { PostStore.clearUnseenDeletedPosts(this.props.channelId); PostStore.addChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onTimeChange); + PreferenceStore.addChangeListener(this.onTimeChange); SocketStore.addChangeListener(this.onSocketChange); const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist)); @@ -156,6 +158,7 @@ export default class PostList extends React.Component { PostStore.removeChangeListener(this.onChange); UserStore.removeStatusesChangeListener(this.onTimeChange); SocketStore.removeChangeListener(this.onSocketChange); + PreferenceStore.removeChangeListener(this.onTimeChange); $('body').off('click.userpopover'); $(window).off('resize'); var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist)); diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx index 131253aa51..467d74681b 100644 --- a/web/react/components/rhs_thread.jsx +++ b/web/react/components/rhs_thread.jsx @@ -3,6 +3,7 @@ var PostStore = require('../stores/post_store.jsx'); var UserStore = require('../stores/user_store.jsx'); +var PreferenceStore = require('../stores/preference_store.jsx'); var utils = require('../utils/utils.jsx'); var SearchBox = require('./search_bar.jsx'); var CreateComment = require('./create_comment.jsx'); @@ -18,6 +19,7 @@ export default class RhsThread extends React.Component { this.onChange = this.onChange.bind(this); this.onChangeAll = this.onChangeAll.bind(this); + this.forceUpdateInfo = this.forceUpdateInfo.bind(this); this.state = this.getStateFromStores(); } @@ -43,6 +45,7 @@ export default class RhsThread extends React.Component { componentDidMount() { PostStore.addSelectedPostChangeListener(this.onChange); PostStore.addChangeListener(this.onChangeAll); + PreferenceStore.addChangeListener(this.forceUpdateInfo); this.resize(); $(window).resize(function resize() { this.resize(); @@ -57,6 +60,16 @@ export default class RhsThread extends React.Component { componentWillUnmount() { PostStore.removeSelectedPostChangeListener(this.onChange); PostStore.removeChangeListener(this.onChangeAll); + PreferenceStore.removeChangeListener(this.forceUpdateInfo); + } + forceUpdateInfo() { + if (this.state.postList) { + for (var postId in this.state.postList.posts) { + if (this.refs[postId]) { + this.refs[postId].forceUpdate(); + } + } + } } onChange() { var newState = this.getStateFromStores(); @@ -174,6 +187,7 @@ export default class RhsThread extends React.Component { />
    diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 6b3dae4cd9..4594555e9b 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -132,11 +132,7 @@ export default class Sidebar extends React.Component { SocketStore.addChangeListener(this.onSocketChange); PreferenceStore.addChangeListener(this.onChange); - AsyncClient.getDirectChannelPreferences(); - - if ($(window).width() > 768) { - $('.nav-pills__container').perfectScrollbar(); - } + $('.nav-pills__container').perfectScrollbar(); this.updateTitle(); this.updateUnreadIndicators(); diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx index 5ce9b6330a..15bf961d64 100644 --- a/web/react/components/user_settings/user_settings.jsx +++ b/web/react/components/user_settings/user_settings.jsx @@ -9,6 +9,7 @@ var GeneralTab = require('./user_settings_general.jsx'); var AppearanceTab = require('./user_settings_appearance.jsx'); var DeveloperTab = require('./user_settings_developer.jsx'); var IntegrationsTab = require('./user_settings_integrations.jsx'); +var DisplayTab = require('./user_settings_display.jsx'); export default class UserSettings extends React.Component { constructor(props) { @@ -98,6 +99,17 @@ export default class UserSettings extends React.Component { />
    ); + } else if (this.props.activeTab === 'display') { + return ( +
    + +
    + ); } return
    ; diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx new file mode 100644 index 0000000000..ec209c2187 --- /dev/null +++ b/web/react/components/user_settings/user_settings_display.jsx @@ -0,0 +1,168 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import { savePreferences } from '../../utils/client.jsx'; +import SettingItemMin from '../setting_item_min.jsx'; +import SettingItemMax from '../setting_item_max.jsx'; +import Constants from '../../utils/constants.jsx'; +import PreferenceStore from '../../stores/preference_store.jsx'; + +function getDisplayStateFromStores() { + const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'}); + + return {militaryTime: militaryTime.value}; +} + +export default class UserSettingsDisplay extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + this.handleClockRadio = this.handleClockRadio.bind(this); + this.updateSection = this.updateSection.bind(this); + this.handleClose = this.handleClose.bind(this); + + this.state = getDisplayStateFromStores(); + } + handleSubmit() { + const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime); + + savePreferences([preference], + () => { + PreferenceStore.emitChange(); + this.updateSection(''); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + handleClockRadio(militaryTime) { + this.setState({militaryTime: militaryTime}); + } + updateSection(section) { + this.setState(getDisplayStateFromStores()); + this.props.updateSection(section); + } + handleClose() { + this.updateSection(''); + } + componentDidMount() { + $('#user_settings').on('hidden.bs.modal', this.handleClose); + } + componentWillUnmount() { + $('#user_settings').off('hidden.bs.modal', this.handleClose); + } + render() { + const serverError = this.state.serverError || null; + let clockSection; + if (this.props.activeSection === 'clock') { + let clockFormat = [false, false]; + if (this.state.militaryTime === 'true') { + clockFormat[1] = true; + } else { + clockFormat[0] = true; + } + + const handleUpdateClockSection = (e) => { + this.updateSection(''); + e.preventDefault(); + }; + + const inputs = [ +
    +
    + +
    +
    +
    + +
    +
    +

    {'Select how you prefer time displayed.'}
    +
    + ]; + + + clockSection = ( + + ); + } else { + let describe = ''; + if (this.state.militaryTime === 'true') { + describe = '24-hour clock (example: 16:00)'; + } else { + describe = '12-hour clock (example: 4:00 PM)'; + } + + const handleUpdateClockSection = () => { + this.props.updateSection('clock'); + }; + + clockSection = ( + + ); + } + + return ( +
    +
    + +

    + + {'Display Settings'} +

    +
    +
    +

    {'Display Settings'}

    +
    + {clockSection} +
    +
    +
    + ); + } +} + +UserSettingsDisplay.propTypes = { + user: React.PropTypes.object, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func, + activeSection: React.PropTypes.string +}; diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index 19b97fc854..692fb26ee4 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -41,6 +41,7 @@ export default class UserSettingsModal extends React.Component { if (global.window.config.EnableIncomingWebhooks === 'true') { tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'}); } + tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'}); return (
    , diff --git a/web/react/stores/preference_store.jsx b/web/react/stores/preference_store.jsx index d71efa10fc..f630d150de 100644 --- a/web/react/stores/preference_store.jsx +++ b/web/react/stores/preference_store.jsx @@ -120,3 +120,4 @@ class PreferenceStoreClass extends EventEmitter { const PreferenceStore = new PreferenceStoreClass(); export default PreferenceStore; +window.PreferenceStore = PreferenceStore; diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index 1bf8a6fee9..b22d7237e2 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -638,16 +638,15 @@ export function getMyTeam() { ); } -export function getDirectChannelPreferences() { - if (isCallInProgress('getDirectChannelPreferences')) { +export function getAllPreferences() { + if (isCallInProgress('getAllPreferences')) { return; } - callTracker.getDirectChannelPreferences = utils.getTimestamp(); - client.getPreferenceCategory( - Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, + callTracker.getAllPreferences = utils.getTimestamp(); + client.getAllPreferences( (data, textStatus, xhr) => { - callTracker.getDirectChannelPreferences = 0; + callTracker.getAllPreferences = 0; if (xhr.status === 304 || !data) { return; @@ -659,8 +658,8 @@ export function getDirectChannelPreferences() { }); }, (err) => { - callTracker.getDirectChannelPreferences = 0; - dispatchError(err, 'getDirectChannelPreferences'); + callTracker.getAllPreferences = 0; + dispatchError(err, 'getAllPreferences'); } ); } diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 76a4028552..f6aee362c2 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -1142,6 +1142,19 @@ export function listIncomingHooks(success, error) { }); } +export function getAllPreferences(success, error) { + $.ajax({ + url: `/api/v1/preferences/`, + dataType: 'json', + type: 'GET', + success, + error: (xhr, status, err) => { + var e = handleError('getAllPreferences', xhr, status, err); + error(e); + } + }); +} + export function getPreferenceCategory(category, success, error) { $.ajax({ url: `/api/v1/preferences/${category}`, diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index b8200db54d..b7b8d3c602 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -289,7 +289,8 @@ module.exports = { } ], Preferences: { - CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show' + CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show', + CATEGORY_DISPLAY_SETTINGS: 'display_settings' }, KeyCodes: { UP: 38, diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 0457d620fe..561c2c4c43 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -4,6 +4,7 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); var UserStore = require('../stores/user_store.jsx'); +var PreferenceStore = require('../stores/preference_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; @@ -164,23 +165,29 @@ export function displayDate(ticks) { } export function displayTime(ticks) { - var d = new Date(ticks); - var hours = d.getHours(); - var minutes = d.getMinutes(); + const d = new Date(ticks); + let hours = d.getHours(); + let minutes = d.getMinutes(); + let ampm = ''; - var ampm = 'AM'; - if (hours >= 12) { - ampm = 'PM'; - } - - hours = hours % 12; - if (!hours) { - hours = '12'; - } if (minutes <= 9) { minutes = '0' + minutes; } - return hours + ':' + minutes + ' ' + ampm; + + const useMilitaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'}).value; + if (useMilitaryTime === 'false') { + ampm = ' AM'; + if (hours >= 12) { + ampm = ' PM'; + } + + hours = hours % 12; + if (!hours) { + hours = '12'; + } + } + + return hours + ':' + minutes + ampm; } export function displayDateTime(ticks) {