summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md13
-rw-r--r--api/preference.go62
-rw-r--r--api/preference_test.go107
-rw-r--r--api/team.go23
-rw-r--r--api/team_test.go9
-rw-r--r--api/user.go43
-rw-r--r--doc/developer/Setup.md2
-rw-r--r--doc/install/Docker-Single-Container.md7
-rw-r--r--doc/install/Troubleshooting.md2
-rw-r--r--model/client.go11
-rw-r--r--model/team.go6
-rw-r--r--model/webhook.go5
-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/create_post.jsx26
-rw-r--r--web/react/components/edit_post_modal.jsx36
-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/textbox.jsx8
-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/post_store.jsx37
-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.jsx13
-rw-r--r--web/react/utils/utils.jsx33
-rw-r--r--web/sass-files/sass/partials/_post_right.scss3
-rw-r--r--web/sass-files/sass/partials/_search.scss3
-rw-r--r--web/sass-files/sass/partials/_sidebar--left.scss3
-rw-r--r--web/static/images/webhook_icon.jpgbin0 -> 68190 bytes
-rw-r--r--web/web.go16
37 files changed, 642 insertions, 165 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eac1e61e5..425902cf0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,14 @@
# Mattermost Changelog
-## UNDER DEVELOPMENT - Release v1.1.0
+## UNDER DEVELOPMENT Release v1.2.0
The "UNDER DEVELOPMENT" section of the Mattermost changelog appears in the product's `master` branch to note key changes committed to master and are on their way to the next stable release. When a stable release is pushed the "UNDER DEVELOPMENT" heading is removed from the final changelog of the release.
-- **Final release anticipated:** 2015-10-16
+- **Final release anticipated:** 2015-11-16
+
+## Release v1.1.0
+
+Released: 2015-10-16
### Release Highlights
@@ -44,6 +48,11 @@ Messaging and Notifications
- Fixed bug where System Administrator did not have Team Administrator permissions
- Fixed bug causing scrolling to jump when the right hand sidebar opened and closed
+### Known Issues
+
+- Slack import is unstable due to change in Slack export format
+- Uploading a .flac file breaks the file previewer on iOS
+
### Contributors
Many thanks to our external contributors. In no particular order:
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..eaa92fe47 100644
--- a/api/preference_test.go
+++ b/api/preference_test.go
@@ -9,6 +9,65 @@ 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")
+
+ // note that user2 will automatically have a preference set for them to show user1 for direct messages
+ if result, err := Client.GetAllPreferences(); err != nil {
+ t.Fatal(err)
+ } else if data := result.Data.(model.Preferences); len(data) != 1 {
+ t.Fatal("received the wrong number of preferences")
+ }
+}
+
func TestSetPreferences(t *testing.T) {
Setup()
@@ -113,54 +172,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 65dfcc95d..666fd909b 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/developer/Setup.md b/doc/developer/Setup.md
index afaef4ee4..d806b7a9b 100644
--- a/doc/developer/Setup.md
+++ b/doc/developer/Setup.md
@@ -61,7 +61,7 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org
3. Reload your bashrc
`source ~/.bashrc`
6. Install Node.js
- 1. Download the newest version of the Node.js sources from https://nodejs.org/download/
+ 1. Download the newest version of the Node.js sources from https://nodejs.org/en/download/
2. Extract the contents of the package and cd into the extracted files
3. Compile and install Node.js
`./configure`
diff --git a/doc/install/Docker-Single-Container.md b/doc/install/Docker-Single-Container.md
index 4b952cd71..7c0784ad0 100644
--- a/doc/install/Docker-Single-Container.md
+++ b/doc/install/Docker-Single-Container.md
@@ -2,6 +2,13 @@
The following install instructions are for single-container installs of Mattermost using Docker for exploring product functionality and upgrading to newer versions.
+### One-line Docker Install ###
+
+If you have Docker set up, Mattermost installs in one-line:
+`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`
+
+Otherwise, see step-by-step available:
+
### Mac OSX ###
1. Install Docker Toolbox using instructions at: http://docs.docker.com/installation/mac/
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/model/webhook.go b/model/webhook.go
index 3bf034908..9b9969b96 100644
--- a/model/webhook.go
+++ b/model/webhook.go
@@ -8,6 +8,11 @@ import (
"io"
)
+const (
+ DEFAULT_WEBHOOK_USERNAME = "webhook"
+ DEFAULT_WEBHOOK_ICON = "/static/images/webhook_icon.jpg"
+)
+
type IncomingWebhook struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
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/create_post.jsx b/web/react/components/create_post.jsx
index 6203e567a..2581bdcca 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -16,6 +16,7 @@ const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
+const KeyCodes = Constants.KeyCodes;
export default class CreatePost extends React.Component {
constructor(props) {
@@ -35,6 +36,7 @@ export default class CreatePost extends React.Component {
this.removePreview = this.removePreview.bind(this);
this.onChange = this.onChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
+ this.handleArrowUp = this.handleArrowUp.bind(this);
PostStore.clearDraftUploads();
@@ -172,7 +174,7 @@ export default class CreatePost extends React.Component {
}
}
postMsgKeyPress(e) {
- if (e.which === 13 && !e.shiftKey && !e.altKey) {
+ if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
e.preventDefault();
ReactDOM.findDOMNode(this.refs.textbox).blur();
this.handleSubmit(e);
@@ -292,6 +294,27 @@ export default class CreatePost extends React.Component {
const draft = PostStore.getDraft(channelId);
return draft.previews.length + draft.uploadsInProgress.length;
}
+ handleArrowUp(e) {
+ if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
+ e.preventDefault();
+
+ const channelId = ChannelStore.getCurrentId();
+ const lastPost = PostStore.getCurrentUsersLatestPost(channelId);
+ if (!lastPost) {
+ return;
+ }
+ var type = (lastPost.root_id && lastPost.root_id.length > 0) ? 'Comment' : 'Post';
+
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.RECIEVED_EDIT_POST,
+ refocusId: '#post_textbox',
+ title: type,
+ message: lastPost.message,
+ postId: lastPost.id,
+ channelId: lastPost.channel_id
+ });
+ }
+ }
render() {
let serverError = null;
if (this.state.serverError) {
@@ -336,6 +359,7 @@ export default class CreatePost extends React.Component {
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.postMsgKeyPress}
+ onKeyDown={this.handleArrowUp}
onHeightChange={this.resizePostHolder}
messageText={this.state.messageText}
createMessage='Write a message...'
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index 3dbff18cb..90d9696e7 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -5,6 +5,7 @@ var Client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var Textbox = require('./textbox.jsx');
var BrowserStore = require('../stores/browser_store.jsx');
+var PostStore = require('../stores/post_store.jsx');
export default class EditPostModal extends React.Component {
constructor() {
@@ -14,6 +15,7 @@ export default class EditPostModal extends React.Component {
this.handleEditInput = this.handleEditInput.bind(this);
this.handleEditKeyPress = this.handleEditKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
+ this.handleEditPostEvent = this.handleEditPostEvent.bind(this);
this.state = {editText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''};
}
@@ -35,16 +37,15 @@ export default class EditPostModal extends React.Component {
Client.updatePost(updatedPost,
function success() {
- AsyncClient.getPosts(this.state.channel_id);
+ AsyncClient.getPosts(updatedPost.channel_id);
window.scrollTo(0, 0);
- }.bind(this),
+ },
function error(err) {
AsyncClient.dispatchError(err, 'updatePost');
}
);
$('#edit_post').modal('hide');
- $(this.state.refocusId).focus();
}
handleEditInput(editMessage) {
this.setState({editText: editMessage});
@@ -59,6 +60,18 @@ export default class EditPostModal extends React.Component {
handleUserInput(e) {
this.setState({editText: e.target.value});
}
+ handleEditPostEvent(options) {
+ this.setState({
+ editText: options.message || '',
+ title: options.title || '',
+ post_id: options.postId || '',
+ channel_id: options.channelId || '',
+ comments: options.comments || 0,
+ refocusId: options.refocusId || ''
+ });
+
+ $(React.findDOMNode(this.refs.modal)).modal('show');
+ }
componentDidMount() {
var self = this;
@@ -68,12 +81,29 @@ export default class EditPostModal extends React.Component {
$(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', function onShow(e) {
var button = e.relatedTarget;
+ if (!button) {
+ return;
+ }
self.setState({editText: $(button).attr('data-message'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments'), refocusId: $(button).attr('data-refoucsid')});
});
$(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', function onShown() {
self.refs.editbox.resize();
+ $('#edit_textbox').get(0).focus();
});
+
+ $(React.findDOMNode(this.refs.modal)).on('hide.bs.modal', function onShown() {
+ if (self.state.refocusId !== '') {
+ setTimeout(() => {
+ $(self.state.refocusId).get(0).focus();
+ });
+ }
+ });
+
+ PostStore.addEditPostListener(this.handleEditPostEvent);
+ }
+ componentWillUnmount() {
+ PostStore.removeEditPostListener(this.handleEditPostEvent);
}
render() {
var error = (<div className='form-group'><br /></div>);
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index 33a7bf26d..2b2cc2f95 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 5561c5b73..2c98db7f3 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/textbox.jsx b/web/react/components/textbox.jsx
index d51fb9523..86bb42f62 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -9,6 +9,7 @@ const ErrorStore = require('../stores/error_store.jsx');
const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
+const KeyCodes = Constants.KeyCodes;
export default class Textbox extends React.Component {
constructor(props) {
@@ -148,8 +149,10 @@ export default class Textbox extends React.Component {
this.doProcessMentions = true;
}
- if (e.keyCode === 8) {
+ if (e.keyCode === KeyCodes.BACKSPACE) {
this.handleBackspace(e);
+ } else if (this.props.onKeyDown) {
+ this.props.onKeyDown(e);
}
}
@@ -318,5 +321,6 @@ Textbox.propTypes = {
onUserInput: React.PropTypes.func.isRequired,
onKeyPress: React.PropTypes.func.isRequired,
onHeightChange: React.PropTypes.func,
- createMessage: React.PropTypes.string.isRequired
+ createMessage: React.PropTypes.string.isRequired,
+ onKeyDown: React.PropTypes.func
};
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 fbb003fd5..4296d7909 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.mm_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 d63bad35e..03e049db0 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -36,6 +36,7 @@ var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
var RegisterAppModal = require('../components/register_app_modal.jsx');
var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx');
+var AsyncClient = require('../utils/async_client.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
@@ -46,6 +47,8 @@ function setupChannelPage(props) {
id: props.ChannelId
});
+ AsyncClient.getAllPreferences();
+
// ChannelLoader must be rendered first
ReactDOM.render(
<ChannelLoader/>,
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index 07e8b461e..b073ca68d 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -6,6 +6,7 @@ var EventEmitter = require('events').EventEmitter;
var ChannelStore = require('../stores/channel_store.jsx');
var BrowserStore = require('../stores/browser_store.jsx');
+var UserStore = require('../stores/user_store.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
@@ -16,6 +17,7 @@ var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
var SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
var ADD_MENTION_EVENT = 'add_mention';
+var EDIT_POST_EVENT = 'edit_post';
class PostStoreClass extends EventEmitter {
constructor() {
@@ -75,6 +77,10 @@ class PostStoreClass extends EventEmitter {
this.clearCommentDraftUploads = this.clearCommentDraftUploads.bind(this);
this.storeLatestUpdate = this.storeLatestUpdate.bind(this);
this.getLatestUpdate = this.getLatestUpdate.bind(this);
+ this.emitEditPost = this.emitEditPost.bind(this);
+ this.addEditPostListener = this.addEditPostListener.bind(this);
+ this.removeEditPostListener = this.removeEditPostListener.bind(this);
+ this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this);
}
emitChange() {
this.emit(CHANGE_EVENT);
@@ -148,6 +154,18 @@ class PostStoreClass extends EventEmitter {
this.removeListener(ADD_MENTION_EVENT, callback);
}
+ emitEditPost(post) {
+ this.emit(EDIT_POST_EVENT, post);
+ }
+
+ addEditPostListener(callback) {
+ this.on(EDIT_POST_EVENT, callback);
+ }
+
+ removeEditPostListener(callback) {
+ this.removeListener(EDIT_POST_EVENT, callback);
+ }
+
getCurrentPosts() {
var currentId = ChannelStore.getCurrentId();
@@ -212,6 +230,22 @@ class PostStoreClass extends EventEmitter {
getPosts(channelId) {
return BrowserStore.getItem('posts_' + channelId);
}
+ getCurrentUsersLatestPost(channelId) {
+ const userId = UserStore.getCurrentId();
+ var postList = makePostListNonNull(this.getPosts(channelId));
+ var i = 0;
+ var len = postList.order.length;
+ var lastPost = null;
+
+ for (i; i < len; i++) {
+ if (postList.posts[postList.order[i]].user_id === userId) {
+ lastPost = postList.posts[postList.order[i]];
+ break;
+ }
+ }
+
+ return lastPost;
+ }
storePost(post) {
this.pStorePost(post);
this.emitChange();
@@ -446,6 +480,9 @@ PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
case ActionTypes.RECIEVED_ADD_MENTION:
PostStore.emitAddMention(action.id, action.username);
break;
+ case ActionTypes.RECIEVED_EDIT_POST:
+ PostStore.emitEditPost(action);
+ break;
default:
}
});
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 379c2fc19..fb7631159 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -639,16 +639,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;
@@ -660,8 +659,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 f1df2a786..3130e9277 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 a7b0b159b..d6da91fe2 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -17,6 +17,7 @@ module.exports = {
RECIEVED_POSTS: null,
RECIEVED_POST: null,
+ RECIEVED_EDIT_POST: null,
RECIEVED_SEARCH: null,
RECIEVED_POST_SELECTED: null,
RECIEVED_MENTION_DATA: null,
@@ -287,6 +288,16 @@ 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,
+ DOWN: 40,
+ LEFT: 37,
+ RIGHT: 39,
+ BACKSPACE: 8,
+ ENTER: 13,
+ ESCAPE: 27
}
};
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) {
diff --git a/web/sass-files/sass/partials/_post_right.scss b/web/sass-files/sass/partials/_post_right.scss
index 9557d7570..91f9355de 100644
--- a/web/sass-files/sass/partials/_post_right.scss
+++ b/web/sass-files/sass/partials/_post_right.scss
@@ -93,7 +93,8 @@
.post-right__scroll {
position: relative;
- overflow: auto;
+ overflow: scroll;
+ -webkit-overflow-scrolling: touch;
}
.post-right-comment-time {
diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss
index ca90ec46d..2f15a445f 100644
--- a/web/sass-files/sass/partials/_search.scss
+++ b/web/sass-files/sass/partials/_search.scss
@@ -67,8 +67,9 @@
}
.search-items-container {
- overflow: auto;
position: relative;
+ overflow: scroll;
+ -webkit-overflow-scrolling: touch;
}
.search-results-header {
diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss
index 831c19cff..585a51f08 100644
--- a/web/sass-files/sass/partials/_sidebar--left.scss
+++ b/web/sass-files/sass/partials/_sidebar--left.scss
@@ -47,7 +47,8 @@
.nav-pills__container {
height: 100%;
position: relative;
- overflow: auto;
+ overflow: scroll;
+ -webkit-overflow-scrolling: touch;
}
.nav-pills__unread-indicator {
diff --git a/web/static/images/webhook_icon.jpg b/web/static/images/webhook_icon.jpg
new file mode 100644
index 000000000..af5303421
--- /dev/null
+++ b/web/static/images/webhook_icon.jpg
Binary files differ
diff --git a/web/web.go b/web/web.go
index 1f7c3ae5e..83b8c7f34 100644
--- a/web/web.go
+++ b/web/web.go
@@ -1031,12 +1031,20 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
post := &model.Post{UserId: hook.UserId, ChannelId: channel.Id, Message: text}
post.AddProp("from_webhook", "true")
- if len(overrideUsername) != 0 && utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
- post.AddProp("override_username", overrideUsername)
+ if utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
+ if len(overrideUsername) != 0 {
+ post.AddProp("override_username", overrideUsername)
+ } else {
+ post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME)
+ }
}
- if len(overrideIconUrl) != 0 && utils.Cfg.ServiceSettings.EnablePostIconOverride {
- post.AddProp("override_icon_url", overrideIconUrl)
+ if utils.Cfg.ServiceSettings.EnablePostIconOverride {
+ if len(overrideIconUrl) != 0 {
+ post.AddProp("override_icon_url", overrideIconUrl)
+ } else {
+ post.AddProp("override_icon_url", model.DEFAULT_WEBHOOK_ICON)
+ }
}
if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN {