summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAsaad Mahmood <Unknowngi@live.com>2015-10-16 18:06:40 +0500
committerAsaad Mahmood <Unknowngi@live.com>2015-10-16 18:06:40 +0500
commit0fbe63eb37b0764a106b0fdd47bc149f122b520d (patch)
tree77cadc54d3a8d0e7812e69c50628cc7f3c5d71b7
parentb00ffa83e7371fa7dd4570130ee8b506943aee01 (diff)
parent89716cb046ee3c8f13b361053d91149f5ce29cbf (diff)
downloadchat-0fbe63eb37b0764a106b0fdd47bc149f122b520d.tar.gz
chat-0fbe63eb37b0764a106b0fdd47bc149f122b520d.tar.bz2
chat-0fbe63eb37b0764a106b0fdd47bc149f122b520d.zip
Merge branch 'master' of https://github.com/mattermost/platform into ui-improvements
-rw-r--r--api/preference.go62
-rw-r--r--api/preference_test.go106
-rw-r--r--api/team.go23
-rw-r--r--api/team_test.go9
-rw-r--r--api/user.go43
-rw-r--r--doc/install/Troubleshooting.md2
-rw-r--r--model/client.go11
-rw-r--r--model/team.go6
-rw-r--r--store/sql_preference_store.go29
-rw-r--r--store/sql_preference_store_test.go48
-rw-r--r--store/store.go1
-rw-r--r--web/react/components/navbar_dropdown.jsx25
-rw-r--r--web/react/components/post_list.jsx3
-rw-r--r--web/react/components/rhs_thread.jsx14
-rw-r--r--web/react/components/sidebar.jsx6
-rw-r--r--web/react/components/user_settings/user_settings.jsx12
-rw-r--r--web/react/components/user_settings/user_settings_display.jsx168
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx1
-rw-r--r--web/react/pages/channel.jsx3
-rw-r--r--web/react/stores/preference_store.jsx1
-rw-r--r--web/react/utils/async_client.jsx15
-rw-r--r--web/react/utils/client.jsx13
-rw-r--r--web/react/utils/constants.jsx3
-rw-r--r--web/react/utils/utils.jsx33
24 files changed, 488 insertions, 149 deletions
diff --git a/api/preference.go b/api/preference.go
index 88cb132f8..6d6ac1a7f 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 318ce9582..9d3db9e2f 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 6aa5ec1bb..f6038566a 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 9b701911b..507f4252a 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 146ede015..ac33e81a1 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 8d82100d8..b87663ab3 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 77b0aaad2..eea65c50e 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 c0f6524cd..584c78f8d 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 46cef38b1..bf6e030bf 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 76b1bcb17..e68203cc3 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 b436a5c40..de335cc2b 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 e0410800f..1cb13bbe5 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 {
</li>
);
- this.state.teams.forEach((teamName) => {
- if (teamName !== this.props.teamName) {
- teams.push(<li key={teamName}><a href={Utils.getWindowLocationOrigin() + '/' + teamName}>{'Switch to ' + teamName}</a></li>);
+ this.state.teams.forEach((team) => {
+ if (team.name !== this.props.teamName) {
+ teams.push(<li key={team.name}><a href={Utils.getWindowLocationOrigin() + '/' + team.name}>{'Switch to ' + team.display_name}</a></li>);
}
});
}
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 7e5160c2b..29728d368 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 131253aa5..467d74681 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 {
/>
<div className='post-right__scroll'>
<RootPost
+ ref={rootPost.id}
post={rootPost}
commentCount={postsArray.length}
/>
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 6b3dae4cd..4594555e9 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 5ce9b6330..15bf961d6 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 {
/>
</div>
);
+ } else if (this.props.activeTab === 'display') {
+ return (
+ <div>
+ <DisplayTab
+ user={this.state.user}
+ activeSection={this.props.activeSection}
+ updateSection={this.props.updateSection}
+ updateTab={this.props.updateTab}
+ />
+ </div>
+ );
}
return <div/>;
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 000000000..ec209c218
--- /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 = [
+ <div key='userDisplayClockOptions'>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={clockFormat[0]}
+ onChange={this.handleClockRadio.bind(this, 'false')}
+ >
+ 12-hour clock (example: 4:00 PM)
+ </input>
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={clockFormat[1]}
+ onChange={this.handleClockRadio.bind(this, 'true')}
+ >
+ 24-hour clock (example: 16:00)
+ </input>
+ </label>
+ <br/>
+ </div>
+ <div><br/>{'Select how you prefer time displayed.'}</div>
+ </div>
+ ];
+
+
+ clockSection = (
+ <SettingItemMax
+ title='Clock Display'
+ inputs={inputs}
+ submit={this.handleSubmit}
+ server_error={serverError}
+ updateSection={handleUpdateClockSection}
+ />
+ );
+ } 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 = (
+ <SettingItemMin
+ title='Clock Display'
+ describe={describe}
+ updateSection={handleUpdateClockSection}
+ />
+ );
+ }
+
+ return (
+ <div>
+ <div className='modal-header'>
+ <button
+ type='button'
+ className='close'
+ data-dismiss='modal'
+ aria-label='Close'
+ >
+ <span aria-hidden='true'>{'×'}</span>
+ </button>
+ <h4
+ className='modal-title'
+ ref='title'
+ >
+ <i className='modal-back'></i>
+ {'Display Settings'}
+ </h4>
+ </div>
+ <div className='user-settings'>
+ <h3 className='tab-header'>{'Display Settings'}</h3>
+ <div className='divider-dark first'/>
+ {clockSection}
+ <div className='divider-dark'/>
+ </div>
+ </div>
+ );
+ }
+}
+
+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 19b97fc85..692fb26ee 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 (
<div
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index d9b78dada..20ed1bf0a 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -37,6 +37,7 @@ var RegisterAppModal = require('../components/register_app_modal.jsx');
var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx');
var TeamStore = require('../stores/team_store.jsx');
+var AsyncClient = require('../utils/async_client.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
@@ -54,6 +55,8 @@ function setupChannelPage(props) {
id: props.TeamId
});
+ AsyncClient.getAllPreferences();
+
// ChannelLoader must be rendered first
ReactDOM.render(
<ChannelLoader/>,
diff --git a/web/react/stores/preference_store.jsx b/web/react/stores/preference_store.jsx
index d71efa10f..f630d150d 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 1bf8a6fee..b22d7237e 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 76a402855..f6aee362c 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 b8200db54..b7b8d3c60 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 0457d620f..561c2c4c4 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();
-
- var ampm = 'AM';
- if (hours >= 12) {
- ampm = 'PM';
- }
+ const d = new Date(ticks);
+ let hours = d.getHours();
+ let minutes = d.getMinutes();
+ let ampm = '';
- 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) {